@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,876 @@
1
+ import type { SkillsAuditRefusal } from '@/claude/skills-audit'
2
+ import type { RankRefusal } from '@/claude/skills-rank'
3
+ import type { ReachRefusal } from '@/claude/skills-reach'
4
+ import type { ContextAuditRefusal } from '@/context/audit'
5
+ import type { AuditRefusal } from '@/deps/audit'
6
+ import type { RestatedRefusal } from '@/gov/restated'
7
+ import type { LabelAuditRefusal } from '@/labels/audit'
8
+ import type { ValidateRefusal as RecordRefusal } from '@/records/validate'
9
+ import type { ScanRefusal } from '@/secrets/scan'
10
+ import type { ValidateRefusal as BoardRefusal } from '@/tasks/validate'
11
+
12
+ /**
13
+ * The audits this repository already owns, and how to read each one's record.
14
+ *
15
+ * Every verb here ships its own `--json` shape with its own keys, and this
16
+ * module reads each shape rather than forcing a common envelope on them. Each
17
+ * record already has consumers, so an envelope would be a breaking change to
18
+ * every one of them bought for tidiness.
19
+ */
20
+
21
+ /** How far a finding from this audit reaches. */
22
+ export type Corpus =
23
+ /** Committed, so every clone measures the same files and a delta is shared. */
24
+ | 'tracked'
25
+ /** Gitignored session scratch, so the numbers are one machine's alone. */
26
+ | 'per-machine'
27
+ /**
28
+ * Read from an index off this machine, so the count moves when someone
29
+ * publishes rather than when someone edits here.
30
+ *
31
+ * Its own member rather than either of the two above. A baseline would
32
+ * record growth nobody caused, which is what keeps it out of the retained
33
+ * set alongside per-machine scratch, and an index this run could not reach
34
+ * is an ordinary absence rather than the broken checkout a missing tracked
35
+ * tree would be.
36
+ */
37
+ | 'upstream'
38
+
39
+ export type AuditStatus =
40
+ /** The audit reported and every count it produced is zero. */
41
+ | 'clean'
42
+ /** The audit reported findings it does not gate on. */
43
+ | 'reported'
44
+ /** The audit reported a finding that is a fact, which fails the aggregate. */
45
+ | 'finding'
46
+ /**
47
+ * The per-machine corpus this audit reads is not on this disk, which is the
48
+ * ordinary state of a gitignored folder on a fresh clone and in CI.
49
+ */
50
+ | 'absent'
51
+ /** The audit did not report, so the aggregate measured less than the set. */
52
+ | 'unmeasured'
53
+
54
+ export interface AuditSpec {
55
+ readonly id: string
56
+ readonly label: string
57
+ /** Arguments after `canon`, always ending in `--json`. */
58
+ readonly argv: readonly string[]
59
+ /**
60
+ * Exit codes this verb sets on a finding that is a fact.
61
+ *
62
+ * Empty for every verb whose findings are judgments. The catalog decides
63
+ * this rather than the exit code, because a reporting verb sets 2 on
64
+ * findings it deliberately does not gate on, so the code alone cannot say
65
+ * whether a finding is a fact.
66
+ */
67
+ readonly gatingExits: readonly number[]
68
+ readonly corpus: Corpus
69
+ /**
70
+ * Refusal reasons that mean this audit has no corpus here rather than that
71
+ * it broke, overriding what the corpus alone would allow.
72
+ *
73
+ * Present only where the corpus answers wrongly. A tracked corpus normally
74
+ * allows nothing, since a tree that ships to targets and cannot be found is
75
+ * a broken checkout, and the secret scan is the exception: a project that
76
+ * publishes nothing has no shipped tree to read, which is an ordinary state
77
+ * rather than a defect.
78
+ */
79
+ readonly absentReasons?: readonly string[]
80
+ /**
81
+ * Pulls the counts worth retaining out of this verb's record.
82
+ *
83
+ * Returns `undefined` when the record does not carry the keys it reads,
84
+ * which is a measure that did not run rather than a corpus with nothing in
85
+ * it. Zero there would read as clean, which is the two-states-look-alike
86
+ * defect this repository has already fixed elsewhere.
87
+ */
88
+ readonly counts: (record: unknown) => Record<string, number> | undefined
89
+ }
90
+
91
+ export interface AuditResult {
92
+ readonly id: string
93
+ readonly label: string
94
+ readonly status: AuditStatus
95
+ /** Retained by the baseline, which is `tracked` alone. */
96
+ readonly tracked: boolean
97
+ /** Why it is or is not retained, which `tracked` alone cannot say. */
98
+ readonly corpus: Corpus
99
+ readonly exitCode: number
100
+ readonly counts?: Record<string, number>
101
+ /** Why the audit did not report, present only on `unmeasured`. */
102
+ readonly reason?: string
103
+ }
104
+
105
+ /** The exit code every command here sets when it refuses. */
106
+ const EXIT_REFUSED = 1
107
+
108
+ /** The exit code every command here sets when it carries findings. */
109
+ const EXIT_FINDINGS = 2
110
+
111
+ function asObject(value: unknown): Record<string, unknown> | undefined {
112
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
113
+ return undefined
114
+ }
115
+ return value as Record<string, unknown>
116
+ }
117
+
118
+ function lengthOf(value: unknown): number | undefined {
119
+ return Array.isArray(value) ? value.length : undefined
120
+ }
121
+
122
+ /**
123
+ * Folds a set of per-key readings into one record, or `undefined` when any of
124
+ * them could not be read. One unreadable key means the record is not the shape
125
+ * the extractor was written against, and reporting the rest would publish a
126
+ * partial count under a name that claims to be whole.
127
+ */
128
+ function allOf(
129
+ readings: Record<string, number | undefined>,
130
+ ): Record<string, number> | undefined {
131
+ const counts: Record<string, number> = {}
132
+ for (const [key, value] of Object.entries(readings)) {
133
+ if (value === undefined) return undefined
134
+ counts[key] = value
135
+ }
136
+ return counts
137
+ }
138
+
139
+ /**
140
+ * Reads the architecture record's three measures, or nothing when the project
141
+ * carries no record.
142
+ *
143
+ * Three states rather than two, matching what the verb publishes. The key
144
+ * absent is a run that never opened the record, which the aggregate never asks
145
+ * for and so reads as a shape that moved. Null is a project entitled to carry
146
+ * no record, whose other context counts still stand, so it contributes no key
147
+ * rather than a zero that would read as a conforming record.
148
+ */
149
+ function architectureCounts(
150
+ root: Record<string, unknown>,
151
+ ): Record<string, number> | undefined | 'absent' {
152
+ if (!('architecture' in root)) return undefined
153
+ if (root.architecture === null) return 'absent'
154
+
155
+ const record = asObject(root.architecture)
156
+ const decisions = record?.decisions
157
+ if (
158
+ record === undefined ||
159
+ !Array.isArray(decisions) ||
160
+ typeof record.lines !== 'number'
161
+ ) {
162
+ return undefined
163
+ }
164
+
165
+ // A record stating no length rule has no ceiling to be past, and reporting
166
+ // zero there would read as one measured and found conforming.
167
+ const ceiling =
168
+ typeof record.ceiling === 'number' ? record.ceiling : undefined
169
+
170
+ let unverifiable = 0
171
+ let unchecked = 0
172
+ for (const raw of decisions) {
173
+ const entry = asObject(raw)
174
+ const claim = entry?.claim
175
+ const checks = lengthOf(entry?.checks)
176
+ if (typeof claim !== 'string' || checks === undefined) return undefined
177
+
178
+ if (claim === 'neither') unverifiable += 1
179
+ else if (checks === 0) unchecked += 1
180
+ }
181
+
182
+ return {
183
+ // A boolean, counted so the aggregate reads it the way it reads every
184
+ // other measure. The verb gates on it separately, and the key is absent
185
+ // rather than zero on a record that declared no ceiling.
186
+ ...(ceiling !== undefined && {
187
+ recordOverLength: record.lines > ceiling ? 1 : 0,
188
+ }),
189
+ recordUnverifiable: unverifiable,
190
+ recordUnchecked: unchecked,
191
+ }
192
+ }
193
+
194
+ function contextCounts(record: unknown): Record<string, number> | undefined {
195
+ const root = asObject(record)
196
+ if (root === undefined) return undefined
197
+
198
+ const entries = root.entries
199
+ if (!Array.isArray(entries)) return undefined
200
+
201
+ let bareReferences = 0
202
+ for (const entry of entries) {
203
+ const bare = lengthOf(asObject(entry)?.bareReferences)
204
+ if (bare === undefined) return undefined
205
+ bareReferences += bare
206
+ }
207
+
208
+ const architecture = architectureCounts(root)
209
+ if (architecture === undefined) return undefined
210
+
211
+ const counts = allOf({
212
+ unresolvedCitations: lengthOf(asObject(root.citations)?.unresolved),
213
+ longEntries: lengthOf(root.length),
214
+ missingSections: lengthOf(root.missingSections),
215
+ indexDrift: lengthOf(root.indexDrift),
216
+ bareReferences,
217
+ })
218
+
219
+ if (counts === undefined) return undefined
220
+ return architecture === 'absent' ? counts : { ...counts, ...architecture }
221
+ }
222
+
223
+ function markdownCounts(record: unknown): Record<string, number> | undefined {
224
+ const root = asObject(record)
225
+ if (root === undefined) return undefined
226
+
227
+ const entries = root.entries
228
+ if (!Array.isArray(entries)) return undefined
229
+
230
+ const depth = asObject(root.checkpoints)?.run
231
+ // A depth reading needs the checkpoint it is measured against. Counting zero
232
+ // without it reports a corpus nobody measured for depth as one with no file
233
+ // past the line.
234
+ if (entries.length > 0 && typeof depth !== 'number') return undefined
235
+
236
+ let bans = 0
237
+ let heavyBullets = 0
238
+ let heavyParagraphs = 0
239
+ let filesPastDepth = 0
240
+ let flatParagraphs = 0
241
+
242
+ for (const raw of entries) {
243
+ const entry = asObject(raw)
244
+ if (entry === undefined) return undefined
245
+
246
+ const entryBans = lengthOf(entry.bans)
247
+ const bullets = lengthOf(entry.heavyBullets)
248
+ const paragraphs = lengthOf(entry.heavyParagraphs)
249
+ const run = entry.longestRun
250
+ if (
251
+ entryBans === undefined ||
252
+ bullets === undefined ||
253
+ paragraphs === undefined ||
254
+ typeof run !== 'number'
255
+ ) {
256
+ return undefined
257
+ }
258
+
259
+ bans += entryBans
260
+ heavyBullets += bullets
261
+ heavyParagraphs += paragraphs
262
+ if (typeof depth === 'number' && run > depth) filesPastDepth += 1
263
+
264
+ // A file below the measuring floor carries no cadence key at all, which is
265
+ // a file that was not measured rather than one with no flat paragraph.
266
+ const flat = asObject(entry.cadence)?.flat
267
+ if (typeof flat === 'number') flatParagraphs += flat
268
+ }
269
+
270
+ return { bans, heavyBullets, heavyParagraphs, filesPastDepth, flatParagraphs }
271
+ }
272
+
273
+ /** The finding arrays `canon claude skills audit` publishes, in its own order. */
274
+ const SKILL_FINDINGS = [
275
+ 'missingRequirement',
276
+ 'nameMismatch',
277
+ 'missingDescription',
278
+ 'longDescription',
279
+ 'readme',
280
+ 'folderName',
281
+ 'requirementSections',
282
+ ] as const
283
+
284
+ function skillCounts(record: unknown): Record<string, number> | undefined {
285
+ const findings = asObject(asObject(record)?.findings)
286
+ if (findings === undefined) return undefined
287
+
288
+ return allOf(
289
+ Object.fromEntries(
290
+ SKILL_FINDINGS.map((key) => [key, lengthOf(findings[key])]),
291
+ ),
292
+ )
293
+ }
294
+
295
+ /**
296
+ * Reads the unqualified citations alone, leaving the qualified ones out.
297
+ *
298
+ * A qualified citation is a repair that already landed, so folding the two
299
+ * together would report a corpus getting worse every time one is fixed. The
300
+ * key is still read rather than assumed present, since a record carrying
301
+ * neither array is a shape that moved rather than a catalog with nothing in it.
302
+ */
303
+ function reachCounts(record: unknown): Record<string, number> | undefined {
304
+ const root = asObject(record)
305
+ if (root === undefined || !Array.isArray(root.qualified)) return undefined
306
+
307
+ return allOf({ unqualifiedCitations: lengthOf(root.unqualified) })
308
+ }
309
+
310
+ /**
311
+ * Reads the routing measure's miss and unmeasurable counts, leaving `rank1`
312
+ * and `top3` out. Both move in lockstep with `misses` over a fixed-size
313
+ * corpus, so carrying them would report one movement three times, the same
314
+ * reasoning `restatedCounts` already takes over its own pair of derived
315
+ * totals. `unmeasurable` is retained separately, since it counts a defect in
316
+ * the instrument rather than a collision the catalog itself carries.
317
+ */
318
+ function rankCounts(record: unknown): Record<string, number> | undefined {
319
+ const root = asObject(record)
320
+ if (root === undefined) return undefined
321
+
322
+ return allOf({
323
+ misses: lengthOf(root.misses),
324
+ unmeasurable: lengthOf(root.unmeasurable),
325
+ })
326
+ }
327
+
328
+ function boardCounts(record: unknown): Record<string, number> | undefined {
329
+ const root = asObject(record)
330
+ if (root === undefined) return undefined
331
+
332
+ // The board keeps its untested rows apart from its findings on purpose, so
333
+ // they move no exit code. Folding them together here would undo that.
334
+ return allOf({
335
+ findings: lengthOf(root.findings),
336
+ untested: lengthOf(root.untested),
337
+ })
338
+ }
339
+
340
+ /**
341
+ * Reads the advisory verb's per-severity object rather than a total.
342
+ *
343
+ * A single count folds a critical advisory into a low one, and the response to
344
+ * the two differs. The keys are read off the record rather than listed here,
345
+ * so a severity the index adds is carried instead of silently dropped.
346
+ */
347
+ function advisoryCounts(record: unknown): Record<string, number> | undefined {
348
+ const severities = asObject(asObject(record)?.severities)
349
+ if (severities === undefined) return undefined
350
+
351
+ const counts: Record<string, number> = {}
352
+ for (const [severity, value] of Object.entries(severities)) {
353
+ if (typeof value !== 'number') return undefined
354
+ counts[`advisories-${severity}`] = value
355
+ }
356
+
357
+ return counts
358
+ }
359
+
360
+ /**
361
+ * Reads the uncovered paths alone, which is the half that is a finding.
362
+ *
363
+ * The declined paths are deliberately not counted. They are whichever declined
364
+ * rows this branch happened to touch rather than a measure of the map, so a
365
+ * clean trunk reads zero while the map declares eight, and the number would
366
+ * describe the branch rather than the decision.
367
+ *
368
+ * Retaining it also broke the verdict. `classify` reads a clean run as quiet
369
+ * only when every count is zero, so a branch touching one declined path exited
370
+ * 0 and still reported as carrying findings.
371
+ */
372
+ function labelCoverageCounts(
373
+ record: unknown,
374
+ ): Record<string, number> | undefined {
375
+ const root = asObject(record)
376
+ if (root === undefined) return undefined
377
+
378
+ return allOf({ uncovered: lengthOf(root.uncovered) })
379
+ }
380
+
381
+ /**
382
+ * Reads the two classes that are findings, leaving the declared mirrors out.
383
+ *
384
+ * A mirror is an authoring root and its consumed copy, which duplicate on
385
+ * purpose, so folding them in would report a corpus getting worse every time a
386
+ * seed is kept in step with the file it is authored from. Same reasoning the
387
+ * label audit drops its declined rows on.
388
+ *
389
+ * The reach count is left out for a different reason. It counts how many
390
+ * surfaces an instruction reached rather than whether anything is wrong, and it
391
+ * moves in lockstep with the two retained here, so a baseline carrying it would
392
+ * report one movement twice.
393
+ */
394
+ function restatedCounts(record: unknown): Record<string, number> | undefined {
395
+ const counts = asObject(asObject(record)?.counts)
396
+ if (counts === undefined) return undefined
397
+
398
+ return allOf({
399
+ contradictions:
400
+ typeof counts.contradictions === 'number'
401
+ ? counts.contradictions
402
+ : undefined,
403
+ repetitions:
404
+ typeof counts.repetitions === 'number' ? counts.repetitions : undefined,
405
+ })
406
+ }
407
+
408
+ function findingsOnly(record: unknown): Record<string, number> | undefined {
409
+ const root = asObject(record)
410
+ if (root === undefined) return undefined
411
+
412
+ return allOf({ findings: lengthOf(root.findings) })
413
+ }
414
+
415
+ function commentCounts(record: unknown): Record<string, number> | undefined {
416
+ const snapshot = asObject(record)?.snapshot
417
+ if (!Array.isArray(snapshot)) return undefined
418
+
419
+ let degradationHits = 0
420
+ for (const language of snapshot) {
421
+ const hits = lengthOf(asObject(language)?.degradationHits)
422
+ if (hits === undefined) return undefined
423
+ degradationHits += hits
424
+ }
425
+
426
+ return { degradationHits }
427
+ }
428
+
429
+ /**
430
+ * Reads the census's three headline totals, leaving the extension breakdown
431
+ * out. The breakdown is what a caller reads for its own sake, and folding
432
+ * every extension into the retained baseline would grow the recorded key set
433
+ * with every language the tree ever picks up.
434
+ */
435
+ function censusCounts(record: unknown): Record<string, number> | undefined {
436
+ const root = asObject(record)
437
+ if (root === undefined || !Array.isArray(root.byExtension)) return undefined
438
+
439
+ return allOf({
440
+ files: typeof root.files === 'number' ? root.files : undefined,
441
+ skipped: typeof root.skipped === 'number' ? root.skipped : undefined,
442
+ lines: typeof root.lines === 'number' ? root.lines : undefined,
443
+ })
444
+ }
445
+
446
+ function testOrderCounts(record: unknown): Record<string, number> | undefined {
447
+ const root = asObject(record)
448
+ if (root === undefined) return undefined
449
+
450
+ // The unclassified count travels with the findings because it is the honest
451
+ // shape of this measure: a refactor and a new behavior cannot be told apart
452
+ // from history, and a findings count alone claims coverage it does not have.
453
+ return allOf({
454
+ findings: lengthOf(root.findings),
455
+ unclassified: lengthOf(root.unclassified),
456
+ })
457
+ }
458
+
459
+ /** The record kinds `canon records validate` takes, and the corpus each reads. */
460
+ const RECORD_KINDS: readonly (readonly [string, Corpus])[] = [
461
+ ['plans', 'per-machine'],
462
+ ['groundwork', 'per-machine'],
463
+ ['intake', 'per-machine'],
464
+ ['memory', 'per-machine'],
465
+ ['standards', 'tracked'],
466
+ ['teach', 'per-machine'],
467
+ ]
468
+
469
+ /**
470
+ * Every audit the aggregate runs.
471
+ *
472
+ * `context`, `markdown`, and `skills` gate because the merge gate in
473
+ * `src/gate/stages.ts` already fails a push on each. `secrets` is the one entry that gates without
474
+ * a stage behind it, added deliberately rather than as a side effect, since a
475
+ * credential in the published tree is a fact and the split this repository
476
+ * records gates a fact and reports a judgment. Weigh any further addition
477
+ * against that test rather than against the count.
478
+ *
479
+ * Each verb runs once in its fullest form. Running the gating half separately
480
+ * would walk the same tree twice for a number the full record already carries.
481
+ */
482
+ export const AUDITS: readonly AuditSpec[] = [
483
+ {
484
+ id: 'context',
485
+ label: 'Context folders',
486
+ argv: ['context', 'audit', '--json'],
487
+ gatingExits: [EXIT_FINDINGS],
488
+ corpus: 'tracked',
489
+ // The one reason this verb refuses for that is an absence rather than a
490
+ // break, on the same test the reach check and the skill audit take: no
491
+ // target adopts `.claude/context/`, `.claude/diagrams/`, and
492
+ // `.claude/wireframes/` all at once, so without the allowance every such
493
+ // project reports the verb unmeasured on every run and never changes.
494
+ absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
495
+ counts: contextCounts,
496
+ },
497
+ {
498
+ id: 'markdown',
499
+ label: 'Markdown corpus',
500
+ argv: ['markdown', 'audit', '--json'],
501
+ // 3 is the empty ban set, which is the corpus walked with nothing looked
502
+ // for. That is a broken check rather than a clean tree, so it fails here
503
+ // the way it already fails the push.
504
+ gatingExits: [EXIT_FINDINGS, 3],
505
+ corpus: 'tracked',
506
+ counts: markdownCounts,
507
+ },
508
+ {
509
+ id: 'skills',
510
+ label: 'Skill corpora',
511
+ argv: ['claude', 'skills', 'audit', '--json'],
512
+ gatingExits: [EXIT_FINDINGS],
513
+ corpus: 'tracked',
514
+ // The one reason this verb refuses for, and it is an absence for the same
515
+ // reason the reach check's is: a project carrying neither `claude/skills/`
516
+ // nor `.claude/skills/` has adopted no skill convention this audit reads.
517
+ absentReasons: ['no-corpus'] satisfies SkillsAuditRefusal[],
518
+ counts: skillCounts,
519
+ },
520
+ {
521
+ id: 'skills-reach',
522
+ label: 'Shipped citation reach',
523
+ argv: ['claude', 'skills', 'reach', '--json'],
524
+ // Reports rather than gates, on the split this file already draws. A body
525
+ // naming a toolkit path is sometimes correct, since the instruction may be
526
+ // meant for a session in this repository, so the verdict is a judgment and
527
+ // a push failing on one teaches a contributor to route around the stage.
528
+ gatingExits: [],
529
+ corpus: 'tracked',
530
+ // The one reason this verb refuses for, and it is an absence rather than a
531
+ // break. A tracked corpus normally allows nothing, since a tree that ships
532
+ // to targets and cannot be found is a broken checkout, and this is the
533
+ // second exception on the same test the secret scan takes: a project
534
+ // carrying neither `claude/skills/` nor `.claude/skills/` has adopted no
535
+ // skill convention, so without the allowance it reports the verb
536
+ // unmeasured on every run and never changes, which is the permanent signal
537
+ // the per-machine allowance exists against.
538
+ absentReasons: ['no-skills'] satisfies ReachRefusal[],
539
+ counts: reachCounts,
540
+ },
541
+ {
542
+ id: 'skills-rank',
543
+ label: 'Skill routing measure',
544
+ argv: ['claude', 'skills', 'rank', '--json'],
545
+ // Reports rather than gates. The corpus is a first run with no baseline to
546
+ // fail a push against, and a lexical ranker bounds a necessary condition
547
+ // rather than reporting real routing behavior, so a push failing on a
548
+ // moved rank would teach a contributor to route around the stage.
549
+ gatingExits: [],
550
+ corpus: 'tracked',
551
+ // The one reason this run refuses for, and it is an absence for the same
552
+ // reason the reach check's is: a project carrying neither `claude/skills/`
553
+ // nor `.claude/skills/` has adopted no skill convention this audit reads.
554
+ // The two case-corpus refusals never reach here, since they need `--cases`
555
+ // and this argv passes none.
556
+ absentReasons: ['no-skills'] satisfies RankRefusal[],
557
+ counts: rankCounts,
558
+ },
559
+ {
560
+ id: 'tasks',
561
+ label: 'Task board',
562
+ argv: ['tasks', 'validate', '--json'],
563
+ gatingExits: [],
564
+ corpus: 'per-machine',
565
+ counts: boardCounts,
566
+ },
567
+ ...RECORD_KINDS.map(([kind, corpus]) => ({
568
+ id: `records-${kind}`,
569
+ label: `Records: ${kind}`,
570
+ argv: ['records', 'validate', kind, '--json'],
571
+ gatingExits: [],
572
+ corpus,
573
+ // `standards` is the one tracked kind here, so it takes none of the
574
+ // per-machine default the other five inherit from their corpus. A target
575
+ // reads standards through `canon standards` rather than a copy in its own
576
+ // tree, so carrying no standards folder at all is the
577
+ // ordinary state of every project but this repository.
578
+ ...(kind === 'standards' && {
579
+ absentReasons: ['no-folder'] satisfies RecordRefusal[],
580
+ }),
581
+ counts: findingsOnly,
582
+ })),
583
+ {
584
+ id: 'comments',
585
+ label: 'Comment census',
586
+ argv: ['comments', 'scan', '--json'],
587
+ gatingExits: [],
588
+ corpus: 'tracked',
589
+ counts: commentCounts,
590
+ },
591
+ {
592
+ id: 'test-order',
593
+ label: 'Test order',
594
+ argv: ['gov', 'test-order', '--json'],
595
+ gatingExits: [],
596
+ corpus: 'tracked',
597
+ counts: testOrderCounts,
598
+ },
599
+ {
600
+ id: 'secrets',
601
+ label: 'Shipped tree secrets',
602
+ argv: ['secrets', 'scan', '--json'],
603
+ // The fourth gate, and the first added since the note above was written.
604
+ // A credential-shaped value sitting in the tree this repository publishes
605
+ // is a fact rather than a judgment, which is the test that note asks any
606
+ // addition to pass. It is decided here and stated in the context entry
607
+ // rather than arriving as a side effect of registering a measure.
608
+ gatingExits: [EXIT_FINDINGS],
609
+ corpus: 'tracked',
610
+ // The three reasons that mean this tree publishes nothing, which is where
611
+ // most targets installing this CLI sit. Without the allowance the
612
+ // aggregate reports `incomplete` on every run in every such project and
613
+ // never changes, which is the permanent signal the per-machine allowance
614
+ // exists against.
615
+ //
616
+ // Two reasons are deliberately left out, and both are a corpus that exists
617
+ // and went unread. `no-git` is a broken checkout, and `no-files-field` is a
618
+ // publish that would pack the whole tree, so calling either an absence
619
+ // would report a pass over a shipped tree nobody measured.
620
+ absentReasons: [
621
+ 'no-manifest',
622
+ 'no-publish',
623
+ 'no-shipped-files',
624
+ ] satisfies ScanRefusal[],
625
+ counts: findingsOnly,
626
+ },
627
+ {
628
+ id: 'labels',
629
+ label: 'Pull request label coverage',
630
+ argv: ['labels', 'audit', '--json'],
631
+ // Reports rather than gates, on the split this file already draws. Whether
632
+ // an uncovered surface deserves a label is a judgment, and a push failing
633
+ // on one would ask a contributor to answer a question only the person who
634
+ // owns the surface can.
635
+ gatingExits: [],
636
+ // The map is committed, so every clone reads the same rows and a delta is
637
+ // shared. The changed set the rows are read against is the branch's, which
638
+ // is what makes a clean trunk report zero rather than nothing.
639
+ corpus: 'tracked',
640
+ // Only `uncovered` is retained, for the reason its extractor states.
641
+ //
642
+ // The one reason that means this project declares no surfaces to cover,
643
+ // which is the recorded decision that a project without a map is labelled
644
+ // silently. A tracked corpus normally allows nothing absent, and this is
645
+ // the second exception beside the secret scan rather than a default.
646
+ //
647
+ // The other three are deliberately left out. A map that will not parse, a
648
+ // map with no usable row, and a range git could not answer are each a
649
+ // corpus that exists and went unread, so calling any of them an absence
650
+ // would report a pass over a branch nobody measured.
651
+ absentReasons: ['no-map'] satisfies LabelAuditRefusal[],
652
+ counts: labelCoverageCounts,
653
+ },
654
+ {
655
+ id: 'restated',
656
+ label: 'Restated instructions',
657
+ argv: ['gov', 'restated', '--json'],
658
+ // Reports rather than gates, on the split this file already draws. Whether
659
+ // a rule stated on two surfaces should be stated on one is a judgment the
660
+ // person owning the surface takes, and most restatements here are correct,
661
+ // so a push failing on one would fail on the ordinary case.
662
+ gatingExits: [],
663
+ corpus: 'tracked',
664
+ // Both reasons the sweep refuses for, and each is an absence rather than a
665
+ // break. A target holds neither the seed nor a shipped skills tree, so
666
+ // without the allowance every project installing this CLI reports the verb
667
+ // unmeasured on every run and never changes, which is the permanent signal
668
+ // the per-machine allowance exists against. Same shape as the reach verb.
669
+ absentReasons: [
670
+ 'no-instructions',
671
+ 'no-surfaces',
672
+ ] satisfies RestatedRefusal[],
673
+ counts: restatedCounts,
674
+ },
675
+ {
676
+ id: 'counts',
677
+ label: 'Self-stated catalog counts',
678
+ argv: ['gov', 'counts', '--json'],
679
+ // Reports rather than gates, on the split this file already draws. The
680
+ // false-positive rate is read off the first real run rather than assumed
681
+ // ahead of it, and gating a measure with an unmeasured rate is what teaches
682
+ // a contributor to route around the stage.
683
+ gatingExits: [],
684
+ corpus: 'tracked',
685
+ counts: findingsOnly,
686
+ },
687
+ {
688
+ id: 'census',
689
+ label: 'Codebase census',
690
+ argv: ['census', '--json'],
691
+ // Reports rather than gates. A file count is a measure, not a judgment, so
692
+ // registering it here exists to retain growth against the baseline rather
693
+ // than to fail a push on a number moving.
694
+ gatingExits: [],
695
+ corpus: 'tracked',
696
+ counts: censusCounts,
697
+ },
698
+ {
699
+ id: 'deps',
700
+ label: 'Dependency advisories',
701
+ argv: ['deps', 'audit', '--json'],
702
+ // Reports rather than gates, on the same split. A published advisory is a
703
+ // fact about the index and a judgment about this tree, since the upgrade
704
+ // may not exist yet, and a push failing on one teaches a contributor to
705
+ // route around the stage while nothing about the dependency has changed.
706
+ gatingExits: [],
707
+ corpus: 'upstream',
708
+ counts: advisoryCounts,
709
+ },
710
+ ]
711
+
712
+ export function auditFor(id: string): AuditSpec | undefined {
713
+ return AUDITS.find((audit) => audit.id === id)
714
+ }
715
+
716
+ export function isTracked(spec: AuditSpec): boolean {
717
+ return spec.corpus === 'tracked'
718
+ }
719
+
720
+ export function countsFor(
721
+ spec: AuditSpec,
722
+ record: unknown,
723
+ ): Record<string, number> | undefined {
724
+ return spec.counts(record)
725
+ }
726
+
727
+ /**
728
+ * Reads the reason a refusing verb published, so the aggregate names the cause
729
+ * rather than only the exit. Every command here puts it in `reason`, and the
730
+ * ones that also carry a sentence put that in `message`.
731
+ */
732
+ function refusalReason(record: unknown, exitCode: number): string {
733
+ const root = asObject(record)
734
+ const reason = typeof root?.reason === 'string' ? root.reason : undefined
735
+ const message = typeof root?.message === 'string' ? root.message : undefined
736
+
737
+ if (reason !== undefined && message !== undefined) {
738
+ return `${reason}: ${message}`
739
+ }
740
+ return reason ?? message ?? `exited ${exitCode} with no reason on stdout`
741
+ }
742
+
743
+ /**
744
+ * The reasons a verb publishes when the folder it reads is simply not there.
745
+ *
746
+ * Typed against the unions those two verbs export rather than spelled as bare
747
+ * strings, so renaming a reason at its source fails this build. A literal here
748
+ * would go on matching nothing and quietly turn every expected absence back
749
+ * into an unmeasured audit.
750
+ */
751
+ const ABSENT_REASONS: readonly (RecordRefusal | BoardRefusal)[] = [
752
+ 'no-folder',
753
+ 'no-board',
754
+ ]
755
+
756
+ /**
757
+ * Every reason the advisory verb refuses for, all of which are an absence.
758
+ *
759
+ * An index it could not reach, a project that is not JavaScript, and one whose
760
+ * dependencies were never resolved are three states in which there is nothing
761
+ * to measure rather than something broken. The verb has no fourth reason, so
762
+ * this is its whole union rather than a chosen subset, and typing it that way
763
+ * fails the build if a later reason arrives without this decision being made.
764
+ *
765
+ * Typed against that module's own union for the reason the record reasons are:
766
+ * a literal here would go on matching nothing after a rename and turn every
767
+ * offline run back into an unmeasured audit.
768
+ */
769
+ const ADVISORY_ABSENT_REASONS: readonly AuditRefusal[] = [
770
+ 'no-record',
771
+ 'no-lockfile',
772
+ 'no-manifest',
773
+ ]
774
+
775
+ /**
776
+ * Whether a refusal is a folder this machine never created rather than a break.
777
+ *
778
+ * Every gitignored record folder is absent on a fresh clone and in CI, so six
779
+ * of the twelve refuse there on every run. Counting those as unmeasured pins
780
+ * the verdict at `incomplete` forever, and a signal that never changes is one
781
+ * nobody reads after the second time they see it.
782
+ *
783
+ * A tracked corpus gets no such allowance. That tree ships to targets, so a
784
+ * checkout that cannot find it is broken, and reading the absence as ordinary
785
+ * would report a pass over a corpus nobody measured.
786
+ *
787
+ * An upstream corpus takes the allowance for a different reason. Its index is
788
+ * off this machine, so an offline run reaches nothing through no fault of the
789
+ * tree, and pinning the verdict at incomplete every time the network is down
790
+ * is the same signal-nobody-reads failure the per-machine case already names.
791
+ */
792
+ function absentReasonsFor(spec: AuditSpec): readonly string[] {
793
+ if (spec.absentReasons !== undefined) return spec.absentReasons
794
+ if (spec.corpus === 'per-machine') return ABSENT_REASONS
795
+ if (spec.corpus === 'upstream') return ADVISORY_ABSENT_REASONS
796
+ return []
797
+ }
798
+
799
+ function isExpectedAbsence(spec: AuditSpec, record: unknown): boolean {
800
+ const allowed = absentReasonsFor(spec)
801
+ if (allowed.length === 0) return false
802
+
803
+ const reason = asObject(record)?.reason
804
+ return typeof reason === 'string' && allowed.includes(reason)
805
+ }
806
+
807
+ /**
808
+ * Turns one verb's exit code and stdout into a result the aggregate can report.
809
+ *
810
+ * Unparseable output is `unmeasured` rather than clean, for the reason the
811
+ * verify pipeline already states about its own skipped stages: a run that does
812
+ * not report is a broken command, and skipping would report the pass the check
813
+ * exists to withhold.
814
+ */
815
+ export function classify(
816
+ spec: AuditSpec,
817
+ exitCode: number,
818
+ stdout: string,
819
+ ): AuditResult {
820
+ const base = {
821
+ id: spec.id,
822
+ label: spec.label,
823
+ tracked: isTracked(spec),
824
+ corpus: spec.corpus,
825
+ exitCode,
826
+ }
827
+
828
+ let record: unknown
829
+ let parsed = true
830
+ try {
831
+ record = JSON.parse(stdout)
832
+ } catch {
833
+ parsed = false
834
+ }
835
+
836
+ if (exitCode === EXIT_REFUSED) {
837
+ return {
838
+ ...base,
839
+ status: isExpectedAbsence(spec, record) ? 'absent' : 'unmeasured',
840
+ reason: parsed
841
+ ? refusalReason(record, exitCode)
842
+ : `refused with no record on stdout`,
843
+ }
844
+ }
845
+
846
+ const counts = parsed ? spec.counts(record) : undefined
847
+
848
+ if (spec.gatingExits.includes(exitCode)) {
849
+ return { ...base, status: 'finding', ...(counts && { counts }) }
850
+ }
851
+
852
+ if (exitCode !== 0 && exitCode !== EXIT_FINDINGS) {
853
+ return {
854
+ ...base,
855
+ status: 'unmeasured',
856
+ reason: `exited ${exitCode}, which this verb does not document`,
857
+ }
858
+ }
859
+
860
+ if (counts === undefined) {
861
+ return {
862
+ ...base,
863
+ status: 'unmeasured',
864
+ reason: parsed
865
+ ? 'the record did not carry the keys this audit reads'
866
+ : 'stdout carried no JSON record',
867
+ }
868
+ }
869
+
870
+ if (exitCode === EXIT_FINDINGS) {
871
+ return { ...base, status: 'reported', counts }
872
+ }
873
+
874
+ const quiet = Object.values(counts).every((value) => value === 0)
875
+ return { ...base, status: quiet ? 'clean' : 'reported', counts }
876
+ }