@erclx/canon 5.39.0 → 5.41.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 (162) hide show
  1. package/README.md +39 -22
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/create-skill/references/skill.md +1 -1
  8. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  9. package/claude/skills/decision-escalate/SKILL.md +1 -2
  10. package/claude/skills/draft-slides/SKILL.md +2 -3
  11. package/claude/skills/draft-slides/references/deck.md +183 -0
  12. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  13. package/claude/skills/git-branch/SKILL.md +63 -7
  14. package/claude/skills/git-pr/references/evidence.md +2 -2
  15. package/claude/skills/git-pr/references/final-command.md +1 -1
  16. package/claude/skills/git-pr/references/labels.md +1 -1
  17. package/claude/skills/git-pr/references/publish.md +1 -1
  18. package/claude/skills/plan-feature/references/batches.md +48 -1
  19. package/claude/skills/plan-feature/references/plan.md +2 -0
  20. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  21. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  22. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  23. package/claude/skills/review-ui/SKILL.md +3 -0
  24. package/claude/skills/review-ui/references/close-out.md +47 -0
  25. package/claude/skills/review-ui/references/post.md +3 -1
  26. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  27. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  28. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  29. package/claude/skills/role-worker/SKILL.md +9 -0
  30. package/claude/skills/task-board/references/board.md +1 -1
  31. package/claude/skills/task-board/references/tasks.md +1 -1
  32. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  33. package/claude/skills/teach-workspace/SKILL.md +1 -2
  34. package/claude/skills/ux-measure/SKILL.md +1 -1
  35. package/docs/agents/audits.md +61 -60
  36. package/docs/agents/context-audit.md +40 -58
  37. package/docs/agents/index.md +5 -51
  38. package/docs/agents/install-and-sync.md +1 -0
  39. package/docs/agents/markdown-audit.md +53 -99
  40. package/docs/agents/records.md +37 -79
  41. package/docs/agents/sessions-transfer.md +26 -70
  42. package/docs/target/index.md +0 -1
  43. package/docs/target/migrations.md +57 -66
  44. package/docs/target/projects.md +36 -78
  45. package/docs/target/scenarios.md +9 -19
  46. package/docs/target/sync.md +27 -68
  47. package/docs/workflow/ai-workflow.md +22 -68
  48. package/docs/workflow/operating-model.md +32 -186
  49. package/docs/workflow/parallel-features.md +30 -66
  50. package/docs/workflow/skill-map-on-request.md +22 -24
  51. package/docs/workflow/skill-map.md +72 -76
  52. package/docs/workflow/visual-design-workflow.md +25 -118
  53. package/docs/workflow/zshrc-aliases.md +18 -37
  54. package/package.json +1 -1
  55. package/src/canvas/client/inspector/fields.css +4 -0
  56. package/src/canvas/index.html +6 -0
  57. package/src/claude/cases/git.ts +4 -0
  58. package/src/claude/skills-reach.ts +2 -2
  59. package/src/cli/ui.ts +3 -1
  60. package/src/commands/canvas.ts +23 -0
  61. package/src/commands/capture.ts +14 -0
  62. package/src/commands/claude.ts +13 -0
  63. package/src/commands/demo.ts +22 -0
  64. package/src/commands/deps.ts +6 -0
  65. package/src/commands/driver.ts +28 -2
  66. package/src/commands/gov.ts +52 -18
  67. package/src/commands/intake.ts +17 -0
  68. package/src/commands/labels.ts +38 -5
  69. package/src/commands/pr.ts +192 -52
  70. package/src/commands/records.ts +19 -3
  71. package/src/commands/secrets.ts +6 -0
  72. package/src/commands/sessions.ts +8 -0
  73. package/src/commands/tasks.ts +52 -0
  74. package/src/commands/teach.ts +13 -0
  75. package/src/commands/upstream.ts +25 -1
  76. package/src/commands/worktrees.ts +5 -0
  77. package/src/context/audit.ts +22 -4
  78. package/src/context/folders.ts +1 -1
  79. package/src/context/shape-gated.ts +5 -2
  80. package/src/counts/scan.ts +1 -1
  81. package/src/design/components.ts +1 -1
  82. package/src/design/document.ts +1 -1
  83. package/src/design/tokens.ts +1 -1
  84. package/src/gate/boundaries.ts +4 -1
  85. package/src/gate/measures.ts +1 -1
  86. package/src/gate/stages.ts +7 -7
  87. package/src/git/env.ts +6 -0
  88. package/src/markdown/scope.ts +11 -19
  89. package/src/pr/frames.ts +53 -9
  90. package/src/pr/lift.ts +153 -19
  91. package/src/pr/painted.ts +87 -0
  92. package/src/records/validate.ts +1 -0
  93. package/src/roots/record.ts +18 -1
  94. package/src/sandbox/census.ts +1 -1
  95. package/src/sandbox/headless/run.ts +3 -2
  96. package/src/sandbox/headless/watch.ts +3 -4
  97. package/src/slides/convert/master.ts +3 -0
  98. package/src/sync/format.ts +46 -0
  99. package/src/sync/stamp.ts +6 -0
  100. package/src/tasks/answers.ts +79 -2
  101. package/src/tasks/fan-in.ts +176 -0
  102. package/src/tasks/stack.ts +1 -1
  103. package/tooling/astro/reference.md +80 -39
  104. package/tooling/base/reference.md +63 -32
  105. package/tooling/base/seeds/canon/context/ci.md +7 -15
  106. package/tooling/base/seeds/canon/context/development.md +14 -14
  107. package/tooling/claude/reference.md +40 -22
  108. package/tooling/cloudflare/reference.md +31 -12
  109. package/tooling/go/reference.md +47 -23
  110. package/tooling/nextjs/reference.md +52 -19
  111. package/tooling/php/reference.md +35 -16
  112. package/tooling/python/reference.md +47 -30
  113. package/tooling/shell/reference.md +6 -5
  114. package/tooling/vite-react/reference.md +49 -21
  115. package/tooling/web/reference.md +131 -71
  116. package/docs/agents/canvas.md +0 -86
  117. package/docs/agents/capture.md +0 -65
  118. package/docs/agents/census.md +0 -23
  119. package/docs/agents/comments.md +0 -34
  120. package/docs/agents/context-audit-checks.md +0 -108
  121. package/docs/agents/context-classify.md +0 -101
  122. package/docs/agents/counts.md +0 -74
  123. package/docs/agents/demo.md +0 -140
  124. package/docs/agents/driver.md +0 -103
  125. package/docs/agents/gate.md +0 -90
  126. package/docs/agents/hooks.md +0 -78
  127. package/docs/agents/intake.md +0 -81
  128. package/docs/agents/key-changes.md +0 -116
  129. package/docs/agents/label-coverage.md +0 -86
  130. package/docs/agents/markdown-audit-density.md +0 -39
  131. package/docs/agents/markdown-audit-weight.md +0 -83
  132. package/docs/agents/pr-evidence-layout.md +0 -70
  133. package/docs/agents/pr-evidence.md +0 -273
  134. package/docs/agents/pr-frames.md +0 -131
  135. package/docs/agents/pr-lift.md +0 -132
  136. package/docs/agents/pr-local.md +0 -111
  137. package/docs/agents/pr-preview.md +0 -155
  138. package/docs/agents/pr-reads.md +0 -111
  139. package/docs/agents/pr-tick.md +0 -72
  140. package/docs/agents/records-push.md +0 -61
  141. package/docs/agents/records-stale.md +0 -40
  142. package/docs/agents/records-validate.md +0 -57
  143. package/docs/agents/restated.md +0 -83
  144. package/docs/agents/routing.md +0 -61
  145. package/docs/agents/rule-citations.md +0 -90
  146. package/docs/agents/sandbox.md +0 -126
  147. package/docs/agents/sessions.md +0 -128
  148. package/docs/agents/skills-audit.md +0 -120
  149. package/docs/agents/skills-reach.md +0 -64
  150. package/docs/agents/slides.md +0 -176
  151. package/docs/agents/state-scoped-risk.md +0 -105
  152. package/docs/agents/superseded.md +0 -85
  153. package/docs/agents/tasks-archive.md +0 -83
  154. package/docs/agents/tasks-plans.md +0 -152
  155. package/docs/agents/tasks-validate.md +0 -133
  156. package/docs/agents/tasks.md +0 -123
  157. package/docs/agents/teach-authoring.md +0 -118
  158. package/docs/agents/teach.md +0 -157
  159. package/docs/agents/test-order.md +0 -58
  160. package/docs/agents/upstream.md +0 -94
  161. package/docs/agents/worktrees.md +0 -118
  162. package/docs/target/standards-moved.md +0 -34
@@ -6,13 +6,13 @@ The shell stack is an add-on that formats and lints the shell scripts a project
6
6
 
7
7
  ## Install
8
8
 
9
- - Install `shfmt` and `shellcheck` by hand, through `sudo apt-get install shfmt shellcheck` or `brew install shfmt shellcheck`. Injection runs only `bun add -D`, and neither tool ships through the registry.
9
+ - Install `shfmt` and `shellcheck` by hand with `sudo apt-get install shfmt shellcheck` or `brew install shfmt shellcheck`. Injection runs only `bun add -D`, and neither ships through the registry.
10
10
  - `scripts/verify.ts` probes both before the `Shell` phase and names the install command when one is missing, rather than letting `find` report a missing executable.
11
11
 
12
12
  ## shfmt
13
13
 
14
14
  - Indent with 2 spaces, set through `--indent 2` on every call.
15
- - `.editorconfig` carries an `[*.sh]` block with `indent_style = space` and `indent_size = 2`, so the editor and shfmt agree. Dropping the block because shfmt already sets indentation produces spurious diffs.
15
+ - `.editorconfig` carries an `[*.sh]` block with `indent_style = space` and `indent_size = 2`, so the editor and shfmt agree. Dropping it because shfmt sets indentation produces spurious diffs.
16
16
 
17
17
  ## shellcheck
18
18
 
@@ -27,16 +27,17 @@ The shell stack is an add-on that formats and lints the shell scripts a project
27
27
 
28
28
  ## Verify
29
29
 
30
- - `scripts/verify.ts` runs `check:shell` in its `Shell` phase wherever `package.json` declares it, between the format check and the unit tests. No stack's `scripts/verify.json` requires it, so a folder that never synced this stack skips the phase.
30
+ `scripts/verify.ts` runs `check:shell` in its `Shell` phase wherever `package.json` declares it, between the format check and the unit tests. No stack's `scripts/verify.json` requires it, so a folder that never synced this stack skips the phase.
31
31
 
32
32
  ## lint-staged
33
33
 
34
- - `.lintstagedrc` is a base seed this stack cannot reach once written. Add a `**/*.sh` entry by hand, running `shfmt --write --indent 2` then `shellcheck --severity=warning`, to run the checks on commit.
34
+ - `.lintstagedrc` is a base seed this stack cannot reach once written. Add a `**/*.sh` entry by hand to run the checks on commit.
35
+ - That entry runs `shfmt --write --indent 2` then `shellcheck --severity=warning`.
35
36
  - Do NOT copy that glob into a `package.json` script. lint-staged expands its own globs and passes matched files as arguments, which a bare shell script does not.
36
37
 
37
38
  ## CI
38
39
 
39
- - `.github/workflows/shell.yml` runs on pull requests targeting `main` and on `workflow_dispatch`: checkout, setup Bun, `bun install --frozen-lockfile`, install both tools through apt, then `check:shell`.
40
+ `.github/workflows/shell.yml` runs on pull requests targeting `main` and on `workflow_dispatch`. Its steps are checkout, setup Bun, `bun install --frozen-lockfile`, installing both tools through apt, then `check:shell`.
40
41
 
41
42
  ## CLI
42
43
 
@@ -4,44 +4,72 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The vite-react stack covers Vite + React + TypeScript projects: web apps and Chrome extensions. It ships golden configs for `vite.config.ts`, `vitest.config.ts` (merging from vite config), `playwright.config.ts`, and a unified `tsconfig.json` with `@/` path alias. Shared web tooling (oxlint, screenshot template, VS Code, CI, verify script) comes from the `web` layer.
7
+ The vite-react stack covers Vite + React + TypeScript web apps and Chrome extensions. It ships golden configs for `vite.config.ts`, `vitest.config.ts`, `playwright.config.ts`, and a unified `tsconfig.json` with an `@/` path alias. Shared web tooling (oxlint, screenshot template, VS Code, CI, verify script) comes from the `web` layer.
8
8
 
9
9
  ## Scaffold checklist
10
10
 
11
11
  1. Scaffold with `bunx create-vite@latest <name> --template react-ts` (web apps) or `bunx create-crxjs@latest` (Chrome extensions).
12
- 2. Install base and web tooling: `canon tooling sync web . --write`
13
- 3. Install vite-react deps and configs: `canon tooling sync vite-react . --write`
12
+ 2. Install base and web tooling with `canon tooling sync web . --write`.
13
+ 3. Install vite-react deps and configs with `canon tooling sync vite-react . --write`.
14
14
  4. Extend the `ci` and `development` context entries under `canon/context/` per the web reference's extend sections plus the vite-react rows below.
15
15
  5. Run `bun run lint:fix`, then `bun run format`, then `bun run check`.
16
16
 
17
17
  ## What ships as golden configs
18
18
 
19
- - `vite.config.ts`: `@vitejs/plugin-react`, `@tailwindcss/vite`, `@` path alias to `./src`, `VITE_BASE_URL` env for base path. Dev port `5173` and preview port `4173`, each plus `WORKTREE_PORT_OFFSET`, both with `strictPort`.
20
- - `vitest.config.ts`: merges from `vite.config.ts` (imported with its `.ts` extension, which Vite 8 requires), jsdom, globals, setup file, `passWithNoTests: true`, v8 coverage.
21
- - `playwright.config.ts`: all browsers, `webServer` on `bun run dev` at port `5173` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`, trace under `use`.
22
- - `tsconfig.json`: unified, `noEmit: true`, `skipLibCheck: true`, `@/` paths, `vitest/globals` and `@testing-library/jest-dom/vitest` in types.
19
+ - `vite.config.ts` adds `@vitejs/plugin-react`, `@tailwindcss/vite`, the `@` alias to `./src`, and `VITE_BASE_URL` for the base path.
20
+ - It serves dev on port `5173` and preview on `4173`, each plus `WORKTREE_PORT_OFFSET`, both with `strictPort`.
21
+ - `vitest.config.ts` merges from `vite.config.ts`, imported with its `.ts` extension, which Vite 8 requires. It sets jsdom, globals, the setup file, `passWithNoTests: true`, and v8 coverage.
22
+ - `playwright.config.ts` runs all browsers, with `webServer` on `bun run dev` at port `5173` plus `WORKTREE_PORT_OFFSET`, `reuseExistingServer: false`, and trace under `use`.
23
+ - `tsconfig.json` is unified, with `noEmit: true`, `skipLibCheck: true`, `@/` paths, and `vitest/globals` and `@testing-library/jest-dom/vitest` in types.
23
24
 
24
25
  ## Chrome extension variant
25
26
 
26
- When scaffolding a Chrome extension, override the installed golden configs:
27
+ When scaffolding a Chrome extension, override the installed golden configs.
27
28
 
28
- - `vite.config.ts`: use `crx({ manifest })` and `zip()` from `@crxjs/vite-plugin` instead of `react()` alone. Keep the derived `server.port` and `server.strictPort: true`, set `server.hmr.clientPort` to the same derived value, and add `chrome-extension://` to CORS origins. Drop `VITE_BASE_URL`.
29
- - `vitest.config.ts`: use a standalone `defineConfig` (no `mergeConfig`). crxjs plugin breaks Vitest. Declare `@vitejs/plugin-react` and `@tailwindcss/vite` directly. Add `**/release/**` to excludes and `manifest.config.ts`, `**/*.d.ts` to coverage excludes.
30
- - `playwright.config.ts`: chromium-only (Firefox and WebKit cannot run extensions). Bundled `chromium` channel. No `baseURL` or `webServer`. Tests load the built extension directly from `dist/`.
31
- - `e2e/fixtures.ts`: extend Playwright base `test` with `context` (persistent context loading the extension from `dist/`) and `extensionId` (extracted from service worker URL). Rename `use` to `apply` to avoid oxlint's `react/rules-of-hooks`. `waitForEvent('serviceworker')` blocks until the MV3 service worker registers.
32
- - `manifest.config.ts`: use `defineManifest` from `@crxjs/vite-plugin`. Read `name` and `version` from `package.json`. Entry points `src/popup/index.html`, `src/sidepanel/index.html`, `src/background/index.ts`, `src/content/main.tsx`. Permissions `sidePanel`, `contentSettings`, `storage`. Icon `public/logo.png` at size 48.
33
- - `screenshot` script: `bun run build && bun e2e/screenshot.ts` (no preview server).
34
- - Additional gitignore entries: `*.crx`, `*.pem`, `release/`.
29
+ ### Build config
30
+
31
+ The crxjs plugin replaces the plain React build.
32
+
33
+ - `vite.config.ts` uses `crx({ manifest })` and `zip()` from `@crxjs/vite-plugin` instead of `react()` alone. Drop `VITE_BASE_URL`.
34
+ - Keep the derived `server.port` and `server.strictPort: true`, set `server.hmr.clientPort` to the same derived value, and add `chrome-extension://` to CORS origins.
35
+
36
+ ### Test configs
37
+
38
+ The plugin also changes how Vitest and Playwright run.
39
+
40
+ - `vitest.config.ts` uses a standalone `defineConfig` with no `mergeConfig`, since the crxjs plugin breaks Vitest. Declare `@vitejs/plugin-react` and `@tailwindcss/vite` directly.
41
+ - It also adds `**/release/**` to excludes, and `manifest.config.ts` and `**/*.d.ts` to coverage excludes.
42
+ - `playwright.config.ts` is chromium-only, since Firefox and WebKit cannot run extensions.
43
+ - It uses the bundled `chromium` channel with no `baseURL` or `webServer`, and tests load the built extension from `dist/`.
44
+
45
+ ### Fixtures and manifest
46
+
47
+ The extension needs a fixture file and a manifest config.
48
+
49
+ - `e2e/fixtures.ts` extends Playwright's base `test` with `context`, a persistent context loading the extension from `dist/`, and `extensionId`, extracted from the service worker URL.
50
+ - Rename `use` to `apply` in that file to avoid oxlint's `react/rules-of-hooks`. `waitForEvent('serviceworker')` blocks until the MV3 service worker registers.
51
+ - `manifest.config.ts` uses `defineManifest` from `@crxjs/vite-plugin` and reads `name` and `version` from `package.json`.
52
+ - Its entry points are `src/popup/index.html`, `src/sidepanel/index.html`, `src/background/index.ts`, and `src/content/main.tsx`.
53
+ - Its permissions are `sidePanel`, `contentSettings`, and `storage`, with icon `public/logo.png` at size 48.
54
+
55
+ ### Scripts and ignores
56
+
57
+ The extension build changes one script and adds ignores.
58
+
59
+ - The `screenshot` script is `bun run build && bun e2e/screenshot.ts`, with no preview server.
60
+ - Add `*.crx`, `*.pem`, and `release/` to gitignore.
35
61
 
36
62
  ## Development docs (extend)
37
63
 
38
- Append to the `## Scripts` table:
64
+ Append these rows to the `## Scripts` table.
39
65
 
40
- | `bun run dev` | Start the Vite dev server on port 5173, plus this worktree's port offset. |
41
- | `bun run build` | Typecheck then build the production bundle. |
42
- | `bun run preview` | Serve the built bundle locally. |
43
- | `bun run typecheck` | Run `tsc --noEmit`. |
66
+ | Command | Purpose |
67
+ | ------------------- | -------------------------------------------------------------- |
68
+ | `bun run dev` | Start the Vite dev server on 5173 plus this worktree's offset. |
69
+ | `bun run build` | Typecheck then build the production bundle. |
70
+ | `bun run preview` | Serve the built bundle locally. |
71
+ | `bun run typecheck` | Run `tsc --noEmit`. |
44
72
 
45
73
  ## CI docs (extend)
46
74
 
47
- In `canon/context/ci.md`, the Typecheck row's assertion reads: `` `tsc --noEmit` passes ``. <!-- audit-ignore-citations: canon/context/ci.md -->
75
+ In `canon/context/development/ci.md`, the Typecheck row asserts `` `tsc --noEmit` passes ``. <!-- audit-ignore-citations: canon/context/development/ci.md -->
@@ -4,128 +4,188 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The web layer covers web-universal tooling shared across Vite + React, Astro, and any future web stack. It ships golden configs for oxlint, Vitest, Playwright, Tailwind v4, screenshots, VS Code integration, and CI. Framework-specific wiring (Vite plugins, Astro islands, Next.js config) lives in per-stack adapter folders that extend this layer.
7
+ The web layer covers the tooling shared across Vite + React, Astro, and any future web stack. It ships golden configs for oxlint, Vitest, Playwright, Tailwind v4, screenshots, VS Code integration, and CI. Framework wiring (Vite plugins, Astro islands, Next.js config) lives in per-stack adapter folders that extend this layer.
8
8
 
9
9
  ## What ships as golden configs
10
10
 
11
- Golden config files live in `tooling/web/configs/` and are copied into the target on `canon tooling sync web . --write`, replacing whatever sits at those paths. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
11
+ Golden configs live in `tooling/web/configs/`. `canon tooling sync web . --write` copies them into the target and replaces whatever sits at those paths. They are the source of truth, so this reference covers rationale and the configs show the setup.
12
12
 
13
- - `.oxlintrc.json`: migrated from the ESLint config it replaced. Runs the recommended JS and TypeScript rules, the React hooks and refresh rules, test-scoped vitest rules plus `no-focused-tests`, and `check-file` and `simple-import-sort` as JS plugins.
14
- - `src/test/setup.ts`: `@testing-library/jest-dom/vitest` import, `cleanup` after each test.
15
- - `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, and a route, and the loop below crosses every case with the `VIEWPORTS` array and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. `VIEWPORTS` holds four breakpoint buckets named by width, 320, 768, 1280 and 1536, with 320 as the base a page has to hold without wrapping or clipping. A case flagged `evidence: true` additionally writes `evidence/<section>/<width>--<theme>.png`, with no hostname segment, at the viewports flagged `evidence` alone, which are 320 and 1280. Committing all four multiplies the tracked frames by the width count, so the flag is what keeps the tracked set at twice the sections rather than four times. A case may name `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking. `SCREENSHOT_FILTER` narrows a run to the cases whose label, `<section>/<width>--<theme>`, contains any comma-separated term, exits 1 when nothing matches and lists the labels that exist, and leaves the rest of the sweep alone. `--require-base-url` skips a flagged case entirely, so a production smoke run neither writes to the committed path nor counts an evidence route in its console-clean check, and exits 1 when that leaves zero cases run, so flagging every case cannot silence the production check without saying so. Per-project cases extend the one record. A route's widths and themes sit together under its section folder, so the filename carries both. A case may also name `sections`, one `{ name, selector }` per addressable part of the route, and each writes its own frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png` beside the whole-page one, plus `evidence/<section>/<name>/<width>--<theme>.png` where the case is flagged and the width carries `evidence`. The field is optional, so a case that names none captures the page alone as before, and a selector matching nothing is reported and skipped rather than raised, since `CASES` is a template a project rewrites and a target that syncs this file before editing its own list would otherwise capture nothing at all. Testing that every named section resolves belongs to the project that owns the selectors, beside its own page. A case may also name `mask`, a list of selectors whose boxes are painted over in every frame the case writes, whole-page and section alike, for a value that moves between runs with no code change, so a double capture still compares everything else. A mask selector matching nothing is reported and skipped the same way. Every case settles before anything is shot: the context is created with `reducedMotion: 'reduce'`, each named section is scrolled into view so an `IntersectionObserver` fires and a frame marked `loading="lazy"` loads, the page returns to the top, and fonts, images and same-origin frames are waited out on a bounded condition rather than on a second `networkidle`, which is a lifecycle event of the navigation and does not re-arm for a load a scroll started. A section frame drops the page's floating chrome, being whatever computes to `position: sticky` or `fixed`, because a section taller than the viewport is scrolled into view to be cut and a sticky bar otherwise rides that scroll into the middle of the frame. The whole-page frame is shot first and keeps its chrome. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
16
- - `scripts/screenshot.sh`: sources `scripts/lib/preview-server.sh`, then runs the capture against the served preview.
17
- - `scripts/lib/preview-server.sh`: sourced, never executed. Builds, serves on `PREVIEW_PORT`, blocks until the port answers, and traps the whole process group on exit, including the detached grandchild `lsof` finds. Both capture scripts source it so the startup lives once.
18
- - `scripts/readme-screenshot.sh`: captures one case under `SCREENSHOT_FILTER` and copies its light and dark frames to `assets/evidence/readme/`. `README_FILTER` ships empty with `header/1280` as its example, and the script exits 1 until a project fills it, since a shipped default would capture the wrong part of the page in every project but one. Its copy reads from `screenshots/<hostname>/`, which is where the harness writes.
19
- - `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/tooling/ci-workflow.md` states the test.
20
- - `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
21
- - `scripts/verify.json`: the phases base's verify runner requires here, being typecheck, lint, unit tests, and build. `## Verify script` covers the order and the card exclusion stage.
22
- - `scripts/worktree-port.sh`: prints a base port plus this working directory's offset. Called with no argument it prints the offset alone. It refuses a folder left under the worktrees directory after its worktree was removed, rather than printing a port for it.
13
+ ### Lint and test setup
14
+
15
+ Two configs carry the lint and unit-test baseline.
16
+
17
+ - `.oxlintrc.json` is migrated from the ESLint config it replaced. It runs the recommended, React, and test-scoped vitest rules, with `check-file` and `simple-import-sort` as JS plugins.
18
+ - `src/test/setup.ts` imports `@testing-library/jest-dom/vitest` and runs `cleanup` after each test.
19
+
20
+ ### Screenshot cases
21
+
22
+ `e2e/screenshot.ts` is the capture template. One `CASES` record carries an entry per output file, each naming a section, a theme, and a route, and per-project cases extend it.
23
+
24
+ - The loop crosses every case with `VIEWPORTS` and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`. The hostname of `SCREENSHOT_BASE_URL` keeps a local run and a deployed run apart.
25
+ - `VIEWPORTS` holds 320, 768, 1280, and 1536. Width 320 is the base a page has to hold without wrapping or clipping.
26
+ - `excludeViewports` skips a width, for a route whose long-form prose reflows at 320 instead of breaking.
27
+ - `evidence: true` also writes `evidence/<section>/<width>--<theme>.png` at 320 and 1280 alone, keeping the tracked set at twice the sections rather than four times.
28
+
29
+ ### Sections and masks
30
+
31
+ A case may name `sections`, one `{ name, selector }` per addressable part of the route. Each writes its own frame beside the whole-page one.
32
+
33
+ - A section frame lands at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png`, plus the same path under `evidence/` where the case and width are flagged.
34
+ - A selector matching nothing is reported and skipped, since a target syncing the template before editing its list would otherwise capture nothing.
35
+ - Testing that every section resolves belongs to the project owning the selectors.
36
+ - `mask` lists selectors painted over in every frame, for a value that moves with no code change, so a double capture compares the rest.
37
+
38
+ ### Settling before a shot
39
+
40
+ Every case settles before anything is shot.
41
+
42
+ - The context uses `reducedMotion: 'reduce'`, and each named section scrolls into view so an `IntersectionObserver` fires and `loading="lazy"` frames load. The page then returns to the top.
43
+ - Fonts, images, and same-origin frames are waited out on a bounded condition. A second `networkidle` does not re-arm for a load a scroll started.
44
+ - A section frame drops floating chrome (`position: sticky` or `fixed`), which would otherwise ride the scroll into the frame. The whole-page frame is shot first and keeps its chrome.
45
+
46
+ ### Run flags
47
+
48
+ Three controls narrow or harden a run.
49
+
50
+ - `SCREENSHOT_FILTER` keeps cases whose `<section>/<width>--<theme>` label contains a comma-separated term. It exits 1 and lists the labels on no match.
51
+ - `--check-console-clean` exits 1 with the list if any `console` error fired, turning the capture into a smoke check.
52
+ - `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, so a deployment script cannot capture `localhost`.
53
+ - With a base URL, it skips every flagged case so a production smoke run writes nothing to the committed path, and exits 1 when zero cases run.
54
+
55
+ ### Scripts and workflows
56
+
57
+ The wrappers around the capture and the verify run. `governance/rules/tooling/ci-workflow.md` states when a workflow's commit-back is right and when a report is.
58
+
59
+ - `scripts/screenshot.sh` sources `scripts/lib/preview-server.sh`, never executed itself. It builds, serves on `PREVIEW_PORT`, waits for the port, and traps the whole process group on exit.
60
+ - `scripts/readme-screenshot.sh` captures one case under `SCREENSHOT_FILTER` into `assets/evidence/readme/`. `README_FILTER` ships empty, so it exits 1 until a project fills it.
61
+ - `.github/workflows/readme-screenshot.yml` ships its path list as `REPLACE_WITH_PROJECT_PATHS`, and its first step exits 1 while that stays.
62
+ - `.github/workflows/verify.yml` runs `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests`.
23
63
 
24
64
  ## What stays in per-stack adapters
25
65
 
26
- Framework glue lives in `tooling/vite-react/configs/` or `tooling/astro/configs/` because the merge helpers and config shapes differ:
66
+ Framework glue lives in `tooling/vite-react/configs/` or `tooling/astro/configs/`, because merge helpers and config shapes differ.
27
67
 
28
- - `vite.config.ts` or `astro.config.mjs`: the framework's config.
29
- - `vitest.config.ts`: uses `mergeConfig` in Vite stacks, `getViteConfig` from `astro/config` in Astro.
30
- - `playwright.config.ts`: `webServer` command differs per stack.
31
- - `tsconfig.json`: `extends` target differs. Astro uses `astro/tsconfigs/strict`. Vite projects use scaffold defaults.
68
+ - `vite.config.ts` or `astro.config.mjs` holds the framework's config.
69
+ - `vitest.config.ts` uses `mergeConfig` in Vite stacks and `getViteConfig` from `astro/config` in Astro.
70
+ - `playwright.config.ts` differs in its `webServer` command.
71
+ - `tsconfig.json` differs in its `extends` target. Astro uses `astro/tsconfigs/strict` and Vite projects use scaffold defaults.
32
72
 
33
73
  ## File layout
34
74
 
35
- - `src/` for app code, `e2e/` for Playwright, `scripts/` for shell, `src/test/setup.ts` for Vitest globals.
75
+ - `src/` holds app code, `e2e/` Playwright, `scripts/` shell, and `src/test/setup.ts` the Vitest globals.
36
76
  - Path alias `@` maps to `./src` in both tsconfig and the framework's build config.
37
- - Tsconfig is unified at root with `noEmit: true` in Vite stacks. Astro uses the scaffold default from `@astrojs/check`.
77
+ - Tsconfig sits at root with `noEmit: true` in Vite stacks. Astro keeps the scaffold default from `@astrojs/check`.
38
78
 
39
79
  ## Ports
40
80
 
41
81
  Two worktrees of one repository run the same stack, so a fixed port makes the second one attach to the first.
42
82
 
83
+ ### Deriving the port
84
+
85
+ The helper owns the offset, and a config only adds it.
86
+
43
87
  - Derive every served port from `scripts/worktree-port.sh`. Never write a port literal into a script string.
44
- - Read `WORKTREE_PORT_OFFSET` in a config and add it to the stack's default port. Unset yields the default, so a plain clone keeps the port it has always served on.
45
- - Draw the offset from a band of 50, hashed from the worktree folder name. Two worktrees can hash to one offset, so set `WORKTREE_PORT_OFFSET` by hand to break a tie.
46
- - Expect a non-zero exit and no port from a folder left under `.claude/worktrees/` once its worktree is gone. Git reports the parent repository from inside one, so the helper cannot read it as a worktree and every base it serves would land on the main checkout's port. Both shapes refuse, and they reach differently. A folder whose own `.git` was deleted refuses only under that directory, since location is the only thing separating it from an ordinary subdirectory the base port is correct for. A folder whose `.git` names a pruned administrative directory refuses wherever it sits, because a pointer to nothing is broken regardless of where the folder is.
47
- - Call it as `VAR=$(bash scripts/worktree-port.sh) && export VAR && <server>`, never as the shorter `VAR=$(bash scripts/worktree-port.sh) <server>`. An assignment prefix discards the exit status of its own substitution, so the shorter form starts the server with `VAR` set to the empty string, every config reads that back as an offset of zero, and the refusal lands on the port it was raised to protect. The assignment alone carries the status, which is what the `&&` reads.
48
- - Set `WORKTREE_PORT_OFFSET` by hand to serve from such a folder anyway. That is the one override, and it is checked before any directory test.
49
- - Force-replace `dev` and `preview` through `[scripts.override]`. Both stacks' scaffolds define those keys, and a plain `[scripts]` entry never replaces a key the scaffold already wrote.
50
- - Set `strictPort` on every dev and preview server. A server that walks to the next free port serves where nothing is looking for it.
51
- - Set Playwright `reuseExistingServer: false`. Reuse attaches to whatever answers on the port, which reports a pass against another branch's code and prints nothing to say so.
88
+ - Read `WORKTREE_PORT_OFFSET` in a config and add it to the stack's default port. Unset yields the default, so a plain clone keeps its port.
89
+ - The offset comes from a band of 50 hashed from the worktree folder name. Two worktrees can collide, so set `WORKTREE_PORT_OFFSET` by hand to break a tie.
90
+ - Force-replace `dev` and `preview` through `[scripts.override]`, since a plain `[scripts]` entry never replaces a key the scaffold wrote.
91
+ - Set `strictPort` on every dev and preview server, and Playwright `reuseExistingServer: false`. A server that walks to another port, or a reused one, passes against the wrong code silently.
92
+
93
+ ### Calling the helper
94
+
95
+ Call it as `VAR=$(bash scripts/worktree-port.sh) && export VAR && <server>`, never as `VAR=$(bash scripts/worktree-port.sh) <server>`. An assignment prefix discards the exit status of its own substitution. The shorter form starts the server with `VAR` empty, which every config reads as offset zero.
96
+
97
+ ### Refusal in a removed worktree
98
+
99
+ Expect a non-zero exit and no port from a folder left under `.claude/worktrees/` once its worktree is gone. Git reports the parent repository from inside one, so every base served would land on the main checkout's port.
100
+
101
+ - A folder whose own `.git` was deleted refuses only under that directory, since location alone separates it from an ordinary subdirectory.
102
+ - A folder whose `.git` names a pruned administrative directory refuses wherever it sits.
103
+ - Set `WORKTREE_PORT_OFFSET` by hand to serve from such a folder anyway. It is the one override and is checked before any directory test.
52
104
 
53
105
  ## Anti-patterns
54
106
 
55
- Sticky negative knowledge. Do not relearn.
107
+ Negative knowledge that costs a failed run to relearn.
56
108
 
57
- - Do NOT use `tsc -b` in a Vite project. Composite mode emits `.js` next to `.ts` and the linter lints the emitted files. Use `tsc --noEmit`.
58
- - Do NOT trust `vite-react`'s scaffolded `lint` script. It ships a bare `"lint": "oxlint"`, which exits 0 on warnings. `[scripts.override]` forces `--max-warnings 0` over it, and the golden `.oxlintrc.json` overwrites the scaffold's bare one.
59
- - Do NOT rely on bare-folder exclude globs like `exclude: ["e2e"]`. Use `"e2e/**/*"`.
60
- - Do NOT ship Vitest with no-tests-fail. Fresh scaffolds have zero tests. Use `passWithNoTests: true` or equivalent until the project has at least one test.
61
- - Do NOT put Playwright `trace` at the top level of `defineConfig`. It lives under `use`.
62
- - Do NOT skip `skipLibCheck: true` in Vite + React tsconfig. `@testing-library/jest-dom` and Vite's `module-runner` produce type conflicts otherwise.
63
- - Do NOT run `bunx shadcn@latest init` over a pre-existing `src/styles/global.css`. Shadcn silently overwrites it. Back up first or run shadcn init before writing custom global styles. Use flags `-t <template> -b <base> -p <primary> -y` to avoid interactive prompts.
109
+ | Do NOT | Why and what to do |
110
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
111
+ | Use `exclude: ["e2e"]` | Bare-folder globs miss. Use `"e2e/**/*"`. |
112
+ | Put Playwright `trace` at the top of the config | It lives under `use`. |
113
+ | Ship Vitest failing on no tests | Fresh scaffolds have none. Use `passWithNoTests: true` until one exists. |
114
+ | Skip `skipLibCheck: true` in Vite + React | `@testing-library/jest-dom` and Vite's `module-runner` conflict otherwise. |
115
+ | Use `tsc -b` in a Vite project | Composite mode emits `.js` next to `.ts` and the linter lints it. Use `tsc --noEmit`. |
116
+ | Trust `vite-react`'s scaffolded `lint` script | Its bare `oxlint` exits 0 on warnings. `[scripts.override]` forces `--max-warnings 0` over it. |
117
+ | Run `bunx shadcn@latest init` over `global.css` | It silently overwrites `src/styles/global.css`. Back up first, and pass `-t -b -p -y` to skip prompts. |
64
118
 
65
119
  ## Tool pairing
66
120
 
67
- - Unit tests: Vitest with jsdom, globals on, setup file at `src/test/setup.ts`, `@testing-library/react`, `@testing-library/user-event`.
68
- - E2E: Playwright in `e2e/`. Chromium-only for Chrome extensions, all browsers for web apps.
69
- - Tailwind: v4 via `@tailwindcss/vite`. Never the v3 integration.
70
- - Lint: oxlint at an exact pin, since its JS plugin host is alpha and a minor release could break `check-file` or `simple-import-sort`. Astro keeps ESLint, which its own reference covers.
121
+ - Unit tests use Vitest with jsdom, globals on, `src/test/setup.ts`, `@testing-library/react`, and `@testing-library/user-event`.
122
+ - E2E uses Playwright in `e2e/`, Chromium only for Chrome extensions and all browsers for web apps.
123
+ - Tailwind is v4 through `@tailwindcss/vite`, never the v3 integration.
124
+ - Lint is oxlint at an exact pin, since its JS plugin host is alpha and a minor release could break `check-file` or `simple-import-sort`. Astro keeps ESLint, which its own reference covers.
71
125
 
72
126
  ## oxlint
73
127
 
74
- - Set `lint` and `lint:fix` through `[scripts.override]`, so an already-synced target stops running ESLint on its next sync.
75
- - Delete a leftover `eslint.config.js` or `eslint.config.mjs`, and `bun remove` the ESLint packages this stack stopped shipping, by hand after that sync. Sync deletes nothing a stack stopped shipping.
76
- - Keep import sorting on `simple-import-sort` rather than oxfmt's built-in sort, which orders imports differently and would rewrite every import block.
128
+ - Set `lint` and `lint:fix` through `[scripts.override]`, so a synced target stops running ESLint on its next sync.
129
+ - After that sync, delete a leftover `eslint.config.js` or `eslint.config.mjs` and `bun remove` the ESLint packages by hand. Sync deletes nothing a stack stopped shipping.
130
+ - Keep import sorting on `simple-import-sort`, because oxfmt's built-in sort orders differently and would rewrite every import block.
77
131
 
78
132
  ## lint-staged (extend)
79
133
 
80
- - Add `**/*.{js,jsx,ts,tsx}` glob running `oxlint --fix --max-warnings 0`, and `oxfmt --no-error-on-unmatched-pattern`.
81
- - Extend the oxfmt glob to include `css`: `**/*.{json,css,md}`.
134
+ - Add `**/*.{js,jsx,ts,tsx}` running `oxlint --fix --max-warnings 0` and `oxfmt --no-error-on-unmatched-pattern`.
135
+ - Extend the oxfmt glob to include `css`, giving `**/*.{json,css,md}`.
82
136
 
83
137
  ## CI docs (extend)
84
138
 
85
- Extend `canon/context/ci.md` so the `## Checks` table reflects the web jobs. <!-- audit-ignore-citations: canon/context/ci.md -->
86
-
87
- Append rows:
139
+ Extend `canon/context/development/ci.md` so the `## Checks` table reflects the web jobs. <!-- audit-ignore-citations: canon/context/development/ci.md -->
88
140
 
89
- | Typecheck | `bun run typecheck` | framework-specific typecheck passes |
90
- | Lint | `bun run lint` | oxlint passes with zero warnings |
91
- | Tests | `bun run test:coverage` | Vitest passes with coverage thresholds |
92
- | Build | `bun run build` | Production build succeeds |
93
- | E2E | `bun run test:e2e` | Playwright passes against the built preview |
141
+ | Check | Command | What it asserts |
142
+ | --------- | ----------------------- | ------------------------------------------- |
143
+ | Typecheck | `bun run typecheck` | framework-specific typecheck passes |
144
+ | Lint | `bun run lint` | oxlint passes with zero warnings |
145
+ | Tests | `bun run test:coverage` | Vitest passes with coverage thresholds |
146
+ | Build | `bun run build` | Production build succeeds |
147
+ | E2E | `bun run test:e2e` | Playwright passes against the built preview |
94
148
 
95
149
  Under `## Running CI locally`, document that `bun run check:full` runs verify plus `test:e2e`.
96
150
 
97
151
  ## Development docs (extend)
98
152
 
99
- Extend the `development` context entry under `canon/context/` so the `## Scripts` table lists every web script. Stack adapters add their `dev`, `build`, `preview`, `typecheck` rows.
100
-
101
- Append rows:
153
+ Extend the `development` context entry under `canon/context/` so the `## Scripts` table lists every web script. Stack adapters add their `dev`, `build`, `preview`, and `typecheck` rows.
102
154
 
103
- | `bun run lint` | Run oxlint with zero warnings allowed. |
104
- | `bun run lint:fix` | Auto-fix oxlint issues. |
105
- | `bun run test` | Run Vitest in watch mode. |
106
- | `bun run test:run` | Run Vitest once with verbose reporter. |
107
- | `bun run test:coverage` | Run Vitest with coverage. |
108
- | `bun run test:e2e` | Run Playwright E2E tests. |
109
- | `bun run test:e2e:changed` | Run Playwright E2E tests for specs the import graph reaches from the current diff. |
110
- | `bun run screenshot` | Build, preview, then capture screenshots. |
111
- | `bun run readme-screenshot` | Build, preview, then capture the README frame into `assets/evidence/readme/`. |
112
- | `bun run smoke:prod` | Capture against `SCREENSHOT_BASE_URL`, requiring it set and failing on any console error. |
155
+ | Command | Purpose |
156
+ | --------------------------- | --------------------------------------- |
157
+ | `bun run lint` | Run oxlint, no warnings allowed. |
158
+ | `bun run lint:fix` | Auto-fix oxlint issues. |
159
+ | `bun run test` | Run Vitest in watch mode. |
160
+ | `bun run test:run` | Run Vitest once, verbose. |
161
+ | `bun run test:coverage` | Run Vitest with coverage. |
162
+ | `bun run test:e2e` | Run Playwright E2E tests. |
163
+ | `bun run test:e2e:changed` | Run specs the diff's import graph hits. |
164
+ | `bun run screenshot` | Build, preview, capture screenshots. |
165
+ | `bun run readme-screenshot` | Capture the README frame. |
166
+ | `bun run smoke:prod` | Capture against a required base URL. |
113
167
 
114
- `canon tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
168
+ ### Screenshot contract
115
169
 
116
- `governance/rules/ui/surface-capture.md` is what asks a session to run the capture after a surface changes. It now fires on every component file too, reversing the route-and-page-only scope this reference once described, after a shipped batch of components carried no capture and a defect went unseen. The rule body states its own exemption for a component the production build strips out, so a reader chasing that case reads it there rather than here.
170
+ `canon tooling verify <stack>` is the only automated caller of `bun run screenshot`. It runs the script for any stack whose `package.json` declares it and asserts that PNG files land under `screenshots/`.
117
171
 
118
- The sweep under `screenshots/` is ignored again, and only a flagged case's `evidence/` output tracks in git, so the first capture a scaffolded target runs after this change is the baseline it commits there. A case naming sections commits a folder per section under that baseline rather than one file per theme, which is what lets a pull request comparison land on the part of the page that moved instead of on one image of the whole of it. The count is the section list times the themes, so a list is worth sizing against how many frames each later change has to regenerate and a reviewer has to read.
172
+ - The count is a recursive find with no depth limit, so the seed's section folders satisfy it. Do not flatten the layout to protect that check.
173
+ - `governance/rules/ui/surface-capture.md` asks a session to run the capture after a surface changes, component files included, with an exemption for a component the production build strips out.
174
+ - The sweep under `screenshots/` is ignored and only a flagged case's `evidence/` output tracks, so a target's first capture is the baseline it commits.
175
+ - A case naming sections commits a folder per section, so size the list against the frames each later change regenerates.
119
176
 
120
177
  ## Gitignore (extend)
121
178
 
122
- `[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
179
+ These `[gitignore]` groups mirror the manifest.
123
180
 
124
181
  - `"# Playwright" = ["test-results/", "playwright-report/", "blob-report/", "playwright/.cache/"]`
125
182
  - `"# Screenshots" = ["screenshots/"]`
126
183
 
127
184
  ## Verify script
128
185
 
129
- The web layer's `scripts/verify.json` requires `typecheck`, `lint`, `test:run`, and `build`, and base's `scripts/verify.ts` runs them in its fixed order: typecheck, lint, format, shell where the `shell` stack declares it, unit tests, build, markdown bans, then the card exclusion check where one is installed. astro, vite-react, and nextjs inherit the list, since the nearest stack's copy wins. An adapter whose phases differ ships its own `verify.json` rather than a script.
186
+ The web layer's `scripts/verify.json` requires `typecheck`, `lint`, `test:run`, and `build`. Base's `scripts/verify.ts` runs them in its fixed order, being typecheck, lint, format, shell where the `shell` stack declares it, unit tests, build, markdown bans, then the card exclusion check where installed.
130
187
 
131
- The card stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, which keeps the social card route's build exclusion on the default path rather than behind a flag. It tests for the script rather than the stack, so a project with no card route runs one less stage with no override, and the stack shipping a card route owns what the check reads, being the build the stage before it produced. The astro reference carries the route itself.
188
+ - Astro, vite-react, and nextjs inherit the list, since the nearest stack's copy wins. An adapter whose phases differ ships its own `verify.json` rather than a script.
189
+ - The card stage runs `scripts/check-card-exclusion.sh` whenever that file is installed, keeping the social card route's build exclusion on the default path rather than behind a flag.
190
+ - It tests for the script rather than the stack, so a project with no card route runs one less stage with no override.
191
+ - The stack shipping a card route owns what the check reads, being the build the stage before it produced. The astro reference carries the route itself.
@@ -1,86 +0,0 @@
1
- ---
2
- title: Canvas
3
- description: The canvas server, its content format, and the verbs that list, arrange, fit, select, edit, mark, and capture frames
4
- ---
5
-
6
- # Canvas
7
-
8
- `canon canvas` serves a local canvas of pages and HTML frames and gives an agent the verbs the operator's mouse has. A page is a folder under `.canon/canvas/`, a frame is one HTML file in it, and `layout.json` beside them holds each frame's box. `claude/skills/canvas-board/references/canvas.md` fixes that content format, and the `canvas-board` skill carries the procedure a session follows to drive the canvas.
9
-
10
- ## Serve
11
-
12
- `canvas serve` serves the canvas on `127.0.0.1`, walks ports like `canon serve`, refuses a `Host` other than loopback on its own port, and runs until interrupted. Each frame gets the project's token stylesheet injected, being this toolkit's tokens in its own checkout, else `.claude/design/base.css` plus anything under `.claude/design/project/`, else none with a notice. An open canvas reloads a frame whose file changes.
13
-
14
- The toolkit's own tokens arrive with the font faces they name embedded, so a frame renders in them on a machine that never installed them. An installed base gets the same treatment: each vendored face its sheets name is embedded, unless a project rule declares that family itself.
15
-
16
- A project's own `@font-face` rules under `.claude/design/project/` have each `url()` inlined as a `data:` URI. A rule whose file is missing, or whose path leads out of `.claude/design/project/` after links resolve, is dropped and the frame still renders. `canvas serve` warns once per dropped rule and names each under `tokens.dropped` in its `--json` record. The confinement check reads each `url()` after percent-decoding it. `canon design fonts add` writes those rules.
17
-
18
- Before it binds a port, `canvas serve` checks that `preact` and `@preact/signals` resolve from the package the CLI runs from, never from the served project. When either is missing it refuses with reason `missing-client-deps` and names `bun install` in the detail, rather than serving a shell that loads as a blank page.
19
-
20
- The write routes the shell posts to also refuse a request carrying an `Origin` other than the server's own, and a body that is not JSON.
21
-
22
- ## List and add
23
-
24
- `canvas list --json` emits every page with its frames. `page add`, `page rename`, and `frame add` (`--width`, `--height`) each emit one record under `--json`, refusing a name that is not one path segment, a page that already exists, or a root that is not a directory, with the reason on stderr.
25
-
26
- ## Arrange and select
27
-
28
- `frame move <page> <name> --x <px> --y <px>` rewrites one frame's position in `layout.json` and keeps its size. An omitted axis stays where the frame is. It refuses a frame or page that does not exist and a layout that does not parse.
29
-
30
- `frame resize <page> <name> --width <px> --height <px>` rewrites one frame's whole box in one write, and `--x` and `--y` move it at the same time. An omitted flag keeps the frame's current value. It refuses a width or height that is not a positive number as `invalid-size`, a position that is not a number as `invalid-position`, and a missing frame, a missing page, or a layout that does not parse as `frame move` does.
31
-
32
- `canvas arrange <page>` re-lays a page's frames. It keeps the rows they already sit in, where boxes whose vertical spans overlap share a row, orders each row left to right by `x`, places the boxes on the 80px frame gap from the page's leftmost edge, and stacks the rows on their tallest box plus the gap from the page's top edge. It also drops every layout entry whose frame file is gone. `--check` writes nothing. `--json` emits `{ ok, page, written, moved, dropped }`, where `moved` lists each frame with its `from` and `to` position and `dropped` lists the entries with no file. Sizes never change. Under `--check` it exits 2 when a frame would move or an entry would drop, and it refuses a missing page as `no-page` and a layout that does not parse as `malformed-layout`.
33
-
34
- `canvas fit <page>/<frame>` and `canvas fit <page>` set each frame's height to its content. The content is measured in the capture browser at the frame's box width, through the served address, after fonts and the load event settle, and the height is that measure rounded up with no margin. The write goes through the same writer as `frame resize`. `--check` writes nothing. `--json` emits `{ ok, written, frames }`, one row per frame carrying `height`, `fitted`, `wide`, `font`, `viewport`, `family`, and `owed`. `wide` is true when the content is wider than the box, which fit reports and never widens, since the width is the frame's declared render width. `font` is true when the first family of the body text is not one the served sheet declares a face for, being the toolkit faces `Geist Variable`, `Noto Sans Mono`, and the hand-drawn pair, plus any face the project declares, because a height fitted against a machine family may not hold in another browser. `viewport` is true when the frame's height depends on the viewport, as a height in `vh` does. The lowest element edge is read with the viewport at the box height and at twice it, and an edge that moved means the content measure is not the height a viewer draws, so its height is kept and nothing is written for it. Under `--check` it exits 2 when any row is owed, and it refuses as `no-page`, `no-frame`, `no-server`, or `capture-failed` the way `canvas capture` does. Rows below a frame that grew do not move, so run `canvas arrange` after a fit.
35
-
36
- The shell writes through the same writer when the operator drags a frame or one of its corner handles. Both take a lock file beside the layout, read it, merge their change, and replace it, so a drag released while a verb writes loses neither position. A writer that cannot get the lock within two seconds refuses with reason `busy`.
37
-
38
- `canvas selection --json` emits `{ ok, selection }` with the page, frame, file, box, and path of the frame the operator selected, or `selection: null` when nothing is selected or the selected frame has since been removed. Read it when the operator says "this one".
39
-
40
- The operator can also pick one element inside a frame, by clicking it on the surface or in the frame's layers tree. The selection then carries `element`, recording the `index` in document order, `tag`, `classes`, and `text` excerpt as they were at the pick. An edit to the frame since can shift that index onto another element, so `stale` turns true once the file's content no longer matches what the pick was made against. Check it before acting on the index.
41
-
42
- A pick is refused as `address-mismatch` when the browser builds elements the file never states, such as a `tbody` a table leaves out or a `body` after a `head` the file states. A fragment is picked like a whole document, since the server writes the missing `html`, `head`, and `body` into the served frame and the shell leaves them out of its count. The shell sends its own element count beside the index, so a frame the two sides count differently is reported rather than recorded against the wrong element. A pick is refused as `stale-address` when the frame file changed between the frame being served and the pick arriving, since every served frame carries the hash of its file and the shell sends that hash back.
43
-
44
- ## Edit
45
-
46
- `canvas edit <page>/<frame> --element <index> --set <property>=<value>` sets one property of the element at that index, the same index `canvas selection` reports. The properties are `text`, `color`, `background-color`, `font-size`, `font-weight`, `font-family`, `line-height`, `letter-spacing`, `text-align`, `width`, `height`, `padding`, `gap`, `display`, `flex-direction`, `justify-content`, `align-items`, `flex-wrap`, `opacity`, `border-radius`, `border-top-left-radius`, `border-top-right-radius`, `border-bottom-right-radius`, and `border-bottom-left-radius`. A style property is written into the element's inline `style`, replacing that property and leaving the others as they were, and an empty value drops it. Writing `border-radius` or `padding` also drops that shorthand's longhands from the same `style`, so no corner or side left over overrides the new value. Writing a longhand leaves its shorthand in place, since the longhand comes later and wins.
47
-
48
- `text` replaces the text of an element holding text alone and refuses one holding other elements as `not-text-only`. Every byte outside the element stays as it was. `--json` emits the page, frame, file, path, and the file's new `hash`.
49
-
50
- The operator edits the same set from the inspector, which posts to the same writer with the hash the frame was served with. Dragging a selected element's corner handle writes `width` and then `height` in pixels through that writer, the second edit carrying the hash the first answered. An edit made against a version of the file Claude has since rewritten is refused as `stale-address` and the shell reloads the frame, so nothing lands on a shifted element. Both writers take the frame file's lock, and an edit that keeps the selected element where it was keeps the selection fresh.
51
-
52
- Size, padding, opacity, radius, and the type properties show in the inspector for every element. Family and font size list the sheet's `font-family` and `font-size` token groups first and write a pick as `var(--<name>)`, the same as a color. The flex fields show only on a flex container, a grid container gets none, and the inspector sets `display` only to `flex`, so any other `display` change goes through the verb.
53
-
54
- A color picked from the project's tokens is written as `var(--<name>)`, and below full opacity as a `color-mix()` of that `var()` with `transparent` at the opacity percent, so it keeps following the theme. A browser older than 2023 drops a `color-mix()` value, which leaves the property unset there. A hex is written as six digits, or eight below full opacity. The inspector's full color picker writes a hex for any color it sets, so the field marks the value raw, and only its alpha keeps a token through the `color-mix()` form.
55
-
56
- Inline style beats a class, so an operator's edit masks a class change Claude makes later. Read a frame's inline styles before restyling it, and remove one the change should replace. The inspector marks a color, a background, or a family or font size the sheet has tokens for, set inline, with the word `raw` when its value names no `var()` and is not `currentColor` or a CSS-wide keyword, since that value stays fixed when the theme changes. A property outside the set, or a value carrying `;`, `{`, `}`, `<`, `>`, or a line break, is refused as `invalid-edit`.
57
-
58
- The Theme tab in the shell lists what the token stylesheet defines, grouped as color, spacing, radius, font family, font size, and other. The page record at `/api/pages` carries the same list as `tokens.groups`.
59
-
60
- ## Undo in the shell
61
-
62
- The operator undoes and redoes their own shell edits, frame moves, and frame resizes from Ctrl or Cmd with Z, Shift with it or Ctrl with Y, or the tool strip's buttons. The history lives in the `canvas serve` process and covers shell edits alone, so a `canvas edit`, `canvas frame move`, or `canvas frame resize` never enters it, and their `--json` records are unchanged. A restart clears it.
63
-
64
- An undo writes the recorded state back only while the element or frame still holds what the operator's edit left. When Claude has since rewritten what that edit changed, being the element's inline style or, for a text edit, its text, or has added or removed an element in that frame, the operator's entry is dropped and nothing is written, and the shell says so. A write that changes another element's style or text, or another part of the same element, leaves the entry live. The page record at `/api/pages` carries `history` as `{ canUndo, canRedo }`.
65
-
66
- The tool strip also holds a toggle, bound to backslash, that hides both side panels to show the whole surface, and each panel resizes from a handle on its inner edge. Both are remembered in the browser and write nothing to the project.
67
-
68
- ## Mark a frame being edited
69
-
70
- `canvas editing <page>/<frame>` marks a frame as being edited, so an open canvas shows who is working on it before the write lands. The shell draws the mark as `<by> editing` on that frame's label with a dashed outline around it, and drops it on the reread after `--done` or at the mark's `until` with no reread. Run it before writing a frame and again with `--done` once the edits are written. A bare `canvas editing` lists the live marks, and `--json` emits `{ ok, editing }` after each of the three, with each mark carrying `page`, `frame`, `by`, `since`, and `until`.
71
-
72
- The label is `--by <name>` when given. Without it, the verb reads the caller's name off the session roster, and falls back to `a session` when the environment names no session or the roster holds none matching it.
73
-
74
- The marks live in `.canon/canvas/editing.json`, keyed by `<page>/<frame>`. Each write takes the record's lock and merges its one key, so two sessions marking two frames both keep their marks. A mark lapses five minutes after it was written, and marking the frame again renews it, so a session that never clears its mark misleads the operator for five minutes at most. A read leaves out a lapsed mark and a mark on a frame since removed, and a record that does not parse reads as holding none and is replaced by the next mark.
75
-
76
- A mark refuses `no-page` or `no-frame` for a target not on disk, and `invalid-name` for a target that is not `<page>/<frame>` or for `--done` with no target. `--done` on a frame holding no mark succeeds and writes nothing. The mark gates no write, so an inspector edit to a marked frame still lands, and the hash check still refuses one made against a file the session rewrote. The page record at `/api/pages` carries the live marks as `editing`, each with `page`, `frame`, `by`, and `until`.
77
-
78
- ## Capture
79
-
80
- `canvas capture <page>/<frame>` renders one frame as a PNG, and `canvas capture <page>` renders every frame on the page. Each goes through a server started for the call, so the injected tokens are in the image, and renders at the frame's own width from the layout, so a media query resolves there. It never captures the file on disk.
81
-
82
- `--out` is the PNG for a frame and a folder for a page, defaulting to session scratch. `--json` emits `{ ok, captures }` with the path and pixel size of each frame. Each PNG is the frame's whole document, so its height follows the document and not the frame's box. The font check `canon capture` makes applies, so a frame naming a font the machine lacks fails with that reason. A refused capture reports `no-page` or `no-frame` for a target that does not exist, `no-server` when the server for the call cannot start, and `capture-failed` for an error the engine throws, such as a browser that will not launch.
83
-
84
- `canvas capture <page> --composite` renders the whole page as one PNG, with every frame at its layout position and its own width. The image is the union of the frame boxes with no padding, transparent where no frame sits, and frames that overlap paint in the board's order. It shows the frames only, with no captions and no selection outline. Each frame is clipped to its box as it is on the board, so its height can differ from the per-frame PNG, and each shows in its default theme, since the board's theme toggle is client state the capture never sees. The font check covers the composite page and not the documents inside its frames.
85
-
86
- Under `--composite`, `--out` is the PNG itself, defaulting to `<page>.png` beside the per-frame folder in session scratch. `--json` emits `{ ok, composite }` carrying the page, the path, and the pixel size. The flag takes a page alone, and a `<page>/<frame>` target is refused as `invalid-name`. For the length of the call the generated page sits at `.canon/canvas/.composite/`, so it shares an origin with the frames, and the capture removes it after.