Guidelines

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/`).**

v1

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:

  1. 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.

  2. 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 in ready / claimed / blocked state on the canon repo (gyrum-labs/canon or gyrum-labs/gyrum-ui). The marker applies to every declaration up to the next blank line or class boundary.

  3. 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 overrides section.

: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