@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,1162 @@
1
+ import { existsSync, statSync } from 'node:fs'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { parseFrontmatter, readField } from '@/indexes/frontmatter'
5
+ import { linesOutsideFences } from '@/markdown/scan'
6
+ import {
7
+ TEACH_GLOSSARY,
8
+ TEACH_MISSION,
9
+ TEACH_RECORDS,
10
+ TEACH_REFERENCE,
11
+ TEACH_RESOURCES,
12
+ TEACH_SUCCESS_HEADING,
13
+ WORKSPACE_NAME,
14
+ } from '@/teach/workspace'
15
+
16
+ export const RECORD_KINDS = [
17
+ 'plans',
18
+ 'groundwork',
19
+ 'intake',
20
+ 'memory',
21
+ 'standards',
22
+ 'teach',
23
+ ] as const
24
+
25
+ export type RecordKind = (typeof RECORD_KINDS)[number]
26
+
27
+ /**
28
+ * The folders each kind reads, in precedence order.
29
+ *
30
+ * Standards carry two because the corpus authors at the project root and
31
+ * installs under `.claude/`. The authoring root wins where both exist, since the
32
+ * installed tree is a generated copy here and a finding fixed there is
33
+ * overwritten by the next regen. A project that consumed the corpus holds only
34
+ * the second, so one order serves both.
35
+ */
36
+ const FOLDERS_BY_KIND: Readonly<Record<RecordKind, readonly string[]>> = {
37
+ plans: [join('.claude', 'plans')],
38
+ groundwork: [join('.claude', 'groundwork')],
39
+ intake: [join('.claude', 'intake')],
40
+ memory: [join('.claude', 'memory')],
41
+ standards: ['standards', join('.claude', 'standards')],
42
+ teach: [join('.claude', 'teach')],
43
+ }
44
+
45
+ /**
46
+ * `unknown-kind` is raised at the argument boundary rather than by the walk, and
47
+ * it sits here because both reach a caller through the same `reason` field. A
48
+ * union covering only what the walk returns would type a record the command can
49
+ * emit as impossible.
50
+ */
51
+ export const VALIDATE_REFUSALS = ['no-folder', 'unknown-kind'] as const
52
+
53
+ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
54
+
55
+ export const FINDING_KINDS = [
56
+ 'name-malformed',
57
+ 'title-missing',
58
+ 'title-is-slug',
59
+ 'section-missing',
60
+ 'scope-unanchored',
61
+ 'entry-unreasoned',
62
+ 'suggestion-missing',
63
+ 'question-unanswerable',
64
+ 'frontmatter-incomplete',
65
+ 'date-malformed',
66
+ 'index-missing',
67
+ 'state-missing',
68
+ 'closing-partial',
69
+ 'item-incomplete',
70
+ 'category-mismatch',
71
+ ] as const
72
+
73
+ export type FindingKind = (typeof FINDING_KINDS)[number]
74
+
75
+ /**
76
+ * The transforms `migrate.ts` carries. Most finding kinds have none, since a
77
+ * transform is only safe where the old shape is recoverable from the file
78
+ * itself, so this stays optional on `Finding` rather than required.
79
+ */
80
+ export const FINDING_REMEDIES = ['category-from-name'] as const
81
+
82
+ export type FindingRemedy = (typeof FINDING_REMEDIES)[number]
83
+
84
+ export interface Finding {
85
+ readonly kind: FindingKind
86
+ /** The record the finding sits in, relative to the validated folder. */
87
+ readonly record: string
88
+ readonly subject: string
89
+ readonly message: string
90
+ readonly remedy?: FindingRemedy
91
+ }
92
+
93
+ export interface ValidateReport {
94
+ readonly ok: true
95
+ readonly kind: RecordKind
96
+ readonly records: number
97
+ readonly findings: readonly Finding[]
98
+ }
99
+
100
+ export interface ValidateRefused {
101
+ readonly ok: false
102
+ readonly reason: ValidateRefusal
103
+ readonly message: string
104
+ }
105
+
106
+ export type ValidateOutcome = ValidateReport | ValidateRefused
107
+
108
+ /** Every folder a kind would accept, whether or not it is on disk. */
109
+ export function recordDirs(root: string, kind: RecordKind): string[] {
110
+ return FOLDERS_BY_KIND[kind].map((folder) => join(root, folder))
111
+ }
112
+
113
+ /**
114
+ * The folder a kind reads. The first candidate on disk wins, and the first
115
+ * candidate stands in when none exists, so a refusal and a test fixture both
116
+ * name the location the kind prefers.
117
+ */
118
+ export function recordsDir(root: string, kind: RecordKind): string {
119
+ const dirs = recordDirs(root, kind)
120
+ return dirs.find((dir) => existsSync(dir)) ?? dirs[0]
121
+ }
122
+
123
+ export function isRecordKind(value: string): value is RecordKind {
124
+ return (RECORD_KINDS as readonly string[]).includes(value)
125
+ }
126
+
127
+ /**
128
+ * Whether a kind's folder is shared session scratch at the main worktree root.
129
+ *
130
+ * The five record folders are, so every session validates the records every
131
+ * other session reads. The corpus is tracked instead, so a linked worktree holds
132
+ * its own edited copy, and defaulting that kind to the main root would report on
133
+ * a tree the session never touched and say nothing about which one it read.
134
+ */
135
+ export function isSharedScratch(kind: RecordKind): boolean {
136
+ return kind !== 'standards'
137
+ }
138
+
139
+ const NONE_IDENTIFIED = 'None identified.'
140
+ const NUMBERED_FILE = /^\d{2}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
141
+
142
+ function finding(
143
+ kind: FindingKind,
144
+ record: string,
145
+ subject: string,
146
+ message: string,
147
+ remedy?: FindingRemedy,
148
+ ): Finding {
149
+ return { kind, record, subject, message, remedy }
150
+ }
151
+
152
+ async function listMarkdown(dir: string): Promise<string[]> {
153
+ const entries = await readdir(dir, { withFileTypes: true })
154
+
155
+ return entries
156
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
157
+ .map((entry) => entry.name)
158
+ .sort()
159
+ }
160
+
161
+ async function listFolders(dir: string): Promise<string[]> {
162
+ const entries = await readdir(dir, { withFileTypes: true })
163
+
164
+ return entries
165
+ .filter((entry) => entry.isDirectory())
166
+ .map((entry) => entry.name)
167
+ .sort()
168
+ }
169
+
170
+ const PLAN_NAME = /^feature-[a-z0-9]+(-[a-z0-9]+)*\.md$/
171
+ const PLAN_TITLE = /^#[ \t]+Feature:[ \t]+\S/
172
+ /**
173
+ * An entry names a file and says something about it. Both halves are tested as
174
+ * facts rather than as a syntax: a backticked span anywhere, and prose left over
175
+ * once the spans are removed.
176
+ *
177
+ * Requiring the path to lead and the reason to follow a colon was the first
178
+ * shape and it reported 80 of 178 archived plans. The corpus writes
179
+ * `- Label: prose naming a path` as often as `- path: reason`, and both name the
180
+ * file and say why, so the stricter rule measured a house style rather than a
181
+ * defect.
182
+ */
183
+ function statesReason(entry: string): boolean {
184
+ if (!/`[^`]+`/.test(entry)) return false
185
+
186
+ const prose = entry.replace(/`[^`]*`/g, '').replace(/^-[ \t]*/, '')
187
+ return /[A-Za-z0-9]/.test(prose)
188
+ }
189
+ const QUESTION_ITEM = /^\d+[a-z]?\.[ \t]+\S/
190
+
191
+ const PLAN_SECTIONS = [
192
+ 'Summary',
193
+ 'Constraints',
194
+ 'Files to touch',
195
+ 'Risks',
196
+ 'Questions',
197
+ ] as const
198
+
199
+ type PlanSection = (typeof PLAN_SECTIONS)[number]
200
+
201
+ const PLAN_REQUIRED: readonly PlanSection[] = [
202
+ 'Summary',
203
+ 'Files to touch',
204
+ 'Risks',
205
+ 'Questions',
206
+ ]
207
+
208
+ /**
209
+ * A line standing alone as a bold label or an H2, whatever it names. A plan is
210
+ * free to carry a section of its own, so the split has to see one to close the
211
+ * section above it.
212
+ */
213
+ const MARKER_LINE = /^(?:##[ \t]+(.+?)|\*\*(.+?):\*\*)[ \t]*$/
214
+
215
+ /**
216
+ * A section opens as a bold label or as an H2 and both count. The corpus writes
217
+ * `Summary` as a heading and the other four as bold labels, and roughly a fifth
218
+ * of it swaps one for the other. Reporting the variant would fail nearly every
219
+ * plan present on the rule a reader is least served by, which is what teaches
220
+ * them to skip the output.
221
+ */
222
+ export function sectionMarker(line: string): PlanSection | undefined {
223
+ const match = MARKER_LINE.exec(line.trim())
224
+ if (!match) return undefined
225
+
226
+ const name = match[1] ?? match[2]
227
+ return PLAN_SECTIONS.find((entry) => entry === name)
228
+ }
229
+
230
+ /** The spelling a finding names, which is the one the standard's template ships. */
231
+ export function preferredMarker(section: PlanSection): string {
232
+ return section === 'Summary' ? '## Summary' : `**${section}:**`
233
+ }
234
+
235
+ export function splitPlanSections(text: string): Map<string, string[]> {
236
+ const sections = new Map<string, string[]>()
237
+ let current: string | undefined
238
+
239
+ for (const line of linesOutsideFences(text)) {
240
+ // Any marker-shaped line closes the section above it, and only a recognized
241
+ // one opens a section. A plan carrying a label of its own would otherwise
242
+ // collect its bullets into whichever section came before.
243
+ if (MARKER_LINE.test(line.trim())) {
244
+ current = sectionMarker(line)
245
+ if (current) sections.set(current, [])
246
+ continue
247
+ }
248
+
249
+ if (current) sections.get(current)?.push(line)
250
+ }
251
+
252
+ return sections
253
+ }
254
+
255
+ interface Question {
256
+ readonly label: string
257
+ readonly body: readonly string[]
258
+ }
259
+
260
+ export function readQuestions(lines: readonly string[]): Question[] {
261
+ const questions: { label: string; body: string[] }[] = []
262
+
263
+ for (const line of lines) {
264
+ const trimmed = line.trim()
265
+ if (QUESTION_ITEM.test(trimmed)) {
266
+ questions.push({ label: trimmed, body: [] })
267
+ continue
268
+ }
269
+
270
+ questions.at(-1)?.body.push(trimmed)
271
+ }
272
+
273
+ return questions
274
+ }
275
+
276
+ function shorten(label: string): string {
277
+ return label.length > 60 ? `${label.slice(0, 57)}...` : label
278
+ }
279
+
280
+ function checkQuestionContract(name: string, lines: string[]): Finding[] {
281
+ if (lines.some((line) => line.trim() === NONE_IDENTIFIED)) return []
282
+
283
+ const findings: Finding[] = []
284
+
285
+ for (const question of readQuestions(lines)) {
286
+ const subject = shorten(question.label)
287
+
288
+ if (!question.body.some((line) => line.startsWith('- Suggested:'))) {
289
+ findings.push(
290
+ finding(
291
+ 'suggestion-missing',
292
+ name,
293
+ subject,
294
+ 'carries no Suggested line, so it arrives at execution as a stop.',
295
+ ),
296
+ )
297
+ }
298
+
299
+ if (!question.body.some((line) => line.startsWith('- Answer:'))) {
300
+ findings.push(
301
+ finding(
302
+ 'question-unanswerable',
303
+ name,
304
+ subject,
305
+ 'carries no Answer slot, so the blank-answer default has nowhere to sit.',
306
+ ),
307
+ )
308
+ }
309
+ }
310
+
311
+ return findings
312
+ }
313
+
314
+ export function checkPlan(name: string, text: string): Finding[] {
315
+ const findings: Finding[] = []
316
+
317
+ if (!PLAN_NAME.test(name)) {
318
+ findings.push(
319
+ finding(
320
+ 'name-malformed',
321
+ name,
322
+ name,
323
+ 'is not named feature-<slug>.md with a kebab-case slug.',
324
+ ),
325
+ )
326
+ }
327
+
328
+ const lines = linesOutsideFences(text)
329
+
330
+ if (!lines.some((line) => PLAN_TITLE.test(line))) {
331
+ findings.push(
332
+ finding('title-missing', name, name, 'opens with no # Feature: heading.'),
333
+ )
334
+ }
335
+
336
+ const sections = splitPlanSections(text)
337
+
338
+ for (const marker of PLAN_REQUIRED) {
339
+ if (!sections.has(marker)) {
340
+ findings.push(
341
+ finding(
342
+ 'section-missing',
343
+ name,
344
+ preferredMarker(marker),
345
+ 'is required and the plan carries no such section.',
346
+ ),
347
+ )
348
+ }
349
+ }
350
+
351
+ for (const line of sections.get('Files to touch') ?? []) {
352
+ const trimmed = line.trim()
353
+ if (!trimmed.startsWith('- ') || trimmed === `- ${NONE_IDENTIFIED}`)
354
+ continue
355
+
356
+ if (!statesReason(trimmed)) {
357
+ findings.push(
358
+ finding(
359
+ 'entry-unreasoned',
360
+ name,
361
+ shorten(trimmed),
362
+ 'names no file, or names one and says nothing about it.',
363
+ ),
364
+ )
365
+ }
366
+ }
367
+
368
+ findings.push(...checkQuestionContract(name, sections.get('Questions') ?? []))
369
+
370
+ return findings
371
+ }
372
+
373
+ const DATE_FIELD = /^date:[ \t]*'?"?(\d{4}-\d{2}-\d{2})'?"?[ \t]*$/m
374
+
375
+ /**
376
+ * Reads the opening date off the raw block rather than the parsed fields. A YAML
377
+ * parser resolves an unquoted `YYYY-MM-DD` to a date value on the core schema
378
+ * and to a string elsewhere, and a check keyed on the parsed type would report a
379
+ * conforming file on one runtime and not the other.
380
+ */
381
+ function hasOpeningDate(raw: string): boolean {
382
+ return DATE_FIELD.test(raw)
383
+ }
384
+
385
+ async function checkFolderFrontmatter(
386
+ dir: string,
387
+ slug: string,
388
+ files: readonly string[],
389
+ indexFile: string,
390
+ ): Promise<Finding[]> {
391
+ const perFile = await Promise.all(
392
+ files.map(async (file) => {
393
+ const found: Finding[] = []
394
+ const frontmatter = parseFrontmatter(
395
+ await readFile(join(dir, file), 'utf8'),
396
+ )
397
+
398
+ const missing = ['title', 'description'].filter(
399
+ (field) => !readField(frontmatter, field),
400
+ )
401
+
402
+ if (missing.length > 0) {
403
+ found.push(
404
+ finding(
405
+ 'frontmatter-incomplete',
406
+ slug,
407
+ file,
408
+ `carries no ${missing.join(' and no ')}.`,
409
+ ),
410
+ )
411
+ }
412
+
413
+ if (file === indexFile && !hasOpeningDate(frontmatter?.raw ?? '')) {
414
+ found.push(
415
+ finding(
416
+ 'date-malformed',
417
+ slug,
418
+ file,
419
+ 'carries no date field as YYYY-MM-DD, so the folder states no opening day.',
420
+ ),
421
+ )
422
+ }
423
+
424
+ if (file !== indexFile && !NUMBERED_FILE.test(file)) {
425
+ found.push(
426
+ finding(
427
+ 'name-malformed',
428
+ slug,
429
+ file,
430
+ 'is not numbered NN-<name>.md, so the folder has no read order.',
431
+ ),
432
+ )
433
+ }
434
+
435
+ return found
436
+ }),
437
+ )
438
+
439
+ return perFile.flat()
440
+ }
441
+
442
+ const GROUNDWORK_INDEX = 'README.md'
443
+ const GROUNDWORK_STATE = '01-current-state.md'
444
+ const GROUNDWORK_DECISION = '06-'
445
+ const GROUNDWORK_HANDOFF = '07-'
446
+
447
+ async function checkTrack(dir: string, slug: string): Promise<Finding[]> {
448
+ const files = await listMarkdown(dir)
449
+ const findings: Finding[] = []
450
+
451
+ if (!files.includes(GROUNDWORK_INDEX)) {
452
+ findings.push(
453
+ finding(
454
+ 'index-missing',
455
+ slug,
456
+ GROUNDWORK_INDEX,
457
+ 'is absent, so the track carries no file map and no reason it is running.',
458
+ ),
459
+ )
460
+ }
461
+
462
+ if (!files.includes(GROUNDWORK_STATE)) {
463
+ findings.push(
464
+ finding(
465
+ 'state-missing',
466
+ slug,
467
+ GROUNDWORK_STATE,
468
+ 'is absent, so the track states no measured current state.',
469
+ ),
470
+ )
471
+ }
472
+
473
+ // A track closes on the decision and the handoff together. One without the
474
+ // other reads as closed to anyone scanning filenames and strands the half a
475
+ // returning session actually opens.
476
+ const decided = files.some((file) => file.startsWith(GROUNDWORK_DECISION))
477
+ const handed = files.some((file) => file.startsWith(GROUNDWORK_HANDOFF))
478
+
479
+ if (decided !== handed) {
480
+ findings.push(
481
+ finding(
482
+ 'closing-partial',
483
+ slug,
484
+ decided ? GROUNDWORK_HANDOFF : GROUNDWORK_DECISION,
485
+ `is absent while ${decided ? '06' : '07'} is present, so the track is neither live nor closed.`,
486
+ ),
487
+ )
488
+ }
489
+
490
+ findings.push(
491
+ ...(await checkFolderFrontmatter(dir, slug, files, GROUNDWORK_INDEX)),
492
+ )
493
+
494
+ return findings
495
+ }
496
+
497
+ const INTAKE_INDEX = '00-overview.md'
498
+ const INTAKE_HANDOFF = '99-next-session.md'
499
+
500
+ const ITEM_HEADING = /^###[ \t]+\S/
501
+ const ITEM_REQUIRED = ['Problem', 'Fix', 'Worth it', 'You'] as const
502
+
503
+ function bulletLabel(line: string): string | undefined {
504
+ const match = /^-[ \t]+\*\*([^:*]+):\*\*/.exec(line.trim())
505
+ return match ? match[1].trim() : undefined
506
+ }
507
+
508
+ export function checkItems(
509
+ slug: string,
510
+ file: string,
511
+ text: string,
512
+ ): Finding[] {
513
+ const findings: Finding[] = []
514
+ const items: { heading: string; labels: string[] }[] = []
515
+
516
+ for (const line of linesOutsideFences(text)) {
517
+ if (ITEM_HEADING.test(line)) {
518
+ items.push({ heading: line.trim().replace(/^###[ \t]+/, ''), labels: [] })
519
+ continue
520
+ }
521
+
522
+ const label = bulletLabel(line)
523
+ if (label) items.at(-1)?.labels.push(label)
524
+ }
525
+
526
+ for (const item of items) {
527
+ const missing = ITEM_REQUIRED.filter(
528
+ (label) => !item.labels.includes(label),
529
+ )
530
+
531
+ if (missing.length > 0) {
532
+ findings.push(
533
+ finding(
534
+ 'item-incomplete',
535
+ slug,
536
+ `${file}: ${shorten(item.heading)}`,
537
+ `states no ${missing.join(', no ')}.`,
538
+ ),
539
+ )
540
+ }
541
+
542
+ if (item.labels.includes('Open') && !item.labels.includes('Suggested')) {
543
+ findings.push(
544
+ finding(
545
+ 'suggestion-missing',
546
+ slug,
547
+ `${file}: ${shorten(item.heading)}`,
548
+ 'asks an open question and suggests nothing, so a bare answer decides it.',
549
+ ),
550
+ )
551
+ }
552
+ }
553
+
554
+ return findings
555
+ }
556
+
557
+ async function checkDump(dir: string, slug: string): Promise<Finding[]> {
558
+ const files = await listMarkdown(dir)
559
+ const findings: Finding[] = []
560
+
561
+ if (!files.includes(INTAKE_INDEX)) {
562
+ findings.push(
563
+ finding(
564
+ 'index-missing',
565
+ slug,
566
+ INTAKE_INDEX,
567
+ 'is absent, so the dump carries no cluster table and no verdict counts.',
568
+ ),
569
+ )
570
+ }
571
+
572
+ findings.push(
573
+ ...(await checkFolderFrontmatter(dir, slug, files, INTAKE_INDEX)),
574
+ )
575
+
576
+ // The two reserved files hold no items. Running the item check over the
577
+ // handoff would report every heading it carries as a malformed item.
578
+ const clusters = files.filter(
579
+ (file) => file !== INTAKE_INDEX && file !== INTAKE_HANDOFF,
580
+ )
581
+
582
+ const perCluster = await Promise.all(
583
+ clusters.map(async (file) =>
584
+ checkItems(slug, file, await readFile(join(dir, file), 'utf8')),
585
+ ),
586
+ )
587
+
588
+ return [...findings, ...perCluster.flat()]
589
+ }
590
+
591
+ const NUMBERED_RECORD = /^\d{4}-[a-z0-9]+(-[a-z0-9]+)*\.md$/
592
+ /**
593
+ * A kebab slug that does not open with an ordinal. The lookahead rejects a
594
+ * leading run of digits followed by a hyphen and nothing else, so a subject
595
+ * whose own name starts with a digit still passes.
596
+ */
597
+ const REFERENCE_NAME = /^(?!\d+-)[a-z0-9]+(-[a-z0-9]+)*\.md$/
598
+
599
+ /** The two fields every markdown file in a workspace carries. */
600
+ async function checkTeachFile(
601
+ dir: string,
602
+ slug: string,
603
+ file: string,
604
+ subject: string,
605
+ ): Promise<Finding[]> {
606
+ const frontmatter = parseFrontmatter(await readFile(join(dir, file), 'utf8'))
607
+
608
+ const missing = ['title', 'description'].filter(
609
+ (field) => !readField(frontmatter, field),
610
+ )
611
+
612
+ if (missing.length === 0) return []
613
+
614
+ return [
615
+ finding(
616
+ 'frontmatter-incomplete',
617
+ slug,
618
+ subject,
619
+ `carries no ${missing.join(' and no ')}.`,
620
+ ),
621
+ ]
622
+ }
623
+
624
+ /**
625
+ * One markdown subfolder of a workspace. `lessons/` and `assets/` are never
626
+ * reached, because a lesson is generated markup carrying no frontmatter and a
627
+ * walk over it would report every one as malformed.
628
+ */
629
+ async function checkTeachSubfolder(
630
+ dir: string,
631
+ slug: string,
632
+ folder: string,
633
+ name: RegExp,
634
+ message: string,
635
+ ): Promise<Finding[]> {
636
+ const path = join(dir, folder)
637
+
638
+ // Tested as a directory rather than for presence. Every other walk in this
639
+ // module takes its path from `listFolders`, and this one is built from a
640
+ // fixed name, so a workspace holding a plain file called `reference` would
641
+ // reach `readdir` and take the whole run down with `ENOTDIR`.
642
+ if (!statSync(path, { throwIfNoEntry: false })?.isDirectory()) return []
643
+
644
+ const files = await listMarkdown(path)
645
+
646
+ const malformed = files
647
+ .filter((file) => !name.test(file))
648
+ .map((file) =>
649
+ finding('name-malformed', slug, `${folder}/${file}`, message),
650
+ )
651
+
652
+ const perFile = await Promise.all(
653
+ files.map((file) => checkTeachFile(path, slug, file, `${folder}/${file}`)),
654
+ )
655
+
656
+ return [...malformed, ...perFile.flat()]
657
+ }
658
+
659
+ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
660
+ const findings: Finding[] = []
661
+
662
+ if (!WORKSPACE_NAME.test(slug)) {
663
+ findings.push(
664
+ finding(
665
+ 'name-malformed',
666
+ slug,
667
+ slug,
668
+ 'is not named NN-<topic> with a two-digit ordinal, so a listing sorts alphabetically rather than by when each workspace opened.',
669
+ ),
670
+ )
671
+ }
672
+
673
+ const files = await listMarkdown(dir)
674
+
675
+ if (!files.includes(TEACH_MISSION)) {
676
+ findings.push(
677
+ finding(
678
+ 'index-missing',
679
+ slug,
680
+ TEACH_MISSION,
681
+ 'is absent, so the workspace states no subject and no success to finish against.',
682
+ ),
683
+ )
684
+ }
685
+
686
+ for (const required of [TEACH_RESOURCES, TEACH_GLOSSARY]) {
687
+ if (!files.includes(required)) {
688
+ findings.push(
689
+ finding(
690
+ 'section-missing',
691
+ slug,
692
+ required,
693
+ 'is required and the workspace carries no such file.',
694
+ ),
695
+ )
696
+ }
697
+ }
698
+
699
+ const perFile = await Promise.all(
700
+ files.map((file) => checkTeachFile(dir, slug, file, file)),
701
+ )
702
+ findings.push(...perFile.flat())
703
+
704
+ if (files.includes(TEACH_MISSION)) {
705
+ const text = await readFile(join(dir, TEACH_MISSION), 'utf8')
706
+
707
+ if (!hasOpeningDate(parseFrontmatter(text)?.raw ?? '')) {
708
+ findings.push(
709
+ finding(
710
+ 'date-malformed',
711
+ slug,
712
+ TEACH_MISSION,
713
+ 'carries no date field as YYYY-MM-DD, so the workspace states no opening day.',
714
+ ),
715
+ )
716
+ }
717
+
718
+ if (
719
+ !linesOutsideFences(text).some(
720
+ (line) => line.trim() === TEACH_SUCCESS_HEADING,
721
+ )
722
+ ) {
723
+ findings.push(
724
+ finding(
725
+ 'section-missing',
726
+ slug,
727
+ TEACH_SUCCESS_HEADING,
728
+ 'is absent, so the mission names no observable thing the learner will be able to do.',
729
+ ),
730
+ )
731
+ }
732
+ }
733
+
734
+ return [
735
+ ...findings,
736
+ ...(await checkTeachSubfolder(
737
+ dir,
738
+ slug,
739
+ TEACH_REFERENCE,
740
+ REFERENCE_NAME,
741
+ 'is not named <slug>.md as a kebab slug opening with no ordinal, so a page looked up rather than worked through implies an order no reader follows.',
742
+ )),
743
+ ...(await checkTeachSubfolder(
744
+ dir,
745
+ slug,
746
+ TEACH_RECORDS,
747
+ NUMBERED_RECORD,
748
+ 'is not numbered NNNN-<slug>.md, so the records carry no read order.',
749
+ )),
750
+ ]
751
+ }
752
+
753
+ const MEMORY_INDEX = 'index.md'
754
+ const MEMORY_FIELDS = ['title', 'description', 'category'] as const
755
+
756
+ /**
757
+ * The filename prefix and the `category` field are one fact in two spellings,
758
+ * so the map is the whole type list and the comparison against it is what
759
+ * catches a prefix outside the set, a field disagreeing with the prefix, and a
760
+ * casing drift that would open a second group in the catalog.
761
+ */
762
+ export const CATEGORY_BY_TYPE = {
763
+ feedback: 'Feedback',
764
+ project: 'Project',
765
+ user: 'User',
766
+ reference: 'Reference',
767
+ } as const
768
+
769
+ export type MemoryType = keyof typeof CATEGORY_BY_TYPE
770
+
771
+ const MEMORY_TYPES = Object.keys(CATEGORY_BY_TYPE) as readonly MemoryType[]
772
+
773
+ export const MEMORY_NAME = /^([a-z]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
774
+
775
+ /** The two markers a rule-bearing body carries, on top of the rule line itself. */
776
+ const MEMORY_MARKERS = ['**Why:**', '**How to apply:**'] as const
777
+
778
+ export function memoryType(value: string): MemoryType | undefined {
779
+ return MEMORY_TYPES.find((type) => type === value)
780
+ }
781
+
782
+ /**
783
+ * The one missing-field shape `migrate.ts` can repair: `category` alone,
784
+ * recoverable from the same filename prefix `checkMemory` already read it
785
+ * from. `title` and `description` are prose nobody wrote down, so a finding
786
+ * naming either carries no remedy.
787
+ */
788
+ function memoryRemedy(
789
+ missing: readonly string[],
790
+ named: MemoryType | undefined,
791
+ ): FindingRemedy | undefined {
792
+ return named && missing.length === 1 && missing[0] === 'category'
793
+ ? 'category-from-name'
794
+ : undefined
795
+ }
796
+
797
+ export function checkMemory(name: string, text: string): Finding[] {
798
+ const findings: Finding[] = []
799
+ const match = MEMORY_NAME.exec(name)
800
+ const named = match ? memoryType(match[1]) : undefined
801
+
802
+ if (!named) {
803
+ findings.push(
804
+ finding(
805
+ 'name-malformed',
806
+ name,
807
+ name,
808
+ `is not named <type>-<slug>.md with a type of ${MEMORY_TYPES.join(', ')}.`,
809
+ ),
810
+ )
811
+ }
812
+
813
+ const frontmatter = parseFrontmatter(text)
814
+ const missing = MEMORY_FIELDS.filter(
815
+ (field) => !readField(frontmatter, field),
816
+ )
817
+
818
+ if (missing.length > 0) {
819
+ findings.push(
820
+ finding(
821
+ 'frontmatter-incomplete',
822
+ name,
823
+ name,
824
+ `carries no ${missing.join(' and no ')}.`,
825
+ memoryRemedy(missing, named),
826
+ ),
827
+ )
828
+ }
829
+
830
+ // Its own kind rather than `title-missing`, which means an absent heading on a
831
+ // plan. One kind covering both leaves a caller filtering the JSON unable to
832
+ // tell a record with no title from one whose title is its own slug.
833
+ if (readField(frontmatter, 'title') === name.replace(/\.md$/, '')) {
834
+ findings.push(
835
+ finding(
836
+ 'title-is-slug',
837
+ name,
838
+ name,
839
+ 'is titled with its own filename, so the catalog renders a slug where the rule belongs.',
840
+ ),
841
+ )
842
+ }
843
+
844
+ const category = readField(frontmatter, 'category')
845
+
846
+ // Reported against the prefix alone. A name the prefix rule already failed
847
+ // has no type to compare against, and reporting it twice names one defect as
848
+ // two.
849
+ if (named && category && category !== CATEGORY_BY_TYPE[named]) {
850
+ findings.push(
851
+ finding(
852
+ 'category-mismatch',
853
+ name,
854
+ category,
855
+ `is not ${CATEGORY_BY_TYPE[named]}, which the filename prefix declares.`,
856
+ ),
857
+ )
858
+ }
859
+
860
+ return [
861
+ ...findings,
862
+ ...checkMemoryBody(
863
+ name,
864
+ text.slice(frontmatter?.raw.length ?? 0),
865
+ named ?? category,
866
+ ),
867
+ ]
868
+ }
869
+
870
+ /**
871
+ * A `user` or `reference` entry is a single sentence by design, so the markers
872
+ * are checked only where a rule is being stated. The type is read off the
873
+ * prefix, falling back to the category so a misnamed file is still checked
874
+ * against the shape it claims.
875
+ */
876
+ function checkMemoryBody(
877
+ name: string,
878
+ text: string,
879
+ claimed: string | undefined,
880
+ ): Finding[] {
881
+ const type = claimed && memoryType(claimed.toLowerCase())
882
+ if (type !== 'feedback' && type !== 'project') return []
883
+
884
+ const body = linesOutsideFences(text).filter((line) => line.trim().length > 0)
885
+
886
+ const findings: Finding[] = []
887
+ const opening = body[0]
888
+
889
+ if (!opening || MEMORY_MARKERS.some((marker) => opening.startsWith(marker))) {
890
+ findings.push(
891
+ finding(
892
+ 'section-missing',
893
+ name,
894
+ 'the rule line',
895
+ 'is absent, so the entry carries a rationale with no rule to apply.',
896
+ ),
897
+ )
898
+ }
899
+
900
+ for (const marker of MEMORY_MARKERS) {
901
+ if (!body.some((line) => line.startsWith(marker))) {
902
+ findings.push(
903
+ finding(
904
+ 'section-missing',
905
+ name,
906
+ marker,
907
+ `is required on a ${type} entry and the body carries no such line.`,
908
+ ),
909
+ )
910
+ }
911
+ }
912
+
913
+ return findings
914
+ }
915
+
916
+ const STANDARD_INDEX = 'index.md'
917
+ const STANDARD_FIELDS = ['title', 'description'] as const
918
+
919
+ const SCOPE_HEADING = /^##[ \t]+Scope[ \t]*$/
920
+ const ANY_HEADING = /^#{1,6}[ \t]+\S/
921
+ const DOES_NOT_GOVERN = 'Does not govern:'
922
+ const ATTRIBUTE_MARKER = 'attribute standard'
923
+ const CODE_SPAN = /`([^`]+)`/g
924
+
925
+ interface Scope {
926
+ /** The first non-blank line under the heading, which is the statement. */
927
+ readonly statement: string
928
+ readonly lines: readonly string[]
929
+ }
930
+
931
+ export function readScope(text: string): Scope | undefined {
932
+ const lines = linesOutsideFences(text)
933
+ const opened = lines.findIndex((line) => SCOPE_HEADING.test(line.trim()))
934
+ if (opened === -1) return undefined
935
+
936
+ const body: string[] = []
937
+
938
+ for (const line of lines.slice(opened + 1)) {
939
+ if (ANY_HEADING.test(line.trim())) break
940
+ body.push(line)
941
+ }
942
+
943
+ const statement = body.find((line) => line.trim().length > 0)
944
+
945
+ return { statement: statement?.trim() ?? '', lines: body }
946
+ }
947
+
948
+ /**
949
+ * The paths a scope statement declares, read the way `scripts/standards/list.sh`
950
+ * reads them for the catalog's `appliesTo` field: backticked spans in the first
951
+ * sentence alone. One sentence read two ways would let a standard pass here
952
+ * while publishing a different jurisdiction to every consumer of the catalog.
953
+ */
954
+ export function governedPaths(statement: string): string[] {
955
+ const [sentence] = statement.split('. ')
956
+ return [...sentence.matchAll(CODE_SPAN)].map((match) => match[1])
957
+ }
958
+
959
+ /**
960
+ * The words a governed path offers a filename. Each segment gives its own word
961
+ * and, where it carries a prefix or a placeholder, the parts either side of a
962
+ * hyphen, so `.claude/tasks/session-<slug>.md` offers `tasks` and `session`.
963
+ *
964
+ * A dotted segment gives nothing. It names the folder holding the artifact
965
+ * rather than the artifact, and a standard named for it would pass this check
966
+ * while naming the container every sibling shares.
967
+ */
968
+ export function pathWords(path: string): string[] {
969
+ const words: string[] = []
970
+
971
+ for (const segment of path.split('/')) {
972
+ if (segment.startsWith('.')) continue
973
+
974
+ const stem = segment.replace(/\.[a-z]+$/i, '').toLowerCase()
975
+ words.push(stem)
976
+ if (stem.includes('-')) words.push(...stem.split('-'))
977
+ }
978
+
979
+ return words.filter((word) => /^[a-z]+$/.test(word))
980
+ }
981
+
982
+ /**
983
+ * Accepts the singular and the plural of one word. A standard over a single
984
+ * document is named for the document and one over a folder of them is named for
985
+ * either, and picking a side would report a conforming half of the corpus.
986
+ */
987
+ function namesWord(stem: string, word: string): boolean {
988
+ return stem === word || `${stem}s` === word || stem === `${word}s`
989
+ }
990
+
991
+ function checkStandardName(name: string, statement: string): Finding[] {
992
+ const paths = governedPaths(statement)
993
+
994
+ // The marker is read only where the first sentence backticks nothing, which
995
+ // is the catalog's own rule. A statement naming a path publishes that path
996
+ // however the rest of the statement describes itself.
997
+ if (paths.length === 0) {
998
+ if (statement.includes(ATTRIBUTE_MARKER)) return []
999
+
1000
+ return [
1001
+ finding(
1002
+ 'scope-unanchored',
1003
+ name,
1004
+ name,
1005
+ 'backticks no path in its first scope sentence and does not call itself an attribute standard, so it names no artifact to be named for.',
1006
+ ),
1007
+ ]
1008
+ }
1009
+
1010
+ const stem = name.replace(/\.md$/, '')
1011
+ const words = paths.flatMap(pathWords)
1012
+
1013
+ if (words.some((word) => namesWord(stem, word))) return []
1014
+
1015
+ return [
1016
+ finding(
1017
+ 'name-malformed',
1018
+ name,
1019
+ name,
1020
+ `names no part of ${paths.join(', ')}, which is what it governs. A rename reaches every target that installed the corpus and every surface citing it by bare filename.`,
1021
+ ),
1022
+ ]
1023
+ }
1024
+
1025
+ export function checkStandard(name: string, text: string): Finding[] {
1026
+ const findings: Finding[] = []
1027
+ const frontmatter = parseFrontmatter(text)
1028
+
1029
+ const missing = STANDARD_FIELDS.filter(
1030
+ (field) => !readField(frontmatter, field),
1031
+ )
1032
+
1033
+ if (missing.length > 0) {
1034
+ findings.push(
1035
+ finding(
1036
+ 'frontmatter-incomplete',
1037
+ name,
1038
+ name,
1039
+ `carries no ${missing.join(' and no ')}.`,
1040
+ ),
1041
+ )
1042
+ }
1043
+
1044
+ const scope = readScope(text)
1045
+
1046
+ // The name derives from the scope statement, so an absent section leaves
1047
+ // nothing to derive against. Reporting the name as well would name one defect
1048
+ // twice and point the fix at the wrong file.
1049
+ if (!scope) {
1050
+ return [
1051
+ ...findings,
1052
+ finding(
1053
+ 'section-missing',
1054
+ name,
1055
+ '## Scope',
1056
+ 'is absent, so the standard claims no jurisdiction and can refuse no rule.',
1057
+ ),
1058
+ ]
1059
+ }
1060
+
1061
+ if (!scope.lines.some((line) => line.trim().startsWith(DOES_NOT_GOVERN))) {
1062
+ findings.push(
1063
+ finding(
1064
+ 'section-missing',
1065
+ name,
1066
+ DOES_NOT_GOVERN,
1067
+ 'is absent from the scope section, so no boundary names the owner it hands off to.',
1068
+ ),
1069
+ )
1070
+ }
1071
+
1072
+ return [...findings, ...checkStandardName(name, scope.statement)]
1073
+ }
1074
+
1075
+ /**
1076
+ * The three kinds whose records are folders rather than files. Keyed by kind so
1077
+ * a seventh arrives as an entry here and the compiler names the walk it owes,
1078
+ * where a ternary chain would silently fall through to whichever branch is last.
1079
+ */
1080
+ const FOLDER_CHECK: Readonly<
1081
+ Record<
1082
+ Exclude<RecordKind, 'plans' | 'memory' | 'standards'>,
1083
+ (dir: string, slug: string) => Promise<Finding[]>
1084
+ >
1085
+ > = {
1086
+ groundwork: checkTrack,
1087
+ intake: checkDump,
1088
+ teach: checkWorkspace,
1089
+ }
1090
+
1091
+ function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
1092
+ return { ok: false, reason, message }
1093
+ }
1094
+
1095
+ /** The walk for a kind whose records are files in one flat folder. */
1096
+ async function validateFiles(
1097
+ dir: string,
1098
+ kind: RecordKind,
1099
+ check: (name: string, text: string) => Finding[],
1100
+ skip: (file: string) => boolean = () => false,
1101
+ ): Promise<ValidateReport> {
1102
+ const files = (await listMarkdown(dir)).filter((file) => !skip(file))
1103
+
1104
+ const perFile = await Promise.all(
1105
+ files.map(async (file) =>
1106
+ check(file, await readFile(join(dir, file), 'utf8')),
1107
+ ),
1108
+ )
1109
+
1110
+ return { ok: true, kind, records: files.length, findings: perFile.flat() }
1111
+ }
1112
+
1113
+ /**
1114
+ * Reports what every record in one folder claims against the shape its standard
1115
+ * fixes. It writes nothing whichever kind runs, and the reason differs by kind.
1116
+ * A session record is per-machine scratch with no history behind it, so a repair
1117
+ * that guessed wrong could not be undone. A standard installs into every target
1118
+ * and is cited by bare filename, so a rename costs more than the file move it
1119
+ * looks like.
1120
+ */
1121
+ export async function validateRecords(
1122
+ root: string,
1123
+ kind: RecordKind,
1124
+ ): Promise<ValidateOutcome> {
1125
+ const dir = recordsDir(root, kind)
1126
+
1127
+ if (!existsSync(dir)) {
1128
+ return refuse(
1129
+ 'no-folder',
1130
+ `No ${kind} folder at ${recordDirs(root, kind).join(' or ')}.`,
1131
+ )
1132
+ }
1133
+
1134
+ if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
1135
+
1136
+ if (kind === 'memory') {
1137
+ return validateFiles(
1138
+ dir,
1139
+ kind,
1140
+ checkMemory,
1141
+ (file) => file === MEMORY_INDEX,
1142
+ )
1143
+ }
1144
+
1145
+ // The walk stays flat, matching install and the catalog.
1146
+ if (kind === 'standards') {
1147
+ return validateFiles(
1148
+ dir,
1149
+ kind,
1150
+ checkStandard,
1151
+ (file) => file === STANDARD_INDEX,
1152
+ )
1153
+ }
1154
+
1155
+ const folders = await listFolders(dir)
1156
+ const check = FOLDER_CHECK[kind]
1157
+ const perFolder = await Promise.all(
1158
+ folders.map((slug) => check(join(dir, slug), slug)),
1159
+ )
1160
+
1161
+ return { ok: true, kind, records: folders.length, findings: perFolder.flat() }
1162
+ }