@erclx/canon 4.4.0 → 4.6.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/claude-markdown-propose/REQUIREMENT.md +0 -1
- package/docs/agents/audits.md +6 -6
- package/docs/agents/commands.md +3 -0
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/driver.md +103 -0
- package/docs/agents/index.md +2 -1
- package/docs/agents/records.md +17 -7
- package/docs/agents/tasks.md +1 -1
- package/package.json +1 -1
- package/scripts/core/check-ignore-parity.sh +9 -3
- package/src/audits/baseline.ts +1 -1
- package/src/browser/engine.ts +50 -7
- package/src/cli.ts +4 -0
- package/src/commands/claude.ts +7 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/design.ts +6 -1
- package/src/commands/driver.ts +266 -0
- package/src/commands/feedback.ts +5 -1
- package/src/commands/gov.ts +2 -1
- package/src/commands/slides.ts +6 -1
- package/src/context/citations.ts +16 -5
- package/src/context/folders.ts +18 -8
- package/src/driver/drive.ts +184 -0
- package/src/driver/probes/details.ts +116 -0
- package/src/driver/probes/diagram.ts +260 -0
- package/src/driver/probes/focus.ts +121 -0
- package/src/driver/probes/viewport.ts +81 -0
- package/src/driver/steps.ts +266 -0
- package/src/gate/measures.ts +1 -1
- package/src/intake/folder.ts +2 -1
- package/src/inventory/walk.ts +6 -18
- package/src/record-root.ts +134 -0
- package/src/records/backup.ts +40 -22
- package/src/records/size.ts +12 -7
- package/src/records/validate.ts +35 -17
- package/src/tasks/answers.ts +14 -7
- package/src/tasks/archive.ts +31 -21
- package/src/teach/workspace.ts +2 -1
- package/tooling/claude/manifest.toml +1 -1
- package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +8 -1
- package/tooling/claude/seeds/.claude/hooks/memory-index.sh +28 -11
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +12 -3
- package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +4 -0
- package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +28 -10
|
@@ -23,7 +23,6 @@ A second failure compounds the first. A claim copied across several files is cor
|
|
|
23
23
|
- Carry three labelled variants on a change whose replacement was invented, and one on a change that corrects text to a recorded fact
|
|
24
24
|
- Write one proposal file per source file under `.claude/proposals/<slug>/` and stop, leaving `You:` empty on every change
|
|
25
25
|
- Apply only a change carrying an answer, one file at a time, and re-grep its anchor before applying it
|
|
26
|
-
- Name a folder that does not collide with `.claude/audits/`, which holds generated data rather than decision state
|
|
27
26
|
- Report what each file leaves alone, so a proposal cannot be read as finding everything wanting
|
|
28
27
|
|
|
29
28
|
## Must not
|
package/docs/agents/audits.md
CHANGED
|
@@ -14,11 +14,11 @@ canon audits run --record
|
|
|
14
14
|
canon audits list --json
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
| Option | Behavior
|
|
18
|
-
| --------------- |
|
|
19
|
-
| `--json` | Add a machine-readable record on stdout, keeping the frame
|
|
20
|
-
| `--root <path>` | Measure this tree instead of the current worktree
|
|
21
|
-
| `--record` | Write this run's tracked counts to `.claude/
|
|
17
|
+
| Option | Behavior |
|
|
18
|
+
| --------------- | ---------------------------------------------------------------- |
|
|
19
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
20
|
+
| `--root <path>` | Measure this tree instead of the current worktree |
|
|
21
|
+
| `--record` | Write this run's tracked counts to `.claude/canon/baseline.json` |
|
|
22
22
|
|
|
23
23
|
## What it runs
|
|
24
24
|
|
|
@@ -73,7 +73,7 @@ Every run states how many corpora it measured against how many it skipped, inclu
|
|
|
73
73
|
|
|
74
74
|
## The retained baseline
|
|
75
75
|
|
|
76
|
-
`.claude/
|
|
76
|
+
`.claude/canon/baseline.json` holds the counts from the last run recorded with `--record`, alongside the day it was taken and the commit it was read at. Each later run reports which counts moved and by how much.
|
|
77
77
|
|
|
78
78
|
The file is committed. A per-machine record cannot answer the question this half exists for, which is whether a number grew since anyone last looked, and a fresh checkout has to inherit that answer rather than start over. The cost is that a branch moving a count either fixes it or re-records it and says why, which is the shape the consumed-copy assertion already carries.
|
|
79
79
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -70,6 +70,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
70
70
|
| `canon audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
71
71
|
| `canon gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
|
|
72
72
|
| `canon inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
|
|
73
|
+
| `canon drive <url> <run>` | Walk a page through named interactions and measure each state it reaches, reporting findings rather than gating (`--json`) |
|
|
73
74
|
| `canon capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
74
75
|
| `canon serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
|
|
75
76
|
| `canon upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
|
|
@@ -94,6 +95,8 @@ routes = ["/", "/pricing", "/docs"]
|
|
|
94
95
|
query = "button, a[href], input, select, textarea, [tabindex]"
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
`canon drive` is the fourth and ships for the same reason the two before it do. What separates it from all three is the axis rather than the destination: `capture` and `inventory` each answer about a page as it loads, and every defect that exists only after a menu opens or the page scrolls is invisible to both. It takes a JSON run file naming the viewport, the probes, and the interaction sequence, since a route catalog is state a project holds and an interaction sequence is a script written for one question. Viewport heights are never defaulted, because the heights a defect hides at belong to the layout rather than to this command. It reports findings and never gates, since every probe it ships carries a class of false finding a throwaway version already produced. See `driver.md`.
|
|
99
|
+
|
|
97
100
|
## Domain commands
|
|
98
101
|
|
|
99
102
|
Each domain exposes a consistent shape where applicable: `list`, `install`, `sync`, `create`.
|
|
@@ -31,7 +31,7 @@ canon context audit tooling/base/seeds --gate
|
|
|
31
31
|
|
|
32
32
|
Scope defaults to `context`, `diagrams`, and `wireframes`, and a folder the project does not carry is skipped rather than reported. A domain that outgrew one file and split into `<domain>/` is audited as its own folder, so a split entry measures at the same grain as a flat one.
|
|
33
33
|
|
|
34
|
-
A name passed to `--folder` resolves under `.
|
|
34
|
+
A name passed to `--folder` resolves under the record roots first, `.canon/` ahead of `.claude/`, and at the project root last, which is what puts `docs/` and any later corpus in reach of the same engine. The root base is reached only by a name the caller passes, so the default list still resolves under a record root alone and a project holding a root `wireframes/` is not audited against a standard it never adopted. `diagrams` is the one default name that is a session record and moves with them, which is why both record roots are in the list rather than one, and `docs/agents/records.md` states the read order every verb shares. The scope line prints the resolved path, so a project carrying both spellings reads which one was taken. The JSON record carries the base per folder as `folders[].base`.
|
|
35
35
|
|
|
36
36
|
A run where no requested name resolves refuses, whichever list it read. Naming the absent ones narrows to `--folder`, since a project carrying one of the three default folders is the ordinary case and a name it never asked for is not a typo. The JSON record carries those names as `unresolvedFolders`.
|
|
37
37
|
|
|
@@ -80,4 +80,4 @@ Append `<!-- audit-ignore-citations: <path> -->` to the source line in either ca
|
|
|
80
80
|
|
|
81
81
|
The marker itself stays out of anything that installs. A seed, a plugin skill body, and a stack reference all reach a target, so a marker there lands as toolkit bookkeeping in someone else's tree. Reword those lines to drop the path instead, and where a stop message has to spell it, move that message into a fenced block, which this check already skips.
|
|
82
82
|
|
|
83
|
-
The pattern spells
|
|
83
|
+
The pattern spells both record-root prefixes, so a citation into a folder that has moved still resolves and a folder resolved at the project root is measured by every other check while contributing nothing here. A pattern fixed at one root matches nothing after a move and reports nothing, which is a stale reference passing the check written to find it. Widening it to a bare `docs/x.md` would match prose that references nothing, which is a separate decision from where entries come from. A run whose folders all resolved at the root says the check is out of scope rather than reporting that zero paths resolved, and the same run under `--citations-only` refuses, because a gate exiting clean on a scope it could not build is the failure the gate exists to catch.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Driver
|
|
3
|
+
description: Walking a page through named interactions, the probe catalog and the false finding each one carries, why viewport heights are never defaulted, and what each refusal reports
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Driver
|
|
7
|
+
|
|
8
|
+
`canon drive <url> <run>` walks a page through a sequence of interactions and measures every state it reaches. A render answers about a page as it loads, so a defect that exists only after a menu opens, an answer is chosen, or the page scrolls is invisible to one. That second axis is what this adds.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
canon drive http://localhost:4173 run.json
|
|
12
|
+
canon drive http://localhost:4173 run.json --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Option | Behavior |
|
|
16
|
+
| -------- | --------------------------------------- |
|
|
17
|
+
| `--json` | Add a machine-readable record on stdout |
|
|
18
|
+
|
|
19
|
+
## The run file
|
|
20
|
+
|
|
21
|
+
The run is a JSON file rather than a key in a project config, because a route catalog is state a project holds and an interaction sequence is a script written for one question. It carries the viewport, the probes to run by default, and the steps.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"viewport": { "width": 1440, "heights": [900, 1200, 1500] },
|
|
26
|
+
"probes": ["focus", "details"],
|
|
27
|
+
"steps": [
|
|
28
|
+
{ "name": "on load", "kind": "wait", "ms": 0 },
|
|
29
|
+
{ "name": "open the menu", "kind": "click", "target": "#menu summary" },
|
|
30
|
+
{
|
|
31
|
+
"name": "reach the diagram",
|
|
32
|
+
"kind": "scroll",
|
|
33
|
+
"target": "figure",
|
|
34
|
+
"probes": ["diagram-geometry", "diagram-strokes"]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every step names what it did, and that name is carried onto each finding it produced, alongside the viewport. A step naming its own `probes` overrides the run-level list for that step alone. Omitting `probes` at the run level runs all four.
|
|
41
|
+
|
|
42
|
+
| Step kind | Fields | What it does |
|
|
43
|
+
| --------- | ------------------ | ---------------------------------------------- |
|
|
44
|
+
| `click` | `target` | Presses the first element the selector matches |
|
|
45
|
+
| `scroll` | `target` | Brings the first match into view |
|
|
46
|
+
| `fill` | `target`, `text` | Types into the first match |
|
|
47
|
+
| `tab` | `count` (optional) | Advances keyboard focus, once by default |
|
|
48
|
+
| `wait` | `ms` | Holds, for a state the page reaches alone |
|
|
49
|
+
|
|
50
|
+
Probes run after a step and never on arrival, so a run reaches the load state by opening with a `wait` step of its own, as the example above does. Nothing probes before the first step runs, which makes that leading step the only way to measure the page as it first painted, and naming it is what puts the load state on its own findings rather than under whatever ran next.
|
|
51
|
+
|
|
52
|
+
Write one in any project that does not have `canon capture`, which is toolkit-only and renders a single state from a committed source. Where capture runs, it already answers about arrival and a leading `wait` duplicates it. Where it does not, this command is the only thing measuring the page at all, and a run without that step reports every driven state and nothing about the one a visitor sees first.
|
|
53
|
+
|
|
54
|
+
Each height is driven in a context of its own from a fresh navigation, rather than by resizing the page the previous height already drove, since a sweep asks the same question of each height rather than a later question of an already-driven page.
|
|
55
|
+
|
|
56
|
+
## The probes
|
|
57
|
+
|
|
58
|
+
Four probes, a fixed catalog rather than a subject system a project extends. Each carries a class of false finding that a throwaway version of this command produced before it produced true ones, and a probe a project authored for itself would re-pay those.
|
|
59
|
+
|
|
60
|
+
| Probe | What it reports |
|
|
61
|
+
| ------------------ | ------------------------------------------------------------------------------------------ |
|
|
62
|
+
| `focus` | An element that takes keyboard focus and changes nothing visible |
|
|
63
|
+
| `details` | A `details` panel off the viewport, or a row under the tap minimum, both read open |
|
|
64
|
+
| `diagram-geometry` | An SVG label outside its frame, covered by a filled shape, or colliding with another label |
|
|
65
|
+
| `diagram-strokes` | An SVG label a stroke crosses, sampled along the stroke's own geometry |
|
|
66
|
+
|
|
67
|
+
`focus` presses Tab before it reads anything. `:focus-visible` does not match a scripted `.focus()` once the page has taken a pointer interaction, and a driver clicks by definition, so a reader without that press reports every correctly styled element as unstyled. It also ignores an outline width, color, or offset moving underneath an outline that resolves to `none`, which paints nothing.
|
|
68
|
+
|
|
69
|
+
`details` discovers every `details` on the page rather than taking a selector, measures each shut and again open, and judges only the open reading. A shut menu still reports a layout box and the box describes the trigger rather than the panel, so the shut numbers travel in the record as context. Discovery is automatic because the one real menu defect in the originating run was missed by a hand-picked probe: nobody thought to name that menu.
|
|
70
|
+
|
|
71
|
+
Both diagram probes read after `document.fonts.ready` and never compute from the markup. A generated page is routinely authored against one font and restyled to another, so a label that cleared a line at its authored width can overlap it once rendered. Every rect either probe compares is inset vertically and never horizontally, since a text rect is loose above and below the glyphs and tight to them left and right, and a horizontal inset passes a real overrun.
|
|
72
|
+
|
|
73
|
+
`diagram-strokes` is separate from `diagram-geometry` because a bounding box cannot answer for a stroke. A probe comparing filled boxes filters out every `line` and every `fill="none"` panel border before it compares, and a diagonal connector's box covers most of a diagram while the stroke itself touches almost none of it. Both paint orders are findings: a stroke drawn after a label crosses the glyphs out, and one drawn before it shows through them, since SVG text carries no plate of its own.
|
|
74
|
+
|
|
75
|
+
## Viewport heights are never defaulted
|
|
76
|
+
|
|
77
|
+
A run with no `viewport.heights` refuses. The heights that separate a passing render from a failing one belong to the layout being driven, and the only evidence behind any default this could ship is one fixture's failure range, too narrow a sample to hand every later caller a number they never chose. A scroll rail that skipped its middle sections passed at 900 and failed at 1200 and 1500, so one height reports clean over a live defect.
|
|
78
|
+
|
|
79
|
+
## What it reports
|
|
80
|
+
|
|
81
|
+
It reports findings and never gates, and the exit code says only whether the drive completed. Every probe here carries a class of false finding already paid for once, so a command ending a build on its own reading makes a claim this catalog has not earned. A caller that wants a verdict reads `findings` off the `--json` record and decides for itself.
|
|
82
|
+
|
|
83
|
+
| Reason | What it means |
|
|
84
|
+
| -------------------- | ------------------------------------------------------ |
|
|
85
|
+
| `no-run-file` | Nothing readable at the path given |
|
|
86
|
+
| `unreadable-plan` | The run file is not valid JSON |
|
|
87
|
+
| `no-steps` | The run names no step |
|
|
88
|
+
| `no-probes` | The run names an empty probe list |
|
|
89
|
+
| `unknown-probe` | The run names a probe this build does not ship |
|
|
90
|
+
| `bad-step` | A step is missing what the driver needs to perform it |
|
|
91
|
+
| `no-viewport` | The run declares no viewport |
|
|
92
|
+
| `no-width` | The viewport has no width to wrap at |
|
|
93
|
+
| `no-heights` | The viewport names no height, which is never defaulted |
|
|
94
|
+
| `engine-missing` | The browser engine is not installed in this project |
|
|
95
|
+
| `browser-missing` | The engine is installed and its browser binary is not |
|
|
96
|
+
| `server-unreachable` | Nothing answered at the URL, so no state was reached |
|
|
97
|
+
| `drive-failed` | The drive failed against a reachable page |
|
|
98
|
+
|
|
99
|
+
An unreachable page refuses rather than returning an empty report, since nothing measured and nothing found read the same to anything counting findings.
|
|
100
|
+
|
|
101
|
+
The browser binary installs separately from the package, once, with `bunx playwright install chromium`. The command ships to targets like `demo` and `inventory` rather than staying toolkit-only like `capture`, because its whole purpose is measuring a page inside someone else's project. It reaches nothing under `src/capture/`, which the package excludes, so the two surfaces move independently.
|
|
102
|
+
|
|
103
|
+
See `commands.md` for where this sits among the browser commands, and `capture.md` for the single-state render it adds an axis to.
|
package/docs/agents/index.md
CHANGED
|
@@ -18,6 +18,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
18
18
|
- [Self-stated counts](counts.md): Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
|
|
19
19
|
- [Demo](demo.md): Compiling a screencast draft into a runnable plan, driving a served application to a recording and a still, the pointer the recording paints, and what each refusal reports
|
|
20
20
|
- [Docs](docs.md): How canon docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
|
|
21
|
+
- [Driver](driver.md): Walking a page through named interactions, the probe catalog and the false finding each one carries, why viewport heights are never defaulted, and what each refusal reports
|
|
21
22
|
- [Merge gate](gate.md): Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
|
|
22
23
|
- [Indexes](indexes.md): Flags, exit codes, and JSON shape for canon indexes regen, plus when it auto-stages what it rewrote
|
|
23
24
|
- [Install and sync](install-and-sync.md): What each install and sync verb writes, refuses, or leaves alone, and how drift is attributed in a target project
|
|
@@ -27,7 +28,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
27
28
|
- [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
|
|
28
29
|
- [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
|
|
29
30
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
30
|
-
- [Records](records.md):
|
|
31
|
+
- [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, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
31
32
|
- [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
|
|
32
33
|
- [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
|
|
33
34
|
- [Routing report](routing.md): Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
|
package/docs/agents/records.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Records
|
|
3
|
-
description:
|
|
3
|
+
description: 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, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Records
|
|
7
7
|
|
|
8
|
+
## Record roots
|
|
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 `.claude/`. The scratch folder is the one name that differs by root, spelled `.claude/.tmp` and `.canon/tmp`, since inside a dotted root the leading dot hides nothing already hidden.
|
|
11
|
+
|
|
12
|
+
The read order and the creation default disagree deliberately. The gitignored record folders are moving to a root of their own, and the CLI learns to read both roots in a release that ships ahead of the move, so the binary a session already holds knows where to look by the time a tree relocates. Creating under the new root before then would write records to a root whose ignore line may not have reached a project yet, and it would split one project's records across two roots with no verb able to reconcile them. The move flips the default and nothing else.
|
|
13
|
+
|
|
14
|
+
A caller never spells a record root by hand for the same reason. A path written as `.claude/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
|
+
|
|
16
|
+
A refusal names every root it looked at, so a message reading `no-folder` says where a write would land as well as where the read failed.
|
|
17
|
+
|
|
8
18
|
## Validate
|
|
9
19
|
|
|
10
20
|
`canon records validate <kind>` reports where a file and the standard governing it disagree. Five kinds are gitignored folders under `.claude/`: `plans`, `groundwork`, `intake`, `memory`, and `teach`. The sixth is `standards`, the authoring corpus, which is tracked and installed rather than scratch.
|
|
@@ -56,7 +66,7 @@ A section runs to the next marker-shaped line whatever it names, so a plan carry
|
|
|
56
66
|
|
|
57
67
|
### Exit codes and refusals
|
|
58
68
|
|
|
59
|
-
Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so the `standards` refusal names
|
|
69
|
+
Exit codes: `0` every check passed, `1` refused, `2` at least one record carries a finding. A `reason` field carries which gate fired: `no-folder` when none of the kind's directories exist, and `unknown-kind` when the argument names no published kind. A `no-folder` message names every candidate, so a record kind's refusal names both record roots and the `standards` refusal names the authoring root and the installed copy.
|
|
60
70
|
|
|
61
71
|
An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `findings` array and its `reason` rather than the exit when a skill consumes this.
|
|
62
72
|
|
|
@@ -96,7 +106,7 @@ Exit codes: `0` nothing carried a known transform, or `--write` repaired everyth
|
|
|
96
106
|
|
|
97
107
|
## Size
|
|
98
108
|
|
|
99
|
-
`canon records size` reports what each record folder holds and how much of it is recent. It reads the ten backed folders named under Push and pull, plus
|
|
109
|
+
`canon records size` reports what each record folder holds and how much of it is recent. It reads the ten backed folders named under Push and pull, plus the scratch folder, and it gates nothing.
|
|
100
110
|
|
|
101
111
|
```bash
|
|
102
112
|
canon records size
|
|
@@ -116,11 +126,11 @@ The reading carries two windows rather than one, at 7 and 30 days. A single wind
|
|
|
116
126
|
|
|
117
127
|
Nothing fails on a number here. A record folder has no correct size, so the reading is a number to notice rather than a threshold to gate, and the point of the verb is that the next reading is taken by a command instead of by someone remembering to count the folder. The memory pen went from 44 entries to 236 between two readings taken by hand two weeks apart, which is the measurement this replaces.
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
The scratch folder is read here and skipped by a backup, because deletable without loss is not the same as empty. The routing handoffs and the memory archive both sit there and both accumulate. `.claude/.records.git` stays out because it is the backup history rather than a record, and `.claude/worktrees/` stays out because each entry is a checkout of the project with its own removal verb, and one of them outweighs every record folder combined.
|
|
120
130
|
|
|
121
131
|
The window counts read `mtime`, so what they report is a file written inside the window rather than one created there. An entry edited long after it landed reads as recent, which overstates growth and never understates it, and these folders are append-mostly so the two readings agree on nearly every file. The one reading that is wrong rather than early is a machine restored by `canon records pull`, which resets the work tree hard and re-dates every file it writes, so a window taken there counts the restore. Nothing on the filesystem separates the two, since a restored file is new by every stamp it carries.
|
|
122
132
|
|
|
123
|
-
Exit codes: `0` the reading completed, `1` refused. The one refusal is `no-folder`, raised when the
|
|
133
|
+
Exit codes: `0` the reading completed, `1` refused. The one refusal is `no-folder`, raised when the project holds neither record root. A project holding a root and no records is empty rather than absent, and each folder's own `present` flag already says which of the ten it carries.
|
|
124
134
|
|
|
125
135
|
## Push and pull
|
|
126
136
|
|
|
@@ -132,9 +142,9 @@ canon records push --json
|
|
|
132
142
|
canon records pull
|
|
133
143
|
```
|
|
134
144
|
|
|
135
|
-
The backed folders are `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, all under
|
|
145
|
+
The backed folders are `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, all under whichever record root the project carries. Eight of them are the Claude ignore group the claude manifest ships, minus three entries: `.claude/.tmp`, which is deletable without loss, `.claude/worktrees/`, whose contents belong to the project repository already, and `.claude/.records.git/`, which is the history the rest are pushed into. `diagrams` is the one that group does not carry at all, since a target still tracks its own copies, which is why the list is spelled out rather than derived. Each name is a top-level record folder and every archive sits inside the one it archives, so the list stays at one entry per surface however many archives appear. It is a constant rather than configuration, and it deliberately does not match the six record kinds `validate` hardcodes.
|
|
136
146
|
|
|
137
|
-
Records are gitignored by design, so the history lives in a second git directory at `.
|
|
147
|
+
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 nine 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.
|
|
138
148
|
|
|
139
149
|
### Setup
|
|
140
150
|
|
package/docs/agents/tasks.md
CHANGED
|
@@ -51,7 +51,7 @@ canon tasks archive --pull-request 673 --json | jq -r 'if .ok then .task else .r
|
|
|
51
51
|
|
|
52
52
|
The record carries `location`, one of `unstated`, `live`, `archived`, or `outside`, and `citedBy`, the other live tasks whose `Plan:` line lands on the same file. Exit codes: `0` read, `1` refused with `no-board` or `no-match`.
|
|
53
53
|
|
|
54
|
-
The target resolves against
|
|
54
|
+
The target resolves against the board folder and against the project root both, so `../plans/x.md` and `.claude/plans/x.md` land on the same file and one plan two tasks spelled differently counts once. Containment is tested at both record roots rather than at the one this tree resolves at, since a line somebody wrote against a root the tree has since left is still a path into the plans folder, and reading it as outside would report a shipped plan as still live. `docs/agents/records.md` states the read order.
|
|
55
55
|
|
|
56
56
|
`canon tasks archive` gates on this same answer, so a caller wanting the count reads it here rather than scanning the board. The `claude-docs` plans sweep is the exception and still states the rule in its own body, because a plugin skill reaches a target on merge while the CLI reaches one on release, so a sweep calling a verb the installed `canon` predates gets no record back and archives nothing.
|
|
57
57
|
|
package/package.json
CHANGED
|
@@ -117,11 +117,17 @@ for entry in "${shipped[@]}"; do
|
|
|
117
117
|
done
|
|
118
118
|
|
|
119
119
|
# Claude-scoped patterns this repository ignores that the manifest does not
|
|
120
|
-
# ship. Scoped to
|
|
121
|
-
# nothing about `node_modules/` or `.env`.
|
|
120
|
+
# ship. Scoped to the two record roots because the manifest is the claude stack
|
|
121
|
+
# and says nothing about `node_modules/` or `.env`.
|
|
122
|
+
#
|
|
123
|
+
# `.canon` is read as a bare root as well as a prefix, since it is one line
|
|
124
|
+
# covering a whole tree where `.claude/` is thirteen lines naming folders inside
|
|
125
|
+
# a root that also holds tracked content. Leaving it out is what would let a new
|
|
126
|
+
# entry sit outside the only stage comparing the two lists, which is the
|
|
127
|
+
# direction that goes silently blind.
|
|
122
128
|
for pattern in "${ignored[@]}"; do
|
|
123
129
|
case "$pattern" in
|
|
124
|
-
.claude/*) ;;
|
|
130
|
+
.claude/* | .canon | .canon/*) ;;
|
|
125
131
|
*) continue ;;
|
|
126
132
|
esac
|
|
127
133
|
contains "$pattern" "${shipped[@]}" && continue
|
package/src/audits/baseline.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { AuditResult } from '@/audits/catalog'
|
|
|
15
15
|
* that installs the CLI. A baseline in the package would hand a target this
|
|
16
16
|
* repository's counts to measure its own tree against.
|
|
17
17
|
*/
|
|
18
|
-
export const BASELINE_REL = join('.claude', '
|
|
18
|
+
export const BASELINE_REL = join('.claude', 'canon', 'baseline.json')
|
|
19
19
|
|
|
20
20
|
export interface Baseline {
|
|
21
21
|
/** The day the record was taken, as `YYYY-MM-DD`. */
|
package/src/browser/engine.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What every browser-driving command needs to know before it can report a
|
|
3
|
-
* failure honestly: how the binary is installed,
|
|
4
|
-
*
|
|
3
|
+
* failure honestly: how the binary is installed, how each of the two ways it
|
|
4
|
+
* can be absent reads when it is thrown, and the one press that decides whether
|
|
5
|
+
* a focus reading means anything.
|
|
5
6
|
*
|
|
6
|
-
* The two are different states with different remedies. A package that
|
|
7
|
-
* resolved means the target installed the CLI without the engine, and a
|
|
8
|
-
* that was never downloaded means the engine is present and its browser
|
|
9
|
-
* Both were spelled inside `src/demo/` when `demo` was the only command
|
|
10
|
-
* a browser, and a second command is what makes them shared rather than
|
|
7
|
+
* The two absences are different states with different remedies. A package that
|
|
8
|
+
* never resolved means the target installed the CLI without the engine, and a
|
|
9
|
+
* binary that was never downloaded means the engine is present and its browser
|
|
10
|
+
* is not. Both were spelled inside `src/demo/` when `demo` was the only command
|
|
11
|
+
* driving a browser, and a second command is what makes them shared rather than
|
|
12
|
+
* local.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
/** Fetches the browser revision the pinned engine expects. */
|
|
@@ -38,3 +40,44 @@ export function isEngineMissing(error: unknown): boolean {
|
|
|
38
40
|
error.code === 'ERR_MODULE_NOT_FOUND'
|
|
39
41
|
)
|
|
40
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Separates a server nobody started from a page that failed for its own
|
|
46
|
+
* reasons. The first is a precondition no browser command can create for
|
|
47
|
+
* itself, and reporting it as an empty reading says the site gives no answers
|
|
48
|
+
* when nothing was ever asked.
|
|
49
|
+
*
|
|
50
|
+
* It moved here alongside the modality press, when `@/driver/drive` became the
|
|
51
|
+
* second module needing it.
|
|
52
|
+
*/
|
|
53
|
+
export function isServerUnreachable(error: unknown): boolean {
|
|
54
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
55
|
+
return /ERR_CONNECTION_REFUSED|ERR_NAME_NOT_RESOLVED|ERR_CONNECTION_RESET|ERR_EMPTY_RESPONSE/i.test(
|
|
56
|
+
text,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The narrowest shape the press below needs, declared here rather than imported
|
|
62
|
+
* as `Page`, so this module keeps its type surface free of the engine and every
|
|
63
|
+
* command reading a refusal string still loads without resolving it.
|
|
64
|
+
*/
|
|
65
|
+
export interface KeyboardPage {
|
|
66
|
+
readonly keyboard: { press(key: string): Promise<void> }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Puts the page in keyboard modality, which is the precondition for reading a
|
|
71
|
+
* `:focus-visible` rule at all. A scripted `.focus()` on its own leaves the
|
|
72
|
+
* browser in pointer modality, where that rule does not match, so a reader
|
|
73
|
+
* calling it alone reports every correctly styled element as unstyled.
|
|
74
|
+
*
|
|
75
|
+
* It moved here when `@/driver/probes/focus` became the second call site,
|
|
76
|
+
* matching how the launch-failure split moved out of `src/demo/` once
|
|
77
|
+
* `canon inventory` became the second command driving a browser. One press
|
|
78
|
+
* covers the page rather than the element that happens to hold focus, so it
|
|
79
|
+
* survives the blur every reader takes next.
|
|
80
|
+
*/
|
|
81
|
+
export async function enterKeyboardModality(page: KeyboardPage): Promise<void> {
|
|
82
|
+
await page.keyboard.press('Tab')
|
|
83
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { register as capture } from '@/commands/capture'
|
|
|
18
18
|
import { register as serve } from '@/commands/serve'
|
|
19
19
|
import { register as demo } from '@/commands/demo'
|
|
20
20
|
import { register as inventory } from '@/commands/inventory'
|
|
21
|
+
import { register as driver } from '@/commands/driver'
|
|
21
22
|
import { register as feedback } from '@/commands/feedback'
|
|
22
23
|
import { register as transcripts } from '@/commands/transcripts'
|
|
23
24
|
import { register as tasks } from '@/commands/tasks'
|
|
@@ -70,6 +71,7 @@ function showHelp(): void {
|
|
|
70
71
|
`${GREY}│${NC} serve [dir] ${GREY}# Serve a directory over localhost and print the preview link${NC}`,
|
|
71
72
|
`${GREY}│${NC} demo [cmd] ${GREY}# Record a running app (compile, run)${NC}`,
|
|
72
73
|
`${GREY}│${NC} inventory [subj] ${GREY}# Report one computed property across every route${NC}`,
|
|
74
|
+
`${GREY}│${NC} drive <url> <run> ${GREY}# Walk a page through named interactions and measure each state${NC}`,
|
|
73
75
|
`${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .claude/review/feedback/${NC}`,
|
|
74
76
|
`${GREY}│${NC} transcripts <url> ${GREY}# Fetch a YouTube transcript with metadata frontmatter${NC}`,
|
|
75
77
|
`${GREY}│${NC} tasks [cmd] ${GREY}# Task board commands (archive)${NC}`,
|
|
@@ -121,6 +123,7 @@ function showHelp(): void {
|
|
|
121
123
|
`${GREY}│${NC} canon capture assets/install.html`,
|
|
122
124
|
`${GREY}│${NC} canon serve .claude/teach`,
|
|
123
125
|
`${GREY}│${NC} canon inventory focus --json`,
|
|
126
|
+
`${GREY}│${NC} canon drive http://localhost:4173 run.json --json`,
|
|
124
127
|
`${GREY}│${NC} pbpaste | canon feedback`,
|
|
125
128
|
`${GREY}│${NC} canon transcripts https://youtu.be/VIDEO_ID`,
|
|
126
129
|
`${GREY}│${NC} canon tasks archive --pull-request 673 --json`,
|
|
@@ -177,6 +180,7 @@ capture(program)
|
|
|
177
180
|
serve(program)
|
|
178
181
|
demo(program)
|
|
179
182
|
inventory(program)
|
|
183
|
+
driver(program)
|
|
180
184
|
feedback(program)
|
|
181
185
|
transcripts(program)
|
|
182
186
|
tasks(program)
|
package/src/commands/claude.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { copyPreservingMode } from '@/copy'
|
|
52
52
|
import { execScript } from '@/exec'
|
|
53
53
|
import { PROJECT_ROOT } from '@/project-root'
|
|
54
|
+
import { recordDir } from '@/record-root'
|
|
54
55
|
import { isDirectory, resolveTarget } from '@/target'
|
|
55
56
|
import { injectGitignore, pruneGitignore } from '@/tooling/inject'
|
|
56
57
|
import {
|
|
@@ -491,13 +492,18 @@ async function runSync(target: string): Promise<number> {
|
|
|
491
492
|
const resolved = resolveTarget(target, PROJECT_ROOT)
|
|
492
493
|
if (typeof resolved === 'number') return resolved
|
|
493
494
|
|
|
495
|
+
// A record folder resolves at either root, so a migrated target is reported as
|
|
496
|
+
// seeded rather than sent to `canon claude init` to re-create records it
|
|
497
|
+
// already holds. The three seeded files and `wireframes` are tracked and stay
|
|
498
|
+
// at `.claude/`, which the resolver answers for them anyway, since nothing
|
|
499
|
+
// ever creates a second root copy for a name that does not move.
|
|
494
500
|
logStep('Seeded')
|
|
495
501
|
for (const name of SEEDED_FILES) {
|
|
496
502
|
if (existsSync(join(resolved, '.claude', name))) logInfo(name)
|
|
497
503
|
else logWarn(`${name} missing. Run \`canon claude init\``)
|
|
498
504
|
}
|
|
499
505
|
for (const name of SEEDED_DIRS) {
|
|
500
|
-
if (isDirectory(
|
|
506
|
+
if (isDirectory(recordDir(resolved, name))) logInfo(`${name}/`)
|
|
501
507
|
else logWarn(`${name}/ missing. Run \`canon claude init\``)
|
|
502
508
|
}
|
|
503
509
|
|
package/src/commands/context.ts
CHANGED
|
@@ -180,7 +180,7 @@ async function runAudit(
|
|
|
180
180
|
if (folders.length === 0) {
|
|
181
181
|
return refuse(
|
|
182
182
|
'no-folders',
|
|
183
|
-
`No audited folder found ${named ? 'under
|
|
183
|
+
`No audited folder found ${named ? 'under a record root or the project root' : 'under a record root'}, since resolving one needs its own index.md file. Looked for: ${names.join(', ')}.`,
|
|
184
184
|
gateOnly,
|
|
185
185
|
root,
|
|
186
186
|
opts.json ?? false,
|
|
@@ -200,7 +200,7 @@ async function runAudit(
|
|
|
200
200
|
if (gateOnly && cited.length === 0) {
|
|
201
201
|
return refuse(
|
|
202
202
|
'no-citation-scope',
|
|
203
|
-
`The citation check spells the
|
|
203
|
+
`The citation check spells the record-root prefixes and no audited folder resolved under one. Looked for: ${names.join(', ')}.`,
|
|
204
204
|
gateOnly,
|
|
205
205
|
root,
|
|
206
206
|
opts.json ?? false,
|
|
@@ -414,7 +414,7 @@ function reportCitations(
|
|
|
414
414
|
|
|
415
415
|
if (cited.length === 0) {
|
|
416
416
|
logInfo(
|
|
417
|
-
'Out of scope. The pattern spells the
|
|
417
|
+
'Out of scope. The pattern spells the record-root prefixes, and no audited folder resolved under one.',
|
|
418
418
|
)
|
|
419
419
|
return
|
|
420
420
|
}
|
package/src/commands/design.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
+
import { creationRel } from '@/record-root'
|
|
4
5
|
import { renderDesignDoc } from '@/design/render'
|
|
5
6
|
import { palette } from '@/ui'
|
|
6
7
|
|
|
@@ -13,7 +14,11 @@ export function register(program: Command): void {
|
|
|
13
14
|
.command('render')
|
|
14
15
|
.description('Render DESIGN.md tokens to HTML and CSS preview')
|
|
15
16
|
.option('-s, --source <path>', 'Source DESIGN.md path', '.claude/DESIGN.md')
|
|
16
|
-
.option(
|
|
17
|
+
.option(
|
|
18
|
+
'-o, --out <path>',
|
|
19
|
+
'Output directory',
|
|
20
|
+
creationRel('review', 'design'),
|
|
21
|
+
)
|
|
17
22
|
.action((opts: { source: string; out: string }) => {
|
|
18
23
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
19
24
|
const outDir = resolve(process.cwd(), opts.out)
|