@erclx/canon 4.112.0 → 4.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-feedback/SKILL.md +3 -3
- package/claude/skills/codebase-layout/SKILL.md +1 -1
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/target-setup/references/detect.md +2 -2
- package/claude/skills/target-setup/references/verify.md +26 -10
- package/claude/skills/test-craft/SKILL.md +1 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/counts.md +3 -3
- package/docs/agents/install-and-sync.md +2 -0
- package/docs/agents/records.md +22 -15
- package/docs/agents/teach.md +8 -4
- package/docs/target-projects.md +9 -2
- package/docs/workflow/ai-workflow.md +1 -1
- package/governance/rules/lang/130-go.md +40 -0
- package/governance/rules/lang/140-php.md +31 -0
- package/governance/rules/lib/335-testing-go.md +35 -0
- package/governance/rules/lib/336-testing-php.md +30 -0
- package/governance/rules/lib/360-security-server.md +2 -0
- package/governance/rules/lib/370-database.md +2 -0
- package/governance/rules/ui/410-a11y.md +1 -0
- package/governance/rules/ui/440-surface-capture.md +2 -0
- package/governance/stacks/nextjs.toml +2 -0
- package/governance/stacks/react.toml +1 -1
- package/package.json +3 -1
- package/src/claude/skills-audit.ts +2 -19
- package/src/commands/feedback-format.ts +24 -0
- package/src/commands/feedback.ts +2 -1
- package/src/commands/records.ts +18 -1
- package/src/commands/teach.ts +41 -1
- package/src/commands/tooling.ts +25 -2
- package/src/design/components.ts +17 -0
- package/src/design/tokens.ts +1 -1
- package/src/gate/measures.ts +13 -2
- package/src/indexes/frontmatter.ts +10 -1
- package/src/pr/evidence.ts +3 -1
- package/src/records/backup.ts +197 -27
- package/src/records/push-guard.ts +71 -0
- package/src/teach/nav.ts +75 -25
- package/src/teach/workspace.ts +14 -6
- package/src/tooling/inject.ts +43 -4
- package/src/tooling/scan.ts +35 -0
- package/src/tooling/subfolder.ts +110 -0
- package/tooling/base/configs/.husky/post-merge +5 -1
- package/tooling/base/seeds/cspell.json +1 -1
- package/tooling/claude/seeds/.claude/hooks/silent-turn.sh +7 -8
- package/tooling/nextjs/configs/vitest.config.ts +1 -1
- package/tooling/python/configs/scripts/verify.sh +32 -15
- package/tooling/python/manifest.toml +1 -1
- package/tooling/python/reference.md +3 -4
- package/tooling/vite-react/configs/vite.config.ts +1 -1
- package/tooling/web/configs/scripts/verify.sh +32 -15
|
@@ -5,7 +5,7 @@ description: Format a report about something broken, missing, or off in canon an
|
|
|
5
5
|
|
|
6
6
|
# Canon feedback
|
|
7
7
|
|
|
8
|
-
Format a `## Toolkit feedback` block from the current session, then open it as a GitHub issue labeled `feedback`
|
|
8
|
+
Format a `## Toolkit feedback` block from the current session, then open it as a GitHub issue on the toolkit repo, labeled `feedback` plus the domain label its surface type names. `canon-feedback-triage` reads that queue.
|
|
9
9
|
|
|
10
10
|
## Guards
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ Format a `## Toolkit feedback` block from the current session, then open it as a
|
|
|
18
18
|
From the conversation so far, identify:
|
|
19
19
|
|
|
20
20
|
- Target project name or kind, never its full path. The path names a folder on one machine and says nothing a triage session can route on, where the project's own name does. Name a private project by its kind instead, such as `a Next.js app`, since the report leaves the machine.
|
|
21
|
-
- Toolkit surface and its type (plugin skill, snippet, tooling config, governance rule, seed, or CLI)
|
|
21
|
+
- Toolkit surface and its type (plugin skill, snippet, tooling config, governance rule, seed, or CLI). The type leads the `### Surface` field, ahead of the first comma, because the CLI picks the issue's domain labels from that text alone.
|
|
22
22
|
- Specific toolkit file or name when the session cites one
|
|
23
23
|
- Observed behavior
|
|
24
24
|
- Expected behavior, or `unclear`
|
|
@@ -81,7 +81,7 @@ cat <<'EOF' | canon feedback --github
|
|
|
81
81
|
EOF
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
The CLI opens a GitHub issue labeled `feedback`
|
|
84
|
+
The CLI opens a GitHub issue on the toolkit repo, labeled `feedback` and each domain label the surface type names, such as `skills` for a plugin skill, and prints the issue URL, which this skill reports back on its own line. It needs `gh` authenticated. `git-issue` is not the route, since it files on the current repository and this report belongs on the toolkit's.
|
|
85
85
|
|
|
86
86
|
From a toolkit checkout, a call that produces no URL falls back to `.canon/feedback/feedback-<slug>-<ts>.md` and prints the reason on stderr. Say so in the same reply, naming the file and the reason, since a silent fallback leaves the report where nothing reads it. A target project has no such folder, so a failed call there prints the block in chat.
|
|
87
87
|
|
|
@@ -54,4 +54,4 @@ Read `${CLAUDE_SKILL_DIR}/references/adopted.md` only when extending this guidan
|
|
|
54
54
|
- Which layer a test belongs at: `test-craft`. This skill decides where the test file sits once its layer is chosen.
|
|
55
55
|
- Where a new path gets written into a plan, and the plan's shape: `plan-feature` and the plan standard
|
|
56
56
|
- The directory principle every session loads: `000-constitution`
|
|
57
|
-
- Runner, suffix, and top-level test folder conventions per language: `300-testing-ts` and `
|
|
57
|
+
- Runner, suffix, and top-level test folder conventions per language: `300-testing-ts`, `330-testing-py`, `335-testing-go`, and `336-testing-php`
|
|
@@ -101,6 +101,8 @@ CANON_NON_INTERACTIVE=1 canon tooling sync python ./backend --skip base --write
|
|
|
101
101
|
|
|
102
102
|
Without `--skip base`, each subtree re-drops husky, and git honors only one `core.hooksPath`, so the extra hook dirs silently break. Each subtree keeps its own framework configs, and its own stack reference reads through `canon tooling reference <stack>`.
|
|
103
103
|
|
|
104
|
+
A subtree sync withholds `.github/`, since GitHub reads workflows only at the repository root, and names each withheld file with the `working-directory` a root CI job needs. It also writes a nested `cspell.json` that registers the subtree's word lists. A subtree needs its own `bun init` before its scripts and dependencies land. The sync warns and skips them until then.
|
|
105
|
+
|
|
104
106
|
Step 3: post-sync fixups. Golden configs arrive from sync, so no config generation is required. But a few items may need a one-time touch:
|
|
105
107
|
|
|
106
108
|
- **ESLint version pin.** If `bun create vite` installed `eslint@^10` and the manifest pins `eslint@^9`, sync does not override a present dep. Run `bun add -d eslint@^9` if `bun run lint:fix` fails with `Class extends value undefined`.
|
|
@@ -30,7 +30,7 @@ since the chain reaches the tooling resolution a moment later.
|
|
|
30
30
|
Read these from the project root in parallel, skipping any that do not exist:
|
|
31
31
|
|
|
32
32
|
- `package.json`: `dependencies` and `devDependencies`
|
|
33
|
-
- Root configs: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`
|
|
33
|
+
- Root configs: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `composer.json`, `Cargo.toml`
|
|
34
34
|
- `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md` if present
|
|
35
35
|
- Directory structure via `ls -1` of the project root and `src/` if present
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ Read these from the project root in parallel, skipping any that do not exist:
|
|
|
38
38
|
|
|
39
39
|
- **Stack:** pick the closest governance stack by matching detected runtime or framework against stack names in the catalog. If nothing matches, fall back to `base` and carry the fallback into the preview.
|
|
40
40
|
- **Tooling stack:** pick the closest tooling stack from `canon tooling list --json` (e.g. `vite-react`, `astro`, `nextjs`). Distinct from the governance stack. Fall back to `base` if no framework match, and carry that fallback into the preview too.
|
|
41
|
-
- **Next.js:** map `next` in `package.json` dependencies, or a root `next.config.ts`/`.js`/`.mjs` file, to the `nextjs` tooling stack. The dependency name does not match
|
|
41
|
+
- **Next.js:** map `next` in `package.json` dependencies, or a root `next.config.ts`/`.js`/`.mjs` file, to the `nextjs` governance stack and the `nextjs` tooling stack. The dependency name does not match either stack name by itself.
|
|
42
42
|
- **Extras:** identify technologies not already covered by the picked stack. For each, find a rule whose `description` or `paths` points at that technology and pass it via `--add`. Dedupe against the stack's own rules. Do not add a rule the stack already pulls in.
|
|
43
43
|
- **Skip (`--skip`):** `wiki` installs by default. Add `--skip wiki` only when the user explicitly wants it left out.
|
|
44
44
|
|
|
@@ -14,19 +14,33 @@ name and a stack exposing a different set is covered without an edit here.
|
|
|
14
14
|
|
|
15
15
|
## Guards
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
A monorepo syncs each language root into its own subfolder, so the project root
|
|
18
|
+
is one folder to verify among several rather than the only one. Collect the
|
|
19
|
+
folders first:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
canon targets list --sweep <root> --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- The root is a folder to verify when it carries a `package.json`.
|
|
26
|
+
- Each path in the record's `targets` that sits under the root and carries its own `package.json` is a folder to verify.
|
|
27
|
+
- A path under the root with no `package.json` is not verified. Name it as skipped with its fix, `run 'bun init' in <path>, then sync the stack again`, and carry on with the rest.
|
|
28
|
+
- If neither the root nor any stamped subfolder carries a `package.json`, stop: `❌ No package.json at the root or in any synced subfolder. Cannot verify.`
|
|
29
|
+
- In each folder to verify, if `node_modules/` does not exist, run `bun install` there first, then proceed.
|
|
19
30
|
|
|
20
31
|
## Step 1: read scripts
|
|
21
32
|
|
|
22
|
-
Read `package.json` from
|
|
23
|
-
|
|
33
|
+
Read `package.json` from each folder and extract its `scripts` block. Do not
|
|
34
|
+
hardcode script names. Different stacks expose different scripts, and a
|
|
35
|
+
subfolder synced with `--skip base` declares none of the scripts the root owns.
|
|
24
36
|
|
|
25
37
|
## Step 2: run the chain
|
|
26
38
|
|
|
27
|
-
Run the
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
Run the chain once per folder, root first. In each, run the scripts for the
|
|
40
|
+
requested depth in order, stop that folder on its first failure, and surface the
|
|
41
|
+
error. Skip any script not present in that folder's `package.json`. Do not
|
|
42
|
+
invent a fallback command. Run each as `bun run <script>` from the folder it was
|
|
43
|
+
read from.
|
|
30
44
|
|
|
31
45
|
### Default depth
|
|
32
46
|
|
|
@@ -79,15 +93,17 @@ port to poll across stacks, so port-probing is out of reach here.
|
|
|
79
93
|
|
|
80
94
|
## Step 3: report
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
Report per folder, under a heading naming its path relative to the root, and
|
|
97
|
+
list each skipped folder with its `bun init` fix. For each script run, report
|
|
98
|
+
one of:
|
|
83
99
|
|
|
84
100
|
- `✅ <script>`
|
|
85
101
|
- `❌ <script>` followed by the failing output (last 40 lines) or, for a server script, the reason judged (process died, or a fatal string in its output)
|
|
86
102
|
|
|
87
103
|
End with a summary line naming the depth that ran:
|
|
88
104
|
|
|
89
|
-
- On pass: `✅ Scaffold verified at <depth> (<n> scripts passed).`
|
|
90
|
-
- On fail: `❌ Scaffold failed at <script>. Fix the error and re-run the verify phase.`
|
|
105
|
+
- On pass: `✅ Scaffold verified at <depth> (<n> scripts passed across <m> folders).`
|
|
106
|
+
- On fail: `❌ Scaffold failed at <script> in <folder>. Fix the error and re-run the verify phase.`
|
|
91
107
|
|
|
92
108
|
## Out of scope
|
|
93
109
|
|
|
@@ -73,5 +73,5 @@ Check every test written this session against each line. A test that fails one i
|
|
|
73
73
|
- The red, green, refactor procedure and its order: `test-first`, which loads this skill at its first step
|
|
74
74
|
- Reproducing a failure with no known cause: `systematic-debugging`
|
|
75
75
|
- Test structure floors such as arrange, act, assert, one behavior per test, isolation, and no snapshots: `010-testing`
|
|
76
|
-
- Runner, file suffix, placement, and query conventions per language: `300-testing-ts` and `
|
|
76
|
+
- Runner, file suffix, placement, and query conventions per language: `300-testing-ts`, `330-testing-py`, `335-testing-go`, and `336-testing-php`
|
|
77
77
|
- Settle, guard, and run-scope rules inside an end to end file: `305-e2e-reliability` and `306-test-scope`
|
package/docs/agents/commands.md
CHANGED
|
@@ -54,7 +54,7 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
|
|
|
54
54
|
| `canon records ordinal` | Report or claim the next ordinal `intake` and `groundwork` share, creating the folder atomically (`--claim`, `--json`) |
|
|
55
55
|
| `canon records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
56
56
|
| `canon records prune-tmp` | Report scratch nobody has touched inside an age window and delete it (`--write`, `--older-than`, `--json`) |
|
|
57
|
-
| `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote (`--json`)
|
|
57
|
+
| `canon records push` | Commit every record folder less the exclusion set and push it to a private records remote, refusing an oversized or credential-bearing file (`--json`) |
|
|
58
58
|
| `canon records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
59
59
|
| `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
|
|
60
60
|
| `canon migrate skill-names` | Move the prefixed skill folders onto their two-word names and rewrite every citation, reporting the plan without `--write` (`--root`, `--json`) |
|
package/docs/agents/counts.md
CHANGED
|
@@ -37,7 +37,7 @@ The set is closed rather than derived, so widening it is a deliberate change to
|
|
|
37
37
|
|
|
38
38
|
A sentence has to carry an assertion verb (`loads`, `ships`, `carries`, `holds`, `counts`, `totals`, `documents`, `declares`, `installs`, `lists`, `contains`, `comprises`, `authors`), the quantifier `all`, or an article (`the`, `a`, `an`) immediately ahead of the number, which is immediately ahead of the catalog noun, with one optional qualifying word between the number and the noun (`sixty-one shipped skills`). The number reads either as digits or as a spelled-out cardinal through ninety-nine, since this corpus states a catalog size in words as often as in digits.
|
|
39
39
|
|
|
40
|
-
Nothing stands between the trigger and the number, which is the rule an author writing a new count has to know. It separates `the toolkit authors
|
|
40
|
+
Nothing stands between the trigger and the number, which is the rule an author writing a new count has to know. It separates `the toolkit authors 81 rules` and `took all 81 rules`, both read, from `a domain of 55 skills` and `denominator of sixty-one shipped skills`, neither of them read. A figure meant as a past state takes a date in the same sentence instead, which reads the sentence past whatever its shape.
|
|
41
41
|
|
|
42
42
|
## How the trigger set grew
|
|
43
43
|
|
|
@@ -45,7 +45,7 @@ The verb gate is not the first design tried. A bare number next to a catalog nou
|
|
|
45
45
|
|
|
46
46
|
The article half was added after a first review of the shipped design found a live miss: `canon/context/development/gates.md` stated a stale audit total while the tree held 20, and the verb gate never reached it, since that sentence puts its verb after the noun rather than ahead of the number. Widening the trigger set to admit an article ahead of the number closed that gap.
|
|
47
47
|
|
|
48
|
-
The quantifier `all` and the verb `authors` joined on the same evidence, two more live misses failing only the trigger test. Allowing words to stand between the trigger and the number was the other candidate, measured and declined: at widths of one, two, and three it reached 77, 104, and 132 sentences against a baseline of 65, missed `took all
|
|
48
|
+
The quantifier `all` and the verb `authors` joined on the same evidence, two more live misses failing only the trigger test. Allowing words to stand between the trigger and the number was the other candidate, measured and declined: at widths of one, two, and three it reached 77, 104, and 132 sentences against a baseline of 65, missed `took all 81 rules` at every width, and reached `the toolkit authors 81 rules` only at a width admitting the indirect-noun shape below. The two words reach both at 75 sentences and no false positive.
|
|
49
49
|
|
|
50
50
|
## The plausibility filter
|
|
51
51
|
|
|
@@ -59,7 +59,7 @@ The article gate is looser than the verb list and carries a real cost. Re-runnin
|
|
|
59
59
|
|
|
60
60
|
A delta phrased as a transition (`from fourteen to fifteen`), a fraction (`thirteen of sixteen`), and a total reached through an indirect noun (`denominator of sixty-one shipped skills`) are all catalog-size claims this corpus carries, and none matches the trigger shape this reads. Each stays a known gap. The false-positive rate that gated closing them has a measurement behind it now, and what it showed is that the widening these three need is the one that costs a false positive rather than the one that does not.
|
|
61
61
|
|
|
62
|
-
A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors
|
|
62
|
+
A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 81 rules under governance/rules/ and consumes 62 into .claude/rules/` is read for its 81 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
|
|
63
63
|
|
|
64
64
|
A calendar date (`2026-08-21`) or a backticked commit reference in the same sentence reads the whole sentence past, since that is how this corpus already marks a figure as a historical record rather than a live claim. `canon/ARCHITECTURE.md` and the context entries carry a figure this way deliberately, and every one of them stays correct forever.
|
|
65
65
|
|
|
@@ -79,6 +79,8 @@ no copy to reconcile. `canon snippets list --json` carries the catalog and
|
|
|
79
79
|
|
|
80
80
|
`canon tooling diff <stack> [target]` is the read-only comparison, and a session asking what differs reaches for it by default. It writes nothing and exits 1 when anything differs and 0 when nothing does, so it gates CI. `--json` adds the scan record on stdout with `ok: true`, and the frame goes to stderr. A refusal such as an unknown stack, an excluded stack, or a toolkit-root target still exits 1 and answers `{ ok: false, reason, message }`, so a caller branches on the record and reads a typo'd stack name as a refusal rather than as drift.
|
|
81
81
|
|
|
82
|
+
The record carries a `withheld` array beside `configs`, each entry `{ rel, stack, present }`. It fills only when the target sits below its git root: every `.github/` config moves there rather than into `configs`, and none of them counts toward the exit, since GitHub never reads a workflow outside the root. `present` marks a copy an earlier sync already left in the subfolder, which the sync reports and never deletes. At a root, and for a target outside any repository, `withheld` is empty.
|
|
83
|
+
|
|
82
84
|
`canon tooling sync <stack> [target] --check` reports the same list and always exits 0. It stays for scripts that already call it. `sync` itself writes only under `--write`, and a headless run with neither flag reports and exits 1.
|
|
83
85
|
|
|
84
86
|
## Install guards
|
package/docs/agents/records.md
CHANGED
|
@@ -7,9 +7,9 @@ description: The two roots a record folder resolves at, validating the session r
|
|
|
7
7
|
|
|
8
8
|
## Record roots
|
|
9
9
|
|
|
10
|
-
Every verb here resolves a record folder at two roots rather than one. `.canon/<folder>` is read first, `.claude/<folder>` second, and a folder neither root carries resolves to the creation default, which is `.
|
|
10
|
+
Every verb here resolves a record folder at two roots rather than one. `.canon/<folder>` is read first, `.claude/<folder>` second, and a folder neither root carries resolves to the creation default, which is `.canon/`. The scratch folder is the one name that differs by root, spelled `.canon/tmp` and `.claude/.tmp`, since inside a dotted root the leading dot hides nothing already hidden. <!-- canon-keep-record-root -->
|
|
11
11
|
|
|
12
|
-
The read order and the creation default
|
|
12
|
+
The read order and the creation default agree. While the record folders were moving to a root of their own, creation stayed at `.claude/` so no record landed under a root whose ignore line had yet to reach a project. The ignore line ships now, so a fresh project scaffolds `.canon/` alone, and `.claude/` stays as a read fallback for a project the move has not reached.
|
|
13
13
|
|
|
14
14
|
A caller never spells a record root by hand for the same reason. A path written as `.canon/plans/...` resolves against one root and reports nothing when it is wrong, which is the quiet failure this ordering exists to prevent: a stale binary meeting a moved layout, writing to the old path, and reporting success. Read a folder through the verb that owns it, and where a skill needs the path itself, take it from that verb's record rather than composing one.
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ A refusal names every root it looked at, so a message reading `no-folder` says w
|
|
|
17
17
|
|
|
18
18
|
## Validate
|
|
19
19
|
|
|
20
|
-
`canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under `.
|
|
20
|
+
`canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under the record root, resolved at `.canon/` first: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
canon records validate plans
|
|
@@ -189,7 +189,9 @@ canon records push --json
|
|
|
189
189
|
canon records pull
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
-
At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The
|
|
192
|
+
At the `.canon` root, the backed folders are every top-level directory less three: `tmp`, which is deletable without loss, `ordinal-locks`, whose entries are transient per claim and would race the claim they guard, and `.records.git`, which is the history the rest are pushed into. Nothing bounds the set from outside, since the claude manifest ships one `.canon/` root entry and names no folder, so a record folder added later enters the payload on its own rather than waiting on a name written here. A push names each folder in scope that the records index has never tracked before, so a folder that picked up a name by mistake, such as a misrouted scratch write, is visible in the report rather than entering the payload silently. The report also lists every file new to the history under `added`, since a stray file inside a folder already tracked is visible only by name.
|
|
193
|
+
|
|
194
|
+
The three excluded names bound the records index as well as the disk. A name an older binary committed, such as `tmp`, is removed from the history once and reported under `dropped`, and no later push stages it again. That removes it from the tip going forward and purges nothing from earlier commits the remote already holds. A project's own drafts folder needs no config to be backed: moved to `.canon/<name>/`, it is a top-level directory like any other and enters the payload on the next push. The legacy `.claude` root keeps a fixed allowlist instead, since that root also holds tracked `skills/`, `rules/`, and `hooks/` a push must never carry, and an exclusion set there would stage all three. Each name is a top-level record folder and every archive sits inside the one it archives, so the set stays at one entry per surface however many archives appear, and it deliberately does not match the six record kinds `validate` hardcodes.
|
|
193
195
|
|
|
194
196
|
Records are gitignored by design, so the history lives in a second git directory at `.records.git` inside the record root, with that root as its work tree. Both resolve off the root together rather than folder by folder, since a history opened at one root beside a work tree at the other would stage the deletion of every folder a move relocated. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the backed folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Each pathspec is a bare folder name and git reads it against the current directory rather than against the work tree the same call names, so the invocation carries `-C` at the work tree beside the other two flags. That is what lets either verb run from a linked worktree under `.claude/worktrees/`, which sits inside the records work tree and would otherwise prefix every name with its own path.
|
|
195
197
|
|
|
@@ -209,20 +211,25 @@ Point it at a private repository, and at one that is not a remote of the project
|
|
|
209
211
|
|
|
210
212
|
### Refusals
|
|
211
213
|
|
|
212
|
-
| Reason | What fired
|
|
213
|
-
| ------------------- |
|
|
214
|
-
| `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set
|
|
215
|
-
| `no-repository` | No `.canon/.records.git`, answered with the two setup commands
|
|
216
|
-
| `no-remote` | The records history has no `origin`
|
|
217
|
-
| `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run
|
|
218
|
-
| `remote-shared` | The records origin is also a remote of the project
|
|
219
|
-
| `no-remote-records` | `pull` found no branch on the records origin
|
|
220
|
-
| `local-changes` | `pull` found records on disk that the history does not carry
|
|
221
|
-
| `local-ahead` | `pull` found local commits that never reached the origin
|
|
222
|
-
| `
|
|
214
|
+
| Reason | What fired |
|
|
215
|
+
| ------------------- | -------------------------------------------------------------------------------------- |
|
|
216
|
+
| `split-roots` | Record folders sit under both roots, so the resolved work tree is not the whole set |
|
|
217
|
+
| `no-repository` | No `.canon/.records.git`, answered with the two setup commands |
|
|
218
|
+
| `no-remote` | The records history has no `origin` |
|
|
219
|
+
| `remote-unreadable` | The project's own remotes could not be read, so the shared-origin gate could not run |
|
|
220
|
+
| `remote-shared` | The records origin is also a remote of the project |
|
|
221
|
+
| `no-remote-records` | `pull` found no branch on the records origin |
|
|
222
|
+
| `local-changes` | `pull` found records on disk that the history does not carry |
|
|
223
|
+
| `local-ahead` | `pull` found local commits that never reached the origin |
|
|
224
|
+
| `unsafe-payload` | `push` found a pending file over 25 MB or carrying a credential, named under `blocked` |
|
|
225
|
+
| `git-failed` | A git call failed, with its stderr in the message |
|
|
223
226
|
|
|
224
227
|
`split-roots` runs ahead of every gate below it and fires on a half-migrated tree, which is what a `canon migrate records` run that failed partway leaves. `recordRoot` answers for the whole tree on the first root that exists, so a folder left at the old root is absent from the work tree while the records index still names it, and an unguarded `add -A` would stage its deletion and drop it from the remote on the next push. Finish the move, or put the stranded folders back beside the others.
|
|
225
228
|
|
|
229
|
+
`unsafe-payload` runs before anything is staged, so a refusal leaves the index, the log, and the object store as it found them. It reads every new or changed file the push would stage and blocks one over 25 MB, which is read off the file's size without opening it, or one carrying a value `canon secrets scan` would report, through the same patterns and the same `canon-allow-secret` marker. It refuses the whole push rather than skipping the file, and `--json` lists each path with its cause and a detail that names the credential's kind and line, never its value. Move the file out of the record folders, such as into `.canon/tmp/`, or remove the credential, then push again. The guard reads only what this push would add, so a credential committed before it existed stays in the history unreported.
|
|
230
|
+
|
|
231
|
+
A push the remote rejects undoes the commit that run made and leaves the records on disk, so the next run does not send a payload the remote already refused. A history an older binary committed and left unpushed is outside that undo, and the guard never reads it either, so every push sends the same blob again. Reset it by hand from the project root to the last commit the origin holds, `git --git-dir=.canon/.records.git --work-tree=.canon reset <commit>`, and push again. The `--work-tree` is required, since the setup `init` marks the history bare and git refuses that reset on a bare repository.
|
|
232
|
+
|
|
226
233
|
The two `pull` refusals exist because the directions are not symmetric. A push only adds, while a pull onto a machine holding work that never left it would discard that work. Resolve either by running `push` first, or by moving the local folders aside. A machine holding no backed folders has nothing to lose, so a restore onto a fresh checkout runs straight through.
|
|
227
234
|
|
|
228
235
|
### When it runs
|
package/docs/agents/teach.md
CHANGED
|
@@ -172,13 +172,15 @@ canon teach nav regular-expressions --json
|
|
|
172
172
|
|
|
173
173
|
With no topic it rewrites every workspace. The teach-root listing is always rewritten regardless, since it reports on every workspace and a scoped run leaving it stale would be a second kind of drift this verb exists to end.
|
|
174
174
|
|
|
175
|
-
A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts
|
|
175
|
+
A lesson carries its chrome as four marker pairs the authoring skill writes empty: `canon:teach:style`, `canon:teach:header`, `canon:teach:footnav`, and `canon:teach:scripts`. This verb splices each one from the current workspace state, embedding the toolkit's favicon, the shared stylesheet, and the script that settles the course panel before the page paints, rebuilding the course sidebar, the breadcrumb and jump menus, the prev/next footer nav, and the behavior scripts. It leaves the authored `<h1>`, lede, body, and quiz between the header and the footnav as written, and wraps them in `<main>` when the lesson holds none, since the column width hangs off that element. A lesson that already carries a `<main>` keeps its own.
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
Every workspace label in that chrome, being the breadcrumb, the sidebar heading, the workspace switcher, and the root listing's row, reads the mission's `title` and falls back to the folder topic in sentence case where the mission names none.
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
It also deletes two links a session used to write by hand into a lesson's `<head>`, each matched as its exact string: the old icon link and `<link rel="stylesheet" href="../assets/course.css" />`. The page keeps only the icon and the stylesheets the `style` region carries. Any other link an author wrote stays.
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
A workspace carrying no `course.css` gets its stylesheet pair seeded first, so the contents page it links is never unstyled, and an existing `course.css` is never overwritten. The `style` region links `assets/base.css` ahead of an embedded copy of `course.css`, so the workspace's own rules still win the cascade. The embedded copy drops the seed's `@import` line, which resolved against the lesson's own folder and never loaded anything, and the link is what reaches the base sheet in its place. A workspace with no `assets/base.css`, one holding a hand-authored `course.css` from before the seed, gets no link rather than one pointing at a missing file.
|
|
182
|
+
|
|
183
|
+
That seeding fires on the absent file alone, so a workspace already holding a `course.css` has neither half rewritten here. A change to the shared chrome therefore takes `canon teach stylesheet <topic>` per workspace, which rewrites `assets/base.css`, and every lesson picks it up through its link with no run of this verb. Running this one alone leaves every lesson on the `base.css` the workspace already holds, and nothing reports it. Only an edit to a workspace's own `course.css` needs this verb, since that half is embedded.
|
|
182
184
|
|
|
183
185
|
A lesson missing one of the four marker pairs is refused by name rather than rewritten, and every other lesson in the run still rewrites. The record's `skipped` list carries the refused files and which marker each is missing.
|
|
184
186
|
|
|
@@ -198,6 +200,8 @@ No `canon teach` verb serves the workspace. `canon serve` does, taking the teach
|
|
|
198
200
|
canon serve .canon/teach --entry 03-fde-system-design/index.html --json
|
|
199
201
|
```
|
|
200
202
|
|
|
203
|
+
`canon teach list <topic>` ends its human output with this line for that workspace, with the teach folder written relative to the cwd, or absolute when it sits outside it, since `canon serve` resolves its directory against the cwd while the list verb reads the main worktree root. The JSON record carries no such field.
|
|
204
|
+
|
|
201
205
|
It stays general rather than becoming `canon teach serve`, because nothing about serving a directory is specific to a learning workspace, and the same verb carries a slide render and a design preview.
|
|
202
206
|
|
|
203
207
|
Read `url` off the record rather than building one from the port that was asked for. The verb walks forward past a port already in use, which is routine when a second workspace is already open, and the port it took is the one thing a composed URL gets wrong.
|
package/docs/target-projects.md
CHANGED
|
@@ -86,13 +86,13 @@ Each diagram entry records the commit and date it was last verified against, and
|
|
|
86
86
|
|
|
87
87
|
The default path is `base`. `canon init` on `base` installs base tooling configs, Claude seeds, and governance core rules, and scaffolds an empty `.claude/wiki/`. Most projects need nothing more.
|
|
88
88
|
|
|
89
|
-
Escalate only for real web apps. The `target-setup` skill reads `package.json` and root configs, then picks the matching tooling stack (`canon tooling list --json` names the current set) and the matching governance stack (`react`, `astro`, `node`).
|
|
89
|
+
Escalate only for real web apps. The `target-setup` skill reads `package.json` and root configs, then picks the matching tooling stack (`canon tooling list --json` names the current set) and the matching governance stack (`react`, `nextjs`, `astro`, `node`). A Next.js app lands on `nextjs`, which extends `react` with the App Router rule a Vite React app does not get.
|
|
90
90
|
|
|
91
91
|
`node-server` is named rather than detected. It carries the server-side security and persistence rules for a project writing request handlers or a persistence layer in TypeScript, and the detect step matches a runtime or a framework against stack names, so nothing there marks a project as a backend. Pass it deliberately with `canon init --stack node-server` or `canon gov install node-server <target>`.
|
|
92
92
|
|
|
93
93
|
Markdown-heavy projects, CLI tools, docs sites, research notebooks, and scripting repos stay on `base`. Escalation is a ceiling move, not a default.
|
|
94
94
|
|
|
95
|
-
A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:target-setup gov` for the governance layer alone, which is language-neutral.
|
|
95
|
+
A project the toolkit ships no stack for lands on `base` the same way, and the skill marks that resolution as a fallback in its preview rather than reporting it as a match. Configs, seeds, and gitignore entries land either way, and the JavaScript development dependencies, scripts, and hook activation land only where a `package.json` exists to carry them. A project outside that ecosystem declines at the preview and takes `canon:target-setup gov` for the governance layer alone, which is language-neutral. Go and PHP are the exception: they ship language and testing rules but no stack, so a project in either lands on `base` and picks those rules up through `--add`, for example `canon gov install base --add 130-go,335-testing-go .`
|
|
96
96
|
|
|
97
97
|
Run `canon tooling list --json` and `canon gov list --json` to see the current catalogs. Never hardcode stack names.
|
|
98
98
|
|
|
@@ -331,6 +331,13 @@ canon tooling sync python ./backend --skip base --write
|
|
|
331
331
|
|
|
332
332
|
`--skip base` drops the `base` layer from each subtree sync, so husky, prettier, cspell, commitlint, and CI stay single at the repo root. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
|
|
333
333
|
|
|
334
|
+
A sync whose target sits below the git root knows it is writing into a subfolder:
|
|
335
|
+
|
|
336
|
+
- It withholds `.github/`, since GitHub reads workflows only at the repository root, and names each withheld file. Run the subtree from a job in the root workflow instead, with `working-directory` set to the subtree path the sync prints, such as `working-directory: frontend`.
|
|
337
|
+
- It writes a nested `cspell.json` that registers the subtree's word lists, so a root `cspell '**'` reads them for files under that folder. It leaves a spell config the subtree already has alone.
|
|
338
|
+
- It installs no dependencies and adds no scripts to a subtree without a `package.json`. It names what it skipped and ends on `run 'bun init' in <path>, then sync again`.
|
|
339
|
+
- The subtree's `scripts/verify.sh` skips the format, spell, and shell phases a subtree does not declare, since the root runs them, and still fails on a missing lint, typecheck, or test script.
|
|
340
|
+
|
|
334
341
|
## Running sync from an agent session
|
|
335
342
|
|
|
336
343
|
`canon sync .` applies every installed domain sync, then offers to commit the result and open a pull request. That last step needs a terminal. Under `CANON_NON_INTERACTIVE=1`, which is how an agent runs it, the domain syncs still apply and the git workflow is refused: the command reports the branch and commit it would have created, writes nothing to git, and exits 0. Review the working tree and commit it yourself, or rerun interactively to reach the commit and pull request options.
|
|
@@ -107,7 +107,7 @@ An execution that picks other than the suggestion rewrites the `- Suggested:` li
|
|
|
107
107
|
|
|
108
108
|
`canon records validate plans` reports where a plan and that standard disagree: a filename that is not `feature-<slug>.md`, a missing required section, a files-to-touch entry naming no file, and a question carrying a suggestion with no answer slot. The same verb takes `groundwork`, `intake`, `memory`, and `teach`, which are governed the same way and were unreachable for the same reason. Nothing fires it automatically, because all five folders are gitignored and every check the repository runs reads changed files from git. It reports and never writes, since the folders are per-machine scratch with no history to recover a wrong repair from.
|
|
109
109
|
|
|
110
|
-
`canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Every top-level entry under `.canon/` is backed except `tmp`, `ordinal-locks`, and `.records.git`, so a folder added later enters the payload on its own rather than waiting on a name written into a list. Push names each folder it carries the first time it sees it, which is what keeps a stray folder visible instead of silently entering the payload. The history lives in a second git directory at `.canon/.records.git` with `.canon/` as its work tree, so every path a task file cites stays where it is.
|
|
110
|
+
`canon records push` carries these folders off the disk they live on, and `canon records pull` brings them back. Every top-level entry under `.canon/` is backed except `tmp`, `ordinal-locks`, and `.records.git`, so a folder added later enters the payload on its own rather than waiting on a name written into a list. Push names each folder it carries the first time it sees it, which is what keeps a stray folder visible instead of silently entering the payload. It refuses the whole push, naming each path, when a new or changed file is over 25 MB or carries a credential `canon secrets scan` would report. The history lives in a second git directory at `.canon/.records.git` with `.canon/` as its work tree, so every path a task file cites stays where it is.
|
|
111
111
|
|
|
112
112
|
A person points it at a private repository once and both verbs refuse until they have, and `push` refuses when that origin is also a remote of the project, since the payload is the memory pen and the groundwork trails. `.husky/post-merge` runs the push after its archive loop, on every merge rather than only on one that closed a task. See [records](../agents/records.md) for the refusal table.
|
|
113
113
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enforce Go naming, error wrapping, context passing, and package boundaries
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*.go'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Go standards
|
|
8
|
+
|
|
9
|
+
## Casing conventions
|
|
10
|
+
|
|
11
|
+
- Use `MixedCaps` for exported names and `mixedCaps` for unexported ones. Never use underscores in a Go identifier.
|
|
12
|
+
- Name packages with one short lowercase word. Do not use `util`, `common`, or `helpers` as a package name.
|
|
13
|
+
- Do not repeat the package name in an exported identifier (`user.New`, not `user.NewUser`).
|
|
14
|
+
- Keep initialisms in one case (`userID`, `HTTPClient`), never `userId` or `HttpClient`.
|
|
15
|
+
|
|
16
|
+
## Errors
|
|
17
|
+
|
|
18
|
+
- Return an error as the last result value.
|
|
19
|
+
- Wrap an error with `fmt.Errorf("...: %w", err)` when adding context.
|
|
20
|
+
- Match an error with `errors.Is` or `errors.As`. Never compare `err.Error()` strings.
|
|
21
|
+
- Never discard an error with `_`.
|
|
22
|
+
- Do not `panic` for a failure a caller can expect. Reserve it for a broken invariant.
|
|
23
|
+
|
|
24
|
+
## Context
|
|
25
|
+
|
|
26
|
+
- Pass `context.Context` as the first parameter, named `ctx`.
|
|
27
|
+
- Never store a `context.Context` in a struct field.
|
|
28
|
+
- Do not pass a `nil` context. Use `context.TODO()` where none is available yet.
|
|
29
|
+
|
|
30
|
+
## Interfaces
|
|
31
|
+
|
|
32
|
+
- Declare an interface in the package that consumes it, not the one that implements it.
|
|
33
|
+
- Keep an interface to the methods its consumer calls.
|
|
34
|
+
- Accept interfaces and return concrete types.
|
|
35
|
+
|
|
36
|
+
## Packages
|
|
37
|
+
|
|
38
|
+
- Place code no other module may import under `internal/`.
|
|
39
|
+
- Do not perform side effects in `init`. Wire dependencies explicitly from `main`.
|
|
40
|
+
- Do not hold mutable state in a package-level variable.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enforce PHP strict types, PSR-4 layout, immutable data shapes, and exception handling
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*.php'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PHP standards
|
|
8
|
+
|
|
9
|
+
## Strictness
|
|
10
|
+
|
|
11
|
+
- Open every file with `declare(strict_types=1);`.
|
|
12
|
+
- Declare native parameter, return, and property types. Do not rely on a docblock type where a native one exists.
|
|
13
|
+
- Compare with `===` and `!==`. Never use loose `==` or `!=`.
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
- Autoload classes through PSR-4 from `composer.json`. Do not `require` a class file by hand.
|
|
18
|
+
- Declare one class, interface, trait, or enum per file, named after the file.
|
|
19
|
+
|
|
20
|
+
## Data shapes
|
|
21
|
+
|
|
22
|
+
- Use a backed `enum` for a closed set over class constants.
|
|
23
|
+
- Use `readonly` properties or a `readonly` class for value objects.
|
|
24
|
+
- Declare classes `final` unless they are designed for extension.
|
|
25
|
+
- Do not pass an associative array across a public boundary where a typed object fits.
|
|
26
|
+
|
|
27
|
+
## Errors
|
|
28
|
+
|
|
29
|
+
- Throw a domain exception for a failure. Do not return `false` or `null` to signal one.
|
|
30
|
+
- Never suppress an error with the `@` operator.
|
|
31
|
+
- Do not use `extract`, variable variables, or `eval`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enforce Go testing package, table-driven cases, and test helper patterns for Go tests
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*_test.go'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Go testing tooling
|
|
8
|
+
|
|
9
|
+
## Layer
|
|
10
|
+
|
|
11
|
+
- Load the `canon:test-craft` skill to pick the layer a test belongs at before choosing the tooling below, and report it rather than proceeding silently when the skill does not resolve.
|
|
12
|
+
|
|
13
|
+
## Framework
|
|
14
|
+
|
|
15
|
+
- Use the standard `testing` package. Do not add an assertion library for what `t.Errorf` and `t.Fatalf` already state.
|
|
16
|
+
- Place a test beside the code it tests, in a `_test.go` file of the same package.
|
|
17
|
+
- Use an external `<pkg>_test` package when a test should see only the exported API.
|
|
18
|
+
|
|
19
|
+
## Table-driven cases
|
|
20
|
+
|
|
21
|
+
- Write cases as a slice of structs run through `t.Run`, over one test function per case.
|
|
22
|
+
- Give every case a `name` field and pass it to `t.Run`.
|
|
23
|
+
|
|
24
|
+
## Helpers and setup
|
|
25
|
+
|
|
26
|
+
- Call `t.Helper()` first in every test helper.
|
|
27
|
+
- Use `t.Cleanup` over `defer` for teardown a helper registers.
|
|
28
|
+
- Use `t.TempDir` and `t.Setenv` over hand-rolled temp directories or environment stashes.
|
|
29
|
+
- Use `net/http/httptest` over a real listener.
|
|
30
|
+
|
|
31
|
+
## Concurrency
|
|
32
|
+
|
|
33
|
+
- Do not synchronize with `time.Sleep`. Wait on a channel, a `sync.WaitGroup`, or a deadline.
|
|
34
|
+
- Run tests with `-race`.
|
|
35
|
+
- Mark a test `t.Parallel()` only when it shares no mutable state with another test.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Enforce PHPUnit test classes, attributes, and strict assertions for PHP tests
|
|
3
|
+
paths:
|
|
4
|
+
- 'tests/**/*.php'
|
|
5
|
+
- '**/*Test.php'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# PHP testing tooling
|
|
9
|
+
|
|
10
|
+
## Layer
|
|
11
|
+
|
|
12
|
+
- Load the `canon:test-craft` skill to pick the layer a test belongs at before choosing the tooling below, and report it rather than proceeding silently when the skill does not resolve.
|
|
13
|
+
|
|
14
|
+
## Framework
|
|
15
|
+
|
|
16
|
+
- Use PHPUnit for all tests.
|
|
17
|
+
- Declare each test class `final` and extend `PHPUnit\Framework\TestCase`.
|
|
18
|
+
- Place tests under `tests/`, mirroring the `src/` namespace layout, in files named `*Test.php`.
|
|
19
|
+
|
|
20
|
+
## Attributes
|
|
21
|
+
|
|
22
|
+
- Use attributes (`#[Test]`, `#[DataProvider]`, `#[CoversClass]`) over docblock annotations.
|
|
23
|
+
- Use a static data provider method for table-driven cases over per-case test methods.
|
|
24
|
+
|
|
25
|
+
## Assertions and doubles
|
|
26
|
+
|
|
27
|
+
- Use `assertSame` over `assertEquals`.
|
|
28
|
+
- Use `expectException` over `try` and `catch` for expected exceptions.
|
|
29
|
+
- Use `createStub` where no interaction is asserted. Reserve `createMock` for asserting a call.
|
|
30
|
+
- Do not make real network calls. Use a fake or a stubbed client.
|
|
@@ -60,3 +60,4 @@ paths:
|
|
|
60
60
|
|
|
61
61
|
- Let content reflow at a viewport 320 CSS pixels wide with no loss of content or function and no two-dimensional scrolling. Only content whose meaning depends on both axes, such as a data table, a map, or a diagram, may scroll sideways (WCAG Reflow, 1.4.10, Level AA).
|
|
62
62
|
- Treat 320 as the criterion's own test condition rather than a device width. It is equivalent to a 1280 pixel window at 400 percent zoom, so a layout that breaks there breaks for a person who zooms.
|
|
63
|
+
- Hold the same 320 floor for the content region beside persistent chrome a person can resize. The criterion measures the viewport, so this extension is this rule's own rather than the criterion's: a panel widened to 480 in a 768 window leaves 288 for the content, under the floor at a window the criterion passes. `440-surface-capture` states where the frame for it gets taken.
|
|
@@ -28,6 +28,8 @@ paths:
|
|
|
28
28
|
- Capture the full page. Do not capture a component in isolation.
|
|
29
29
|
- Declare the widths a layout case captures. Always include 320, the reflow floor from the reflow criterion in `410-a11y`, then add each breakpoint bucket the project writes rules for, read from the `## Layout` section of `DESIGN.md`.
|
|
30
30
|
- Fall back to the 320 floor alone when the project has no `DESIGN.md` or its record carries no `## Layout` section. Do not impose another project's breakpoints.
|
|
31
|
+
- Add a layout case at each bound when a surface renders persistent chrome a person can resize, such as a draggable sidebar or a split pane, one with the chrome at its narrowest and one at its widest. Take both at the narrowest declared width where the chrome stays persistent rather than collapsing, since a frame where it has turned into an overlay shows none of the width it takes.
|
|
32
|
+
- Read those bounds from the code that clamps the chrome, since `## Layout` has no slot for them. Judge each frame by the content width left beside the chrome rather than by the window width, against the 320 floor `410-a11y` holds for that region.
|
|
31
33
|
- Apply the declaration to a case that covers layout only. A case driving a menu open or an answer chosen tests a state, so it takes one width rather than the full set.
|
|
32
34
|
- Read a capture at 320 as proof that a frame was taken there, not that the page reflows correctly. The criterion is stated in `410-a11y` and nothing here enforces it.
|
|
33
35
|
- Write the capture into a folder named `evidence`. The image is collected for the pull request comment when any segment of its path is literally `evidence` and its filename carries an image extension, so the folder name is fixed and its position in the tree is the project's own.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
extends = "node"
|
|
2
|
-
rules = ["200-react", "
|
|
2
|
+
rules = ["200-react", "250-tailwind", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "310-zod", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "465-interface-casing","470-motion"]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erclx/canon",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.114.0",
|
|
5
5
|
"description": "Infrastructure and quality tooling for developer workflows",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"web:previews": "bun scripts/core/regen-web-previews.ts",
|
|
50
50
|
"web:build": "bun run web:tokens && bun run web:favicon && bun run web:previews && cd web && astro check && astro build",
|
|
51
51
|
"web:gallery": "cd web && astro build --config gallery.config.mjs",
|
|
52
|
+
"web:card": "cd web && astro dev --config card.config.mjs",
|
|
53
|
+
"web:card:check": "PROJECT_ROOT=web bash tooling/astro/configs/scripts/check-card-exclusion.sh",
|
|
52
54
|
"web:preview": "cd web && astro preview",
|
|
53
55
|
"web:e2e": "cd web && playwright test"
|
|
54
56
|
},
|