@erclx/canon 4.0.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 (643) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/claude/.claude-plugin/plugin.json +19 -0
  4. package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
  5. package/claude/skills/bash-cli-script/SKILL.md +48 -0
  6. package/claude/skills/bash-cli-script/references/template.md +43 -0
  7. package/claude/skills/bash-script/REQUIREMENT.md +36 -0
  8. package/claude/skills/bash-script/SKILL.md +100 -0
  9. package/claude/skills/bash-script/references/patterns.md +349 -0
  10. package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
  11. package/claude/skills/canon-cli/SKILL.md +103 -0
  12. package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
  13. package/claude/skills/canon-feedback-file/SKILL.md +80 -0
  14. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
  15. package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
  16. package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
  17. package/claude/skills/canon-operator/SKILL.md +108 -0
  18. package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
  19. package/claude/skills/canon-rollout/SKILL.md +147 -0
  20. package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
  21. package/claude/skills/canon-screencast/SKILL.md +167 -0
  22. package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
  23. package/claude/skills/canon-slides-draft/SKILL.md +62 -0
  24. package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
  25. package/claude/skills/ci-workflow/SKILL.md +65 -0
  26. package/claude/skills/ci-workflow/references/workflows.md +98 -0
  27. package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
  28. package/claude/skills/claude-address-review/SKILL.md +212 -0
  29. package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
  30. package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
  31. package/claude/skills/claude-autoship/SKILL.md +207 -0
  32. package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
  33. package/claude/skills/claude-design-extract/SKILL.md +102 -0
  34. package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
  35. package/claude/skills/claude-diagram/SKILL.md +177 -0
  36. package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
  37. package/claude/skills/claude-docs/SKILL.md +287 -0
  38. package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
  39. package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
  40. package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
  41. package/claude/skills/claude-feature/SKILL.md +115 -0
  42. package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
  43. package/claude/skills/claude-groundwork/SKILL.md +142 -0
  44. package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
  45. package/claude/skills/claude-intake/SKILL.md +114 -0
  46. package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
  47. package/claude/skills/claude-intake-answer/SKILL.md +90 -0
  48. package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
  49. package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
  50. package/claude/skills/claude-markdown-propose/references/format.md +107 -0
  51. package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
  52. package/claude/skills/claude-memory-capture/SKILL.md +101 -0
  53. package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
  54. package/claude/skills/claude-memory-review/SKILL.md +210 -0
  55. package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
  56. package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
  57. package/claude/skills/claude-orchestrate/SKILL.md +241 -0
  58. package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
  59. package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
  60. package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
  61. package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
  62. package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
  63. package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
  64. package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
  65. package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
  66. package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
  67. package/claude/skills/claude-pr-review/SKILL.md +295 -0
  68. package/claude/skills/claude-review/REQUIREMENT.md +39 -0
  69. package/claude/skills/claude-review/SKILL.md +142 -0
  70. package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
  71. package/claude/skills/claude-seed-sync/SKILL.md +156 -0
  72. package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
  73. package/claude/skills/claude-standards-audit/SKILL.md +99 -0
  74. package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
  75. package/claude/skills/claude-tasks/SKILL.md +161 -0
  76. package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
  77. package/claude/skills/claude-teach/SKILL.md +196 -0
  78. package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
  79. package/claude/skills/claude-teach/references/pedagogy.md +67 -0
  80. package/claude/skills/claude-teach/references/promotion.md +54 -0
  81. package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
  82. package/claude/skills/claude-ui-test/SKILL.md +77 -0
  83. package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
  84. package/claude/skills/claude-ux-audit/SKILL.md +79 -0
  85. package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
  86. package/claude/skills/claude-ux-measure/SKILL.md +122 -0
  87. package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
  88. package/claude/skills/claude-worker/SKILL.md +96 -0
  89. package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
  90. package/claude/skills/claude-worktree/SKILL.md +136 -0
  91. package/claude/skills/create-rule/REQUIREMENT.md +45 -0
  92. package/claude/skills/create-rule/SKILL.md +68 -0
  93. package/claude/skills/create-skill/REQUIREMENT.md +38 -0
  94. package/claude/skills/create-skill/SKILL.md +32 -0
  95. package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
  96. package/claude/skills/create-snippet/SKILL.md +30 -0
  97. package/claude/skills/create-standard/REQUIREMENT.md +35 -0
  98. package/claude/skills/create-standard/SKILL.md +29 -0
  99. package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
  100. package/claude/skills/decision-escalate/SKILL.md +79 -0
  101. package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
  102. package/claude/skills/docs-sync/SKILL.md +95 -0
  103. package/claude/skills/git-branch/REQUIREMENT.md +38 -0
  104. package/claude/skills/git-branch/SKILL.md +60 -0
  105. package/claude/skills/git-commit/REQUIREMENT.md +36 -0
  106. package/claude/skills/git-commit/SKILL.md +49 -0
  107. package/claude/skills/git-followup/REQUIREMENT.md +43 -0
  108. package/claude/skills/git-followup/SKILL.md +48 -0
  109. package/claude/skills/git-issue/REQUIREMENT.md +38 -0
  110. package/claude/skills/git-issue/SKILL.md +65 -0
  111. package/claude/skills/git-pr/REQUIREMENT.md +50 -0
  112. package/claude/skills/git-pr/SKILL.md +164 -0
  113. package/claude/skills/git-pr/references/labels.md +95 -0
  114. package/claude/skills/git-ship/REQUIREMENT.md +42 -0
  115. package/claude/skills/git-ship/SKILL.md +55 -0
  116. package/claude/skills/git-split/REQUIREMENT.md +39 -0
  117. package/claude/skills/git-split/SKILL.md +162 -0
  118. package/claude/skills/git-stage/REQUIREMENT.md +39 -0
  119. package/claude/skills/git-stage/SKILL.md +73 -0
  120. package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
  121. package/claude/skills/git-worktree/SKILL.md +130 -0
  122. package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
  123. package/claude/skills/migration-claude-md/SKILL.md +76 -0
  124. package/claude/skills/migration-context/REQUIREMENT.md +36 -0
  125. package/claude/skills/migration-context/SKILL.md +95 -0
  126. package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
  127. package/claude/skills/migration-standards-drop/SKILL.md +113 -0
  128. package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
  129. package/claude/skills/migration-superseded/SKILL.md +115 -0
  130. package/claude/skills/project-commands/REQUIREMENT.md +42 -0
  131. package/claude/skills/project-commands/SKILL.md +85 -0
  132. package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
  133. package/claude/skills/restate-plainly/SKILL.md +39 -0
  134. package/claude/skills/session-map/REQUIREMENT.md +57 -0
  135. package/claude/skills/session-map/SKILL.md +70 -0
  136. package/claude/skills/session-resume/REQUIREMENT.md +49 -0
  137. package/claude/skills/session-resume/SKILL.md +51 -0
  138. package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
  139. package/claude/skills/setup-gov/SKILL.md +77 -0
  140. package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
  141. package/claude/skills/setup-indexes/SKILL.md +153 -0
  142. package/claude/skills/setup-init/REQUIREMENT.md +45 -0
  143. package/claude/skills/setup-init/SKILL.md +127 -0
  144. package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
  145. package/claude/skills/setup-plugins/SKILL.md +81 -0
  146. package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
  147. package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
  148. package/claude/skills/setup-verify/SKILL.md +51 -0
  149. package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
  150. package/claude/skills/systematic-debugging/SKILL.md +70 -0
  151. package/claude/skills/write-human/REQUIREMENT.md +46 -0
  152. package/claude/skills/write-human/SKILL.md +68 -0
  153. package/claude/skills/write-human/references/density.md +38 -0
  154. package/claude/skills/write-human/references/machine-tells.md +107 -0
  155. package/claude/skills/write-human/references/source-material.md +37 -0
  156. package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
  157. package/claude/skills/youtube-transcripts/SKILL.md +34 -0
  158. package/docs/agents/audits.md +98 -0
  159. package/docs/agents/capture.md +37 -0
  160. package/docs/agents/census.md +23 -0
  161. package/docs/agents/commands.md +146 -0
  162. package/docs/agents/comments.md +34 -0
  163. package/docs/agents/context-audit-checks.md +120 -0
  164. package/docs/agents/context-audit.md +83 -0
  165. package/docs/agents/counts.md +76 -0
  166. package/docs/agents/demo.md +86 -0
  167. package/docs/agents/docs.md +17 -0
  168. package/docs/agents/gate.md +84 -0
  169. package/docs/agents/index.md +47 -0
  170. package/docs/agents/indexes.md +35 -0
  171. package/docs/agents/install-and-sync.md +385 -0
  172. package/docs/agents/intake.md +81 -0
  173. package/docs/agents/key-changes.md +103 -0
  174. package/docs/agents/label-coverage.md +73 -0
  175. package/docs/agents/markdown-audit.md +197 -0
  176. package/docs/agents/output-shape.md +70 -0
  177. package/docs/agents/overview.md +26 -0
  178. package/docs/agents/records.md +170 -0
  179. package/docs/agents/restated.md +81 -0
  180. package/docs/agents/review-classification.md +77 -0
  181. package/docs/agents/routing.md +61 -0
  182. package/docs/agents/rule-citations.md +98 -0
  183. package/docs/agents/sandbox.md +71 -0
  184. package/docs/agents/scripting.md +149 -0
  185. package/docs/agents/sessions.md +120 -0
  186. package/docs/agents/skills-audit.md +94 -0
  187. package/docs/agents/skills-reach.md +64 -0
  188. package/docs/agents/standards-audit.md +38 -0
  189. package/docs/agents/state-scoped-risk.md +105 -0
  190. package/docs/agents/superseded.md +85 -0
  191. package/docs/agents/targets.md +83 -0
  192. package/docs/agents/tasks.md +200 -0
  193. package/docs/agents/teach.md +158 -0
  194. package/docs/agents/test-order.md +56 -0
  195. package/docs/agents/worktrees.md +62 -0
  196. package/docs/ai-workflow.md +317 -0
  197. package/docs/index.md +23 -0
  198. package/docs/operating-model.md +223 -0
  199. package/docs/target-projects.md +258 -0
  200. package/docs/visual-design-workflow.md +151 -0
  201. package/docs/zshrc-aliases.md +65 -0
  202. package/governance/rules/ci/700-ci-workflow.md +44 -0
  203. package/governance/rules/claude/500-prose.md +15 -0
  204. package/governance/rules/claude/501-markdown.md +14 -0
  205. package/governance/rules/claude/510-context.md +28 -0
  206. package/governance/rules/claude/511-indexes.md +14 -0
  207. package/governance/rules/claude/520-wireframes.md +12 -0
  208. package/governance/rules/claude/530-requirements.md +11 -0
  209. package/governance/rules/claude/540-architecture.md +11 -0
  210. package/governance/rules/claude/550-design.md +11 -0
  211. package/governance/rules/claude/555-tasks.md +12 -0
  212. package/governance/rules/claude/556-groundwork.md +11 -0
  213. package/governance/rules/claude/557-intake.md +11 -0
  214. package/governance/rules/claude/558-plan.md +22 -0
  215. package/governance/rules/claude/559-memory.md +11 -0
  216. package/governance/rules/claude/560-diagrams.md +18 -0
  217. package/governance/rules/claude/561-teach.md +13 -0
  218. package/governance/rules/claude/562-session.md +15 -0
  219. package/governance/rules/claude/570-skill.md +24 -0
  220. package/governance/rules/claude/575-hooks.md +17 -0
  221. package/governance/rules/claude/576-settings.md +14 -0
  222. package/governance/rules/claude/580-readme.md +11 -0
  223. package/governance/rules/claude/590-rule-authoring.md +12 -0
  224. package/governance/rules/claude/591-standard-authoring.md +12 -0
  225. package/governance/rules/claude/592-claude-md.md +19 -0
  226. package/governance/rules/core/000-constitution.md +30 -0
  227. package/governance/rules/core/005-behavior.md +27 -0
  228. package/governance/rules/core/010-testing.md +35 -0
  229. package/governance/rules/core/015-output.md +20 -0
  230. package/governance/rules/core/020-concurrency.md +22 -0
  231. package/governance/rules/core/025-indexes.md +9 -0
  232. package/governance/rules/core/030-error-handling.md +31 -0
  233. package/governance/rules/core/035-tasks.md +13 -0
  234. package/governance/rules/core/040-performance.md +20 -0
  235. package/governance/rules/core/045-memory.md +12 -0
  236. package/governance/rules/core/050-logging.md +20 -0
  237. package/governance/rules/core/055-scratch.md +9 -0
  238. package/governance/rules/core/060-naming.md +19 -0
  239. package/governance/rules/core/065-spelling.md +19 -0
  240. package/governance/rules/core/070-planning.md +18 -0
  241. package/governance/rules/core/075-dependencies.md +25 -0
  242. package/governance/rules/core/080-config-comments.md +22 -0
  243. package/governance/rules/core/085-worktrees.md +17 -0
  244. package/governance/rules/core/087-git.md +11 -0
  245. package/governance/rules/core/090-code-comments.md +39 -0
  246. package/governance/rules/framework/200-react.md +51 -0
  247. package/governance/rules/framework/210-astro.md +41 -0
  248. package/governance/rules/framework/220-fastapi.md +43 -0
  249. package/governance/rules/framework/230-nextjs.md +48 -0
  250. package/governance/rules/framework/250-tailwind.md +32 -0
  251. package/governance/rules/framework/260-shadcn.md +34 -0
  252. package/governance/rules/lang/100-typescript.md +40 -0
  253. package/governance/rules/lang/110-python.md +42 -0
  254. package/governance/rules/lang/120-bash.md +19 -0
  255. package/governance/rules/lib/300-testing-ts.md +39 -0
  256. package/governance/rules/lib/305-e2e-reliability.md +34 -0
  257. package/governance/rules/lib/306-test-scope.md +25 -0
  258. package/governance/rules/lib/310-zod.md +25 -0
  259. package/governance/rules/lib/320-tanstack-query.md +32 -0
  260. package/governance/rules/lib/330-testing-py.md +44 -0
  261. package/governance/rules/lib/340-pydantic.md +38 -0
  262. package/governance/rules/lib/350-security-web.md +32 -0
  263. package/governance/rules/lib/360-security-server.md +39 -0
  264. package/governance/rules/lib/370-database.md +35 -0
  265. package/governance/rules/snippets/505-at-references.md +9 -0
  266. package/governance/rules/ui/400-ui.md +36 -0
  267. package/governance/rules/ui/410-a11y.md +48 -0
  268. package/governance/rules/ui/420-forms.md +36 -0
  269. package/governance/rules/ui/430-ux-completeness.md +65 -0
  270. package/governance/rules/ui/440-surface-capture.md +34 -0
  271. package/governance/rules/ui/450-link-behavior.md +19 -0
  272. package/governance/stacks/astro.toml +2 -0
  273. package/governance/stacks/base.toml +8 -0
  274. package/governance/stacks/node-server.toml +2 -0
  275. package/governance/stacks/node.toml +2 -0
  276. package/governance/stacks/python-fastapi.toml +2 -0
  277. package/governance/stacks/python.toml +2 -0
  278. package/governance/stacks/react.toml +2 -0
  279. package/package.json +69 -0
  280. package/scripts/config.sh +11 -0
  281. package/scripts/core/bootstrap.sh +81 -0
  282. package/scripts/core/check-color-source.sh +41 -0
  283. package/scripts/core/check-ignore-parity.sh +162 -0
  284. package/scripts/core/check-plugin-boundary.sh +45 -0
  285. package/scripts/core/check-seed-independence.sh +59 -0
  286. package/scripts/core/check-skill-paths.sh +24 -0
  287. package/scripts/core/clean.sh +36 -0
  288. package/scripts/core/install-check.sh +101 -0
  289. package/scripts/core/list-seed-roots.sh +18 -0
  290. package/scripts/core/regen-claude-copies.sh +10 -0
  291. package/scripts/core/regen-hero.sh +217 -0
  292. package/scripts/core/regen-indexes.sh +10 -0
  293. package/scripts/core/regen-tooling-paths.sh +61 -0
  294. package/scripts/core/repair-bare-flag.sh +19 -0
  295. package/scripts/core/snapshot.sh +134 -0
  296. package/scripts/core/update.sh +35 -0
  297. package/scripts/docs/list.sh +165 -0
  298. package/scripts/lib/frontmatter.sh +30 -0
  299. package/scripts/lib/gov.sh +14 -0
  300. package/scripts/lib/sandbox-fixtures.sh +191 -0
  301. package/scripts/lib/sandbox-git.sh +125 -0
  302. package/scripts/lib/sandbox-path.sh +206 -0
  303. package/scripts/lib/tooling.sh +35 -0
  304. package/scripts/lib/ui.sh +266 -0
  305. package/scripts/lib/worktree.sh +20 -0
  306. package/scripts/manage-sandbox.sh +466 -0
  307. package/scripts/snippets/create.sh +156 -0
  308. package/scripts/standards/list.sh +115 -0
  309. package/scripts/tooling/create.sh +109 -0
  310. package/scripts/tooling/verify.sh +179 -0
  311. package/snippets/align.md +12 -0
  312. package/snippets/claude/decision-memo.md +39 -0
  313. package/snippets/claude/feature-recap.md +19 -0
  314. package/snippets/claude/figma-steps.md +24 -0
  315. package/snippets/compact-summary.md +5 -0
  316. package/snippets/decision-help.md +6 -0
  317. package/snippets/meta-prompt.md +14 -0
  318. package/snippets/research-prompt.md +7 -0
  319. package/snippets/session-notes.md +11 -0
  320. package/snippets/snippets.toml +5 -0
  321. package/snippets/step-by-step.md +10 -0
  322. package/snippets/web-research.md +21 -0
  323. package/src/audits/baseline.ts +201 -0
  324. package/src/audits/catalog.ts +876 -0
  325. package/src/audits/run.ts +204 -0
  326. package/src/autoship/classify.ts +75 -0
  327. package/src/autoship/paths.ts +51 -0
  328. package/src/binary.ts +16 -0
  329. package/src/browser/engine.ts +40 -0
  330. package/src/census/count.ts +113 -0
  331. package/src/claude/cases/all.ts +24 -0
  332. package/src/claude/cases/authoring.ts +53 -0
  333. package/src/claude/cases/claude-workflow.ts +158 -0
  334. package/src/claude/cases/git.ts +44 -0
  335. package/src/claude/cases/misc.ts +27 -0
  336. package/src/claude/cases/setup.ts +94 -0
  337. package/src/claude/gitignore.ts +51 -0
  338. package/src/claude/routing.ts +283 -0
  339. package/src/claude/seeds-list.ts +47 -0
  340. package/src/claude/seeds.ts +150 -0
  341. package/src/claude/settings.ts +151 -0
  342. package/src/claude/skills-audit.ts +228 -0
  343. package/src/claude/skills-drift.ts +156 -0
  344. package/src/claude/skills-list.ts +99 -0
  345. package/src/claude/skills-rank.ts +320 -0
  346. package/src/claude/skills-reach.ts +227 -0
  347. package/src/cli-run.ts +43 -0
  348. package/src/cli.ts +200 -0
  349. package/src/commands/audits.ts +350 -0
  350. package/src/commands/autoship.ts +129 -0
  351. package/src/commands/capture.ts +133 -0
  352. package/src/commands/census.ts +105 -0
  353. package/src/commands/claude.ts +1286 -0
  354. package/src/commands/comments.ts +240 -0
  355. package/src/commands/context.ts +857 -0
  356. package/src/commands/demo.ts +389 -0
  357. package/src/commands/deps.ts +173 -0
  358. package/src/commands/design.ts +36 -0
  359. package/src/commands/docs.ts +60 -0
  360. package/src/commands/feedback-format.ts +23 -0
  361. package/src/commands/feedback.ts +112 -0
  362. package/src/commands/gate.ts +189 -0
  363. package/src/commands/gov.ts +1265 -0
  364. package/src/commands/indexes.ts +184 -0
  365. package/src/commands/init.ts +113 -0
  366. package/src/commands/intake.ts +406 -0
  367. package/src/commands/inventory.ts +256 -0
  368. package/src/commands/labels.ts +361 -0
  369. package/src/commands/markdown.ts +544 -0
  370. package/src/commands/migrate.ts +175 -0
  371. package/src/commands/pass-through.ts +39 -0
  372. package/src/commands/pr.ts +411 -0
  373. package/src/commands/records.ts +728 -0
  374. package/src/commands/sandbox.ts +468 -0
  375. package/src/commands/secrets.ts +132 -0
  376. package/src/commands/serve.ts +159 -0
  377. package/src/commands/sessions.ts +408 -0
  378. package/src/commands/slides.ts +126 -0
  379. package/src/commands/snippets.ts +84 -0
  380. package/src/commands/standards.ts +247 -0
  381. package/src/commands/sync.ts +428 -0
  382. package/src/commands/targets.ts +319 -0
  383. package/src/commands/tasks.ts +743 -0
  384. package/src/commands/teach.ts +786 -0
  385. package/src/commands/tooling.ts +573 -0
  386. package/src/commands/transcripts.ts +44 -0
  387. package/src/commands/upgrade.ts +231 -0
  388. package/src/commands/wiki.ts +100 -0
  389. package/src/commands/worktrees.ts +191 -0
  390. package/src/comments/scan.ts +338 -0
  391. package/src/comments/trend.ts +207 -0
  392. package/src/comments/vocabulary.ts +85 -0
  393. package/src/context/architecture.ts +364 -0
  394. package/src/context/audit.ts +790 -0
  395. package/src/context/citations.ts +196 -0
  396. package/src/context/folders.ts +186 -0
  397. package/src/context/gate.ts +57 -0
  398. package/src/context/index-drift.ts +64 -0
  399. package/src/context/narration.ts +99 -0
  400. package/src/copy.ts +30 -0
  401. package/src/counts/catalogs.ts +96 -0
  402. package/src/counts/numbers.ts +79 -0
  403. package/src/counts/scan.ts +314 -0
  404. package/src/demo/beats.ts +135 -0
  405. package/src/demo/compile.ts +326 -0
  406. package/src/demo/container.ts +63 -0
  407. package/src/demo/cursors.ts +55 -0
  408. package/src/demo/drive.ts +357 -0
  409. package/src/demo/pointer.ts +178 -0
  410. package/src/demo/theme.ts +112 -0
  411. package/src/deps/audit.ts +153 -0
  412. package/src/design/parse.ts +116 -0
  413. package/src/design/render.ts +249 -0
  414. package/src/docs/read.ts +77 -0
  415. package/src/exec.ts +16 -0
  416. package/src/exempt-marker.ts +43 -0
  417. package/src/frontmatter.ts +13 -0
  418. package/src/gate/measures.ts +682 -0
  419. package/src/gate/sequencer.ts +386 -0
  420. package/src/gate/stages.ts +412 -0
  421. package/src/git-env.ts +36 -0
  422. package/src/git-files.ts +105 -0
  423. package/src/git-ignore.ts +46 -0
  424. package/src/github-format.ts +13 -0
  425. package/src/github.ts +24 -0
  426. package/src/gov/adapter.ts +103 -0
  427. package/src/gov/citations.ts +514 -0
  428. package/src/gov/consumed.ts +129 -0
  429. package/src/gov/install.ts +132 -0
  430. package/src/gov/list.ts +106 -0
  431. package/src/gov/payload.ts +39 -0
  432. package/src/gov/restated.ts +814 -0
  433. package/src/gov/stacks.ts +205 -0
  434. package/src/gov/superseded.ts +415 -0
  435. package/src/gov/test-order.ts +407 -0
  436. package/src/indexes/frontmatter.ts +46 -0
  437. package/src/indexes/regen.ts +84 -0
  438. package/src/indexes/render.ts +201 -0
  439. package/src/indexes/walk.ts +83 -0
  440. package/src/init/flags.ts +60 -0
  441. package/src/init/plan.ts +114 -0
  442. package/src/init/run.ts +46 -0
  443. package/src/init/steps.ts +77 -0
  444. package/src/intake/folder.ts +320 -0
  445. package/src/intake/items.ts +174 -0
  446. package/src/inventory/config.ts +117 -0
  447. package/src/inventory/group.ts +76 -0
  448. package/src/inventory/subjects.ts +114 -0
  449. package/src/inventory/walk.ts +129 -0
  450. package/src/labels/audit.ts +82 -0
  451. package/src/labels/coverage.ts +79 -0
  452. package/src/labels/map.ts +101 -0
  453. package/src/labels/phase.ts +95 -0
  454. package/src/markdown/bans.ts +94 -0
  455. package/src/markdown/files.ts +102 -0
  456. package/src/markdown/gate.ts +28 -0
  457. package/src/markdown/scan.ts +295 -0
  458. package/src/markdown/structure.ts +730 -0
  459. package/src/migrate/apply.ts +115 -0
  460. package/src/migrate/plan.ts +103 -0
  461. package/src/migrate/rename.ts +183 -0
  462. package/src/pr/bijection.ts +145 -0
  463. package/src/pr/paths.ts +335 -0
  464. package/src/process/harness.ts +167 -0
  465. package/src/project-root.ts +19 -0
  466. package/src/records/backup.ts +455 -0
  467. package/src/records/migrate.ts +78 -0
  468. package/src/records/size.ts +260 -0
  469. package/src/records/validate.ts +1162 -0
  470. package/src/sandbox/census.ts +228 -0
  471. package/src/sandbox/coverage.ts +115 -0
  472. package/src/sandbox/expect.ts +629 -0
  473. package/src/sandbox/tree.ts +47 -0
  474. package/src/secrets/marker.ts +30 -0
  475. package/src/secrets/patterns.ts +142 -0
  476. package/src/secrets/scan.ts +123 -0
  477. package/src/secrets/shipped.ts +111 -0
  478. package/src/seed-marker.ts +74 -0
  479. package/src/serve/static.ts +322 -0
  480. package/src/sessions/claim.ts +85 -0
  481. package/src/sessions/live.ts +79 -0
  482. package/src/sessions/registry.ts +137 -0
  483. package/src/sessions/resolve.ts +333 -0
  484. package/src/slides/layouts.ts +391 -0
  485. package/src/slides/open.ts +18 -0
  486. package/src/slides/parse.ts +84 -0
  487. package/src/slides/render.ts +88 -0
  488. package/src/slides/styles.ts +44 -0
  489. package/src/snippets/categories.ts +66 -0
  490. package/src/snippets/list.ts +32 -0
  491. package/src/snippets/presets.ts +50 -0
  492. package/src/standards/audit.ts +132 -0
  493. package/src/standards/read.ts +100 -0
  494. package/src/sync/check.ts +692 -0
  495. package/src/sync/engine.ts +576 -0
  496. package/src/sync/git.ts +225 -0
  497. package/src/sync/history.ts +123 -0
  498. package/src/sync/layout.ts +140 -0
  499. package/src/sync/reverse.ts +268 -0
  500. package/src/sync/seeds-report.ts +126 -0
  501. package/src/sync/stamp.ts +358 -0
  502. package/src/sync/target.ts +70 -0
  503. package/src/sync/workflow.ts +200 -0
  504. package/src/target.ts +43 -0
  505. package/src/targets/pulls.ts +250 -0
  506. package/src/targets/registry.ts +203 -0
  507. package/src/targets/resolve.ts +145 -0
  508. package/src/targets/sweep.ts +246 -0
  509. package/src/tasks/archive.ts +506 -0
  510. package/src/tasks/record.ts +311 -0
  511. package/src/tasks/trunk.ts +89 -0
  512. package/src/tasks/validate.ts +1001 -0
  513. package/src/teach/lesson.ts +180 -0
  514. package/src/teach/workspace.ts +842 -0
  515. package/src/tooling/gitignore.ts +122 -0
  516. package/src/tooling/inject.ts +193 -0
  517. package/src/tooling/list.ts +39 -0
  518. package/src/tooling/manifest.ts +176 -0
  519. package/src/tooling/package.ts +166 -0
  520. package/src/tooling/read.ts +65 -0
  521. package/src/tooling/scan.ts +147 -0
  522. package/src/tooling/stamp.ts +44 -0
  523. package/src/transcripts/fetch.ts +156 -0
  524. package/src/transcripts/metadata.ts +54 -0
  525. package/src/transcripts/vtt.ts +114 -0
  526. package/src/ui.ts +266 -0
  527. package/src/version/compare.ts +53 -0
  528. package/src/version/installed.ts +40 -0
  529. package/src/version/manager.ts +67 -0
  530. package/src/version/skew.ts +192 -0
  531. package/src/wiki/init.ts +85 -0
  532. package/src/worktree.ts +143 -0
  533. package/src/worktrees/reclaim.ts +306 -0
  534. package/standards/architecture.md +72 -0
  535. package/standards/branch.md +59 -0
  536. package/standards/commit.md +72 -0
  537. package/standards/context.md +151 -0
  538. package/standards/design.md +93 -0
  539. package/standards/diagrams.md +152 -0
  540. package/standards/glossary.md +75 -0
  541. package/standards/groundwork.md +211 -0
  542. package/standards/index.md +36 -0
  543. package/standards/intake.md +192 -0
  544. package/standards/issue.md +94 -0
  545. package/standards/markdown.md +137 -0
  546. package/standards/memory.md +144 -0
  547. package/standards/plan.md +172 -0
  548. package/standards/pr.md +139 -0
  549. package/standards/publish.md +51 -0
  550. package/standards/readme.md +208 -0
  551. package/standards/requirements.md +70 -0
  552. package/standards/rule.md +118 -0
  553. package/standards/session.md +109 -0
  554. package/standards/skill.md +300 -0
  555. package/standards/slug.md +39 -0
  556. package/standards/snippets.md +76 -0
  557. package/standards/standard.md +170 -0
  558. package/standards/tasks.md +254 -0
  559. package/standards/teach.md +153 -0
  560. package/standards/versioning.md +71 -0
  561. package/standards/wireframes.md +113 -0
  562. package/tooling/astro/configs/astro.config.mjs +31 -0
  563. package/tooling/astro/configs/eslint.config.js +79 -0
  564. package/tooling/astro/configs/playwright.config.ts +26 -0
  565. package/tooling/astro/configs/tsconfig.json +12 -0
  566. package/tooling/astro/configs/vitest.config.ts +22 -0
  567. package/tooling/astro/manifest.toml +34 -0
  568. package/tooling/astro/reference.md +60 -0
  569. package/tooling/base/configs/.editorconfig +5 -0
  570. package/tooling/base/configs/.github/pull_request_template.md +18 -0
  571. package/tooling/base/configs/.github/workflows/verify.yml +35 -0
  572. package/tooling/base/configs/.husky/commit-msg +1 -0
  573. package/tooling/base/configs/.husky/post-merge +61 -0
  574. package/tooling/base/configs/.husky/post-rewrite +21 -0
  575. package/tooling/base/configs/.husky/pre-commit +1 -0
  576. package/tooling/base/configs/.husky/pre-push +1 -0
  577. package/tooling/base/configs/.prettierrc +12 -0
  578. package/tooling/base/configs/.shellcheckrc +1 -0
  579. package/tooling/base/configs/.vscode/extensions.json +9 -0
  580. package/tooling/base/configs/.vscode/settings.json +3 -0
  581. package/tooling/base/configs/commitlint.config.js +11 -0
  582. package/tooling/base/configs/scripts/verify.sh +64 -0
  583. package/tooling/base/manifest.toml +30 -0
  584. package/tooling/base/reference.md +98 -0
  585. package/tooling/base/seeds/.claude/context/ci.md +33 -0
  586. package/tooling/base/seeds/.claude/context/development.md +37 -0
  587. package/tooling/base/seeds/.claude/context/index.md +11 -0
  588. package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
  589. package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
  590. package/tooling/base/seeds/.lintstagedrc +8 -0
  591. package/tooling/base/seeds/.prettierignore +0 -0
  592. package/tooling/base/seeds/cspell.json +20 -0
  593. package/tooling/claude/manifest.toml +14 -0
  594. package/tooling/claude/reference.md +79 -0
  595. package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
  596. package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
  597. package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
  598. package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
  599. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
  600. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
  601. package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
  602. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
  603. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
  604. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
  605. package/tooling/claude/seeds/.claude/memory/index.md +8 -0
  606. package/tooling/claude/seeds/.claude/settings.json +47 -0
  607. package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
  608. package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
  609. package/tooling/claude/seeds/CLAUDE.md +29 -0
  610. package/tooling/claude/user/settings.template.json +10 -0
  611. package/tooling/claude/user/statusline-command.sh +53 -0
  612. package/tooling/python/configs/.coveragerc +14 -0
  613. package/tooling/python/configs/.python-version +1 -0
  614. package/tooling/python/configs/mypy.ini +6 -0
  615. package/tooling/python/configs/pytest.ini +4 -0
  616. package/tooling/python/configs/ruff.toml +15 -0
  617. package/tooling/python/configs/scripts/verify.sh +77 -0
  618. package/tooling/python/manifest.toml +16 -0
  619. package/tooling/python/reference.md +66 -0
  620. package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
  621. package/tooling/python/seeds/tests/test_smoke.py +2 -0
  622. package/tooling/vite-react/configs/playwright.config.ts +26 -0
  623. package/tooling/vite-react/configs/tsconfig.json +35 -0
  624. package/tooling/vite-react/configs/vite.config.ts +24 -0
  625. package/tooling/vite-react/configs/vitest.config.ts +26 -0
  626. package/tooling/vite-react/manifest.toml +23 -0
  627. package/tooling/vite-react/reference.md +55 -0
  628. package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
  629. package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
  630. package/tooling/web/configs/.github/workflows/verify.yml +134 -0
  631. package/tooling/web/configs/.vscode/extensions.json +13 -0
  632. package/tooling/web/configs/.vscode/settings.json +10 -0
  633. package/tooling/web/configs/e2e/home.spec.ts +6 -0
  634. package/tooling/web/configs/e2e/screenshot.ts +53 -0
  635. package/tooling/web/configs/eslint.config.js +82 -0
  636. package/tooling/web/configs/scripts/screenshot.sh +28 -0
  637. package/tooling/web/configs/scripts/verify.sh +80 -0
  638. package/tooling/web/configs/scripts/worktree-port.sh +74 -0
  639. package/tooling/web/configs/src/test/setup.ts +8 -0
  640. package/tooling/web/manifest.toml +58 -0
  641. package/tooling/web/reference.md +115 -0
  642. package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
  643. package/tsconfig.json +14 -0
@@ -0,0 +1,1265 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, writeFile } from 'node:fs/promises'
3
+ import { basename, dirname, join, resolve } from 'node:path'
4
+ import type { Command } from 'commander'
5
+ import {
6
+ type CountFinding,
7
+ type CountsRefusal,
8
+ type CountsReport,
9
+ scanCounts,
10
+ } from '@/counts/scan'
11
+ import { PROJECT_ROOT } from '@/project-root'
12
+ import { createGovAdapter } from '@/gov/adapter'
13
+ import { regenConsumedRules } from '@/gov/consumed'
14
+ import { installRules, lookupRules } from '@/gov/install'
15
+ import { buildGovCatalog, describeRule, describeStack } from '@/gov/list'
16
+ import { buildRulesPayload, listRuleFiles } from '@/gov/payload'
17
+ import {
18
+ govStackExists,
19
+ listGovStacks,
20
+ mergeExtraRules,
21
+ resolveRules,
22
+ } from '@/gov/stacks'
23
+ import {
24
+ INSTRUCTIONS_REL,
25
+ type RestatedEntry,
26
+ type RestatedRefusal,
27
+ type RestatedReport,
28
+ readRestated,
29
+ RULES_REL as RESTATED_RULES_REL,
30
+ SEED_REL,
31
+ SHIPPED_SKILLS_REL,
32
+ } from '@/gov/restated'
33
+ import {
34
+ CITATION_MARKER,
35
+ type CitationReport,
36
+ GLOB_CORPUS,
37
+ readCitations,
38
+ RULE_DIRS,
39
+ type RuleCitation,
40
+ } from '@/gov/citations'
41
+ import {
42
+ readSuperseded,
43
+ SUPERSEDED_MARKER,
44
+ type SupersededHit,
45
+ type SupersededReport,
46
+ } from '@/gov/superseded'
47
+ import {
48
+ type PairRecord,
49
+ readTestOrder,
50
+ type TestOrderReport,
51
+ } from '@/gov/test-order'
52
+ import { recordStamp, runDomainSync } from '@/sync/engine'
53
+ import { writeChainStamp } from '@/sync/stamp'
54
+ import { resolveTarget } from '@/target'
55
+ import {
56
+ intro,
57
+ isNonInteractive,
58
+ logAdd,
59
+ logError,
60
+ logInfo,
61
+ logStep,
62
+ logWarn,
63
+ outro,
64
+ palette,
65
+ select,
66
+ } from '@/ui'
67
+
68
+ const PAYLOAD_REL = join('.claude', '.tmp', 'gov', 'rules.md')
69
+ const RULES_REL = join('.claude', 'rules')
70
+
71
+ interface InstallOptions {
72
+ readonly add?: string
73
+ }
74
+
75
+ interface RegenOptions {
76
+ readonly root?: string
77
+ }
78
+
79
+ interface ListOptions {
80
+ readonly stacks?: boolean
81
+ readonly rules?: boolean
82
+ readonly json?: boolean
83
+ }
84
+
85
+ interface TestOrderOptions {
86
+ readonly base?: string
87
+ readonly root?: string
88
+ readonly json?: boolean
89
+ }
90
+
91
+ interface SupersededOptions {
92
+ readonly root?: string
93
+ readonly json?: boolean
94
+ }
95
+
96
+ interface CitationsOptions {
97
+ readonly root?: string
98
+ readonly json?: boolean
99
+ }
100
+
101
+ interface RestatedOptions {
102
+ readonly root?: string
103
+ readonly json?: boolean
104
+ }
105
+
106
+ interface CountsOptions {
107
+ readonly root?: string
108
+ readonly json?: boolean
109
+ }
110
+
111
+ /** What a reader does about each way the sweep produced no reading. */
112
+ const RESTATED_REFUSALS: Record<RestatedRefusal, string> = {
113
+ 'no-instructions': `No ${INSTRUCTIONS_REL} here, or it carries no bullet, so there is no instruction corpus to sweep.`,
114
+ 'no-surfaces': `None of ${SEED_REL}, ${SHIPPED_SKILLS_REL}/, or ${RESTATED_RULES_REL}/ is here, so no further surface exists to match against.`,
115
+ }
116
+
117
+ const COUNTS_REFUSALS: Record<CountsRefusal, string> = {
118
+ 'no-git':
119
+ 'This tree is not a git checkout, so there is no tracked markdown corpus to read.',
120
+ 'no-markdown': 'No markdown file in this tree, so there is nothing to scan.',
121
+ }
122
+
123
+ export function register(program: Command): void {
124
+ const gov = program
125
+ .command('gov')
126
+ .description('Governance commands (install, sync, build, list)')
127
+ .helpOption('-h, --help', 'Show this help message')
128
+
129
+ gov
130
+ .command('install')
131
+ .description('Install a governance stack into .claude/rules/')
132
+ .argument('[stack]', 'Stack name (e.g. base, node, react)')
133
+ .argument('[target]', 'Target directory', '.')
134
+ .helpOption('-h, --help', 'Show this help message')
135
+ .option('--add <rules>', 'Comma-separated rules to layer on the stack')
136
+ .addHelpText(
137
+ 'after',
138
+ [
139
+ '',
140
+ 'Examples:',
141
+ ' canon gov install react',
142
+ ' canon gov install node ../my-app',
143
+ ' canon gov install astro --add 200-react,260-shadcn ../my-app',
144
+ '',
145
+ ].join('\n'),
146
+ )
147
+ .action(
148
+ async (
149
+ stack: string | undefined,
150
+ target: string,
151
+ opts: InstallOptions,
152
+ ) => {
153
+ process.exitCode = await runInstall(stack, target, opts)
154
+ },
155
+ )
156
+
157
+ gov
158
+ .command('sync')
159
+ .description('Update rules already installed under .claude/rules/')
160
+ .argument('[target]', 'Target directory', '.')
161
+ .helpOption('-h, --help', 'Show this help message')
162
+ .action(async (target: string) => {
163
+ process.exitCode = await runDomainSync(
164
+ createGovAdapter(PROJECT_ROOT),
165
+ target,
166
+ { protectedRoot: PROJECT_ROOT },
167
+ )
168
+ })
169
+
170
+ gov
171
+ .command('build')
172
+ .description('Concatenate installed rules into .claude/.tmp/gov/rules.md')
173
+ .argument('[target]', 'Target directory', '.')
174
+ .helpOption('-h, --help', 'Show this help message')
175
+ .action(async (target: string) => {
176
+ process.exitCode = await runBuild(target)
177
+ })
178
+
179
+ gov
180
+ .command('regen')
181
+ .description("Rebuild a repository's own .claude/rules/ from its record")
182
+ .helpOption('-h, --help', 'Show this help message')
183
+ .option('--root <path>', 'Repository root to regenerate', PROJECT_ROOT)
184
+ .addHelpText(
185
+ 'after',
186
+ [
187
+ '',
188
+ 'Reads internal/governance.toml and installs the stack it names, plus',
189
+ 'any rules under internal/rules/. Unlike install and sync, this runs',
190
+ 'against the toolkit root, whose .claude/rules/ is produced output.',
191
+ '',
192
+ ].join('\n'),
193
+ )
194
+ .action(async (opts: RegenOptions) => {
195
+ process.exitCode = await runRegen(opts)
196
+ })
197
+
198
+ gov
199
+ .command('list')
200
+ .description('Emit the catalog of stacks and rules')
201
+ .helpOption('-h, --help', 'Show this help message')
202
+ .option('--stacks', 'Only list stacks')
203
+ .option('--rules', 'Only list rules')
204
+ .option('--json', 'Emit machine-readable JSON')
205
+ .action((opts: ListOptions) => {
206
+ process.exitCode = runList(opts)
207
+ })
208
+
209
+ gov
210
+ .command('test-order')
211
+ .description(
212
+ 'Report where an implementation reached history before its test',
213
+ )
214
+ .helpOption('-h, --help', 'Show this help message')
215
+ .option('--base <ref>', 'Far side of the range, defaulting to the trunk')
216
+ .option('--root <path>', 'Repository to read, defaulting to the cwd')
217
+ .option('--json', 'Add a machine-readable record on stdout')
218
+ .addHelpText(
219
+ 'after',
220
+ [
221
+ '',
222
+ 'Measures the rule in .claude/rules/core/070-planning.md that asks for',
223
+ 'the test before the code. It reports and never gates, because pairing',
224
+ 'a test to an implementation is a judgment.',
225
+ '',
226
+ 'Coverage:',
227
+ ' a test sits beside its subject under the same name, minus .test',
228
+ ' only .ts and .tsx are paired, and every other path is named as read past',
229
+ ' a module the range modified rather than added is unclassified, since',
230
+ ' a refactor and a new behavior cannot be told apart from history',
231
+ '',
232
+ 'Exit codes:',
233
+ ' 0 no implementation reached history ahead of its test',
234
+ ' 1 refused, with the reason on stderr or in the JSON record',
235
+ ' 2 at least one implementation reached history ahead of its test',
236
+ '',
237
+ 'Examples:',
238
+ ' canon gov test-order',
239
+ ' canon gov test-order --base origin/main --json',
240
+ '',
241
+ ].join('\n'),
242
+ )
243
+ .action((opts: TestOrderOptions) => {
244
+ process.exitCode = runTestOrder(opts)
245
+ })
246
+
247
+ gov
248
+ .command('counts')
249
+ .description(
250
+ 'Report a self-stated catalog count that disagrees with what the tree holds',
251
+ )
252
+ .helpOption('-h, --help', 'Show this help message')
253
+ .option('--root <path>', 'Tree to read, defaulting to the cwd')
254
+ .option('--json', 'Add a machine-readable record on stdout')
255
+ .addHelpText(
256
+ 'after',
257
+ [
258
+ '',
259
+ 'Reads every tracked markdown file for a sentence stating how many',
260
+ 'members a closed catalog holds, then compares the stated figure',
261
+ 'against what the tree actually counts.',
262
+ '',
263
+ 'Catalogs read: skills, governance rules, standards, snippets, CLI',
264
+ 'commands, and audit ids. Closed rather than derived, so widening the',
265
+ 'set is a deliberate change rather than a side effect of a new list',
266
+ 'command shipping elsewhere.',
267
+ '',
268
+ 'A sentence carrying a calendar date or a backticked commit reference',
269
+ 'is read past, since that is how this corpus already marks a figure as',
270
+ 'a historical record rather than a live claim.',
271
+ '',
272
+ 'What it does not measure:',
273
+ ' a delta phrased as a transition (from fourteen to fifteen) and a',
274
+ ' fraction (thirteen of sixteen) are both catalog-size claims this',
275
+ ' corpus carries, and neither matches the number-then-noun shape read here',
276
+ '',
277
+ 'Exit codes:',
278
+ ' 0 no stated figure disagrees with the tree',
279
+ ' 1 refused, with the reason on stderr or in the JSON record',
280
+ ' 2 at least one stated figure disagrees with the tree',
281
+ '',
282
+ 'It reports and never gates. The false-positive rate is read off the',
283
+ 'first run rather than assumed, and a push failing on a stated figure',
284
+ 'that reads correctly to a person would teach a contributor to route',
285
+ 'around the stage.',
286
+ '',
287
+ 'Examples:',
288
+ ' canon gov counts',
289
+ ' canon gov counts --json',
290
+ '',
291
+ ].join('\n'),
292
+ )
293
+ .action(async (opts: CountsOptions) => {
294
+ process.exitCode = await runCounts(opts)
295
+ })
296
+
297
+ gov
298
+ .command('superseded')
299
+ .description(
300
+ 'Report where the tree still asserts a value a changed convention no longer produces',
301
+ )
302
+ .argument('<superseded>', 'The value the convention used to produce')
303
+ .argument('<replacement>', 'What it produces now')
304
+ .helpOption('-h, --help', 'Show this help message')
305
+ .option('--root <path>', 'Tree to read, defaulting to the cwd')
306
+ .option('--json', 'Add a machine-readable record on stdout')
307
+ .addHelpText(
308
+ 'after',
309
+ [
310
+ '',
311
+ 'Keyed on the value rather than on the file stating the rule. A fixture',
312
+ 'asserting an old output names neither the rule nor the standard behind',
313
+ 'it, so a file-scoped map from the changed rule reaches none of them and',
314
+ 'the value both sides carry is the only key there is.',
315
+ '',
316
+ 'It reports and never gates. A string appears for reasons unrelated to',
317
+ 'the convention, so the output is a reading rather than a verdict, and a',
318
+ 'declaration disagreeing for a stated reason carries a',
319
+ `\`${SUPERSEDED_MARKER}: <reason>\` marker on its own line or the one above.`,
320
+ '',
321
+ 'Pass an empty replacement to retire a value outright. Findings report',
322
+ 'the same way, none is annotated, since a line cannot carry a',
323
+ 'replacement that does not exist, and no templated form is read, since',
324
+ 'there is no second value for a stem to diverge from.',
325
+ '',
326
+ 'Templated forms, matched on the segment the two values differ on:',
327
+ ' glob <stem>-* , the family written as a pattern',
328
+ ' placeholder <stem>-<X> , the family written with a stand-in segment',
329
+ ' prefix <stem>- , the family written bare',
330
+ '',
331
+ 'A hit under one of them counts as a finding, since a stale templated',
332
+ 'citation is as real as a literal one. Each carries the nearest heading',
333
+ 'above it in a markdown file, because a line reading as a prohibition',
334
+ 'under one section reads as an instruction under another.',
335
+ '',
336
+ 'Blind spots:',
337
+ ' a prose reference that went stale without carrying the value, such as',
338
+ ' a declaration citing the wrong standard for the transform, matches',
339
+ ' nothing here and is reached by reading rather than by this sweep',
340
+ ' a family named in any other form, such as a bracket style other than',
341
+ ' <>, a trailing glob with no separator, or a description in words',
342
+ '',
343
+ 'Exit codes:',
344
+ ' 0 nothing in the corpus asserts the superseded value or its family',
345
+ ' 1 refused, with the reason on stderr or in the JSON record',
346
+ ' 2 at least one declaration asserts either',
347
+ '',
348
+ 'Examples:',
349
+ ' canon gov superseded feature-feat- feature-',
350
+ ' canon gov superseded feature-feat- feature- --json',
351
+ '',
352
+ ].join('\n'),
353
+ )
354
+ .action(
355
+ async (
356
+ superseded: string,
357
+ replacement: string,
358
+ opts: SupersededOptions,
359
+ ) => {
360
+ process.exitCode = await runSuperseded(superseded, replacement, opts)
361
+ },
362
+ )
363
+
364
+ gov
365
+ .command('citations')
366
+ .description(
367
+ 'Resolve every path a rule cites and every internal frontmatter glob, naming the ones reaching nothing',
368
+ )
369
+ .helpOption('-h, --help', 'Show this help message')
370
+ .option('--root <path>', 'Tree to read, defaulting to the cwd')
371
+ .option('--json', 'Add a machine-readable record on stdout')
372
+ .addHelpText(
373
+ 'after',
374
+ [
375
+ '',
376
+ `Reads bodies across ${RULE_DIRS.join(' and ')}, and frontmatter globs`,
377
+ `under ${GLOB_CORPUS} alone. A rule citing a file that moved fails`,
378
+ 'silently: nothing resolves the path until a session opens it, and the',
379
+ 'drift check beside this one passes an authored rule and its consumed',
380
+ 'copy that are wrong together. A rule whose glob names a directory that',
381
+ 'moved fails the same way, by never firing again.',
382
+ '',
383
+ 'This gates, unlike the superseded sweep beside it. A path resolving to',
384
+ 'nothing carries no judgment, and the classes where absence is correct',
385
+ 'are separated before the verdict rather than left for a reader.',
386
+ '',
387
+ `Frontmatter globs read under ${GLOB_CORPUS} and nowhere else. A rule`,
388
+ `under ${RULE_DIRS[0]} installs into a target and its paths: entries`,
389
+ "name that project's shape, so src/pages/** in the Astro rule cannot be",
390
+ 'told by pattern from a path here. Measured over that corpus, 32 of 72',
391
+ 'globs match nothing in this tree and every one of them is correct, so',
392
+ 'gating them would ship an exemption list the length of the corpus. The',
393
+ 'internal corpus ships nowhere, which makes the tree it governs the tree',
394
+ 'present and the question answerable.',
395
+ '',
396
+ 'Forms read, with where each resolves:',
397
+ ' standard canon standards <name> , against standards/ alone, which',
398
+ ' is where the verb itself looks. internal/standards/ is not',
399
+ ' tried, since a name resolving only there refuses for the',
400
+ ' session that opens the citation.',
401
+ ' path a backticked path carrying a directory segment and a file',
402
+ ' extension, against the root',
403
+ " sibling a bare <nnn>-<slug>.md , against the citing rule's folder",
404
+ '',
405
+ 'Not read, each a shape the corpus writes and none of them a citation:',
406
+ ' a placeholder or glob segment, which describes a shape rather than',
407
+ ' naming a file, such as .claude/context/<domain>.md or app/**/route.ts',
408
+ ' a bare filename naming a convention, such as route.ts or manifest.toml',
409
+ ' a span carrying no file extension, which is a folder or a module',
410
+ ' specifier, such as src/pages/ , next/font , or claude/standards',
411
+ ' a fenced block, which displays a path rather than pointing at one',
412
+ '',
413
+ 'Exempt, reported by name rather than dropped:',
414
+ ' governed the citing rule spells the path in its own frontmatter, so',
415
+ ' it names a target artifact rather than a file here. A glob',
416
+ ' never exempts, since a typo inside one is the defect.',
417
+ ' ignored git ignores the path, which is session scratch a clone is',
418
+ ' not expected to hold',
419
+ ` exempt the line carries a \`${CITATION_MARKER}: <reason>\` marker`,
420
+ ' on itself or the one above',
421
+ '',
422
+ 'Blind spots: a citation that resolves and points at the wrong file, a',
423
+ 'path written without backticks, a folder or a path carrying no extension,',
424
+ 'which this declines rather than guesses at, and a glob that matches real',
425
+ 'files while reaching none of the work it was scoped at, which is a',
426
+ 'reading rather than a resolution.',
427
+ '',
428
+ 'Exit codes:',
429
+ ' 0 every citation resolves or is exempt, and every glob read matches',
430
+ ' 1 refused, with the reason on stderr or in the JSON record',
431
+ ' 2 at least one cited path resolves to nothing, or one glob matches',
432
+ ' nothing in this tree',
433
+ '',
434
+ 'Examples:',
435
+ ' canon gov citations',
436
+ ' canon gov citations --json',
437
+ '',
438
+ ].join('\n'),
439
+ )
440
+ .action(async (opts: CitationsOptions) => {
441
+ process.exitCode = await runCitations(opts)
442
+ })
443
+
444
+ gov
445
+ .command('restated')
446
+ .description(
447
+ 'Report every instruction the always-loaded file states that a second surface states too',
448
+ )
449
+ .helpOption('-h, --help', 'Show this help message')
450
+ .option('--root <path>', 'Tree to read, defaulting to the cwd')
451
+ .option('--json', 'Add a machine-readable record on stdout')
452
+ .addHelpText(
453
+ 'after',
454
+ [
455
+ '',
456
+ `Matches every bullet in ${INSTRUCTIONS_REL} and every rule under`,
457
+ `${RESTATED_RULES_REL}/ against ${SEED_REL}, every`,
458
+ `${SHIPPED_SKILLS_REL}/*/SKILL.md body, and every other rule. Matching is`,
459
+ 'recall-first, keyed on distinctive tokens two statements share rather',
460
+ 'than on a phrase they spell the same way, because the case this exists',
461
+ 'for was one rule written three different ways.',
462
+ '',
463
+ 'What it separates:',
464
+ ' mirror a declared authoring-to-consumed pair, where repeating is the design',
465
+ ' repetition two surfaces state one rule and neither is declared a copy',
466
+ ' contradiction the prohibition falls on one surface alone, on a strong match',
467
+ '',
468
+ 'The contradiction class is a polarity reading rather than a judgment',
469
+ 'about meaning, so weigh each against the surfaces it names.',
470
+ '',
471
+ 'What it does not measure:',
472
+ ' a rule stated in two skill bodies and never in the always-loaded file',
473
+ ' or a path-scoped rule, since a body is searched but never read as a subject',
474
+ '',
475
+ 'Exit codes:',
476
+ ' 0 no instruction is restated outside a declared mirror',
477
+ ' 1 refused, with the reason on stderr or in the JSON record',
478
+ ' 2 at least one instruction is restated outside a declared mirror',
479
+ '',
480
+ 'Examples:',
481
+ ' canon gov restated',
482
+ ' canon gov restated --json',
483
+ '',
484
+ ].join('\n'),
485
+ )
486
+ .action((opts: RestatedOptions) => {
487
+ process.exitCode = runRestated(opts)
488
+ })
489
+ }
490
+
491
+ /**
492
+ * Reports and never gates, matching the two sweeps above. A restatement is
493
+ * legitimate more often than not, so failing a push on one would fail on the
494
+ * ordinary case and teach contributors to route around the stage.
495
+ */
496
+ function runRestated(opts: RestatedOptions): number {
497
+ const root = resolve(opts.root ?? process.cwd())
498
+ const report = readRestated(root)
499
+ const emitJson = opts.json ?? false
500
+
501
+ if (report.kind === 'unreadable') {
502
+ intro('canon gov restated')
503
+ logStep('Refused')
504
+ logWarn(RESTATED_REFUSALS[report.reason])
505
+ outro()
506
+
507
+ if (emitJson) {
508
+ process.stdout.write(
509
+ `${JSON.stringify({
510
+ root,
511
+ reason: report.reason,
512
+ message: RESTATED_REFUSALS[report.reason],
513
+ })}\n`,
514
+ )
515
+ }
516
+
517
+ return 1
518
+ }
519
+
520
+ reportRestated(report, root)
521
+
522
+ if (emitJson) {
523
+ process.stdout.write(`${JSON.stringify({ root, ...report })}\n`)
524
+ }
525
+
526
+ const findings = report.counts.contradictions + report.counts.repetitions
527
+ return findings > 0 ? 2 : 0
528
+ }
529
+
530
+ function describeEntry(entry: RestatedEntry): string[] {
531
+ const lines = [`${entry.subject.file}:${entry.subject.line}`]
532
+
533
+ for (const surface of entry.surfaces) {
534
+ lines.push(
535
+ ` [${surface.restatement}] ${surface.file}:${surface.line} via ${surface.anchors.join(', ')}`,
536
+ )
537
+ }
538
+
539
+ return lines
540
+ }
541
+
542
+ function reportRestated(
543
+ report: Extract<RestatedReport, { kind: 'measured' }>,
544
+ root: string,
545
+ ): void {
546
+ intro('canon gov restated')
547
+
548
+ // A count of what matched reads as a verdict on the repository unless the run
549
+ // also says how wide the corpus behind it was.
550
+ logStep('Corpus')
551
+ logInfo(
552
+ `${report.corpus.instructions} always-loaded instruction(s) and ${report.corpus.rules} rule file(s) against ${report.corpus.candidates} statement(s) from the seed, ${report.corpus.bodies} shipped body/bodies, and every rule again, in ${root}`,
553
+ )
554
+ logInfo(
555
+ `matched on ${report.matcher.anchors} weighted anchor(s), dropping any token in more than ${report.matcher.common} statements`,
556
+ )
557
+
558
+ // Named rather than counted. A polarity split is the only class claiming a
559
+ // defect, and a reader weighing one has to reach both surfaces.
560
+ logStep(
561
+ report.counts.contradictions === 0 ? 'No contradiction' : 'Contradictions',
562
+ )
563
+ if (report.counts.contradictions === 0) {
564
+ logInfo('no restatement puts a prohibition on one surface alone')
565
+ } else {
566
+ for (const entry of report.restatements) {
567
+ const carries = entry.surfaces.some(
568
+ (surface) => surface.restatement === 'contradiction',
569
+ )
570
+ if (!carries) continue
571
+ for (const line of describeEntry(entry)) logWarn(line)
572
+ }
573
+ }
574
+
575
+ logStep('Restated')
576
+ logInfo(
577
+ `${report.counts.repetitions} repetition(s) outside a declared mirror, and ${report.counts.mirrors} on one`,
578
+ )
579
+ logInfo(
580
+ `${report.counts.threeSurface} instruction(s) reach three surfaces or more`,
581
+ )
582
+
583
+ outro()
584
+ }
585
+
586
+ /**
587
+ * Reports and never gates, matching `test-order` above. The finding count moves
588
+ * the exit code with nothing wiring it into a push, since a value sweep
589
+ * over-reports by construction and gating a measure carrying a known
590
+ * false-positive class is what teaches contributors to route around a stage.
591
+ */
592
+ async function runSuperseded(
593
+ superseded: string,
594
+ replacement: string,
595
+ opts: SupersededOptions,
596
+ ): Promise<number> {
597
+ const root = resolve(opts.root ?? process.cwd())
598
+ const report = await readSuperseded(root, { superseded, replacement })
599
+ const emitJson = opts.json ?? false
600
+
601
+ if (report.kind === 'unreadable') {
602
+ intro('canon gov superseded')
603
+ logStep('Refused')
604
+ logError(report.reason)
605
+ outro()
606
+
607
+ if (emitJson) {
608
+ process.stdout.write(
609
+ `${JSON.stringify({ ok: false, reason: report.reason })}\n`,
610
+ )
611
+ }
612
+
613
+ return 1
614
+ }
615
+
616
+ reportSuperseded(report, root)
617
+
618
+ if (emitJson) {
619
+ process.stdout.write(`${JSON.stringify({ ok: true, root, ...report })}\n`)
620
+ }
621
+
622
+ return report.findings.length > 0 ? 2 : 0
623
+ }
624
+
625
+ /**
626
+ * Gates, unlike `superseded` and `test-order` above. The classes where a path
627
+ * reaching nothing is correct are separated inside the sweep, which is what
628
+ * leaves the remainder a defect with no judgment in it, so a finding fails the
629
+ * push rather than asking a reader to weigh it.
630
+ */
631
+ async function runCitations(opts: CitationsOptions): Promise<number> {
632
+ const root = resolve(opts.root ?? process.cwd())
633
+ const report = await readCitations(root)
634
+ const emitJson = opts.json ?? false
635
+
636
+ if (report.kind === 'unreadable') {
637
+ intro('canon gov citations')
638
+ logStep('Refused')
639
+ logError(report.reason)
640
+ outro()
641
+
642
+ if (emitJson) {
643
+ process.stdout.write(
644
+ `${JSON.stringify({ ok: false, reason: report.reason })}\n`,
645
+ )
646
+ }
647
+
648
+ return 1
649
+ }
650
+
651
+ const dead = report.citations.filter((citation) => citation.status === 'dead')
652
+ const unmatched = report.globs.filter((glob) => !glob.matched)
653
+ reportCitations(report, root)
654
+
655
+ if (emitJson) {
656
+ process.stdout.write(`${JSON.stringify({ ok: true, root, ...report })}\n`)
657
+ }
658
+
659
+ return dead.length + unmatched.length > 0 ? 2 : 0
660
+ }
661
+
662
+ function describeCitation(citation: RuleCitation): string {
663
+ const tried = citation.candidates.join(', ')
664
+ return `[${citation.form}] ${citation.file}:${citation.line}: ${citation.cited} reaches nothing at ${tried}`
665
+ }
666
+
667
+ /**
668
+ * Named rather than counted, matching the exempt section of the sweep beside
669
+ * this one. An exemption is a judgment the tree recorded, and a reader weighing
670
+ * this report has to be able to reach the line that carries it.
671
+ */
672
+ function reportExcused(
673
+ citations: readonly RuleCitation[],
674
+ status: RuleCitation['status'],
675
+ label: string,
676
+ empty: string,
677
+ ): void {
678
+ const excused = citations.filter((citation) => citation.status === status)
679
+
680
+ logStep(label)
681
+ if (excused.length === 0) {
682
+ logInfo(empty)
683
+ return
684
+ }
685
+
686
+ for (const citation of excused) {
687
+ logInfo(`${citation.file}:${citation.line}: ${citation.cited}`)
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Names the corpus on every run, clean or not.
693
+ *
694
+ * A section reporting that every glob resolves, over a corpus the reader
695
+ * assumes is both of them, says the shipped rules were checked and they were
696
+ * not. The scope is the finding here as much as the count is.
697
+ */
698
+ function reportGlobs(
699
+ report: Extract<CitationReport, { kind: 'measured' }>,
700
+ ): void {
701
+ const unmatched = report.globs.filter((glob) => !glob.matched)
702
+
703
+ logStep('Frontmatter globs')
704
+ logInfo(`${report.globs.length} globs read, under ${GLOB_CORPUS} alone`)
705
+
706
+ if (unmatched.length === 0) {
707
+ logInfo('every glob matches a file in this tree')
708
+ } else {
709
+ for (const glob of unmatched) {
710
+ logError(
711
+ `${glob.file}:${glob.line}: ${glob.glob} matches nothing, so the rule never fires`,
712
+ )
713
+ }
714
+ }
715
+
716
+ logInfo(
717
+ `not read: a glob under ${RULE_DIRS[0]}, which names the shape a target holds rather than this tree, and a glob matching real files while reaching none of the work it was scoped at`,
718
+ )
719
+ }
720
+
721
+ function reportCitations(
722
+ report: Extract<CitationReport, { kind: 'measured' }>,
723
+ root: string,
724
+ ): void {
725
+ intro('canon gov citations')
726
+
727
+ logStep('Sweep')
728
+ logInfo(`${report.rules} rules under ${RULE_DIRS.join(' and ')} in ${root}`)
729
+
730
+ const counted = new Map<RuleCitation['form'], number>()
731
+ for (const citation of report.citations) {
732
+ counted.set(citation.form, (counted.get(citation.form) ?? 0) + 1)
733
+ }
734
+ logInfo(
735
+ report.citations.length === 0
736
+ ? 'no citation in either corpus'
737
+ : `${report.citations.length} citations: ${[...counted]
738
+ .map(([form, count]) => `${count} ${form}`)
739
+ .join(', ')}`,
740
+ )
741
+
742
+ const dead = report.citations.filter((citation) => citation.status === 'dead')
743
+
744
+ logStep('Unresolved')
745
+ if (dead.length === 0) {
746
+ logInfo('every cited path resolves')
747
+ } else {
748
+ for (const citation of dead) logError(describeCitation(citation))
749
+ }
750
+
751
+ reportGlobs(report)
752
+
753
+ reportExcused(
754
+ report.citations,
755
+ 'governed',
756
+ 'Governed',
757
+ 'no rule cites a path it declares in its own frontmatter',
758
+ )
759
+ reportExcused(
760
+ report.citations,
761
+ 'ignored',
762
+ 'Ignored',
763
+ 'no rule cites a path git ignores',
764
+ )
765
+ reportExcused(
766
+ report.citations,
767
+ 'exempt',
768
+ 'Exempt',
769
+ `no line carries a ${CITATION_MARKER} marker`,
770
+ )
771
+
772
+ logInfo(
773
+ 'not read: a citation that resolves and points at the wrong file, a path written without backticks, and a folder or a path carrying no extension',
774
+ )
775
+
776
+ outro()
777
+ }
778
+
779
+ function describeHit(hit: SupersededHit): string {
780
+ const note = hit.carriesReplacement
781
+ ? ' (the replacement is on this line)'
782
+ : ''
783
+ const section = hit.heading === undefined ? '' : ` under ${hit.heading}`
784
+ return `[${hit.match}] ${hit.file}:${hit.line}:${hit.column}${section}${note}: ${hit.preview}`
785
+ }
786
+
787
+ /**
788
+ * Named separately from the literal hits rather than counted in with them. A
789
+ * templated hit is a wider match on a shorter string, so a reader weighing one
790
+ * is weighing a different question, and the section is where the forms it still
791
+ * cannot read are stated.
792
+ */
793
+ function reportTemplated(
794
+ report: Extract<SupersededReport, { kind: 'measured' }>,
795
+ ): void {
796
+ const templated = report.findings.filter((hit) => hit.match !== 'literal')
797
+
798
+ logStep('Templated')
799
+ if (report.stems === undefined) {
800
+ logInfo(
801
+ 'no stem derives from these two values, so no templated citation was read',
802
+ )
803
+ } else if (templated.length === 0) {
804
+ logInfo(
805
+ `nothing names the family as ${report.stems.superseded}-*, ${report.stems.superseded}-<X>, or ${report.stems.superseded}- bare`,
806
+ )
807
+ } else {
808
+ for (const hit of templated) logWarn(describeHit(hit))
809
+ }
810
+
811
+ logInfo(
812
+ 'not read: a family named in any other form, such as a bracket style other than <>, a trailing glob with no separator, or a description in words',
813
+ )
814
+ }
815
+
816
+ function reportSuperseded(
817
+ report: Extract<SupersededReport, { kind: 'measured' }>,
818
+ root: string,
819
+ ): void {
820
+ intro('canon gov superseded')
821
+
822
+ logStep('Sweep')
823
+ logInfo(`${report.superseded} → ${report.replacement} in ${root}`)
824
+ logInfo(
825
+ report.stems === undefined
826
+ ? 'no family stem, so the sweep is the literal value alone'
827
+ : `family stem ${report.stems.superseded} → ${report.stems.replacement}`,
828
+ )
829
+
830
+ const literal = report.findings.filter((hit) => hit.match === 'literal')
831
+
832
+ // Named for the kind rather than for a verdict. `Clean` over the literal half
833
+ // alone would read as a clean tree on a run whose templated half is the one
834
+ // carrying every finding.
835
+ logStep('Literal')
836
+ if (literal.length === 0) {
837
+ logInfo('nothing in the corpus asserts the superseded value')
838
+ } else {
839
+ for (const finding of literal) logWarn(describeHit(finding))
840
+ }
841
+
842
+ reportTemplated(report)
843
+
844
+ // Named rather than counted. A muted line is a judgment someone recorded,
845
+ // and a reader weighing this report has to be able to reach the reason.
846
+ logStep('Exempt')
847
+ if (report.exempt.length === 0) {
848
+ logInfo('no line carries a marker')
849
+ } else {
850
+ for (const hit of report.exempt) logInfo(describeHit(hit))
851
+ }
852
+
853
+ // A count of what passed reads as a verdict on the repository unless the run
854
+ // also says how much of it the corpus left out.
855
+ logStep('Corpus')
856
+ logInfo(
857
+ `${report.files} file(s) opened of ${report.listed} listed, ${report.skipped} skipped as binary or unreadable`,
858
+ )
859
+
860
+ outro()
861
+ }
862
+
863
+ /**
864
+ * Reports and never gates, so the finding count moves the exit code without
865
+ * anything wiring it into a push. `canon tasks validate` set that shape: a
866
+ * measure carrying a known false-positive class is what forces contributors to
867
+ * route around a stage, and the unclassified bucket here is that class.
868
+ */
869
+ function runTestOrder(opts: TestOrderOptions): number {
870
+ const root = resolve(opts.root ?? process.cwd())
871
+ const report = readTestOrder(root, { base: opts.base })
872
+ const emitJson = opts.json ?? false
873
+
874
+ // The frame renders on stderr in both modes and the record goes to stdout
875
+ // alone, which is the split `docs/agents/output-shape.md` fixes. A consumer
876
+ // reading stdout sees pure data either way, and an operator reading the
877
+ // terminal sees the refusal rather than a command that appeared to do nothing.
878
+ if (report.kind === 'unreadable') {
879
+ intro('canon gov test-order')
880
+ logStep('Refused')
881
+ logError(report.message)
882
+ outro()
883
+
884
+ if (emitJson) {
885
+ process.stdout.write(
886
+ `${JSON.stringify({ root, reason: report.reason, message: report.message })}\n`,
887
+ )
888
+ }
889
+
890
+ return 1
891
+ }
892
+
893
+ reportTestOrder(report, root)
894
+
895
+ if (emitJson) {
896
+ process.stdout.write(`${JSON.stringify({ ok: true, root, ...report })}\n`)
897
+ }
898
+
899
+ return report.findings.length > 0 ? 2 : 0
900
+ }
901
+
902
+ function describePair(record: PairRecord): string {
903
+ const test = record.test === null ? 'no test' : record.test
904
+ return `${record.subject} → ${test}: ${record.reason}`
905
+ }
906
+
907
+ function reportTestOrder(
908
+ report: Extract<TestOrderReport, { kind: 'measured' }>,
909
+ root: string,
910
+ ): void {
911
+ intro('canon gov test-order')
912
+
913
+ logStep('Range')
914
+ logInfo(`${report.base.slice(0, 8)}..${report.head.slice(0, 8)} in ${root}`)
915
+
916
+ logStep(report.findings.length === 0 ? 'Clean' : 'Findings')
917
+ if (report.findings.length === 0) {
918
+ logInfo('no implementation reached history ahead of the test covering it')
919
+ } else {
920
+ for (const finding of report.findings) logWarn(describePair(finding))
921
+ }
922
+
923
+ logStep('Satisfied')
924
+ logInfo(`${report.satisfied.length} pair(s) whose test came first`)
925
+
926
+ // The unclassified rows carry the warn glyph and move no exit code. A pass
927
+ // over a change the pairing could not read is the claim this check exists to
928
+ // avoid making, so the rows are named rather than counted into the clean line.
929
+ logStep('Unclassified')
930
+ if (report.unclassified.length === 0) {
931
+ logInfo('every changed module paired')
932
+ } else {
933
+ logWarn(
934
+ `${report.unclassified.length} change(s) the pairing could not read`,
935
+ )
936
+ for (const record of report.unclassified) logWarn(describePair(record))
937
+ }
938
+
939
+ // Coverage is narrower than the rule, and a report that did not say so would
940
+ // read as a verdict over every behavior in the range.
941
+ logStep('Read past')
942
+ logInfo(
943
+ `${report.ignored.length} path(s) outside ${report.scope.extensions.join(', ')}`,
944
+ )
945
+
946
+ outro()
947
+ }
948
+
949
+ /**
950
+ * Reports and never gates, matching the two sweeps above. The false-positive
951
+ * rate here is unmeasured before a first run against a real corpus, and
952
+ * gating a measure with an unknown false-positive rate is what teaches a
953
+ * contributor to route around the stage.
954
+ */
955
+ async function runCounts(opts: CountsOptions): Promise<number> {
956
+ const root = resolve(opts.root ?? process.cwd())
957
+ const report = await scanCounts(root)
958
+ const emitJson = opts.json ?? false
959
+
960
+ if (report.kind === 'unreadable') {
961
+ intro('canon gov counts')
962
+ logStep('Refused')
963
+ logWarn(COUNTS_REFUSALS[report.reason])
964
+ outro()
965
+
966
+ if (emitJson) {
967
+ process.stdout.write(
968
+ `${JSON.stringify({
969
+ root,
970
+ reason: report.reason,
971
+ message: COUNTS_REFUSALS[report.reason],
972
+ })}\n`,
973
+ )
974
+ }
975
+
976
+ return 1
977
+ }
978
+
979
+ reportCounts(report, root)
980
+
981
+ if (emitJson) {
982
+ process.stdout.write(`${JSON.stringify({ root, ...report })}\n`)
983
+ }
984
+
985
+ return report.findings.length > 0 ? 2 : 0
986
+ }
987
+
988
+ function describeFinding(finding: CountFinding): string {
989
+ return `${finding.file}:${finding.line} states ${finding.stated} but the tree holds ${finding.actual} ${finding.catalog}`
990
+ }
991
+
992
+ function reportCounts(
993
+ report: Extract<CountsReport, { kind: 'measured' }>,
994
+ root: string,
995
+ ): void {
996
+ intro('canon gov counts')
997
+
998
+ logStep('Corpus')
999
+ logInfo(`${report.filesScanned} tracked markdown file(s) in ${root}`)
1000
+ logInfo(
1001
+ Object.entries(report.catalogs)
1002
+ .map(([id, count]) => `${id}: ${count ?? 'n/a'}`)
1003
+ .join(', '),
1004
+ )
1005
+
1006
+ logStep(report.findings.length === 0 ? 'Clean' : 'Findings')
1007
+ if (report.findings.length === 0) {
1008
+ logInfo('no self-stated catalog count disagrees with the tree')
1009
+ } else {
1010
+ for (const finding of report.findings) logWarn(describeFinding(finding))
1011
+ }
1012
+
1013
+ outro()
1014
+ }
1015
+
1016
+ /**
1017
+ * Both selectors absent means both sections, which is the bash default. Naming
1018
+ * both is the same as naming neither rather than an error, since the two flags
1019
+ * read as filters and a caller passing both is asking for everything.
1020
+ */
1021
+ function selectedSections(opts: ListOptions): {
1022
+ stacks: boolean
1023
+ rules: boolean
1024
+ } {
1025
+ const stacks = opts.stacks === true
1026
+ const rules = opts.rules === true
1027
+ if (stacks === rules) return { stacks: true, rules: true }
1028
+ return { stacks, rules }
1029
+ }
1030
+
1031
+ /**
1032
+ * `JSON.stringify` replaces a `printf` that interpolated a description into a
1033
+ * JSON string literal through a hand-rolled escaper, so a rule carrying a
1034
+ * character that escaper missed emitted output a consuming skill could not
1035
+ * parse.
1036
+ *
1037
+ * `unreferenced` rides the same payload rather than taking a flag of its own.
1038
+ * The verify stage and a skill asking what a stack leaves out read one call,
1039
+ * and the key is additive, so a consumer reading `stacks` or `rules` is
1040
+ * untouched by it.
1041
+ */
1042
+ function runList(opts: ListOptions): number {
1043
+ const catalog = buildGovCatalog(PROJECT_ROOT)
1044
+ const sections = selectedSections(opts)
1045
+
1046
+ if (opts.json) {
1047
+ process.stdout.write(
1048
+ `${JSON.stringify({
1049
+ ...(sections.stacks ? { stacks: catalog.stacks } : {}),
1050
+ ...(sections.rules ? { rules: catalog.rules } : {}),
1051
+ unreferenced: catalog.unreferenced,
1052
+ })}\n`,
1053
+ )
1054
+ return 0
1055
+ }
1056
+
1057
+ intro('canon gov list')
1058
+
1059
+ if (sections.stacks) {
1060
+ logStep('Stacks')
1061
+ for (const entry of catalog.stacks) logInfo(describeStack(entry))
1062
+ }
1063
+
1064
+ if (sections.rules) {
1065
+ logStep('Rules')
1066
+ for (const entry of catalog.rules) logInfo(describeRule(entry))
1067
+ }
1068
+
1069
+ if (catalog.unreferenced.length > 0) {
1070
+ logStep('Reached by no stack')
1071
+ for (const rule of catalog.unreferenced) logInfo(rule)
1072
+ }
1073
+
1074
+ outro()
1075
+ return 0
1076
+ }
1077
+
1078
+ /**
1079
+ * Silent on success so the consumed-copy stage that calls it stays quiet. It is
1080
+ * the only work that stage does now. The installed set is readable on disk, so
1081
+ * printing it would only add noise to every `bun run check`.
1082
+ */
1083
+ async function runRegen(opts: RegenOptions): Promise<number> {
1084
+ const result = await regenConsumedRules(resolve(opts.root ?? PROJECT_ROOT))
1085
+
1086
+ if (!result.ok) {
1087
+ process.stderr.write(`Consumed-rules regen failed: ${result.reason}\n`)
1088
+ return 1
1089
+ }
1090
+
1091
+ return 0
1092
+ }
1093
+
1094
+ /**
1095
+ * Renders the target-relative path the prompt quotes, keeping the argument the
1096
+ * caller typed rather than the absolute path it resolves to.
1097
+ */
1098
+ function displayPath(target: string, rel: string): string {
1099
+ const trimmed = target.replace(/\/$/, '').replace(/^\.\//, '')
1100
+ return trimmed === '' || trimmed === '.' ? rel : `${trimmed}/${rel}`
1101
+ }
1102
+
1103
+ /**
1104
+ * Refuses rather than picking. `select_option` returned `options[0]` under
1105
+ * `CANON_NON_INTERACTIVE=1`, so a headless `canon gov install` with no stack
1106
+ * installed whichever stack sorted first, measured as 26 astro rules into an
1107
+ * empty directory. Every documented agent path already passes the argument.
1108
+ */
1109
+ async function chooseStack(root: string): Promise<string | number> {
1110
+ const stacks = listGovStacks(root)
1111
+
1112
+ if (stacks.length === 0) {
1113
+ logError('No stacks found in governance/stacks/')
1114
+ outro()
1115
+ return 1
1116
+ }
1117
+
1118
+ if (isNonInteractive()) {
1119
+ logError(
1120
+ `Stack argument is required in non-interactive mode. One of: ${stacks.join(', ')}.`,
1121
+ )
1122
+ outro()
1123
+ return 1
1124
+ }
1125
+
1126
+ return select({
1127
+ message: 'Select stack to install:',
1128
+ options: stacks.map((stack) => ({ value: stack, label: stack })),
1129
+ })
1130
+ }
1131
+
1132
+ async function runInstall(
1133
+ stack: string | undefined,
1134
+ target: string,
1135
+ opts: InstallOptions,
1136
+ ): Promise<number> {
1137
+ intro('canon gov install')
1138
+
1139
+ const resolved = resolveTarget(target, PROJECT_ROOT)
1140
+ if (typeof resolved === 'number') return resolved
1141
+
1142
+ let selected = stack
1143
+ if (selected === undefined) {
1144
+ const choice = await chooseStack(PROJECT_ROOT)
1145
+ if (typeof choice === 'number') return choice
1146
+ selected = choice
1147
+ }
1148
+
1149
+ if (!govStackExists(PROJECT_ROOT, selected)) {
1150
+ logError(`Stack not found: ${selected}`)
1151
+ outro()
1152
+ return 1
1153
+ }
1154
+
1155
+ const resolution = resolveRules(PROJECT_ROOT, selected)
1156
+ if (!resolution.ok) {
1157
+ logError(`Stack not found: ${resolution.missingStack}`)
1158
+ outro()
1159
+ return 1
1160
+ }
1161
+
1162
+ const add = opts.add ?? ''
1163
+ const rules =
1164
+ add === '' ? resolution.rules : mergeExtraRules(resolution.rules, add)
1165
+
1166
+ if (rules.length === 0) {
1167
+ logWarn(`No rules defined for stack: ${selected}`)
1168
+ outro()
1169
+ return 0
1170
+ }
1171
+
1172
+ logStep(
1173
+ add === ''
1174
+ ? `Resolving stack: ${selected}`
1175
+ : `Resolving stack: ${selected} + extras`,
1176
+ )
1177
+
1178
+ const { found, missing } = lookupRules(PROJECT_ROOT, rules)
1179
+ for (const rule of missing) logWarn(`${rule} (source not found, skipping)`)
1180
+ for (const source of found) logInfo(source.rule)
1181
+
1182
+ const shouldInstall = await select({
1183
+ message: `Install ${found.length} rules to ${displayPath(target, RULES_REL)}?`,
1184
+ options: [
1185
+ { value: true, label: 'Yes' },
1186
+ { value: false, label: 'No' },
1187
+ ],
1188
+ nonInteractiveDefault: true,
1189
+ })
1190
+
1191
+ if (!shouldInstall) {
1192
+ logWarn('Cancelled')
1193
+ outro()
1194
+ return 0
1195
+ }
1196
+
1197
+ logStep('Installing rules')
1198
+ for (const rel of await installRules(found, resolved)) logAdd(rel)
1199
+
1200
+ const now = new Date()
1201
+ await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, now)
1202
+ await writeChainStamp(
1203
+ resolved,
1204
+ { domain: 'governance', toolkitRoot: PROJECT_ROOT },
1205
+ [selected],
1206
+ now,
1207
+ )
1208
+
1209
+ const { GREEN, NC } = palette(process.stderr)
1210
+ outro()
1211
+ process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
1212
+ return 0
1213
+ }
1214
+
1215
+ /**
1216
+ * Emits the rules payload the `gov-*` skills read. The output lands in the
1217
+ * target's scratch directory rather than stdout because the file is the
1218
+ * contract, and the skills point at the path.
1219
+ */
1220
+ async function runBuild(target: string): Promise<number> {
1221
+ intro('canon gov build')
1222
+
1223
+ const resolved = resolve(target)
1224
+ const rulesDir = join(resolved, RULES_REL)
1225
+
1226
+ if (!existsSync(rulesDir)) {
1227
+ logError(`No rules found at ${rulesDir}. Run \`canon gov install\` first.`)
1228
+ outro()
1229
+ return 1
1230
+ }
1231
+
1232
+ const files = listRuleFiles(rulesDir)
1233
+ logStep(`Reading ${RULES_REL} (${files.length} found)`)
1234
+ for (const file of files) {
1235
+ logInfo(basename(file))
1236
+ }
1237
+
1238
+ const shouldBuild = await select({
1239
+ message: `Build ${files.length} rules to ${PAYLOAD_REL}?`,
1240
+ options: [
1241
+ { value: true, label: 'Yes' },
1242
+ { value: false, label: 'No' },
1243
+ ],
1244
+ nonInteractiveDefault: true,
1245
+ })
1246
+
1247
+ if (!shouldBuild) {
1248
+ logWarn('Cancelled')
1249
+ outro()
1250
+ return 0
1251
+ }
1252
+
1253
+ logStep('Building rules payload')
1254
+ const output = join(resolved, PAYLOAD_REL)
1255
+ await mkdir(dirname(output), { recursive: true })
1256
+ await writeFile(output, buildRulesPayload(files))
1257
+ logAdd(PAYLOAD_REL)
1258
+
1259
+ const { GREEN, NC } = palette(process.stderr)
1260
+ outro()
1261
+ process.stderr.write(
1262
+ `${GREEN}✓ Rules built (${files.length} rules → ${PAYLOAD_REL})${NC}\n`,
1263
+ )
1264
+ return 0
1265
+ }