@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,842 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { join, relative } from 'node:path'
4
+ import { parseFrontmatter, readField } from '@/indexes/frontmatter'
5
+ import { type BodyLine, bodyLines } from '@/markdown/scan'
6
+
7
+ export const TEACH_REFUSALS = [
8
+ 'no-teach',
9
+ 'no-workspace',
10
+ 'ambiguous',
11
+ 'exists',
12
+ 'no-file',
13
+ 'no-section',
14
+ 'listed',
15
+ 'defined',
16
+ 'bad-input',
17
+ ] as const
18
+
19
+ export type TeachRefusal = (typeof TEACH_REFUSALS)[number]
20
+
21
+ export interface TeachRefused {
22
+ readonly ok: false
23
+ readonly reason: TeachRefusal
24
+ readonly message: string
25
+ readonly detail: readonly string[]
26
+ }
27
+
28
+ /** The files and folders `standards/teach.md` fixes for a workspace. */
29
+ export const TEACH_MISSION = 'MISSION.md'
30
+ export const TEACH_RESOURCES = 'RESOURCES.md'
31
+ export const TEACH_GLOSSARY = 'GLOSSARY.md'
32
+ export const TEACH_REFERENCE = 'reference'
33
+ export const TEACH_RECORDS = 'learning-records'
34
+ export const TEACH_LESSONS = 'lessons'
35
+ export const TEACH_ASSETS = 'assets'
36
+
37
+ /**
38
+ * The one stylesheet every lesson in a workspace links. The name is fixed here
39
+ * rather than chosen per lesson, because the second lesson has to reach the
40
+ * file the first one wrote and a name composed twice is a name that can differ.
41
+ */
42
+ export const TEACH_STYLESHEET = 'course.css'
43
+
44
+ /**
45
+ * The mission heading whose list a session reads as exit criteria. The writer,
46
+ * the reader below, and the record validator all match this one spelling, so a
47
+ * heading none of them can find fails the validator rather than reading as an
48
+ * empty list.
49
+ */
50
+ export const TEACH_SUCCESS_HEADING = '## Success looks like'
51
+
52
+ /**
53
+ * A workspace folder as the standard names it, capturing the ordinal and the
54
+ * topic separately so a listing sorts by the first and a selector matches the
55
+ * second.
56
+ */
57
+ export const WORKSPACE_NAME = /^(\d{2})-([a-z0-9]+(?:-[a-z0-9]+)*)$/
58
+
59
+ const TOPIC_SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/
60
+
61
+ const READ_HEADING = '## Read'
62
+ const LEADS_HEADING = '## Leads'
63
+
64
+ /**
65
+ * The line a scaffolded section carries until something real lands in it. An
66
+ * empty section reads as one someone forgot to write, and the insert verbs drop
67
+ * this line as the first real entry arrives.
68
+ */
69
+ const PLACEHOLDER = '- None yet.'
70
+
71
+ /** Two digits on the folder, per the standard, because a person opens few. */
72
+ const ORDINAL_WIDTH = 2
73
+
74
+ const DATE_LENGTH = 'YYYY-MM-DD'.length
75
+
76
+ export interface WorkspaceSummary {
77
+ readonly slug: string
78
+ /** `NaN` when the folder name carries no ordinal, which a listing reports. */
79
+ readonly ordinal: number
80
+ readonly topic: string
81
+ /** Relative to the root, so a caller prints a path a reader can open. */
82
+ readonly path: string
83
+ readonly title: string | undefined
84
+ readonly opened: string | undefined
85
+ readonly lessons: number
86
+ readonly records: number
87
+ readonly reference: number
88
+ readonly terms: number
89
+ /** Required files the workspace does not carry, by the standard's layout. */
90
+ readonly missing: readonly string[]
91
+ }
92
+
93
+ export interface WorkspaceDetail extends WorkspaceSummary {
94
+ readonly lessonFiles: readonly string[]
95
+ readonly recordFiles: readonly string[]
96
+ readonly referenceFiles: readonly string[]
97
+ readonly glossary: readonly string[]
98
+ /** The mission's success lines, which a session reports progress against. */
99
+ readonly success: readonly string[]
100
+ }
101
+
102
+ export interface WorkspacesListed {
103
+ readonly ok: true
104
+ readonly workspaces: readonly WorkspaceSummary[]
105
+ /** The ordinal an open would take, so no caller derives one by hand. */
106
+ readonly next: string
107
+ }
108
+
109
+ export interface WorkspaceRead {
110
+ readonly ok: true
111
+ readonly workspace: WorkspaceDetail
112
+ }
113
+
114
+ export interface WorkspaceOpened {
115
+ readonly ok: true
116
+ readonly slug: string
117
+ readonly path: string
118
+ readonly created: readonly string[]
119
+ }
120
+
121
+ export interface SourcesWritten {
122
+ readonly ok: true
123
+ readonly slug: string
124
+ readonly path: string
125
+ readonly read: readonly Source[]
126
+ readonly leads: readonly Source[]
127
+ }
128
+
129
+ export interface TermsWritten {
130
+ readonly ok: true
131
+ readonly slug: string
132
+ readonly path: string
133
+ readonly defined: readonly Term[]
134
+ }
135
+
136
+ export type ListOutcome = WorkspacesListed | TeachRefused
137
+ export type ReadOutcome = WorkspaceRead | TeachRefused
138
+ export type OpenOutcome = WorkspaceOpened | TeachRefused
139
+ export type SourceOutcome = SourcesWritten | TeachRefused
140
+ export type TermOutcome = TermsWritten | TeachRefused
141
+
142
+ export interface Source {
143
+ readonly title: string
144
+ readonly url: string
145
+ }
146
+
147
+ export interface Term {
148
+ readonly term: string
149
+ readonly definition: string
150
+ }
151
+
152
+ export interface OpenRequest {
153
+ readonly topic: string
154
+ readonly subject: string
155
+ readonly startingPoint: string
156
+ readonly success: readonly string[]
157
+ readonly outOfScope: readonly string[]
158
+ readonly title?: string
159
+ readonly date?: string
160
+ }
161
+
162
+ export function refuse(
163
+ reason: TeachRefusal,
164
+ message: string,
165
+ detail: readonly string[] = [],
166
+ ): TeachRefused {
167
+ return { ok: false, reason, message, detail }
168
+ }
169
+
170
+ /**
171
+ * Every workspace sits under the main worktree root rather than under the
172
+ * checkout the caller stands in. Resolving that root belongs to the caller, so
173
+ * this takes one and never reads the working directory.
174
+ */
175
+ export function teachDir(root: string): string {
176
+ return join(root, '.claude', 'teach')
177
+ }
178
+
179
+ async function listSlugs(dir: string): Promise<string[]> {
180
+ const entries = await readdir(dir, { withFileTypes: true })
181
+
182
+ return entries
183
+ .filter((entry) => entry.isDirectory())
184
+ .map((entry) => entry.name)
185
+ .sort()
186
+ }
187
+
188
+ async function filesIn(
189
+ dir: string,
190
+ folder: string,
191
+ suffix?: string,
192
+ ): Promise<string[]> {
193
+ const path = join(dir, folder)
194
+ if (!existsSync(path)) return []
195
+
196
+ const entries = await readdir(path, { withFileTypes: true })
197
+
198
+ return entries
199
+ .filter(
200
+ (entry) => entry.isFile() && (!suffix || entry.name.endsWith(suffix)),
201
+ )
202
+ .map((entry) => entry.name)
203
+ .sort()
204
+ }
205
+
206
+ function isEntry(line: string): boolean {
207
+ const trimmed = line.trim()
208
+ return trimmed.startsWith('- ') && trimmed !== PLACEHOLDER
209
+ }
210
+
211
+ /** One bullet per term the subject defines, which is what a glossary holds. */
212
+ function glossaryTerms(text: string): string[] {
213
+ return unfenced(text)
214
+ .map((line) => line.text)
215
+ .filter(isEntry)
216
+ .map((line) => line.trim().slice('- '.length))
217
+ }
218
+
219
+ /**
220
+ * The mission's success lines, each one an observable thing the learner will be
221
+ * able to do. A session reads them as exit criteria, so a wrapped entry is
222
+ * joined back into one line rather than reported as two criteria.
223
+ *
224
+ * A mission carrying no such heading yields nothing rather than refusing. The
225
+ * record validator is what reports the absent section, and a listing that
226
+ * refused would take the whole workspace down with it.
227
+ */
228
+ function successLines(text: string): string[] {
229
+ const lines = text.split('\n')
230
+ const section = sectionRange(
231
+ unfenced(text),
232
+ TEACH_SUCCESS_HEADING,
233
+ lines.length,
234
+ )
235
+
236
+ if (!section) return []
237
+
238
+ return bulletBlocks(lines.slice(section.start, section.end)).map((block) =>
239
+ block.join(' ').trim().slice('- '.length).replace(/\s+/g, ' ').trim(),
240
+ )
241
+ }
242
+
243
+ /** The ordinal a folder name carries, or `NaN` when it carries none. */
244
+ function ordinalOf(slug: string): number {
245
+ const match = WORKSPACE_NAME.exec(slug)
246
+ return match ? Number(match[1]) : Number.NaN
247
+ }
248
+
249
+ async function summarize(
250
+ root: string,
251
+ dir: string,
252
+ slug: string,
253
+ ): Promise<WorkspaceDetail> {
254
+ const match = WORKSPACE_NAME.exec(slug)
255
+ const missionPath = join(dir, TEACH_MISSION)
256
+ const mission = existsSync(missionPath)
257
+ ? await readFile(missionPath, 'utf8')
258
+ : undefined
259
+ const frontmatter =
260
+ mission === undefined ? undefined : parseFrontmatter(mission)
261
+
262
+ const glossaryPath = join(dir, TEACH_GLOSSARY)
263
+ const glossary = existsSync(glossaryPath)
264
+ ? glossaryTerms(await readFile(glossaryPath, 'utf8'))
265
+ : []
266
+
267
+ const [lessonFiles, recordFiles, referenceFiles] = await Promise.all([
268
+ filesIn(dir, TEACH_LESSONS),
269
+ filesIn(dir, TEACH_RECORDS, '.md'),
270
+ filesIn(dir, TEACH_REFERENCE, '.md'),
271
+ ])
272
+
273
+ return {
274
+ slug,
275
+ ordinal: match ? Number(match[1]) : Number.NaN,
276
+ topic: match ? match[2] : slug,
277
+ path: relative(root, dir),
278
+ title: readField(frontmatter, 'title'),
279
+ opened: readField(frontmatter, 'date'),
280
+ lessons: lessonFiles.length,
281
+ records: recordFiles.length,
282
+ reference: referenceFiles.length,
283
+ terms: glossary.length,
284
+ missing: [TEACH_MISSION, TEACH_RESOURCES, TEACH_GLOSSARY].filter(
285
+ (file) => !existsSync(join(dir, file)),
286
+ ),
287
+ lessonFiles,
288
+ recordFiles,
289
+ referenceFiles,
290
+ glossary,
291
+ success: mission === undefined ? [] : successLines(mission),
292
+ }
293
+ }
294
+
295
+ function byOrdinal(left: WorkspaceSummary, right: WorkspaceSummary): number {
296
+ if (Number.isNaN(left.ordinal)) return Number.isNaN(right.ordinal) ? 0 : 1
297
+ if (Number.isNaN(right.ordinal)) return -1
298
+ return left.ordinal - right.ordinal
299
+ }
300
+
301
+ /**
302
+ * Reads the ordinals off the folder names rather than off summaries, so opening
303
+ * a workspace costs one directory listing instead of a read of every file in
304
+ * every workspace already there.
305
+ */
306
+ function nextOrdinal(slugs: readonly string[]): string {
307
+ const highest = slugs
308
+ .map(ordinalOf)
309
+ .filter((ordinal) => !Number.isNaN(ordinal))
310
+ .reduce((carry, ordinal) => Math.max(carry, ordinal), 0)
311
+
312
+ return String(highest + 1).padStart(ORDINAL_WIDTH, '0')
313
+ }
314
+
315
+ /**
316
+ * Every workspace under the root, with the ordinal a new one would take.
317
+ *
318
+ * A folder failing the name pattern is listed rather than dropped, since
319
+ * dropping it hides the one workspace a session most needs to see. Its ordinal
320
+ * reads as absent, it sorts last, and it never moves the next number.
321
+ */
322
+ export async function listWorkspaces(root: string): Promise<ListOutcome> {
323
+ const dir = teachDir(root)
324
+
325
+ if (!existsSync(dir)) {
326
+ return refuse('no-teach', `No teach folder at ${relative(root, dir)}.`)
327
+ }
328
+
329
+ const slugs = await listSlugs(dir)
330
+
331
+ const workspaces = await Promise.all(
332
+ slugs.map((slug) => summarize(root, join(dir, slug), slug)),
333
+ )
334
+
335
+ return {
336
+ ok: true,
337
+ workspaces: [...workspaces].sort(byOrdinal),
338
+ next: nextOrdinal(slugs),
339
+ }
340
+ }
341
+
342
+ /**
343
+ * The workspace a selector names, matched on the folder name or on the topic
344
+ * behind the ordinal. Two topics matching is a refusal rather than a pick,
345
+ * because the caller meant one of them and no verb here can say which.
346
+ */
347
+ async function findWorkspace(
348
+ root: string,
349
+ selector: string,
350
+ ): Promise<{ slug: string; dir: string } | TeachRefused> {
351
+ const dir = teachDir(root)
352
+
353
+ if (!existsSync(dir)) {
354
+ return refuse('no-teach', `No teach folder at ${relative(root, dir)}.`)
355
+ }
356
+
357
+ const slugs = await listSlugs(dir)
358
+ const matched = slugs.filter(
359
+ (slug) => slug === selector || WORKSPACE_NAME.exec(slug)?.[2] === selector,
360
+ )
361
+
362
+ if (matched.length === 0) {
363
+ return refuse('no-workspace', `No workspace named ${selector}.`, slugs)
364
+ }
365
+
366
+ if (matched.length > 1) {
367
+ return refuse(
368
+ 'ambiguous',
369
+ `${selector} names ${matched.length} workspaces.`,
370
+ matched,
371
+ )
372
+ }
373
+
374
+ return { slug: matched[0], dir: join(dir, matched[0]) }
375
+ }
376
+
377
+ /** One workspace with the filenames behind each count. */
378
+ export async function readWorkspace(
379
+ root: string,
380
+ selector: string,
381
+ ): Promise<ReadOutcome> {
382
+ const found = await findWorkspace(root, selector)
383
+ if ('ok' in found) return found
384
+
385
+ return { ok: true, workspace: await summarize(root, found.dir, found.slug) }
386
+ }
387
+
388
+ function titleFor(topic: string, given: string | undefined): string {
389
+ if (given) return given
390
+
391
+ const words = topic.split('-')
392
+ return [
393
+ words[0].charAt(0).toUpperCase() + words[0].slice(1),
394
+ ...words.slice(1),
395
+ ].join(' ')
396
+ }
397
+
398
+ function missionText(
399
+ request: OpenRequest,
400
+ title: string,
401
+ date: string,
402
+ ): string {
403
+ const outOfScope =
404
+ request.outOfScope.length > 0
405
+ ? request.outOfScope
406
+ : ['Nothing has been ruled out yet.']
407
+
408
+ return [
409
+ '---',
410
+ `title: ${title}`,
411
+ `description: ${request.subject}`,
412
+ `date: ${date}`,
413
+ '---',
414
+ '',
415
+ `# ${title}`,
416
+ '',
417
+ request.subject,
418
+ '',
419
+ '## Starting point',
420
+ '',
421
+ request.startingPoint,
422
+ '',
423
+ '## Success looks like',
424
+ '',
425
+ ...request.success.map((line) => `- ${line}`),
426
+ '',
427
+ '## Out of scope',
428
+ '',
429
+ ...outOfScope.map((line) => `- ${line}`),
430
+ '',
431
+ ].join('\n')
432
+ }
433
+
434
+ function resourcesText(title: string): string {
435
+ return [
436
+ '---',
437
+ `title: Sources for ${title}`,
438
+ `description: Sources read for ${title}, and leads found but not opened`,
439
+ '---',
440
+ '',
441
+ `# Sources for ${title}`,
442
+ '',
443
+ 'Sources that stand behind this workspace, kept apart from leads nobody opened.',
444
+ '',
445
+ READ_HEADING,
446
+ '',
447
+ PLACEHOLDER,
448
+ '',
449
+ LEADS_HEADING,
450
+ '',
451
+ PLACEHOLDER,
452
+ '',
453
+ ].join('\n')
454
+ }
455
+
456
+ function glossaryText(title: string): string {
457
+ return [
458
+ '---',
459
+ `title: Glossary for ${title}`,
460
+ `description: Terms ${title} defines, one entry each`,
461
+ '---',
462
+ '',
463
+ `# Glossary for ${title}`,
464
+ '',
465
+ 'One entry per term the subject defines, sorted alphabetically.',
466
+ '',
467
+ PLACEHOLDER,
468
+ '',
469
+ ].join('\n')
470
+ }
471
+
472
+ /** Today as `YYYY-MM-DD`, which is the one field the mission dates. */
473
+ function today(): string {
474
+ return new Date().toISOString().slice(0, DATE_LENGTH)
475
+ }
476
+
477
+ /**
478
+ * Creates a workspace at the next ordinal with all three required files.
479
+ *
480
+ * The ordinal, the folder name, and every path are derived here rather than by
481
+ * the caller. A caller standing in a linked worktree cannot reach this root
482
+ * through its file-editing tools, so a path it composed by hand is one nothing
483
+ * checks before the write lands somewhere else.
484
+ */
485
+ export async function openWorkspace(
486
+ root: string,
487
+ request: OpenRequest,
488
+ ): Promise<OpenOutcome> {
489
+ if (!TOPIC_SLUG.test(request.topic)) {
490
+ return refuse('bad-input', `Not a kebab-case topic: ${request.topic}.`, [
491
+ request.topic,
492
+ ])
493
+ }
494
+
495
+ if (request.success.length === 0) {
496
+ return refuse(
497
+ 'bad-input',
498
+ 'A mission needs at least one success line. Pass --success <line>.',
499
+ )
500
+ }
501
+
502
+ const dir = teachDir(root)
503
+ await mkdir(dir, { recursive: true })
504
+
505
+ const slugs = await listSlugs(dir)
506
+ const existing = slugs.find(
507
+ (slug) => WORKSPACE_NAME.exec(slug)?.[2] === request.topic,
508
+ )
509
+
510
+ if (existing) {
511
+ return refuse(
512
+ 'exists',
513
+ `${existing} already covers ${request.topic}. Resume it rather than opening a second.`,
514
+ [existing],
515
+ )
516
+ }
517
+
518
+ const slug = `${nextOrdinal(slugs)}-${request.topic}`
519
+ const folder = join(dir, slug)
520
+ const title = titleFor(request.topic, request.title)
521
+
522
+ await mkdir(folder, { recursive: true })
523
+
524
+ const files: ReadonlyArray<readonly [string, string]> = [
525
+ [TEACH_MISSION, missionText(request, title, request.date ?? today())],
526
+ [TEACH_RESOURCES, resourcesText(title)],
527
+ [TEACH_GLOSSARY, glossaryText(title)],
528
+ ]
529
+
530
+ for (const [name, text] of files) {
531
+ await writeFile(join(folder, name), text)
532
+ }
533
+
534
+ return {
535
+ ok: true,
536
+ slug,
537
+ path: relative(root, folder),
538
+ created: files.map(([name]) => join(relative(root, folder), name)),
539
+ }
540
+ }
541
+
542
+ interface Range {
543
+ readonly start: number
544
+ readonly end: number
545
+ }
546
+
547
+ /**
548
+ * Body lines outside every fence, each still carrying the source line it came
549
+ * from. `bodyLines` numbers from one past the frontmatter it drops, so `number
550
+ * - 1` addresses the same line in the array a caller splits itself.
551
+ */
552
+ function unfenced(text: string): BodyLine[] {
553
+ return bodyLines(text).filter((line) => !line.fenced)
554
+ }
555
+
556
+ /**
557
+ * The half-open source range a heading owns, ending at the next heading of the
558
+ * same level or above. A heading quoted inside a fenced example selects nothing,
559
+ * since the scan never sees it.
560
+ */
561
+ function sectionRange(
562
+ lines: readonly BodyLine[],
563
+ heading: string,
564
+ total: number,
565
+ ): Range | undefined {
566
+ let start: number | undefined
567
+
568
+ for (const line of lines) {
569
+ if (start === undefined) {
570
+ if (line.text.trim() === heading) start = line.number
571
+ continue
572
+ }
573
+
574
+ if (/^##?[ \t]/.test(line.text)) return { start, end: line.number - 1 }
575
+ }
576
+
577
+ return start === undefined ? undefined : { start, end: total }
578
+ }
579
+
580
+ /**
581
+ * The half-open source range holding a bullet list, from its first entry to the
582
+ * last line of its last entry. A list carrying only the placeholder yields that
583
+ * line's range, so the first real entry replaces it rather than landing beside
584
+ * it.
585
+ *
586
+ * An indented non-blank line extends the range because that is how markdown
587
+ * wraps an entry too long for one line. An unindented one does not, so a
588
+ * paragraph written under a list stays outside and is neither sorted nor moved.
589
+ */
590
+ function bulletRange(
591
+ lines: readonly BodyLine[],
592
+ within?: Range,
593
+ ): Range | undefined {
594
+ let first: number | undefined
595
+ let last: number | undefined
596
+
597
+ for (const line of lines) {
598
+ const index = line.number - 1
599
+
600
+ if (within && (index < within.start || index >= within.end)) continue
601
+
602
+ if (line.text.trim().startsWith('- ')) {
603
+ first ??= index
604
+ last = index
605
+ continue
606
+ }
607
+
608
+ const wraps = first !== undefined && /^\s+\S/.test(line.text)
609
+ if (wraps) last = index
610
+ }
611
+
612
+ return first === undefined || last === undefined
613
+ ? undefined
614
+ : { start: first, end: last + 1 }
615
+ }
616
+
617
+ /**
618
+ * The bullet blocks in a run of lines, each a bullet with the continuation lines
619
+ * wrapped under it.
620
+ *
621
+ * Splitting on the bullet marker rather than keeping the lines that are bullets
622
+ * is what holds a wrapped entry together. A filter over lines keeps the first
623
+ * line of one and silently drops the rest, which loses half of every entry an
624
+ * author wrapped at the margin.
625
+ */
626
+ function bulletBlocks(lines: readonly string[]): string[][] {
627
+ const blocks: string[][] = []
628
+
629
+ for (const line of lines) {
630
+ if (line.trim().startsWith('- ')) {
631
+ blocks.push([line])
632
+ continue
633
+ }
634
+
635
+ blocks.at(-1)?.push(line)
636
+ }
637
+
638
+ return blocks.filter((block) => block[0].trim() !== PLACEHOLDER)
639
+ }
640
+
641
+ /**
642
+ * Places entries in a line range, dropping the scaffolded placeholder as the
643
+ * first real entry lands. `sorted` merges alphabetically and anything else
644
+ * appends, which is the split between a glossary and a source list.
645
+ */
646
+ function placeEntries(
647
+ lines: readonly string[],
648
+ range: Range,
649
+ entries: readonly string[],
650
+ sorted: boolean,
651
+ ): string {
652
+ const kept = bulletBlocks(lines.slice(range.start, range.end))
653
+ const added = entries.map((entry) => [entry])
654
+
655
+ const placed = sorted
656
+ ? [...kept, ...added].sort((left, right) =>
657
+ left[0].toLowerCase().localeCompare(right[0].toLowerCase()),
658
+ )
659
+ : [...kept, ...added]
660
+
661
+ return [
662
+ ...lines.slice(0, range.start),
663
+ ...placed.flat(),
664
+ ...lines.slice(range.end),
665
+ ].join('\n')
666
+ }
667
+
668
+ /** Appends entries under a heading, keeping the blank lines around the list. */
669
+ function insertUnderHeading(
670
+ text: string,
671
+ heading: string,
672
+ entries: readonly string[],
673
+ ): string | undefined {
674
+ const lines = text.split('\n')
675
+ const body = unfenced(text)
676
+ const section = sectionRange(body, heading, lines.length)
677
+ if (!section) return undefined
678
+
679
+ const bullets = bulletRange(body, section)
680
+
681
+ if (!bullets) {
682
+ return [
683
+ ...lines.slice(0, section.start),
684
+ '',
685
+ ...entries,
686
+ '',
687
+ ...lines.slice(section.end),
688
+ ].join('\n')
689
+ }
690
+
691
+ return placeEntries(lines, bullets, entries, false)
692
+ }
693
+
694
+ async function openFile(
695
+ dir: string,
696
+ slug: string,
697
+ name: string,
698
+ ): Promise<string | TeachRefused> {
699
+ const path = join(dir, name)
700
+
701
+ if (!existsSync(path)) {
702
+ return refuse('no-file', `${slug} carries no ${name}.`)
703
+ }
704
+
705
+ return path
706
+ }
707
+
708
+ /**
709
+ * Appends sources to `RESOURCES.md`, keeping what was read apart from what was
710
+ * only found.
711
+ *
712
+ * A URL already listed under either heading is refused rather than repeated,
713
+ * since a second entry for one source splits what rests on it across two lines.
714
+ */
715
+ export async function recordSources(
716
+ root: string,
717
+ selector: string,
718
+ read: readonly Source[],
719
+ leads: readonly Source[],
720
+ ): Promise<SourceOutcome> {
721
+ const found = await findWorkspace(root, selector)
722
+ if ('ok' in found) return found
723
+
724
+ const path = await openFile(found.dir, found.slug, TEACH_RESOURCES)
725
+ if (typeof path !== 'string') return path
726
+
727
+ let text = await readFile(path, 'utf8')
728
+ const listed = unfenced(text)
729
+ .map((line) => line.text)
730
+ .filter(isEntry)
731
+
732
+ const repeated = [...read, ...leads].filter((source) =>
733
+ listed.some((line) => line.includes(`(${source.url})`)),
734
+ )
735
+
736
+ if (repeated.length > 0) {
737
+ return refuse(
738
+ 'listed',
739
+ `${TEACH_RESOURCES} already lists ${repeated.map((source) => source.url).join(', ')}.`,
740
+ repeated.map((source) => source.url),
741
+ )
742
+ }
743
+
744
+ for (const [heading, sources] of [
745
+ [READ_HEADING, read],
746
+ [LEADS_HEADING, leads],
747
+ ] as const) {
748
+ if (sources.length === 0) continue
749
+
750
+ const written = insertUnderHeading(
751
+ text,
752
+ heading,
753
+ sources.map((source) => `- [${source.title}](${source.url})`),
754
+ )
755
+
756
+ if (written === undefined) {
757
+ return refuse(
758
+ 'no-section',
759
+ `${TEACH_RESOURCES} carries no ${heading} section to write into.`,
760
+ [heading],
761
+ )
762
+ }
763
+
764
+ text = written
765
+ }
766
+
767
+ await writeFile(path, text)
768
+
769
+ return { ok: true, slug: found.slug, path, read, leads }
770
+ }
771
+
772
+ /**
773
+ * The entry shape the standard fixes, which leads with the bolded term.
774
+ *
775
+ * The definition is terminated before the citation is appended, since a caller
776
+ * passing a bare phrase would otherwise run it into the sentence naming where
777
+ * the term first appears.
778
+ */
779
+ function termEntry(term: Term, firstSeen: string | undefined): string {
780
+ const definition = /[.!?]$/.test(term.definition)
781
+ ? term.definition
782
+ : `${term.definition}.`
783
+
784
+ const where = firstSeen ? ` First seen in ${firstSeen}.` : ''
785
+
786
+ return `- **${term.term}**: ${definition}${where}`
787
+ }
788
+
789
+ /** The term a glossary entry defines, read back out of its bolded span. */
790
+ function definedTerm(entry: string): string {
791
+ return entry.replace(/^\*\*(.+?)\*\*.*$/s, '$1').toLowerCase()
792
+ }
793
+
794
+ /**
795
+ * Adds terms to `GLOSSARY.md`, alphabetically, in one read and one write.
796
+ *
797
+ * A term already defined is refused rather than replaced. A definition the
798
+ * subject has moved under is a revision of the entry rather than a second one,
799
+ * and no verb here can tell those two apart from the command line.
800
+ */
801
+ export async function defineTerms(
802
+ root: string,
803
+ selector: string,
804
+ terms: readonly Term[],
805
+ firstSeen: string | undefined,
806
+ ): Promise<TermOutcome> {
807
+ const found = await findWorkspace(root, selector)
808
+ if ('ok' in found) return found
809
+
810
+ const path = await openFile(found.dir, found.slug, TEACH_GLOSSARY)
811
+ if (typeof path !== 'string') return path
812
+
813
+ const text = await readFile(path, 'utf8')
814
+ const existing = glossaryTerms(text).map(definedTerm)
815
+
816
+ const defined = terms.filter((term) =>
817
+ existing.includes(term.term.toLowerCase()),
818
+ )
819
+
820
+ if (defined.length > 0) {
821
+ return refuse(
822
+ 'defined',
823
+ `${TEACH_GLOSSARY} already defines ${defined.map((term) => term.term).join(', ')}.`,
824
+ defined.map((term) => term.term),
825
+ )
826
+ }
827
+
828
+ const entries = terms.map((term) => termEntry(term, firstSeen))
829
+ const lines = text.split('\n')
830
+ const bullets = bulletRange(unfenced(text))
831
+
832
+ // A glossary carries no heading over its list, so the entries land in the
833
+ // bullet range itself. A file holding none yet is appended to, which covers a
834
+ // glossary written by hand rather than scaffolded here.
835
+ const written = bullets
836
+ ? placeEntries(lines, bullets, entries, true)
837
+ : `${text.trimEnd()}\n\n${entries.join('\n')}\n`
838
+
839
+ await writeFile(path, written)
840
+
841
+ return { ok: true, slug: found.slug, path, defined: terms }
842
+ }