@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
@@ -18,9 +18,9 @@ canon capture https://example.com --selector .window --out preview.png
18
18
 
19
19
  ## What this repository captures
20
20
 
21
- A capture set here spans two folders. `assets/captures/` holds five sources and the template each is written from, and `assets/evidence/` holds the five images a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the five is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them, and `bun run check` regenerates them and fails on the difference.
21
+ A capture set here spans two folders. `assets/captures/` holds three sources and the template each is written from, and `assets/evidence/` holds their images and the built-page capture a document points at with the stamp answering for each, under the segment `canon pr evidence` compares. One run over the source folder rebuilds every image, and it takes `--out assets/evidence` to put each PNG where its document points rather than beside the markup it rendered. None of the three sources is edited by hand. `scripts/core/regen-hero.sh` writes each `.html` from the template beside it, filling one shared value map into all of them, and `bun run check` regenerates them and fails on the difference.
22
22
 
23
- Three of the five take catalog data, so a stack gaining a rule moves the frame on the next run. `install.html` and `task-board.html` are the two exceptions: the first holds terminal text from a real run and the second holds a hand-frozen snapshot of a gitignored board, each in its template rather than derived from a live catalog at build time.
23
+ Two of the three take catalog data, so a stack gaining a rule moves the frame on the next run. `install.html` is the exception, since it holds terminal text from a real run in its template rather than derived from a live catalog at build time. `assets/evidence/arrival.png` is the one image with no source here: it is the built page's `.fold` element captured in dark mode against a local `web/` preview, since `canon capture` takes no color scheme and the page follows the browser's default of light, so it is retaken by hand when the page's first screen changes.
24
24
 
25
25
  `assets/captures/` is read flat and never descends, by the regeneration script, by this command, and by the drift stage alike. A source nested a further folder down is skipped by all three with nothing reported, so a new frame is a template dropped directly in rather than a folder of its own. The split is what retired the name prefix the flat layout used to need: a frame is named for itself now, since `assets/captures/` no longer mixes markup in with the images.
26
26
 
@@ -94,7 +94,8 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
94
94
  | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
95
95
  | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
96
96
  | `canon pr review-state` | Report the commit and instant the last review pass covered, off the pass's own marker rather than the fields GitHub stamps at submission (`--root`, `--json`) |
97
- | `canon pr evidence` | Render a before-and-after comment comparing changed `evidence/` images against the merge base, editing its own prior comment in place rather than posting a second (`--root`, `--json`) |
97
+ | `canon pr evidence` | Render a before-and-after comment comparing changed `evidence/` images against the merge base, editing its own prior comment in place rather than posting a second (`--root`, `--json`, `--preview`) |
98
+ | `canon pr preview` | Dispatch the Pages deploy on a pull request's branch and report the preview address the run prints, refusing a deploy that passes no `--branch` (`--root`, `--json`, `--timeout`) |
98
99
  | `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
99
100
  | `canon repo metadata apply` | Write an explicitly supplied description, homepage, or topic set to the remote through `gh repo edit` (`--description`, `--homepage`, `--topics`, `--root`, `--json`) |
100
101
  | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
@@ -31,6 +31,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
31
31
  - [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
32
32
  - [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
33
33
  - [The pull request evidence comparison](pr-evidence.md): What canon pr evidence compares, the marker that lets it edit its own comment rather than duplicate it, the refusal reasons it names, and why the comparison anchors at the merge base rather than the previous push
34
+ - [The pull request preview deploy](pr-preview.md): How canon pr preview finds a fenced Cloudflare Pages deploy, dispatches it on a pull request's branch, reads the alias the run prints, the refusal reasons it names, and why an unfenced deploy is refused before anything runs
34
35
  - [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
35
36
  - [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, claiming the ordinal intake and groundwork share, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
36
37
  - [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
@@ -51,7 +51,7 @@ image or marks the case new.
51
51
  | `ok` | A body was rendered. `commentId` is set when a marked comment already exists. |
52
52
  | `no-evidence` | Nothing in the diff carries an `evidence/` segment. An ordinary silent no-op. |
53
53
  | `gh-missing` | `gh` is not on the path, so no pull request could be resolved. |
54
- | `gh-failed` | `gh` could not answer for this repository or branch. |
54
+ | `gh-failed` | `gh` could not answer for this repository or branch, or read its comments. |
55
55
  | `no-branch` | The pull request carries no head branch name. |
56
56
  | `no-object-head` | The pull request object reported no head commit. |
57
57
  | `no-base` | No base resolves against the trunk. |
@@ -99,6 +99,14 @@ also why this verb renders the whole comment body rather than handing each
99
99
  skill a record to format on its own: a fix to the table shape or the
100
100
  collapsed-details wrapper lands once, not twice.
101
101
 
102
+ ## The preview line
103
+
104
+ `--preview <url>` opens the body with the branch's preview address, and a run
105
+ without it carries forward the address the marked comment already holds. With
106
+ no evidence in the diff and an address in hand, the body is the address and the
107
+ marker alone, reported as `ok`. `canon docs pr-preview` covers where the
108
+ address comes from.
109
+
102
110
  ## What a collapsed comment still leaves to GitHub
103
111
 
104
112
  GitHub already draws its own before-and-after comparison on the Files Changed
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: The pull request preview deploy
3
+ description: How canon pr preview finds a fenced Cloudflare Pages deploy, dispatches it on a pull request's branch, reads the alias the run prints, the refusal reasons it names, and why an unfenced deploy is refused before anything runs
4
+ ---
5
+
6
+ # The pull request preview deploy
7
+
8
+ `canon pr preview` publishes a pull request's branch to a Cloudflare Pages
9
+ preview and reports the address. `git-pr` calls it after the evidence
10
+ comparison, then re-renders that comment through `canon pr evidence --preview`
11
+ so the link opens it. A reviewer gets a page to click into before merging
12
+ rather than screenshots and a checklist alone.
13
+
14
+ ```bash
15
+ canon pr preview --json
16
+ canon pr preview 1341 --json --timeout 20
17
+ ```
18
+
19
+ ## Which workflow it dispatches
20
+
21
+ The verb reads `.github/workflows/*.yml` and `*.yaml` in the checkout and picks
22
+ the first file, in path order, that meets four tests:
23
+
24
+ - It runs `pages deploy`.
25
+ - It carries a `workflow_dispatch` trigger, since nothing else lets a caller
26
+ start it on a branch.
27
+ - Its deploy passes `--branch=${{ github.ref_name }}`.
28
+ - It prints a `canon-preview-alias:` line.
29
+
30
+ The workflow is read before the pull request, so a project with no fenced
31
+ deploy is refused without a network call.
32
+
33
+ ## Why an unfenced deploy is refused
34
+
35
+ Cloudflare Pages publishes a deploy that names no branch to production. A
36
+ dispatch from an unmerged branch through such a workflow ships that branch to
37
+ the live site, so the verb refuses as `unfenced` and dispatches nothing. The
38
+ fix is one flag on the deploy command, and the cloudflare stack's
39
+ `deploy.yml` already carries it.
40
+
41
+ ## Where the address comes from
42
+
43
+ The workflow prints the alias Cloudflare returned for the deploy, read from
44
+ `wrangler-action`'s `pages-deployment-alias-url` output, on a fixed line:
45
+
46
+ ```plaintext
47
+ canon-preview-alias: https://<alias>.<project>.pages.dev
48
+ ```
49
+
50
+ The verb reads that line out of the finished run's log rather than computing
51
+ the address from the branch name. Cloudflare flattens a branch into its alias
52
+ and may truncate it, and a computed address that is wrong is worse than none.
53
+ The output needs wrangler 3.78 or later, so the workflow pins
54
+ `wranglerVersion`.
55
+
56
+ `gh workflow run` reports no run id. The verb lists the branch's dispatched
57
+ runs before dispatching and takes the one id that appears afterwards, which
58
+ keeps the match independent of the local clock.
59
+
60
+ ## What the record carries
61
+
62
+ `reason` on the record is what a caller branches on, not the exit code:
63
+
64
+ | Reason | What it means |
65
+ | ------------- | -------------------------------------------------------------------------- |
66
+ | `ok` | The preview was published. `url` carries the address and `runId` the run. |
67
+ | `no-deploy` | No dispatchable workflow runs `pages deploy`. |
68
+ | `unfenced` | The deploy passes no `--branch`, so nothing was dispatched. |
69
+ | `no-alias` | The workflow prints no alias line, or the finished run's log carried none. |
70
+ | `run-failed` | The deploy run finished without succeeding. `runId` names it. |
71
+ | `timeout` | The run did not finish inside `--timeout` minutes, 15 by default. |
72
+ | `bad-timeout` | `--timeout` was not a positive number. |
73
+ | `gh-missing` | `gh` is not on the path. |
74
+ | `gh-failed` | `gh` could not read the pull request, or list or dispatch the workflow. |
75
+ | `no-branch` | The pull request carries no head branch name. |
76
+
77
+ The exit is 0 on `ok` and 1 on every refusal. A `timeout` leaves the run
78
+ going, so the preview may still land after the verb has given up on it.
79
+
80
+ ## How the address reaches the pull request
81
+
82
+ `canon pr evidence --preview <url>` puts `**Preview:** <url>` on the first line
83
+ of the evidence body. With no evidence image in the diff, the body is that line
84
+ and the trailing marker alone, so a pull request with a checklist and no
85
+ screenshots still gets one comment a later call can find and edit.
86
+
87
+ A later `canon pr evidence` run without `--preview` reads the address off the
88
+ marked comment and carries it into the new body. `git-followup` re-renders the
89
+ comment after every push, and without that carry the first push would delete
90
+ the link.
91
+
92
+ ## Removing a preview
93
+
94
+ A `pull_request` trigger on `closed` in the same workflow runs a cleanup job
95
+ that deletes every preview deployment Cloudflare holds for the branch. The
96
+ deploy job carries `if: github.event_name != 'pull_request'`, so a closed pull
97
+ request never deploys.
98
+
99
+ ## What this does not cover
100
+
101
+ The verb builds only the deployed-and-public case. A private repository whose
102
+ previews need access protection, and a project with no deploy at all where a
103
+ local server would stand in, both wait for a project that needs them.
@@ -223,6 +223,7 @@ This section is the corpus the coverage claim is measured against: every name `c
223
223
  | `canon:draft-and-pick` | When the call is taste and wants several candidates rendered side by side |
224
224
  | `canon:task-board` | When a decided item needs a file on the board, or a shipped one needs archiving |
225
225
  | `canon:plan-feature` | When the approach is settled and the next step is a plan |
226
+ | `canon:codebase-layout` | When a plan or change adds a file and you need to decide which folder holds it |
226
227
 
227
228
  ### Build the feature
228
229
 
@@ -19,7 +19,7 @@ Your primary directive is to maintain long-term system health over short-term co
19
19
  - Implement only the functionality required for the immediate task (YAGNI).
20
20
  - Extract shared logic into single-purpose utilities. Never duplicate behavior across modules (DRY).
21
21
  - Each function, module, and component should have a single reason to change (SRP).
22
- - Apply SRP to directories: once a folder mixes distinct roles and grows past a handful of files, split it into subfolders by role.
22
+ - Apply SRP to directories: once a folder mixes distinct roles and grows past a handful of files, split it into subfolders by role. Load the `canon:codebase-layout` skill before placing a new file, and report it rather than proceeding silently when it does not resolve.
23
23
  - Favor composition over inheritance.
24
24
  - Prioritize native platform capabilities over third-party libraries.
25
25
 
@@ -27,6 +27,7 @@ paths:
27
27
  - Place all Playwright tests within the `e2e/` directory.
28
28
  - Never place Playwright tests inside `src/`.
29
29
  - Scope the three rules above to tests the Playwright runner executes. A Vitest test importing a browser driver to exercise project code stays beside its module, where Vitest looks for it.
30
+ - Keep specs apart from their fixtures, page objects, and helpers inside `e2e/`. Load the `canon:codebase-layout` skill for the folder shape.
30
31
 
31
32
  ## Timers and async
32
33
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.96.0",
4
+ "version": "4.98.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -77,8 +77,11 @@ GOV_JSON="$(catalog gov list)"
77
77
  STANDARDS_JSON="$(catalog standards list)"
78
78
  SNIPPETS_JSON="$(catalog snippets list)"
79
79
  TOOLING_JSON="$(catalog tooling list)"
80
+ # The catalog ledger names three commands, and the registered set is only
81
+ # readable from the root help, which lists one per line under `Commands:`.
82
+ COMMANDS_HELP="$(cd "$PROJECT_ROOT" && CANON_NON_INTERACTIVE=1 bun src/cli.ts --help 2>/dev/null)"
80
83
 
81
- for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON"; do
84
+ for payload in "$COUNTS_JSON" "$SKILLS_JSON" "$GOV_JSON" "$STANDARDS_JSON" "$SNIPPETS_JSON" "$TOOLING_JSON" "$COMMANDS_HELP"; do
82
85
  if [ -z "$payload" ]; then
83
86
  echo "regen-hero: a catalog returned nothing, refusing to write a zeroed hero" >&2
84
87
  exit 1
@@ -98,6 +101,7 @@ printf '%s' "$GOV_JSON" >"$PAYLOAD_DIR/gov.json"
98
101
  printf '%s' "$STANDARDS_JSON" >"$PAYLOAD_DIR/standards.json"
99
102
  printf '%s' "$SNIPPETS_JSON" >"$PAYLOAD_DIR/snippets.json"
100
103
  printf '%s' "$TOOLING_JSON" >"$PAYLOAD_DIR/tooling.json"
104
+ printf '%s' "$COMMANDS_HELP" >"$PAYLOAD_DIR/commands-help.txt"
101
105
  printf '%s' "$TOKEN_CSS" >"$PAYLOAD_DIR/tokens.css"
102
106
 
103
107
  export PAYLOAD_DIR
@@ -326,8 +330,36 @@ for (const [label, rows] of [
326
330
  }
327
331
  }
328
332
 
333
+ // The ledger names a few real entries per domain. Chosen by hand for the same
334
+ // reason FEATURED_SKILLS is, and asserted against the catalog so a rename fails
335
+ // the run rather than leaving a name the repository no longer ships.
336
+ const commandNames = readFileSync(PAYLOAD_DIR + "/commands-help.txt", "utf8")
337
+ .split("\n")
338
+ .map((line) => line.match(/^[│\s]{2,}(?<name>[a-z][a-z-]*)\s/)?.groups?.name)
339
+ .filter(Boolean)
340
+ const LEDGER = [
341
+ ["skills", skills.length, ["plan-feature", "role-orchestrator", "review-pr"], skills],
342
+ ["rules", rules.length, ["behavior", "memory", "testing"], rules],
343
+ ["standards", standards.length, ["markdown", "plan", "teach"], standards],
344
+ ["commands", commandCount, ["gov", "tasks", "design"], commandNames],
345
+ ["stacks", toolingStacks.length, ["astro", "python", "web"], toolingStacks.map((stack) => stack.name)],
346
+ ]
347
+ for (const [domain, , chosen, names] of LEDGER) {
348
+ const known = new Set(names)
349
+ const missing = chosen.filter((name) => !known.has(name))
350
+ if (missing.length > 0) {
351
+ console.error(`regen-hero: ledger ${domain} names missing from the catalog: ${missing.join(", ")}`)
352
+ process.exit(1)
353
+ }
354
+ }
355
+ const ledgerRows = LEDGER.map(
356
+ ([domain, count, chosen]) =>
357
+ ` <div class="row"><span class="count">${count}</span><span class="domain">${domain}</span><span class="names">${escape(chosen.join(", "))}</span></div>`,
358
+ ).join("\n")
359
+
329
360
  const values = {
330
361
  TOKENS: tokenCss,
362
+ LEDGER_ROWS: ledgerRows,
331
363
  GOV_STACK_ROWS: govStackRows,
332
364
  GOV_RULE_ROWS: govRuleRows,
333
365
  STANDARD_ROWS: standardRows,
@@ -59,6 +59,11 @@ export const MISC_CASES: readonly SkillCase[] = [
59
59
  'The loading state already shipped. Should its test be an e2e test or a component one, and is the one I wrote any good?',
60
60
  expect: 'test-craft',
61
61
  },
62
+ {
63
+ prompt:
64
+ "The e2e folder has specs and helpers mixed together and I'm adding two more, where should they go?",
65
+ expect: 'codebase-layout',
66
+ },
62
67
  {
63
68
  prompt: 'Does our github about text still match what the readme says?',
64
69
  expect: 'repo-metadata',