@erclx/canon 4.96.0 → 4.98.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 (37) hide show
  1. package/README.md +29 -27
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/codebase-layout/REQUIREMENT.md +46 -0
  4. package/claude/skills/codebase-layout/SKILL.md +57 -0
  5. package/claude/skills/codebase-layout/references/adopted.md +34 -0
  6. package/claude/skills/codebase-layout/references/frontend.md +43 -0
  7. package/claude/skills/codebase-layout/references/tests.md +28 -0
  8. package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -0
  9. package/claude/skills/draft-and-pick/SKILL.md +2 -1
  10. package/claude/skills/draft-and-pick/references/frame.html +118 -0
  11. package/claude/skills/git-pr/REQUIREMENT.md +3 -0
  12. package/claude/skills/git-pr/SKILL.md +25 -0
  13. package/claude/skills/plan-feature/REQUIREMENT.md +1 -0
  14. package/claude/skills/plan-feature/SKILL.md +2 -1
  15. package/claude/skills/test-craft/REQUIREMENT.md +9 -0
  16. package/claude/skills/test-craft/references/e2e.md +1 -1
  17. package/docs/agents/capture.md +2 -2
  18. package/docs/agents/commands.md +2 -1
  19. package/docs/agents/index.md +1 -0
  20. package/docs/agents/pr-evidence.md +9 -1
  21. package/docs/agents/pr-preview.md +103 -0
  22. package/docs/workflow/ai-workflow.md +1 -0
  23. package/governance/rules/core/000-constitution.md +1 -1
  24. package/governance/rules/lib/300-testing-ts.md +1 -0
  25. package/package.json +1 -1
  26. package/scripts/core/regen-hero.sh +33 -1
  27. package/src/claude/cases/misc.ts +5 -0
  28. package/src/commands/pr.ts +300 -13
  29. package/src/pr/evidence.ts +36 -1
  30. package/src/pr/preview.ts +178 -0
  31. package/src/slides/styles.ts +28 -3
  32. package/tooling/astro/configs/playwright.config.ts +3 -1
  33. package/tooling/cloudflare/configs/.github/workflows/deploy.yml +64 -2
  34. package/tooling/cloudflare/reference.md +6 -3
  35. package/tooling/nextjs/configs/playwright.config.ts +3 -1
  36. package/tooling/vite-react/configs/playwright.config.ts +3 -1
  37. package/tooling/web/configs/e2e/home.spec.ts +8 -0
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/brand/mark-accent.svg" alt="canon" width="56" height="56">
4
+
1
5
  # canon
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/@erclx/canon)](https://www.npmjs.com/package/@erclx/canon)
@@ -6,17 +10,13 @@
6
10
 
7
11
  canon is a CLI and Claude Code plugin that stops your AI conventions drifting apart across repositories. It keeps one authoritative copy and installs it into each project on demand.
8
12
 
9
- ![The canon catalog, listing skills, governance rules, and standards with the count each ships, the workflow skills named, and a sample of the rule and standard names](assets/evidence/hero.png)
10
-
11
- The counts above are read from the catalogs when the image is built, so they're what the repo actually ships today. See it live at [canon.erclx.dev](https://canon.erclx.dev).
12
-
13
- ## It runs on itself
13
+ **[See it run at canon.erclx.dev](https://canon.erclx.dev)**
14
14
 
15
- The workflow this toolkit ships is the workflow that built it. Several Claude Code sessions run at once, each in its own git worktree on its own branch, and each opens its own pull request.
15
+ </div>
16
16
 
17
- ![The canon landing page's session list, with the orchestrator pinned above bands of working and completed sessions, workers and planners in both, each naming what it is doing and the pull request it opened, and one row moving from Working into Completed](demos/agent-view.gif)
17
+ ![The first screen of the canon landing page, a project before and after one canon init, with the file counts each step lays down](assets/evidence/arrival.png)
18
18
 
19
- That is the landing page's own agent view, recorded from a local build by `canon demo run`. The session rows behind it are a real `canon sessions list --json` read. The activity text and the pull request number are transcribed, because no verb here reports either.
19
+ The page is one real session the toolkit ran on itself, and every count on it is read from the repo when the page builds.
20
20
 
21
21
  ## Why
22
22
 
@@ -53,35 +53,37 @@ bun install --global @erclx/canon
53
53
 
54
54
  ## What is inside
55
55
 
56
- Each domain has a canonical source in this repo and a thin install or sync CLI on your side.
56
+ Each domain has a canonical source in this repo and a thin install or sync CLI on your side. The domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
57
57
 
58
- - Claude Code plugin: skills that plan a feature, review a diff, sync the planning docs, and run the ship chain from branch through pull request
59
- - Governance rules: coding and authoring rules that load into a Claude session when a matching path is edited, installed per project and refreshed by sync
60
- - Standards: authoring conventions for commits, branches, plans, tasks, and markdown, read by name with `canon standards <name>` rather than copied into your project
61
- - Snippets: reusable prompts fired by `@` reference in a Claude Code session, resolved live from the plugin with no install step
62
- - Tooling stacks: golden configs, seeds, and a reference per framework, laid down by `canon init` and reconciled by `canon tooling sync`
63
- - Design system: a `DESIGN.md` token format, a skill that drafts one from an existing project or from scratch, and a render command
64
- - Slides: a `SLIDES.md` source format with a layout catalog, rendered to PowerPoint by `canon slides render`
65
- - Transcripts: `canon transcripts` pulls a YouTube transcript with metadata frontmatter into any repo
66
- - Sandbox: scenario-based scaffolds that provision an isolated project state for verifying each domain flow
58
+ | Domain | What it is | How it reaches you |
59
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
60
+ | Plugin skills | Skills that plan a feature, review a diff, sync the planning docs, and run the ship chain | Loaded live from the plugin, never copied |
61
+ | Governance | Coding and authoring rules that load into a Claude session when a matching path is edited | Installed per project by `canon gov install`, refreshed by sync |
62
+ | Standards | Authoring conventions for commits, branches, plans, tasks, and markdown | Opened by name, read by name with `canon standards <name>` |
63
+ | Snippets | Reusable prompts fired by `@` reference | Resolved live from the plugin |
64
+ | Tooling stacks | Golden configs, seeds, and a reference per framework | Laid down by `canon init`, reconciled by `canon tooling sync` |
65
+ | Design system | Ships a `DESIGN.md` token format, a skill that drafts one from an existing project or from scratch, and a render command | `canon design render` |
66
+ | Slides | A `SLIDES.md` source format with a layout catalog | `canon slides render` writes PowerPoint |
67
+ | Transcripts | A YouTube transcript with metadata frontmatter | `canon transcripts <url>` writes it into any repo |
68
+ | Sandbox | Scenario scaffolds that provision an isolated project state for verifying each domain flow | `canon sandbox` |
67
69
 
68
- Those domains split on one line: some are copied into your project and become yours to edit, and some are never copied at all.
70
+ ![The canon catalog as a ledger of five rows, with the count each ships and a few real names: skills, rules, standards, commands, and stacks](assets/evidence/catalog.png)
69
71
 
70
- ![canon tooling list and canon standards list side by side, the first showing five stacks with their dependency and script counts, the second showing standards against the artifact each governs](assets/evidence/install-surface.png)
72
+ Every count in that image is read from the catalogs when it is built, so they're what the repo ships today. A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A standard stays here and is opened by name, so there is no copy in your repo to drift from this one. Governance is the third shape. A rule with a path glob loads only when a matching path is edited, and a rule with none loads every session. Run `canon gov list` to see the glob beside each rule.
71
73
 
72
- A tooling stack lands as real files under version control, because a config is something your build reads and your project owns. A standard stays here and is opened by name, so there is no copy in your repo to drift from this one.
74
+ Two of the outputs are documents you open rather than files you run. A `SLIDES.md` renders to a deck, and a learning workspace renders to a small site.
73
75
 
74
- Governance is the third shape, and it is worth seeing rather than reading about, because the glob beside each rule is what decides whether it reaches a session at all.
76
+ ![A slide from the showcase deck rendered by canon slides render, a bulleted list under a heading on a dark warm background](examples/slides/images/showcase-dark-04.png)
75
77
 
76
- ![canon gov list, showing seven stacks with the rules each carries and a sample of rules beside the path glob that loads each one](assets/evidence/governance.png)
78
+ ![The learning workspace listing rendered by canon teach, three workspaces with their lesson and term counts, one still a stub](examples/teach/evidence/root-listing.png)
77
79
 
78
- Stacks compose, so a react project inherits node and node inherits base. A rule with a glob loads only when a matching path is edited, and a rule with none loads every session. Both counts and every row above are read from the catalogs when the image is built.
80
+ ## It runs on itself
79
81
 
80
- The toolkit tracks its own work the same way, in a task board no catalog can read back.
82
+ The workflow this toolkit ships is the workflow that built it. Several Claude Code sessions run at once, each in its own git worktree on its own branch, and each opens its own pull request.
81
83
 
82
- ![the task board's Run now table, one row per task in flight, each naming the files it touches and the plan it runs under](assets/evidence/task-board.png)
84
+ ![The canon landing page scrolling from the dispatch section to the branch graph, three workers and a planner on disjoint file sets with the pull request each opened](demos/agent-view.gif)
83
85
 
84
- `.canon/tasks/` is gitignored session scratch, so unlike the two frames above, this one is a hand-taken snapshot rather than something the build reads live, and it goes stale the moment the board moves.
86
+ That recording is the landing page's own `dispatch` and `workers` sections, driven by `canon demo run` against a local build. The branch graph beside the pull request numbers is authored rather than read, because nothing on a build machine records which files four sessions held.
85
87
 
86
88
  ## Documentation
87
89
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.96.0",
4
+ "version": "4.98.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: codebase-layout
3
+ description: Why the session writing a new path into a plan needs placement guidance at that moment, and where that guidance stops short of test layers and module interfaces
4
+ ---
5
+
6
+ # Codebase layout requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, the session that decides where a new file goes has nothing about layout in front of it when it decides. The planner writes each new path into the plan, the worker copies it, and the planner's only instruction on the tree is to measure against it, which for a new path reads as copying the flat folder beside it.
11
+
12
+ The failure is measured on a real target. Two source folders grew from 11 files to 46 and 36 flat files in six days, with the constitution's directory line and a stack rule mandating feature folders both installed throughout. The split that followed touched 98 files, and the end to end folder is flat again, four specs beside five helpers. Archived plans from the same project name every new file as a literal flat path.
13
+
14
+ Sharpening the rules does not reach the decision. Both were loaded and neither was applied, since an always-loaded line competes with everything else a session holds and a path-scoped rule fires on a read of an existing file rather than on a path written into a plan.
15
+
16
+ ## Must
17
+
18
+ - Be loaded by `plan-feature` whenever a plan names a path that does not exist yet, so the guidance is in front of the planner at the moment it writes the path
19
+ - Tell the session to name and keep to the project's existing layout strategy before placing a file, since two strategies in one tree cost every reader a guess
20
+ - State the split trigger as a second role entering a folder, with a file count only as a prompt to look
21
+ - State colocation with the one consumer and promotion on the second, and imports flowing from shared toward features toward the app entry
22
+ - Require a one-clause placement reason on every new path the plan names
23
+ - Hold the stack-neutral core in the body and defer category specifics to a reference loaded only when a new path falls in that category
24
+ - Record the external layouts adopted and declined with the reason for each, so a later session extends the position instead of re-deriving it
25
+ - Fire on a direct question about placement as well, such as where a file goes or whether a flat folder should split, since a session outside any plan still places files
26
+
27
+ ## Must not
28
+
29
+ - Name a framework or runner in the body. Stack specifics live in the references.
30
+ - Carry a hard file-count threshold. A command folder holding sixty files of one role reads fine flat, and a number would fail it.
31
+ - Restate the layer rule, the layer table, or the final filter `test-craft` owns
32
+ - Mandate one layout across projects, such as a `features/` tree, over the project's own strategy
33
+ - Restate the constitution's directory line. The rule points here and this body carries the depth.
34
+
35
+ ## Guards
36
+
37
+ - A plan that only edits existing files does not load the skill, so the cost lands only where a placement is decided
38
+ - A strategy change is proposed as its own decision rather than made while placing one file
39
+
40
+ ## Out of scope
41
+
42
+ - Which layer a test belongs at: `test-craft`
43
+ - Interface depth, seams, and where a module boundary sits in code, which govern a module's shape rather than its path on disk
44
+ - Per-language placement conventions such as test suffixes and the top-level test folder: the language testing rules
45
+ - Backend, asset, and script layouts, which wait for a driving project before a reference is written
46
+ - Whether a plan that loaded the skill actually places files differently, which needs a measured run on a target rather than a rule here
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: codebase-layout
3
+ description: Carries the rules that decide where a new file goes, when a folder splits, and when a file moves from its one consumer to shared code, so a plan places each new path by role and feature with a one-clause reason rather than beside its nearest neighbor. Use when a plan names a file or folder that does not exist yet, when `plan-feature` reaches its placement step, or when asked "where should this file go", "this folder is flat", "restructure this folder", or "should this be its own folder". Do NOT use to choose which layer a test belongs at, which is `test-craft`, or to shape a module's interface and seams.
4
+ ---
5
+
6
+ # Codebase layout
7
+
8
+ A session placing a new file copies the folder next to it, so a flat folder stays flat until a refactor pays to split it. In one target, two source folders grew to 46 and 36 flat files under installed rules against it, and the split cost a pull request touching 98 files. This skill carries the placement call to the moment the path is written, which is the one moment it is cheap.
9
+
10
+ Load it before writing a new path into a plan, not after. A worker copies the path the plan gives it, so the placement is settled once the plan is.
11
+
12
+ Skip it when every path the work names already exists.
13
+
14
+ ## Read the project's strategy first
15
+
16
+ - Name the layout the project already uses before placing anything: by feature, by route, by role, or flat. Look at the folders a new file would sit beside and at any stated rule on layout.
17
+ - Keep to that strategy. A second strategy beside the first costs every later reader a guess about which one a file follows.
18
+ - Propose a strategy change as its own decision, never as a side effect of placing one file.
19
+
20
+ ## Group what changes together
21
+
22
+ - Put files that change for the same reason in one folder, and separate files that change for different reasons.
23
+ - Name a folder for what it holds for a reader, being a feature, a route, or a role, over its technical kind alone. `checkout/` tells a reader what is inside where `helpers/` does not.
24
+ - Never create a bucket named `misc`, `common`, `utils`, or `helpers` on its own. Qualify it by what it serves, or place the file with its consumer.
25
+
26
+ ## Split before the file lands
27
+
28
+ - Split a folder when the new file brings a second role into it, before the file lands rather than in a later refactor. The split that touched 98 files would have touched a handful when the folder held eleven.
29
+ - Treat roughly ten files as a prompt to look, not a gate. A folder holding one role can stay flat at any size, and a folder mixing two roles is worth splitting at six.
30
+ - Give a new role a named folder in the plan, rather than dropping its first file beside the nearest neighbor.
31
+
32
+ ## Place a file with its consumer
33
+
34
+ - Colocate a new file with the one module that uses it.
35
+ - Promote it to shared code when a second consumer arrives, and move it in the change that adds that consumer.
36
+ - Keep imports flowing one way, from shared code toward features toward the app entry. A shared module importing from a feature, or one feature importing another, names a file in the wrong place.
37
+
38
+ ## State the reason on every new path
39
+
40
+ - Give each new path in a plan a one-clause reason for where it sits: the role it holds, the consumer it serves, or the strategy it follows.
41
+ - Name a new folder in the plan entry that creates it, so the worker creates the folder rather than inferring it.
42
+
43
+ ## Read the reference for the category
44
+
45
+ Read the reference matching what the new paths hold, and skip the rest.
46
+
47
+ - Application source for an interface: `${CLAUDE_SKILL_DIR}/references/frontend.md`
48
+ - Test files of any layer: `${CLAUDE_SKILL_DIR}/references/tests.md`
49
+
50
+ Read `${CLAUDE_SKILL_DIR}/references/adopted.md` only when extending this guidance or arguing against a rule in it. It records which external layouts were adopted, which were declined, and why.
51
+
52
+ ## What this delegates
53
+
54
+ - Which layer a test belongs at: `test-craft`. This skill decides where the test file sits once its layer is chosen.
55
+ - Where a new path gets written into a plan, and the plan's shape: `plan-feature` and the plan standard
56
+ - The directory principle every session loads: `000-constitution`
57
+ - Runner, suffix, and top-level test folder conventions per language: `300-testing-ts` and `330-testing-py`
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Adopted and declined
3
+ description: Which external layout patterns this skill adopted, which it declined, and why, so a later session extends the position instead of re-deriving it
4
+ ---
5
+
6
+ # Adopted and declined
7
+
8
+ External sources were read and filtered rather than imported. This file is the record. A later session extending the skill adds to it rather than re-arguing an item already settled here.
9
+
10
+ ## Adopted
11
+
12
+ **Group what changes together.** Robert Martin's restatement of the single responsibility principle: "Gather together the things that change for the same reasons. Separate those things that change for different reasons" ([The Single Responsibility Principle](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html)). It gives "split by role" a test a planner can apply to a folder: would these files change together?
13
+
14
+ **Name folders for the domain rather than the framework.** Martin's Screaming Architecture argues the top level should tell readers "about the system, not about the frameworks you used in your system" ([Screaming Architecture](https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html)). It is the root of naming a folder for what it holds rather than its technical kind.
15
+
16
+ **Feature folders, no cross-feature imports, one-way flow.** bulletproof-react puts most code under per-feature folders holding only the subfolders each needs, forbids one feature importing another, and enforces a shared, then features, then app direction with an import-path lint rule ([project structure](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md)). Adopted as the direction rule in the body and the feature-folder guidance in `frontend.md`, without the mandate.
17
+
18
+ **Grow in levels and promote on the second consumer.** Robin Wieruch grows a project from one file to feature folders, each level triggered by a named event rather than a count, and moves a helper to the shared layer once two or more features need it ([React folder structure](https://www.robinwieruch.de/react-folder-structure/)). Adopted as the promotion rule in the body and the levels in `frontend.md`.
19
+
20
+ **Consistency over any one strategy.** The Next.js docs call the framework unopinionated about organization, list three strategies, and ask a project to "choose a strategy that works for you and your team and be consistent across the project" ([project structure](https://nextjs.org/docs/app/getting-started/project-structure)). Adopted as the body's first section, reading and keeping the project's own strategy.
21
+
22
+ **Fixtures in modules, page objects on their own.** Playwright defines fixtures in dedicated modules that extend the base test, merges them into one module the specs import, and keeps page objects as their own modules ([fixtures](https://playwright.dev/docs/test-fixtures)). Adopted in `tests.md` as the split between specs and support.
23
+
24
+ ## Declined
25
+
26
+ **Deep modules and seams.** Matt Pocock's codebase-design skill covers a lot of behavior behind a small interface placed at a clean seam ([codebase-design](https://github.com/mattpocock/skills/blob/main/skills/engineering/codebase-design/SKILL.md)). It answers where a boundary goes in code, not where a file goes on disk, and absorbing it doubles the skill's scope before the placement problem is shown solved. It becomes a reference only when a plan places files well and still draws a boundary badly.
27
+
28
+ **A mandated `features/` tree.** bulletproof-react's layout, and a stack rule some projects install, put domain code under `src/features/`. Declined as a mandate because it conflicts with keeping the project's own strategy, and a project that never adopted feature folders gains a second strategy rather than a better one. A stack rule mandating the tree still loads beside this skill in a project that installs it, and the two give different answers there until that rule is softened on its own.
29
+
30
+ **A numeric file ceiling.** None of the sources gives a reliable number, and a count fails a folder holding sixty files of one role that reads fine flat. The body triggers on a second role and treats roughly ten files as a prompt to look.
31
+
32
+ **A required placement field in the plan format.** It would touch every plan, including the ones that create nothing. The body asks for a one-clause reason on each new path instead, and the field is worth revisiting only if plans load the skill and still list flat paths.
33
+
34
+ **Loading the skill in the worker.** The worker copies the path the plan gives it, so placement is already decided by the time a worker would read this.
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Frontend layout
3
+ description: Where interface source goes, being feature folders against shared UI, routes and their private folders, hooks and helpers split by consumer, fixtures by feature, and the promotion rule, with one worked failure
4
+ ---
5
+
6
+ # Frontend layout
7
+
8
+ Read this before placing a new component, hook, helper, route, or fixture in an interface project. The body's rules hold here unchanged, and this file says how they land on the folders an interface project grows.
9
+
10
+ ## Feature folders against shared UI
11
+
12
+ - Put code that serves one feature under that feature's folder, holding only the subfolders it needs: its components, its hooks, its data access, its types ([bulletproof-react](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md)).
13
+ - Keep the shared `components/` folder for pieces two or more features render, such as a button, a dialog, or a layout shell. A component only one feature renders is feature code, even when it looks generic.
14
+ - Never import one feature from another. Lift the piece both need to shared code, or compose the two features at the app or route level ([bulletproof-react](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md)).
15
+ - Enforce the flow from shared toward features toward the app where the project already lints imports. An import-path restriction turns the direction into a check rather than a habit.
16
+
17
+ ## Grow in levels
18
+
19
+ - Start flat. A small project with one screen needs no feature folders, and adding them early is structure nobody reads.
20
+ - Move to the next level on a named event rather than a count: a second feature, a second role in `components/`, a helper only one feature uses ([Robin Wieruch](https://www.robinwieruch.de/react-folder-structure/)).
21
+ - Take one level per change. A plan that jumps from flat to a full feature tree rewrites every import in the project.
22
+
23
+ ## Routes
24
+
25
+ - Follow the router's own conventions where the framework ships file-based routing, and treat everything outside them as project strategy.
26
+ - A framework calling itself unopinionated about organization leaves the strategy to the project. Pick one of keeping code outside the routes folder, top-level folders inside it, or splitting by feature or route, and "be consistent across the project" ([Next.js project structure](https://nextjs.org/docs/app/getting-started/project-structure)).
27
+ - Colocate a component only one route renders beside that route, in a folder the router excludes from routing, such as a private `_components` folder in a router that supports one.
28
+ - Treat `components` and `lib` as placeholder names with no framework meaning. Their contents still follow the body's rules.
29
+
30
+ ## Hooks and helpers
31
+
32
+ - Put a hook or helper one feature uses inside that feature. Promote it to a shared `hooks/` or `lib/` folder when a second feature needs it, and move it in the change that adds the second consumer ([Robin Wieruch](https://www.robinwieruch.de/react-folder-structure/)).
33
+ - Split a shared `lib/` by what each file serves once it holds two roles, such as data access beside formatting beside browser APIs. A flat `lib/` is the second folder to overflow after `components/`.
34
+ - Name a helper file for what it does rather than `utils` or `helpers`, so the folder listing reads as an index.
35
+
36
+ ## Fixtures and sample data
37
+
38
+ - Keep fixtures and sample data beside the feature they describe, and name the file for that feature.
39
+ - Promote a fixture to a shared folder only when two features' tests build from it.
40
+
41
+ ## A worked failure
42
+
43
+ An interface project's `components/` folder held 11 files when its first plan landed and 46 six days later, with its `lib/` at 36, all flat. Every plan in between named each new file as a sibling of the last, since the folder it was measured against was flat. The installed rules said to split by role and to keep domain logic in feature folders, and neither was applied. The split, when it came, touched 98 files, and the project's end to end folder has since grown flat again, four specs beside five helpers. Placing the twelfth component in a named subfolder would have cost that plan one folder.
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: Test layout
3
+ description: Where test files live once their layer is chosen, being unit tests beside their module and an end to end folder split into specs, a fixtures module, page objects, and support, with one worked failure
4
+ ---
5
+
6
+ # Test layout
7
+
8
+ Read this before placing a new test file or a file a test imports. Which layer the test belongs at is settled first, by `test-craft`. This file starts from that answer and says where the file goes.
9
+
10
+ ## Unit and component tests
11
+
12
+ - Put a unit or component test beside the module it tests, named after it, so a reader opening the module finds its test in the same listing.
13
+ - Move the test with the module. A test left behind in a folder its module has left is the first sign the folder no longer holds one role.
14
+ - Keep a factory or builder one test file uses inside that file. Move it beside the tests that share it once a second file needs it, and name it for what it builds.
15
+
16
+ ## End to end tests
17
+
18
+ A browser suite starts as a folder of specs, and every helper written after the first lands beside them unless something says otherwise. Split the folder by role before the first support file lands.
19
+
20
+ - Keep specs in the folder the runner reads, and nothing there but specs once support code exists.
21
+ - Put fixtures in a dedicated module that extends the runner's base test, and merge several into one fixtures module the specs import ([Playwright fixtures](https://playwright.dev/docs/test-fixtures)). A spec importing its setup from one place is how a new spec finds what exists.
22
+ - Give each page object its own module, named for the page or region it drives ([Playwright fixtures](https://playwright.dev/docs/test-fixtures)).
23
+ - Put helpers, stubs, and seed data in a support folder beside the specs rather than among them. Name each file for what it serves, never a single `helpers` file every spec imports.
24
+ - Group specs by journey once they mix, such as checkout specs in one folder and account specs in another. A folder of a handful of specs covering one journey stays flat.
25
+
26
+ ## A worked failure
27
+
28
+ An interface project's end to end folder held four specs beside five helper files, all flat, after an earlier refactor had split its source folders by role. The project's plans named every new file as a literal flat path, so each helper landed as a sibling of the specs it served. A new spec then had no single place to find its setup, and the next helper went beside the others. One plan naming a support folder when the first helper arrived would have kept the specs as the folder's only role.
@@ -28,6 +28,7 @@ Without this skill, a session facing a decision nobody can settle from a diff:
28
28
  - Names no layer at all, since the instruction to name one carries no catalog to name from and nothing reports the omission. Four consecutive rounds against one page each varied a property at the top layer while the defect sat two below it, and three of the four changed nothing the operator could see.
29
29
  - Judges a lower layer through a finished higher one, so an operator asked about composition looks at the palette instead and answers about that.
30
30
  - Hands over the combined page alone for a decision about layout, where every arm sits in a column while the media queries answer to the whole window, so no arm is ever seen at its own viewport and a fault that appears only narrower survives the pick.
31
+ - Rebuilds a navigation harness by hand on a run with several rounds, since each round writes its own folder and nothing reaches all of them from one address. Two design tracks hand-wrote the same page picker and theme toggle.
31
32
  - Sets the theme on an arm that persists its own and reads it back on load, so the control reports one theme while the arm renders the other and the set is compared across two.
32
33
 
33
34
  ## Must
@@ -38,6 +39,7 @@ Without this skill, a session facing a decision nobody can settle from a diff:
38
39
  - Grey-box the set when the declared layer sits below typography, and say the set is grey-boxed when handing it over, so the operator reads a flat page as the question rather than as unfinished work.
39
40
  - Render every arm in one theme at a time, with a control that sets the whole set, so the comparison holds still while the operator reads it, and set the theme in a way that survives an arm reading its own stored preference back on load.
40
41
  - Hand over the per-arm file addresses beside the combined page where the declared layer is composition, layout or space, since only a whole-page file at a real viewport answers how an arm behaves at a width, and the browser's own device toolbar is the control.
42
+ - Write a frame from `references/frame.html` where the run has several rounds, so a picker reaches every round from one address. The frame carries no width control of its own.
41
43
  - Judge each render against a stated bar before handing it over, name the weakest thing on the page, and fix it where that sentence would embarrass the work.
42
44
  - Write each iteration to its own folder rather than narrowing the previous one in place, so every earlier round stays openable.
43
45
  - Write the run's renders inside the record that cites them wherever one exists, and reserve the scratch path for inputs that are re-runnable and cited by nothing.
@@ -31,6 +31,7 @@ Write every arm side by side on one self-contained HTML page at `<dest>/candidat
31
31
  - Label each arm on the page with its id and its cost, so the render carries what the question will ask about.
32
32
  - Give the combined page one control that sets every arm's theme at once, beside whatever per-arm control the arms carry. A set spanning both themes cannot be compared, since the operator has to toggle each arm and hold the earlier ones in memory, which is the failure the single-page rule exists to prevent.
33
33
  - Set that theme so it survives the arm reading its own preference back. An arm that stores a theme and applies it on load will overwrite whatever the control set, which shows the control in one state and the arm in the other, and it passes a fresh browser profile because the stored value is not there yet.
34
+ - Write `<dest>/frame.html` from `${CLAUDE_SKILL_DIR}/references/frame.html` where the run has more than one round, and hand over its address beside the combined page. Replace the template's one data token with the JSON object its comment describes, listing the combined page and every per-arm file of this round and each earlier one, with each `src` relative to the frame and `themeKey` set to the key the arms read their theme from. The frame is the route to every round from one address, with a page picker and the global theme toggle. Earlier rounds are sibling folders, so serve the parent folder with `canon serve <parent> --entry <dest-name>/frame.html` rather than `<dest>`, and keep the per-arm files as the fallback where nothing is served.
34
35
  - Hand over the per-arm file addresses beside the combined page where the declared layer is composition, layout or space, and say that an arm is judged at a width by opening its own file in the browser's device toolbar. The combined page cannot answer that question at all: every arm sits in a column there while the media queries answer to the whole window, so each one renders its widest layout in a narrow space no matter how the window is sized. Build no width control of your own, since the browser already has one.
35
36
  - Take the live-app branch instead when the surface under decision is a running app: lift the rendered markup and link a copy of the built stylesheet rather than inlining, per `${CLAUDE_SKILL_DIR}/references/live-arms.md`.
36
37
  - On the default path, inline every style, script, and asset the page needs. The render reads the file off disk, so a page reaching for a build step or a network font renders without it and the arms differ by something nobody chose.
@@ -70,7 +71,7 @@ Put the choice to the operator through the structured question surface, since a
70
71
  ## Step 5: loop on the pick
71
72
 
72
73
  1. Write each iteration to its own `<dest>` rather than narrowing the previous one in place, suffixing the slug so the folders sort. An iteration overwritten is one a later pass cannot open, and the losing round is what stops a correction re-proposing something already rejected.
73
- 2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not.
74
+ 2. Write fresh arms off the pick and return to Step 2 where the correction opens a new question. Revise the one arm where it does not. Each pass through Step 2 rewrites the frame with every earlier round's arms, since the list is baked in when the frame is written.
74
75
  3. Re-render, hand off again, and take the next answer.
75
76
  4. Repeat until the operator says it is right. The loop stops on their word and on nothing else, so a run stopping because the arms stopped differing has stopped early.
76
77
  5. Hold the real surface untouched across every iteration. Nothing outside the run's own folders changes until the pick is final.
@@ -0,0 +1,118 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Draft and pick frame</title>
6
+ <style>
7
+ :root {
8
+ --bg: #f4f4f5;
9
+ --fg: #18181b;
10
+ --bar: #ffffff;
11
+ --line: #d4d4d8;
12
+ }
13
+ :root[data-theme='dark'] {
14
+ --bg: #18181b;
15
+ --fg: #f4f4f5;
16
+ --bar: #27272a;
17
+ --line: #3f3f46;
18
+ }
19
+ * {
20
+ box-sizing: border-box;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ background: var(--bg);
28
+ color: var(--fg);
29
+ font-family: system-ui, sans-serif;
30
+ font-size: 14px;
31
+ }
32
+ header {
33
+ display: flex;
34
+ gap: 12px;
35
+ align-items: center;
36
+ padding: 8px 12px;
37
+ background: var(--bar);
38
+ border-bottom: 1px solid var(--line);
39
+ }
40
+ select,
41
+ button {
42
+ font: inherit;
43
+ color: inherit;
44
+ background: var(--bg);
45
+ border: 1px solid var(--line);
46
+ border-radius: 4px;
47
+ padding: 4px 8px;
48
+ }
49
+ main {
50
+ flex: 1;
51
+ overflow: auto;
52
+ display: flex;
53
+ justify-content: center;
54
+ padding: 12px;
55
+ }
56
+ iframe {
57
+ width: 100%;
58
+ height: 100%;
59
+ border: 1px solid var(--line);
60
+ background: #fff;
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <header>
66
+ <label
67
+ >Page
68
+ <select id="picker"></select
69
+ ></label>
70
+ <button id="theme" type="button">Theme: light</button>
71
+ <span>F12 device toolbar to resize</span>
72
+ </header>
73
+ <main><iframe id="page" title="Candidate page"></iframe></main>
74
+ <!-- The skill replaces the token below with one JSON object: {"themeKey": "<key the arms read>", "rounds": [{"label": "...", "pages": [{"label": "...", "src": "<path relative to this file>"}]}]} -->
75
+ <script type="application/json" id="frame-data">
76
+ __FRAME_DATA__
77
+ </script>
78
+ <script>
79
+ const data = JSON.parse(document.getElementById('frame-data').textContent)
80
+ const picker = document.getElementById('picker')
81
+ const themeButton = document.getElementById('theme')
82
+ const frame = document.getElementById('page')
83
+ let theme = 'light'
84
+
85
+ for (const round of data.rounds) {
86
+ for (const page of round.pages) {
87
+ const option = document.createElement('option')
88
+ option.value = page.src
89
+ option.textContent = round.label + ' · ' + page.label
90
+ picker.append(option)
91
+ }
92
+ }
93
+
94
+ // A page that stores its own theme and reads it back on load overwrites the
95
+ // attribute alone, so write the stored key as well as the attribute.
96
+ function applyTheme() {
97
+ document.documentElement.dataset.theme = theme
98
+ themeButton.textContent = 'Theme: ' + theme
99
+ try {
100
+ const win = frame.contentWindow
101
+ win.document.documentElement.setAttribute('data-theme', theme)
102
+ win.localStorage.setItem(data.themeKey, theme)
103
+ } catch {}
104
+ }
105
+
106
+ picker.addEventListener('change', () => {
107
+ frame.src = picker.value
108
+ })
109
+ themeButton.addEventListener('click', () => {
110
+ theme = theme === 'light' ? 'dark' : 'light'
111
+ applyTheme()
112
+ })
113
+ frame.addEventListener('load', applyTheme)
114
+
115
+ if (picker.options.length > 0) frame.src = picker.value
116
+ </script>
117
+ </body>
118
+ </html>
@@ -26,6 +26,8 @@ A lookup that resolves by head branch alone carries its own failure. A branch na
26
26
  - Label from the paths the branch changed, against a map the project declares, so the label set belongs to the project rather than to the skill
27
27
  - Apply labels after the pull request exists, so a label the remote does not carry costs a warning rather than the pull request
28
28
  - Report a refused label, since a warning nothing surfaces leaves the run indistinguishable from one that labelled
29
+ - Put a preview address on a pull request that changes a rendered surface when the project's deploy can mint one, since screenshots and a checklist leave a reviewer nothing to click into before merging
30
+ - Bound the wait on that preview and finish the chain without the link on a timeout, since a slow deploy must not hold the ship
29
31
 
30
32
  ## Must not
31
33
 
@@ -35,6 +37,7 @@ A lookup that resolves by head branch alone carries its own failure. A branch na
35
37
  - Edit a pull request that is not open, or record its number on a task
36
38
  - Name a domain of any one project in the skill body or its references
37
39
  - Create a label the map names and the remote lacks
40
+ - Dispatch a deploy whose command names no branch, since Cloudflare Pages publishes such a deploy to production
38
41
  - Emit anything after the result line
39
42
 
40
43
  ## Guards
@@ -197,6 +197,27 @@ Clean up the tmp file the way the UI-checklist step does, only after the call re
197
197
 
198
198
  Any other `reason` is one of the mirrored git refusals (`gh-missing`, `gh-failed`, `no-base`, `unreadable-tree`, `unreadable-changes`). Report it and move on without stopping the chain: a branch that carries no evidence images most of the time should not fail here on a transient git or `gh` read.
199
199
 
200
+ ### Post the preview address
201
+
202
+ Run this step only when the evidence step above returned `ok` or the UI checklist step posted a comment. Either one means the pull request changes a rendered surface, and a reviewer holding a checklist with no screenshots needs the live page most. Otherwise skip it silently.
203
+
204
+ The evidence comment is already posted, so the reviewer has the screenshots while the deploy runs. Mint the preview against the same `<number>`:
205
+
206
+ ```bash
207
+ canon pr preview <number> --json
208
+ ```
209
+
210
+ The verb dispatches the project's Pages deploy on the pull request's branch and waits on the run for up to 15 minutes. It refuses before dispatching anything when the deploy command passes no `--branch`, since that deploy would publish the branch to production. `canon docs pr-preview` states the contract. Read `reason` on the record rather than the exit code.
211
+
212
+ - `ok`: re-render the evidence body with the address on its first line, then post or update it exactly as the evidence step does, through the same tmp file and the `commentId` the record carries:
213
+
214
+ ```bash
215
+ canon pr evidence <number> --preview <url> --json
216
+ ```
217
+
218
+ - `no-deploy`: the project deploys nothing a dispatch can start. Say nothing and move on.
219
+ - Any other `reason`, being `unfenced`, `no-alias`, `run-failed`, `timeout`, or a mirrored `gh` refusal: report it with the record's `message` and move on without stopping the chain. A missing link costs the reviewer a click, and a held ship costs the whole chain.
220
+
200
221
  ### Record the number on the task
201
222
 
202
223
  Write the `number` the final command printed onto the task the branch is closing. Do not resolve it again. `${CLAUDE_SKILL_DIR}/REQUIREMENT.md` states why: a lookup that resolves by branch alone can return a closed pull request sharing that head, so the number is resolved once and reused rather than re-derived.
@@ -233,4 +254,8 @@ Add a line when the UI checklist step posted a comment, naming the pull request
233
254
 
234
255
  `📋 Posted the UI checklist to <number>.`
235
256
 
257
+ Add a line when the preview step returned an address:
258
+
259
+ `🔗 Preview: <url>`
260
+
236
261
  Do not add any other text.
@@ -13,6 +13,7 @@ Without this skill, implementation starts before anyone knows what it touches. A
13
13
 
14
14
  - Read the project's own Claude setup before scanning source, so the plan inherits decisions already made instead of reopening them
15
15
  - Name every file the work touches with the reason it is touched
16
+ - Place each new path against the layout guidance with a one-clause reason, loading that guidance only when the plan creates a file or folder, since the planner decides placement and the worker copies whatever path it is given
16
17
  - Surface each unresolved ambiguity as a numbered question carrying a suggested answer and an empty answer slot, so the plan is decision-ready in one pass
17
18
  - State which act a constraint forbids when it names a surface to leave alone, since conforming that surface and retargeting a pointer into it are different acts and only one is out of scope
18
19
  - Scale the output to the work. A two-file change with nothing to decide should not produce a plan file.
@@ -33,7 +33,7 @@ Coding standards live in `.claude/rules/`. Claude Code loads them automatically.
33
33
 
34
34
  Based on the feature description, identify and read source files that are directly relevant. Do not read entire directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
35
35
 
36
- Measure against the tree rather than recall. Grep for each construct the plan will name and count the sites, so the plan carries the count the tree holds today. Confirm any work the plan sequences behind is still open, so it does not lead with an item that already shipped.
36
+ Measure against the tree rather than recall. Grep for each construct the plan will name and count the sites, so the plan carries the count the tree holds today. The tree tells a new path what exists beside it, not where it belongs, so a flat folder is a reading to place against rather than a pattern to copy. Confirm any work the plan sequences behind is still open, so it does not lead with an item that already shipped.
37
37
 
38
38
  Open each file before describing what is in it. A count or a claim carried from an earlier session, a summary, or another document is the most common way a plan ships the wrong scope.
39
39
 
@@ -45,6 +45,7 @@ What this skill adds on top of the standard:
45
45
 
46
46
  - Apply senior judgment to every `- Suggested:` line. Pick the best option and state it in one line with its reason or main tradeoff. No padding, no alternatives unless they change the pick.
47
47
  - Suggest a real default when best practice, the codebase, or prior context points to one.
48
+ - Load the `canon:codebase-layout` skill before writing the `**Files to touch:**` entries when any of them names a file or folder that does not exist yet, and give each new path its placement reason in its entry. Skip the load when every entry edits an existing file. Report it rather than proceeding silently when the skill does not resolve.
48
49
  - Prefer `None identified.` over low-signal fillers. A small feature should produce a short plan, not a padded one.
49
50
  - When three or more questions remain, keep chat output to the file pointer plus a short summary. Inline chat is fine when two or fewer remain.
50
51
 
@@ -52,3 +52,12 @@ Moving the guidance into a path-scoped rule does not reach the session that need
52
52
  - Writing a UI change's tests and its visual checklist after implementation: `ui-test`
53
53
  - Grading the tests a pull request adds: `review-pr`
54
54
  - Whether the guidance moves where a session puts a test, which needs a measured with-and-without run rather than a rule here
55
+
56
+ ## Measured
57
+
58
+ 2026-09-19, one Sonnet run per arm over the `pull` arm of the toolkit's own sandbox scenario for this skill, a page with an end to end spec already beside it and a prompt adding a loading state.
59
+
60
+ - With the skill: the loading state landed in a new component test and the existing end to end spec stayed unchanged.
61
+ - Without it, on a plugin dir lacking the skill: no `Skill` call fired, and the run made the same placement, one component test with three cases and the spec unchanged.
62
+
63
+ The arm did not discriminate, so the run shows no movement from the skill. It also shows that the pull toward extending an existing spec is too weak to move an unaided session, which is a fixture finding and not evidence about the body. A stronger pull, such as a prompt naming the spec, is the next arm to try before reading this as a rework signal on the skill. One run per arm on one model cannot separate a difference of one test from noise.
@@ -28,7 +28,7 @@ Suites dominated by end to end tests inflate both runtime and flake ([Google Tes
28
28
  ## Motion off by default
29
29
 
30
30
  - Run with motion reduced unless the test asserts motion, and opt back in per test with `page.emulateMedia({ reducedMotion: 'no-preference' })`.
31
- - Set it once in the config's `use` block as `reducedMotion: 'reduce'` rather than per test, since the option defaults to `'no-preference'` ([TestOptions](https://playwright.dev/docs/api/class-testoptions)). A site honoring the reduced preference then has nothing to wait out in any test that does not opt back in.
31
+ - Set it once in the config's `use` block as `contextOptions: { reducedMotion: 'reduce' }` rather than per test, since the option defaults to `'no-preference'` ([BrowserContext options](https://playwright.dev/docs/api/class-browser#browser-new-context)). A top-level `use.reducedMotion` reaches no page and type-checks nowhere, so a spec asserting the media query is what catches it. A site honoring the reduced preference then has nothing to wait out in any test that does not opt back in.
32
32
 
33
33
  ## Isolation and speed
34
34