@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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/claude/.claude-plugin/plugin.json +19 -0
- package/claude/skills/bash-cli-script/REQUIREMENT.md +42 -0
- package/claude/skills/bash-cli-script/SKILL.md +48 -0
- package/claude/skills/bash-cli-script/references/template.md +43 -0
- package/claude/skills/bash-script/REQUIREMENT.md +36 -0
- package/claude/skills/bash-script/SKILL.md +100 -0
- package/claude/skills/bash-script/references/patterns.md +349 -0
- package/claude/skills/canon-cli/REQUIREMENT.md +41 -0
- package/claude/skills/canon-cli/SKILL.md +103 -0
- package/claude/skills/canon-feedback-file/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-file/SKILL.md +80 -0
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +40 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +63 -0
- package/claude/skills/canon-operator/REQUIREMENT.md +61 -0
- package/claude/skills/canon-operator/SKILL.md +108 -0
- package/claude/skills/canon-rollout/REQUIREMENT.md +59 -0
- package/claude/skills/canon-rollout/SKILL.md +147 -0
- package/claude/skills/canon-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/canon-screencast/SKILL.md +167 -0
- package/claude/skills/canon-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/canon-slides-draft/SKILL.md +62 -0
- package/claude/skills/ci-workflow/REQUIREMENT.md +40 -0
- package/claude/skills/ci-workflow/SKILL.md +65 -0
- package/claude/skills/ci-workflow/references/workflows.md +98 -0
- package/claude/skills/claude-address-review/REQUIREMENT.md +57 -0
- package/claude/skills/claude-address-review/SKILL.md +212 -0
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +39 -0
- package/claude/skills/claude-autoship/REQUIREMENT.md +50 -0
- package/claude/skills/claude-autoship/SKILL.md +207 -0
- package/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-design-extract/SKILL.md +102 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-diagram/SKILL.md +177 -0
- package/claude/skills/claude-docs/REQUIREMENT.md +60 -0
- package/claude/skills/claude-docs/SKILL.md +287 -0
- package/claude/skills/claude-docs/references/anchor-sweep.md +58 -0
- package/claude/skills/claude-docs/references/wireframe-sweep.md +45 -0
- package/claude/skills/claude-feature/REQUIREMENT.md +36 -0
- package/claude/skills/claude-feature/SKILL.md +115 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +48 -0
- package/claude/skills/claude-groundwork/SKILL.md +142 -0
- package/claude/skills/claude-intake/REQUIREMENT.md +49 -0
- package/claude/skills/claude-intake/SKILL.md +114 -0
- package/claude/skills/claude-intake-answer/REQUIREMENT.md +48 -0
- package/claude/skills/claude-intake-answer/SKILL.md +90 -0
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +48 -0
- package/claude/skills/claude-markdown-propose/SKILL.md +118 -0
- package/claude/skills/claude-markdown-propose/references/format.md +107 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-capture/SKILL.md +101 -0
- package/claude/skills/claude-memory-review/REQUIREMENT.md +50 -0
- package/claude/skills/claude-memory-review/SKILL.md +210 -0
- package/claude/skills/claude-memory-review/references/receipt-format.md +48 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +121 -0
- package/claude/skills/claude-orchestrate/SKILL.md +241 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +132 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-handoff.md +32 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +72 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +87 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +30 -0
- package/claude/skills/claude-orchestrate/references/orchestrator-sweep.md +21 -0
- package/claude/skills/claude-orchestrate/scripts/poll.sh +373 -0
- package/claude/skills/claude-orchestrate/scripts/watch.sh +181 -0
- package/claude/skills/claude-pr-review/REQUIREMENT.md +47 -0
- package/claude/skills/claude-pr-review/SKILL.md +295 -0
- package/claude/skills/claude-review/REQUIREMENT.md +39 -0
- package/claude/skills/claude-review/SKILL.md +142 -0
- package/claude/skills/claude-seed-sync/REQUIREMENT.md +45 -0
- package/claude/skills/claude-seed-sync/SKILL.md +156 -0
- package/claude/skills/claude-standards-audit/REQUIREMENT.md +33 -0
- package/claude/skills/claude-standards-audit/SKILL.md +99 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-tasks/SKILL.md +161 -0
- package/claude/skills/claude-teach/REQUIREMENT.md +56 -0
- package/claude/skills/claude-teach/SKILL.md +196 -0
- package/claude/skills/claude-teach/references/lesson-craft.md +59 -0
- package/claude/skills/claude-teach/references/pedagogy.md +67 -0
- package/claude/skills/claude-teach/references/promotion.md +54 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ui-test/SKILL.md +77 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/SKILL.md +79 -0
- package/claude/skills/claude-ux-measure/REQUIREMENT.md +48 -0
- package/claude/skills/claude-ux-measure/SKILL.md +122 -0
- package/claude/skills/claude-worker/REQUIREMENT.md +54 -0
- package/claude/skills/claude-worker/SKILL.md +96 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +58 -0
- package/claude/skills/claude-worktree/SKILL.md +136 -0
- package/claude/skills/create-rule/REQUIREMENT.md +45 -0
- package/claude/skills/create-rule/SKILL.md +68 -0
- package/claude/skills/create-skill/REQUIREMENT.md +38 -0
- package/claude/skills/create-skill/SKILL.md +32 -0
- package/claude/skills/create-snippet/REQUIREMENT.md +39 -0
- package/claude/skills/create-snippet/SKILL.md +30 -0
- package/claude/skills/create-standard/REQUIREMENT.md +35 -0
- package/claude/skills/create-standard/SKILL.md +29 -0
- package/claude/skills/decision-escalate/REQUIREMENT.md +45 -0
- package/claude/skills/decision-escalate/SKILL.md +79 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +41 -0
- package/claude/skills/docs-sync/SKILL.md +95 -0
- package/claude/skills/git-branch/REQUIREMENT.md +38 -0
- package/claude/skills/git-branch/SKILL.md +60 -0
- package/claude/skills/git-commit/REQUIREMENT.md +36 -0
- package/claude/skills/git-commit/SKILL.md +49 -0
- package/claude/skills/git-followup/REQUIREMENT.md +43 -0
- package/claude/skills/git-followup/SKILL.md +48 -0
- package/claude/skills/git-issue/REQUIREMENT.md +38 -0
- package/claude/skills/git-issue/SKILL.md +65 -0
- package/claude/skills/git-pr/REQUIREMENT.md +50 -0
- package/claude/skills/git-pr/SKILL.md +164 -0
- package/claude/skills/git-pr/references/labels.md +95 -0
- package/claude/skills/git-ship/REQUIREMENT.md +42 -0
- package/claude/skills/git-ship/SKILL.md +55 -0
- package/claude/skills/git-split/REQUIREMENT.md +39 -0
- package/claude/skills/git-split/SKILL.md +162 -0
- package/claude/skills/git-stage/REQUIREMENT.md +39 -0
- package/claude/skills/git-stage/SKILL.md +73 -0
- package/claude/skills/git-worktree/REQUIREMENT.md +38 -0
- package/claude/skills/git-worktree/SKILL.md +130 -0
- package/claude/skills/migration-claude-md/REQUIREMENT.md +40 -0
- package/claude/skills/migration-claude-md/SKILL.md +76 -0
- package/claude/skills/migration-context/REQUIREMENT.md +36 -0
- package/claude/skills/migration-context/SKILL.md +95 -0
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +55 -0
- package/claude/skills/migration-standards-drop/SKILL.md +113 -0
- package/claude/skills/migration-superseded/REQUIREMENT.md +44 -0
- package/claude/skills/migration-superseded/SKILL.md +115 -0
- package/claude/skills/project-commands/REQUIREMENT.md +42 -0
- package/claude/skills/project-commands/SKILL.md +85 -0
- package/claude/skills/restate-plainly/REQUIREMENT.md +41 -0
- package/claude/skills/restate-plainly/SKILL.md +39 -0
- package/claude/skills/session-map/REQUIREMENT.md +57 -0
- package/claude/skills/session-map/SKILL.md +70 -0
- package/claude/skills/session-resume/REQUIREMENT.md +49 -0
- package/claude/skills/session-resume/SKILL.md +51 -0
- package/claude/skills/setup-gov/REQUIREMENT.md +37 -0
- package/claude/skills/setup-gov/SKILL.md +77 -0
- package/claude/skills/setup-indexes/REQUIREMENT.md +45 -0
- package/claude/skills/setup-indexes/SKILL.md +153 -0
- package/claude/skills/setup-init/REQUIREMENT.md +45 -0
- package/claude/skills/setup-init/SKILL.md +127 -0
- package/claude/skills/setup-plugins/REQUIREMENT.md +42 -0
- package/claude/skills/setup-plugins/SKILL.md +81 -0
- package/claude/skills/setup-plugins/references/plugin-catalog.md +53 -0
- package/claude/skills/setup-verify/REQUIREMENT.md +39 -0
- package/claude/skills/setup-verify/SKILL.md +51 -0
- package/claude/skills/systematic-debugging/REQUIREMENT.md +41 -0
- package/claude/skills/systematic-debugging/SKILL.md +70 -0
- package/claude/skills/write-human/REQUIREMENT.md +46 -0
- package/claude/skills/write-human/SKILL.md +68 -0
- package/claude/skills/write-human/references/density.md +38 -0
- package/claude/skills/write-human/references/machine-tells.md +107 -0
- package/claude/skills/write-human/references/source-material.md +37 -0
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +38 -0
- package/claude/skills/youtube-transcripts/SKILL.md +34 -0
- package/docs/agents/audits.md +98 -0
- package/docs/agents/capture.md +37 -0
- package/docs/agents/census.md +23 -0
- package/docs/agents/commands.md +146 -0
- package/docs/agents/comments.md +34 -0
- package/docs/agents/context-audit-checks.md +120 -0
- package/docs/agents/context-audit.md +83 -0
- package/docs/agents/counts.md +76 -0
- package/docs/agents/demo.md +86 -0
- package/docs/agents/docs.md +17 -0
- package/docs/agents/gate.md +84 -0
- package/docs/agents/index.md +47 -0
- package/docs/agents/indexes.md +35 -0
- package/docs/agents/install-and-sync.md +385 -0
- package/docs/agents/intake.md +81 -0
- package/docs/agents/key-changes.md +103 -0
- package/docs/agents/label-coverage.md +73 -0
- package/docs/agents/markdown-audit.md +197 -0
- package/docs/agents/output-shape.md +70 -0
- package/docs/agents/overview.md +26 -0
- package/docs/agents/records.md +170 -0
- package/docs/agents/restated.md +81 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/routing.md +61 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/agents/sandbox.md +71 -0
- package/docs/agents/scripting.md +149 -0
- package/docs/agents/sessions.md +120 -0
- package/docs/agents/skills-audit.md +94 -0
- package/docs/agents/skills-reach.md +64 -0
- package/docs/agents/standards-audit.md +38 -0
- package/docs/agents/state-scoped-risk.md +105 -0
- package/docs/agents/superseded.md +85 -0
- package/docs/agents/targets.md +83 -0
- package/docs/agents/tasks.md +200 -0
- package/docs/agents/teach.md +158 -0
- package/docs/agents/test-order.md +56 -0
- package/docs/agents/worktrees.md +62 -0
- package/docs/ai-workflow.md +317 -0
- package/docs/index.md +23 -0
- package/docs/operating-model.md +223 -0
- package/docs/target-projects.md +258 -0
- package/docs/visual-design-workflow.md +151 -0
- package/docs/zshrc-aliases.md +65 -0
- package/governance/rules/ci/700-ci-workflow.md +44 -0
- package/governance/rules/claude/500-prose.md +15 -0
- package/governance/rules/claude/501-markdown.md +14 -0
- package/governance/rules/claude/510-context.md +28 -0
- package/governance/rules/claude/511-indexes.md +14 -0
- package/governance/rules/claude/520-wireframes.md +12 -0
- package/governance/rules/claude/530-requirements.md +11 -0
- package/governance/rules/claude/540-architecture.md +11 -0
- package/governance/rules/claude/550-design.md +11 -0
- package/governance/rules/claude/555-tasks.md +12 -0
- package/governance/rules/claude/556-groundwork.md +11 -0
- package/governance/rules/claude/557-intake.md +11 -0
- package/governance/rules/claude/558-plan.md +22 -0
- package/governance/rules/claude/559-memory.md +11 -0
- package/governance/rules/claude/560-diagrams.md +18 -0
- package/governance/rules/claude/561-teach.md +13 -0
- package/governance/rules/claude/562-session.md +15 -0
- package/governance/rules/claude/570-skill.md +24 -0
- package/governance/rules/claude/575-hooks.md +17 -0
- package/governance/rules/claude/576-settings.md +14 -0
- package/governance/rules/claude/580-readme.md +11 -0
- package/governance/rules/claude/590-rule-authoring.md +12 -0
- package/governance/rules/claude/591-standard-authoring.md +12 -0
- package/governance/rules/claude/592-claude-md.md +19 -0
- package/governance/rules/core/000-constitution.md +30 -0
- package/governance/rules/core/005-behavior.md +27 -0
- package/governance/rules/core/010-testing.md +35 -0
- package/governance/rules/core/015-output.md +20 -0
- package/governance/rules/core/020-concurrency.md +22 -0
- package/governance/rules/core/025-indexes.md +9 -0
- package/governance/rules/core/030-error-handling.md +31 -0
- package/governance/rules/core/035-tasks.md +13 -0
- package/governance/rules/core/040-performance.md +20 -0
- package/governance/rules/core/045-memory.md +12 -0
- package/governance/rules/core/050-logging.md +20 -0
- package/governance/rules/core/055-scratch.md +9 -0
- package/governance/rules/core/060-naming.md +19 -0
- package/governance/rules/core/065-spelling.md +19 -0
- package/governance/rules/core/070-planning.md +18 -0
- package/governance/rules/core/075-dependencies.md +25 -0
- package/governance/rules/core/080-config-comments.md +22 -0
- package/governance/rules/core/085-worktrees.md +17 -0
- package/governance/rules/core/087-git.md +11 -0
- package/governance/rules/core/090-code-comments.md +39 -0
- package/governance/rules/framework/200-react.md +51 -0
- package/governance/rules/framework/210-astro.md +41 -0
- package/governance/rules/framework/220-fastapi.md +43 -0
- package/governance/rules/framework/230-nextjs.md +48 -0
- package/governance/rules/framework/250-tailwind.md +32 -0
- package/governance/rules/framework/260-shadcn.md +34 -0
- package/governance/rules/lang/100-typescript.md +40 -0
- package/governance/rules/lang/110-python.md +42 -0
- package/governance/rules/lang/120-bash.md +19 -0
- package/governance/rules/lib/300-testing-ts.md +39 -0
- package/governance/rules/lib/305-e2e-reliability.md +34 -0
- package/governance/rules/lib/306-test-scope.md +25 -0
- package/governance/rules/lib/310-zod.md +25 -0
- package/governance/rules/lib/320-tanstack-query.md +32 -0
- package/governance/rules/lib/330-testing-py.md +44 -0
- package/governance/rules/lib/340-pydantic.md +38 -0
- package/governance/rules/lib/350-security-web.md +32 -0
- package/governance/rules/lib/360-security-server.md +39 -0
- package/governance/rules/lib/370-database.md +35 -0
- package/governance/rules/snippets/505-at-references.md +9 -0
- package/governance/rules/ui/400-ui.md +36 -0
- package/governance/rules/ui/410-a11y.md +48 -0
- package/governance/rules/ui/420-forms.md +36 -0
- package/governance/rules/ui/430-ux-completeness.md +65 -0
- package/governance/rules/ui/440-surface-capture.md +34 -0
- package/governance/rules/ui/450-link-behavior.md +19 -0
- package/governance/stacks/astro.toml +2 -0
- package/governance/stacks/base.toml +8 -0
- package/governance/stacks/node-server.toml +2 -0
- package/governance/stacks/node.toml +2 -0
- package/governance/stacks/python-fastapi.toml +2 -0
- package/governance/stacks/python.toml +2 -0
- package/governance/stacks/react.toml +2 -0
- package/package.json +69 -0
- package/scripts/config.sh +11 -0
- package/scripts/core/bootstrap.sh +81 -0
- package/scripts/core/check-color-source.sh +41 -0
- package/scripts/core/check-ignore-parity.sh +162 -0
- package/scripts/core/check-plugin-boundary.sh +45 -0
- package/scripts/core/check-seed-independence.sh +59 -0
- package/scripts/core/check-skill-paths.sh +24 -0
- package/scripts/core/clean.sh +36 -0
- package/scripts/core/install-check.sh +101 -0
- package/scripts/core/list-seed-roots.sh +18 -0
- package/scripts/core/regen-claude-copies.sh +10 -0
- package/scripts/core/regen-hero.sh +217 -0
- package/scripts/core/regen-indexes.sh +10 -0
- package/scripts/core/regen-tooling-paths.sh +61 -0
- package/scripts/core/repair-bare-flag.sh +19 -0
- package/scripts/core/snapshot.sh +134 -0
- package/scripts/core/update.sh +35 -0
- package/scripts/docs/list.sh +165 -0
- package/scripts/lib/frontmatter.sh +30 -0
- package/scripts/lib/gov.sh +14 -0
- package/scripts/lib/sandbox-fixtures.sh +191 -0
- package/scripts/lib/sandbox-git.sh +125 -0
- package/scripts/lib/sandbox-path.sh +206 -0
- package/scripts/lib/tooling.sh +35 -0
- package/scripts/lib/ui.sh +266 -0
- package/scripts/lib/worktree.sh +20 -0
- package/scripts/manage-sandbox.sh +466 -0
- package/scripts/snippets/create.sh +156 -0
- package/scripts/standards/list.sh +115 -0
- package/scripts/tooling/create.sh +109 -0
- package/scripts/tooling/verify.sh +179 -0
- package/snippets/align.md +12 -0
- package/snippets/claude/decision-memo.md +39 -0
- package/snippets/claude/feature-recap.md +19 -0
- package/snippets/claude/figma-steps.md +24 -0
- package/snippets/compact-summary.md +5 -0
- package/snippets/decision-help.md +6 -0
- package/snippets/meta-prompt.md +14 -0
- package/snippets/research-prompt.md +7 -0
- package/snippets/session-notes.md +11 -0
- package/snippets/snippets.toml +5 -0
- package/snippets/step-by-step.md +10 -0
- package/snippets/web-research.md +21 -0
- package/src/audits/baseline.ts +201 -0
- package/src/audits/catalog.ts +876 -0
- package/src/audits/run.ts +204 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/binary.ts +16 -0
- package/src/browser/engine.ts +40 -0
- package/src/census/count.ts +113 -0
- package/src/claude/cases/all.ts +24 -0
- package/src/claude/cases/authoring.ts +53 -0
- package/src/claude/cases/claude-workflow.ts +158 -0
- package/src/claude/cases/git.ts +44 -0
- package/src/claude/cases/misc.ts +27 -0
- package/src/claude/cases/setup.ts +94 -0
- package/src/claude/gitignore.ts +51 -0
- package/src/claude/routing.ts +283 -0
- package/src/claude/seeds-list.ts +47 -0
- package/src/claude/seeds.ts +150 -0
- package/src/claude/settings.ts +151 -0
- package/src/claude/skills-audit.ts +228 -0
- package/src/claude/skills-drift.ts +156 -0
- package/src/claude/skills-list.ts +99 -0
- package/src/claude/skills-rank.ts +320 -0
- package/src/claude/skills-reach.ts +227 -0
- package/src/cli-run.ts +43 -0
- package/src/cli.ts +200 -0
- package/src/commands/audits.ts +350 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/capture.ts +133 -0
- package/src/commands/census.ts +105 -0
- package/src/commands/claude.ts +1286 -0
- package/src/commands/comments.ts +240 -0
- package/src/commands/context.ts +857 -0
- package/src/commands/demo.ts +389 -0
- package/src/commands/deps.ts +173 -0
- package/src/commands/design.ts +36 -0
- package/src/commands/docs.ts +60 -0
- package/src/commands/feedback-format.ts +23 -0
- package/src/commands/feedback.ts +112 -0
- package/src/commands/gate.ts +189 -0
- package/src/commands/gov.ts +1265 -0
- package/src/commands/indexes.ts +184 -0
- package/src/commands/init.ts +113 -0
- package/src/commands/intake.ts +406 -0
- package/src/commands/inventory.ts +256 -0
- package/src/commands/labels.ts +361 -0
- package/src/commands/markdown.ts +544 -0
- package/src/commands/migrate.ts +175 -0
- package/src/commands/pass-through.ts +39 -0
- package/src/commands/pr.ts +411 -0
- package/src/commands/records.ts +728 -0
- package/src/commands/sandbox.ts +468 -0
- package/src/commands/secrets.ts +132 -0
- package/src/commands/serve.ts +159 -0
- package/src/commands/sessions.ts +408 -0
- package/src/commands/slides.ts +126 -0
- package/src/commands/snippets.ts +84 -0
- package/src/commands/standards.ts +247 -0
- package/src/commands/sync.ts +428 -0
- package/src/commands/targets.ts +319 -0
- package/src/commands/tasks.ts +743 -0
- package/src/commands/teach.ts +786 -0
- package/src/commands/tooling.ts +573 -0
- package/src/commands/transcripts.ts +44 -0
- package/src/commands/upgrade.ts +231 -0
- package/src/commands/wiki.ts +100 -0
- package/src/commands/worktrees.ts +191 -0
- package/src/comments/scan.ts +338 -0
- package/src/comments/trend.ts +207 -0
- package/src/comments/vocabulary.ts +85 -0
- package/src/context/architecture.ts +364 -0
- package/src/context/audit.ts +790 -0
- package/src/context/citations.ts +196 -0
- package/src/context/folders.ts +186 -0
- package/src/context/gate.ts +57 -0
- package/src/context/index-drift.ts +64 -0
- package/src/context/narration.ts +99 -0
- package/src/copy.ts +30 -0
- package/src/counts/catalogs.ts +96 -0
- package/src/counts/numbers.ts +79 -0
- package/src/counts/scan.ts +314 -0
- package/src/demo/beats.ts +135 -0
- package/src/demo/compile.ts +326 -0
- package/src/demo/container.ts +63 -0
- package/src/demo/cursors.ts +55 -0
- package/src/demo/drive.ts +357 -0
- package/src/demo/pointer.ts +178 -0
- package/src/demo/theme.ts +112 -0
- package/src/deps/audit.ts +153 -0
- package/src/design/parse.ts +116 -0
- package/src/design/render.ts +249 -0
- package/src/docs/read.ts +77 -0
- package/src/exec.ts +16 -0
- package/src/exempt-marker.ts +43 -0
- package/src/frontmatter.ts +13 -0
- package/src/gate/measures.ts +682 -0
- package/src/gate/sequencer.ts +386 -0
- package/src/gate/stages.ts +412 -0
- package/src/git-env.ts +36 -0
- package/src/git-files.ts +105 -0
- package/src/git-ignore.ts +46 -0
- package/src/github-format.ts +13 -0
- package/src/github.ts +24 -0
- package/src/gov/adapter.ts +103 -0
- package/src/gov/citations.ts +514 -0
- package/src/gov/consumed.ts +129 -0
- package/src/gov/install.ts +132 -0
- package/src/gov/list.ts +106 -0
- package/src/gov/payload.ts +39 -0
- package/src/gov/restated.ts +814 -0
- package/src/gov/stacks.ts +205 -0
- package/src/gov/superseded.ts +415 -0
- package/src/gov/test-order.ts +407 -0
- package/src/indexes/frontmatter.ts +46 -0
- package/src/indexes/regen.ts +84 -0
- package/src/indexes/render.ts +201 -0
- package/src/indexes/walk.ts +83 -0
- package/src/init/flags.ts +60 -0
- package/src/init/plan.ts +114 -0
- package/src/init/run.ts +46 -0
- package/src/init/steps.ts +77 -0
- package/src/intake/folder.ts +320 -0
- package/src/intake/items.ts +174 -0
- package/src/inventory/config.ts +117 -0
- package/src/inventory/group.ts +76 -0
- package/src/inventory/subjects.ts +114 -0
- package/src/inventory/walk.ts +129 -0
- package/src/labels/audit.ts +82 -0
- package/src/labels/coverage.ts +79 -0
- package/src/labels/map.ts +101 -0
- package/src/labels/phase.ts +95 -0
- package/src/markdown/bans.ts +94 -0
- package/src/markdown/files.ts +102 -0
- package/src/markdown/gate.ts +28 -0
- package/src/markdown/scan.ts +295 -0
- package/src/markdown/structure.ts +730 -0
- package/src/migrate/apply.ts +115 -0
- package/src/migrate/plan.ts +103 -0
- package/src/migrate/rename.ts +183 -0
- package/src/pr/bijection.ts +145 -0
- package/src/pr/paths.ts +335 -0
- package/src/process/harness.ts +167 -0
- package/src/project-root.ts +19 -0
- package/src/records/backup.ts +455 -0
- package/src/records/migrate.ts +78 -0
- package/src/records/size.ts +260 -0
- package/src/records/validate.ts +1162 -0
- package/src/sandbox/census.ts +228 -0
- package/src/sandbox/coverage.ts +115 -0
- package/src/sandbox/expect.ts +629 -0
- package/src/sandbox/tree.ts +47 -0
- package/src/secrets/marker.ts +30 -0
- package/src/secrets/patterns.ts +142 -0
- package/src/secrets/scan.ts +123 -0
- package/src/secrets/shipped.ts +111 -0
- package/src/seed-marker.ts +74 -0
- package/src/serve/static.ts +322 -0
- package/src/sessions/claim.ts +85 -0
- package/src/sessions/live.ts +79 -0
- package/src/sessions/registry.ts +137 -0
- package/src/sessions/resolve.ts +333 -0
- package/src/slides/layouts.ts +391 -0
- package/src/slides/open.ts +18 -0
- package/src/slides/parse.ts +84 -0
- package/src/slides/render.ts +88 -0
- package/src/slides/styles.ts +44 -0
- package/src/snippets/categories.ts +66 -0
- package/src/snippets/list.ts +32 -0
- package/src/snippets/presets.ts +50 -0
- package/src/standards/audit.ts +132 -0
- package/src/standards/read.ts +100 -0
- package/src/sync/check.ts +692 -0
- package/src/sync/engine.ts +576 -0
- package/src/sync/git.ts +225 -0
- package/src/sync/history.ts +123 -0
- package/src/sync/layout.ts +140 -0
- package/src/sync/reverse.ts +268 -0
- package/src/sync/seeds-report.ts +126 -0
- package/src/sync/stamp.ts +358 -0
- package/src/sync/target.ts +70 -0
- package/src/sync/workflow.ts +200 -0
- package/src/target.ts +43 -0
- package/src/targets/pulls.ts +250 -0
- package/src/targets/registry.ts +203 -0
- package/src/targets/resolve.ts +145 -0
- package/src/targets/sweep.ts +246 -0
- package/src/tasks/archive.ts +506 -0
- package/src/tasks/record.ts +311 -0
- package/src/tasks/trunk.ts +89 -0
- package/src/tasks/validate.ts +1001 -0
- package/src/teach/lesson.ts +180 -0
- package/src/teach/workspace.ts +842 -0
- package/src/tooling/gitignore.ts +122 -0
- package/src/tooling/inject.ts +193 -0
- package/src/tooling/list.ts +39 -0
- package/src/tooling/manifest.ts +176 -0
- package/src/tooling/package.ts +166 -0
- package/src/tooling/read.ts +65 -0
- package/src/tooling/scan.ts +147 -0
- package/src/tooling/stamp.ts +44 -0
- package/src/transcripts/fetch.ts +156 -0
- package/src/transcripts/metadata.ts +54 -0
- package/src/transcripts/vtt.ts +114 -0
- package/src/ui.ts +266 -0
- package/src/version/compare.ts +53 -0
- package/src/version/installed.ts +40 -0
- package/src/version/manager.ts +67 -0
- package/src/version/skew.ts +192 -0
- package/src/wiki/init.ts +85 -0
- package/src/worktree.ts +143 -0
- package/src/worktrees/reclaim.ts +306 -0
- package/standards/architecture.md +72 -0
- package/standards/branch.md +59 -0
- package/standards/commit.md +72 -0
- package/standards/context.md +151 -0
- package/standards/design.md +93 -0
- package/standards/diagrams.md +152 -0
- package/standards/glossary.md +75 -0
- package/standards/groundwork.md +211 -0
- package/standards/index.md +36 -0
- package/standards/intake.md +192 -0
- package/standards/issue.md +94 -0
- package/standards/markdown.md +137 -0
- package/standards/memory.md +144 -0
- package/standards/plan.md +172 -0
- package/standards/pr.md +139 -0
- package/standards/publish.md +51 -0
- package/standards/readme.md +208 -0
- package/standards/requirements.md +70 -0
- package/standards/rule.md +118 -0
- package/standards/session.md +109 -0
- package/standards/skill.md +300 -0
- package/standards/slug.md +39 -0
- package/standards/snippets.md +76 -0
- package/standards/standard.md +170 -0
- package/standards/tasks.md +254 -0
- package/standards/teach.md +153 -0
- package/standards/versioning.md +71 -0
- package/standards/wireframes.md +113 -0
- package/tooling/astro/configs/astro.config.mjs +31 -0
- package/tooling/astro/configs/eslint.config.js +79 -0
- package/tooling/astro/configs/playwright.config.ts +26 -0
- package/tooling/astro/configs/tsconfig.json +12 -0
- package/tooling/astro/configs/vitest.config.ts +22 -0
- package/tooling/astro/manifest.toml +34 -0
- package/tooling/astro/reference.md +60 -0
- package/tooling/base/configs/.editorconfig +5 -0
- package/tooling/base/configs/.github/pull_request_template.md +18 -0
- package/tooling/base/configs/.github/workflows/verify.yml +35 -0
- package/tooling/base/configs/.husky/commit-msg +1 -0
- package/tooling/base/configs/.husky/post-merge +61 -0
- package/tooling/base/configs/.husky/post-rewrite +21 -0
- package/tooling/base/configs/.husky/pre-commit +1 -0
- package/tooling/base/configs/.husky/pre-push +1 -0
- package/tooling/base/configs/.prettierrc +12 -0
- package/tooling/base/configs/.shellcheckrc +1 -0
- package/tooling/base/configs/.vscode/extensions.json +9 -0
- package/tooling/base/configs/.vscode/settings.json +3 -0
- package/tooling/base/configs/commitlint.config.js +11 -0
- package/tooling/base/configs/scripts/verify.sh +64 -0
- package/tooling/base/manifest.toml +30 -0
- package/tooling/base/reference.md +98 -0
- package/tooling/base/seeds/.claude/context/ci.md +33 -0
- package/tooling/base/seeds/.claude/context/development.md +37 -0
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/base/seeds/.cspell/project-terms.txt +0 -0
- package/tooling/base/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/base/seeds/.lintstagedrc +8 -0
- package/tooling/base/seeds/.prettierignore +0 -0
- package/tooling/base/seeds/cspell.json +20 -0
- package/tooling/claude/manifest.toml +14 -0
- package/tooling/claude/reference.md +79 -0
- package/tooling/claude/seeds/.claude/ARCHITECTURE.md +13 -0
- package/tooling/claude/seeds/.claude/DESIGN.md +62 -0
- package/tooling/claude/seeds/.claude/REQUIREMENTS.md +18 -0
- package/tooling/claude/seeds/.claude/diagrams/index.md +8 -0
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +50 -0
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +68 -0
- package/tooling/claude/seeds/.claude/hooks/path-form.sh +57 -0
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +52 -0
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +86 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +71 -0
- package/tooling/claude/seeds/.claude/memory/index.md +8 -0
- package/tooling/claude/seeds/.claude/settings.json +47 -0
- package/tooling/claude/seeds/.claude/tasks/index.md +8 -0
- package/tooling/claude/seeds/.claude/wireframes/index.md +8 -0
- package/tooling/claude/seeds/CLAUDE.md +29 -0
- package/tooling/claude/user/settings.template.json +10 -0
- package/tooling/claude/user/statusline-command.sh +53 -0
- package/tooling/python/configs/.coveragerc +14 -0
- package/tooling/python/configs/.python-version +1 -0
- package/tooling/python/configs/mypy.ini +6 -0
- package/tooling/python/configs/pytest.ini +4 -0
- package/tooling/python/configs/ruff.toml +15 -0
- package/tooling/python/configs/scripts/verify.sh +77 -0
- package/tooling/python/manifest.toml +16 -0
- package/tooling/python/reference.md +66 -0
- package/tooling/python/seeds/.cspell/tech-stack.txt +19 -0
- package/tooling/python/seeds/tests/test_smoke.py +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +26 -0
- package/tooling/vite-react/configs/tsconfig.json +35 -0
- package/tooling/vite-react/configs/vite.config.ts +24 -0
- package/tooling/vite-react/configs/vitest.config.ts +26 -0
- package/tooling/vite-react/manifest.toml +23 -0
- package/tooling/vite-react/reference.md +55 -0
- package/tooling/vite-react/seeds/.cspell/project-terms.txt +1 -0
- package/tooling/vite-react/seeds/.cspell/tech-stack.txt +1 -0
- package/tooling/web/configs/.github/workflows/verify.yml +134 -0
- package/tooling/web/configs/.vscode/extensions.json +13 -0
- package/tooling/web/configs/.vscode/settings.json +10 -0
- package/tooling/web/configs/e2e/home.spec.ts +6 -0
- package/tooling/web/configs/e2e/screenshot.ts +53 -0
- package/tooling/web/configs/eslint.config.js +82 -0
- package/tooling/web/configs/scripts/screenshot.sh +28 -0
- package/tooling/web/configs/scripts/verify.sh +80 -0
- package/tooling/web/configs/scripts/worktree-port.sh +74 -0
- package/tooling/web/configs/src/test/setup.ts +8 -0
- package/tooling/web/manifest.toml +58 -0
- package/tooling/web/reference.md +115 -0
- package/tooling/web/seeds/.cspell/tech-stack.txt +18 -0
- 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
|
+
}
|