@erclx/canon 5.33.0 → 5.35.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 (190) hide show
  1. package/README.md +3 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/REQUIREMENT.md +2 -3
  4. package/claude/skills/api-design/SKILL.md +2 -3
  5. package/claude/skills/api-design/references/adopted.md +1 -1
  6. package/claude/skills/auto-ship/references/draft-mark.md +1 -1
  7. package/claude/skills/canon-cli/SKILL.md +10 -11
  8. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
  9. package/claude/skills/canon-feedback/SKILL.md +10 -28
  10. package/claude/skills/canon-feedback/references/template.md +53 -0
  11. package/claude/skills/canvas-board/SKILL.md +4 -1
  12. package/claude/skills/canvas-board/references/canvas.md +26 -3
  13. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  14. package/claude/skills/code-craft/SKILL.md +0 -1
  15. package/claude/skills/create-rule/SKILL.md +1 -1
  16. package/claude/skills/create-rule/references/rule.md +3 -1
  17. package/claude/skills/create-skill/SKILL.md +2 -1
  18. package/claude/skills/create-skill/references/skill-paths.md +12 -15
  19. package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
  20. package/claude/skills/create-skill/references/skill.md +10 -4
  21. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  22. package/claude/skills/git-commit/references/versioning.md +13 -13
  23. package/claude/skills/git-issue/references/issue.md +4 -1
  24. package/claude/skills/git-pr/SKILL.md +1 -1
  25. package/claude/skills/git-pr/references/publish.md +1 -1
  26. package/claude/skills/review-address/REQUIREMENT.md +1 -1
  27. package/claude/skills/review-address/SKILL.md +2 -2
  28. package/claude/skills/review-craft/references/ui.md +1 -1
  29. package/claude/skills/review-pr/SKILL.md +3 -3
  30. package/claude/skills/review-pr/references/post.md +1 -1
  31. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  32. package/claude/skills/review-ui/SKILL.md +8 -6
  33. package/claude/skills/review-ui/references/driving.md +8 -8
  34. package/claude/skills/review-ui/references/post.md +52 -16
  35. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
  36. package/claude/skills/role-orchestrator/SKILL.md +11 -12
  37. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
  38. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
  39. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
  40. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +11 -2
  41. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
  42. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
  43. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
  44. package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
  45. package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
  46. package/claude/skills/role-reviewer/SKILL.md +17 -10
  47. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  48. package/claude/skills/role-worker/SKILL.md +6 -6
  49. package/claude/skills/session-compact/references/handoff-note.md +1 -1
  50. package/claude/skills/target-setup/SKILL.md +12 -6
  51. package/claude/skills/target-setup/references/detect.md +22 -5
  52. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  53. package/claude/skills/test-craft/SKILL.md +15 -3
  54. package/docs/agents/audits.md +1 -1
  55. package/docs/agents/canvas.md +5 -1
  56. package/docs/agents/commands.md +3 -5
  57. package/docs/agents/counts.md +5 -6
  58. package/docs/agents/drift-surfaces.md +15 -19
  59. package/docs/agents/gov-detect.md +47 -0
  60. package/docs/agents/index.md +6 -4
  61. package/docs/agents/install-and-sync.md +11 -31
  62. package/docs/agents/markdown-audit.md +10 -12
  63. package/docs/agents/migrate.md +1 -1
  64. package/docs/agents/pr-evidence-layout.md +70 -0
  65. package/docs/agents/pr-evidence.md +8 -9
  66. package/docs/agents/pr-frames.md +7 -2
  67. package/docs/agents/pr-lift.md +132 -0
  68. package/docs/agents/records-stale.md +1 -1
  69. package/docs/agents/records-validate.md +11 -23
  70. package/docs/agents/records.md +1 -1
  71. package/docs/agents/routing.md +1 -1
  72. package/docs/agents/rule-citations.md +11 -14
  73. package/docs/agents/scripting.md +4 -21
  74. package/docs/agents/state-scoped-risk.md +1 -1
  75. package/docs/agents/tasks-validate.md +1 -1
  76. package/docs/agents/teach.md +1 -1
  77. package/docs/target/index.md +1 -0
  78. package/docs/target/migrations.md +4 -0
  79. package/docs/target/projects.md +11 -11
  80. package/docs/target/scenarios.md +5 -6
  81. package/docs/target/standards-moved.md +32 -0
  82. package/docs/target/sync.md +5 -5
  83. package/docs/workflow/operating-model.md +16 -18
  84. package/docs/workflow/skill-map.md +1 -1
  85. package/governance/detect.toml +197 -0
  86. package/governance/renames.toml +0 -1
  87. package/governance/rules/code/000-code.md +1 -13
  88. package/governance/rules/code/010-testing.md +2 -30
  89. package/governance/rules/ui/400-ui.md +1 -1
  90. package/governance/rules/ui/480-canvas.md +13 -0
  91. package/governance/stacks/base.toml +15 -8
  92. package/governance/stacks/go.toml +2 -0
  93. package/governance/stacks/node.toml +1 -1
  94. package/governance/stacks/php.toml +2 -0
  95. package/governance/stacks/python.toml +1 -1
  96. package/package.json +1 -4
  97. package/scripts/core/install-check.sh +2 -3
  98. package/scripts/core/regen-hero.ts +0 -6
  99. package/scripts/lib/ui.sh +0 -9
  100. package/src/audits/catalog.ts +0 -9
  101. package/src/canvas/arrange.ts +66 -0
  102. package/src/canvas/capture.ts +20 -6
  103. package/src/canvas/content.ts +39 -0
  104. package/src/canvas/fit.ts +238 -0
  105. package/src/canvas/tokens.ts +28 -10
  106. package/src/capture/frame-values.ts +2 -32
  107. package/src/claude/routing.ts +1 -1
  108. package/src/claude/skills-reach.ts +4 -6
  109. package/src/cli/help.ts +3 -4
  110. package/src/cli.ts +0 -2
  111. package/src/commands/canvas.ts +179 -1
  112. package/src/commands/gov.ts +149 -10
  113. package/src/commands/init.ts +1 -1
  114. package/src/commands/labels.ts +4 -44
  115. package/src/commands/migrate.ts +1 -1
  116. package/src/commands/pr.ts +376 -1
  117. package/src/commands/records.ts +4 -20
  118. package/src/commands/tooling.ts +2 -4
  119. package/src/counts/catalogs.ts +0 -6
  120. package/src/feedback/body.ts +26 -5
  121. package/src/gate/boundaries.ts +2 -2
  122. package/src/gate/measures.ts +64 -72
  123. package/src/gate/sequencer.ts +51 -2
  124. package/src/gate/stages.ts +3 -22
  125. package/src/gov/citations.ts +11 -43
  126. package/src/gov/detect.ts +301 -0
  127. package/src/gov/list.ts +23 -2
  128. package/src/gov/stacks.ts +9 -0
  129. package/src/markdown/bans.ts +1 -1
  130. package/src/migrate/scratch-evidence.ts +2 -2
  131. package/src/pr/evidence.ts +218 -45
  132. package/src/pr/frames.ts +23 -3
  133. package/src/pr/lift.ts +418 -0
  134. package/src/pr/rest.ts +3 -0
  135. package/src/pr/review-scope.ts +76 -0
  136. package/src/records/validate.ts +6 -194
  137. package/src/sandbox/equivalence.ts +14 -7
  138. package/src/sandbox/provision.ts +0 -5
  139. package/src/secrets/shipped.ts +1 -2
  140. package/src/serve/static.ts +1 -2
  141. package/src/shipped/references.ts +29 -80
  142. package/src/sync/check.ts +18 -14
  143. package/src/tooling/inject.ts +6 -37
  144. package/src/tooling/read.ts +2 -3
  145. package/src/tooling/scan.ts +4 -17
  146. package/src/tooling/subfolder.ts +2 -66
  147. package/tooling/base/configs/scripts/verify.json +1 -1
  148. package/tooling/base/configs/scripts/verify.ts +1 -12
  149. package/tooling/base/manifest.toml +1 -2
  150. package/tooling/base/reference.md +10 -16
  151. package/tooling/base/seeds/.github/workflows/verify.yml +0 -3
  152. package/tooling/base/seeds/.lintstagedrc +1 -2
  153. package/tooling/base/seeds/canon/context/ci.md +0 -1
  154. package/tooling/base/seeds/canon/context/development.md +1 -1
  155. package/tooling/claude/reference.md +1 -1
  156. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
  157. package/tooling/go/reference.md +1 -1
  158. package/tooling/php/reference.md +1 -1
  159. package/tooling/python/reference.md +2 -2
  160. package/tooling/web/configs/.github/workflows/verify.yml +0 -3
  161. package/tooling/web/manifest.toml +0 -1
  162. package/tooling/web/reference.md +2 -4
  163. package/docs/agents/standards-audit.md +0 -40
  164. package/governance/rules/tooling/765-spelling.md +0 -19
  165. package/governance/stacks/astro.toml +0 -2
  166. package/governance/stacks/nextjs.toml +0 -2
  167. package/governance/stacks/node-server.toml +0 -2
  168. package/governance/stacks/python-fastapi.toml +0 -2
  169. package/governance/stacks/react.toml +0 -2
  170. package/src/commands/standards.ts +0 -278
  171. package/src/labels/spelling.ts +0 -120
  172. package/src/standards/audit.ts +0 -146
  173. package/src/standards/list.ts +0 -33
  174. package/src/standards/read.ts +0 -177
  175. package/src/standards/scope.ts +0 -56
  176. package/standards/index.md +0 -10
  177. package/tooling/base/configs/.vscode/extensions.json +0 -9
  178. package/tooling/base/configs/.vscode/settings.json +0 -3
  179. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  180. package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
  181. package/tooling/base/seeds/cspell.json +0 -20
  182. package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
  183. package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
  184. package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
  185. package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
  186. package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
  187. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
  188. package/tooling/web/configs/.vscode/extensions.json +0 -13
  189. package/tooling/web/configs/.vscode/settings.json +0 -10
  190. package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
@@ -9,12 +9,15 @@ import {
9
9
  DEFAULT_FRAME,
10
10
  listPages,
11
11
  moveFrame,
12
+ pruneLayout,
12
13
  readPage,
13
14
  readSelection,
14
15
  renamePage,
15
16
  resizeFrame,
16
17
  } from '@/canvas/content'
18
+ import { planArrange } from '@/canvas/arrange'
17
19
  import { captureCanvas, captureComposite } from '@/canvas/capture'
20
+ import { fitCanvas, isOwed } from '@/canvas/fit'
18
21
  import {
19
22
  type EditRefused,
20
23
  editFrameAtIndex,
@@ -148,7 +151,7 @@ export function register(program: Command): void {
148
151
  .command('canvas')
149
152
  .helpOption('-h, --help', 'Show this help message')
150
153
  .description(
151
- 'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, editing, capture)',
154
+ 'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, editing, capture, fit, arrange)',
152
155
  )
153
156
  .addHelpText(
154
157
  'after',
@@ -167,6 +170,8 @@ export function register(program: Command): void {
167
170
  " canon canvas edit drafts/hero --element 4 --set 'color=var(--color-accent)'",
168
171
  ' canon canvas editing drafts/hero',
169
172
  ' canon canvas capture drafts/hero',
173
+ ' canon canvas fit drafts/hero --json',
174
+ ' canon canvas arrange drafts --check',
170
175
  ' canon canvas list --json',
171
176
  '',
172
177
  ].join('\n'),
@@ -728,6 +733,179 @@ export function register(program: Command): void {
728
733
  outro()
729
734
  if (failed) process.exitCode = 1
730
735
  })
736
+
737
+ withRoot(
738
+ canvas
739
+ .command('fit')
740
+ .description("Set each frame's height to its content")
741
+ .argument('<target>', '<page>/<frame> for one frame, <page> for all')
742
+ .option('--check', 'Report what is owed and write nothing'),
743
+ )
744
+ .addHelpText(
745
+ 'after',
746
+ [
747
+ '',
748
+ 'Measures each frame in the capture browser at its box width, through',
749
+ 'the served address, so the faces the canvas serves are the ones read.',
750
+ 'The height is the content height rounded up, with no margin. A',
751
+ 'content wider than the box is reported and the box is never widened.',
752
+ 'A frame whose body text names a family the canvas does not serve is',
753
+ 'reported as font, since its height may not hold in another browser.',
754
+ '',
755
+ 'Run canvas arrange after a fit that grew a frame, since rows below',
756
+ 'do not move on their own.',
757
+ '',
758
+ 'Exit codes:',
759
+ ' 0 written, or nothing owed under --check',
760
+ ' 1 refused or the browser failed',
761
+ ' 2 --check found a height off, a wide frame, or a font',
762
+ '',
763
+ ].join('\n'),
764
+ )
765
+ .action(async (target: string, opts: FitOptions) => {
766
+ process.exitCode = await runFit(target, opts)
767
+ })
768
+
769
+ withRoot(
770
+ canvas
771
+ .command('arrange')
772
+ .description('Re-lay a page in rows on the frame gap')
773
+ .argument('<page>', 'Page to arrange')
774
+ .option('--check', 'Report what is owed and write nothing'),
775
+ )
776
+ .addHelpText(
777
+ 'after',
778
+ [
779
+ '',
780
+ 'Keeps the rows the frames already sit in, orders each left to right on',
781
+ 'the frame gap, stacks the rows on their tallest frame, and drops',
782
+ 'layout entries whose frame file is gone.',
783
+ '',
784
+ 'Exit codes:',
785
+ ' 0 written, or nothing owed under --check',
786
+ ' 1 refused',
787
+ ' 2 --check found frames to move or entries to drop',
788
+ '',
789
+ ].join('\n'),
790
+ )
791
+ .action(async (pageName: string, opts: FitOptions) => {
792
+ process.exitCode = await runArrange(pageName, opts)
793
+ })
794
+ }
795
+
796
+ interface FitOptions extends RootOptions {
797
+ readonly check?: boolean
798
+ }
799
+
800
+ async function runFit(target: string, opts: FitOptions): Promise<number> {
801
+ const emitJson = opts.json ?? false
802
+ const resolved = await resolveRoot(opts)
803
+ if (!resolved.ok) return refuse(resolved, emitJson)
804
+
805
+ const check = opts.check ?? false
806
+ let outcome: Awaited<ReturnType<typeof fitCanvas>>
807
+ try {
808
+ outcome = await fitCanvas(resolved.root, target, !check)
809
+ } catch (error) {
810
+ const detail = error instanceof Error ? error.message : String(error)
811
+ return refuse({ ok: false, reason: 'capture-failed', detail }, emitJson)
812
+ }
813
+ if (!outcome.ok) return refuse(outcome, emitJson)
814
+
815
+ const frames = outcome.rows.map((row) => ({ ...row, owed: isOwed(row) }))
816
+ const owed = frames.some((row) => row.owed)
817
+ if (emitJson) writeJson({ ok: true, written: outcome.written, frames })
818
+ intro(BANNER)
819
+ for (const row of frames) {
820
+ const name = `${row.page}/${row.frame}`
821
+ if (row.fitted === row.height) logInfo(`${name} fits at ${row.height}`)
822
+ else if (outcome.written) logAdd(`${name} ${row.height} to ${row.fitted}`)
823
+ else logStep(`${name} owes ${row.height} to ${row.fitted}`)
824
+ if (row.wide) logWarn(`${name} is wider than its box, so widen the box`)
825
+ if (row.viewport) {
826
+ logWarn(
827
+ `${name} sets a height in viewport units, so its height was not written and a fixed height fits it`,
828
+ )
829
+ }
830
+ if (row.font) {
831
+ logWarn(
832
+ `${name} sets ${row.family}, which the canvas does not serve, so its height may not hold in another browser`,
833
+ )
834
+ }
835
+ }
836
+ outro()
837
+ return check && owed ? 2 : 0
838
+ }
839
+
840
+ async function runArrange(pageName: string, opts: FitOptions): Promise<number> {
841
+ const emitJson = opts.json ?? false
842
+ const resolved = await resolveRoot(opts)
843
+ if (!resolved.ok) return refuse(resolved, emitJson)
844
+ const { root } = resolved
845
+
846
+ const page = readPage(root, pageName)
847
+ if (!page) {
848
+ return refuse(
849
+ {
850
+ ok: false,
851
+ reason: 'no-page',
852
+ detail: `page ${pageName} does not exist`,
853
+ },
854
+ emitJson,
855
+ )
856
+ }
857
+ if (page.layoutIssue) {
858
+ return refuse(
859
+ {
860
+ ok: false,
861
+ reason: 'malformed-layout',
862
+ detail: `${pageName}/layout.json does not parse`,
863
+ },
864
+ emitJson,
865
+ )
866
+ }
867
+
868
+ const check = opts.check ?? false
869
+ const pruned = pruneLayout(root, pageName, !check)
870
+ if (!pruned.ok) return refuse(pruned, emitJson)
871
+
872
+ const moves = planArrange(page.frames)
873
+ if (!check) {
874
+ for (const move of moves) {
875
+ const outcome = resizeFrame(root, pageName, move.name, move.to)
876
+ if (!outcome.ok) return refuse(outcome, emitJson)
877
+ }
878
+ }
879
+
880
+ if (emitJson) {
881
+ writeJson({
882
+ ok: true,
883
+ page: pageName,
884
+ written: !check,
885
+ moved: moves.map((move) => ({
886
+ frame: move.name,
887
+ from: { x: move.from.x, y: move.from.y },
888
+ to: { x: move.to.x, y: move.to.y },
889
+ })),
890
+ dropped: pruned.dropped,
891
+ })
892
+ }
893
+ intro(BANNER)
894
+ for (const move of moves) {
895
+ const text = `${pageName}/${move.name} ${move.from.x}, ${move.from.y} to ${move.to.x}, ${move.to.y}`
896
+ if (check) logStep(`${text} is owed`)
897
+ else logAdd(text)
898
+ }
899
+ for (const name of pruned.dropped) {
900
+ logWarn(
901
+ `${pageName}/${name} has no frame file, so its entry is ${check ? 'owed a drop' : 'dropped'}`,
902
+ )
903
+ }
904
+ if (moves.length === 0 && pruned.dropped.length === 0) {
905
+ logInfo(`${pageName} is already laid out`)
906
+ }
907
+ outro()
908
+ return check && (moves.length > 0 || pruned.dropped.length > 0) ? 2 : 0
731
909
  }
732
910
 
733
911
  interface CaptureOptions extends RootOptions {
@@ -19,6 +19,7 @@ import {
19
19
  govStackExists,
20
20
  listGovStacks,
21
21
  mergeExtraRules,
22
+ resolveExtraRules,
22
23
  resolveRules,
23
24
  } from '@/gov/stacks'
24
25
  import {
@@ -51,6 +52,7 @@ import {
51
52
  type TestOrderReport,
52
53
  } from '@/gov/test-order'
53
54
  import { recordStamp, runDomainSync } from '@/sync/engine'
55
+ import { detectRules } from '@/gov/detect'
54
56
  import { writeChainStamp } from '@/sync/stamp'
55
57
  import { resolveTarget } from '@/targets/validate'
56
58
  import {
@@ -141,7 +143,7 @@ export function register(program: Command): void {
141
143
  gov
142
144
  .command('install')
143
145
  .description('Install a governance stack into .claude/rules/canon/')
144
- .argument('[stack]', 'Stack name (e.g. base, node, react)')
146
+ .argument('[stack]', 'Stack name (e.g. base, node, python)')
145
147
  .argument('[target]', 'Target directory', '.')
146
148
  .helpOption('-h, --help', 'Show this help message')
147
149
  .option('--add <rules>', 'Comma-separated rules to layer on the stack')
@@ -150,9 +152,14 @@ export function register(program: Command): void {
150
152
  [
151
153
  '',
152
154
  'Examples:',
153
- ' canon gov install react',
155
+ ' canon gov install node',
154
156
  ' canon gov install node ../my-app',
155
- ' canon gov install astro --add 200-react,260-shadcn ../my-app',
157
+ ' canon gov install node --add 210-astro,260-shadcn ../my-app',
158
+ ' canon gov install --add tasks,plan ../my-app',
159
+ '',
160
+ 'With --add and no stack, the named rules install alone. A first',
161
+ 'positional naming a stack is read as the stack, so pass . or a path',
162
+ 'carrying a slash to force a target that shares a stack name.',
156
163
  '',
157
164
  ].join('\n'),
158
165
  )
@@ -221,6 +228,31 @@ export function register(program: Command): void {
221
228
  process.exitCode = runList(opts)
222
229
  })
223
230
 
231
+ gov
232
+ .command('detect')
233
+ .description('Propose opt-in rules from the evidence a target holds')
234
+ .argument('[target]', 'Target directory', '.')
235
+ .helpOption('-h, --help', 'Show this help message')
236
+ .option('--stack <stack>', 'Leave out every rule this stack resolves')
237
+ .option('--json', 'Emit machine-readable JSON')
238
+ .addHelpText(
239
+ 'after',
240
+ [
241
+ '',
242
+ 'Reads governance/detect.toml and matches each rule against the',
243
+ "target's manifests and git-listed paths to depth two. Pass the",
244
+ 'proposals to canon gov install --add once confirmed.',
245
+ '',
246
+ 'Examples:',
247
+ ' canon gov detect --json',
248
+ ' canon gov detect ../my-app --stack node --json',
249
+ '',
250
+ ].join('\n'),
251
+ )
252
+ .action(async (target: string, opts: DetectOptions) => {
253
+ process.exitCode = await runDetect(target, opts)
254
+ })
255
+
224
256
  gov
225
257
  .command('test-order')
226
258
  .description(
@@ -275,7 +307,7 @@ export function register(program: Command): void {
275
307
  'members a closed catalog holds, then compares the stated figure',
276
308
  'against what the tree actually counts.',
277
309
  '',
278
- 'Catalogs read: skills, governance rules, standards, CLI commands,',
310
+ 'Catalogs read: skills, governance rules, CLI commands,',
279
311
  'and audit ids. Closed rather than derived, so widening the',
280
312
  'set is a deliberate change rather than a side effect of a new list',
281
313
  'command shipping elsewhere.',
@@ -409,10 +441,6 @@ export function register(program: Command): void {
409
441
  'present and the question answerable.',
410
442
  '',
411
443
  'Forms read, with where each resolves:',
412
- ' standard canon standards <name> , against standards/ alone, which',
413
- ' is where the verb itself looks. internal/standards/ is not',
414
- ' tried, since a name resolving only there refuses for the',
415
- ' session that opens the citation.',
416
444
  ' path a backticked path carrying a directory segment and a file',
417
445
  ' extension, against the root',
418
446
  " sibling a bare <nnn>-<slug>.md , against the citing rule's folder",
@@ -422,7 +450,7 @@ export function register(program: Command): void {
422
450
  ' naming a file, such as canon/context/<domain>.md or app/**/route.ts',
423
451
  ' a bare filename naming a convention, such as route.ts or manifest.toml',
424
452
  ' a span carrying no file extension, which is a folder or a module',
425
- ' specifier, such as src/pages/ , next/font , or claude/standards',
453
+ ' specifier, such as src/pages/ , next/font , or claude/skills',
426
454
  ' a fenced block, which displays a path rather than pointing at one',
427
455
  '',
428
456
  'Exempt, reported by name rather than dropped:',
@@ -1070,6 +1098,7 @@ function runList(opts: ListOptions): number {
1070
1098
  ...(sections.stacks ? { stacks: catalog.stacks } : {}),
1071
1099
  ...(sections.rules ? { rules: catalog.rules } : {}),
1072
1100
  unreferenced: catalog.unreferenced,
1101
+ staleDetect: catalog.staleDetect,
1073
1102
  })}\n`,
1074
1103
  )
1075
1104
  return 0
@@ -1097,6 +1126,51 @@ function runList(opts: ListOptions): number {
1097
1126
  return 0
1098
1127
  }
1099
1128
 
1129
+ interface DetectOptions {
1130
+ readonly stack?: string
1131
+ readonly json?: boolean
1132
+ }
1133
+
1134
+ async function runDetect(target: string, opts: DetectOptions): Promise<number> {
1135
+ const resolved = resolve(target)
1136
+ if (!existsSync(resolved)) {
1137
+ logError(`Target not found: ${target}`)
1138
+ return 1
1139
+ }
1140
+
1141
+ const result = await detectRules(PROJECT_ROOT, resolved, opts.stack)
1142
+ if (!result.ok) {
1143
+ logError(`Stack not found: ${result.missingStack}`)
1144
+ return 1
1145
+ }
1146
+
1147
+ if (opts.json) {
1148
+ process.stdout.write(
1149
+ `${JSON.stringify({
1150
+ target: resolved,
1151
+ stack: opts.stack ?? null,
1152
+ proposals: result.proposals,
1153
+ })}\n`,
1154
+ )
1155
+ return 0
1156
+ }
1157
+
1158
+ intro('canon gov detect')
1159
+ if (result.proposals.length === 0) {
1160
+ logInfo('No opt-in rule matched this target')
1161
+ } else {
1162
+ logStep('Proposed rules')
1163
+ for (const proposal of result.proposals) {
1164
+ const reasons = proposal.evidence
1165
+ .map((evidence) => `${evidence.match} in ${evidence.path}`)
1166
+ .join(', ')
1167
+ logInfo(`${proposal.rule}: ${reasons}`)
1168
+ }
1169
+ }
1170
+ outro()
1171
+ return 0
1172
+ }
1173
+
1100
1174
  /**
1101
1175
  * Silent on success so the consumed-copy stage that calls it stays quiet. It is
1102
1176
  * the only work that stage does now. The installed set is readable on disk, so
@@ -1171,6 +1245,11 @@ async function runInstall(
1171
1245
  ): Promise<number> {
1172
1246
  intro('canon gov install')
1173
1247
 
1248
+ const add = opts.add ?? ''
1249
+ if (add !== '' && isAddWithoutStack(stack, target)) {
1250
+ return installExtras(add, stack ?? target)
1251
+ }
1252
+
1174
1253
  const resolved = resolveTarget(target, PROJECT_ROOT)
1175
1254
  if (typeof resolved === 'number') return resolved
1176
1255
 
@@ -1194,7 +1273,6 @@ async function runInstall(
1194
1273
  return 1
1195
1274
  }
1196
1275
 
1197
- const add = opts.add ?? ''
1198
1276
  const rules =
1199
1277
  add === '' ? resolution.rules : mergeExtraRules(resolution.rules, add)
1200
1278
 
@@ -1247,6 +1325,67 @@ async function runInstall(
1247
1325
  return 0
1248
1326
  }
1249
1327
 
1328
+ /**
1329
+ * Whether `--add` runs with no stack under it. Commander fills the first
1330
+ * positional before the second, so `--add a,b ../my-app` arrives with the
1331
+ * target in `stack` and the default `.` in `target`. A first positional that
1332
+ * names a stack keeps today's meaning, so a folder named `node` reads as the
1333
+ * stack, and `.` or a path carrying a slash is how a caller forces the target.
1334
+ */
1335
+ function isAddWithoutStack(stack: string | undefined, target: string): boolean {
1336
+ if (stack === undefined) return true
1337
+ if (target !== '.') return false
1338
+ if (stack === '.' || stack.includes('/')) return true
1339
+ return !govStackExists(PROJECT_ROOT, stack)
1340
+ }
1341
+
1342
+ /**
1343
+ * Installs `--add` names alone. The chain stamp is left as it was, since the
1344
+ * extras are not a stack, and overwriting the chain would cost a target the
1345
+ * missing-rule report its stack install recorded.
1346
+ */
1347
+ async function installExtras(add: string, target: string): Promise<number> {
1348
+ const resolved = resolveTarget(target, PROJECT_ROOT)
1349
+ if (typeof resolved === 'number') return resolved
1350
+
1351
+ logStep('Resolving extras with no stack')
1352
+
1353
+ const { found, missing } = lookupRules(PROJECT_ROOT, resolveExtraRules(add))
1354
+ for (const rule of missing) logWarn(`${rule} (source not found, skipping)`)
1355
+ for (const source of found) logInfo(source.rule)
1356
+
1357
+ if (found.length === 0) {
1358
+ logError('No named rule resolved, so nothing was installed.')
1359
+ outro()
1360
+ return 1
1361
+ }
1362
+
1363
+ const shouldInstall = await select({
1364
+ message: `Install ${found.length} rules to ${displayPath(target, CANON_RULES_REL)}?`,
1365
+ options: [
1366
+ { value: true, label: 'Yes' },
1367
+ { value: false, label: 'No' },
1368
+ ],
1369
+ nonInteractiveDefault: true,
1370
+ })
1371
+
1372
+ if (!shouldInstall) {
1373
+ logWarn('Cancelled')
1374
+ outro()
1375
+ return 0
1376
+ }
1377
+
1378
+ logStep('Installing rules')
1379
+ for (const rel of await installRules(found, resolved)) logAdd(rel)
1380
+
1381
+ await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, new Date())
1382
+
1383
+ const { GREEN, NC } = palette(process.stderr)
1384
+ outro()
1385
+ process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
1386
+ return 0
1387
+ }
1388
+
1250
1389
  /**
1251
1390
  * Emits the rules payload the `gov-*` skills read. The output lands in the
1252
1391
  * target's scratch directory rather than stdout because the file is the
@@ -40,7 +40,7 @@ export function register(program: Command): void {
40
40
  'Examples:',
41
41
  ' canon init',
42
42
  ' canon init ../my-app',
43
- ' canon init --stack astro --add 260-shadcn ../my-app',
43
+ ' canon init --stack node --add 210-astro,260-shadcn ../my-app',
44
44
  ' canon init --skip governance ../my-app',
45
45
  '',
46
46
  ].join('\n'),
@@ -10,7 +10,6 @@ import { checkTitleFormat, type TitleFormatIssue } from '@/labels/format'
10
10
  import { MAP_REL } from '@/labels/map'
11
11
  import { isReleasePullRequest, scanPhaseLabels } from '@/labels/phase'
12
12
  import { checkSections } from '@/labels/sections'
13
- import { scanTitleSpelling } from '@/labels/spelling'
14
13
  import { BAN_SETS } from '@/markdown/bans'
15
14
  import { bodyLines, scanBans } from '@/markdown/scan'
16
15
  import { intro, logInfo, logStep, logWarn, outro, plural } from '@/cli/ui'
@@ -169,7 +168,7 @@ export function register(program: Command): void {
169
168
  labels
170
169
  .command('scan')
171
170
  .description(
172
- 'Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a board identifier, a session link, or a title with an unspelled word',
171
+ 'Fail a pull request or a posted review whose title, body, or review comment carries a phase label, a board identifier, or a session link',
173
172
  )
174
173
  .helpOption('-h, --help', 'Show this help message')
175
174
  .option(
@@ -188,7 +187,7 @@ export function register(program: Command): void {
188
187
  'after',
189
188
  [
190
189
  '',
191
- 'Reads canon standards versioning for the two namespaces and sorts every',
190
+ 'Reads the git-commit versioning reference for the two namespaces and sorts every',
192
191
  'version-shaped token this pull request carries into the one the pull',
193
192
  'request is allowed to hold. A release-please pull request, read off its',
194
193
  'own fixed head branch and title, may carry semver references. Every',
@@ -212,13 +211,6 @@ export function register(program: Command): void {
212
211
  'session and for no other reader, which no clone repairs, so it is read',
213
212
  'on a release pull request too, where the board identifier is not.',
214
213
  '',
215
- "It also spell-checks the title alone against this repository's own",
216
- 'cspell config, since release-please copies the title into',
217
- 'CHANGELOG.md and nothing else spell-checks it first. The check shells',
218
- "this repository's own resolved cspell binary and reports nothing when",
219
- 'a target project carries none, rather than reaching the network or',
220
- 'forcing a new dependency.',
221
- '',
222
214
  "It also grades the title alone against claude/skills/git-pr/references/pr.md's ## Title",
223
215
  'section: the `<type>(<scope>)[!]: <subject>` structure, lowercase casing',
224
216
  'for the type, the scope, and the first subject word, and a 72-',
@@ -234,11 +226,11 @@ export function register(program: Command): void {
234
226
  'request or a review, outside a code span or a link.',
235
227
  '',
236
228
  'Exit codes:',
237
- ' 0 none of the seven found',
229
+ ' 0 none of the six found',
238
230
  ' 1 refused, with the reason on stderr or in the JSON record',
239
231
  ' 2 the title or body carries a phase label, a board identifier, a',
240
232
  ' session link, a banned character, a heading outside the closed',
241
- ' list or a required one missing, a title word no dictionary holds, or a title',
233
+ ' list or a required one missing, or a title',
242
234
  " breaking claude/skills/git-pr/references/pr.md's format, casing, or length",
243
235
  '',
244
236
  'Examples:',
@@ -532,7 +524,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
532
524
  }
533
525
 
534
526
  const result = scanPhaseLabels(resolved)
535
- const spelling = await scanTitleSpelling(resolved.title, process.cwd())
536
527
  // A scan given no title, whatever produced that absence, has nothing to
537
528
  // grade and would otherwise fail as `structure` for the wrong reason.
538
529
  const titleFormat =
@@ -579,34 +570,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
579
570
  for (const link of result.sessionLinks) logWarn(link)
580
571
  }
581
572
 
582
- const spellingChecked = spelling.kind === 'checked'
583
- const unspelledWords =
584
- spelling.kind === 'checked' ? spelling.unknownWords : []
585
-
586
- logStep(
587
- spelling.kind === 'unavailable'
588
- ? 'Spelling unavailable'
589
- : unspelledWords.length === 0
590
- ? 'Clean'
591
- : 'Unspelled word found',
592
- )
593
- if (spelling.kind === 'unavailable' && spelling.reason === 'no-binary') {
594
- logInfo(
595
- `no node_modules/.bin/cspell resolved walking up from ${spelling.probedFrom}, so the title was not checked`,
596
- )
597
- } else if (spelling.kind === 'unavailable') {
598
- logInfo(
599
- `cspell did not exit clean or with issues found (${spelling.message}), so the title was not checked`,
600
- )
601
- } else if (unspelledWords.length === 0) {
602
- logInfo('no word in the title is absent from every dictionary')
603
- } else {
604
- logWarn(
605
- `${plural(unspelledWords.length, 'word')} in the title absent from every dictionary. Fix the spelling, or add jargon and project-specific terms to .cspell/project-terms.txt and dependency vocabulary to .cspell/tech-stack.txt.`,
606
- )
607
- for (const word of unspelledWords) logWarn(word)
608
- }
609
-
610
573
  const titleFormatIssues = titleFormat?.issues ?? []
611
574
 
612
575
  // A review carries its own headings, a release-please body opens on a
@@ -707,8 +670,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
707
670
  semverTags: result.semverTags,
708
671
  boardReferences: result.boardReferences,
709
672
  sessionLinks: result.sessionLinks,
710
- unspelledWords,
711
- spellingChecked,
712
673
  titleFormatIssues,
713
674
  sections: { checked: sectionsChecked, ...sections },
714
675
  bannedCharacters,
@@ -719,7 +680,6 @@ async function runScan(opts: ScanOptions): Promise<number> {
719
680
  return result.phaseLabels.length === 0 &&
720
681
  result.boardReferences.length === 0 &&
721
682
  result.sessionLinks.length === 0 &&
722
- unspelledWords.length === 0 &&
723
683
  titleFormatIssues.length === 0 &&
724
684
  sections.outside.length === 0 &&
725
685
  sections.missing.length === 0 &&
@@ -1124,7 +1124,7 @@ export function register(program: Command): void {
1124
1124
  'repoints every citation that names one, live or archived.',
1125
1125
  '',
1126
1126
  'A promoted folder is one a durable record cites and no source file',
1127
- 'under claude/, scripts/, src/, governance/, or standards/ names by',
1127
+ 'under claude/, scripts/, src/, or governance/ names by',
1128
1128
  'path. A folder a script still writes into stays in scratch, since',
1129
1129
  'moving it needs a code change first.',
1130
1130
  '',