@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
@@ -25,7 +25,6 @@ import {
25
25
  skillFamily,
26
26
  skillPracticeShape,
27
27
  skillProvenance,
28
- standardCriteria,
29
28
  unreferencedRules,
30
29
  visualPathGlobs,
31
30
  } from '@/gate/measures'
@@ -502,11 +501,6 @@ export const STAGES: readonly Stage[] = [
502
501
  label: 'Skill family',
503
502
  checks: [{ kind: 'measure', measure: skillFamily }],
504
503
  },
505
- {
506
- id: 'standard-criteria',
507
- label: 'Standard success criteria',
508
- checks: [{ kind: 'measure', measure: standardCriteria }],
509
- },
510
504
  {
511
505
  id: 'sandbox-coverage',
512
506
  label: 'Sandbox coverage',
@@ -522,18 +516,6 @@ export const STAGES: readonly Stage[] = [
522
516
  label: 'Plugin manifests',
523
517
  checks: [{ kind: 'measure', measure: pluginManifests }],
524
518
  },
525
- {
526
- id: 'spelling',
527
- label: 'Spelling',
528
- checks: [
529
- {
530
- kind: 'command',
531
- argv: ['bun', 'run', 'check:spell'],
532
- failure: 'Spell check failed',
533
- },
534
- ],
535
- success: 'Spell check passed',
536
- },
537
519
  {
538
520
  id: 'shell',
539
521
  label: 'Shell',
@@ -1,12 +1,11 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
- import { dirname, join, resolve } from 'node:path'
3
+ import { join, resolve } from 'node:path'
4
4
  import { $ } from 'bun'
5
5
  import { isMarked } from '@/sweep/exempt-marker'
6
6
  import { gitEnv } from '@/git/env'
7
7
  import { listRuleFiles } from '@/gov/payload'
8
8
  import { parseFrontmatter } from '@/indexes/frontmatter'
9
- import { movedStandardPath } from '@/standards/read'
10
9
  import { findBacktickSpans } from '@/markdown/backticks'
11
10
 
12
11
  /**
@@ -42,14 +41,11 @@ export const RULE_DIRS: readonly string[] = [
42
41
  export const GLOB_CORPUS = join('internal', 'rules')
43
42
 
44
43
  /**
45
- * How the citation was written, kept on the finding because the three resolve
46
- * against different roots and a reader repairing one needs to know which.
47
- *
48
- * `standard` is the live form, carried by 20 rules. `path` is a backticked
49
- * repository path. `sibling` is a rule naming another rule by filename alone,
50
- * which resolves inside the folder the citing rule sits in.
44
+ * How the citation was written, being a backticked repository path. A bare
45
+ * filename is no form, since with no folder around it nothing tells a rule
46
+ * from a convention such as `index.md`.
51
47
  */
52
- export type CitationForm = 'path' | 'standard' | 'sibling'
48
+ export type CitationForm = 'path'
53
49
 
54
50
  /**
55
51
  * What the sweep decided about one citation.
@@ -116,16 +112,6 @@ const FENCE = /^\s*(?:```|~~~)/
116
112
 
117
113
  const FRONTMATTER_DELIMITER = /^---\s*$/
118
114
 
119
- /**
120
- * The verb form, with the name captured. A leading letter or digit is required,
121
- * which is what leaves `canon standards <name>` unmatched: that line teaches the
122
- * form rather than citing a standard, and it is the only one in either corpus.
123
- */
124
- const STANDARD_CALL = /canon standards ([A-Za-z0-9][A-Za-z0-9._-]*)/g
125
-
126
- /** A rule filename, which is how a rule names a sibling with no folder around it. */
127
- const SIBLING_RULE = /^\d{3}-[a-z0-9-]+\.md$/
128
-
129
115
  /**
130
116
  * A character that puts the span outside this repository, or outside paths
131
117
  * altogether.
@@ -146,8 +132,8 @@ function isNotRepositoryPath(span: string): boolean {
146
132
  * This is what separates a citation from the folder and module conventions the
147
133
  * corpus is full of. `next/font`, `try/except`, `react-hooks/set-state-in-effect`,
148
134
  * and `oven-sh/setup-bun@v2` all carry a slash and name no file, and a trailing
149
- * slash is a folder rather than a document. The cost is that `claude/standards`
150
- * is a real path this declines to check, which is the bound the report states.
135
+ * slash is a folder rather than a document. The cost is that a real folder path
136
+ * goes unchecked, which is the bound the report states.
151
137
  */
152
138
  function namesAFile(span: string): boolean {
153
139
  if (span.endsWith('/')) return false
@@ -160,46 +146,15 @@ function namesAFile(span: string): boolean {
160
146
  * sweep can resolve.
161
147
  *
162
148
  * The whole span is classified rather than a trailing pattern inside it.
163
- * Extracting `standards/tooling-reference.md` out of
164
- * `internal/standards/tooling-reference.md` and resolving that against the
165
- * standards root manufactures a dead citation out of a file that exists, which
166
- * a session measuring this corpus did before the check was written.
149
+ * Extracting a trailing `<folder>/<name>.md` out of a longer path and resolving
150
+ * that against another root manufactures a dead citation out of a file that
151
+ * exists, which a session measuring this corpus did before the check was
152
+ * written.
167
153
  */
168
154
  export function classifySpan(span: string): CitationForm | undefined {
169
155
  if (isNotRepositoryPath(span)) return undefined
170
156
  if (!namesAFile(span)) return undefined
171
- if (span.includes('/')) return 'path'
172
- return SIBLING_RULE.test(span) ? 'sibling' : undefined
173
- }
174
-
175
- /**
176
- * Where a citation could answer, which is wherever the reader's own tools look
177
- * and nowhere else.
178
- *
179
- * A standard name takes the authoring root alone, matching `standardRoots` in
180
- * `@/standards/read`, which reads `standards/` at the working root and then the
181
- * package corpus, plus the skill reference a retired name moved to. This verb
182
- * refuses a tree holding no rule corpus, so it runs only where those two roots
183
- * are one directory. `internal/standards/` is
184
- * deliberately absent: `canon standards <name>` never reaches it, so admitting it
185
- * here would pass a citation that refuses for the session opening it, which is a
186
- * gate failing open.
187
- *
188
- * A sibling resolves inside the folder the citing rule sits in, since that is
189
- * the only place a bare rule filename means anything.
190
- */
191
- function candidatesFor(
192
- form: CitationForm,
193
- cited: string,
194
- ruleFile: string,
195
- ): string[] {
196
- if (form === 'standard') {
197
- const moved = movedStandardPath(cited)
198
-
199
- return [join('standards', `${cited}.md`), ...(moved ? [moved] : [])]
200
- }
201
- if (form === 'sibling') return [join(dirname(ruleFile), cited)]
202
- return [cited]
157
+ return span.includes('/') ? 'path' : undefined
203
158
  }
204
159
 
205
160
  interface RawCitation {
@@ -244,16 +199,6 @@ export function collectCitations(text: string): RawCitation[] {
244
199
  : trimmed
245
200
  const marked = isMarked(lines, index, CITATION_MARKER)
246
201
 
247
- for (const match of line.matchAll(STANDARD_CALL)) {
248
- found.push({
249
- line: index + 1,
250
- form: 'standard',
251
- cited: match[1] ?? '',
252
- preview,
253
- marked,
254
- })
255
- }
256
-
257
202
  // A backticked span is the only carrier a rule writes a citation in. No
258
203
  // rule in either corpus uses a markdown link, and matching running prose
259
204
  // would report every sentence that happens to name a file.
@@ -300,7 +245,7 @@ function isGoverned(cited: string, declared: readonly string[]): boolean {
300
245
  *
301
246
  * Anchored on the entry shape rather than searched for as a substring. A bare
302
247
  * scan for the glob text finds it in the `description:` line first wherever a
303
- * rule names what it governs in prose, which is how `596-claude-md.md` reported
248
+ * rule names what it governs in prose, which is how `seed-mirror.md` reported
304
249
  * its `CLAUDE.md` glob against line 2 instead of line 4.
305
250
  */
306
251
  const LIST_ENTRY = /^\s*-\s*(?:'([^']*)'|"([^"]*)"|(\S.*?))\s*$/
@@ -442,7 +387,7 @@ export async function readCitations(root: string): Promise<CitationReport> {
442
387
  }
443
388
 
444
389
  for (const raw of collectCitations(text)) {
445
- const candidates = candidatesFor(raw.form, raw.cited, file)
390
+ const candidates = [raw.cited]
446
391
  const resolved = candidates.find((path) =>
447
392
  existsSync(resolve(root, path)),
448
393
  )
@@ -471,7 +416,7 @@ function classifyStatus(
471
416
  ): CitationStatus {
472
417
  if (resolved !== undefined) return 'resolved'
473
418
  if (raw.marked) return 'exempt'
474
- if (raw.form === 'path' && isGoverned(raw.cited, declared)) return 'governed'
419
+ if (isGoverned(raw.cited, declared)) return 'governed'
475
420
  return 'dead'
476
421
  }
477
422
 
@@ -488,7 +433,7 @@ async function applyIgnored(
488
433
  measured: Extract<CitationReport, { kind: 'measured' }>,
489
434
  ): Promise<CitationReport> {
490
435
  const pending = measured.citations.filter(
491
- (citation) => citation.status === 'dead' && citation.form === 'path',
436
+ (citation) => citation.status === 'dead',
492
437
  )
493
438
  if (pending.length === 0) return measured
494
439
 
package/src/gov/stacks.ts CHANGED
@@ -78,9 +78,9 @@ export function loadGovStack(
78
78
  * entry resolves to itself, so a folder and a slug reach the caller as one
79
79
  * shape rather than two the caller has to tell apart.
80
80
  *
81
- * The directory wins over a rule file of the same name. They cannot collide
82
- * while `claude/skills/create-rule/references/rule.md` requires a numeric prefix on a rule slug, since a
83
- * band folder carries none.
81
+ * The directory wins over a rule file of the same name, which would leave
82
+ * that rule unreachable from a stack entry. `src/gov/rule-layout.test.ts`
83
+ * fails on any rule basename equal to a folder name, so the two never meet.
84
84
  */
85
85
  export function expandStackEntry(root: string, entry: string): string[] {
86
86
  const dir = join(rulesSourceDir(root), entry)
@@ -27,7 +27,7 @@ export interface Subject {
27
27
 
28
28
  /**
29
29
  * Reports what each element changes about itself when it takes focus, which is
30
- * the reading `governance/rules/ui/410-a11y.md` states three rules against and
30
+ * the reading `governance/rules/ui/a11y.md` states three rules against and
31
31
  * nothing measures.
32
32
  *
33
33
  * The difference between rest and focus is the treatment, rather than the
@@ -16,7 +16,7 @@ const CHARACTERS = ['—', ';'] as const
16
16
  * Parsing the prose was the alternative and it put a parser contract on a
17
17
  * document authored for people. The set is closed rather than extensible,
18
18
  * since a character is the one class a literal match settles. A word is left
19
- * to `write-human` as guidance and a spelling to cspell, because a word ban
19
+ * to `write-human` as guidance and a spelling to no check at all, because a word ban
20
20
  * catches the token and misses the habit behind it.
21
21
  */
22
22
  export const BAN_SETS: BanSets = {
@@ -34,9 +34,14 @@ export interface CeilingFinding {
34
34
  readonly exempt: string | null
35
35
  }
36
36
 
37
+ /** Whether the release tool writes this document, which no measure reads. */
38
+ export function isChangelog(rel: string): boolean {
39
+ return basename(rel) === CHANGELOG
40
+ }
41
+
37
42
  /** The reason a document is exempt from the ceiling, or null where it is not. */
38
43
  export function lengthExemption(rel: string, source: string): string | null {
39
- if (basename(rel) === CHANGELOG) return CHANGELOG_REASON
44
+ if (isChangelog(rel)) return CHANGELOG_REASON
40
45
 
41
46
  for (const line of bodyLines(source)) {
42
47
  if (line.fenced) continue
@@ -0,0 +1,86 @@
1
+ import { type BodyLine, LINK, maskDisplayed } from '@/markdown/scan'
2
+
3
+ export interface ColonFinding {
4
+ readonly line: number
5
+ /** Zero-based, the colon itself. */
6
+ readonly column: number
7
+ /** The whole source line, so a reader can match it against new text. */
8
+ readonly text: string
9
+ /**
10
+ * The run of non-space characters ending at the colon. A hook matches it
11
+ * against new text, since `column` is a UTF-16 offset that a reader counting
12
+ * code points would misplace after an emoji.
13
+ */
14
+ readonly lead: string
15
+ }
16
+
17
+ const HEADING = /^#{1,6}\s/
18
+ const TABLE_ROW = /^\s*\|/
19
+ const LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/
20
+ const QUOTE_MARKER = /^(?:\s*>\s?)+/
21
+
22
+ /** A colon followed by whitespace and more text, ruling out `10:30` and a scheme. */
23
+ const COLON = /:(?=\s+\S)/g
24
+
25
+ const BOLD_LABEL = /^(\*\*|__)[^*_]+\1$/
26
+
27
+ /** Blanks a span while holding its width, so a column stays where it was. */
28
+ function blank(match: string): string {
29
+ return ' '.repeat(match.length)
30
+ }
31
+
32
+ /**
33
+ * Whether the text before a line's first colon is a label rather than a
34
+ * clause: the lead of a list item, or a prefix that is only emphasis and a
35
+ * masked code span.
36
+ */
37
+ function isLabel(prefix: string, listItem: boolean): boolean {
38
+ if (BOLD_LABEL.test(prefix.trim())) return true
39
+
40
+ const words = prefix.replace(/[*_]/g, ' ').trim()
41
+ if (words === '') return true
42
+ return listItem && words.split(/\s+/).length <= 4
43
+ }
44
+
45
+ /**
46
+ * Finds a colon joining two clauses in the middle of a prose sentence.
47
+ *
48
+ * A heading, a table row, a fence, a code span, a link, and a line-final colon
49
+ * are left alone, and so is the first colon of a list item or of a line opening
50
+ * on a bold or code label. The measure is advisory, because a colon is
51
+ * sometimes right and the corpus count shows a gate would be a sweep nobody
52
+ * asked for.
53
+ */
54
+ export function findColons(lines: readonly BodyLine[]): ColonFinding[] {
55
+ const found: ColonFinding[] = []
56
+
57
+ for (const line of lines) {
58
+ if (line.fenced || HEADING.test(line.text) || TABLE_ROW.test(line.text)) {
59
+ continue
60
+ }
61
+
62
+ const masked = maskDisplayed(line.text.replace(LINK, blank))
63
+ const quote = QUOTE_MARKER.exec(masked)?.[0] ?? ''
64
+ const marker = LIST_MARKER.exec(masked.slice(quote.length))?.[0] ?? ''
65
+ const start = quote.length + marker.length
66
+ let labelled = false
67
+
68
+ for (const match of masked.matchAll(COLON)) {
69
+ if (match.index < start) continue
70
+
71
+ if (!labelled) {
72
+ labelled = true
73
+ if (isLabel(masked.slice(start, match.index), marker !== '')) continue
74
+ }
75
+
76
+ found.push({
77
+ line: line.number,
78
+ column: match.index,
79
+ text: line.text,
80
+ lead: line.text.slice(0, match.index).replace(/^.*\s/, ''),
81
+ })
82
+ }
83
+ }
84
+
85
+ return found
86
+ }
@@ -1,3 +1,5 @@
1
+ import type { ColonFinding } from '@/markdown/colons'
2
+ import type { HeadingCaseFinding } from '@/markdown/heading-case'
1
3
  import type { LinkFinding } from '@/markdown/links'
2
4
  import type { BanFinding } from '@/markdown/scan'
3
5
  import type { StructureReport } from '@/markdown/structure'
@@ -16,6 +18,19 @@ export interface GateInput {
16
18
  * alone states the same rule and leaves no place to assert it.
17
19
  */
18
20
  readonly structure: readonly StructureReport[]
21
+ /**
22
+ * Every mid-sentence colon the run measured, read by nothing here.
23
+ *
24
+ * A colon is sometimes right and the corpus carries over a thousand
25
+ * candidates, so the measure warns and a push never fails on it.
26
+ */
27
+ readonly colons: readonly ColonFinding[]
28
+ /**
29
+ * Every heading or title with a capital after its first word, read by
30
+ * nothing here. A proper noun is the one case it cannot tell from title
31
+ * case, and gating it would need a names list and an escape marker.
32
+ */
33
+ readonly headingCase: readonly HeadingCaseFinding[]
19
34
  }
20
35
 
21
36
  /**
@@ -0,0 +1,74 @@
1
+ import { bodyLines, maskDisplayed } from '@/markdown/scan'
2
+
3
+ export interface HeadingCaseFinding {
4
+ readonly line: number
5
+ /** The heading or title as written, markers removed. */
6
+ readonly text: string
7
+ /** Every word after the first that opens on a capital. */
8
+ readonly words: readonly string[]
9
+ }
10
+
11
+ const HEADING = /^#{1,6}\s+(.*?)(?:\s+#+)?\s*$/
12
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/
13
+ const TITLE = /^title:\s*(["']?)(.*?)\1\s*$/
14
+ const CODE_SPAN = /(`+)(?:(?!\1).)*\1/g
15
+ const CAPITALIZED = /^[A-Z][a-z]/
16
+
17
+ /**
18
+ * The words of `text` that break sentence case.
19
+ *
20
+ * A code span stands for one lowercase word, so a heading opening on one
21
+ * counts that span as its first word. The first word with a letter in it is
22
+ * exempt, which keeps a leading numeral from promoting the next word.
23
+ */
24
+ function capitalizedAfterFirst(text: string): string[] {
25
+ const masked = maskDisplayed(text.replace(CODE_SPAN, 'code'))
26
+ .replace(/\s+/g, ' ')
27
+ .trim()
28
+ const words = masked === '' ? [] : masked.split(' ')
29
+ const flagged: string[] = []
30
+ let seenFirst = false
31
+
32
+ for (const raw of words) {
33
+ const word = raw.replace(/^[^\p{L}]+/u, '')
34
+ if (!/\p{L}/u.test(raw)) continue
35
+ if (!seenFirst) {
36
+ seenFirst = true
37
+ continue
38
+ }
39
+ if (CAPITALIZED.test(word)) flagged.push(word)
40
+ }
41
+
42
+ return flagged
43
+ }
44
+
45
+ /**
46
+ * Finds a heading or a frontmatter `title` carrying a capitalized word after
47
+ * its first, outside code spans and all-caps tokens.
48
+ *
49
+ * A proper noun is the one case it cannot tell from title case, so the finding
50
+ * is a warning and no names list backs it.
51
+ */
52
+ export function findHeadingCase(source: string): HeadingCaseFinding[] {
53
+ const found: HeadingCaseFinding[] = []
54
+ const check = (line: number, text: string): void => {
55
+ const words = capitalizedAfterFirst(text)
56
+ if (words.length > 0) found.push({ line, text, words })
57
+ }
58
+
59
+ const frontmatter = FRONTMATTER.exec(source)
60
+ if (frontmatter) {
61
+ for (const [index, text] of frontmatter[1].split('\n').entries()) {
62
+ const title = TITLE.exec(text)
63
+ if (title) check(index + 2, title[2])
64
+ }
65
+ }
66
+
67
+ for (const line of bodyLines(source)) {
68
+ if (line.fenced) continue
69
+ const heading = HEADING.exec(line.text)
70
+ if (heading) check(line.number, heading[1])
71
+ }
72
+
73
+ return found
74
+ }
@@ -6,7 +6,7 @@
6
6
  * durable record naming a folder under it as its evidence is a citation into
7
7
  * something the backup never covers. The nine promoted here are every folder
8
8
  * under scratch that a live or archived record cites and no source file under
9
- * `claude/`, `scripts/`, `src/`, `governance/`, or `standards/` names by path,
9
+ * `claude/`, `scripts/`, `src/`, or `governance/` names by path,
10
10
  * which is what separates them from a folder a script still writes into on its
11
11
  * own schedule.
12
12
  *
@@ -42,7 +42,7 @@ import { recordDir, SCRATCH } from '@/roots/record'
42
42
  * Derived by the two-clause test `canon migrate scratch-evidence` exists to
43
43
  * apply mechanically: a durable record under a `BACKED_FOLDERS` entry, live
44
44
  * or archived, names the folder as its evidence, and no file under `claude/`,
45
- * `scripts/`, `src/`, `governance/`, or `standards/` names that path. Measured
45
+ * `scripts/`, `src/`, or `governance/` names that path. Measured
46
46
  * 2026-09-06 against nine folders holding thirteen files.
47
47
  *
48
48
  * `verify-astro` and `verify-vite-react` pass the same test and are excluded