@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,357 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { dirname, join } from 'node:path'
4
+ import { chromium } from 'playwright-core'
5
+ import type { Browser, BrowserContext, Page } from 'playwright-core'
6
+ import { isBrowserMissing } from '@/browser/engine'
7
+ import { deriveSteps } from '@/demo/compile'
8
+ import type { DemoPlan, DemoStep } from '@/demo/compile'
9
+ import type { CursorSet } from '@/demo/pointer'
10
+ import { pointerSource } from '@/demo/pointer'
11
+
12
+ declare global {
13
+ interface Window {
14
+ __canon_demo_caption__?: (text: string) => void
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Drives a running application and records what it did. Every browser reference
20
+ * the demo feature adds lives here, and `src/commands/demo.ts` reaches it
21
+ * through a dynamic import so no other command resolves the engine at startup.
22
+ *
23
+ * Unlike `@/capture/render`, this module ships. The capture command is excluded
24
+ * from the published package because it regenerates images committed to this
25
+ * repository, and that reason does not transfer to a command whose whole
26
+ * purpose is running in someone else's project.
27
+ *
28
+ * It imports `playwright-core` rather than `@playwright/test`, which stays a
29
+ * development dependency for the capture module. Shipping puts the import in
30
+ * every target's dependency tree, and a target needs the driver rather than a
31
+ * test runner and an assertion library. Both are pinned to one version rather
32
+ * than a range, because `bunx playwright install chromium` fetches the browser
33
+ * revision the installed engine expects and a float would leave a target
34
+ * resolving a binary its engine cannot launch.
35
+ */
36
+
37
+ const POINTER_SIZE = 32
38
+ /**
39
+ * Where the pointer starts. Any position inside the viewport works, since the
40
+ * point is giving it one move to install and paint before it travels to the
41
+ * first target. A corner keeps that first move out of the way of the content.
42
+ */
43
+ const START = { x: 8, y: 8 }
44
+ const SETTLE_MS = 250
45
+ /** Round trips sampled to price one, on the page a step is actually about to move across. */
46
+ const CALIBRATION_STEPS = 8
47
+ /** DOM id the caption bar installs under, read back by `drive.e2e.test.ts`. */
48
+ export const CAPTION_ID = '__canon_demo_caption_bar__'
49
+
50
+ /**
51
+ * The two output paths arrive resolved rather than as a root this re-resolves
52
+ * against, because the plan already carries a directory and resolving it twice
53
+ * nests the whole path inside itself.
54
+ *
55
+ * An absent path means the caller asked for that artifact not to be produced.
56
+ */
57
+ export interface DriveOptions {
58
+ readonly plan: DemoPlan
59
+ readonly cursors: CursorSet
60
+ readonly videoPath?: string
61
+ readonly stillPath?: string
62
+ }
63
+
64
+ export type DriveResult =
65
+ | {
66
+ status: 'recorded'
67
+ videoPath?: string
68
+ stillPath?: string
69
+ steps: number
70
+ durationMs: number
71
+ }
72
+ | { status: 'failed'; reason: DriveRefusal; message: string }
73
+
74
+ export type DriveRefusal = 'browser-missing' | 'drive-failed'
75
+
76
+ interface DriveFailure {
77
+ readonly status: 'failed'
78
+ readonly reason: DriveRefusal
79
+ readonly message: string
80
+ }
81
+
82
+ type Launch = { status: 'launched'; value: Browser } | DriveFailure
83
+
84
+ export async function drive(options: DriveOptions): Promise<DriveResult> {
85
+ const { plan } = options
86
+
87
+ const browser = await launch()
88
+ if (browser.status === 'failed') return browser
89
+
90
+ let videoDir: string | undefined
91
+ let context: BrowserContext
92
+ const started = Date.now()
93
+ try {
94
+ // Created after the launch, so a target with no browser binary does not
95
+ // leave an empty directory behind for a run that never started, and inside
96
+ // the try so a failure here closes the browser rather than leaking it.
97
+ videoDir = options.videoPath
98
+ ? mkdtempSync(join(tmpdir(), 'canon-demo-'))
99
+ : undefined
100
+
101
+ context = await browser.value.newContext({
102
+ viewport: plan.viewport,
103
+ // Pointed the opposite way from a test. A recording wants the motion the
104
+ // interface was designed with, where a test wants it suppressed.
105
+ reducedMotion: 'no-preference',
106
+ ...(videoDir
107
+ ? {
108
+ recordVideo: {
109
+ dir: videoDir,
110
+ size: plan.viewport,
111
+ showActions: {
112
+ duration: plan.annotations.durationMs,
113
+ position: plan.annotations.position,
114
+ fontSize: plan.annotations.fontSize,
115
+ },
116
+ },
117
+ }
118
+ : {}),
119
+ })
120
+ } catch (error) {
121
+ await browser.value.close()
122
+ if (videoDir) rmSync(videoDir, { recursive: true, force: true })
123
+ return failed('drive-failed', error)
124
+ }
125
+
126
+ let stillPath: string | undefined
127
+ let videoPath: string | undefined
128
+
129
+ try {
130
+ await context.addInitScript({
131
+ content: pointerSource(options.cursors, POINTER_SIZE),
132
+ })
133
+ await context.addInitScript({ content: captionInitScript() })
134
+ const page = await context.newPage()
135
+ const video = page.video()
136
+ const pace: PointerPace = {}
137
+
138
+ // The opening navigate is skipped when the plan already starts with one,
139
+ // because a draft written around an opening verb compiles to a `navigate`
140
+ // step for the same URL and the second load is a visible reload.
141
+ if (plan.steps[0]?.kind !== 'navigate') {
142
+ await page.goto(plan.url)
143
+ await page.mouse.move(START.x, START.y, { steps: 2 })
144
+ }
145
+
146
+ for (const step of plan.steps) {
147
+ await runStep(page, plan, step, pace)
148
+ // The first marked step wins. One file holds one frame, so a plan a
149
+ // person edited to mark several would otherwise write each over the last
150
+ // and keep whichever ran last, with nothing saying so.
151
+ if (step.still && options.stillPath && !stillPath) {
152
+ stillPath = options.stillPath
153
+ mkdirSync(dirname(stillPath), { recursive: true })
154
+ await page.screenshot({ path: stillPath })
155
+ }
156
+ }
157
+
158
+ await context.close()
159
+
160
+ if (video && options.videoPath) {
161
+ videoPath = options.videoPath
162
+ mkdirSync(dirname(videoPath), { recursive: true })
163
+ await video.saveAs(videoPath)
164
+ // The engine keeps the auto-named recording beside the saved copy, so a
165
+ // run that skipped this would leave two files for every demo.
166
+ await video.delete()
167
+ }
168
+ } catch (error) {
169
+ return failed('drive-failed', error)
170
+ } finally {
171
+ await browser.value.close()
172
+ if (videoDir) rmSync(videoDir, { recursive: true, force: true })
173
+ }
174
+
175
+ return {
176
+ status: 'recorded',
177
+ ...(videoPath ? { videoPath } : {}),
178
+ ...(stillPath ? { stillPath } : {}),
179
+ steps: plan.steps.length,
180
+ durationMs: Date.now() - started,
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Reads the launch through `@/browser/engine`, which is where the separation
186
+ * between a binary that was never installed and every other launch failure now
187
+ * lives. It moved out of this file when `canon inventory` became the second
188
+ * command needing it, rather than being copied.
189
+ */
190
+ async function launch(): Promise<Launch> {
191
+ try {
192
+ return { status: 'launched', value: await chromium.launch() }
193
+ } catch (error) {
194
+ return failed(
195
+ isBrowserMissing(error) ? 'browser-missing' : 'drive-failed',
196
+ error,
197
+ )
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Holds the round trip once a step has measured it, so every `moveTo` after
203
+ * the first reuses the same reading rather than re-timing on every move.
204
+ */
205
+ export interface PointerPace {
206
+ roundTripMs?: number
207
+ }
208
+
209
+ /**
210
+ * Exported so `drive.e2e.test.ts` can drive one real step against a real
211
+ * caption and read it back, which is the integration a full `drive()` call
212
+ * cannot assert without decoding the video it writes.
213
+ */
214
+ export async function runStep(
215
+ page: Page,
216
+ plan: DemoPlan,
217
+ step: DemoStep,
218
+ pace: PointerPace,
219
+ ): Promise<void> {
220
+ switch (step.kind) {
221
+ case 'navigate':
222
+ await page.goto(step.target || plan.url)
223
+ await page.mouse.move(START.x, START.y, { steps: 2 })
224
+ break
225
+ case 'click':
226
+ await moveTo(page, plan, step, pace)
227
+ await page.mouse.down()
228
+ await page.mouse.up()
229
+ break
230
+ case 'fill':
231
+ await moveTo(page, plan, step, pace)
232
+ await page.mouse.down()
233
+ await page.mouse.up()
234
+ await page.keyboard.type(step.text, { delay: plan.pointer.typeDelayMs })
235
+ break
236
+ case 'hover':
237
+ await moveTo(page, plan, step, pace)
238
+ break
239
+ case 'scroll':
240
+ await page.locator(step.target).first().scrollIntoViewIfNeeded()
241
+ await page.waitForTimeout(SETTLE_MS)
242
+ await moveTo(page, plan, step, pace)
243
+ break
244
+ case 'wait':
245
+ case 'hold':
246
+ break
247
+ }
248
+
249
+ // Set after the action rather than before it, so the caption shows for the
250
+ // hold that follows rather than for the page the action is about to leave.
251
+ await setCaption(page, step.caption)
252
+ if (step.waitFor) await page.locator(step.waitFor).first().waitFor()
253
+ await page.waitForTimeout(step.holdMs)
254
+ }
255
+
256
+ /**
257
+ * Timed on the page a `moveTo` is actually about to move across, never on the
258
+ * blank page before it, since layout, paint, and page script are what a step
259
+ * pays the round trip against and a blank page has none of the three.
260
+ */
261
+ async function calibrateRoundTrip(page: Page): Promise<number> {
262
+ const startedAt = Date.now()
263
+ await page.mouse.move(START.x, START.y, { steps: CALIBRATION_STEPS })
264
+ return (Date.now() - startedAt) / CALIBRATION_STEPS
265
+ }
266
+
267
+ /**
268
+ * Travel is the whole point of driving the engine's pointer rather than calling
269
+ * the element-clicking helper, which resolves a target and jumps to it. The
270
+ * step count is what separates a cursor that glides from one that teleports.
271
+ *
272
+ * Resolving through a bounding box assumes the target is in the viewport, so a
273
+ * target below the fold needs a `scroll` step ahead of it.
274
+ */
275
+ async function moveTo(
276
+ page: Page,
277
+ plan: DemoPlan,
278
+ step: DemoStep,
279
+ pace: PointerPace,
280
+ ): Promise<void> {
281
+ pace.roundTripMs ??= await calibrateRoundTrip(page)
282
+ const locator = page.locator(step.target).first()
283
+ await locator.waitFor()
284
+ const box = await locator.boundingBox()
285
+ if (!box) throw new Error(`${step.target} has no box to point at`)
286
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, {
287
+ steps: deriveSteps(plan.pointer.travelMs, pace.roundTripMs),
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Playwright's own `showActions` overlay names the API call it made, not the
293
+ * beat's narration, so a caption needs an element of its own rather than
294
+ * reusing that annotation. Runs alongside `pointerSource`, guarded the same
295
+ * way against a page that already carries one.
296
+ */
297
+ export function captionInitScript(): string {
298
+ return `(() => {
299
+ if (window.__canon_demo_caption__) return;
300
+
301
+ let label;
302
+
303
+ const install = () => {
304
+ if (label || !document.body) return;
305
+ const bar = document.createElement('div');
306
+ bar.id = '${CAPTION_ID}';
307
+ bar.setAttribute('aria-hidden', 'true');
308
+ bar.style.cssText = [
309
+ 'position:fixed',
310
+ 'left:0',
311
+ 'right:0',
312
+ 'bottom:32px',
313
+ 'display:flex',
314
+ 'justify-content:center',
315
+ 'pointer-events:none',
316
+ 'z-index:2147483647',
317
+ ].join(';');
318
+ label = document.createElement('span');
319
+ label.style.cssText = [
320
+ 'background:rgba(16,16,20,0.85)',
321
+ 'color:#f4f4f5',
322
+ 'font:600 20px/1.4 system-ui,sans-serif',
323
+ 'padding:10px 22px',
324
+ 'border-radius:8px',
325
+ 'max-width:80vw',
326
+ 'text-align:center',
327
+ 'display:none',
328
+ ].join(';');
329
+ bar.appendChild(label);
330
+ document.body.appendChild(bar);
331
+ window.__canon_demo_caption__ = (text) => {
332
+ label.textContent = text || '';
333
+ label.style.display = text ? 'inline-block' : 'none';
334
+ };
335
+ };
336
+
337
+ if (document.readyState === 'loading') {
338
+ addEventListener('DOMContentLoaded', install, { once: true });
339
+ } else {
340
+ install();
341
+ }
342
+ })();`
343
+ }
344
+
345
+ async function setCaption(page: Page, caption: string): Promise<void> {
346
+ await page.evaluate((text) => {
347
+ window.__canon_demo_caption__?.(text)
348
+ }, caption)
349
+ }
350
+
351
+ function failed(reason: DriveRefusal, error: unknown): DriveFailure {
352
+ return {
353
+ status: 'failed',
354
+ reason,
355
+ message: error instanceof Error ? error.message : String(error),
356
+ }
357
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The pointer the recording shows. The browser engine's own annotation draws a
3
+ * dot at the moment of a click and paints no cursor, so a run without this
4
+ * looks like the pointer teleports between targets.
5
+ *
6
+ * Nothing here touches a browser. `pointerSource` returns the script text that
7
+ * `@/demo/drive` installs before navigation, which keeps the hotspot arithmetic
8
+ * and the state table testable without launching anything.
9
+ */
10
+
11
+ export interface CursorHotspot {
12
+ readonly width: number
13
+ readonly height: number
14
+ readonly hotspotX: number
15
+ readonly hotspotY: number
16
+ }
17
+
18
+ export interface Cursor {
19
+ readonly image: string
20
+ readonly hotspot: CursorHotspot
21
+ }
22
+
23
+ export type CursorSet = Readonly<Record<string, Cursor>>
24
+
25
+ const HEADER_BYTES = 6
26
+ const ENTRY_BYTES = 16
27
+ const CURSOR_TYPE = 2
28
+ /** The format stores 256 as a zero, since the field is one byte wide. */
29
+ const SIZE_256 = 256
30
+
31
+ /**
32
+ * Reads the directory of a Windows cursor resource and returns the hotspot of
33
+ * its largest entry. A resource carries several sizes with a hotspot each, and
34
+ * taking the largest is what matches the artwork the recorder draws at.
35
+ *
36
+ * Only the header and directory are read. The pixel payload is left to the
37
+ * browser, which spike 3 measured as decoding the format directly with no
38
+ * conversion step and no image tooling on the machine.
39
+ */
40
+ export function cursorHotspot(bytes: Uint8Array): CursorHotspot | undefined {
41
+ if (bytes.byteLength < HEADER_BYTES + ENTRY_BYTES) return undefined
42
+
43
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
44
+ if (view.getUint16(2, true) !== CURSOR_TYPE) return undefined
45
+
46
+ const count = view.getUint16(4, true)
47
+ if (count === 0) return undefined
48
+
49
+ let largest: CursorHotspot | undefined
50
+ for (let index = 0; index < count; index += 1) {
51
+ const at = HEADER_BYTES + index * ENTRY_BYTES
52
+ if (at + ENTRY_BYTES > bytes.byteLength) break
53
+
54
+ const entry: CursorHotspot = {
55
+ width: bytes[at] === 0 ? SIZE_256 : (bytes[at] ?? 0),
56
+ height: bytes[at + 1] === 0 ? SIZE_256 : (bytes[at + 1] ?? 0),
57
+ hotspotX: view.getUint16(at + 4, true),
58
+ hotspotY: view.getUint16(at + 6, true),
59
+ }
60
+ if (!largest || entry.width * entry.height > largest.width * largest.height)
61
+ largest = entry
62
+ }
63
+ return largest
64
+ }
65
+
66
+ /**
67
+ * Converts a hotspot stated against the source artwork into an offset in the
68
+ * size the pointer is drawn at. Skipping this puts the artwork's top left where
69
+ * the click lands instead of its tip, which offsets every action by roughly a
70
+ * third of a cursor.
71
+ */
72
+ export function scaleHotspot(
73
+ hotspot: CursorHotspot,
74
+ drawnSize: number,
75
+ ): { x: number; y: number } {
76
+ const scale = drawnSize / hotspot.width
77
+ return {
78
+ x: hotspot.hotspotX * scale,
79
+ y: hotspot.hotspotY * (drawnSize / hotspot.height),
80
+ }
81
+ }
82
+
83
+ interface PointerState {
84
+ readonly image: string
85
+ readonly x: number
86
+ readonly y: number
87
+ }
88
+
89
+ /**
90
+ * Returns the script installed before navigation. It is a string rather than a
91
+ * function reference because the cursor payload is data resolved on this side,
92
+ * and passing artwork through an argument would still need serializing.
93
+ *
94
+ * The element inherits the page's world, so a site with its own element at this
95
+ * id, a stacking context that outranks it, or a style rule reaching it will
96
+ * interfere. That is the cost of drawing the pointer inside the page rather
97
+ * than reaching for a desktop recorder.
98
+ */
99
+ export function pointerSource(cursors: CursorSet, size: number): string {
100
+ const states: Record<string, PointerState> = {}
101
+ for (const [name, cursor] of Object.entries(cursors)) {
102
+ const offset = scaleHotspot(cursor.hotspot, size)
103
+ states[name] = { image: cursor.image, x: offset.x, y: offset.y }
104
+ }
105
+
106
+ const config = JSON.stringify({ size, states })
107
+
108
+ return `(() => {
109
+ const CONFIG = ${config};
110
+ const ID = '__canon_demo_pointer__';
111
+ if (window[ID]) return;
112
+ window[ID] = true;
113
+
114
+ let node;
115
+ let state = 'default';
116
+ let pressed = false;
117
+ let x = -9999;
118
+ let y = -9999;
119
+
120
+ const install = () => {
121
+ if (node || !document.body) return;
122
+ node = document.createElement('img');
123
+ node.id = ID;
124
+ node.setAttribute('aria-hidden', 'true');
125
+ node.style.cssText = [
126
+ 'position:fixed',
127
+ 'left:0',
128
+ 'top:0',
129
+ 'width:' + CONFIG.size + 'px',
130
+ 'height:' + CONFIG.size + 'px',
131
+ 'z-index:2147483647',
132
+ 'pointer-events:none',
133
+ 'user-select:none',
134
+ 'will-change:transform',
135
+ 'transition:transform 90ms ease-out',
136
+ ].join(';');
137
+ document.body.appendChild(node);
138
+ paint();
139
+ };
140
+
141
+ const paint = () => {
142
+ if (!node) return;
143
+ const cursor = CONFIG.states[state] || CONFIG.states.default;
144
+ if (!cursor) return;
145
+ if (node.getAttribute('src') !== cursor.image) node.setAttribute('src', cursor.image);
146
+ const scale = pressed ? 0.88 : 1;
147
+ node.style.transform =
148
+ 'translate(' + (x - cursor.x) + 'px,' + (y - cursor.y) + 'px) scale(' + scale + ')';
149
+ };
150
+
151
+ const stateAt = (target) => {
152
+ if (!(target instanceof Element)) return 'default';
153
+ const tag = target.tagName;
154
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || target.isContentEditable) return 'text';
155
+ const style = getComputedStyle(target).cursor;
156
+ if (style === 'pointer' && CONFIG.states.pointer) return 'pointer';
157
+ if (style === 'text' && CONFIG.states.text) return 'text';
158
+ return 'default';
159
+ };
160
+
161
+ addEventListener('mousemove', (event) => {
162
+ x = event.clientX;
163
+ y = event.clientY;
164
+ state = stateAt(event.target);
165
+ install();
166
+ paint();
167
+ }, true);
168
+
169
+ addEventListener('mousedown', () => { pressed = true; paint(); }, true);
170
+ addEventListener('mouseup', () => { pressed = false; paint(); }, true);
171
+
172
+ if (document.readyState === 'loading') {
173
+ addEventListener('DOMContentLoaded', install, { once: true });
174
+ } else {
175
+ install();
176
+ }
177
+ })();`
178
+ }
@@ -0,0 +1,112 @@
1
+ import { readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { cursorHotspot, type Cursor, type CursorSet } from '@/demo/pointer'
4
+
5
+ /**
6
+ * Reads a cursor theme folder so `--cursor` can point at one. Spike 3 measured
7
+ * the browser decoding a Windows cursor resource directly, so nothing here
8
+ * converts anything: the bytes go into a data URI and the header supplies the
9
+ * hotspot.
10
+ *
11
+ * Three of the nineteen states a theme carries are read. A drag, a resize, or a
12
+ * wait shows the default arrow where a real session would show something else,
13
+ * and the two animated states have no still frame to draw at all.
14
+ */
15
+
16
+ const CURSOR_MIME = 'image/x-icon'
17
+ const EXTENSION = /\.cur$/i
18
+
19
+ /** Ordered per state, most specific first, so an exact name beats a longer one. */
20
+ const NAMES: Record<string, readonly string[]> = {
21
+ default: ['arrow', 'normal', 'default'],
22
+ pointer: ['link', 'hand', 'pointer'],
23
+ text: ['ibeam', 'beam', 'text'],
24
+ }
25
+
26
+ export type CursorFiles = Partial<Record<string, string>>
27
+
28
+ /**
29
+ * Picks one file per state out of a directory listing. Matching runs on the
30
+ * name rather than on the contents because a theme states its intent there, and
31
+ * a shorter match wins so `Arrow.cur` beats `Arrow Alternate.cur`.
32
+ */
33
+ export function matchCursorFiles(files: readonly string[]): CursorFiles {
34
+ const cursors = files.filter((file) => EXTENSION.test(file))
35
+ const matched: CursorFiles = {}
36
+
37
+ for (const [state, names] of Object.entries(NAMES)) {
38
+ const found = names
39
+ .flatMap((name) =>
40
+ cursors.filter((file) => file.toLowerCase().includes(name)),
41
+ )
42
+ .sort((left, right) => left.length - right.length)[0]
43
+ if (found) matched[state] = found
44
+ }
45
+
46
+ return matched
47
+ }
48
+
49
+ export type ThemeLoad =
50
+ | { status: 'loaded'; cursors: CursorSet; states: string[] }
51
+ | { status: 'failed'; reason: string }
52
+
53
+ /**
54
+ * Falls back to the bundled artwork per state rather than per theme, so a
55
+ * folder carrying an arrow and no hand still contributes its arrow.
56
+ */
57
+ export function loadCursorTheme(dir: string, fallback: CursorSet): ThemeLoad {
58
+ let listing: string[]
59
+ try {
60
+ listing = readdirNames(dir)
61
+ } catch (error) {
62
+ return {
63
+ status: 'failed',
64
+ reason: `${dir} could not be read: ${message(error)}`,
65
+ }
66
+ }
67
+
68
+ const matched = matchCursorFiles(listing)
69
+ const states = Object.keys(matched)
70
+ if (!states.length) {
71
+ return {
72
+ status: 'failed',
73
+ reason: `${dir} holds no .cur file named after a pointer, link, or text state`,
74
+ }
75
+ }
76
+
77
+ const cursors: Record<string, Cursor> = { ...fallback }
78
+ for (const [state, file] of Object.entries(matched)) {
79
+ if (!file) continue
80
+ const loaded = readCursor(join(dir, file))
81
+ if (loaded) cursors[state] = loaded
82
+ }
83
+
84
+ return { status: 'loaded', cursors, states }
85
+ }
86
+
87
+ function readCursor(path: string): Cursor | undefined {
88
+ let bytes: Uint8Array
89
+ try {
90
+ bytes = readFileSync(path)
91
+ } catch {
92
+ return undefined
93
+ }
94
+
95
+ const hotspot = cursorHotspot(bytes)
96
+ if (!hotspot) return undefined
97
+
98
+ return {
99
+ image: `data:${CURSOR_MIME};base64,${Buffer.from(bytes).toString('base64')}`,
100
+ hotspot,
101
+ }
102
+ }
103
+
104
+ function readdirNames(dir: string): string[] {
105
+ return readdirSync(dir, { withFileTypes: true })
106
+ .filter((entry) => entry.isFile())
107
+ .map((entry) => entry.name)
108
+ }
109
+
110
+ function message(error: unknown): string {
111
+ return error instanceof Error ? error.message : String(error)
112
+ }