@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,65 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { PROJECT_ROOT } from '@/project-root'
4
+
5
+ /**
6
+ * Spells the package root in a report where every other root spells a
7
+ * project-relative path. The `source` field promises a path inside the project
8
+ * and a package copy is the one source that promise cannot cover, so the
9
+ * spelling has to be one nothing will join to a project root.
10
+ */
11
+ const PACKAGE_LABEL = '<canon>'
12
+
13
+ export interface ReferenceRoot {
14
+ /** Absolute `tooling/` directory to search. */
15
+ readonly dir: string
16
+ /** How a report spells a copy found under this root. */
17
+ readonly label: string
18
+ }
19
+
20
+ export interface ResolvedReference {
21
+ readonly path: string
22
+ /** The label of the root that won, joined to the stack and filename. */
23
+ readonly source: string
24
+ }
25
+
26
+ function toolingSourceDir(root: string): string {
27
+ return join(root, 'tooling')
28
+ }
29
+
30
+ /**
31
+ * The roots a stack reference resolves against, mirroring `standardRoots` in
32
+ * `src/standards/read.ts`. No stack reference installs into a project any
33
+ * more, so the package corpus is the only root that answers there. The
34
+ * authoring root stays ahead of it for the toolkit's own repository, which
35
+ * authors the corpus this reads, and for a project authoring stacks of its
36
+ * own under the same layout.
37
+ */
38
+ export function referenceRoots(root: string): ReferenceRoot[] {
39
+ return [
40
+ { dir: toolingSourceDir(root), label: 'tooling' },
41
+ {
42
+ dir: toolingSourceDir(PROJECT_ROOT),
43
+ label: join(PACKAGE_LABEL, 'tooling'),
44
+ },
45
+ ]
46
+ }
47
+
48
+ /** Resolves a stack's reference doc, working root first. */
49
+ export function resolveReference(
50
+ root: string,
51
+ stack: string,
52
+ ): ResolvedReference | undefined {
53
+ for (const { dir, label } of referenceRoots(root)) {
54
+ const path = join(dir, stack, 'reference.md')
55
+ if (existsSync(path)) {
56
+ return { path, source: join(label, stack, 'reference.md') }
57
+ }
58
+ }
59
+
60
+ return undefined
61
+ }
62
+
63
+ export function readReference(reference: ResolvedReference): string {
64
+ return readFileSync(reference.path, 'utf8')
65
+ }
@@ -0,0 +1,147 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { mergeSections } from '@/tooling/gitignore'
4
+ import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
5
+ import {
6
+ collectDeps,
7
+ collectScripts,
8
+ type DepState,
9
+ readPackage,
10
+ type ScriptState,
11
+ } from '@/tooling/package'
12
+
13
+ export interface ConfigState {
14
+ readonly rel: string
15
+ readonly stack: string
16
+ readonly state: 'new' | 'drifted' | 'matching'
17
+ }
18
+
19
+ export interface SeedState {
20
+ readonly rel: string
21
+ readonly state: 'missing' | 'present'
22
+ }
23
+
24
+ export interface EntryState {
25
+ readonly entry: string
26
+ readonly state: 'missing' | 'present'
27
+ }
28
+
29
+ export interface ScanResult {
30
+ readonly configs: readonly ConfigState[]
31
+ readonly seeds: readonly SeedState[]
32
+ readonly scripts: readonly ScriptState[]
33
+ readonly deps: readonly DepState[]
34
+ readonly gitignore: readonly EntryState[]
35
+ readonly hasPackageJson: boolean
36
+ readonly totalChanges: number
37
+ }
38
+
39
+ function isIdentical(a: string, b: string): boolean {
40
+ if (!existsSync(b)) return false
41
+ return readFileSync(a).equals(readFileSync(b))
42
+ }
43
+
44
+ /**
45
+ * Compares every stack in the chain against the target and reports what would
46
+ * change. Nothing is written. Which stack wins a duplicate differs per
47
+ * category and mirrors the bash: configs, seeds, and scripts resolve nearest
48
+ * stack first, while dependencies and gitignore entries resolve from the
49
+ * furthest ancestor inward.
50
+ */
51
+ export function scan(chain: readonly Manifest[], target: string): ScanResult {
52
+ const configs: ConfigState[] = []
53
+ const seenConfigs = new Set<string>()
54
+
55
+ for (const manifest of chain) {
56
+ for (const rel of listFiles(manifest.configsDir)) {
57
+ if (seenConfigs.has(rel)) continue
58
+ seenConfigs.add(rel)
59
+
60
+ const source = join(manifest.configsDir, rel)
61
+ const dest = join(target, rel)
62
+ const state = !existsSync(dest)
63
+ ? 'new'
64
+ : isIdentical(source, dest)
65
+ ? 'matching'
66
+ : 'drifted'
67
+
68
+ configs.push({ rel, stack: manifest.name, state })
69
+ }
70
+ }
71
+
72
+ const seeds: SeedState[] = []
73
+ const seenSeeds = new Set<string>()
74
+
75
+ for (const manifest of chain) {
76
+ for (const rel of listFiles(manifest.seedsDir)) {
77
+ if (seenSeeds.has(rel)) continue
78
+ seenSeeds.add(rel)
79
+
80
+ seeds.push({
81
+ rel,
82
+ state: existsSync(join(target, rel)) ? 'present' : 'missing',
83
+ })
84
+ }
85
+ }
86
+
87
+ const packagePath = join(target, 'package.json')
88
+ const pkg = readPackage(packagePath)
89
+ const scripts = pkg ? collectScripts(chain, pkg) : []
90
+ const deps = pkg ? collectDeps(chain, pkg) : []
91
+
92
+ const gitignorePath = join(target, '.gitignore')
93
+ const gitignoreContent = existsSync(gitignorePath)
94
+ ? readFileSync(gitignorePath, 'utf8')
95
+ : ''
96
+ const gitignore = scanGitignore(chain, gitignoreContent)
97
+
98
+ const totalChanges =
99
+ configs.filter((entry) => entry.state !== 'matching').length +
100
+ seeds.filter((entry) => entry.state === 'missing').length +
101
+ scripts.filter((entry) => entry.state !== 'matching').length +
102
+ deps.filter((entry) => entry.state === 'missing').length +
103
+ gitignore.filter((entry) => entry.state === 'missing').length
104
+
105
+ return {
106
+ configs,
107
+ seeds,
108
+ scripts,
109
+ deps,
110
+ gitignore,
111
+ hasPackageJson: pkg !== undefined,
112
+ totalChanges,
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Replays the merge against a copy of the file so an entry that an earlier
118
+ * section or stack would add is not also reported as missing.
119
+ */
120
+ function scanGitignore(
121
+ chain: readonly Manifest[],
122
+ content: string,
123
+ ): EntryState[] {
124
+ const states: EntryState[] = []
125
+ const seen = new Set<string>()
126
+ let projected = content
127
+
128
+ for (const manifest of ancestorsFirst(chain)) {
129
+ for (const section of manifest.gitignore) {
130
+ for (const entry of section.entries) {
131
+ if (seen.has(entry)) continue
132
+ seen.add(entry)
133
+
134
+ const result = mergeSections(projected, [
135
+ { header: section.header, entries: [entry] },
136
+ ])
137
+ states.push({
138
+ entry,
139
+ state: result.added.length > 0 ? 'missing' : 'present',
140
+ })
141
+ projected = result.content
142
+ }
143
+ }
144
+ }
145
+
146
+ return states
147
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { writeChainStamp } from '@/sync/stamp'
4
+ import type { Manifest } from '@/tooling/manifest'
5
+ import { readPackage } from '@/tooling/package'
6
+
7
+ /**
8
+ * Whether the target is a workspace root, where every package resolves its own
9
+ * stack. `package.json` covers bun, npm, and yarn, and pnpm declares the same
10
+ * fact in its own file instead.
11
+ */
12
+ export function isWorkspaceRoot(target: string): boolean {
13
+ if (existsSync(join(target, 'pnpm-workspace.yaml'))) return true
14
+
15
+ const workspaces = readPackage(join(target, 'package.json'))?.workspaces
16
+ return Array.isArray(workspaces) || isRecord(workspaces)
17
+ }
18
+
19
+ /**
20
+ * Records the chain an install resolved, and returns whether it wrote one. A
21
+ * workspace root is the single refusal: one chain recorded there would be a
22
+ * guess at what its packages hold, and the report reading tooling as unmeasured
23
+ * is the true answer rather than a clean one.
24
+ */
25
+ export async function recordToolingChain(
26
+ toolkitRoot: string,
27
+ target: string,
28
+ chain: readonly Manifest[],
29
+ now: Date,
30
+ ): Promise<boolean> {
31
+ if (isWorkspaceRoot(target)) return false
32
+
33
+ await writeChainStamp(
34
+ target,
35
+ { domain: 'tooling', toolkitRoot },
36
+ chain.map((manifest) => manifest.name),
37
+ now,
38
+ )
39
+ return true
40
+ }
41
+
42
+ function isRecord(value: unknown): value is Record<string, unknown> {
43
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
44
+ }
@@ -0,0 +1,156 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readdirSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { readFileSync } from 'node:fs'
11
+ import { execa } from 'execa'
12
+ import {
13
+ type VideoMetadata,
14
+ formatUploadDate,
15
+ renderFrontmatter,
16
+ slugify,
17
+ } from '@/transcripts/metadata'
18
+ import { dedupeRollingCues, parseVtt, renderBody } from '@/transcripts/vtt'
19
+
20
+ const YT_DLP_TIMEOUT_MS = 120_000
21
+
22
+ export function ensureYtDlp(): void {
23
+ if (Bun.which('yt-dlp') === null) {
24
+ throw new Error(
25
+ 'yt-dlp not found on PATH. Install it from https://github.com/yt-dlp/yt-dlp.',
26
+ )
27
+ }
28
+ }
29
+
30
+ interface FetchResult {
31
+ readonly metadata: VideoMetadata
32
+ readonly subPath: string | null
33
+ }
34
+
35
+ async function runYtDlp(url: string, workDir: string): Promise<FetchResult> {
36
+ const outputTemplate = join(workDir, '%(id)s.%(ext)s')
37
+ let stdout: string
38
+ try {
39
+ const result = await execa(
40
+ 'yt-dlp',
41
+ [
42
+ '--dump-json',
43
+ '--no-simulate',
44
+ '--write-sub',
45
+ '--write-auto-sub',
46
+ '--sub-langs',
47
+ 'en,en-orig',
48
+ '--sub-format',
49
+ 'vtt',
50
+ '--skip-download',
51
+ '-o',
52
+ outputTemplate,
53
+ url,
54
+ ],
55
+ { timeout: YT_DLP_TIMEOUT_MS },
56
+ )
57
+ stdout = result.stdout
58
+ } catch (error) {
59
+ throw new Error(`yt-dlp failed for ${url}: ${lastLine(error)}`)
60
+ }
61
+
62
+ const info = JSON.parse(stdout) as Record<string, unknown>
63
+ const videoId = String(info['id'] ?? '').trim()
64
+ if (!videoId) throw new Error('yt-dlp returned no video id')
65
+
66
+ const metadata: VideoMetadata = {
67
+ videoId,
68
+ title: String(info['title'] || 'untitled'),
69
+ channel: String(info['channel'] || info['uploader'] || 'unknown'),
70
+ durationSeconds: Number(info['duration'] || 0),
71
+ published: formatUploadDate(asString(info['upload_date'])),
72
+ url: String(info['webpage_url'] || url),
73
+ }
74
+ const subPath = findSubtitle(workDir, videoId)
75
+ return { metadata, subPath }
76
+ }
77
+
78
+ interface WriteOptions {
79
+ readonly outDir: string
80
+ readonly keepTimestamps: boolean
81
+ readonly fetchedAt: string
82
+ }
83
+
84
+ export function writeTranscript(
85
+ metadata: VideoMetadata,
86
+ subPath: string | null,
87
+ { outDir, keepTimestamps, fetchedAt }: WriteOptions,
88
+ ): string {
89
+ mkdirSync(outDir, { recursive: true })
90
+ const slug = `${slugify(metadata.title)}--${metadata.videoId}`
91
+ const target = join(outDir, `${slug}.md`)
92
+
93
+ const hasTranscript = subPath !== null
94
+ let body = ''
95
+ if (subPath !== null) {
96
+ const cues = parseVtt(readFileSync(subPath, 'utf8'))
97
+ body = renderBody(dedupeRollingCues(cues), { keepTimestamps })
98
+ } else {
99
+ body =
100
+ 'Transcript unavailable. Auto-captions are not enabled for this video.\n'
101
+ }
102
+
103
+ const frontmatter = renderFrontmatter(metadata, { hasTranscript, fetchedAt })
104
+ const heading = `# ${metadata.title}\n\n`
105
+ writeFileSync(target, frontmatter + heading + body)
106
+ return target
107
+ }
108
+
109
+ interface FetchOptions {
110
+ readonly outDir: string
111
+ readonly keepTimestamps: boolean
112
+ }
113
+
114
+ export async function fetchOne(
115
+ url: string,
116
+ { outDir, keepTimestamps }: FetchOptions,
117
+ ): Promise<string> {
118
+ const workDir = mkdtempSync(join(tmpdir(), 'yt-transcripts-'))
119
+ try {
120
+ const { metadata, subPath } = await runYtDlp(url, workDir)
121
+ return writeTranscript(metadata, subPath, {
122
+ outDir,
123
+ keepTimestamps,
124
+ fetchedAt: todayLocal(),
125
+ })
126
+ } finally {
127
+ rmSync(workDir, { recursive: true, force: true })
128
+ }
129
+ }
130
+
131
+ function findSubtitle(workDir: string, videoId: string): string | null {
132
+ const matches = readdirSync(workDir)
133
+ .filter((name) => name.startsWith(videoId) && name.endsWith('.vtt'))
134
+ .sort()
135
+ return matches.length ? join(workDir, matches[0]) : null
136
+ }
137
+
138
+ function todayLocal(): string {
139
+ const now = new Date()
140
+ const year = now.getFullYear()
141
+ const month = String(now.getMonth() + 1).padStart(2, '0')
142
+ const day = String(now.getDate()).padStart(2, '0')
143
+ return `${year}-${month}-${day}`
144
+ }
145
+
146
+ function asString(value: unknown): string | undefined {
147
+ return typeof value === 'string' ? value : undefined
148
+ }
149
+
150
+ function lastLine(error: unknown): string {
151
+ const stderr =
152
+ error instanceof Error && 'stderr' in error ? String(error.stderr) : ''
153
+ const lines = stderr.trim().split('\n').filter(Boolean)
154
+ if (lines.length) return lines[lines.length - 1]
155
+ return error instanceof Error ? error.message : String(error)
156
+ }
@@ -0,0 +1,54 @@
1
+ export interface VideoMetadata {
2
+ readonly videoId: string
3
+ readonly title: string
4
+ readonly channel: string
5
+ readonly durationSeconds: number
6
+ readonly published: string
7
+ readonly url: string
8
+ }
9
+
10
+ const SLUG_PATTERN = /[^a-z0-9]+/g
11
+
12
+ export function slugify(text: string, maxLength = 50): string {
13
+ const cleaned = text
14
+ .toLowerCase()
15
+ .replace(SLUG_PATTERN, '-')
16
+ .replace(/(^-|-$)/g, '')
17
+ if (!cleaned) return 'untitled'
18
+ return cleaned.slice(0, maxLength).replace(/-$/, '') || 'untitled'
19
+ }
20
+
21
+ export function formatUploadDate(yyyymmdd: string | undefined): string {
22
+ if (!yyyymmdd || yyyymmdd.length !== 8 || !/^\d{8}$/.test(yyyymmdd)) return ''
23
+ return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6)}`
24
+ }
25
+
26
+ function yamlEscape(value: string): string {
27
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
28
+ }
29
+
30
+ interface FrontmatterOptions {
31
+ readonly hasTranscript: boolean
32
+ readonly fetchedAt: string
33
+ }
34
+
35
+ export function renderFrontmatter(
36
+ video: VideoMetadata,
37
+ { hasTranscript, fetchedAt }: FrontmatterOptions,
38
+ ): string {
39
+ const lines = [
40
+ '---',
41
+ `title: ${yamlEscape(video.title)}`,
42
+ `channel: ${yamlEscape(video.channel)}`,
43
+ `video_id: ${video.videoId}`,
44
+ `url: ${video.url}`,
45
+ `duration: ${video.durationSeconds}`,
46
+ video.published ? `published: ${video.published}` : 'published:',
47
+ `fetched_at: ${fetchedAt}`,
48
+ `has_transcript: ${hasTranscript ? 'true' : 'false'}`,
49
+ '---',
50
+ '',
51
+ '',
52
+ ]
53
+ return lines.join('\n')
54
+ }
@@ -0,0 +1,114 @@
1
+ export interface Cue {
2
+ readonly timestamp: string
3
+ readonly text: string
4
+ }
5
+
6
+ const VTT_TIMESTAMP = /^(\d\d:)?\d\d:\d\d\.\d{3} --> .*$/
7
+ const VTT_INLINE_TAG = /<[^>]+>/g
8
+ const VTT_NUMERIC_LINE = /^\d+$/
9
+
10
+ export function parseVtt(raw: string): Cue[] {
11
+ const cues: Cue[] = []
12
+ let currentTimestamp: string | null = null
13
+ let currentLines: string[] = []
14
+ let inHeader = true
15
+
16
+ const flush = (): void => {
17
+ if (currentTimestamp !== null && currentLines.length) {
18
+ const text = currentLines.join(' ').trim()
19
+ if (text) cues.push({ timestamp: currentTimestamp, text })
20
+ }
21
+ currentTimestamp = null
22
+ currentLines = []
23
+ }
24
+
25
+ for (const rawLine of raw.split(/\r?\n/)) {
26
+ const line = rawLine.replace(/\s+$/, '')
27
+ if (inHeader) {
28
+ if (VTT_TIMESTAMP.test(line)) inHeader = false
29
+ else continue
30
+ }
31
+ if (VTT_TIMESTAMP.test(line)) {
32
+ flush()
33
+ currentTimestamp = line.split(' --> ', 1)[0].trim()
34
+ continue
35
+ }
36
+ if (line.trim() === '') {
37
+ flush()
38
+ continue
39
+ }
40
+ if (VTT_NUMERIC_LINE.test(line.trim())) continue
41
+ const cleaned = line.replace(VTT_INLINE_TAG, '').trim()
42
+ if (cleaned) currentLines.push(cleaned)
43
+ }
44
+ flush()
45
+ return cues
46
+ }
47
+
48
+ export function dedupeRollingCues(cues: Cue[]): Cue[] {
49
+ const deduped: Cue[] = []
50
+ let running = ''
51
+ for (const { timestamp, text } of cues) {
52
+ const maxOverlap = Math.min(running.length, text.length)
53
+ let overlap = 0
54
+ for (let size = maxOverlap; size > 0; size -= 1) {
55
+ if (running.endsWith(text.slice(0, size))) {
56
+ overlap = size
57
+ break
58
+ }
59
+ }
60
+ const addition = text.slice(overlap).trim()
61
+ if (!addition) continue
62
+ running = `${running} ${addition}`.trim()
63
+ deduped.push({ timestamp, text: addition })
64
+ }
65
+ return deduped
66
+ }
67
+
68
+ function vttTimestampToSeconds(stamp: string): number {
69
+ const parts = stamp.split(':')
70
+ let hours = '0'
71
+ let minutes: string
72
+ let rest: string
73
+ if (parts.length === 3) [hours, minutes, rest] = parts
74
+ else if (parts.length === 2) [minutes, rest] = parts
75
+ else return 0
76
+ const seconds = rest.split('.', 1)[0]
77
+ return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds)
78
+ }
79
+
80
+ function formatSeconds(seconds: number): string {
81
+ const minutes = Math.floor(seconds / 60)
82
+ const secs = seconds % 60
83
+ return `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
84
+ }
85
+
86
+ interface RenderOptions {
87
+ readonly keepTimestamps: boolean
88
+ }
89
+
90
+ export function renderBody(
91
+ cues: Cue[],
92
+ { keepTimestamps }: RenderOptions,
93
+ ): string {
94
+ if (!cues.length) return ''
95
+ if (keepTimestamps) {
96
+ const lines = cues.map(
97
+ ({ timestamp, text }) =>
98
+ `[${formatSeconds(vttTimestampToSeconds(timestamp))}] ${text}`,
99
+ )
100
+ return `${lines.join('\n')}\n`
101
+ }
102
+ const joined = cues.map(({ text }) => text).join(' ')
103
+ return `${paragraphWrap(joined)}\n`
104
+ }
105
+
106
+ function paragraphWrap(text: string, wordsPerParagraph = 60): string {
107
+ const words = text.split(/\s+/).filter(Boolean)
108
+ if (!words.length) return ''
109
+ const paragraphs: string[] = []
110
+ for (let start = 0; start < words.length; start += wordsPerParagraph) {
111
+ paragraphs.push(words.slice(start, start + wordsPerParagraph).join(' '))
112
+ }
113
+ return paragraphs.join('\n\n')
114
+ }