Canon Extension Discipline Guideline
Version: v1
Applies to: any consumer file importing a canon primitive (@gyrum-labs/svelte/* or anything under src/lib/components/canon/).
This guideline is the operational rule body that the upcoming structural canon-extension gate enforces. It is the consumer-tier expression of the css-is-canon-tier-work principle. PRs touching files in scope cite Guideline: canon-extension-discipline@v1 in the PR body so persona reviewers (ADR-115) verify against this exact version.
Rule
A consumer file that imports a canon primitive must satisfy ONE of the following for every CSS class declaration in its <style> block:
Token-only. The declaration sets ≤3 properties, every value is a
var(--*)token assignment, and none of the properties is a layout (display,grid-*,flex-*,gap,position,top/right/bottom/left,inset*), sizing (width,height,min-*,max-*,aspect-ratio), or transform (transform,translate,scale,rotate) property.Canon-extension marker. The declaration carries an immediately-preceding line-comment
/* canon-extension: warp#NNN */(or/* canon-extension: warp#NNN — <short reason> */) referencing an open sister ticket inready/claimed/blockedstate on the canon repo (gyrum-labs/canonorgyrum-labs/gyrum-ui). The marker applies to every declaration up to the next blank line or class boundary.Audited skip flag.
gyrum-review-pr --skip-canon-extension "<reason ≥ 30 chars>"was invoked, the reason was logged to~/.gyrum/admin-overrides.log, and the PR body carries the same reason in the## Audited overridessection.
:global(...) selectors, inline style="..." attributes, and style:property={value} directives are subject to the same rule — they are CSS authoring by any other name.
Why
Today's empirical evidence: V93-V99 (the canon Phase 1.x wave today) repeatedly hit a shape where the consumer-tier file passed every existing canon-adoption check (every value was a var(--*) token, the canon primitive was imported, the tier composition was clean) AND simultaneously declared 150+ lines of bespoke layout grammar that no other consumer could read. The local-instinct "I'll just add a wrapper class" produced ProjectCard.svelte's 162-line <style> block (warp#3672 NOT-YET-DONE table) — 16 class roots all token-clean, all redefining the canon primitive's internal layout grammar from outside.
The failure-mode rate-of-incidence is high because the local instinct is rational at consumer scale: "I have a deadline; the canon primitive doesn't quite fit; one wrapper class doesn't seem like canon-tier work." The instinct is correct at consumer scale and wrong at fleet scale — the cumulative effect across 8-12 consumers IS a parallel CSS system, and the rate of growth outpaces the canon-tier review bandwidth.
This guideline forces the consumer to make the choice EXPLICIT. Either (a) the declaration is genuinely token-only and the consumer's needs are met by canon as-is, or (b) the declaration carries a marker showing the consumer has filed-or-cited a sister ticket and the work is going to canon, or (c) the audited skip flag captures an operator-acknowledged exception. The skip-flag's logged reason is the audit trail; routine use is visible at the audit-log level, not invisible at the diff level.
Examples — passes
Token-only, single tone declaration:
.row-name {
color: var(--text);
}
Token-only, ≤3 properties, no layout:
.row-meta {
color: var(--text-muted);
font-size: var(--text-sm);
line-height: var(--line-height-tight);
}
Marker + open sister ticket:
/* canon-extension: warp#3699 — list-row needs configurable horizontal stack */
.row-stack {
display: flex;
gap: var(--space-2);
}
Audited skip — only when the class genuinely doesn't belong in canon (e.g. one-off operator-facing debug shim):
<!-- PR body section: ## Audited overrides
--skip-canon-extension "operator-only debug panel CSS; one-off
surface; never seen by end-users; lift to canon would pollute
primitives with debug-only patterns" -->
<style>
.debug-trace {
display: grid;
grid-template-columns: 1fr auto;
}
</style>
Examples — fails
Layout property, no marker:
/* FAILS — layout property (display: flex), no canon-extension marker */
.row-stack {
display: flex;
gap: var(--space-2);
}
Token-only count exceeded, no marker:
/* FAILS — four properties (max 3 for token-only path) */
.row-meta {
color: var(--text-muted);
font-size: var(--text-sm);
line-height: var(--line-height-tight);
letter-spacing: var(--tracking-wide);
}
Global-selector escape:
/* FAILS — :global() is CSS authoring by another name; same gate applies */
:global(.row-flag) {
display: inline-block;
padding: 0 var(--space-2);
}
Inline-style bypass:
<!-- FAILS — inline style declares layout from a consumer template -->
<GyListRow style="display: grid; gap: var(--space-2)">
Marker references a closed ticket:
/* FAILS — canon-extension marker references warp#1234 which is in 'done' state */
/* canon-extension: warp#1234 */
.row-stack {
display: flex;
}
Path forward when canon doesn't have what you need
Path A — in-PR (warp#3100)
When the author tier is at or above the consumer tier (e.g. consumer is a template and the missing primitive is a molecule or organism), author the canon primitive in the SAME PR. Register it in canon-manifest.yaml's owns: or lifts: block in the same diff. The reviewer sees the contract end-to-end; the canon + first consumer either both land or both don't. The PR body cites both warp#NNN (the consumer work) and the canon-side acceptance criteria.
This is the DEFAULT path per warp#3100. Atoms have many future consumers and deserve dedicated review (Path B); molecules/organisms/templates can ship with their first consumer.
Path B — sister ticket
When the missing primitive is an atom (no current canon-consumer expresses that shape), file a sister ticket. The current PR tags blocked-by:warp#<atom-ticket>. Add a /* canon-extension: warp#NNN */ marker over every affected CSS declaration so the canon-extension gate accepts the consumer-side PR while the atom is in flight.
When the atom ships, the closed-loop sweeper auto-files a retirement ticket on the consumer to clean up the marker + collapse the bespoke CSS into the canon adapter call.
Audited override
gyrum-review-pr --skip-canon-extension "<reason ≥ 30 chars>" — logged to ~/.gyrum/admin-overrides.log per the audit-on-override convention. Use ONLY when the class genuinely doesn't fit canon-tier (operator debug, one-off integration shim, dev-only preview route). The reason text MUST name (a) why this class doesn't belong in canon and (b) what makes it one-off rather than a parallel-system seed. Generic reasons ("temporary", "will fix later", "not in scope") are rejected by the operator audit.
The skip flag does NOT apply to :global(...) declarations or inline styles touching layout — those require a --skip-canon-extension-global flag with an even stricter reason length (≥60 chars) because global escapes leak across consumers.
PR body citation
Required line in the PR body when changing consumer files:
Guideline: canon-extension-discipline@v1
The persona reviewer (ADR-115) fetches this exact version and verifies the implementation against this text. Bumping the version is a separate PR through gyrum-revise-guideline canon-extension-discipline that includes a regression-corpus re-run.
Revision process
This document is versioned. Updates ride gyrum-revise-guideline canon-extension-discipline (separate devtools wrapper, separate ticket). Hand-edits without that wrapper skip the regression suite.
References
- css-is-canon-tier-work principle — the parent principle this guideline operationalises
- warp#3672 — this guideline's live ticket
- warp#3100 — canon-extension Path-A / Path-B decision rule
- warp#3036 — ticket-section structural enforcement (sister shape)
- warp#2278 — canon-tier substrate EPIC
- warp#2284 — Svelte canon primitives EPIC (ADR-182)
- ADR-115 — principle-aware reviewers
- ADR-117 — module guidelines
- ADR-182 — Svelte-native canon-tier
- canon site: https://canon.gyrum.ai/
- registry endpoint: https://canon.gyrum.ai/data/migrations.json