@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,250 @@
1
+ import { execa } from 'execa'
2
+ import { gitEnv } from '@/git-env'
3
+ import { isDirectory } from '@/target'
4
+
5
+ const GH_TIMEOUT_MS = 30_000
6
+
7
+ /**
8
+ * The two headings a review pass posts under.
9
+ *
10
+ * Owned by `claude-pr-review`, which states the full set once, and pinned here
11
+ * the way `claude-orchestrate/scripts/poll.sh` pins them. All three surfaces
12
+ * ship separately, so a heading added in that skill goes stale here with
13
+ * nothing comparing the copies.
14
+ */
15
+ const REVIEW_OPEN = '## Review'
16
+ const REVIEW_CLOSED = '## Review closed'
17
+
18
+ /** Why a target produced no reading, so an unreachable one never reads as having no work. */
19
+ export type TargetRefusal = 'not-a-directory' | 'gh-unavailable' | 'list-failed'
20
+
21
+ export type ChecksState = 'passing' | 'failing' | 'pending'
22
+
23
+ /** Whether the newest review pass left work owed. */
24
+ export type ReviewState = 'open' | 'closed'
25
+
26
+ export interface PullState {
27
+ readonly number: number
28
+ readonly title: string
29
+ readonly url: string
30
+ readonly head: string
31
+ /** Null when GitHub reported no check at all, which is not the same answer as passing. */
32
+ readonly checks: ChecksState | null
33
+ /** Null when no pass carrying a review heading has landed on the thread. */
34
+ readonly review: ReviewState | null
35
+ /** False when the review read failed, leaving `review` covering nothing. */
36
+ readonly reviewReadable: boolean
37
+ }
38
+
39
+ export type TargetPulls =
40
+ | {
41
+ readonly kind: 'refused'
42
+ readonly path: string
43
+ readonly reason: TargetRefusal
44
+ }
45
+ | {
46
+ readonly kind: 'read'
47
+ readonly path: string
48
+ readonly pulls: readonly PullState[]
49
+ }
50
+
51
+ /** Runs one `gh` invocation in a target and hands back its stdout, or null when it failed. */
52
+ export type GhRunner = (
53
+ cwd: string,
54
+ args: readonly string[],
55
+ ) => Promise<string | null>
56
+
57
+ export interface PullsOptions {
58
+ readonly run?: GhRunner
59
+ }
60
+
61
+ /**
62
+ * `gh` resolves its repository through the same environment variables git does
63
+ * and they beat `cwd`, so a run from inside a hook would read whichever
64
+ * repository that hook's environment names rather than the target handed here.
65
+ */
66
+ const runGh: GhRunner = async (cwd, args) => {
67
+ if (Bun.which('gh') === null) return null
68
+
69
+ try {
70
+ const result = await execa('gh', [...args], {
71
+ cwd,
72
+ timeout: GH_TIMEOUT_MS,
73
+ env: gitEnv(),
74
+ extendEnv: false,
75
+ })
76
+ return result.stdout
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ interface RawPull {
83
+ readonly number?: number
84
+ readonly title?: string
85
+ readonly url?: string
86
+ readonly headRefOid?: string
87
+ readonly statusCheckRollup?: readonly RawCheck[]
88
+ }
89
+
90
+ interface RawCheck {
91
+ readonly status?: string
92
+ readonly conclusion?: string
93
+ readonly state?: string
94
+ }
95
+
96
+ interface RawReview {
97
+ readonly body?: string
98
+ readonly submittedAt?: string
99
+ }
100
+
101
+ const FAILED = new Set([
102
+ 'FAILURE',
103
+ 'TIMED_OUT',
104
+ 'CANCELLED',
105
+ 'ACTION_REQUIRED',
106
+ 'STARTUP_FAILURE',
107
+ 'ERROR',
108
+ ])
109
+
110
+ /**
111
+ * Collapses every check on a head into one word.
112
+ *
113
+ * A failure outranks a pending one, because a run still going cannot clear a
114
+ * job that already failed and reporting the head as pending would invite a
115
+ * wait for an answer that has arrived.
116
+ */
117
+ export function rollup(checks: readonly RawCheck[]): ChecksState | null {
118
+ if (checks.length === 0) return null
119
+
120
+ const verdicts = checks.map((check) => check.conclusion ?? check.state ?? '')
121
+
122
+ if (verdicts.some((verdict) => FAILED.has(verdict))) return 'failing'
123
+
124
+ const running = checks.some(
125
+ (check) =>
126
+ (check.status !== undefined && check.status !== 'COMPLETED') ||
127
+ check.state === 'PENDING' ||
128
+ (check.conclusion === undefined && check.state === undefined),
129
+ )
130
+
131
+ return running ? 'pending' : 'passing'
132
+ }
133
+
134
+ /**
135
+ * Reads the heading of the newest pass carrying one, matching on the first line
136
+ * alone the way `poll.sh` does.
137
+ *
138
+ * The reviews arrive oldest first, so the last match is the current state of
139
+ * the thread. A pass carrying neither heading is somebody reviewing by hand and
140
+ * says nothing about whether the loop owes work.
141
+ */
142
+ export function latestReview(
143
+ reviews: readonly RawReview[],
144
+ ): ReviewState | null {
145
+ let state: ReviewState | null = null
146
+
147
+ for (const review of reviews) {
148
+ const first = (review.body ?? '').split('\n')[0]?.replace(/\r$/, '')
149
+ if (first === REVIEW_OPEN) state = 'open'
150
+ else if (first === REVIEW_CLOSED) state = 'closed'
151
+ }
152
+
153
+ return state
154
+ }
155
+
156
+ function parse<T>(text: string | null): T | null {
157
+ if (text === null) return null
158
+ try {
159
+ return JSON.parse(text) as T
160
+ } catch {
161
+ return null
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Reports the open pull requests in one target with their checks and the
167
+ * heading their newest review pass carries.
168
+ *
169
+ * A list that failed and a target with no open pull request are separated
170
+ * rather than collapsed, since reading the first as the second reports a target
171
+ * as done when nothing was read at all. That is the failure mode the shell loop
172
+ * this replaces had no way to surface.
173
+ */
174
+ export async function readTargetPulls(
175
+ path: string,
176
+ opts: PullsOptions = {},
177
+ ): Promise<TargetPulls> {
178
+ const run = opts.run ?? runGh
179
+
180
+ if (!isDirectory(path))
181
+ return { kind: 'refused', path, reason: 'not-a-directory' }
182
+
183
+ if (opts.run === undefined && Bun.which('gh') === null) {
184
+ return { kind: 'refused', path, reason: 'gh-unavailable' }
185
+ }
186
+
187
+ const listed = parse<readonly RawPull[]>(
188
+ await run(path, [
189
+ 'pr',
190
+ 'list',
191
+ '--state',
192
+ 'open',
193
+ '--json',
194
+ 'number,title,url,headRefOid,statusCheckRollup',
195
+ ]),
196
+ )
197
+
198
+ if (listed === null) return { kind: 'refused', path, reason: 'list-failed' }
199
+
200
+ const pulls = await Promise.all(
201
+ listed
202
+ .filter(
203
+ (raw): raw is RawPull & { number: number } =>
204
+ typeof raw.number === 'number',
205
+ )
206
+ .map(async (raw) => {
207
+ // One query per pull request, so a review read that failed surfaces on
208
+ // the thread it failed for rather than emptying the whole target.
209
+ const reviews = parse<{ reviews?: readonly RawReview[] }>(
210
+ await run(path, [
211
+ 'pr',
212
+ 'view',
213
+ String(raw.number),
214
+ '--json',
215
+ 'reviews',
216
+ ]),
217
+ )
218
+
219
+ return {
220
+ number: raw.number,
221
+ title: raw.title ?? '',
222
+ url: raw.url ?? '',
223
+ head: raw.headRefOid ?? '',
224
+ checks: rollup(raw.statusCheckRollup ?? []),
225
+ review: reviews === null ? null : latestReview(reviews.reviews ?? []),
226
+ reviewReadable: reviews !== null,
227
+ }
228
+ }),
229
+ )
230
+
231
+ return { kind: 'read', path, pulls }
232
+ }
233
+
234
+ /**
235
+ * Reads every target, one at a time.
236
+ *
237
+ * The reads are serial rather than batched because each one spends a GitHub API
238
+ * quota shared across all of them, and a wave running over a dozen targets that
239
+ * fired them together would meet the secondary rate limit rather than an answer.
240
+ */
241
+ export async function readPullsAcross(
242
+ paths: readonly string[],
243
+ opts: PullsOptions = {},
244
+ ): Promise<readonly TargetPulls[]> {
245
+ const reports: TargetPulls[] = []
246
+
247
+ for (const path of paths) reports.push(await readTargetPulls(path, opts))
248
+
249
+ return reports
250
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { homedir } from 'node:os'
9
+ import { dirname, join, resolve } from 'node:path'
10
+
11
+ /**
12
+ * One project the toolkit has installed into, as the install recorded it.
13
+ *
14
+ * The path is the target root rather than its git directory, because the
15
+ * record is written by a sync that knows where it wrote and not by anything
16
+ * that resolved a repository. Whether two entries are one project is a
17
+ * question about their origins, which `src/targets/sweep.ts` answers.
18
+ */
19
+ export interface TargetRecord {
20
+ readonly path: string
21
+ /** ISO stamp of the most recent sync that recorded this target. */
22
+ readonly stampedAt: string
23
+ }
24
+
25
+ /**
26
+ * An absent file and an empty one are separate answers, the same split
27
+ * `src/sessions/registry.ts` draws.
28
+ *
29
+ * The first means no sync has ever recorded a target on this machine, so the
30
+ * population is unknown and the sweep is the only reading available. The
31
+ * second means the file was read and holds no usable row, which a caller
32
+ * should be able to tell apart from a lookup that never ran.
33
+ */
34
+ export type TargetRegistry =
35
+ | { readonly kind: 'absent'; readonly path: string }
36
+ | {
37
+ readonly kind: 'read'
38
+ readonly path: string
39
+ readonly targets: readonly TargetRecord[]
40
+ }
41
+
42
+ interface StoredRegistry {
43
+ readonly version: number
44
+ readonly targets: readonly TargetRecord[]
45
+ }
46
+
47
+ const VERSION = 1
48
+
49
+ /**
50
+ * Resolves the folder holding this machine's toolkit state.
51
+ *
52
+ * Twin of `sandboxTree` in `src/commands/sandbox.ts`, which resolves the same
53
+ * three sources in the same order. The override exists so a test never writes
54
+ * into the home directory of whoever runs it.
55
+ */
56
+ export function stateDir(): string {
57
+ return stateDirNamed('canon')
58
+ }
59
+
60
+ /**
61
+ * Where this machine's state sat before the rename. Read below when the
62
+ * current folder holds no registry, and never written to by anything here.
63
+ */
64
+ export function retiredStateDir(): string {
65
+ // canon-keep-retired
66
+ return stateDirNamed('aitk')
67
+ }
68
+
69
+ /**
70
+ * The override honors the retired variable too, since an operator who exported
71
+ * it is the same operator whose registry sits under the retired folder.
72
+ */
73
+ function stateDirNamed(folder: string): string {
74
+ // canon-keep-retired
75
+ const override = process.env.CANON_STATE_DIR ?? process.env.AITK_STATE_DIR
76
+ if (override !== undefined && override !== '') return override
77
+
78
+ const state = process.env.XDG_STATE_HOME
79
+ const base =
80
+ state !== undefined && state !== ''
81
+ ? state
82
+ : join(homedir(), '.local', 'state')
83
+
84
+ return join(base, folder)
85
+ }
86
+
87
+ /**
88
+ * The registry this machine uses, current path first and the retired one
89
+ * behind it.
90
+ *
91
+ * The rename moved the folder and nothing migrates it, so a machine that has
92
+ * been recording targets for months would otherwise answer with an empty
93
+ * registry the first time it ran the renamed binary. Empty is
94
+ * indistinguishable from a machine that never installed anything, which is
95
+ * exactly the confident wrong answer this record exists to prevent.
96
+ *
97
+ * Resolving one path for both the read and the write is deliberate. A machine
98
+ * already holding the retired file keeps using it rather than being migrated
99
+ * underneath, which mirrors how `readStamp` treats a target's retired config.
100
+ */
101
+ export function registryPath(): string {
102
+ const current = join(stateDir(), 'targets.json')
103
+ if (existsSync(current)) return current
104
+
105
+ const retired = join(retiredStateDir(), 'targets.json')
106
+ return existsSync(retired) ? retired : current
107
+ }
108
+
109
+ function isRecord(value: Partial<TargetRecord>): value is TargetRecord {
110
+ return (
111
+ typeof value.path === 'string' &&
112
+ value.path.length > 0 &&
113
+ typeof value.stampedAt === 'string' &&
114
+ value.stampedAt.length > 0
115
+ )
116
+ }
117
+
118
+ /**
119
+ * Reads every recorded target, sorted by path.
120
+ *
121
+ * A row missing either field is dropped rather than reported. This module is
122
+ * the file's only writer, so a malformed row is a hand edit or a truncated
123
+ * write and neither is a finding the caller can act on. What a caller can act
124
+ * on is the file being absent, which is its own kind above.
125
+ */
126
+ export function readTargetRegistry(
127
+ path: string = registryPath(),
128
+ ): TargetRegistry {
129
+ let text: string
130
+ try {
131
+ text = readFileSync(path, 'utf8')
132
+ } catch {
133
+ return { kind: 'absent', path }
134
+ }
135
+
136
+ let parsed: unknown
137
+ try {
138
+ parsed = JSON.parse(text)
139
+ } catch {
140
+ return { kind: 'read', path, targets: [] }
141
+ }
142
+
143
+ if (typeof parsed !== 'object' || parsed === null) {
144
+ return { kind: 'read', path, targets: [] }
145
+ }
146
+
147
+ const stored = parsed as Partial<StoredRegistry>
148
+ const rows = Array.isArray(stored.targets) ? stored.targets : []
149
+ const targets = rows
150
+ .filter((row): row is TargetRecord =>
151
+ isRecord(row as Partial<TargetRecord>),
152
+ )
153
+ .sort((a, b) => a.path.localeCompare(b.path))
154
+
155
+ return { kind: 'read', path, targets }
156
+ }
157
+
158
+ /** Why a record attempt did not land, so a caller can say so rather than assume it did. */
159
+ export type RecordOutcome = 'recorded' | 'unwritten'
160
+
161
+ /**
162
+ * Records one target, keyed by its resolved path and replacing any row already
163
+ * held for it.
164
+ *
165
+ * The write is a temp file plus a rename, so a reader never meets a half
166
+ * written file. Two syncs finishing together still resolve last-writer-wins on
167
+ * the merged set, which can drop the row the loser added. That is left rather
168
+ * than locked: the authoritative record of an install is the stamp inside the
169
+ * target, this index is a cache over those, and the next sync of the dropped
170
+ * target restores its row.
171
+ *
172
+ * Nothing removes a row either, so a target that was deleted or that dropped
173
+ * the toolkit stays here and the count drifts upward. `canon targets pulls`
174
+ * meets that on use, since it refuses a path it cannot open rather than
175
+ * reading it as a target with no work, but `canon targets list` does not: it
176
+ * never opens a recorded path, and the count is its whole output.
177
+ */
178
+ export function recordTarget(
179
+ target: string,
180
+ now: Date,
181
+ path: string = registryPath(),
182
+ ): RecordOutcome {
183
+ const resolved = resolve(target)
184
+ const current = readTargetRegistry(path)
185
+ const existing = current.kind === 'read' ? current.targets : []
186
+
187
+ const targets = [
188
+ ...existing.filter((row) => row.path !== resolved),
189
+ { path: resolved, stampedAt: now.toISOString() },
190
+ ].sort((a, b) => a.path.localeCompare(b.path))
191
+
192
+ const payload: StoredRegistry = { version: VERSION, targets }
193
+ const temp = `${path}.${process.pid}.tmp`
194
+
195
+ try {
196
+ mkdirSync(dirname(path), { recursive: true })
197
+ writeFileSync(temp, `${JSON.stringify(payload, null, 2)}\n`)
198
+ renameSync(temp, path)
199
+ return 'recorded'
200
+ } catch {
201
+ return 'unwritten'
202
+ }
203
+ }
@@ -0,0 +1,145 @@
1
+ import { resolve } from 'node:path'
2
+ import { isLegacyStamped } from '@/sync/stamp'
3
+ import {
4
+ readTargetRegistry,
5
+ type TargetRegistry,
6
+ registryPath,
7
+ } from '@/targets/registry'
8
+ import {
9
+ type SweepBound,
10
+ type SweepOptions,
11
+ sweepTargets,
12
+ } from '@/targets/sweep'
13
+
14
+ /**
15
+ * Where a target came from, carried on the row rather than inferred.
16
+ *
17
+ * A caller deciding whether an answer is trustworthy needs to know which rows
18
+ * the machine recorded for itself and which a walk guessed at, and the two
19
+ * carry different bounds.
20
+ */
21
+ export type TargetSource = 'given' | 'record' | 'sweep'
22
+
23
+ export interface KnownTarget {
24
+ /** Every checkout of this project on this machine, one for the ordinary case. */
25
+ readonly paths: readonly string[]
26
+ readonly origin: string | null
27
+ readonly source: TargetSource
28
+ /** When a sync last recorded this target, or null for a row only a sweep found. */
29
+ readonly stampedAt: string | null
30
+ /** True while the install stamp still sits at the retired path. */
31
+ readonly legacy: boolean
32
+ }
33
+
34
+ export interface ResolvedTargets {
35
+ readonly targets: readonly KnownTarget[]
36
+ /** Null when the caller named its targets, so no registry read was attempted. */
37
+ readonly registry: TargetRegistry | null
38
+ /** Null when no sweep ran, which is the ordinary case. */
39
+ readonly bound: SweepBound | null
40
+ }
41
+
42
+ export interface ResolveTargetsOptions extends SweepOptions {
43
+ /** Paths the caller named. These win outright and suppress both other sources. */
44
+ readonly paths?: readonly string[]
45
+ /** Roots to walk, supplementing the record rather than replacing it. */
46
+ readonly sweep?: readonly string[]
47
+ readonly registryFile?: string
48
+ }
49
+
50
+ /**
51
+ * Answers which projects the toolkit has installed into.
52
+ *
53
+ * The record written at install time is the primary source and a walk is the
54
+ * fallback, which is the shape the population needs: a sweep alone cannot see
55
+ * another machine or a clone under a path nobody named, and that is exactly how
56
+ * the count moved from four to seven inside one pass and was then wrong in both
57
+ * directions at once.
58
+ *
59
+ * A caller naming paths gets those and no lookup at all, since it has already
60
+ * answered the question this resolves.
61
+ */
62
+ export async function resolveTargets(
63
+ opts: ResolveTargetsOptions = {},
64
+ ): Promise<ResolvedTargets> {
65
+ if (opts.paths !== undefined && opts.paths.length > 0) {
66
+ return {
67
+ targets: opts.paths.map((path) => given(resolve(path))),
68
+ registry: null,
69
+ bound: null,
70
+ }
71
+ }
72
+
73
+ const file = opts.registryFile ?? registryPath()
74
+ const registry = readTargetRegistry(file)
75
+
76
+ const recorded: KnownTarget[] =
77
+ registry.kind === 'read'
78
+ ? registry.targets.map((row) => ({
79
+ paths: [row.path],
80
+ origin: null,
81
+ source: 'record' as const,
82
+ stampedAt: row.stampedAt,
83
+ legacy: isLegacyStamped(row.path),
84
+ }))
85
+ : []
86
+
87
+ if (opts.sweep === undefined || opts.sweep.length === 0) {
88
+ return { targets: recorded, registry, bound: null }
89
+ }
90
+
91
+ const swept = await sweepTargets(opts.sweep, opts)
92
+ const known = new Set(recorded.flatMap((target) => target.paths))
93
+
94
+ // A sweep row whose paths the record already holds is the same project read
95
+ // twice, so it adds nothing. A row holding one known path and one unknown one
96
+ // is the second-clone case, and it replaces the record's row rather than
97
+ // sitting beside it, since the sweep is the only source that can see both.
98
+ const added: KnownTarget[] = []
99
+ const superseded = new Set<string>()
100
+
101
+ for (const target of swept.targets) {
102
+ const overlap = target.paths.filter((path) => known.has(path))
103
+
104
+ if (overlap.length === target.paths.length) continue
105
+
106
+ for (const path of overlap) superseded.add(path)
107
+
108
+ // The recorded clone leads, because the record only names one a sync
109
+ // actually ran in, where the rest are checkouts a walk happened to find.
110
+ // Every caller reading a single path takes the first, and picking that by
111
+ // sort order is how a repair ran in one clone while the count was taken
112
+ // against another and the target read as untouched.
113
+ added.push({
114
+ paths: [...overlap, ...target.paths.filter((path) => !known.has(path))],
115
+ origin: target.origin,
116
+ source: overlap.length > 0 ? 'record' : 'sweep',
117
+ stampedAt:
118
+ recorded.find((row) => overlap.includes(row.paths[0] ?? ''))
119
+ ?.stampedAt ?? null,
120
+ legacy: target.legacy,
121
+ })
122
+ }
123
+
124
+ const kept = recorded.filter(
125
+ (row) => !row.paths.some((path) => superseded.has(path)),
126
+ )
127
+
128
+ return {
129
+ targets: [...kept, ...added].sort((a, b) =>
130
+ (a.paths[0] ?? '').localeCompare(b.paths[0] ?? ''),
131
+ ),
132
+ registry,
133
+ bound: swept.bound,
134
+ }
135
+ }
136
+
137
+ function given(path: string): KnownTarget {
138
+ return {
139
+ paths: [path],
140
+ origin: null,
141
+ source: 'given',
142
+ stampedAt: null,
143
+ legacy: isLegacyStamped(path),
144
+ }
145
+ }