@erclx/canon 5.34.0 → 5.36.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 (248) hide show
  1. package/README.md +15 -17
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/REQUIREMENT.md +1 -1
  4. package/claude/skills/auto-ship/references/draft-mark.md +1 -1
  5. package/claude/skills/canon-cli/REQUIREMENT.md +2 -2
  6. package/claude/skills/canon-cli/SKILL.md +12 -12
  7. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  8. package/claude/skills/canvas-board/SKILL.md +2 -2
  9. package/claude/skills/canvas-board/references/design.md +53 -36
  10. package/claude/skills/codebase-layout/REQUIREMENT.md +1 -1
  11. package/claude/skills/codebase-layout/SKILL.md +2 -2
  12. package/claude/skills/create-rule/REQUIREMENT.md +6 -8
  13. package/claude/skills/create-rule/SKILL.md +8 -8
  14. package/claude/skills/create-rule/references/rule.md +11 -23
  15. package/claude/skills/create-skill/SKILL.md +2 -1
  16. package/claude/skills/create-skill/references/skill-paths.md +12 -15
  17. package/{standards/standard.md → claude/skills/create-skill/references/skill-reference.md} +50 -45
  18. package/claude/skills/create-skill/references/skill.md +9 -3
  19. package/claude/skills/design-taste/REQUIREMENT.md +6 -6
  20. package/claude/skills/design-taste/SKILL.md +2 -2
  21. package/claude/skills/design-taste/references/craft.md +1 -1
  22. package/claude/skills/design-taste/references/preflight.md +2 -2
  23. package/claude/skills/design-taste/references/tells.md +1 -1
  24. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  25. package/claude/skills/draft-identity/SKILL.md +3 -2
  26. package/claude/skills/git-commit/references/versioning.md +13 -13
  27. package/claude/skills/git-pr/SKILL.md +1 -1
  28. package/claude/skills/git-pr/references/publish.md +1 -1
  29. package/claude/skills/markdown-craft/references/markdown.md +3 -2
  30. package/claude/skills/plan-feature/SKILL.md +1 -1
  31. package/claude/skills/review-address/REQUIREMENT.md +1 -1
  32. package/claude/skills/review-address/SKILL.md +2 -2
  33. package/claude/skills/review-craft/references/ui.md +1 -1
  34. package/claude/skills/review-pr/SKILL.md +3 -3
  35. package/claude/skills/review-pr/references/post.md +1 -1
  36. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  37. package/claude/skills/review-ui/SKILL.md +1 -1
  38. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
  39. package/claude/skills/role-orchestrator/SKILL.md +11 -12
  40. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +3 -1
  41. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +11 -8
  42. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +10 -4
  43. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +2 -2
  44. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -12
  45. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +12 -15
  46. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +6 -4
  47. package/claude/skills/role-orchestrator/scripts/poll.ts +50 -1
  48. package/claude/skills/role-reviewer/REQUIREMENT.md +17 -7
  49. package/claude/skills/role-reviewer/SKILL.md +17 -10
  50. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  51. package/claude/skills/role-worker/SKILL.md +6 -6
  52. package/claude/skills/session-compact/references/handoff-note.md +1 -1
  53. package/claude/skills/target-setup/SKILL.md +2 -2
  54. package/claude/skills/target-setup/references/detect.md +3 -3
  55. package/claude/skills/test-craft/SKILL.md +2 -2
  56. package/claude/skills/ux-audit/REQUIREMENT.md +1 -1
  57. package/claude/skills/ux-audit/SKILL.md +2 -2
  58. package/claude/skills/ux-measure/REQUIREMENT.md +1 -1
  59. package/claude/skills/write-human/references/machine-tells.md +12 -0
  60. package/docs/agents/audits.md +1 -1
  61. package/docs/agents/commands.md +14 -13
  62. package/docs/agents/comments.md +1 -1
  63. package/docs/agents/counts.md +5 -6
  64. package/docs/agents/drift-surfaces.md +15 -19
  65. package/docs/agents/gov-detect.md +1 -1
  66. package/docs/agents/index.md +2 -3
  67. package/docs/agents/install-and-sync.md +10 -34
  68. package/docs/agents/markdown-audit.md +22 -12
  69. package/docs/agents/migrate.md +1 -1
  70. package/docs/agents/output-shape.md +1 -1
  71. package/docs/agents/overview.md +2 -2
  72. package/docs/agents/records-stale.md +1 -1
  73. package/docs/agents/records-validate.md +11 -23
  74. package/docs/agents/records.md +1 -1
  75. package/docs/agents/routing.md +1 -1
  76. package/docs/agents/rule-citations.md +9 -17
  77. package/docs/agents/scripting.md +2 -20
  78. package/docs/agents/state-scoped-risk.md +1 -1
  79. package/docs/agents/tasks-validate.md +1 -1
  80. package/docs/agents/teach.md +1 -1
  81. package/docs/target/index.md +1 -0
  82. package/docs/target/migrations.md +4 -0
  83. package/docs/target/projects.md +10 -10
  84. package/docs/target/scenarios.md +5 -6
  85. package/docs/target/standards-moved.md +32 -0
  86. package/docs/target/sync.md +5 -5
  87. package/docs/workflow/ai-workflow.md +1 -1
  88. package/docs/workflow/operating-model.md +16 -18
  89. package/docs/workflow/skill-map.md +1 -1
  90. package/docs/workflow/visual-design-workflow.md +3 -3
  91. package/governance/detect.toml +32 -36
  92. package/governance/renames.toml +44 -1
  93. package/governance/rules/lib/{305-e2e-reliability.md → e2e-reliability.md} +1 -1
  94. package/governance/rules/lib/{306-test-scope.md → test-scope.md} +2 -2
  95. package/governance/rules/ui/{410-a11y.md → a11y.md} +1 -1
  96. package/governance/rules/ui/{440-surface-capture.md → surface-capture.md} +3 -3
  97. package/governance/stacks/base.toml +15 -8
  98. package/governance/stacks/go.toml +2 -0
  99. package/governance/stacks/node.toml +1 -1
  100. package/governance/stacks/php.toml +2 -0
  101. package/governance/stacks/python.toml +1 -1
  102. package/package.json +1 -4
  103. package/scripts/core/install-check.sh +4 -10
  104. package/src/audits/catalog.ts +0 -9
  105. package/src/capture/frame-values.ts +3 -6
  106. package/src/claude/routing.ts +1 -1
  107. package/src/claude/skills-reach.ts +4 -6
  108. package/src/cli/help-all.ts +96 -0
  109. package/src/cli/help.ts +29 -29
  110. package/src/cli.ts +8 -3
  111. package/src/commands/claude.ts +29 -0
  112. package/src/commands/design.ts +22 -1
  113. package/src/commands/gov.ts +29 -11
  114. package/src/commands/init.ts +10 -1
  115. package/src/commands/labels.ts +4 -44
  116. package/src/commands/markdown.ts +96 -3
  117. package/src/commands/migrate.ts +1 -1
  118. package/src/commands/records.ts +5 -21
  119. package/src/commands/sync.ts +11 -0
  120. package/src/commands/tooling.ts +10 -4
  121. package/src/counts/catalogs.ts +0 -6
  122. package/src/design/budget.ts +14 -10
  123. package/src/design/document.ts +5 -5
  124. package/src/design/parse.ts +21 -4
  125. package/src/design/render.ts +3 -3
  126. package/src/gate/boundaries.ts +2 -2
  127. package/src/gate/measures.ts +7 -29
  128. package/src/gate/sequencer.ts +51 -2
  129. package/src/gate/stages.ts +0 -18
  130. package/src/gov/citations.ts +16 -71
  131. package/src/gov/stacks.ts +3 -3
  132. package/src/inventory/subjects.ts +1 -1
  133. package/src/markdown/bans.ts +1 -1
  134. package/src/markdown/ceiling.ts +6 -1
  135. package/src/markdown/colons.ts +86 -0
  136. package/src/markdown/gate.ts +15 -0
  137. package/src/markdown/heading-case.ts +74 -0
  138. package/src/migrate/scratch-evidence.ts +2 -2
  139. package/src/records/validate.ts +6 -194
  140. package/src/sandbox/provision.ts +0 -5
  141. package/src/secrets/shipped.ts +1 -2
  142. package/src/serve/static.ts +1 -2
  143. package/src/shipped/references.ts +42 -93
  144. package/src/sync/check.ts +18 -14
  145. package/src/sync/copy.ts +4 -5
  146. package/src/tooling/inject.ts +6 -37
  147. package/src/tooling/read.ts +2 -3
  148. package/src/tooling/scan.ts +4 -17
  149. package/src/tooling/subfolder.ts +2 -66
  150. package/tooling/astro/manifest.toml +0 -1
  151. package/tooling/astro/reference.md +0 -8
  152. package/tooling/base/configs/scripts/verify.json +1 -1
  153. package/tooling/base/configs/scripts/verify.ts +22 -17
  154. package/tooling/base/manifest.toml +7 -7
  155. package/tooling/base/reference.md +19 -32
  156. package/tooling/base/seeds/.github/workflows/verify.yml +0 -9
  157. package/tooling/base/seeds/.lintstagedrc +2 -4
  158. package/tooling/base/seeds/canon/context/ci.md +4 -6
  159. package/tooling/base/seeds/canon/context/development.md +2 -2
  160. package/tooling/claude/reference.md +1 -1
  161. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +13 -1
  162. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
  163. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +27 -2
  164. package/tooling/claude/seeds/CLAUDE.md +1 -1
  165. package/tooling/claude/seeds/canon/DESIGN.md +24 -37
  166. package/tooling/go/reference.md +1 -1
  167. package/tooling/php/reference.md +1 -1
  168. package/tooling/python/reference.md +2 -2
  169. package/tooling/shell/configs/.github/workflows/shell.yml +30 -0
  170. package/tooling/shell/manifest.toml +12 -0
  171. package/tooling/shell/reference.md +46 -0
  172. package/tooling/vite-react/manifest.toml +0 -1
  173. package/tooling/vite-react/reference.md +0 -8
  174. package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -5
  175. package/tooling/web/configs/.github/workflows/verify.yml +0 -9
  176. package/tooling/web/manifest.toml +1 -2
  177. package/tooling/web/reference.md +4 -6
  178. package/docs/agents/standards-audit.md +0 -40
  179. package/governance/rules/tooling/765-spelling.md +0 -19
  180. package/governance/stacks/astro.toml +0 -2
  181. package/governance/stacks/nextjs.toml +0 -2
  182. package/governance/stacks/node-server.toml +0 -2
  183. package/governance/stacks/python-fastapi.toml +0 -2
  184. package/governance/stacks/react.toml +0 -2
  185. package/src/commands/standards.ts +0 -278
  186. package/src/labels/spelling.ts +0 -120
  187. package/src/standards/audit.ts +0 -146
  188. package/src/standards/list.ts +0 -33
  189. package/src/standards/read.ts +0 -177
  190. package/src/standards/scope.ts +0 -56
  191. package/standards/index.md +0 -10
  192. package/tooling/base/configs/.vscode/extensions.json +0 -9
  193. package/tooling/base/configs/.vscode/settings.json +0 -3
  194. package/tooling/base/configs/scripts/verify.sh +0 -2
  195. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  196. package/tooling/base/seeds/.cspell/tech-stack.txt +0 -25
  197. package/tooling/base/seeds/cspell.json +0 -20
  198. package/tooling/go/seeds/.cspell/tech-stack.txt +0 -16
  199. package/tooling/nextjs/seeds/.cspell/tech-stack.txt +0 -3
  200. package/tooling/php/seeds/.cspell/tech-stack.txt +0 -5
  201. package/tooling/python/seeds/.cspell/tech-stack.txt +0 -18
  202. package/tooling/vite-react/seeds/.cspell/project-terms.txt +0 -1
  203. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +0 -2
  204. package/tooling/web/configs/.vscode/extensions.json +0 -13
  205. package/tooling/web/configs/.vscode/settings.json +0 -10
  206. package/tooling/web/seeds/.cspell/tech-stack.txt +0 -21
  207. /package/governance/rules/canon/{606-git.md → git.md} +0 -0
  208. /package/governance/rules/canon/{611-indexes.md → indexes.md} +0 -0
  209. /package/governance/rules/canon/{604-scratch.md → scratch.md} +0 -0
  210. /package/governance/rules/canon/{602-tasks.md → task-scope.md} +0 -0
  211. /package/governance/rules/canon/{605-worktrees.md → worktrees.md} +0 -0
  212. /package/governance/rules/claude/{566-output.md → output.md} +0 -0
  213. /package/governance/rules/code/{000-code.md → routing.md} +0 -0
  214. /package/governance/rules/code/{010-testing.md → testing.md} +0 -0
  215. /package/governance/rules/framework/{210-astro.md → astro.md} +0 -0
  216. /package/governance/rules/framework/{220-fastapi.md → fastapi.md} +0 -0
  217. /package/governance/rules/framework/{230-nextjs.md → nextjs.md} +0 -0
  218. /package/governance/rules/framework/{200-react.md → react.md} +0 -0
  219. /package/governance/rules/framework/{260-shadcn.md → shadcn.md} +0 -0
  220. /package/governance/rules/framework/{250-tailwind.md → tailwind.md} +0 -0
  221. /package/governance/rules/lang/{130-go.md → go.md} +0 -0
  222. /package/governance/rules/lang/{140-php.md → php.md} +0 -0
  223. /package/governance/rules/lang/{110-python.md → python.md} +0 -0
  224. /package/governance/rules/lang/{100-typescript.md → typescript.md} +0 -0
  225. /package/governance/rules/lib/{370-database.md → database.md} +0 -0
  226. /package/governance/rules/lib/{340-pydantic.md → pydantic.md} +0 -0
  227. /package/governance/rules/lib/{360-security-server.md → security-server.md} +0 -0
  228. /package/governance/rules/lib/{350-security-web.md → security-web.md} +0 -0
  229. /package/governance/rules/lib/{320-tanstack-query.md → tanstack-query.md} +0 -0
  230. /package/governance/rules/lib/{335-testing-go.md → testing-go.md} +0 -0
  231. /package/governance/rules/lib/{336-testing-php.md → testing-php.md} +0 -0
  232. /package/governance/rules/lib/{330-testing-py.md → testing-py.md} +0 -0
  233. /package/governance/rules/lib/{300-testing-ts.md → testing-ts.md} +0 -0
  234. /package/governance/rules/lib/{310-zod.md → zod.md} +0 -0
  235. /package/governance/rules/tooling/{700-ci-workflow.md → ci-workflow.md} +0 -0
  236. /package/governance/rules/tooling/{790-code-comments.md → code-comments.md} +0 -0
  237. /package/governance/rules/tooling/{780-config-comments.md → config-comments.md} +0 -0
  238. /package/governance/rules/tooling/{775-dependencies.md → dependencies.md} +0 -0
  239. /package/governance/rules/ui/{480-canvas.md → canvas.md} +0 -0
  240. /package/governance/rules/ui/{400-ui.md → copy.md} +0 -0
  241. /package/governance/rules/ui/{460-design-taste.md → design-taste.md} +0 -0
  242. /package/governance/rules/ui/{420-forms.md → forms.md} +0 -0
  243. /package/governance/rules/ui/{465-interface-casing.md → interface-casing.md} +0 -0
  244. /package/governance/rules/ui/{450-link-behavior.md → link-behavior.md} +0 -0
  245. /package/governance/rules/ui/{470-motion.md → motion.md} +0 -0
  246. /package/governance/rules/ui/{430-ux-completeness.md → ux-completeness.md} +0 -0
  247. /package/tooling/{base → shell}/configs/.editorconfig +0 -0
  248. /package/tooling/{base → shell}/configs/.shellcheckrc +0 -0
@@ -7,7 +7,6 @@ import {
7
7
  recordDir as resolveRecordDir,
8
8
  recordDirs as resolveRecordDirs,
9
9
  } from '@/roots/record'
10
- import { ATTRIBUTE_MARKER, governedPaths, readScope } from '@/standards/scope'
11
10
  import {
12
11
  TEACH_GLOSSARY,
13
12
  TEACH_MISSION,
@@ -18,34 +17,10 @@ import {
18
17
  WORKSPACE_NAME,
19
18
  } from '@/teach/workspace'
20
19
 
21
- export const RECORD_KINDS = [
22
- 'plans',
23
- 'groundwork',
24
- 'intake',
25
- 'standards',
26
- 'teach',
27
- ] as const
20
+ export const RECORD_KINDS = ['plans', 'groundwork', 'intake', 'teach'] as const
28
21
 
29
22
  export type RecordKind = (typeof RECORD_KINDS)[number]
30
23
 
31
- /**
32
- * The folders standards reads, in precedence order.
33
- *
34
- * It carries two because the corpus authors at the project root and installs
35
- * under `.claude/`. The authoring root wins where both exist, since the
36
- * installed tree is a generated copy here and a finding fixed there is
37
- * overwritten by the next regen. A project that consumed the corpus holds only
38
- * the second, so one order serves both.
39
- *
40
- * Every other kind is a session record and takes the record roots instead,
41
- * resolved by `@/roots/record`. Standards is the one kind that is tracked, so it
42
- * does not move and spells its own two candidates here.
43
- */
44
- const STANDARDS_FOLDERS: readonly string[] = [
45
- 'standards',
46
- join('.claude', 'standards'),
47
- ]
48
-
49
24
  /**
50
25
  * `unknown-kind` is raised at the argument boundary rather than by the walk, and
51
26
  * it sits here because both reach a caller through the same `reason` field. A
@@ -60,7 +35,6 @@ export const FINDING_KINDS = [
60
35
  'name-malformed',
61
36
  'title-missing',
62
37
  'section-missing',
63
- 'scope-unanchored',
64
38
  'entry-unreasoned',
65
39
  'suggestion-missing',
66
40
  'question-unanswerable',
@@ -103,10 +77,6 @@ export type ValidateOutcome = ValidateReport | ValidateRefused
103
77
 
104
78
  /** Every folder a kind would accept, whether or not it is on disk. */
105
79
  export function recordDirs(root: string, kind: RecordKind): string[] {
106
- if (kind === 'standards') {
107
- return STANDARDS_FOLDERS.map((folder) => join(root, folder))
108
- }
109
-
110
80
  return resolveRecordDirs(root, kind)
111
81
  }
112
82
 
@@ -114,19 +84,8 @@ export function recordDirs(root: string, kind: RecordKind): string[] {
114
84
  * The folder a kind reads. The first candidate on disk wins, and the creation
115
85
  * default stands in when none exists, so a refusal and a test fixture both name
116
86
  * the location the kind would be written to.
117
- *
118
- * The default is where this parts company with `dirs[0]`, which the record
119
- * kinds can no longer take: their first candidate is the root the move lands at
120
- * and nothing creates there yet, so a fallback reading it would name a folder no
121
- * verb would ever write. Standards has no such split, since it is tracked and
122
- * its first candidate is where it is authored.
123
87
  */
124
88
  export function recordsDir(root: string, kind: RecordKind): string {
125
- if (kind === 'standards') {
126
- const dirs = recordDirs(root, kind)
127
- return dirs.find((dir) => existsSync(dir)) ?? dirs[0]
128
- }
129
-
130
89
  return resolveRecordDir(root, kind)
131
90
  }
132
91
 
@@ -134,18 +93,6 @@ export function isRecordKind(value: string): value is RecordKind {
134
93
  return (RECORD_KINDS as readonly string[]).includes(value)
135
94
  }
136
95
 
137
- /**
138
- * Whether a kind's folder is shared session scratch at the main worktree root.
139
- *
140
- * The five record folders are, so every session validates the records every
141
- * other session reads. The corpus is tracked instead, so a linked worktree holds
142
- * its own edited copy, and defaulting that kind to the main root would report on
143
- * a tree the session never touched and say nothing about which one it read.
144
- */
145
- export function isSharedScratch(kind: RecordKind): boolean {
146
- return kind !== 'standards'
147
- }
148
-
149
96
  const NONE_IDENTIFIED = 'None identified.'
150
97
  const NUMBERED_FILE = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
151
98
 
@@ -920,127 +867,6 @@ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
920
867
  ]
921
868
  }
922
869
 
923
- const STANDARD_INDEX = 'index.md'
924
- const STANDARD_FIELDS = ['title', 'description'] as const
925
-
926
- const DOES_NOT_GOVERN = 'Does not govern:'
927
-
928
- /**
929
- * The words a governed path offers a filename. Each segment gives its own word
930
- * and, where it carries a prefix or a placeholder, the parts either side of a
931
- * hyphen, so `.canon/tasks/session-<slug>.md` offers `tasks` and `session`.
932
- *
933
- * A dotted segment gives nothing. It names the folder holding the artifact
934
- * rather than the artifact, and a standard named for it would pass this check
935
- * while naming the container every sibling shares.
936
- */
937
- export function pathWords(path: string): string[] {
938
- const words: string[] = []
939
-
940
- for (const segment of path.split('/')) {
941
- if (segment.startsWith('.')) continue
942
-
943
- const stem = segment.replace(/\.[a-z]+$/i, '').toLowerCase()
944
- words.push(stem)
945
- if (stem.includes('-')) words.push(...stem.split('-'))
946
- }
947
-
948
- return words.filter((word) => /^[a-z]+$/.test(word))
949
- }
950
-
951
- /**
952
- * Accepts the singular and the plural of one word. A standard over a single
953
- * document is named for the document and one over a folder of them is named for
954
- * either, and picking a side would report a conforming half of the corpus.
955
- */
956
- function namesWord(stem: string, word: string): boolean {
957
- return stem === word || `${stem}s` === word || stem === `${word}s`
958
- }
959
-
960
- function checkStandardName(name: string, statement: string): Finding[] {
961
- const paths = governedPaths(statement)
962
-
963
- // The marker is read only where the first sentence backticks nothing, which
964
- // is the catalog's own rule. A statement naming a path publishes that path
965
- // however the rest of the statement describes itself.
966
- if (paths.length === 0) {
967
- if (statement.includes(ATTRIBUTE_MARKER)) return []
968
-
969
- return [
970
- finding(
971
- 'scope-unanchored',
972
- name,
973
- name,
974
- 'backticks no path in its first scope sentence and does not call itself an attribute standard, so it names no artifact to be named for.',
975
- ),
976
- ]
977
- }
978
-
979
- const stem = name.replace(/\.md$/, '')
980
- const words = paths.flatMap(pathWords)
981
-
982
- if (words.some((word) => namesWord(stem, word))) return []
983
-
984
- return [
985
- finding(
986
- 'name-malformed',
987
- name,
988
- name,
989
- `names no part of ${paths.join(', ')}, which is what it governs. A rename reaches every target that installed the corpus and every surface citing it by bare filename.`,
990
- ),
991
- ]
992
- }
993
-
994
- export function checkStandard(name: string, text: string): Finding[] {
995
- const findings: Finding[] = []
996
- const frontmatter = parseFrontmatter(text)
997
-
998
- const missing = STANDARD_FIELDS.filter(
999
- (field) => !readField(frontmatter, field),
1000
- )
1001
-
1002
- if (missing.length > 0) {
1003
- findings.push(
1004
- finding(
1005
- 'frontmatter-incomplete',
1006
- name,
1007
- name,
1008
- `carries no ${missing.join(' and no ')}.`,
1009
- ),
1010
- )
1011
- }
1012
-
1013
- const scope = readScope(text)
1014
-
1015
- // The name derives from the scope statement, so an absent section leaves
1016
- // nothing to derive against. Reporting the name as well would name one defect
1017
- // twice and point the fix at the wrong file.
1018
- if (!scope) {
1019
- return [
1020
- ...findings,
1021
- finding(
1022
- 'section-missing',
1023
- name,
1024
- '## Scope',
1025
- 'is absent, so the standard claims no jurisdiction and can refuse no rule.',
1026
- ),
1027
- ]
1028
- }
1029
-
1030
- if (!scope.lines.some((line) => line.trim().startsWith(DOES_NOT_GOVERN))) {
1031
- findings.push(
1032
- finding(
1033
- 'section-missing',
1034
- name,
1035
- DOES_NOT_GOVERN,
1036
- 'is absent from the scope section, so no boundary names the owner it hands off to.',
1037
- ),
1038
- )
1039
- }
1040
-
1041
- return [...findings, ...checkStandardName(name, scope.statement)]
1042
- }
1043
-
1044
870
  /**
1045
871
  * The three kinds whose records are folders rather than files. Keyed by kind so
1046
872
  * a seventh arrives as an entry here and the compiler names the walk it owes,
@@ -1048,7 +874,7 @@ export function checkStandard(name: string, text: string): Finding[] {
1048
874
  */
1049
875
  const FOLDER_CHECK: Readonly<
1050
876
  Record<
1051
- Exclude<RecordKind, 'plans' | 'standards'>,
877
+ Exclude<RecordKind, 'plans'>,
1052
878
  (dir: string, slug: string) => Promise<Finding[]>
1053
879
  >
1054
880
  > = {
@@ -1066,9 +892,8 @@ async function validateFiles(
1066
892
  dir: string,
1067
893
  kind: RecordKind,
1068
894
  check: (name: string, text: string) => Finding[],
1069
- skip: (file: string) => boolean = () => false,
1070
895
  ): Promise<ValidateReport> {
1071
- const files = (await listMarkdown(dir)).filter((file) => !skip(file))
896
+ const files = await listMarkdown(dir)
1072
897
 
1073
898
  const perFile = await Promise.all(
1074
899
  files.map(async (file) =>
@@ -1080,12 +905,9 @@ async function validateFiles(
1080
905
  }
1081
906
 
1082
907
  /**
1083
- * Reports what every record in one folder claims against the shape its standard
1084
- * fixes. It writes nothing whichever kind runs, and the reason differs by kind.
1085
- * A session record is per-machine scratch with no history behind it, so a repair
1086
- * that guessed wrong could not be undone. A standard installs into every target
1087
- * and is cited by bare filename, so a rename costs more than the file move it
1088
- * looks like.
908
+ * Reports what every record in one folder claims against the shape its reference
909
+ * fixes. It writes nothing, since a session record is per-machine scratch with
910
+ * no history behind it, so a repair that guessed wrong could not be undone.
1089
911
  */
1090
912
  export async function validateRecords(
1091
913
  root: string,
@@ -1102,16 +924,6 @@ export async function validateRecords(
1102
924
 
1103
925
  if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
1104
926
 
1105
- // The walk stays flat, matching install and the catalog.
1106
- if (kind === 'standards') {
1107
- return validateFiles(
1108
- dir,
1109
- kind,
1110
- checkStandard,
1111
- (file) => file === STANDARD_INDEX,
1112
- )
1113
- }
1114
-
1115
927
  const folders = await listFolders(dir)
1116
928
  const check = FOLDER_CHECK[kind]
1117
929
  const perFolder = await Promise.all(
@@ -220,11 +220,6 @@ function install(
220
220
  fail(`Could not install ${label} into the sandbox.`)
221
221
  }
222
222
 
223
- /**
224
- * No standards install. The corpus installs into no target, so a scenario
225
- * driving a skill that reads one takes the `canon standards <name>` path a real
226
- * project takes.
227
- */
228
223
  function setupAssets(
229
224
  root: string,
230
225
  sandbox: string,
@@ -13,8 +13,7 @@ import { join } from 'node:path'
13
13
  *
14
14
  * What it does not cover is the plugin, which a marketplace install reads live
15
15
  * from `claude/` rather than from a tarball. That folder is a `files` entry
16
- * too, and its `standards` symlink resolves into a tree the field lists in its
17
- * own right, so both routes land inside the same corpus.
16
+ * too, so both routes land inside the same corpus.
18
17
  */
19
18
  export type ShipEntries =
20
19
  /** The field declares a corpus, which is what this check reads. */
@@ -200,8 +200,7 @@ export function resolveWithin(
200
200
  /**
201
201
  * Re-tests containment after following symlinks. `resolveWithin` is lexical
202
202
  * and `resolve` does not follow a link, so a link inside the root clears that
203
- * test while the file it points at sits outside. This repository is a live
204
- * instance, since `claude/standards` is a link out of `claude/`.
203
+ * test while the file it points at sits outside.
205
204
  *
206
205
  * Only a path that exists is checked, because a link can be followed only once
207
206
  * there is something on the other end, and a path that resolves to nothing is
@@ -10,8 +10,7 @@ import { isMarked } from '@/sweep/exempt-marker'
10
10
  * on purpose rather than by omission. What decides membership here is whether
11
11
  * anything serves the file to a reader who does not hold this repository: a
12
12
  * skill body loads into a session, a docs page is read through `canon docs`, a
13
- * standard through `canon standards`, a rule through a glob match, and a seed
14
- * through an install. A `src/` doc comment
13
+ * rule through a glob match, and a seed through an install. A `src/` doc comment
15
14
  * is read by someone working on the toolkit, in this checkout, where every
16
15
  * number resolves.
17
16
  *
@@ -19,18 +18,12 @@ import { isMarked } from '@/sweep/exempt-marker'
19
18
  * `src/design/base.css` and `src/design/tokens.ts` carry sixteen values shaped
20
19
  * `#191512`, which no width or boundary rule separates from a pull request
21
20
  * number, so telling them apart would need the line's syntactic position.
22
- *
23
- * `claude/standards` is a symlink into one of the roots below, and a walk that
24
- * followed it would read that corpus twice and report every finding in it
25
- * under two paths. Nothing is lost by not following, since it is a corpus here
26
- * in its own right.
27
21
  */
28
22
  export const SHIPPED_CORPORA = [
29
23
  'claude',
30
24
  'docs',
31
25
  'governance',
32
26
  'scripts',
33
- 'standards',
34
27
  'tooling',
35
28
  ] as const
36
29
 
@@ -61,7 +54,7 @@ export const REFERENCE_MARKER = 'canon-allow-reference'
61
54
  * already ships `anthropics/claude-code#58345` in exactly that form.
62
55
  *
63
56
  * The trailing boundary is what the first shape of this pattern lacked, and it
64
- * costs a live finding to omit. `governance/rules/framework/250-tailwind.md`
57
+ * costs a live finding to omit. `governance/rules/framework/tailwind.md`
65
58
  * writes `bg-[#316ff6]`, a Tailwind arbitrary hex color, which an unbounded
66
59
  * `#[0-9]+` reads as pull request 316. Requiring a non-word character after the
67
60
  * digits rejects it and rejects nothing real, since a citation is always
@@ -146,37 +139,15 @@ const SAME_REPOSITORY =
146
139
  const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
147
140
 
148
141
  /**
149
- * A bare `standards/<name>.md` citation from a body under `claude/skills/`,
150
- * which `598-authoring-layout.md` fixes as the broken form there:
151
- * `${CLAUDE_SKILL_DIR}` is what resolves off the `claude/standards` symlink
152
- * in every plugin cache, and a raw path has nothing to expand it.
153
- *
154
- * Scoped to that one corpus rather than every `isShippedCorpus` reads,
155
- * because `docs/agents/` and `docs/workflow/` carry the identical
156
- * `standards/<name>.md` shape correctly: a docs page resolves from this
157
- * checkout's own root rather than through a skill's `${CLAUDE_SKILL_DIR}`,
158
- * so the same string is the fix in one corpus and the defect in the other.
159
- * Widening the pattern to every corpus turns it into a mass false-positive
160
- * over the dozens of correct citations those two folders carry.
161
- *
162
- * The same leading-boundary discipline as `DOCS_PATH` excludes the already
163
- * correct form: a `/` sits ahead of `standards` in
164
- * `${CLAUDE_SKILL_DIR}/../../standards/<name>.md`, which the negative
165
- * lookbehind rejects, so a citation already rewritten to the resolving form
166
- * does not fail this check a second time.
142
+ * A call to `canon standards`, a verb no installed CLI carries any more. A
143
+ * shipped body naming it sends a target reader to an unknown-command error,
144
+ * so every corpus reports it, `docs/` included. A page saying the verb is gone
145
+ * carries the reference marker on that line.
167
146
  *
168
- * Reported unless `isStandardsPathReportable` reads the match as a
169
- * deliberate placeholder rather than a real citation. A body illustrating
170
- * the shape a project's own `standards/<name>.md` takes writes a bracketed
171
- * token this pattern also matches, such as `standards/<slug>.md`
172
- * in a skill that drafts one, which names no real file. A bare,
173
- * non-bracketed match now reports whether or not the file exists, unlike
174
- * `DOCS_PATH`, which still gates on existence: a same-repository citation
175
- * under this corpus names no target project's own tree the way a `docs/`
176
- * path can, so shape alone separates a placeholder from a broken citation
177
- * here with no false-positive class existence was catching.
147
+ * The trailing lookahead keeps a hyphenated token such as a `standards-audit`
148
+ * page name from matching, since that names a file rather than a call.
178
149
  */
179
- const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
150
+ const STANDARDS_CALL = /\bcanon standards(?![\w-])/g
180
151
 
181
152
  /**
182
153
  * A phase-label-shaped token: exactly two numeric groups, with a negative
@@ -200,20 +171,20 @@ const STANDARDS_PATH = /(?<![\w./-])standards\/[^\s`)\]]*\.md\b/g
200
171
  const PHASE_LABEL = /\bv\d+\.\d+(?!\.\d)\b/g
201
172
 
202
173
  /**
203
- * A `.claude/rules/<segments>/<nnn>-<slug>.md` citation from a body under
204
- * `claude/skills/`, the installed-path spelling `598-authoring-layout.md`
205
- * bans a shipped skill body from citing as authority for its own behavior.
174
+ * A `.claude/rules/<segments>/<slug>.md` citation from a body under
175
+ * `claude/skills/`, the installed-path spelling `authoring-layout.md` bans a
176
+ * shipped skill body from citing as authority for its own behavior.
206
177
  *
207
- * Anchored on the trailing `\d{3}-[\w-]+\.md` rather than on the bare
208
- * `.claude/rules/` prefix, which is what keeps a folder mention carrying no
209
- * number, such as `create-rule`, `target-check`, and `target-setup` already
210
- * write correctly, from matching. The segment group between `rules/` and the
211
- * numbered file admits both a governance-namespace path
212
- * (`canon/canon/604-scratch.md`) and a project-namespace one
213
- * (`project/<subdir>/<n>-<slug>.md`) without distinguishing them, since
214
- * either shape is the same broken citation.
178
+ * Anchored on a trailing `.md` file rather than on the bare `.claude/rules/`
179
+ * prefix, which is what keeps a folder mention, such as `create-rule`,
180
+ * `target-check`, and `target-setup` write, from matching: it ends in `/` or
181
+ * names no `.md`. The segments between `rules/` and the file admit both a
182
+ * governance-namespace path (`canon/canon/scratch.md`) and a
183
+ * project-namespace one (`project/<subdir>/<slug>.md`) without distinguishing
184
+ * them, since either shape is the same broken citation, and the placeholder
185
+ * check spares the bracketed illustration.
215
186
  */
216
- const RULE_PATH = /(?<![\w./-])\.claude\/rules\/[^\s`)\]]*\/\d{3}-[\w-]+\.md\b/g
187
+ const RULE_PATH = /(?<![\w./-])\.claude\/rules\/[^\s`)\]]*?[\w-]+\.md\b/g
217
188
 
218
189
  export interface ShippedReference {
219
190
  readonly file: string
@@ -223,7 +194,7 @@ export interface ShippedReference {
223
194
  | 'pull-request'
224
195
  | 'commit'
225
196
  | 'docs-path'
226
- | 'standards-path'
197
+ | 'retired-verb'
227
198
  | 'phase-label'
228
199
  | 'rule-path'
229
200
  /** The reference as written, so a report names the token to qualify. */
@@ -272,50 +243,31 @@ function isDocsPathReportable(
272
243
  }
273
244
 
274
245
  /**
275
- * Whether `file` sits in the one corpus `STANDARDS_PATH` and `RULE_PATH`
276
- * both gate: a shipped skill body, minus its own `REQUIREMENT.md`.
246
+ * Whether `file` sits in the one corpus `RULE_PATH` gates: a shipped skill
247
+ * body, minus its own `REQUIREMENT.md`.
277
248
  *
278
- * Both patterns share this scope because both bans live in
279
- * `598-authoring-layout.md`, stated for the same reader: a session loading
280
- * the `SKILL.md` body a target actually receives. `REQUIREMENT.md` is
281
- * excluded for the reason that file states there, since a maintainer or an
282
- * audit command reads it rather than a session loading it, so neither
283
- * resolver rule this pair enforces ever applies to it. One predicate serves
284
- * both call sites rather than two copies drifting apart with nothing
285
- * comparing them.
249
+ * The ban lives in `authoring-layout.md`, stated for one reader: a
250
+ * session loading the `SKILL.md` body a target actually receives.
251
+ * `REQUIREMENT.md` is excluded for the reason that file states there, since a
252
+ * maintainer or an audit command reads it rather than a session loading it,
253
+ * so the resolver rule never applies to it.
286
254
  */
287
255
  function isSkillBodyScope(file: string): boolean {
288
256
  return file.startsWith('claude/skills/') && !file.endsWith('/REQUIREMENT.md')
289
257
  }
290
258
 
291
- /**
292
- * Whether a `STANDARDS_PATH` match should be reported: every match that is
293
- * not a placeholder, whether or not the named file exists.
294
- *
295
- * Existence used to gate this the same way `isDocsPathReportable` gates its
296
- * corpus, which conflated a deliberate placeholder with a genuinely broken,
297
- * real-looking citation: both fail existence and both passed muted. A bare
298
- * `standards/<name>.md` names no target-project tree the way a `docs/` path
299
- * can, so shape alone separates the two classes here with no false-positive
300
- * class existence was catching.
301
- */
302
- function isStandardsPathReportable(path: string): boolean {
303
- return !isPlaceholderPath(path)
304
- }
305
-
306
259
  /**
307
260
  * Every reference in one shipped file that no marker mutes.
308
261
  *
309
262
  * The corpus walk is deliberately absent, which lets most of the shape be
310
263
  * tested against a string rather than against a fixture. That is the seam
311
264
  * `headingCitationsIn` draws in `src/claude/skills-headings.ts` and `citationsIn`
312
- * draws in `skills-reach.ts`. `DOCS_PATH` and `STANDARDS_PATH` are the two
313
- * patterns that still need a filesystem, since resolving against this
314
- * checkout is the only thing that separates a real citation from an
315
- * illustration for either, so `referencesIn` takes `root` as the one
316
- * caller-supplied exception to that rule. `root` is required rather than
317
- * defaulted, since a caller that dropped it silently would report zero
318
- * findings for both rather than raising, which is the wrong failure
265
+ * draws in `skills-reach.ts`. `DOCS_PATH` is the one pattern that still
266
+ * needs a filesystem, since resolving against this checkout is the only thing
267
+ * that separates a real citation from an illustration, so `referencesIn`
268
+ * takes `root` as the one caller-supplied exception to that rule. `root` is
269
+ * required rather than defaulted, since a caller that dropped it silently
270
+ * would report zero findings rather than raising, which is the wrong failure
319
271
  * direction for a gate.
320
272
  *
321
273
  * The unit is the match rather than the line, unlike those two, because one
@@ -375,16 +327,13 @@ export function referencesIn(
375
327
  })
376
328
  }
377
329
 
378
- if (isSkillBodyScope(file)) {
379
- for (const match of line.matchAll(STANDARDS_PATH)) {
380
- if (!isStandardsPathReportable(match[0])) continue
381
- references.push({
382
- file,
383
- line: index + 1,
384
- kind: 'standards-path',
385
- text: match[0],
386
- })
387
- }
330
+ for (const match of line.matchAll(STANDARDS_CALL)) {
331
+ references.push({
332
+ file,
333
+ line: index + 1,
334
+ kind: 'retired-verb',
335
+ text: match[0],
336
+ })
388
337
  }
389
338
 
390
339
  for (const match of line.matchAll(PHASE_LABEL)) {
package/src/sync/check.ts CHANGED
@@ -3,9 +3,10 @@ import { basename, join, sep } from 'node:path'
3
3
  import { execa } from 'execa'
4
4
  import { gitEnv } from '@/git/env'
5
5
  import { createDesignAdapter, DESIGN_INSTALL_DIR } from '@/design/adapter'
6
- import { createGovAdapter, rulesSourceDir } from '@/gov/adapter'
6
+ import { createGovAdapter } from '@/gov/adapter'
7
7
  import { successorResolver } from '@/gov/renames'
8
- import { loadGovStack, resolveMissingRules, resolveRules } from '@/gov/stacks'
8
+ import { lookupRules } from '@/gov/install'
9
+ import { resolveMissingRules, resolveRules } from '@/gov/stacks'
9
10
  import { planSync, type ScanEntry, type SyncAdapter } from '@/sync/engine'
10
11
  import {
11
12
  collectSuperseded,
@@ -576,7 +577,8 @@ export function readInstalledRules(
576
577
  * not already hold.
577
578
  *
578
579
  * The band test is the entitlement filter, and `bands` carries two sources: the
579
- * folders the target already holds, and the folders the base stack takes whole.
580
+ * folders the target already holds, and the folders holding a rule the base
581
+ * stack resolves to.
580
582
  * A rule authored under `lang/` or `ui/` is named by an individual stack, so it
581
583
  * belongs to some targets and not others, and listing every added file would
582
584
  * tell a base consumer about rules it was never entitled to. The test
@@ -615,27 +617,29 @@ export function selectNewRules(
615
617
  }
616
618
 
617
619
  /**
618
- * Band folders the base stack takes whole. Every governance stack extends base,
619
- * so a rule authored under one of these is entitled to every target.
620
+ * Band folders holding a rule the base stack resolves to. Every governance
621
+ * stack extends base, so a new rule authored beside one of base's rules is
622
+ * reported to every target the fallback below reads.
620
623
  *
621
624
  * This is what covers a band no target can carry yet. Entitlement is otherwise
622
- * read off folders the target already holds, and a folder added to base later
625
+ * read off folders the target already holds, and a band base reaches later
623
626
  * exists in no installed tree, so without this the rules inside it would reach
624
- * nobody. Read from the stack file rather than fixed, so that addition needs no
625
- * code change here.
627
+ * nobody. Read through `resolveRules` rather than off the stack's entries,
628
+ * since base names its rules one by one and an entry is a slug rather than a
629
+ * folder.
626
630
  *
627
- * A folder only a leaf stack names is deliberately absent. It is entitled to
631
+ * A folder only a leaf stack reaches is deliberately absent. It is entitled to
628
632
  * some targets and not others, which is the distinction the band test makes and
629
633
  * the installed tree is the only evidence of.
630
634
  */
631
635
  export function baseBands(root: string): Set<string> {
632
- const stack = loadGovStack(root, 'base')
633
- if (stack === undefined) return new Set()
636
+ const resolution = resolveRules(root, 'base')
637
+ if (!resolution.ok) return new Set()
634
638
 
635
639
  return new Set(
636
- stack.rules.filter((entry) =>
637
- isDirectory(join(rulesSourceDir(root), entry)),
638
- ),
640
+ lookupRules(root, resolution.rules)
641
+ .found.map((source) => source.subdir.split(sep)[0] ?? '')
642
+ .filter((band) => band !== ''),
639
643
  )
640
644
  }
641
645
 
package/src/sync/copy.ts CHANGED
@@ -4,11 +4,10 @@ import { dirname } from 'node:path'
4
4
  /**
5
5
  * Copies with `cp` semantics. Two behaviors matter and neither is the default
6
6
  * in the obvious alternatives. `Bun.write` drops the executable bit, which
7
- * would land husky hooks and `scripts/verify.sh` non-executable. Plain
8
- * `copyFile` imposes the source mode on an existing destination, so a file the
9
- * target deliberately restricted to 600, or an earlier stack set executable,
10
- * comes back at the source's mode. `cp` leaves an existing destination's mode
11
- * alone.
7
+ * would land husky hooks non-executable. Plain `copyFile` imposes the source
8
+ * mode on an existing destination, so a file the target deliberately
9
+ * restricted to 600, or an earlier stack set executable, comes back at the
10
+ * source's mode. `cp` leaves an existing destination's mode alone.
12
11
  *
13
12
  * Every domain that writes into a target routes through here. The tooling
14
13
  * inject path found the trap first, and the sync engine inherits it for every