@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,1001 @@
1
+ import { existsSync, statSync } from 'node:fs'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+ import {
5
+ archiveDir,
6
+ isReservedStem,
7
+ readOutcomes,
8
+ readPullRequest,
9
+ tasksDir,
10
+ } from '@/tasks/archive'
11
+ import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
12
+
13
+ const ORDERING_FILE = 'priority.md'
14
+ const BACKLOG_FILE = 'backlog.md'
15
+
16
+ /**
17
+ * The readiness headings `standards/tasks.md` fixes. The names are the
18
+ * contract rather than a suggestion, so a board grouping under names of its own
19
+ * reads as carrying no group at all and the run refuses instead of reporting a
20
+ * clean board it never parsed.
21
+ */
22
+ export const BOARD_GROUPS = ['Run now', 'Up next', 'Needs a plan'] as const
23
+
24
+ export type BoardGroup = (typeof BOARD_GROUPS)[number]
25
+
26
+ export const VALIDATE_REFUSALS = [
27
+ 'no-board',
28
+ 'no-ordering',
29
+ 'no-groups',
30
+ ] as const
31
+
32
+ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
33
+
34
+ export const FINDING_KINDS = [
35
+ 'plan-unstated',
36
+ 'plan-unresolved',
37
+ 'task-unresolved',
38
+ 'row-missing',
39
+ 'row-duplicated',
40
+ 'row-misshapen',
41
+ 'row-untabled',
42
+ 'row-misordered',
43
+ 'touches-unstated',
44
+ 'touches-collided',
45
+ 'blocker-settled',
46
+ 'blocker-unresolved',
47
+ ] as const
48
+
49
+ export type FindingKind = (typeof FINDING_KINDS)[number]
50
+
51
+ export interface Finding {
52
+ readonly kind: FindingKind
53
+ readonly group: BoardGroup | undefined
54
+ readonly subject: string
55
+ readonly message: string
56
+ }
57
+
58
+ /**
59
+ * A parked row neither half of the blocker check reached. Three of the five
60
+ * blocker kinds put no fact on disk, so a run reporting findings alone would
61
+ * read as a board with nothing stale on it.
62
+ */
63
+ export interface Untested {
64
+ readonly group: BoardGroup
65
+ readonly subject: string
66
+ readonly message: string
67
+ }
68
+
69
+ /**
70
+ * A `Touches` cell naming a bare folder, which claims every file under it and
71
+ * so collides with every row a later session writes there. The claim is often
72
+ * correct, since a row rewriting a whole directory has no other way to say so,
73
+ * which is why it reports beside the findings rather than inside them and moves
74
+ * no exit code.
75
+ */
76
+ export interface FolderClaim {
77
+ readonly group: BoardGroup
78
+ readonly subject: string
79
+ readonly message: string
80
+ }
81
+
82
+ /**
83
+ * A backlog line, which carries a pointer and nothing else. The backlog is
84
+ * explicitly unordered, so a line has no position to read and no columns to
85
+ * resolve.
86
+ */
87
+ export interface BacklogRow {
88
+ readonly label: string
89
+ readonly stem: string | undefined
90
+ }
91
+
92
+ export interface BoardRow {
93
+ readonly group: BoardGroup
94
+ readonly label: string
95
+ readonly stem: string | undefined
96
+ readonly plan: string | undefined
97
+ /** Absent when the group fixes no `Touches` column, empty when it read none. */
98
+ readonly touches: readonly string[] | undefined
99
+ /** Absent when the group fixes no `Waiting on` column, which is `## Run now`. */
100
+ readonly waiting: string | undefined
101
+ /** The ordinal phrase a `Waiting on` cell states about its own position, undefined when the cell carries none. */
102
+ readonly ordinal: OrdinalWord | 'last' | undefined
103
+ }
104
+
105
+ export interface ValidateReport {
106
+ readonly ok: true
107
+ readonly rows: number
108
+ readonly backlog: number
109
+ readonly tasks: number
110
+ readonly findings: readonly Finding[]
111
+ readonly untested: readonly Untested[]
112
+ readonly claims: readonly FolderClaim[]
113
+ }
114
+
115
+ export interface ValidateRefused {
116
+ readonly ok: false
117
+ readonly reason: ValidateRefusal
118
+ readonly message: string
119
+ }
120
+
121
+ export type ValidateOutcome = ValidateReport | ValidateRefused
122
+
123
+ export function orderingPath(root: string): string {
124
+ return join(tasksDir(root), ORDERING_FILE)
125
+ }
126
+
127
+ export function backlogPath(root: string): string {
128
+ return join(tasksDir(root), BACKLOG_FILE)
129
+ }
130
+
131
+ /**
132
+ * Pulls the target out of a markdown link, which is how both the `Task` and the
133
+ * `Plan` column spell their pointer. A cell carrying prose instead of a link
134
+ * yields nothing, and that absence is the finding rather than a parse failure.
135
+ */
136
+ function linkTarget(cell: string): string | undefined {
137
+ const match = /\[[^\]]*\]\(([^)]+)\)/.exec(cell)
138
+ return match ? match[1].trim() : undefined
139
+ }
140
+
141
+ function linkText(cell: string): string {
142
+ const match = /\[([^\]]*)\]\([^)]+\)/.exec(cell)
143
+ return (match ? match[1] : cell).trim()
144
+ }
145
+
146
+ function stemOf(target: string): string | undefined {
147
+ const name = target.split('/').pop()
148
+ if (!name || !name.endsWith('.md')) return undefined
149
+ return name.slice(0, -'.md'.length)
150
+ }
151
+
152
+ /**
153
+ * Reads the file set out of a `Touches` cell written as prose. Paths are the
154
+ * backticked spans, which is the only marker the column carries, and a span
155
+ * naming a skill or a command rather than a file is dropped by the same test.
156
+ *
157
+ * An extension opens with a letter, which is what separates `commands.md` from
158
+ * a task version like `v40.2`. Reading the version as a path would collide two
159
+ * rows that merely cite the same task.
160
+ */
161
+ export function readPaths(cell: string): string[] {
162
+ const spans = cell.match(/`[^`]+`/g) ?? []
163
+ const paths = spans
164
+ .map((span) => span.slice(1, -1).trim())
165
+ .filter(
166
+ (span) => span.includes('/') || /\.[A-Za-z][A-Za-z0-9]*$/.test(span),
167
+ )
168
+ .map((span) => span.replace(/^\.\//, '').replace(/\/+$/, ''))
169
+
170
+ return [...new Set(paths)]
171
+ }
172
+
173
+ /**
174
+ * Two paths touch the same work when they are equal or one contains the other.
175
+ * A row naming a directory and a row naming a file inside it collide, which a
176
+ * set intersection on the written strings alone would miss.
177
+ */
178
+ function sharesPath(left: string, right: string): boolean {
179
+ return (
180
+ left === right ||
181
+ left.startsWith(`${right}/`) ||
182
+ right.startsWith(`${left}/`)
183
+ )
184
+ }
185
+
186
+ /**
187
+ * Whether a `Touches` path names a directory. The tree answers for a path that
188
+ * resolves, which is the only reading that separates a folder from a file
189
+ * carrying no extension, such as a hook script.
190
+ *
191
+ * A path the row has yet to create resolves to nothing, so the name decides
192
+ * there. `readPaths` admits a span for one of two reasons, a slash or an
193
+ * extension, so a span surviving without an extension is one a slash let
194
+ * through and reads as a folder.
195
+ */
196
+ function isFolder(path: string, root: string): boolean {
197
+ const target = resolve(root, path)
198
+ if (existsSync(target)) return statSync(target).isDirectory()
199
+
200
+ return !/\.[A-Za-z][A-Za-z0-9]*$/.test(path)
201
+ }
202
+
203
+ function splitCells(line: string): string[] {
204
+ return line
205
+ .trim()
206
+ .replace(/^\|/, '')
207
+ .replace(/\|$/, '')
208
+ .split('|')
209
+ .map((cell) => cell.trim())
210
+ }
211
+
212
+ function isSeparator(cells: readonly string[]): boolean {
213
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell))
214
+ }
215
+
216
+ function columnIndex(header: readonly string[], name: string): number {
217
+ return header.findIndex((cell) => cell.toLowerCase() === name)
218
+ }
219
+
220
+ function isGroup(heading: string): heading is BoardGroup {
221
+ return (BOARD_GROUPS as readonly string[]).includes(heading)
222
+ }
223
+
224
+ const ORDINAL_WORDS = [
225
+ 'first',
226
+ 'second',
227
+ 'third',
228
+ 'fourth',
229
+ 'fifth',
230
+ 'sixth',
231
+ 'seventh',
232
+ 'eighth',
233
+ 'ninth',
234
+ 'tenth',
235
+ 'eleventh',
236
+ 'twelfth',
237
+ 'thirteenth',
238
+ 'fourteenth',
239
+ 'fifteenth',
240
+ 'sixteenth',
241
+ 'seventeenth',
242
+ 'eighteenth',
243
+ 'nineteenth',
244
+ 'twentieth',
245
+ ] as const
246
+
247
+ type OrdinalWord = (typeof ORDINAL_WORDS)[number]
248
+
249
+ const ORDINAL_HERE = new RegExp(
250
+ `\\b(${ORDINAL_WORDS.join('|')})\\s+here\\b`,
251
+ 'i',
252
+ )
253
+ const LAST_AT_END = /\blast\.?\s*$/i
254
+
255
+ /**
256
+ * Reads the ordinal phrase a `Waiting on` cell states about its own position,
257
+ * searching the whole cell for `<ordinal> here` or the bare word `last` rather
258
+ * than anchoring to where the cell opens. The phrase sits at the end of the
259
+ * sentence on every row that carries one, as in `nothing, cleared 2026-08-31
260
+ * when it merged. Third here`. The two-word phrase is safe to find anywhere,
261
+ * since it needs both a closed vocabulary word and `here` beside it, but a
262
+ * bare `last` is one of the commonest words in English and this corpus writes
263
+ * it constantly, as in `the last of the two instruments this rename needs`.
264
+ * Anchoring it to the end of the cell is what keeps that prose out of the
265
+ * findings, since every row that actually declares itself last puts the word
266
+ * there and nowhere else. A cell carrying neither reads as unordered rather
267
+ * than as an error, which keeps a legitimately-phrased row out of the
268
+ * findings.
269
+ */
270
+ function readOrdinal(cell: string): OrdinalWord | 'last' | undefined {
271
+ const here = ORDINAL_HERE.exec(cell)
272
+ if (here?.[1]) return here[1].toLowerCase() as OrdinalWord
273
+
274
+ return LAST_AT_END.test(cell) ? 'last' : undefined
275
+ }
276
+
277
+ function isRowLine(line: string): boolean {
278
+ return line.trimStart().startsWith('|')
279
+ }
280
+
281
+ /**
282
+ * Whether a header candidate genuinely opens a table, meaning the line right
283
+ * behind it is a separator carrying the same cell count. A row landing where a
284
+ * blank line already closed the table above it looks identical to a fresh
285
+ * header until this read, since both are the first pipe line the walk has
286
+ * seen since the reset.
287
+ */
288
+ function opensTable(
289
+ header: readonly string[],
290
+ next: string | undefined,
291
+ ): boolean {
292
+ if (next === undefined || !isRowLine(next)) return false
293
+ const cells = splitCells(next)
294
+ return isSeparator(cells) && cells.length === header.length
295
+ }
296
+
297
+ /**
298
+ * Parses the ordering file into rows keyed by readiness group. Columns are read
299
+ * from each table's own header rather than by position, so a group whose shape
300
+ * differs from this repository's is reported for what it lacks instead of
301
+ * having its second cell read as something it never was.
302
+ *
303
+ * The walk carries one more piece of state than a header: whether the table
304
+ * that header opened is still open. A blank or prose line closes it, so a row
305
+ * stranded behind that close is read as one candidate header among many rather
306
+ * than as a continuation of the table above, and each surviving row's cell
307
+ * count is checked against the header it actually followed.
308
+ */
309
+ export function readBoard(text: string): {
310
+ readonly rows: readonly BoardRow[]
311
+ readonly groups: readonly BoardGroup[]
312
+ readonly findings: readonly Finding[]
313
+ } {
314
+ const rows: BoardRow[] = []
315
+ const groups: BoardGroup[] = []
316
+ const findings: Finding[] = []
317
+
318
+ const lines = text.split('\n')
319
+
320
+ let group: BoardGroup | undefined
321
+ let header: string[] | undefined
322
+
323
+ for (let i = 0; i < lines.length; i += 1) {
324
+ const line = lines[i] ?? ''
325
+ const heading = /^##\s+(.+?)\s*$/.exec(line)
326
+ if (heading) {
327
+ const title = heading[1]
328
+ group = isGroup(title) ? title : undefined
329
+ header = undefined
330
+ if (group && !groups.includes(group)) groups.push(group)
331
+ continue
332
+ }
333
+
334
+ if (!group) continue
335
+
336
+ if (!isRowLine(line)) {
337
+ header = undefined
338
+ continue
339
+ }
340
+
341
+ const cells = splitCells(line)
342
+ if (isSeparator(cells)) continue
343
+
344
+ if (!header) {
345
+ if (!opensTable(cells, lines[i + 1])) {
346
+ findings.push({
347
+ kind: 'row-untabled',
348
+ group,
349
+ subject: line.trim(),
350
+ message:
351
+ 'sits outside a table. The table above it already closed at the line before it.',
352
+ })
353
+ continue
354
+ }
355
+
356
+ header = cells
357
+ continue
358
+ }
359
+
360
+ if (cells.length !== header.length) {
361
+ findings.push({
362
+ kind: 'row-misshapen',
363
+ group,
364
+ subject: line.trim(),
365
+ message: `carries ${cells.length} cell(s) against the ${header.length}-cell header above it.`,
366
+ })
367
+ continue
368
+ }
369
+
370
+ const taskAt = columnIndex(header, 'task')
371
+ const planAt = columnIndex(header, 'plan')
372
+ const touchesAt = columnIndex(header, 'touches')
373
+ const waitingAt = columnIndex(header, 'waiting on')
374
+
375
+ const task = taskAt >= 0 ? (cells[taskAt] ?? '') : ''
376
+ const target = linkTarget(task)
377
+ const plan = planAt >= 0 ? linkTarget(cells[planAt] ?? '') : undefined
378
+ const waiting = waitingAt >= 0 ? (cells[waitingAt] ?? '') : undefined
379
+
380
+ rows.push({
381
+ group,
382
+ label: linkText(task) || task,
383
+ stem: target ? stemOf(target) : undefined,
384
+ plan,
385
+ touches: touchesAt >= 0 ? readPaths(cells[touchesAt] ?? '') : undefined,
386
+ waiting,
387
+ ordinal: waiting ? readOrdinal(waiting) : undefined,
388
+ })
389
+ }
390
+
391
+ return { rows, groups, findings }
392
+ }
393
+
394
+ /**
395
+ * Reports a `## Needs a plan` row whose stated ordinal disagrees with where it
396
+ * actually sits in that group. Reading the sequence for gaps and duplicates on
397
+ * its own would detect less, since row position is contiguous by
398
+ * construction, and a hand-renumbered sequence is exactly a case where the
399
+ * prose and the position have come apart.
400
+ */
401
+ function checkOrdinals(rows: readonly BoardRow[]): Finding[] {
402
+ const findings: Finding[] = []
403
+ const parked = rows.filter((row) => row.group === 'Needs a plan')
404
+
405
+ parked.forEach((row, index) => {
406
+ if (!row.ordinal) return
407
+
408
+ const position = index + 1
409
+ const expected =
410
+ row.ordinal === 'last'
411
+ ? parked.length
412
+ : ORDINAL_WORDS.indexOf(row.ordinal) + 1
413
+
414
+ if (expected === position) return
415
+
416
+ const phrase = row.ordinal === 'last' ? 'last' : `${row.ordinal} here`
417
+ findings.push({
418
+ kind: 'row-misordered',
419
+ group: row.group,
420
+ subject: subjectOf(row),
421
+ message: `declares itself "${phrase}", but sits at position ${position} of ${parked.length} in ${row.group}.`,
422
+ })
423
+ })
424
+
425
+ return findings
426
+ }
427
+
428
+ /**
429
+ * Parses the backlog into one row per bullet carrying a link. The backlog is a
430
+ * flat list rather than a table, so there is no header to resolve and no group
431
+ * to sit under, and a bullet holding prose instead of a pointer is skipped.
432
+ *
433
+ * Skipping it rather than reporting it is what keeps the intro paragraph and
434
+ * any explanatory bullet out of the findings. The task that bullet meant to
435
+ * name is still accounted for, because a stem no line reaches is reported by
436
+ * `checkMapping` as carrying no row on either surface.
437
+ */
438
+ export function readBacklog(text: string): readonly BacklogRow[] {
439
+ const rows: BacklogRow[] = []
440
+
441
+ for (const line of text.split('\n')) {
442
+ if (!/^\s*[-*]\s/.test(line)) continue
443
+
444
+ const target = linkTarget(line)
445
+ if (!target) continue
446
+
447
+ // A pointer carrying a directory names something other than a sibling task,
448
+ // the way `citedStem` reads the same shape on the board.
449
+ const path = target.split('#')[0] ?? ''
450
+ if (path.includes('/')) continue
451
+
452
+ const stem = stemOf(path)
453
+ if (stem && isReservedStem(stem)) continue
454
+
455
+ rows.push({ label: linkText(line) || line.trim(), stem })
456
+ }
457
+
458
+ return rows
459
+ }
460
+
461
+ async function listTaskStems(dir: string): Promise<string[]> {
462
+ const entries = await readdir(dir)
463
+
464
+ return entries
465
+ .filter((entry) => entry.endsWith('.md'))
466
+ .map((entry) => entry.slice(0, -'.md'.length))
467
+ .filter((stem) => !isReservedStem(stem))
468
+ .sort()
469
+ }
470
+
471
+ /**
472
+ * Resolves a pointer against the board and against the project root both, the
473
+ * way `claude-docs` reads the same line. A fragment is dropped first, since an
474
+ * anchor is part of the link and never part of the path.
475
+ */
476
+ function resolves(target: string, dir: string, root: string): boolean {
477
+ const path = target.split('#')[0]
478
+ if (!path) return false
479
+
480
+ return existsSync(resolve(dir, path)) || existsSync(resolve(root, path))
481
+ }
482
+
483
+ /**
484
+ * Accounts every task file against both surfaces the board spans. A task sits
485
+ * on `priority.md` when it would plausibly be planned soon and on `backlog.md`
486
+ * otherwise, so a file reached by neither is the dropped one this reports and a
487
+ * file reached by both claims two contradictory things about itself.
488
+ */
489
+ function checkMapping(
490
+ rows: readonly BoardRow[],
491
+ backlog: readonly BacklogRow[],
492
+ stems: readonly string[],
493
+ dir: string,
494
+ ): Finding[] {
495
+ const findings: Finding[] = []
496
+ const seen = new Map<string, number>()
497
+ const listed = new Set<string>()
498
+
499
+ for (const row of backlog) {
500
+ if (!row.stem) {
501
+ findings.push({
502
+ kind: 'task-unresolved',
503
+ group: undefined,
504
+ subject: row.label,
505
+ message: 'is a backlog line naming no task file.',
506
+ })
507
+ continue
508
+ }
509
+
510
+ listed.add(row.stem)
511
+
512
+ if (!existsSync(join(dir, `${row.stem}.md`))) {
513
+ findings.push({
514
+ kind: 'task-unresolved',
515
+ group: undefined,
516
+ subject: row.stem,
517
+ message: 'has a backlog line and no task file.',
518
+ })
519
+ }
520
+ }
521
+
522
+ for (const row of rows) {
523
+ if (!row.stem) {
524
+ findings.push({
525
+ kind: 'task-unresolved',
526
+ group: row.group,
527
+ subject: row.label,
528
+ message: 'names no task file, so the row points at nothing.',
529
+ })
530
+ continue
531
+ }
532
+
533
+ seen.set(row.stem, (seen.get(row.stem) ?? 0) + 1)
534
+
535
+ if (!existsSync(join(dir, `${row.stem}.md`))) {
536
+ findings.push({
537
+ kind: 'task-unresolved',
538
+ group: row.group,
539
+ subject: row.stem,
540
+ message: 'has a row and no task file.',
541
+ })
542
+ }
543
+ }
544
+
545
+ for (const [stem, count] of seen) {
546
+ if (count > 1) {
547
+ findings.push({
548
+ kind: 'row-duplicated',
549
+ group: undefined,
550
+ subject: stem,
551
+ message: `carries ${count} rows. A task belongs to exactly one group.`,
552
+ })
553
+ }
554
+
555
+ if (listed.has(stem)) {
556
+ findings.push({
557
+ kind: 'row-duplicated',
558
+ group: undefined,
559
+ subject: stem,
560
+ message:
561
+ 'carries a row on the board and a line on the backlog. A task sits on one surface.',
562
+ })
563
+ }
564
+ }
565
+
566
+ for (const stem of stems) {
567
+ if (!seen.has(stem) && !listed.has(stem)) {
568
+ findings.push({
569
+ kind: 'row-missing',
570
+ group: undefined,
571
+ subject: stem,
572
+ message:
573
+ 'is a task file with no row on the board and no line on the backlog.',
574
+ })
575
+ }
576
+ }
577
+
578
+ return findings
579
+ }
580
+
581
+ function checkPlans(
582
+ rows: readonly BoardRow[],
583
+ dir: string,
584
+ root: string,
585
+ ): Finding[] {
586
+ const findings: Finding[] = []
587
+
588
+ for (const row of rows) {
589
+ if (row.group !== 'Run now') continue
590
+
591
+ if (!row.plan) {
592
+ findings.push({
593
+ kind: 'plan-unstated',
594
+ group: row.group,
595
+ subject: row.stem ?? row.label,
596
+ message:
597
+ 'states no plan pointer, so the readiness claim cannot be checked.',
598
+ })
599
+ continue
600
+ }
601
+
602
+ if (!resolves(row.plan, dir, root)) {
603
+ findings.push({
604
+ kind: 'plan-unresolved',
605
+ group: row.group,
606
+ subject: row.stem ?? row.label,
607
+ message: `points at ${row.plan}, which resolves to no file.`,
608
+ })
609
+ }
610
+ }
611
+
612
+ return findings
613
+ }
614
+
615
+ /**
616
+ * The half of the `## Run now` test a person cannot check by eye. Two rows a
617
+ * worker may be handed at once must touch disjoint files, and the `Touches`
618
+ * column is the only place either set is written down.
619
+ */
620
+ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
621
+ const findings: Finding[] = []
622
+ const ready = rows.filter((row) => row.group === 'Run now')
623
+
624
+ for (const row of ready) {
625
+ // An absent column and an unreadable one both leave the row untested by
626
+ // the loop below, so reporting only the second would pass a board whose
627
+ // `## Run now` table declares no file set at all.
628
+ if (!row.touches || row.touches.length === 0) {
629
+ findings.push({
630
+ kind: 'touches-unstated',
631
+ group: row.group,
632
+ subject: row.stem ?? row.label,
633
+ message: 'names no file, so its collisions cannot be read.',
634
+ })
635
+ }
636
+ }
637
+
638
+ for (let i = 0; i < ready.length; i += 1) {
639
+ for (let j = i + 1; j < ready.length; j += 1) {
640
+ const left = ready[i]
641
+ const right = ready[j]
642
+ const shared = (left.touches ?? []).flatMap((path) => {
643
+ const other = (right.touches ?? []).find((candidate) =>
644
+ sharesPath(path, candidate),
645
+ )
646
+ return other === undefined
647
+ ? []
648
+ : [describeShared(path, other, left, right)]
649
+ })
650
+
651
+ if (shared.length === 0) continue
652
+
653
+ findings.push({
654
+ kind: 'touches-collided',
655
+ group: 'Run now',
656
+ subject: `${subjectOf(left)} and ${subjectOf(right)}`,
657
+ message: `both touch ${joinShared(shared)}.`,
658
+ })
659
+ }
660
+ }
661
+
662
+ return findings
663
+ }
664
+
665
+ function subjectOf(row: BoardRow): string {
666
+ return row.stem ?? row.label
667
+ }
668
+
669
+ /**
670
+ * Names one path two rows share, and the row that claimed it as a folder when
671
+ * one side named a directory holding the other's file. Which side contributed
672
+ * the containing path is the fact a reader acts on, and printing the shared
673
+ * strings alone leaves an over-broad cell and a genuine overlap identical.
674
+ *
675
+ * The shorter path is the container, because `sharesPath` holds for an unequal
676
+ * pair only when one is a prefix of the other up to a separator.
677
+ */
678
+ function describeShared(
679
+ path: string,
680
+ other: string,
681
+ left: BoardRow,
682
+ right: BoardRow,
683
+ ): string {
684
+ if (path === other) return path
685
+
686
+ const container = path.length < other.length ? path : other
687
+ const owner = container === path ? left : right
688
+ return `${container}, which ${subjectOf(owner)} claims as a folder`
689
+ }
690
+
691
+ /**
692
+ * A folder clause carries a comma of its own, so a comma between clauses would
693
+ * read as another path. The plain list keeps the comma it has always had.
694
+ */
695
+ function joinShared(clauses: readonly string[]): string {
696
+ const separator = clauses.some((clause) => clause.includes(',')) ? '; ' : ', '
697
+ return clauses.join(separator)
698
+ }
699
+
700
+ /**
701
+ * Reports a `Touches` cell naming a bare folder. The claim collides with every
702
+ * row a later session writes under that folder, and it is legitimate whenever
703
+ * the row does rewrite the directory, so this states the reach rather than
704
+ * calling it a defect.
705
+ *
706
+ * The scan takes `## Run now` alone, where `checkCollisions` takes it. A cell in
707
+ * another group describes work nobody has planned, so it is written as a
708
+ * sentence and rewritten at planning time, and a claim read off one reports on
709
+ * prose rather than on a file set. That is the shape that teaches a reader to
710
+ * skip the report. A parked folder claim surfaces when the row is promoted,
711
+ * which is also when its cell becomes a set anything can act on.
712
+ */
713
+ function checkFolderClaims(
714
+ rows: readonly BoardRow[],
715
+ root: string,
716
+ ): FolderClaim[] {
717
+ const claims: FolderClaim[] = []
718
+
719
+ for (const row of rows.filter((candidate) => candidate.group === 'Run now')) {
720
+ for (const path of row.touches ?? []) {
721
+ if (!isFolder(path, root)) continue
722
+
723
+ claims.push({
724
+ group: row.group,
725
+ subject: subjectOf(row),
726
+ message: `claims the whole ${path} folder, so it collides with every row written under it.`,
727
+ })
728
+ }
729
+ }
730
+
731
+ return claims
732
+ }
733
+
734
+ /**
735
+ * Reads the task a blocker cell cites. A task pointer is a bare sibling
736
+ * filename, the way every `Task` column spells one, so a target carrying a
737
+ * directory names something else and yields nothing. A row waiting on a plan
738
+ * links that plan, and reading its stem as a task would report the row settled
739
+ * against a folder the plan does not sit in.
740
+ */
741
+ function citedStem(cell: string): string | undefined {
742
+ const target = linkTarget(cell)?.split('#')[0]
743
+ if (!target || target.includes('/')) return undefined
744
+ return stemOf(target)
745
+ }
746
+
747
+ /** What one blocker citation produced, since a row can be neither settled nor open. */
748
+ interface CitedResult {
749
+ readonly findings: readonly Finding[]
750
+ readonly untested: readonly Untested[]
751
+ }
752
+
753
+ function nothing(): CitedResult {
754
+ return { findings: [], untested: [] }
755
+ }
756
+
757
+ /**
758
+ * Reports what a cited task does to the row waiting on it. A file sitting in
759
+ * the archive settles the row, and a live file settles it only once the work it
760
+ * carries is on the trunk. A file carrying no outcome box settles nothing,
761
+ * since a file the check could not parse is not evidence of a finished one.
762
+ *
763
+ * A citation resolving in neither folder is a broken pointer rather than a
764
+ * closed task, and the two take different findings. Reading an absent file as
765
+ * archived states a specific fact about a file nobody ever wrote, which is what
766
+ * a renamed task or a typo produces.
767
+ *
768
+ * A closed outcome is not the same fact as landed work. The ship chain marks
769
+ * outcomes as its first step and opens the pull request several steps later, so
770
+ * a check reading the checkbox reports the row settled while the branch is
771
+ * still in review. The pull request the task names is what the trunk is asked
772
+ * about, and a task naming none leaves the row untested rather than settled,
773
+ * because the only local signal left is the checkbox that produced the defect.
774
+ *
775
+ * The outcome list comes off `readOutcomes` rather than a pattern of its own,
776
+ * so this check cannot disagree with the archive and outcome verbs about which
777
+ * checkboxes are outcomes and which sit inside a block a task displays.
778
+ */
779
+ async function checkCitedTask(
780
+ group: BoardGroup,
781
+ subject: string,
782
+ cited: string,
783
+ root: string,
784
+ trunk: TrunkReader,
785
+ ): Promise<CitedResult> {
786
+ const live = join(tasksDir(root), `${cited}.md`)
787
+
788
+ if (!existsSync(live)) {
789
+ if (existsSync(join(archiveDir(root), `${cited}.md`))) {
790
+ return settled(group, subject, `waits on ${cited}, which is archived.`)
791
+ }
792
+
793
+ return {
794
+ findings: [
795
+ {
796
+ kind: 'blocker-unresolved',
797
+ group,
798
+ subject,
799
+ message: `waits on ${cited}, which is neither on the board nor archived.`,
800
+ },
801
+ ],
802
+ untested: [],
803
+ }
804
+ }
805
+
806
+ const text = await readFile(live, 'utf8')
807
+ const { open, closed } = readOutcomes(text)
808
+ if (open.length > 0 || closed.length === 0) return nothing()
809
+
810
+ const pullRequest = readPullRequest(text)
811
+ if (pullRequest === undefined) {
812
+ return untestedRow(
813
+ group,
814
+ subject,
815
+ `waits on ${cited}, which closed every outcome but names no pull request, so nothing tests whether the work reached the trunk.`,
816
+ )
817
+ }
818
+
819
+ const landed = await trunk(pullRequest)
820
+ if (landed === undefined) {
821
+ return untestedRow(
822
+ group,
823
+ subject,
824
+ `waits on ${cited}, whose pull request #${pullRequest} could not be read against the trunk.`,
825
+ )
826
+ }
827
+
828
+ if (!landed) return nothing()
829
+
830
+ return settled(
831
+ group,
832
+ subject,
833
+ `waits on ${cited}, whose pull request #${pullRequest} reached the trunk.`,
834
+ )
835
+ }
836
+
837
+ function settled(
838
+ group: BoardGroup,
839
+ subject: string,
840
+ message: string,
841
+ ): CitedResult {
842
+ return {
843
+ findings: [{ kind: 'blocker-settled', group, subject, message }],
844
+ untested: [],
845
+ }
846
+ }
847
+
848
+ function untestedRow(
849
+ group: BoardGroup,
850
+ subject: string,
851
+ message: string,
852
+ ): CitedResult {
853
+ return { findings: [], untested: [{ group, subject, message }] }
854
+ }
855
+
856
+ /**
857
+ * Re-takes the two blocker kinds a command can settle, over every row outside
858
+ * `## Run now`. A cited task is settled by being archived or by closing every
859
+ * outcome, and a cited path is settled by no `## Run now` row still holding it.
860
+ *
861
+ * Both halves gate on a citation the cell carries, never on a column beside it.
862
+ * The board standard gives a collision cell the file held by the running task,
863
+ * so a row whose cell names no file was parked by something else and its
864
+ * `Touches` column says nothing about what holds it. Reading that column
865
+ * instead reports a cleared collision on a row no collision ever parked, and
866
+ * counts the row as re-tested, which is the more expensive half of that error.
867
+ *
868
+ * The cell is read for citations rather than parsed into fields, since the
869
+ * standard fixes three forms for it and leaves it prose. A row neither half
870
+ * reached is returned as untested, because the other three kinds are a person's
871
+ * to judge and silence about them reads as a board with nothing stale on it.
872
+ */
873
+ async function checkParked(
874
+ rows: readonly BoardRow[],
875
+ root: string,
876
+ trunk: TrunkReader,
877
+ ): Promise<{ findings: Finding[]; untested: Untested[] }> {
878
+ const findings: Finding[] = []
879
+ const untested: Untested[] = []
880
+ const running = rows.filter((row) => row.group === 'Run now')
881
+
882
+ for (const row of rows) {
883
+ if (row.group === 'Run now') continue
884
+
885
+ const subject = row.stem ?? row.label
886
+ const cell = row.waiting ?? ''
887
+ const cited = citedStem(cell)
888
+ const contested = readPaths(cell)
889
+
890
+ if (cited) {
891
+ const result = await checkCitedTask(
892
+ row.group,
893
+ subject,
894
+ cited,
895
+ root,
896
+ trunk,
897
+ )
898
+ findings.push(...result.findings)
899
+ untested.push(...result.untested)
900
+ }
901
+
902
+ const held = contested.filter((path) =>
903
+ running.some((run) =>
904
+ (run.touches ?? []).some((other) => sharesPath(path, other)),
905
+ ),
906
+ )
907
+
908
+ if (contested.length > 0 && held.length === 0) {
909
+ findings.push({
910
+ kind: 'blocker-settled',
911
+ group: row.group,
912
+ subject,
913
+ message: `waits on ${contested.join(', ')}, which nothing under Run now holds.`,
914
+ })
915
+ }
916
+
917
+ if (!cited && contested.length === 0) {
918
+ untested.push({
919
+ group: row.group,
920
+ subject,
921
+ message:
922
+ 'cites no task and no file, so neither half of its blocker is mechanical.',
923
+ })
924
+ }
925
+ }
926
+
927
+ return { findings, untested }
928
+ }
929
+
930
+ function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
931
+ return { ok: false, reason, message }
932
+ }
933
+
934
+ export interface ValidateOptions {
935
+ /** Overridden by tests, which supply the trunk rather than reaching for git. */
936
+ readonly trunk?: TrunkReader
937
+ }
938
+
939
+ /**
940
+ * Reports what every board row claims against what the tree holds. It writes
941
+ * nothing: a row is a session's claim about readiness, and a validator that
942
+ * repaired one would be asserting the claim it exists to test.
943
+ */
944
+ export async function validateBoard(
945
+ root: string,
946
+ options: ValidateOptions = {},
947
+ ): Promise<ValidateOutcome> {
948
+ const trunk = options.trunk ?? gitTrunkReader(root)
949
+ const dir = tasksDir(root)
950
+ if (!existsSync(dir)) {
951
+ return refuse('no-board', `No task board at ${dir}.`)
952
+ }
953
+
954
+ const ordering = orderingPath(root)
955
+ if (!existsSync(ordering)) {
956
+ return refuse('no-ordering', `No ordering file at ${ordering}.`)
957
+ }
958
+
959
+ const {
960
+ rows,
961
+ groups,
962
+ findings: shapeFindings,
963
+ } = readBoard(await readFile(ordering, 'utf8'))
964
+
965
+ if (groups.length === 0) {
966
+ return refuse(
967
+ 'no-groups',
968
+ `No readiness group in ${ORDERING_FILE}. Expected one of: ${BOARD_GROUPS.join(', ')}.`,
969
+ )
970
+ }
971
+
972
+ // An absent backlog reads as empty rather than refusing. A project that has
973
+ // never needed the second surface keeps every task on the board, which is the
974
+ // one-to-one mapping this check ran before the backlog existed.
975
+ const backlogFile = backlogPath(root)
976
+ const backlog = existsSync(backlogFile)
977
+ ? readBacklog(await readFile(backlogFile, 'utf8'))
978
+ : []
979
+
980
+ const stems = await listTaskStems(dir)
981
+ const parked = await checkParked(rows, root, trunk)
982
+
983
+ const findings = [
984
+ ...shapeFindings,
985
+ ...checkMapping(rows, backlog, stems, dir),
986
+ ...checkPlans(rows, dir, root),
987
+ ...checkCollisions(rows),
988
+ ...checkOrdinals(rows),
989
+ ...parked.findings,
990
+ ]
991
+
992
+ return {
993
+ ok: true,
994
+ rows: rows.length,
995
+ backlog: backlog.length,
996
+ tasks: stems.length,
997
+ findings,
998
+ untested: parked.untested,
999
+ claims: checkFolderClaims(rows, root),
1000
+ }
1001
+ }