@erclx/canon 4.117.0 → 4.119.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.
Files changed (84) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/canon-operator/SKILL.md +1 -1
  3. package/claude/skills/canon-rollout/SKILL.md +2 -2
  4. package/claude/skills/design-extract/SKILL.md +1 -1
  5. package/claude/skills/docs-fold/SKILL.md +2 -2
  6. package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
  7. package/claude/skills/draft-ready/SKILL.md +1 -1
  8. package/claude/skills/draft-ready/references/assembly.md +8 -0
  9. package/claude/skills/draft-screencast/SKILL.md +1 -1
  10. package/claude/skills/git-pr/SKILL.md +5 -3
  11. package/claude/skills/memory-capture/SKILL.md +3 -3
  12. package/claude/skills/memory-review/SKILL.md +3 -3
  13. package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
  14. package/claude/skills/plan-feature/SKILL.md +8 -1
  15. package/claude/skills/review-branch/SKILL.md +1 -1
  16. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  17. package/claude/skills/review-pr/SKILL.md +4 -2
  18. package/claude/skills/role-planner/SKILL.md +1 -1
  19. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  20. package/claude/skills/role-worker/SKILL.md +10 -41
  21. package/claude/skills/seed-sync/SKILL.md +1 -1
  22. package/claude/skills/session-compact/SKILL.md +1 -1
  23. package/claude/skills/session-map/SKILL.md +1 -1
  24. package/claude/skills/session-relay/REQUIREMENT.md +7 -1
  25. package/claude/skills/session-relay/SKILL.md +4 -7
  26. package/claude/skills/session-worktree/REQUIREMENT.md +5 -1
  27. package/claude/skills/session-worktree/SKILL.md +7 -1
  28. package/claude/skills/session-worktree/references/main-root-writes.md +24 -0
  29. package/claude/skills/target-check/references/domains.md +1 -1
  30. package/claude/skills/task-board/SKILL.md +4 -3
  31. package/claude/skills/teach-workspace/SKILL.md +4 -4
  32. package/claude/skills/ui-checklist/SKILL.md +1 -1
  33. package/claude/skills/ux-audit/SKILL.md +1 -1
  34. package/claude/skills/ux-measure/SKILL.md +1 -1
  35. package/claude/skills/ux-walkthrough/references/relay.md +1 -1
  36. package/docs/agents/commands.md +3 -3
  37. package/docs/agents/context-audit-checks.md +2 -28
  38. package/docs/agents/context-audit.md +1 -1
  39. package/docs/agents/install-and-sync.md +28 -17
  40. package/docs/agents/tasks.md +4 -4
  41. package/docs/target-projects.md +2 -2
  42. package/docs/workflow/ai-workflow.md +9 -9
  43. package/docs/workflow/operating-model.md +1 -1
  44. package/docs/workflow/visual-design-workflow.md +2 -2
  45. package/governance/renames.toml +13 -0
  46. package/governance/rules/claude/510-context.md +0 -12
  47. package/governance/rules/core/005-behavior.md +1 -2
  48. package/governance/rules/core/015-output.md +1 -3
  49. package/governance/rules/core/035-tasks.md +1 -3
  50. package/governance/rules/core/045-memory.md +0 -1
  51. package/governance/rules/core/055-scratch.md +2 -4
  52. package/governance/rules/core/070-planning.md +1 -4
  53. package/governance/rules/core/085-worktrees.md +2 -8
  54. package/governance/rules/core/087-git.md +2 -2
  55. package/governance/rules/core/090-code-comments.md +1 -1
  56. package/governance/rules/ui/460-design-taste.md +0 -15
  57. package/package.json +1 -1
  58. package/src/claude/cases/workflow.ts +10 -0
  59. package/src/commands/context.ts +11 -111
  60. package/src/commands/sync.ts +11 -3
  61. package/src/commands/tasks.ts +9 -8
  62. package/src/context/audit.ts +3 -145
  63. package/src/design/base.css +19 -19
  64. package/src/design/board.ts +18 -7
  65. package/src/design/neutral.ts +127 -0
  66. package/src/design/regen.ts +6 -4
  67. package/src/design/tokens.ts +5 -2
  68. package/src/gate/measures.ts +37 -0
  69. package/src/gate/stages.ts +13 -4
  70. package/src/gov/adapter.ts +59 -7
  71. package/src/gov/install.ts +13 -3
  72. package/src/gov/renames.ts +76 -0
  73. package/src/gov/stacks.ts +5 -1
  74. package/src/records/validate.ts +5 -1
  75. package/src/sync/check.ts +25 -4
  76. package/src/sync/engine.ts +154 -10
  77. package/src/sync/stamp.ts +44 -0
  78. package/src/tasks/archive.ts +28 -7
  79. package/src/tasks/record.ts +16 -9
  80. package/src/tasks/validate.ts +3 -1
  81. package/standards/plan.md +24 -1
  82. package/standards/tasks.md +5 -3
  83. package/governance/rules/core/091-channel.md +0 -13
  84. package/src/context/narration.ts +0 -99
@@ -163,11 +163,21 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
163
163
  prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
164
164
  expect: 'session-worktree',
165
165
  },
166
+ {
167
+ prompt:
168
+ 'My Write to the plan file at the main root got refused for session isolation. How do I land it there?',
169
+ expect: 'session-worktree',
170
+ },
166
171
  {
167
172
  prompt:
168
173
  'Relay this over to the session that dispatched me. It gave me a sessionId rather than a name, so work out who that is and send it.',
169
174
  expect: 'session-relay',
170
175
  },
176
+ {
177
+ prompt:
178
+ 'I am stuck on a question only the operator can answer. Before I stop and ask it, tell the session that dispatched me, it gave me a sessionId.',
179
+ expect: 'session-relay',
180
+ },
171
181
 
172
182
  // Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
173
183
  {
@@ -56,12 +56,6 @@ import {
56
56
  } from '@/context/folders'
57
57
  import { isGating } from '@/context/gate'
58
58
  import { auditIndexes, type FolderDrift } from '@/context/index-drift'
59
- import {
60
- loadNarration,
61
- type Narration,
62
- PRONOUN_HEADING,
63
- VERB_HEADING,
64
- } from '@/context/narration'
65
59
  import {
66
60
  measureWireframeFolder,
67
61
  type WireframeStatesReport,
@@ -101,7 +95,7 @@ export function register(program: Command): void {
101
95
  context
102
96
  .command('audit')
103
97
  .description(
104
- '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',
98
+ 'Report required sections, entry length, citations, reference form, catalog tables, provenance, index drift, the architecture record against its own ceiling, its own entry cap, and its word weight, and wireframe states against their evidence folders',
105
99
  )
106
100
  .argument('[path]', 'Project root, defaulting to the current directory')
107
101
  .helpOption('-h, --help', 'Show this help message')
@@ -133,8 +127,8 @@ export function register(program: Command): void {
133
127
  'findings that are facts rather than judgments: a missing required',
134
128
  'section and index drift. A context entry requires Overview and',
135
129
  'Layout, and a wireframe requires Regions, States, Copy, and Not on',
136
- 'this surface. Entry length, reference form, table, provenance, narration,',
137
- 'and the record claim classification are judgments under both.',
130
+ 'this surface. Entry length, reference form, table, provenance, and the',
131
+ 'record claim classification are judgments under both.',
138
132
  '',
139
133
  'Depth and bullet weight are stated over every markdown file rather',
140
134
  'than over a context entry, so `canon markdown audit` measures them.',
@@ -653,24 +647,13 @@ async function runAudit(
653
647
  )
654
648
  }
655
649
 
656
- const narration: Narration = gateOnly
657
- ? { kind: 'absent' }
658
- : await loadNarration(root)
659
- const entries = gateOnly
660
- ? []
661
- : await measureFolders(
662
- root,
663
- folders,
664
- narration.kind === 'loaded' ? narration : undefined,
665
- )
650
+ const entries = gateOnly ? [] : await measureFolders(root, folders)
666
651
  const drift = gateOnly ? [] : await auditIndexes(folders)
667
652
  const sections = gateOnly ? [] : missingSections(root, folders, entries)
668
653
  const length = gateOnly ? undefined : lengthFindings(entries)
669
654
  // Absent under `--citations-only` and null when the project carries no
670
- // record, for the reason `checkpoints.narration` states about its own two
671
- // absences. A run that never looked and a project with nothing to look at
672
- // are different answers, and one value for both reports the second as the
673
- // first.
655
+ // record. A run that never looked and a project with nothing to look at are
656
+ // different answers, and one value for both reports the second as the first.
674
657
  const record = gateOnly ? undefined : await measureArchitecture(root)
675
658
  const wireframes = gateOnly
676
659
  ? []
@@ -693,7 +676,6 @@ async function runAudit(
693
676
  reportLength(length ?? [])
694
677
  reportTables(entries)
695
678
  reportProvenance(entries, folders)
696
- reportNarration(entries, folders, narration)
697
679
  reportDrift(drift)
698
680
  reportRecord(record, root)
699
681
  reportWireframeStates(wireframes)
@@ -722,10 +704,11 @@ async function runAudit(
722
704
  // answers, and one wrong restatement is a cause reported against the
723
705
  // wrong entry.
724
706
  //
725
- // Absent rather than empty under `--citations-only`, for the reason
726
- // `checkpoints.narration` below carries. That mode measures no entry,
727
- // so an empty array here reads as a corpus with nothing past the
728
- // checkpoint rather than as a run that never looked.
707
+ // Absent rather than empty under `--citations-only`. That mode
708
+ // measures no entry, so an empty array here reads as a corpus with
709
+ // nothing past the checkpoint rather than as a run that never looked,
710
+ // and collapsing the two reports a measured answer for a run that
711
+ // never opened an entry.
729
712
  length,
730
713
  missingSections: sections,
731
714
  indexDrift: drift,
@@ -747,20 +730,6 @@ async function runAudit(
747
730
  // caller that read it before wireframes owed a section, since turning
748
731
  // it into this map would break that caller without an error.
749
732
  requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
750
- // Three states rather than two, so a record showing no finding says
751
- // which terms were looked for. The key is absent when the run never
752
- // scanned, which is `--citations-only`, and null when it scanned and
753
- // no rule published both headings. Collapsing the first into null
754
- // reports an absent rule against a run that never opened one.
755
- narration: gateOnly
756
- ? undefined
757
- : narration.kind === 'loaded'
758
- ? {
759
- source: narration.source,
760
- pronouns: narration.pronouns,
761
- verbs: narration.verbs,
762
- }
763
- : null,
764
733
  },
765
734
  })}\n`,
766
735
  )
@@ -1144,75 +1113,6 @@ function reportProvenance(
1144
1113
  )
1145
1114
  }
1146
1115
 
1147
- /**
1148
- * Reports the bullets narrating a decision the bullet above them replaced.
1149
- *
1150
- * The reach line names the rule the sets were read from, since the check is
1151
- * silent when no rule publishes them and a run that scanned nothing otherwise
1152
- * prints the same clean line as a run that scanned everything.
1153
- *
1154
- * The legitimate-hit line is here for the same reason the provenance section
1155
- * says a marker is a judgment. The standard keeps a rejected alternative and
1156
- * why it lost, which is a back-reference in the past tense by construction, and
1157
- * no measure separates one from the shape the rule bans.
1158
- */
1159
- function reportNarration(
1160
- entries: readonly EntryReport[],
1161
- folders: readonly AuditedFolder[],
1162
- narration: Narration,
1163
- ): void {
1164
- logStep('Narration')
1165
-
1166
- const governed = folders.filter(governsContent)
1167
- if (governed.length === 0) {
1168
- logInfo(
1169
- `Out of scope. The rule is stated in the standard governing canon/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
1170
- )
1171
- return
1172
- }
1173
-
1174
- if (narration.kind === 'absent') {
1175
- logWarn(
1176
- `Not scanned. No rule under .claude/rules/ or governance/rules/ publishes both ${PRONOUN_HEADING} and ${VERB_HEADING}.`,
1177
- )
1178
- return
1179
- }
1180
-
1181
- logInfo(
1182
- `Covers canon/${PROVENANCE_FOLDER}/ alone, reading ${plural(narration.pronouns.length, 'pronoun')} and ${plural(narration.verbs.length, 'verb')} from ${narration.source}.`,
1183
- )
1184
- logInfo(
1185
- 'A rejected alternative is a legitimate hit, since the standard keeps what was tried and why it lost.',
1186
- )
1187
-
1188
- const carrying = entries
1189
- .filter((entry) => entry.narration.length > 0)
1190
- .sort((a, b) => b.narration.length - a.narration.length)
1191
-
1192
- if (carrying.length === 0) {
1193
- logInfo('No bullet narrates a decision the bullet above it replaced.')
1194
- return
1195
- }
1196
-
1197
- const total = carrying.reduce((sum, entry) => sum + entry.narration.length, 0)
1198
- logWarn(
1199
- `${plural(total, 'bullet')} to read across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
1200
- )
1201
- pipeOutput(
1202
- carrying
1203
- .map(
1204
- (entry) =>
1205
- `${entry.rel} ${plural(entry.narration.length, 'bullet')}\n${entry.narration
1206
- .map(
1207
- (found) =>
1208
- ` :${found.line} ${found.pronoun} with ${found.verb}`,
1209
- )
1210
- .join('\n')}`,
1211
- )
1212
- .join('\n'),
1213
- )
1214
- }
1215
-
1216
1116
  /** How each classification reads in the report. */
1217
1117
  const CLAIM_LABEL: Record<string, string> = {
1218
1118
  countable: 'countable claim',
@@ -133,9 +133,17 @@ function renderCheck(report: CheckReport): void {
133
133
  logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
134
134
  }
135
135
 
136
- const { stale, customized, drifted, stranded, orphaned, missing } =
137
- domain.counts
138
- if (stale + customized + drifted + stranded === 0) {
136
+ const {
137
+ stale,
138
+ customized,
139
+ drifted,
140
+ stranded,
141
+ retired,
142
+ renamed,
143
+ orphaned,
144
+ missing,
145
+ } = domain.counts
146
+ if (stale + customized + drifted + stranded + retired + renamed === 0) {
139
147
  logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
140
148
  }
141
149
  // A local file is not a deficiency and a missing rule is, so this never
@@ -409,12 +409,14 @@ export function register(program: Command): void {
409
409
  [
410
410
  '',
411
411
  'Exit codes:',
412
- ' 0 the line was added, corrected, or already correct',
412
+ ' 0 the line was added, appended to, corrected, or already correct',
413
413
  ' 1 refused, with the reason on stderr or in the JSON record',
414
414
  '',
415
415
  'It adds Pull request: #NNN under the origin lines the task carries,',
416
- 'and corrects the number in place when the line exists. The board is',
417
- 'shared scratch, so a linked worktree records against the same board.',
416
+ 'and appends the number to the line when one exists, so a task shipped',
417
+ 'in slices lists every pull request. A line that does not read as a',
418
+ 'list is replaced. The board is shared scratch, so a linked worktree',
419
+ 'records against the same board.',
418
420
  '',
419
421
  'Examples:',
420
422
  ' canon tasks pull-request 673 v28.1-trigger-escalation',
@@ -448,11 +450,10 @@ export function register(program: Command): void {
448
450
  ' 0 the line was added, corrected, or already correct',
449
451
  ' 1 refused, with the reason on stderr or in the JSON record',
450
452
  '',
451
- 'It writes Plan: [<label>](<target>) right after the H1, the same',
452
- 'add/correct/unchanged shape canon tasks pull-request writes with, and',
453
- 'resolves both a bare slug and a board-relative path the way',
454
- 'canon tasks plan-answers does. Safe from a linked worktree, since it',
455
- 'resolves the board root in-process.',
453
+ 'It writes Plan: [<label>](<target>) right after the H1, corrects the',
454
+ 'target in place when the line exists, and resolves both a bare slug',
455
+ 'and a board-relative path the way canon tasks plan-answers does. Safe',
456
+ 'from a linked worktree, since it resolves the board root in-process.',
456
457
  '',
457
458
  'Examples:',
458
459
  ' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises'
2
2
  import { basename, relative } from 'node:path'
3
3
  import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
4
4
  import type { AuditedFolder } from '@/context/folders'
5
- import type { NarrationTerms } from '@/context/narration'
6
5
  import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
7
6
  import { documentHeight } from '@/markdown/structure'
8
7
  import { isStubSeed } from '@/seed-marker'
@@ -80,10 +79,6 @@ const TABLE_ROW = /^\s*\|/
80
79
  const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
81
80
  const NAMED_CELL = /`[^`]+`|\[[^\]]+\]\([^)]+\)/
82
81
 
83
- const TOP_BULLET = /^-\s+(.+)$/
84
- /** Any indented line, which is a nested bullet or a wrapped continuation. */
85
- const INSIDE_LIST = /^\s+\S/
86
-
87
82
  /**
88
83
  * Spellings of how the domain reached its shape rather than what it is now.
89
84
  *
@@ -156,9 +151,9 @@ const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
156
151
  /**
157
152
  * The folder whose standard carries the exclusion above.
158
153
  *
159
- * Three measures narrow here rather than one: provenance, the required
160
- * sections, and the superseded-decision narration. The name is the first of
161
- * them because it was the first, and it stays because renaming a constant the
154
+ * Two measures narrow here rather than one: provenance and the required
155
+ * sections. The name is the first of them because it was the first, and it
156
+ * stays because renaming a constant the
162
157
  * JSON record publishes as `checkpoints.provenanceFolder` breaks every consumer
163
158
  * reading that field for a gain of one word.
164
159
  *
@@ -202,14 +197,6 @@ export interface BareReferenceFinding {
202
197
  readonly name: string
203
198
  }
204
199
 
205
- export interface NarrationFinding {
206
- readonly line: number
207
- /** The opening that points back at the bullet above. */
208
- readonly pronoun: string
209
- /** The past-tense verb that turns the back-reference into a narration. */
210
- readonly verb: string
211
- }
212
-
213
200
  export interface EntryReport {
214
201
  readonly rel: string
215
202
  /**
@@ -229,12 +216,6 @@ export interface EntryReport {
229
216
  readonly catalogTables: readonly TableFinding[]
230
217
  /** Empty for an entry no standard bans a change narrative in. */
231
218
  readonly provenance: readonly ProvenanceFinding[]
232
- /**
233
- * Empty outside the governed folder, and empty on a run whose caller loaded
234
- * no term sets. The report distinguishes the two from the vocabulary itself,
235
- * since an empty list here is silent about which one produced it.
236
- */
237
- readonly narration: readonly NarrationFinding[]
238
219
  /**
239
220
  * References naming a sibling entry by bare filename, and empty for a caller
240
221
  * that passed no sibling names. Which entries have siblings worth matching is
@@ -434,125 +415,6 @@ function stampsMeasurement(text: string, index: number): boolean {
434
415
  return STAMPING.test(clause) || STAMPING_RUN.test(clause)
435
416
  }
436
417
 
437
- function escape(term: string): string {
438
- return term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
439
- }
440
-
441
- /**
442
- * Copulas that turn the verb behind them into a present-tense passive.
443
- *
444
- * `used to` is the term this exists for, since `is used to resolve the folder`
445
- * is the passive of `use` rather than the past habitual the set means. This
446
- * list is English grammar rather than corpus vocabulary, so it stays in code
447
- * while the two tunable sets stay in the rule, and a rule publishing two of
448
- * three headings would be a fourth absent state to carry for no tuning anyone
449
- * wants.
450
- *
451
- * What it costs is the perfect passive. `has been superseded` narrates a
452
- * supersession and is rejected with the rest, which is a recall gap taken
453
- * knowingly on the trade this check already makes everywhere else.
454
- */
455
- const COPULA = ['is', 'are', 'be', 'been', 'being', 'was', 'were']
456
-
457
- /**
458
- * Finds the bullets narrating a decision the bullet above them replaced.
459
- *
460
- * The signal is structural rather than lexical, and the corpus is what decides
461
- * that. The terms carrying clean signal for a supersession are too rare to
462
- * catch anything, and the one term that would have caught the known case is
463
- * `now`, which appears 57 times across 24 entries in correct present-tense
464
- * prose. What separates the shape instead is a bullet pointing back at its
465
- * sibling and putting the sibling's design in the past: an opening pronoun with
466
- * no antecedent of its own, plus a past-tense verb, plus a bullet above it to
467
- * refer to. Precision is the whole value, so recall is the accepted exposure
468
- * and a narration written as a single bullet is not reached.
469
- *
470
- * The pronoun is matched cased and anchored, since a mid-sentence `this` is a
471
- * determiner rather than a back-reference. The verb is matched uncased anywhere
472
- * in the bullet, since the tense is what carries the signal wherever it sits,
473
- * and rejected behind a `COPULA` for the reason stated there. Displayed spans
474
- * are masked so a term quoted in backticks is not read as prose the entry
475
- * writes, and fenced blocks are skipped for the reason the scans above skip
476
- * them.
477
- *
478
- * A blank line does not end the run. Markdown reads two bullets around one as a
479
- * single loose list, so breaking there would leave the shape reachable by
480
- * anyone who spaced their bullets out. What ends a run is content that is
481
- * neither a bullet nor indented under one, which the fenced branch below has to
482
- * answer for itself because a fenced line is skipped before that test.
483
- */
484
- function narration(
485
- lines: readonly BodyLine[],
486
- terms: NarrationTerms,
487
- ): NarrationFinding[] {
488
- if (terms.pronouns.length === 0 || terms.verbs.length === 0) return []
489
-
490
- const findings: NarrationFinding[] = []
491
- let following = false
492
- let fenceBlock: number | undefined
493
- let fenceInsideList = false
494
-
495
- for (const line of lines) {
496
- // A fenced line is never a bullet, but an unindented block still ends the
497
- // run. CommonMark reads a fence at column zero as interrupting the list, so
498
- // the bullets around it are two lists and the second has no antecedent
499
- // above it. A block indented under its bullet stays inside the item.
500
- //
501
- // The opening delimiter decides for every line of its own block. Reading
502
- // each line instead ends the run on a blank line inside an indented fence,
503
- // which has no indentation to read, and on a content line at column zero,
504
- // which CommonMark permits since only the fence's own indent is stripped.
505
- //
506
- // The block index is what re-opens that decision, so two blocks written
507
- // with nothing between them are answered separately rather than the second
508
- // inheriting the first. Indentation stays the test here because what
509
- // interrupts a list is this check's own judgment, and the walker only says
510
- // which lines share a block.
511
- if (line.fenced) {
512
- if (line.fenceBlock !== fenceBlock) {
513
- fenceBlock = line.fenceBlock
514
- fenceInsideList = INSIDE_LIST.test(line.text)
515
- }
516
- if (!fenceInsideList) following = false
517
- continue
518
- }
519
-
520
- if (line.text.trim() === '') continue
521
-
522
- const bullet = line.text.match(TOP_BULLET)
523
- if (!bullet) {
524
- // A nested bullet and a wrapped continuation both sit inside the list, so
525
- // neither ends the run. Anything else does, which is what keeps a bullet
526
- // opening the list under a heading from reading as a reply to the last
527
- // bullet of the list before it.
528
- if (!INSIDE_LIST.test(line.text)) following = false
529
- continue
530
- }
531
-
532
- const text = maskDisplayed(bullet[1])
533
-
534
- if (following) {
535
- const pronoun = terms.pronouns.find((term) =>
536
- new RegExp(`^${escape(term)}\\b`).test(text),
537
- )
538
- const verb = pronoun
539
- ? terms.verbs.find((term) =>
540
- new RegExp(
541
- `(?<!\\b(?:${COPULA.join('|')})\\s+)\\b${escape(term)}\\b`,
542
- 'i',
543
- ).test(text),
544
- )
545
- : undefined
546
-
547
- if (pronoun && verb) findings.push({ line: line.number, pronoun, verb })
548
- }
549
-
550
- following = true
551
- }
552
-
553
- return findings
554
- }
555
-
556
418
  /**
557
419
  * Finds which required sections the entry declares.
558
420
  *
@@ -639,7 +501,6 @@ export function measureEntry(
639
501
  rel: string,
640
502
  source: string,
641
503
  governsContent = true,
642
- terms?: NarrationTerms,
643
504
  siblings: readonly string[] = [],
644
505
  required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
645
506
  ): EntryReport {
@@ -650,7 +511,6 @@ export function measureEntry(
650
511
  lines: documentHeight(source),
651
512
  catalogTables: catalogTables(lines),
652
513
  provenance: governsContent ? provenance(lines) : [],
653
- narration: governsContent && terms ? narration(lines, terms) : [],
654
514
  bareReferences: bareReferences(lines, siblings),
655
515
  sections: declaredSections(lines, required),
656
516
  stub: isStubSeed(source),
@@ -714,7 +574,6 @@ function historyCause(entry: EntryReport): LengthCause {
714
574
  export async function measureFolders(
715
575
  root: string,
716
576
  folders: readonly AuditedFolder[],
717
- terms?: NarrationTerms,
718
577
  ): Promise<EntryReport[]> {
719
578
  const reports: EntryReport[] = []
720
579
 
@@ -731,7 +590,6 @@ export async function measureFolders(
731
590
  relative(root, path),
732
591
  await readFile(path, 'utf8'),
733
592
  governsContent(folder),
734
- terms,
735
593
  names.filter((name) => name !== self),
736
594
  requiredSections(folder),
737
595
  ),
@@ -1,29 +1,29 @@
1
- /* Generated by `canon design regen` from src/design/tokens.ts. Do not edit.
1
+ /* Generated by `canon design regen` from src/design/neutral.ts. Do not edit.
2
2
  This is the base half of a design install. A target overrides a value in
3
3
  .claude/design/project/, which the three-way merge leaves alone, so this
4
4
  file changes freely and nothing a project wrote lives in it. */
5
5
 
6
6
  :root {
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;
7
+ --color-background: #0e0e0e;
8
+ --color-surface: #141414;
9
+ --color-chrome: #1a1a1a;
10
+ --color-border: #292929;
11
+ --color-text: #dddddd;
12
+ --color-text-body: #b2b2b2;
13
+ --color-text-secondary: #8e8e8e;
14
+ --color-muted: #7f7f7f;
15
+ --color-accent: #ececec;
16
16
  --color-success: #61c454;
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;
17
+ --color-light-background: #fafafa;
18
+ --color-light-surface: #f1f1f1;
19
+ --color-light-chrome: #e8e8e8;
20
+ --color-light-text: #252525;
21
+ --color-light-text-body: #444444;
22
+ --color-light-text-secondary: #5f5f5f;
23
+ --color-light-muted: #6c6c6c;
24
+ --color-light-accent: #191919;
25
25
  --color-light-success: #2d6b22;
26
- --color-light-border: #d5d4d1;
26
+ --color-light-border: #d4d4d4;
27
27
  --space-xs: 0.25rem;
28
28
  --space-sm: 0.5rem;
29
29
  --space-md: 0.875rem;
@@ -10,6 +10,7 @@ import {
10
10
  import { join, relative, sep } from 'node:path'
11
11
  import { DESIGN_DOCUMENT } from '@/design/regen'
12
12
  import { renderDesignDoc } from '@/design/render'
13
+ import { NEUTRAL_TOKENS } from '@/design/neutral'
13
14
  import { colorValue } from '@/design/tokens'
14
15
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
15
16
  import { recordDir } from '@/record-root'
@@ -131,6 +132,16 @@ const SCROLLSPY_SCRIPT = [
131
132
  '})();</script>',
132
133
  ].join('')
133
134
 
135
+ /**
136
+ * A role's toolkit value, falling back to the neutral install base's where the
137
+ * toolkit record drops the role, so the board never restates a hex of its own.
138
+ */
139
+ function roleColor(role: string): string {
140
+ const value = colorValue(role) ?? colorValue(role, NEUTRAL_TOKENS)
141
+ if (value === undefined) throw new Error(`No design token declares ${role}`)
142
+ return value
143
+ }
144
+
134
145
  /**
135
146
  * The same brand mark `src/design/render.ts` embeds in its own preview,
136
147
  * colored with the dark accent since the board's default chrome is dark
@@ -139,7 +150,7 @@ const SCROLLSPY_SCRIPT = [
139
150
  * copies rather than a shared icon helper for the first second caller.
140
151
  */
141
152
  function faviconLink(): string {
142
- const color = colorValue('accent') ?? '#e0724b'
153
+ const color = roleColor('accent')
143
154
  const href = `data:image/svg+xml,${encodeURIComponent(
144
155
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${color}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${color}" /></svg>`,
145
156
  )}`
@@ -232,12 +243,12 @@ ${SCROLLSPY_SCRIPT}
232
243
  }
233
244
 
234
245
  function panelPage(title: string, body: string): string {
235
- const accent = colorValue('light-accent') ?? '#a4471c'
236
- const surface = colorValue('light-surface') ?? '#f4efe6'
237
- const border = colorValue('light-border') ?? '#e4dcd0'
238
- const muted = colorValue('light-muted') ?? '#726b62'
239
- const text = colorValue('light-text') ?? '#1a1815'
240
- const background = colorValue('light-background') ?? '#faf7f2'
246
+ const accent = roleColor('light-accent')
247
+ const surface = roleColor('light-surface')
248
+ const border = roleColor('light-border')
249
+ const muted = roleColor('light-muted')
250
+ const text = roleColor('light-text')
251
+ const background = roleColor('light-background')
241
252
 
242
253
  return `<!doctype html>
243
254
  <html lang="en">