@erclx/canon 5.43.0 → 5.45.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 (245) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/backlog-triage/references/apply.md +6 -6
  4. package/claude/skills/canon-cli/SKILL.md +4 -5
  5. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/context-fold/references/context.md +1 -1
  8. package/claude/skills/create-skill/REQUIREMENT.md +1 -1
  9. package/claude/skills/create-skill/SKILL.md +2 -2
  10. package/claude/skills/create-skill/references/skill-requirement.md +68 -9
  11. package/claude/skills/create-skill/references/skill.md +1 -1
  12. package/claude/skills/design-identity/REQUIREMENT.md +40 -0
  13. package/claude/skills/design-identity/SKILL.md +138 -0
  14. package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
  15. package/claude/skills/design-taste/REQUIREMENT.md +6 -6
  16. package/claude/skills/design-taste/SKILL.md +2 -2
  17. package/claude/skills/design-taste/references/kinds.md +1 -1
  18. package/claude/skills/design-taste/references/preflight.md +1 -1
  19. package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
  20. package/claude/skills/draft-and-pick/SKILL.md +3 -153
  21. package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
  22. package/claude/skills/draft-doc/SKILL.md +4 -76
  23. package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
  24. package/claude/skills/draft-figure/SKILL.md +3 -77
  25. package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
  26. package/claude/skills/draft-identity/SKILL.md +3 -130
  27. package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
  28. package/claude/skills/draft-ready/SKILL.md +3 -128
  29. package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
  30. package/claude/skills/draft-screencast/SKILL.md +3 -62
  31. package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
  32. package/claude/skills/draft-slides/SKILL.md +3 -72
  33. package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
  34. package/claude/skills/draw-figure/SKILL.md +85 -0
  35. package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
  36. package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
  37. package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
  38. package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
  39. package/claude/skills/make-deck/REQUIREMENT.md +44 -0
  40. package/claude/skills/make-deck/SKILL.md +80 -0
  41. package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
  42. package/claude/skills/markdown-craft/SKILL.md +2 -2
  43. package/claude/skills/pick-by-eye/REQUIREMENT.md +36 -0
  44. package/claude/skills/pick-by-eye/SKILL.md +161 -0
  45. package/claude/skills/plan-feature/SKILL.md +2 -1
  46. package/claude/skills/plan-feature/references/plan.md +24 -10
  47. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  48. package/claude/skills/plan-intake/SKILL.md +1 -1
  49. package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
  50. package/claude/skills/plan-ready/SKILL.md +136 -0
  51. package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
  52. package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
  53. package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
  54. package/claude/skills/record-screencast/SKILL.md +2 -2
  55. package/claude/skills/role-orchestrator/REQUIREMENT.md +16 -99
  56. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
  57. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  58. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
  59. package/claude/skills/role-planner/REQUIREMENT.md +15 -73
  60. package/claude/skills/role-planner/SKILL.md +2 -1
  61. package/claude/skills/role-worker/REQUIREMENT.md +13 -41
  62. package/claude/skills/role-worker/SKILL.md +9 -0
  63. package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
  64. package/claude/skills/script-screencast/SKILL.md +70 -0
  65. package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
  66. package/claude/skills/search-craft/REQUIREMENT.md +4 -4
  67. package/claude/skills/search-craft/SKILL.md +1 -1
  68. package/claude/skills/target-setup/SKILL.md +3 -2
  69. package/claude/skills/target-setup/references/detect.md +0 -1
  70. package/claude/skills/target-setup/references/verify.md +2 -2
  71. package/claude/skills/task-board/SKILL.md +11 -9
  72. package/claude/skills/task-board/references/board.md +15 -12
  73. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  74. package/claude/skills/teach-workspace/SKILL.md +1 -1
  75. package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
  76. package/claude/skills/teach-workspace/references/promotion.md +5 -5
  77. package/claude/skills/ui-checklist/SKILL.md +2 -0
  78. package/claude/skills/write-doc/REQUIREMENT.md +46 -0
  79. package/claude/skills/write-doc/SKILL.md +83 -0
  80. package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
  81. package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
  82. package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
  83. package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +3 -3
  84. package/docs/agents/install-and-sync.md +2 -2
  85. package/docs/agents/markdown-audit.md +4 -2
  86. package/docs/agents/migrate.md +2 -2
  87. package/docs/agents/output-shape.md +1 -2
  88. package/docs/target/migrations.md +35 -4
  89. package/docs/target/projects.md +2 -3
  90. package/docs/target/scenarios.md +4 -1
  91. package/docs/workflow/parallel-features.md +3 -2
  92. package/docs/workflow/skill-map-on-request.md +19 -13
  93. package/docs/workflow/skill-map.md +13 -12
  94. package/docs/workflow/visual-design-workflow.md +2 -2
  95. package/governance/detect.toml +4 -0
  96. package/governance/ledger/framework/astro.md +73 -0
  97. package/governance/ledger/framework/fastapi.md +76 -0
  98. package/governance/ledger/framework/nextjs.md +86 -0
  99. package/governance/ledger/framework/react.md +83 -0
  100. package/governance/ledger/framework/shadcn.md +60 -0
  101. package/governance/ledger/framework/tailwind.md +55 -0
  102. package/governance/ledger/lang/go.md +61 -0
  103. package/governance/ledger/lang/php.md +50 -0
  104. package/governance/ledger/lang/python.md +68 -0
  105. package/governance/ledger/lang/typescript.md +64 -0
  106. package/governance/ledger/lib/database.md +65 -0
  107. package/governance/ledger/lib/e2e-reliability.md +57 -0
  108. package/governance/ledger/lib/pydantic.md +61 -0
  109. package/governance/ledger/lib/security-server.md +62 -0
  110. package/governance/ledger/lib/security-web.md +51 -0
  111. package/governance/ledger/lib/tanstack-query.md +57 -0
  112. package/governance/ledger/lib/test-scope.md +43 -0
  113. package/governance/ledger/lib/testing-go.md +57 -0
  114. package/governance/ledger/lib/testing-php.md +51 -0
  115. package/governance/ledger/lib/testing-py.md +67 -0
  116. package/governance/ledger/lib/testing-ts.md +63 -0
  117. package/governance/ledger/lib/zod.md +47 -0
  118. package/governance/ledger/tooling/ci-workflow.md +33 -0
  119. package/governance/ledger/tooling/code-comments.md +48 -0
  120. package/governance/ledger/tooling/config-comments.md +27 -0
  121. package/governance/ledger/tooling/dependencies.md +38 -0
  122. package/governance/ledger/ui/a11y.md +55 -0
  123. package/governance/ledger/ui/copy.md +49 -0
  124. package/governance/ledger/ui/design-reference.md +42 -0
  125. package/governance/ledger/ui/design-taste.md +19 -0
  126. package/governance/ledger/ui/forms.md +71 -0
  127. package/governance/ledger/ui/interface-casing.md +47 -0
  128. package/governance/ledger/ui/keyboard.md +68 -0
  129. package/governance/ledger/ui/link-behavior.md +29 -0
  130. package/governance/ledger/ui/motion.md +33 -0
  131. package/governance/ledger/ui/surface-capture.md +69 -0
  132. package/governance/ledger/ui/ux-completeness.md +79 -0
  133. package/governance/rules/framework/astro.md +12 -14
  134. package/governance/rules/framework/fastapi.md +20 -24
  135. package/governance/rules/framework/nextjs.md +19 -27
  136. package/governance/rules/framework/react.md +22 -33
  137. package/governance/rules/framework/shadcn.md +11 -15
  138. package/governance/rules/framework/tailwind.md +12 -14
  139. package/governance/rules/lang/go.md +7 -16
  140. package/governance/rules/lang/php.md +3 -3
  141. package/governance/rules/lang/python.md +9 -18
  142. package/governance/rules/lang/typescript.md +9 -19
  143. package/governance/rules/lib/database.md +5 -5
  144. package/governance/rules/lib/e2e-reliability.md +4 -6
  145. package/governance/rules/lib/pydantic.md +14 -16
  146. package/governance/rules/lib/security-server.md +4 -4
  147. package/governance/rules/lib/security-web.md +6 -10
  148. package/governance/rules/lib/tanstack-query.md +10 -12
  149. package/governance/rules/lib/test-scope.md +4 -4
  150. package/governance/rules/lib/testing-go.md +1 -2
  151. package/governance/rules/lib/testing-php.md +4 -5
  152. package/governance/rules/lib/testing-py.md +11 -19
  153. package/governance/rules/lib/testing-ts.md +7 -16
  154. package/governance/rules/lib/zod.md +6 -8
  155. package/governance/rules/tooling/code-comments.md +2 -4
  156. package/governance/rules/tooling/config-comments.md +1 -4
  157. package/governance/rules/tooling/dependencies.md +1 -1
  158. package/governance/rules/ui/a11y.md +16 -45
  159. package/governance/rules/ui/copy.md +6 -9
  160. package/governance/rules/ui/forms.md +13 -19
  161. package/governance/rules/ui/interface-casing.md +5 -7
  162. package/governance/rules/ui/keyboard.md +38 -0
  163. package/governance/rules/ui/link-behavior.md +3 -3
  164. package/governance/rules/ui/motion.md +3 -3
  165. package/governance/rules/ui/surface-capture.md +15 -27
  166. package/governance/rules/ui/ux-completeness.md +16 -39
  167. package/governance/rules/writing/markdown.md +1 -1
  168. package/package.json +1 -1
  169. package/scripts/core/install-check.sh +1 -1
  170. package/src/claude/cases/authoring.ts +15 -3
  171. package/src/claude/cases/misc.ts +12 -0
  172. package/src/claude/cases/workflow.ts +19 -1
  173. package/src/claude/skills-audit.ts +113 -1
  174. package/src/cli/help.ts +0 -1
  175. package/src/cli.ts +0 -2
  176. package/src/commands/claude.ts +34 -11
  177. package/src/commands/demo.ts +1 -1
  178. package/src/commands/gov.ts +4 -0
  179. package/src/commands/init.ts +2 -2
  180. package/src/commands/markdown.ts +1 -1
  181. package/src/commands/migrate.ts +4 -0
  182. package/src/commands/pr.ts +30 -6
  183. package/src/commands/tasks.ts +383 -2
  184. package/src/commands/tooling.ts +18 -5
  185. package/src/context/audit.ts +1 -1
  186. package/src/demo/beats.ts +1 -1
  187. package/src/design/components.ts +1 -1
  188. package/src/gov/audit.ts +25 -4
  189. package/src/init/plan.ts +1 -8
  190. package/src/init/steps.ts +0 -8
  191. package/src/inventory/subjects.ts +1 -1
  192. package/src/markdown/scope.ts +49 -35
  193. package/src/migrate/record-layout.ts +1 -1
  194. package/src/migrate/scratch-evidence.ts +12 -4
  195. package/src/migrate/skill-names.ts +68 -21
  196. package/src/pr/frames.ts +14 -3
  197. package/src/records/backup.ts +3 -3
  198. package/src/records/validate.ts +31 -0
  199. package/src/roots/record.ts +17 -8
  200. package/src/tasks/board.ts +854 -0
  201. package/src/tasks/record.ts +20 -1
  202. package/src/tasks/validate.ts +206 -9
  203. package/src/tooling/inject.ts +26 -7
  204. package/src/tooling/package.ts +32 -0
  205. package/src/tooling/read.ts +46 -3
  206. package/src/tooling/scan.ts +8 -2
  207. package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
  208. package/tooling/astro/reference.md +64 -93
  209. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +1 -1
  210. package/tooling/base/configs/scripts/verify.ts +1 -1
  211. package/tooling/base/reference.md +1 -9
  212. package/tooling/base/seeds/canon/context/{ci.md → development/ci.md} +1 -1
  213. package/tooling/base/seeds/canon/context/development/index.md +11 -0
  214. package/tooling/base/seeds/canon/context/{development.md → development/setup.md} +8 -12
  215. package/tooling/base/seeds/canon/context/index.md +1 -2
  216. package/tooling/claude/manifest.toml +1 -5
  217. package/tooling/claude/reference.md +36 -59
  218. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
  219. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -7
  220. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -7
  221. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +3 -10
  222. package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +2 -7
  223. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +2 -4
  224. package/tooling/claude/seeds/.claude/settings.json +4 -0
  225. package/tooling/nextjs/reference.md +0 -6
  226. package/tooling/php/reference.md +0 -4
  227. package/tooling/web/configs/.github/workflows/verify.yml +28 -0
  228. package/tooling/web/configs/.oxlintrc.json +7 -0
  229. package/tooling/web/configs/e2e/capture-types.ts +35 -0
  230. package/tooling/web/configs/e2e/screenshot.ts +23 -72
  231. package/tooling/web/manifest.toml +1 -1
  232. package/tooling/web/reference.md +77 -124
  233. package/tooling/web/seeds/e2e/capture/cases.ts +47 -0
  234. package/src/commands/wiki.ts +0 -100
  235. package/src/wiki/init.ts +0 -85
  236. /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
  237. /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
  238. /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
  239. /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
  240. /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
  241. /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
  242. /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
  243. /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
  244. /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
  245. /package/claude/skills/{draft-doc → write-doc}/references/wiki.md +0 -0
@@ -18,14 +18,9 @@ session=$(printf '%s' "$input" | jq -r '.session_id // empty')
18
18
  [ -n "$session" ] || exit 0
19
19
 
20
20
  key=$(printf '%s' "$session" | tr -c 'A-Za-z0-9' '_')
21
- # The marker is scratch, so it follows the scratch folder to whichever record
22
- # root the project carries rather than creating a second one beside it.
21
+ # The marker is scratch, so it lives under `.canon/tmp/`.
23
22
  project="${CLAUDE_PROJECT_DIR:-.}"
24
- if [ -d "$project/.canon" ]; then
25
- marker_dir="$project/.canon/tmp/hooks/search-reminder"
26
- else
27
- marker_dir="$project/.claude/.tmp/hooks/search-reminder"
28
- fi
23
+ marker_dir="$project/.canon/tmp/hooks/search-reminder"
29
24
  marker="$marker_dir/$key"
30
25
  [ -f "$marker" ] && exit 0
31
26
  mkdir -p "$marker_dir"
@@ -18,11 +18,9 @@ case "$file" in
18
18
  *) exit 0 ;;
19
19
  esac
20
20
 
21
- # Three record folders, at either root. A skip list fixed at the old spelling
22
- # audits a project's own session records the moment the move reaches it, which
23
- # reports findings against prose nobody publishes.
21
+ # Session record folders are skipped, since they hold prose nobody publishes.
24
22
  case "$file" in
25
- *.claude/.tmp/* | *.claude/review/* | *.claude/plans/*) exit 0 ;;
23
+ *.claude/review/* | *.claude/plans/*) exit 0 ;;
26
24
  *.canon/tmp/* | *.canon/review/* | *.canon/plans/*) exit 0 ;;
27
25
  esac
28
26
 
@@ -54,6 +54,10 @@
54
54
  "type": "command",
55
55
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/records-index.sh"
56
56
  },
57
+ {
58
+ "type": "command",
59
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/board-validate.sh"
60
+ },
57
61
  {
58
62
  "type": "command",
59
63
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/path-form.sh"
@@ -64,12 +64,6 @@ Next has no config-file port hook, unlike `server.port` in `astro.config.mjs` or
64
64
 
65
65
  `create-next-app`'s default `tsconfig.json` needs no changes beyond project-specific path aliases, so a golden copy would ship nothing the scaffold does not already write.
66
66
 
67
- ## Gitignore (extend)
68
-
69
- These `[gitignore]` groups mirror the manifest.
70
-
71
- - `"# Next" = [".next/"]`
72
-
73
67
  ## Development docs (extend)
74
68
 
75
69
  Append these rows to the `## Scripts` table.
@@ -59,10 +59,6 @@ The manifest prepares a scaffold that lacks a `package.json`.
59
59
  - The manifest's `[verify] prepare` runs checklist steps 2 and 5 for `canon tooling verify php`, being `bun init -y` and the `composer require --dev` line.
60
60
  - The scaffold writes no `package.json`, and a verify that reaches its end with none fails.
61
61
 
62
- ## Gitignore
63
-
64
- - `# PHP`: `vendor/`, `.php-cs-fixer.cache`, `.phpunit.cache/`, `.phpunit.result.cache`
65
-
66
62
  ## CI
67
63
 
68
64
  - Install PHP with `shivammathur/setup-php` and `tools: composer`.
@@ -62,9 +62,37 @@ jobs:
62
62
  - name: Install Dependencies
63
63
  run: bun install --frozen-lockfile
64
64
 
65
+ - name: Detect Browser Tests
66
+ id: browser
67
+ run: |
68
+ if jq -e '.scripts["test:browser"]' package.json >/dev/null; then
69
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
70
+ fi
71
+
72
+ - name: Install Playwright System Dependencies
73
+ if: steps.browser.outputs.enabled == 'true'
74
+ timeout-minutes: 10
75
+ run: bunx playwright install-deps chromium
76
+
77
+ - name: Cache Playwright Browsers
78
+ id: playwright-cache
79
+ if: steps.browser.outputs.enabled == 'true'
80
+ uses: actions/cache@v6
81
+ with:
82
+ path: ~/.cache/ms-playwright
83
+ key: playwright-chromium-${{ runner.os }}-${{ hashFiles('**/package.json') }}
84
+
85
+ - name: Install Playwright Chromium
86
+ if: steps.browser.outputs.enabled == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
87
+ run: bunx playwright install chromium
88
+
65
89
  - name: Run Tests
66
90
  run: bun run test:coverage
67
91
 
92
+ - name: Run Browser Tests
93
+ if: steps.browser.outputs.enabled == 'true'
94
+ run: bun run test:browser
95
+
68
96
  build-verify:
69
97
  name: 🏗️ Build
70
98
  runs-on: ubuntu-latest
@@ -183,6 +183,13 @@
183
183
  "browser": true
184
184
  }
185
185
  },
186
+ {
187
+ "files": ["**/components/ui/**"],
188
+ "plugins": ["react"],
189
+ "rules": {
190
+ "react/only-export-components": "off"
191
+ }
192
+ },
186
193
  {
187
194
  "files": ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
188
195
  "rules": {
@@ -0,0 +1,35 @@
1
+ import type { Page } from '@playwright/test'
2
+
3
+ /** One addressable part of a route, captured beside the whole-page frame. */
4
+ export interface CaptureSection {
5
+ name: string
6
+ selector: string
7
+ }
8
+
9
+ export interface Viewport {
10
+ readonly name: string
11
+ readonly width: number
12
+ readonly height: number
13
+ /** Whether a case flagged `evidence: true` also writes its frames at this width. */
14
+ readonly evidence: boolean
15
+ }
16
+
17
+ export interface CaptureCase {
18
+ section: string
19
+ theme: string
20
+ route: string
21
+ /** Names of viewports this case skips, for a route that reflows rather than breaks at that width. */
22
+ excludeViewports?: readonly string[]
23
+ evidence?: boolean
24
+ /** Selectors painted over in every frame the case writes, for a value that moves between runs with no code change. */
25
+ mask?: readonly string[]
26
+ sections?: CaptureSection[]
27
+ setup?: (page: Page) => Promise<void>
28
+ }
29
+
30
+ /** What the harness loads from the project's `e2e/capture/cases.ts`. */
31
+ export interface CaptureModule {
32
+ CASES: CaptureCase[]
33
+ /** Where a case flagged `evidence: true` writes its frames, relative to the working directory. */
34
+ EVIDENCE_ROOT: string
35
+ }
@@ -1,21 +1,15 @@
1
1
  import type { Locator, Page } from '@playwright/test'
2
2
  import { chromium } from '@playwright/test'
3
+ import { existsSync } from 'fs'
3
4
  import { mkdir } from 'fs/promises'
4
5
  import path from 'path'
5
-
6
- /** One addressable part of a route, captured beside the whole-page frame. */
7
- interface CaptureSection {
8
- name: string
9
- selector: string
10
- }
11
-
12
- interface Viewport {
13
- readonly name: string
14
- readonly width: number
15
- readonly height: number
16
- /** Whether a case flagged `evidence: true` also writes its frames at this width. */
17
- readonly evidence: boolean
18
- }
6
+ import { pathToFileURL } from 'url'
7
+ import type {
8
+ CaptureCase,
9
+ CaptureModule,
10
+ CaptureSection,
11
+ Viewport,
12
+ } from './capture-types'
19
13
 
20
14
  /**
21
15
  * The breakpoint buckets every case is crossed with, named by width. 320 is
@@ -30,61 +24,18 @@ const VIEWPORTS: readonly Viewport[] = [
30
24
  { name: '1536', width: 1536, height: 800, evidence: false },
31
25
  ]
32
26
 
33
- interface CaptureCase {
34
- section: string
35
- theme: string
36
- route: string
37
- /** Names of viewports this case skips, for a route that reflows rather than breaks at that width. */
38
- excludeViewports?: readonly string[]
39
- evidence?: boolean
40
- /** Selectors painted over in every frame the case writes, for a value that moves between runs with no code change. */
41
- mask?: readonly string[]
42
- sections?: CaptureSection[]
43
- setup?: (page: Page) => Promise<void>
44
- }
27
+ const CASES_FILE = path.resolve('e2e/capture/cases.ts')
45
28
 
46
- /**
47
- * The landing page's own beats, top to bottom. Every entry is a section a
48
- * reviewer reads on its own, so a diff lands on the part that moved rather
49
- * than on one image of the whole page.
50
- */
51
- const HOME_SECTIONS: CaptureSection[] = [
52
- { name: 'top', selector: '#top' },
53
- { name: 'proof', selector: '#proof' },
54
- { name: 'ask', selector: '#ask' },
55
- { name: 'rules', selector: '#rules' },
56
- { name: 'skills', selector: '#skills' },
57
- { name: 'plan', selector: '#plan' },
58
- { name: 'dispatch', selector: '#dispatch' },
59
- { name: 'workers', selector: '#workers' },
60
- { name: 'gate', selector: '#gate' },
61
- { name: 'evidence', selector: '#evidence' },
62
- { name: 'loop', selector: '#loop' },
63
- { name: 'merge', selector: '#merge' },
64
- { name: 'provenance', selector: '#provenance' },
65
- { name: 'start', selector: '#start' },
66
- { name: 'field', selector: '#field' },
67
- { name: 'close', selector: '#close' },
68
- ]
29
+ if (!existsSync(CASES_FILE)) {
30
+ console.error(
31
+ 'e2e/capture/cases.ts is missing. Run canon tooling sync to seed it.',
32
+ )
33
+ process.exit(1)
34
+ }
69
35
 
70
- const CASES: CaptureCase[] = [
71
- {
72
- section: 'home',
73
- theme: 'default',
74
- route: '/',
75
- evidence: true,
76
- mask: ['.version'],
77
- sections: HOME_SECTIONS,
78
- },
79
- {
80
- section: 'home',
81
- theme: 'dark',
82
- route: '/',
83
- mask: ['.version'],
84
- sections: HOME_SECTIONS,
85
- setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
86
- },
87
- ]
36
+ const { CASES, EVIDENCE_ROOT } = (await import(
37
+ pathToFileURL(CASES_FILE).href
38
+ )) as CaptureModule
88
39
 
89
40
  /** One case at one viewport, which is the unit a filter selects and a context is built for. */
90
41
  interface CaptureRun {
@@ -170,9 +121,9 @@ async function settle(
170
121
  const element = page.locator(section.selector)
171
122
  // Read the count before scrolling, since a selector matching nothing
172
123
  // otherwise costs the default timeout once per section. A miss is
173
- // reported and skipped rather than raised, because `CASES` is a template
174
- // a project rewrites and a target that syncs this file before editing its
175
- // own list would otherwise capture nothing at all. The project owning the
124
+ // reported and skipped rather than raised, because the seeded `CASES` name
125
+ // sections a fresh target's page may not have, and it would otherwise
126
+ // capture nothing at all. The project owning the
176
127
  // selectors is the one that tests them, which is what the section spec
177
128
  // beside its own page does.
178
129
  if ((await element.count()) === 0) {
@@ -240,7 +191,7 @@ async function markFloatingChrome(page: Page) {
240
191
  /**
241
192
  * Turns a case's mask selectors into the locators every frame paints over. A
242
193
  * selector matching nothing is reported and kept out rather than raised, for
243
- * the same template reason `settle` gives for a missing section.
194
+ * the same reason `settle` gives for a missing section.
244
195
  */
245
196
  async function resolveMask(
246
197
  page: Page,
@@ -318,7 +269,7 @@ for (const { captureCase, viewport, label } of RUNS) {
318
269
  const mask = await resolveMask(page, label, captureCase.mask ?? [])
319
270
 
320
271
  const sweepDir = path.join(OUT_DIR, captureCase.section)
321
- const evidenceDir = path.join('evidence', captureCase.section)
272
+ const evidenceDir = path.join(EVIDENCE_ROOT, captureCase.section)
322
273
  const file = `${viewport.name}--${captureCase.theme}.png`
323
274
  const writesEvidence = captureCase.evidence && viewport.evidence
324
275
 
@@ -47,7 +47,7 @@ packages = [
47
47
 
48
48
  [gitignore]
49
49
  "# Build" = ["dist/"]
50
- "# Coverage" = ["coverage/"]
50
+ "# Coverage" = ["coverage/", ".vitest/"]
51
51
  "# Playwright" = [
52
52
  "test-results/",
53
53
  "playwright-report/",
@@ -4,139 +4,118 @@
4
4
 
5
5
  ## Overview
6
6
 
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.
7
+ The web layer covers the tooling shared across Vite + React, Astro, and Next.js. It sets lint, unit and end-to-end tests, Tailwind, screenshots, ports, and CI, and each framework stack adds its own wiring on top.
8
8
 
9
- ## What ships as golden configs
10
-
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.
9
+ ## oxlint
12
10
 
13
- ### Lint and test setup
11
+ - Run the recommended, React, and test-scoped vitest rules, with `check-file` and `simple-import-sort` as JS plugins
12
+ - Pin oxlint exactly, since its JS plugin host is alpha and a minor release could break `check-file` or `simple-import-sort`
13
+ - Keep import sorting on `simple-import-sort`, because oxfmt's built-in sort orders differently and would rewrite every import block
14
+ - Expect sync to force-replace `lint` and `lint:fix`, so a target stops running ESLint. Astro keeps ESLint, which its own reference covers.
15
+ - Delete a leftover `eslint.config.js` or `eslint.config.mjs` and `bun remove` the ESLint packages by hand after that sync, since sync deletes nothing a stack stopped shipping
14
16
 
15
- Two configs carry the lint and unit-test baseline.
17
+ ## Vitest
16
18
 
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
+ - Use jsdom, globals on, and `src/test/setup.ts`, which imports `@testing-library/jest-dom/vitest` and runs `cleanup` after each test
20
+ - Test components with `@testing-library/react` and `@testing-library/user-event`
19
21
 
20
- ### Screenshot cases
22
+ ## Playwright
21
23
 
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.
24
+ - Keep end-to-end specs in `e2e/`, running Chromium alone for a Chrome extension and every browser for a web app
23
25
 
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.
26
+ ## Tailwind
28
27
 
29
- ### Sections and masks
28
+ - Use v4 through `@tailwindcss/vite`, never the v3 integration
29
+ - Add shadcn through the sequence under `## Adding shadcn`, since a bare `init` overwrites `src/styles/global.css` and waits on prompts
30
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.
31
+ ## Adding shadcn
32
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.
33
+ 1. Copy `src/styles/global.css` aside, since init overwrites it.
34
+ 2. Confirm `global.css` starts with `@import 'tailwindcss';`, which init reads to find Tailwind v4, and that the `@/*` alias sits in both `tsconfig.json` and `vite.config.ts`.
35
+ 3. Take a preset name from `bunx shadcn@latest init --help`, since `-p` names a preset and not a primary color, and the default drifts between releases.
36
+ 4. Run `bunx shadcn@latest init -t vite -b radix -p <preset> -y </dev/null` with a timeout. The stdin redirect stops a non-TTY session from hanging on a prompt.
37
+ 5. Expect `components.json`, `src/lib/utils.ts`, and `src/components/ui/button.tsx`. Init installs the `cn` package for the helper and merges its theme into `global.css`, so reapply any copied styles.
37
38
 
38
- ### Settling before a shot
39
+ ## TypeScript and layout
39
40
 
40
- Every case settles before anything is shot.
41
+ - Keep app code in `src/`, Playwright in `e2e/`, shell in `scripts/`, and the Vitest globals in `src/test/setup.ts`
42
+ - Map the `@` alias to `./src` in both tsconfig and the framework's build config
43
+ - Keep the tsconfig at the root with `noEmit: true` in a Vite stack. Astro keeps the scaffold default from `@astrojs/check`.
44
+ - Typecheck with `tsc --noEmit`, never `tsc -b`, since composite mode emits `.js` next to `.ts` and the linter lints it
41
45
 
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.
46
+ ## Screenshot cases
45
47
 
46
- ### Run flags
48
+ - Edit the `CASES` in the seeded `e2e/capture/cases.ts`, one entry per output file naming a section, a theme, and a route, since sync overwrites the `e2e/screenshot.ts` harness
49
+ - Move cases from `e2e/screenshot.ts` to `e2e/capture/cases.ts` before a sync, and set `EVIDENCE_ROOT = 'evidence'` to keep an existing baseline
50
+ - Expect each case crossed with `VIEWPORTS`, written to `screenshots/<hostname>/<section>/<width>--<theme>.png`, so local and deployed runs stay apart
51
+ - `VIEWPORTS` holds 320, 768, 1280, and 1536. Width 320 is the base a page has to hold without wrapping or clipping.
52
+ - Set `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking
47
53
 
48
- Three controls narrow or harden a run.
54
+ ## Screenshot sections and masks
49
55
 
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.
56
+ - Set `evidence: true` to also write `<EVIDENCE_ROOT>/<section>/<width>--<theme>.png` at 320 and 1280 alone. `EVIDENCE_ROOT` defaults to `e2e/evidence`.
57
+ - Name `sections` on a case, one `{ name, selector }` per addressable part of the route, each written as its own frame beside the whole-page one
58
+ - Expect a section frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png`, plus the same path under `EVIDENCE_ROOT` where flagged
59
+ - Expect a selector matching nothing to be reported and skipped, since a seeded list may name sections the page lacks, and test that they resolve in the project owning the selectors
60
+ - List under `mask` the selectors painted over in every frame, for a value that moves with no code change, so a double capture compares the rest
54
61
 
55
- ### Scripts and workflows
62
+ ## Screenshot settling
56
63
 
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.
64
+ - Expect `reducedMotion: 'reduce'`, and each named section scrolled into view so an `IntersectionObserver` fires and `loading="lazy"` frames load before the page returns to the top
65
+ - Expect fonts, images, and same-origin frames waited out on a bounded condition, since a second `networkidle` does not re-arm for a load a scroll started
66
+ - Expect a section frame to drop floating chrome (`position: sticky` or `fixed`), while the whole-page frame is shot first and keeps its chrome
58
67
 
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`.
68
+ ## Screenshot run flags
63
69
 
64
- ## What stays in per-stack adapters
70
+ - `SCREENSHOT_FILTER` keeps cases whose `<section>/<width>--<theme>` label contains a comma-separated term, and exits 1 listing the labels on no match
71
+ - `--check-console-clean` exits 1 with the list if any `console` error fired, turning the capture into a smoke check
72
+ - `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, so a deployment script cannot capture `localhost`
73
+ - With a base URL, every flagged case is skipped so a production smoke run writes nothing to the committed path, and zero cases run exits 1
65
74
 
66
- Framework glue lives in `tooling/vite-react/configs/` or `tooling/astro/configs/`, because merge helpers and config shapes differ.
75
+ ## Screenshot evidence
67
76
 
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.
77
+ - Expect the sweep under `screenshots/` to be ignored and only a flagged case's `EVIDENCE_ROOT` output tracked, so a target's first capture is the baseline it commits
78
+ - Size a case's `sections` list against the frames each later change regenerates, since a case naming sections commits a folder per section
79
+ - Run the capture after a surface changes, component files included, per `governance/rules/ui/surface-capture.md`, unless the production build strips the component out
72
80
 
73
- ## File layout
81
+ ## Screenshot scripts and workflows
74
82
 
75
- - `src/` holds app code, `e2e/` Playwright, `scripts/` shell, and `src/test/setup.ts` the Vitest globals.
76
- - Path alias `@` maps to `./src` in both tsconfig and the framework's build config.
77
- - Tsconfig sits at root with `noEmit: true` in Vite stacks. Astro keeps the scaffold default from `@astrojs/check`.
83
+ - `scripts/screenshot.sh` sources `scripts/lib/preview-server.sh`, builds, serves on `PREVIEW_PORT`, waits for the port, and traps the whole process group on exit
84
+ - `scripts/readme-screenshot.sh` captures one case under `SCREENSHOT_FILTER` into `assets/evidence/readme/`, and exits 1 until a project fills `README_FILTER`
85
+ - `.github/workflows/readme-screenshot.yml` ships its path list as `REPLACE_WITH_PROJECT_PATHS`, and its first step exits 1 while that stays
86
+ - `.github/workflows/verify.yml` runs `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests`. `unit-tests` installs Chromium and runs `test:browser` only when the project defines that script.
87
+ - Read `governance/rules/tooling/ci-workflow.md` for when a workflow's commit-back is right and when a report is
78
88
 
79
89
  ## Ports
80
90
 
81
- Two worktrees of one repository run the same stack, so a fixed port makes the second one attach to the first.
82
-
83
- ### Deriving the port
84
-
85
- The helper owns the offset, and a config only adds it.
86
-
87
- - Derive every served port from `scripts/worktree-port.sh`. Never write a port literal into a script string.
91
+ - Derive every served port from `scripts/worktree-port.sh`, never a port literal in a script string, since two worktrees of one repository otherwise attach to one server
88
92
  - 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.
104
-
105
- ## Anti-patterns
106
-
107
- Negative knowledge that costs a failed run to relearn.
108
-
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. |
93
+ - Expect an offset from a band of 50 hashed from the worktree folder name, and set `WORKTREE_PORT_OFFSET` by hand to break a tie between two worktrees
94
+ - Set `strictPort` on every dev and preview server, and Playwright `reuseExistingServer: false`, since a walked or reused server passes against the wrong code
95
+ - Call the helper as `VAR=$(bash scripts/worktree-port.sh) && export VAR && <server>`, since an assignment prefix discards the substitution's exit and starts the server at offset zero
118
96
 
119
- ## Tool pairing
97
+ ## Ports in a removed worktree
120
98
 
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.
99
+ - Expect a non-zero exit and no port from a folder left under `.claude/worktrees/` once its worktree is gone, since git reports the parent repository from inside one
100
+ - Expect a folder whose own `.git` was deleted to refuse only under that directory, since location alone separates it from an ordinary subdirectory
101
+ - Expect a folder whose `.git` names a pruned administrative directory to refuse wherever it sits
102
+ - Set `WORKTREE_PORT_OFFSET` by hand to serve from such a folder anyway, since it is the one override and is checked before any directory test
125
103
 
126
- ## oxlint
104
+ ## lint-staged (extend)
127
105
 
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.
106
+ - Add `**/*.{js,jsx,ts,tsx}` running `oxlint --fix --max-warnings 0` and `oxfmt --no-error-on-unmatched-pattern`
107
+ - Extend the oxfmt glob to include `css`, giving `**/*.{json,css,md}`
131
108
 
132
- ## lint-staged (extend)
109
+ ## Verify script
133
110
 
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}`.
111
+ - Expect `scripts/verify.json` to require `typecheck`, `lint`, `test:run`, and `build`, which base's `scripts/verify.ts` runs in its fixed order
112
+ - That order is typecheck, lint, format, shell where the `shell` stack declares it, unit tests, build, markdown bans, then the card exclusion check
113
+ - Expect the card stage to run `scripts/check-card-exclusion.sh` whenever that file is installed, so a project with no card route runs one less stage with no override
136
114
 
137
115
  ## CI docs (extend)
138
116
 
139
- Extend `canon/context/development/ci.md` so the `## Checks` table reflects the web jobs. <!-- audit-ignore-citations: canon/context/development/ci.md -->
117
+ - Extend the `## Checks` table in `canon/context/development/ci.md` with the web jobs <!-- audit-ignore-citations: canon/context/development/ci.md -->
118
+ - Document under `## Running CI locally` that `bun run check:full` runs verify plus `test:e2e`
140
119
 
141
120
  | Check | Command | What it asserts |
142
121
  | --------- | ----------------------- | ------------------------------------------- |
@@ -146,11 +125,10 @@ Extend `canon/context/development/ci.md` so the `## Checks` table reflects the w
146
125
  | Build | `bun run build` | Production build succeeds |
147
126
  | E2E | `bun run test:e2e` | Playwright passes against the built preview |
148
127
 
149
- Under `## Running CI locally`, document that `bun run check:full` runs verify plus `test:e2e`.
150
-
151
128
  ## Development docs (extend)
152
129
 
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.
130
+ - Extend the `## Scripts` table in the `development` context entry under `canon/context/` with every web script
131
+ - Expect each framework stack to add its `dev`, `build`, `preview`, and `typecheck` rows
154
132
 
155
133
  | Command | Purpose |
156
134
  | --------------------------- | --------------------------------------- |
@@ -164,28 +142,3 @@ Extend the `development` context entry under `canon/context/` so the `## Scripts
164
142
  | `bun run screenshot` | Build, preview, capture screenshots. |
165
143
  | `bun run readme-screenshot` | Capture the README frame. |
166
144
  | `bun run smoke:prod` | Capture against a required base URL. |
167
-
168
- ### Screenshot contract
169
-
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/`.
171
-
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.
176
-
177
- ## Gitignore (extend)
178
-
179
- These `[gitignore]` groups mirror the manifest.
180
-
181
- - `"# Playwright" = ["test-results/", "playwright-report/", "blob-report/", "playwright/.cache/"]`
182
- - `"# Screenshots" = ["screenshots/"]`
183
-
184
- ## Verify script
185
-
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.
187
-
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.
@@ -0,0 +1,47 @@
1
+ import type { CaptureCase, CaptureSection } from '../capture-types'
2
+
3
+ /** Where a case flagged `evidence: true` writes its committed frames. */
4
+ export const EVIDENCE_ROOT = 'e2e/evidence'
5
+
6
+ /**
7
+ * The landing page's own beats, top to bottom. Every entry is a section a
8
+ * reviewer reads on its own, so a diff lands on the part that moved rather
9
+ * than on one image of the whole page.
10
+ */
11
+ const HOME_SECTIONS: CaptureSection[] = [
12
+ { name: 'top', selector: '#top' },
13
+ { name: 'proof', selector: '#proof' },
14
+ { name: 'ask', selector: '#ask' },
15
+ { name: 'rules', selector: '#rules' },
16
+ { name: 'skills', selector: '#skills' },
17
+ { name: 'plan', selector: '#plan' },
18
+ { name: 'dispatch', selector: '#dispatch' },
19
+ { name: 'workers', selector: '#workers' },
20
+ { name: 'gate', selector: '#gate' },
21
+ { name: 'evidence', selector: '#evidence' },
22
+ { name: 'loop', selector: '#loop' },
23
+ { name: 'merge', selector: '#merge' },
24
+ { name: 'provenance', selector: '#provenance' },
25
+ { name: 'start', selector: '#start' },
26
+ { name: 'field', selector: '#field' },
27
+ { name: 'close', selector: '#close' },
28
+ ]
29
+
30
+ export const CASES: CaptureCase[] = [
31
+ {
32
+ section: 'home',
33
+ theme: 'default',
34
+ route: '/',
35
+ evidence: true,
36
+ mask: ['.version'],
37
+ sections: HOME_SECTIONS,
38
+ },
39
+ {
40
+ section: 'home',
41
+ theme: 'dark',
42
+ route: '/',
43
+ mask: ['.version'],
44
+ sections: HOME_SECTIONS,
45
+ setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
46
+ },
47
+ ]