@erclx/canon 4.88.1 → 4.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.88.1",
4
+ "version": "4.90.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -28,6 +28,8 @@ This skill writes canonical docs at the end of a long build and never reviews wh
28
28
  - Write tracked docs at the current worktree root and the task board at the main root, since only the first commits with the branch
29
29
  - Count every other citation before archiving a plan, comparing resolved targets rather than raw strings or bare filenames
30
30
  - Retarget a closed task at the archived plan, so the reasoning behind finished work stays reachable
31
+ - Write a decision to the domain context entry it constrains unless it fills an architecture slot, since reach admits nearly every decision to an always-loaded file
32
+ - Merge or retire an architecture entry before adding one at the record's stated cap, and name which in the report, so the cap never turns into compressed prose or two decisions packed under one heading
31
33
  - Anchor a decision entry this run writes or amends whose reasoning cites a measured number, re-reading the number against the tree before writing the marker
32
34
  - Report an anchored decision whose cited path the diff touched, since the number was read before the branch moved what it counted
33
35
  - Scan every memory-review receipt rather than the one matching this slug, since the skill that writes them runs after this one in the ship chain and a slug is unique per feature
@@ -110,6 +110,8 @@ Read `ok` and `reason` out of that record rather than the exit. An operator's sh
110
110
  - Do not rewrite sections unrelated to what changed.
111
111
  - Rewrite a restated or superseded statement in place rather than appending the replacement beside it. State the fact that stands and keep the earlier reasoning only where it is the alternative that lost, per `${CLAUDE_SKILL_DIR}/../../standards/context.md` and `${CLAUDE_SKILL_DIR}/../../standards/architecture.md`.
112
112
  - Follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` and the `write-human` skill for all edits.
113
+ - Write a session decision into the `canon/context/` entry for the domain it constrains, under that entry's `## Decisions`, by default. Touch `canon/ARCHITECTURE.md` only for a decision that fills one of the slots `${CLAUDE_SKILL_DIR}/../../standards/architecture.md` names, however many domains its reasoning reaches.
114
+ - Read the entry cap the record states before adding a decision to it. At the cap, merge two decisions or retire one to the domain entry it constrains, and name which in the report. Never compress a decision's prose to fit, and never pack two decisions under one heading.
113
115
  - Close a decision entry in `canon/ARCHITECTURE.md` with its verification anchor whenever this run writes that entry or amends its reasoning and that reasoning cites a measured number. Re-read the number against the tree first, since the marker records the read rather than the edit. `${CLAUDE_SKILL_DIR}/../../standards/architecture.md` fixes the sentence.
114
116
  - Leave every decision entry this run did not write alone, anchored or not. The rule is scoped forward, so an entry written before it is dated by blame rather than by a read. Step 5 reports a stale anchor and no step writes one on an entry it did not amend.
115
117
 
@@ -269,6 +271,10 @@ Output one line per file updated:
269
271
 
270
272
  `✅ Updated: .claude/<filename>`
271
273
 
274
+ When Step 3 met the architecture record's cap, add one line naming what it did there:
275
+
276
+ `↪ Architecture at cap: merged <heading> into <heading>` or `↪ Architecture at cap: retired <heading> to <context entry>`
277
+
272
278
  Step 10 adds its own lines when it applied or reported a finding, in the exact shape `${CLAUDE_SKILL_DIR}/references/classify.md` gives them under its own Report section. Do not shorten or paraphrase those lines here or in the reply, since the quote and the reason are what a reader checks the finding against.
273
279
 
274
280
  If no files were updated and nothing was swept, output:
@@ -36,7 +36,7 @@ Each is invoked as the CLI the caller is running rather than as a global `canon`
36
36
 
37
37
  Five findings fail the run: an unresolved context citation, a banned character, word, or spelling, a relative link resolving to nothing on disk, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
38
38
 
39
- Four of the five are the ones `canon gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the architecture record already ranks content leaving the repository above content that stays.
39
+ Four of the five are the ones `canon gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the toolkit already ranks content leaving the repository above content that stays.
40
40
 
41
41
  Everything else reports. A heavy bullet, a long entry, a board row nothing resolves, a degradation term in a comment, and an implementation reaching history ahead of its test are judgments a reader settles. A push failing on one of those teaches contributors to route around the stage, which is the split recorded across every audit here and the one this command inherits rather than moves.
42
42
 
@@ -68,7 +68,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
68
68
  | `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
69
69
  | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
70
70
  | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
71
- | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record's word weight, and wireframe states against their evidence folders |
71
+ | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and entry cap and its word weight, and wireframe states against their evidence folders |
72
72
  | `canon context classify diff` | Classify the chunks a git range changed, each with its enclosing section, as keep, replace, history, or move (`--base`, `--doc-types`, `--json`) |
73
73
  | `canon context classify sweep` | Classify every section of the five canonical doc types, split at H3, as keep, rewrite, or move (`--doc-types`, `--json`) |
74
74
  | `canon context classifier show` | Report the resolved classifier backend and model and which source decided them (`--json`) |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Context audit checks
3
- description: What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length gate and claim coverage, which folders each check reaches, and what moved to the attribute tier
3
+ description: What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length and entry cap gates and claim coverage, which folders each check reaches, and what moved to the attribute tier
4
4
  ---
5
5
 
6
6
  # Context audit checks
@@ -91,7 +91,9 @@ The JSON record carries the findings per entry as `entries[].narration` and the
91
91
 
92
92
  ## The architecture record
93
93
 
94
- Three findings read `canon/ARCHITECTURE.md` rather than a folder, and only the first is a fact.
94
+ Four findings read `canon/ARCHITECTURE.md` rather than a folder, and only the first two are facts.
95
+
96
+ The entry cap check counts the record's decisions against the cap it states for itself, in a clause of the form `This record holds at most 12 decisions.` Like the length check, the cap belongs to the record rather than to the toolkit, so a record stating none is measured and never gated. A decision is a `###` heading outside a fenced block, so the template a standard shows does not count, and a heading carrying two decisions counts once. The JSON record carries what it read as `architecture.entryCap`, absent where the record states no cap, and the audit catalog counts a record past it as `recordOverCount`.
95
97
 
96
98
  The length check compares the record against the ceiling it derives for itself, and only a record that states its own allowances has one. No standard sets a length rule for this document, so the numbers belong to whichever record declares them. The check reads a frame allowance and an allowance per decision out of the record's own prose and puts the ceiling at the frame plus the allowance times the decision count. The JSON record carries what it read as `architecture.allowances` and the reading as `architecture.lines` against `architecture.ceiling`.
97
99
 
@@ -37,7 +37,9 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
37
37
 
38
38
  ## Exit codes
39
39
 
40
- Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`, and a record stating none is reported and never gated.
40
+ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`. A record that states an entry cap gates the same way when it holds more decisions than the cap, and a record stating neither limit is reported and never gated.
41
+
42
+ `bun run check` in the toolkit repository reads both limits through its own Architecture record stage rather than through this verb, since the verb's only gating stage there runs `--citations-only` and never opens the record.
41
43
 
42
44
  Entry length, reference form, table, provenance, narration, the record's claim classification, and every word figure print and return `0` under every mode, because each is a judgment or a weight read alongside one, and failing a push on either would make the check something to route around. Narration is the weakest of the printed measures, since whether two bullets share a subject is a call the measure approximates from structure alone, and one of the shapes it matches is the rejected alternative the standard asks an entry to keep.
43
45
 
@@ -13,7 +13,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
13
13
  - [Census](census.md): Tracked-plus-untracked file count, a breakdown by extension, and a line total that skips whatever reads as binary
14
14
  - [Command catalog](commands.md): Every project-level command and every domain subcommand, plus the shape each domain exposes
15
15
  - [Comments](comments.md): Comment density by language and kind, the two structural exclusions, and how the degradation sweep finds its vocabulary
16
- - [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length gate and claim coverage, which folders each check reaches, and what moved to the attribute tier
16
+ - [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length and entry cap gates and claim coverage, which folders each check reaches, and what moved to the attribute tier
17
17
  - [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
18
18
  - [Doc classifier](context-classify.md): Classifying canonical-doc content as keep, replace/rewrite, history, or move in diff and sweep mode, the classifier setting pair, the record shape, and the exit codes
19
19
  - [Self-stated counts](counts.md): Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
@@ -48,7 +48,7 @@ Most carry two spellings, the one a surface authors at and the one it reaches a
48
48
 
49
49
  `CLAUDE.md` is matched as a whole path rather than as a prefix, because a prefix reaches nothing that sits in no folder. A nested `docs/CLAUDE.md` stays informational.
50
50
 
51
- The set is data at `src/autoship/paths.ts`. That makes it permanently exempt from any later design that folds a machine-parsed list back into the surface citing it, per the machine-parsed clause in `canon/ARCHITECTURE.md`.
51
+ The set is data at `src/autoship/paths.ts`. That makes it permanently exempt from any later design that folds a machine-parsed list back into the surface citing it, per the toolkit's machine-parsed clause: a rule restating the list a parser reads is two sources for one list.
52
52
 
53
53
  ## Exit codes and refusals
54
54
 
@@ -5,7 +5,7 @@ description: Reading per CLAUDE.md section how many bullets name a path, what co
5
5
 
6
6
  # Routing report
7
7
 
8
- `canon claude routing` reports, per `CLAUDE.md` section, how many top-level bullets name a path and how many of those a path-scoped rule already covers. It answers the firing axis of the tier test in `.claude/rules/canon/claude/592-claude-md.md`, whether a fact applies every session or fires on one path, and which until this verb existed was a judgment nothing counted. `canon/ARCHITECTURE.md` names two further axes, conditional presence and updatability, that this verb does not check.
8
+ `canon claude routing` reports, per `CLAUDE.md` section, how many top-level bullets name a path and how many of those a path-scoped rule already covers. It answers the firing axis of the tier test in `.claude/rules/canon/claude/592-claude-md.md`, whether a fact applies every session or fires on one path, and which until this verb existed was a judgment nothing counted. The tier test names two further axes, conditional presence and updatability, that this verb does not check.
9
9
 
10
10
  ```bash
11
11
  canon claude routing
@@ -32,7 +32,7 @@ Three root files are read whether or not the field names them, being `package.js
32
32
 
33
33
  The corpus answers what the package publishes, which is narrower than what the repository holds. Measured against this repository on 2026-08-21, the scan read 544 files and left 593 of the 1139 git lists unread. Those include everything under `.claude/`, `wiki/`, and `internal/`, the workflow definitions under `.github/`, and the trees the publish negations remove, being `scripts/sandbox/`, `scripts/eval/`, and every test file. `src/capture/` was a fourth negation when that reading was taken and is not one now. The folder holds 5 tracked files, 2 of them tests the publish still excludes, so the corpus is 3 wider than the numbers above describe and the unread count is 3 smaller.
34
34
 
35
- A public repository makes that gap readable by anyone, so a clean run means no credential in the published tree rather than none in the repository. The run states the number on every pass, including a clean one, so the bound travels with the verdict. Widening the corpus to every tracked file is a separate decision, since the row this implements puts the shipped tree first on the record's rule that content leaving the repository gates harder than content that stays.
35
+ A public repository makes that gap readable by anyone, so a clean run means no credential in the published tree rather than none in the repository. The run states the number on every pass, including a clean one, so the bound travels with the verdict. Widening the corpus to every tracked file is a separate decision, since the row this implements puts the shipped tree first on the toolkit's rule that content leaving the repository gates harder than content that stays.
36
36
 
37
37
  ### What it keys on
38
38
 
@@ -7,6 +7,8 @@ description: Listing learning workspaces with what their records schedule next,
7
7
 
8
8
  Learning workspaces sit under `.canon/teach/<nn>-<topic>/`, and `standards/teach.md` fixes their layout, naming, and file formats, apart from the glossary, whose shape `standards/glossary.md` fixes, cited from the `teach-workspace` skill, so it travels with the file wherever a promotion lands it. Every verb here resolves that folder against the main worktree root rather than against the working directory, so a session standing in a linked worktree reaches the one workspace the learner has rather than opening a second.
9
9
 
10
+ Passing `--root` a path already named `teach` reaches that folder directly rather than wrapping it in the `.canon/`-or-`.claude/` record-root lookup, which is what lets a workspace committed outside the record root, such as a worked example kept in version control on purpose, take every verb below the same as one under `.canon/teach/`.
11
+
10
12
  That root resolution is also why the writing verbs exist at all. The file-editing tools refuse a main-root path from a linked worktree and offer a worktree copy instead, and a caller naming only the destination reports a success that did not happen. A whole-file create still goes out as a shell heredoc. Changing a line inside a file that already exists has no shell route, because the stream editors are banned, so `resource` and `glossary` are the route for the two files a running workspace edits.
11
13
 
12
14
  ## List
@@ -10,3 +10,4 @@ paths:
10
10
  ## Authority
11
11
 
12
12
  - Follow the architecture standard for the overview, named decision entries, and risks. It is the single source. Read it with `canon standards architecture`.
13
+ - Add a decision here only when it fills one of the standard's slots, and write any other decision into the context entry for the domain it constrains. At the record's stated cap, merge or retire an entry before adding one.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.88.1",
4
+ "version": "4.90.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -168,6 +168,8 @@ function architectureCounts(
168
168
  // zero there would read as one measured and found conforming.
169
169
  const ceiling =
170
170
  typeof record.ceiling === 'number' ? record.ceiling : undefined
171
+ const entryCap =
172
+ typeof record.entryCap === 'number' ? record.entryCap : undefined
171
173
 
172
174
  let unverifiable = 0
173
175
  let unchecked = 0
@@ -188,6 +190,10 @@ function architectureCounts(
188
190
  ...(ceiling !== undefined && {
189
191
  recordOverLength: record.lines > ceiling ? 1 : 0,
190
192
  }),
193
+ // Absent on a record stating no entry cap, for the same reason.
194
+ ...(entryCap !== undefined && {
195
+ recordOverCount: decisions.length > entryCap ? 1 : 0,
196
+ }),
191
197
  recordUnverifiable: unverifiable,
192
198
  recordUnchecked: unchecked,
193
199
  }
@@ -7,7 +7,7 @@
7
7
  * application three times, so the set moved here and the body now calls a verb
8
8
  * that reads it. Being machine-parsed makes it permanently exempt from any
9
9
  * later design that folds a standard back into the surface citing it, per the
10
- * machine-parsed clause in `canon/ARCHITECTURE.md`.
10
+ * machine-parsed clause in `canon/context/standards/resolution.md`.
11
11
  */
12
12
 
13
13
  /**
@@ -20,6 +20,7 @@ import {
20
20
  architectureRel,
21
21
  type ArchitectureReport,
22
22
  coveredCount,
23
+ isOverCount,
23
24
  isOverLength,
24
25
  measureArchitecture,
25
26
  testableCount,
@@ -98,7 +99,7 @@ export function register(program: Command): void {
98
99
  context
99
100
  .command('audit')
100
101
  .description(
101
- 'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and its word weight, and wireframe states against their evidence folders',
102
+ 'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
102
103
  )
103
104
  .argument('[path]', 'Project root, defaulting to the current directory')
104
105
  .helpOption('-h, --help', 'Show this help message')
@@ -124,9 +125,11 @@ export function register(program: Command): void {
124
125
  'An unresolved citation always gates. An architecture record that',
125
126
  'states its own line allowances gates when it is past the ceiling',
126
127
  'those derive, on any run except --citations-only, which never',
127
- 'measures it. A record stating no allowance is reported and never',
128
- 'gated. --gate widens the gate to the other two findings that are',
129
- 'facts rather than judgments: a missing required section and index',
128
+ 'measures it. A record stating an entry cap gates the same way when',
129
+ 'it holds more decisions than the cap. A record stating neither is',
130
+ 'reported and never gated. --gate widens the gate to the other two',
131
+ 'findings that are facts rather than judgments: a missing required',
132
+ 'section and index',
130
133
  'drift. Entry length, reference form, table, provenance, narration,',
131
134
  'and the record claim classification are judgments under both.',
132
135
  '',
@@ -759,6 +762,7 @@ async function runAudit(
759
762
  const gating = isGating({
760
763
  unresolvedCitations: citations.unresolved.length,
761
764
  recordOverLength: record !== undefined && isOverLength(record),
765
+ recordOverCount: record !== undefined && isOverCount(record),
762
766
  sections,
763
767
  drift,
764
768
  wireframes,
@@ -1256,6 +1260,20 @@ function reportRecord(
1256
1260
  )
1257
1261
  }
1258
1262
 
1263
+ if (report.entryCap === undefined) {
1264
+ logInfo(
1265
+ `${plural(decisions, 'decision')} and no entry cap stated, so the count is reported and never gated.`,
1266
+ )
1267
+ } else if (isOverCount(report)) {
1268
+ logError(
1269
+ `${plural(decisions, 'decision')} against a cap of ${report.entryCap}. Merge two or retire one, never compress.`,
1270
+ )
1271
+ } else {
1272
+ logInfo(
1273
+ `${plural(decisions, 'decision')} against a cap of ${report.entryCap}.`,
1274
+ )
1275
+ }
1276
+
1259
1277
  if (decisions === 0) {
1260
1278
  logWarn('The record declares no decision, so nothing was classified.')
1261
1279
  return
@@ -80,6 +80,12 @@ export interface ArchitectureReport {
80
80
  readonly allowances?: Allowances
81
81
  /** The frame plus the per-decision allowance, absent alongside it. */
82
82
  readonly ceiling?: number
83
+ /**
84
+ * The most decisions the record says it holds, absent when it states no
85
+ * cap. Declared by the record for the reason the allowances are, so a
86
+ * project that never adopted a cap is measured and never gated.
87
+ */
88
+ readonly entryCap?: number
83
89
  readonly decisions: readonly DecisionReport[]
84
90
  }
85
91
 
@@ -314,14 +320,32 @@ const SPELLED: Record<string, number> = {
314
320
  eight: 8,
315
321
  nine: 9,
316
322
  ten: 10,
323
+ eleven: 11,
324
+ twelve: 12,
325
+ thirteen: 13,
326
+ fourteen: 14,
327
+ fifteen: 15,
328
+ sixteen: 16,
329
+ seventeen: 17,
330
+ eighteen: 18,
331
+ nineteen: 19,
332
+ twenty: 20,
317
333
  }
318
334
 
319
- const CARDINAL = String.raw`(\d+|${Object.keys(SPELLED).join('|')})`
335
+ // Longest first, so `fourteen` is tried ahead of the `four` it starts with.
336
+ const SPELLED_ALTERNATION = Object.keys(SPELLED)
337
+ .sort((a, b) => b.length - a.length)
338
+ .join('|')
339
+ const CARDINAL = String.raw`(\d+|${SPELLED_ALTERNATION})`
320
340
  const FRAME_CLAUSE = new RegExp(String.raw`${CARDINAL}-line frame`, 'i')
321
341
  const PER_DECISION_CLAUSE = new RegExp(
322
342
  String.raw`${CARDINAL}\s+lines?\s+a\s+decision`,
323
343
  'i',
324
344
  )
345
+ const ENTRY_CAP_CLAUSE = new RegExp(
346
+ String.raw`\bat\s+most\s+${CARDINAL}\s+decisions?\b`,
347
+ 'i',
348
+ )
325
349
 
326
350
  function readCardinal(token: string | undefined): number | undefined {
327
351
  if (token === undefined) return undefined
@@ -348,6 +372,17 @@ export function readAllowances(source: string): Allowances | undefined {
348
372
  return { frame, perDecision }
349
373
  }
350
374
 
375
+ /**
376
+ * Reads the entry cap a record declares for itself, or nothing.
377
+ *
378
+ * The clause is the record's own, like the allowances above, so a record
379
+ * whose wording drifts past it falls back to reporting rather than to a cap
380
+ * held here that no project wrote.
381
+ */
382
+ export function readEntryCap(source: string): number | undefined {
383
+ return readCardinal(source.match(ENTRY_CAP_CLAUSE)?.[1])
384
+ }
385
+
351
386
  /** Whether a read failed because nothing sits at the path. */
352
387
  function isMissing(error: unknown): boolean {
353
388
  const code = (error as { code?: unknown }).code
@@ -385,6 +420,7 @@ export async function measureArchitecture(
385
420
  }
386
421
 
387
422
  const allowances = readAllowances(source)
423
+ const entryCap = readEntryCap(source)
388
424
  const raw = splitDecisions(source)
389
425
  const decisions = await Promise.all(
390
426
  raw.map(async (entry) => {
@@ -416,10 +452,24 @@ export async function measureArchitecture(
416
452
  allowances,
417
453
  ceiling: ceilingFor(allowances, raw.length),
418
454
  }),
455
+ ...(entryCap !== undefined && { entryCap }),
419
456
  decisions,
420
457
  }
421
458
  }
422
459
 
460
+ /**
461
+ * Whether the record holds more decisions than the cap it states.
462
+ *
463
+ * False for a record stating no cap, for the reason `isOverLength` gives. The
464
+ * count is by heading, so a heading carrying several decisions counts once,
465
+ * which is the undercount `splitDecisions` already names.
466
+ */
467
+ export function isOverCount(report: ArchitectureReport): boolean {
468
+ return (
469
+ report.entryCap !== undefined && report.decisions.length > report.entryCap
470
+ )
471
+ }
472
+
423
473
  /**
424
474
  * Whether the record is longer than the ceiling it derives for itself.
425
475
  *
@@ -14,6 +14,11 @@ export interface GateInput {
14
14
  * construction, so widening it here would gate on a reading it never took.
15
15
  */
16
16
  readonly recordOverLength: boolean
17
+ /**
18
+ * Whether the record holds more decisions than the cap it states, which
19
+ * gates under the same two modes and is false in the same two cases.
20
+ */
21
+ readonly recordOverCount: boolean
17
22
  readonly sections: readonly SectionFinding[]
18
23
  readonly drift: readonly FolderDrift[]
19
24
  /**
@@ -68,9 +73,9 @@ export function hasSketchWithEvidence(
68
73
  * Whether the audit found something that should fail the caller.
69
74
  *
70
75
  * An unresolved citation is a broken pointer and gates unconditionally, and so
71
- * does a record past its own ceiling: the record states the limit for itself
72
- * and derives it from a count, which makes it the one measure here that is a
73
- * fact rather than a threshold a reader weighs. The findings `--gate` adds are
76
+ * does a record past its own ceiling or its own entry cap: the record states
77
+ * each limit for itself, which makes those the measures here that are facts
78
+ * rather than thresholds a reader weighs. The findings `--gate` adds are
74
79
  * the ones answerable from the file itself: a required section it does not
75
80
  * declare, an index disagreeing with its folder, and a wireframe's States
76
81
  * table disagreeing with its evidence folders. Entry length, depth, bullet,
@@ -82,13 +87,14 @@ export function hasSketchWithEvidence(
82
87
  export function isGating({
83
88
  unresolvedCitations,
84
89
  recordOverLength,
90
+ recordOverCount,
85
91
  sections,
86
92
  drift,
87
93
  wireframes,
88
94
  widened,
89
95
  }: GateInput): boolean {
90
96
  if (unresolvedCitations > 0) return true
91
- if (recordOverLength) return true
97
+ if (recordOverLength || recordOverCount) return true
92
98
  if (!widened) return false
93
99
 
94
100
  return sections.length > 0 || hasDrift(drift) || hasStatesMismatch(wireframes)
@@ -4,26 +4,26 @@
4
4
  file changes freely and nothing a project wrote lives in it. */
5
5
 
6
6
  :root {
7
- --color-background: #191512;
8
- --color-surface: #211c19;
9
- --color-chrome: #241e1a;
10
- --color-border: #2f2823;
11
- --color-text: #f4efe9;
12
- --color-text-body: #c9c0b7;
13
- --color-text-secondary: #a79d94;
14
- --color-muted: #948a81;
15
- --color-accent: #e0724b;
7
+ --color-background: #0f0e0c;
8
+ --color-surface: #151412;
9
+ --color-chrome: #1b1a18;
10
+ --color-border: #2a2926;
11
+ --color-text: #d9d7d4;
12
+ --color-text-body: #aeada9;
13
+ --color-text-secondary: #8c8b86;
14
+ --color-muted: #7f7f7c;
15
+ --color-accent: #c76b5f;
16
16
  --color-success: #61c454;
17
- --color-light-background: #faf7f2;
18
- --color-light-surface: #f4efe6;
19
- --color-light-chrome: #ede4d6;
20
- --color-light-text: #1a1815;
21
- --color-light-text-body: #3d3630;
22
- --color-light-text-secondary: #5c544b;
23
- --color-light-muted: #726b62;
24
- --color-light-accent: #a4471c;
17
+ --color-light-background: #fbfaf8;
18
+ --color-light-surface: #f1f1ee;
19
+ --color-light-chrome: #e9e8e5;
20
+ --color-light-text: #2c2c29;
21
+ --color-light-text-body: #4b4947;
22
+ --color-light-text-secondary: #666561;
23
+ --color-light-muted: #6e6d6c;
24
+ --color-light-accent: #ad4a4b;
25
25
  --color-light-success: #2d6b22;
26
- --color-light-border: #e4dcd0;
26
+ --color-light-border: #d5d4d1;
27
27
  --space-xs: 6px;
28
28
  --space-sm: 12px;
29
29
  --space-md: 18px;
@@ -94,7 +94,7 @@ export const TOKENS: DesignTokens = {
94
94
  '',
95
95
  'The two rendered captures read it as well. `scripts/core/regen-hero.sh` fills `assets/captures/hero.html.tmpl` and `assets/captures/install.html.tmpl` with what `canon design css --no-components` emits, so both frames now carry the custom properties rather than their own copies of the hex, and a value moved here moves what the next capture renders.',
96
96
  '',
97
- 'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/ARCHITECTURE.md` records one color source per language with a check behind each, so generating a third spelling from here would break the rule those two checks enforce. What the record is still incomplete about is the other half of those six: `WHITE` and `GREY` name no role below, so the terminal palette is described here in part rather than in whole.',
97
+ 'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/context/scripts/framing.md` records one color source per language with a check behind each, so generating a third spelling from here would break the rule those two checks enforce. What the record is still incomplete about is the other half of those six: `WHITE` and `GREY` name no role below, so the terminal palette is described here in part rather than in whole.',
98
98
  ].join('\n'),
99
99
 
100
100
  personality: [
@@ -108,58 +108,64 @@ export const TOKENS: DesignTokens = {
108
108
  '',
109
109
  'Warning and error hold ANSI codes because that is what `scripts/lib/ui.sh` writes and no rendered surface implements an equivalent. Giving either a hex value would invent a mapping no file has, so they carry no contrast reading either.',
110
110
  '',
111
- 'Success is the one of the three that does have a rendered equivalent, which is why it carries a hex. `assets/captures/install.html` marks every confirmed step with it, and the shell writes `ANSI 32` for the same role, so the two are one role in two registers rather than one value in two spellings. The hex is what the rendered surface picked and no reading claims the terminal renders that value. It declares `background` alone as its ground, since that is the only role it is drawn on, where every other dark text role is drawn on both.',
111
+ 'Success is the one of the three that does have a rendered equivalent, which is why it carries a hex. `assets/captures/install.html` marks every confirmed step with it, and the shell writes `ANSI 32` for the same role, so the two are one role in two registers rather than one value in two spellings. The hex is what the rendered surface picked and no reading claims the terminal renders that value. It declares `background` alone as its ground, since that is the only role it is drawn on, where every other dark text role is drawn on both. It is the one role below that is not derived.',
112
+ '',
113
+ "Every other role is derived rather than picked, solved in OKLCH by binary search for the lightness that hits a target contrast against its ground, using this module's own anchors. Six anchors are the whole system: ground lightness 0.985 light and 0.165 dark, neutral chroma 0.003 light and 0.004 dark, neutral hue 90 both, accent hue 22 light and 28 dark, accent chroma 0.13 light and 0.12 dark for a mark and 0.095 light and 0.09 dark for a fill. Only the mark step is rendered below, in `accent`. That token already stands in for a fill in practice: `assets/captures/hero.html`'s `.cmd` rule paints its whole background with `var(--color-accent)` and sets text on top, which is a fill use rather than a mark one. The fill chroma step has no token of its own yet, so a lower-saturation `accent-fill` would recolor that button rather than introduce a new consumer. Targets, which are inputs rather than results: text 13.5, body 8.6, secondary 5.6, muted 4.6, accent 5.2. Dark is its own anchor set rather than an inversion of light, and every arm measured in the groundwork behind this landed on identical neutral ratios, which is what made the accent choice a question about hue alone.",
114
+ '',
115
+ '`muted` and `light-muted` solve against `surface` rather than `background`. Both declare two grounds and the groundwork solved its target against one, so the recorded hex cleared 4.6 against `background` and read 4.40 dark, 4.24 light against `surface`, the tighter of the two since surface sits a step closer to its text color. The values here are re-solved for the same 4.6 target read against `surface` instead, which clears both: 4.58 dark, 4.56 light against `surface`, and 4.80 dark, 4.95 light against `background`.',
116
+ '',
117
+ 'The accent is a quiet red at hue 22 light, 28 dark, chosen over a vivid red that read as an error state against a page reporting success, over indigo which carries less distinctiveness at hue 280 in tooling already dominated by that hue, and over rust at hue 42, a larger temperature shift than the problem required. The debt this accepts: `error` and `warning` now share a register with the most repeated element on every rendered surface, so both need differentiating by lightness or by an icon rather than by hue, and that work is unscheduled.',
112
118
  ].join('\n'),
113
119
 
114
120
  color: [
115
121
  {
116
122
  role: 'background',
117
123
  intent: 'page canvas',
118
- value: '#191512',
124
+ value: '#0f0e0c',
119
125
  },
120
126
  {
121
127
  role: 'surface',
122
128
  intent: 'cards, panels, raised blocks',
123
- value: '#211c19',
129
+ value: '#151412',
124
130
  },
125
131
  {
126
132
  role: 'chrome',
127
133
  intent: 'the window titlebar, one step above the canvas',
128
- value: '#241e1a',
134
+ value: '#1b1a18',
129
135
  },
130
136
  {
131
137
  role: 'border',
132
138
  intent: 'every rule and panel edge',
133
- value: '#2f2823',
139
+ value: '#2a2926',
134
140
  },
135
141
  {
136
142
  role: 'text',
137
143
  intent: 'headings, counts, emphasized runs',
138
- value: '#f4efe9',
144
+ value: '#d9d7d4',
139
145
  grounds: DARK_GROUNDS,
140
146
  },
141
147
  {
142
148
  role: 'text-body',
143
149
  intent: 'default body copy',
144
- value: '#c9c0b7',
150
+ value: '#aeada9',
145
151
  grounds: DARK_GROUNDS,
146
152
  },
147
153
  {
148
154
  role: 'text-secondary',
149
155
  intent: 'labels, captions, supporting copy',
150
- value: '#a79d94',
156
+ value: '#8c8b86',
151
157
  grounds: DARK_GROUNDS,
152
158
  },
153
159
  {
154
160
  role: 'muted',
155
161
  intent: 'the faintest step, trailing notes',
156
- value: '#948a81',
162
+ value: '#7f7f7c',
157
163
  grounds: DARK_GROUNDS,
158
164
  },
159
165
  {
160
166
  role: 'accent',
161
167
  intent: 'install command, mark, primary action',
162
- value: '#e0724b',
168
+ value: '#c76b5f',
163
169
  grounds: DARK_GROUNDS,
164
170
  },
165
171
  {
@@ -173,46 +179,46 @@ export const TOKENS: DesignTokens = {
173
179
  {
174
180
  role: 'light-background',
175
181
  intent: 'page canvas on a light ground',
176
- value: '#faf7f2',
182
+ value: '#fbfaf8',
177
183
  },
178
184
  {
179
185
  role: 'light-surface',
180
186
  intent: 'cards and panels on a light ground',
181
- value: '#f4efe6',
187
+ value: '#f1f1ee',
182
188
  },
183
189
  {
184
190
  role: 'light-chrome',
185
191
  intent: 'the window titlebar, one step above the canvas',
186
- value: '#ede4d6',
192
+ value: '#e9e8e5',
187
193
  },
188
194
  {
189
195
  role: 'light-text',
190
196
  intent: 'primary text on a light ground',
191
- value: '#1a1815',
197
+ value: '#2c2c29',
192
198
  grounds: LIGHT_GROUNDS,
193
199
  },
194
200
  {
195
201
  role: 'light-text-body',
196
202
  intent: 'default body copy on a light ground',
197
- value: '#3d3630',
203
+ value: '#4b4947',
198
204
  grounds: LIGHT_GROUNDS,
199
205
  },
200
206
  {
201
207
  role: 'light-text-secondary',
202
208
  intent: 'labels, captions, supporting copy on a light ground',
203
- value: '#5c544b',
209
+ value: '#666561',
204
210
  grounds: LIGHT_GROUNDS,
205
211
  },
206
212
  {
207
213
  role: 'light-muted',
208
214
  intent: 'secondary text on a light ground',
209
- value: '#726b62',
215
+ value: '#6e6d6c',
210
216
  grounds: LIGHT_GROUNDS,
211
217
  },
212
218
  {
213
219
  role: 'light-accent',
214
220
  intent: 'links and primary action on light',
215
- value: '#a4471c',
221
+ value: '#ad4a4b',
216
222
  grounds: LIGHT_GROUNDS,
217
223
  },
218
224
  {
@@ -224,7 +230,7 @@ export const TOKENS: DesignTokens = {
224
230
  {
225
231
  role: 'light-border',
226
232
  intent: 'rules and panel edges on light',
227
- value: '#e4dcd0',
233
+ value: '#d5d4d1',
228
234
  verify: true,
229
235
  },
230
236
  ],
@@ -7,6 +7,11 @@ import {
7
7
  type ClientCommand,
8
8
  clientCommandCitationsIn,
9
9
  } from '@/client-commands'
10
+ import {
11
+ isOverCount,
12
+ isOverLength,
13
+ measureArchitecture,
14
+ } from '@/context/architecture'
10
15
  import { listRepositoryFiles } from '@/git-files'
11
16
  import {
12
17
  isShippedCorpus,
@@ -222,6 +227,41 @@ function citedPaths(record: { paths?: unknown } | undefined): string[] {
222
227
  })
223
228
  }
224
229
 
230
+ /**
231
+ * The architecture record against the two limits it states for itself: the
232
+ * line ceiling its allowances derive and the entry cap.
233
+ *
234
+ * Read in-process rather than through `context audit`, whose one gating stage
235
+ * here runs `--citations-only` and never opens the record, which is why the
236
+ * line ceiling went unenforced by `bun run check` until this stage. A project
237
+ * carrying no record, or a record stating neither limit, passes, since both
238
+ * limits belong to the record rather than to the toolkit.
239
+ */
240
+ export const architectureRecord: Measure = async (ctx) => {
241
+ const report = await measureArchitecture(ctx.root)
242
+ if (report === undefined) {
243
+ return { emissions: [info('No architecture record to measure')] }
244
+ }
245
+
246
+ const decisions = report.decisions.length
247
+ const failures = [
248
+ isOverCount(report) &&
249
+ `${decisions} decisions against a cap of ${report.entryCap}. Merge two or retire one in ${report.rel}, never compress.`,
250
+ isOverLength(report) &&
251
+ `${report.lines} lines against a ceiling of ${report.ceiling} in ${report.rel}.`,
252
+ ].filter((failure): failure is string => typeof failure === 'string')
253
+
254
+ if (failures.length > 0) return { emissions: [], failure: failures.join(' ') }
255
+
256
+ const cap =
257
+ report.entryCap === undefined
258
+ ? 'no entry cap stated'
259
+ : `a cap of ${report.entryCap}`
260
+ return {
261
+ emissions: [info(`${decisions} decisions against ${cap}`)],
262
+ }
263
+ }
264
+
225
265
  /**
226
266
  * A second run of the records move should rewrite nothing, and the count is
227
267
  * only knowable once the folders themselves have landed.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ architectureRecord,
2
3
  auditSet,
3
4
  captureStamps,
4
5
  clientCommandCitations,
@@ -326,7 +327,7 @@ export const STAGES: readonly Stage[] = [
326
327
  {
327
328
  // A hook, a workflow, or a husky script reaching one side of the seed or
328
329
  // config boundary and not the other is a capability withheld with no
329
- // recorded reason, per the criterion in .claude/ARCHITECTURE.md.
330
+ // recorded reason, per the criterion in canon/context/tooling.md.
330
331
  id: 'capability-seeding',
331
332
  label: 'Capability seeding',
332
333
  checks: [
@@ -362,6 +363,13 @@ export const STAGES: readonly Stage[] = [
362
363
  ],
363
364
  success: 'Context citations resolve',
364
365
  },
366
+ {
367
+ // The record's two limits are facts it states about itself, unlike the
368
+ // judgment thresholds the stage above leaves out, so they gate here.
369
+ id: 'architecture-record',
370
+ label: 'Architecture record',
371
+ checks: [{ kind: 'measure', measure: architectureRecord }],
372
+ },
365
373
  {
366
374
  // A rule citing a file that moved fails silently. The consumed-copy drift
367
375
  // stage passes an authored rule and its copy that are wrong together, and
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
3
- import { join, relative } from 'node:path'
3
+ import { basename, join, relative } from 'node:path'
4
4
  import { TEACH_STYLESHEET_COMPONENTS } from '@/design/components'
5
5
  import { buildDesignCss } from '@/design/css'
6
6
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
@@ -230,9 +230,14 @@ export function refuse(
230
230
  * Every workspace sits under the main worktree root rather than under the
231
231
  * checkout the caller stands in. Resolving that root belongs to the caller, so
232
232
  * this takes one and never reads the working directory.
233
+ *
234
+ * A root already named `teach` is taken as the teach folder itself rather than
235
+ * a project root to wrap, which is what lets a caller point straight at a
236
+ * folder such as `examples/teach` that holds workspaces outside any `.canon/`
237
+ * or `.claude/` record root.
233
238
  */
234
239
  export function teachDir(root: string): string {
235
- return recordDir(root, 'teach')
240
+ return basename(root) === 'teach' ? root : recordDir(root, 'teach')
236
241
  }
237
242
 
238
243
  async function listSlugs(dir: string): Promise<string[]> {
@@ -5,7 +5,7 @@ description: Shape and content rules for canon/ARCHITECTURE.md
5
5
 
6
6
  # Architecture reference
7
7
 
8
- Applies to `canon/ARCHITECTURE.md`. Describes the system shape and the decisions behind it, not a tutorial, setup guide, or implementation walkthrough. Pair it with `CLAUDE.md`: principles live there, patterns and decisions live here. Update when a decision is made or a risk is resolved.
8
+ Applies to `canon/ARCHITECTURE.md`. Describes the system shape and the decisions behind it, not a tutorial, setup guide, or implementation walkthrough. Pair it with `CLAUDE.md`: session behavior lives there, and the system's shape lives here. Update when a decision is made or a risk is resolved.
9
9
 
10
10
  ## Scope
11
11
 
@@ -20,15 +20,26 @@ Does not govern:
20
20
  ## What goes in
21
21
 
22
22
  - A high-level overview of how the system is structured and why
23
- - Key technical decisions as named H3 entries: what was chosen and why over the alternatives, including stack and library choices
23
+ - Key technical decisions as named H3 entries, each filling one of the five slots below: what was chosen and why over the alternatives
24
24
  - Risks and open questions still unresolved
25
25
 
26
+ A decision belongs here only when it fills a slot:
27
+
28
+ - Stack and runtime: the language, the runtime, and the major libraries the system is built on
29
+ - Delivery: how the system's content or code reaches the place it runs
30
+ - Enforced boundaries: a boundary the system checks mechanically, and what enforces it
31
+ - Layout: how the project's top-level roots and context tiers divide what they hold
32
+ - Build principles: a rule for how a behavior is built that changes work in more than one domain, stated in one sentence
33
+
34
+ The slots come from a scale test. Ask whether the decision would still be in this file if the project were ten times its size. At that size an always-loaded file holding one entry per decision fits no context window, and what survives is the shape: the stack, the delivery, the boundaries, the layout, and the principles. A decision that fits no slot is a domain decision, and its home is that domain's `canon/context/<domain>.md` entry under `## Decisions`, however many domains its reasoning touches. Reach is not the test, since nearly every decision reaches a second domain.
35
+
26
36
  ## What does not go in
27
37
 
28
38
  - How individual functions work line by line. The code carries its own behavior.
29
39
  - Full type definitions. They live in code. Reference the shape conceptually if needed.
30
- - A measurement paragraph specific to one domain's own mechanism. Route it to that domain's `canon/context/<domain>.md` entry instead. The choice and its rejected alternative stay here whatever their reach, since reach is what makes a decision cross-domain, not how many domains its supporting measurement happens to touch.
31
- - A decision that constrains one domain alone. It lives in that domain's context entry, and this file carries at most one line pointing at it.
40
+ - A decision that fills no slot. It lives in the domain context entry it constrains, and this file carries at most one line pointing at it.
41
+ - A measurement paragraph specific to one domain's own mechanism, even behind a slot decision. Route it to that domain's context entry and keep the choice, the alternative that lost, and one reason here.
42
+ - The instances of a build principle. The principle takes one sentence here, and each instance lives in the domain entry where it applies.
32
43
  - The history of how a decision was reached or revised: rounds of candidates, a figure followed by its correction, a branch or change that moved a number. That trail goes to the decision log or the change that introduced it.
33
44
 
34
45
  ## Sections
@@ -38,7 +49,7 @@ Use `## Overview`, `## Key technical decisions` with one named H3 per decision,
38
49
  ## Keeping it current
39
50
 
40
51
  - Rewrite a decision a later one changed rather than appending the change beside it. A reader should find the design that stands in one place, with the alternative that lost stated once.
41
- - Hold only what is open under `## Risks / open questions`. An entry leaves the section in the change that settles it, becoming a decision here when it constrains more than one domain and moving to that domain's context entry when it does not.
52
+ - Hold only what is open under `## Risks / open questions`. An entry leaves the section in the change that settles it, becoming a decision here when it fills a slot and moving to the domain context entry it constrains when it does not.
42
53
 
43
54
  ## Verification anchors
44
55
 
@@ -54,27 +65,41 @@ A decision's reasoning stays correct while the numbers it cites move. The anchor
54
65
  - Do not edit a claim in the pass that first anchors it. The anchor states what the claim was measured against, so changing both at once leaves nothing to check the anchor against.
55
66
  - Refresh the anchor whenever the number is re-read, whether or not it moved. A confirmed number and an unread one are the same text without the date.
56
67
 
57
- ## Length
68
+ ## Entry cap
58
69
 
59
- Every session pays for this file before any work starts, so a heavy read is a real cost. Judge weight by reading the file rather than by counting it: a file that reads heavy is carrying too many decisions, not decisions written too long. A word count, for the file and for each decision, is read alongside that judgment when one is available, and it never gates.
70
+ Every session pays for this file before any work starts, so a heavy read is a real cost. A file that reads heavy is carrying too many decisions, not decisions written too long, so the bound is a count of decisions rather than of words.
60
71
 
61
- - Bring a heavy file back by merging two decisions or retiring one, never by compressing a decision's prose.
72
+ - State the cap in the record itself as a clause of the form `This record holds at most <n> decisions.` The cap is the record's own, so a checker reads it from the file, and a record stating none is measured and never gated.
73
+ - Set the cap near one decision per slot plus a small margin.
74
+ - At the cap, merge two decisions or retire one before adding another, and name which in the change that does it. Never compress a decision's prose to fit, and never pack two decisions under one heading, which the count cannot see.
75
+ - Retire a decision by moving it to the domain context entry it constrains, not by deleting its reasoning.
76
+ - A word count, for the file and for each decision, is read alongside the judgment when one is available, and it never gates.
62
77
  - Yield to the paragraph weight checkpoint in `markdown.md`. A paragraph past the checkpoint is a defect no length guideline licenses.
63
78
 
64
79
  ## Template
65
80
 
66
- The anchor sentence closes a decision whose reasoning cites a measured number and is absent from one that cites none.
81
+ The anchor sentence closes a decision whose reasoning cites a measured number and is absent from one that cites none. Each heading below names a slot, so a record starts with one decision per slot and renames each heading to the choice it records.
67
82
 
68
83
  ```markdown
69
84
  # Architecture
70
85
 
71
86
  ## Overview
72
87
 
88
+ This record holds at most 12 decisions.
89
+
73
90
  ## Key technical decisions
74
91
 
75
- ### Decision name
92
+ ### Stack and runtime
93
+
94
+ What was chosen, the alternative that lost, and why. Measured at <short-sha> on <YYYY-MM-DD>.
95
+
96
+ ### Delivery
97
+
98
+ ### Enforced boundaries
99
+
100
+ ### Layout
76
101
 
77
- Reasoning and tradeoffs, carrying the measured number the choice rested on. Measured at <short-sha> on <YYYY-MM-DD>.
102
+ ### Build principles
78
103
 
79
104
  ## Risks / open questions
80
105
  ```
@@ -13,7 +13,7 @@ Governs per-domain narrative entries under `canon/context/`: their structure, th
13
13
 
14
14
  Does not govern:
15
15
 
16
- - Cross-domain decisions and system-wide risks: `architecture.md`
16
+ - Decisions filling a stack, delivery, boundary, layout, or build-principle slot, and system-wide risks: `architecture.md`
17
17
  - Product scope, goals, and non-goals: `requirements.md`
18
18
  - Path-scoped coding rules: `rule.md`
19
19
  - Diagrams and wireframes, which answer structure and layout questions an entry hands off: `diagrams.md` and `wireframes.md`
@@ -82,7 +82,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
82
82
  ## What goes in
83
83
 
84
84
  - Per-domain narrative on how the code is structured and why
85
- - Decisions specific to the domain. Broader cross-domain decisions belong in `canon/ARCHITECTURE.md`.
85
+ - Decisions that constrain the domain, whether or not their reasoning touches another one. This is a decision's default home, and `canon/ARCHITECTURE.md` takes only a decision that fills one of the slots its standard names.
86
86
  - Constraints, gotchas, things tried and rejected
87
87
  - Domain-specific conventions that do not fit a `paths:`-scoped rule
88
88
  - A measured figure, anchored the way `architecture.md` states under `## Verification anchors`, so an entry and the architecture record date a number the same way.
@@ -4,10 +4,20 @@ Authoring guidance: the architecture standard.
4
4
 
5
5
  ## Overview
6
6
 
7
+ This record holds at most 12 decisions.
8
+
7
9
  ## Key technical decisions
8
10
 
9
- ### Decision name
11
+ ### Stack and runtime
12
+
13
+ What was chosen, the alternative that lost, and why.
14
+
15
+ ### Delivery
16
+
17
+ ### Enforced boundaries
18
+
19
+ ### Layout
10
20
 
11
- Reasoning and tradeoffs.
21
+ ### Build principles
12
22
 
13
23
  ## Risks / open questions