@erclx/canon 4.114.0 → 4.115.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/document-health/references/axes.md +1 -1
- package/claude/skills/target-setup/SKILL.md +1 -1
- package/claude/skills/target-setup/references/detect.md +43 -9
- package/claude/skills/teach-workspace/SKILL.md +5 -3
- package/docs/agents/audits.md +4 -2
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit-checks.md +6 -6
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/context-classify.md +1 -1
- package/docs/agents/teach.md +6 -0
- package/docs/target-projects.md +4 -2
- package/package.json +1 -1
- package/src/audits/catalog.ts +52 -0
- package/src/commands/context.ts +30 -10
- package/src/commands/teach.ts +4 -1
- package/src/context/audit.ts +54 -18
- package/src/teach/nav.ts +203 -9
- package/standards/teach.md +13 -12
- package/tooling/go/configs/.golangci.yml +25 -0
- package/tooling/go/configs/scripts/verify.sh +95 -0
- package/tooling/go/manifest.toml +13 -0
- package/tooling/go/reference.md +60 -0
- package/tooling/go/seeds/.cspell/tech-stack.txt +16 -0
- package/tooling/go/seeds/smoke_test.go +9 -0
- package/tooling/php/configs/.php-cs-fixer.dist.php +18 -0
- package/tooling/php/configs/phpstan.neon +5 -0
- package/tooling/php/configs/phpunit.xml +18 -0
- package/tooling/php/configs/scripts/verify.sh +100 -0
- package/tooling/php/manifest.toml +15 -0
- package/tooling/php/reference.md +74 -0
- package/tooling/php/seeds/.cspell/tech-stack.txt +5 -0
- package/tooling/php/seeds/tests/SmokeTest.php +13 -0
|
@@ -40,7 +40,7 @@ Weight is a judgment a reader settles rather than a defect, which the verb state
|
|
|
40
40
|
|
|
41
41
|
**Evidence class:** `measured` for the audit's findings, `classified` for the sweep's verdicts.
|
|
42
42
|
|
|
43
|
-
The audit answers required sections, entry length, citation resolution, reference form, catalog tables, provenance, and index drift. A citation resolving to nothing is a fact and gates. Entry length, reference form, table shape, and provenance are judgments under every flag.
|
|
43
|
+
The audit answers required sections, entry length, citation resolution, reference form, catalog tables, provenance, and index drift. Required sections reach wireframes as well as context entries, naming each wireframe short of the regions, states, copy, or exclusions section its standard requires, which is how a surface on an older wireframe shape reports rather than reading as clean. A citation resolving to nothing is a fact and gates. Entry length, reference form, table shape, and provenance are judgments under every flag.
|
|
44
44
|
|
|
45
45
|
The sweep classifies each section as keep, replace or rewrite, history, or move. A `move` verdict is the placement finding proper, naming content sitting in the wrong document. Each verdict carries its own confidence, which is why the class is `classified` rather than `measured`, and a low-confidence `move` is a prompt to look rather than an instruction to cut.
|
|
46
46
|
|
|
@@ -92,7 +92,7 @@ Step 2: `canon tooling sync <tooling-stack> --write` installs stack deps, script
|
|
|
92
92
|
CANON_NON_INTERACTIVE=1 canon tooling sync <tooling-stack> <target> --write
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, prettier, cspell, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped.
|
|
95
|
+
Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, prettier, cspell, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped. Take the subtree commands from the per-root rows the preview printed rather than picking folders by hand. Detection found each root and its stack, and the two commands below show the shape.
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
98
|
CANON_NON_INTERACTIVE=1 canon tooling sync vite-react ./frontend --skip base --write
|
|
@@ -27,19 +27,53 @@ since the chain reaches the tooling resolution a moment later.
|
|
|
27
27
|
|
|
28
28
|
## Detect
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
A monorepo keeps its languages in subfolders, so read the markers from every
|
|
31
|
+
folder to depth two rather than from the root alone. Collect the language roots
|
|
32
|
+
first:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git ls-files --cached --others --exclude-standard \
|
|
36
|
+
| grep -E '(^|/)(package\.json|pyproject\.toml|go\.mod|composer\.json|Cargo\.toml)$' \
|
|
37
|
+
| awk -F/ 'NF <= 3'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every folder holding a marker is a language root, and the repository root is
|
|
41
|
+
one when it holds a marker itself. Going through git keeps ignored trees such as
|
|
42
|
+
`node_modules/`, `vendor/`, and `.venv/` off the list, since each carries
|
|
43
|
+
markers of its own. Outside a git repository, fall back to
|
|
44
|
+
`find . -maxdepth 3 \( -path '*/.*' -o -name node_modules -o -name vendor \) -prune -o -type f -print`
|
|
45
|
+
filtered to the same five names.
|
|
46
|
+
|
|
47
|
+
Depth two reaches a service folder at depth one and a workspace package under
|
|
48
|
+
`packages/<name>/` at depth two. Deeper, the walk starts finding fixtures and
|
|
49
|
+
examples as false roots.
|
|
50
|
+
|
|
51
|
+
Then read these per language root in parallel, skipping any that do not exist:
|
|
31
52
|
|
|
32
53
|
- `package.json`: `dependencies` and `devDependencies`
|
|
33
|
-
-
|
|
54
|
+
- The root's own configs: `astro.config.*`, `next.config.*`, `vite.config.*`, `tailwind.config.*`, `tsconfig.json`, `pyproject.toml`, `go.mod`, `composer.json`, `Cargo.toml`
|
|
55
|
+
|
|
56
|
+
A non-JavaScript marker outranks a `package.json` in the same folder. A Go or
|
|
57
|
+
Composer service often carries a `package.json` only to host scripts, so that
|
|
58
|
+
folder is a Go or PHP root rather than a JavaScript one.
|
|
59
|
+
|
|
60
|
+
Read these once, from the repository root:
|
|
61
|
+
|
|
34
62
|
- `canon/REQUIREMENTS.md` and `canon/ARCHITECTURE.md` if present
|
|
35
63
|
- Directory structure via `ls -1` of the project root and `src/` if present
|
|
36
64
|
|
|
37
65
|
## Resolve arguments
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
Governance installs once, at the repository root, and a rule's `paths:` globs
|
|
68
|
+
already reach every subfolder. Tooling installs per folder. So resolve one
|
|
69
|
+
governance stack for the whole repository and one tooling stack per language
|
|
70
|
+
root.
|
|
71
|
+
|
|
72
|
+
- **Stack:** pick the closest governance stack by matching the repository root's detected runtime or framework against stack names in the catalog. If the root matched nothing, `base` is the stack, including when subfolders matched, since no subfolder owns the repository and picking one by order would state a preference nobody gave. Mark it `fallback` in the preview only when no root matched anywhere.
|
|
73
|
+
- **Tooling stack:** for each language root, 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` for a root with no framework or language match, and carry that fallback into its preview row.
|
|
74
|
+
- **Next.js:** map `next` in a root's `package.json` dependencies, or a `next.config.ts`/`.js`/`.mjs` file in that root, to the `nextjs` governance stack and the `nextjs` tooling stack. The dependency name does not match either stack name by itself.
|
|
75
|
+
- **Extras:** identify technologies not already covered by the picked governance stack, across every language root. A subfolder that matched a governance stack of its own contributes that stack's rules, read from `canon gov list --json`, and every root contributes its language rule. For each technology, find a rule whose `description` or `paths` points at it and pass it via `--add`. Dedupe against the picked stack's own rules. Do not add a rule the stack already pulls in.
|
|
76
|
+
- **Subfolder tooling:** a language root below the repository root takes its own `canon tooling sync <stack> <path> --skip base`, since `canon init` already lands `base` once at the root. Leave a root whose stack resolves to `base` out of the commands, since that sync would land nothing. A repository-root `package.json` hosting only formatters and hooks is that case, and it adds no row.
|
|
43
77
|
- **Skip (`--skip`):** `wiki` installs by default. Add `--skip wiki` only when the user explicitly wants it left out.
|
|
44
78
|
|
|
45
79
|
## Gap handling
|
|
@@ -67,10 +101,10 @@ toolkit repository, and option 2 hands the project-local file to `create-rule`.
|
|
|
67
101
|
|
|
68
102
|
Before executing, output:
|
|
69
103
|
|
|
70
|
-
- **Detected:** each technology with its evidence file
|
|
104
|
+
- **Detected:** each technology with its evidence file, path included, so a subfolder marker reads as one
|
|
71
105
|
- **Stack:** picked governance stack and resolved rule count. Mark it `fallback` when no detected runtime or framework matched a catalog name.
|
|
72
|
-
- **Tooling stack:** picked tooling stack
|
|
73
|
-
- **Extras:** each `--add` rule with a one-line reason
|
|
106
|
+
- **Tooling stack:** one row per language root, each carrying its path, its picked tooling stack, a `fallback` mark on the same test, and its own command. On the repository root's row, name what `base` lands: configs, seeds, and gitignore entries in every case, plus the JavaScript development dependencies, scripts, and hook activation wherever a `package.json` exists to carry them. A project outside that ecosystem runs none of the second group and keeps the first. Omit these rows entirely on the `gov` phase, which syncs no tooling.
|
|
107
|
+
- **Extras:** each `--add` rule with a one-line reason, listed in full. A second matched stack reaches the install only through this list, since `canon init --stack` takes one stack, so a shortened list reads as that stack dropped.
|
|
74
108
|
- **Skip:** any `--skip` entries with reason
|
|
75
109
|
- **Target:** resolved target path
|
|
76
110
|
- **Commands:** the full chain that will run
|
|
@@ -86,7 +86,7 @@ Reading the last record alone is what the schedule replaces. A topic missed thre
|
|
|
86
86
|
Two outputs with two lifetimes, and the split decides the format.
|
|
87
87
|
|
|
88
88
|
- A lesson is a self-contained page carrying its own quiz, a teach-back block, and the feedback for each question. It embeds one shared stylesheet rather than restating styles, and it is disposable and never promoted.
|
|
89
|
-
- A reference page goes to `reference/<slug>.md`, written for a reader with no learner in it. This is the half that survives the workspace, so it is written in markdown to pass the authoring gates a promotion would put it through.
|
|
89
|
+
- A reference page goes to `reference/<slug>.md`, written for a reader with no learner in it. This is the half that survives the workspace, so it is written in markdown to pass the authoring gates a promotion would put it through. `canon teach nav` renders it to a `reference/<slug>.html` sibling, and a lesson links that sibling, since a served `.md` opens as plain text.
|
|
90
90
|
|
|
91
91
|
Resolve the lesson before writing it, rather than composing its name or its quiz order by hand:
|
|
92
92
|
|
|
@@ -167,12 +167,14 @@ Follow `${CLAUDE_SKILL_DIR}/references/lesson-craft.md` for what makes a lesson
|
|
|
167
167
|
|
|
168
168
|
### Hand over a link, never a path
|
|
169
169
|
|
|
170
|
-
A lesson is a page carrying a script, and an editor preview cannot run it, so a path alone opens markup with no working quiz. Serve the teach root and give the learner a link they can click:
|
|
170
|
+
A lesson is a page carrying a script, and an editor preview cannot run it, so a path alone opens markup with no working quiz. Serve the teach root and give the learner a link they can click. Take the serve line from the `Open` step of `canon teach list <topic>`, which resolves the teach folder against the directory the session runs in:
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
canon
|
|
173
|
+
canon teach list <topic>
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
Run the line it prints with `--json` appended, rather than writing `canon serve .canon/teach` by hand. `canon serve` resolves its folder against the cwd, so that literal serves an absent folder from a linked worktree, where the list verb reads the main worktree root.
|
|
177
|
+
|
|
176
178
|
Start it in the background so the session keeps going, and read `url` off the record rather than composing one. The verb walks past a port already in use, so the port it took is exactly the half a guessed URL gets wrong. Report the refusal and its `reason` when `ok` is false, and report it rather than proceeding silently when the verb does not resolve at all, which is an installed CLI predating it.
|
|
177
179
|
|
|
178
180
|
Do this on every run that opens or resumes a workspace, including one that writes no lesson, since the learner's route into what is already there is the same link.
|
package/docs/agents/audits.md
CHANGED
|
@@ -24,11 +24,13 @@ canon audits list --json
|
|
|
24
24
|
|
|
25
25
|
## What it runs
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Twenty-one verbs, listed by `canon audits list`. Each runs once in its fullest form, and the aggregate reads that verb's own record rather than imposing a shared envelope on it. Every one of those records already has consumers naming its keys, so a common shape would be a breaking change bought for tidiness.
|
|
28
28
|
|
|
29
29
|
The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `canon deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's: three clean runs measured 44.7, 77.6, and 62.9 seconds, and a stalled lookup runs to an explicit 120-second timeout rather than to bun's own 299-second ceiling. `--corpus` is what keeps a caller from paying that cost when it wants only the corpora on this disk.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Eighteen of the twenty-one read a tree on this disk, the one added by `restated.md` and the one added by `census.md` among them, since the first reads four such trees against each other and the second counts every file in one. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction, and the one added by `label-coverage.md` reads a branch range against a map the project declares.
|
|
32
|
+
|
|
33
|
+
The placement entry runs `canon context classify sweep` pinned to `--backend off`, so it reads the regex layer alone whatever classifier setting the project resolves. A count that moves with whichever model a machine runs is not comparable across clones, and the model layer turned a sub-second read into one measured at about 17 minutes over this repository's context and wireframe sections on the authoring machine. It retains one tally per verdict, `keep`, `rewrite`, and `move`, and gates on nothing, since each verdict is a judgment. `document-health` is the surface that reads the same sections with the model.
|
|
32
34
|
|
|
33
35
|
Each is invoked as the CLI the caller is running rather than as a global `canon`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.
|
|
34
36
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -47,7 +47,7 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
|
|
|
47
47
|
| `canon teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
48
48
|
| `canon teach lesson` | Resolve the numbered path, shared stylesheet, mission success lines, and one quiz option order per question for the next lesson, writing nothing (`--json`) |
|
|
49
49
|
| `canon teach stylesheet` | Seed a workspace stylesheet from the design source, leaving an existing one alone without `--force` (`--json`) |
|
|
50
|
-
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page,
|
|
50
|
+
| `canon teach nav` | Rewrite the teach-root listing, a workspace's contents page, each lesson's chrome from its four marker regions, and each reference page's rendered HTML sibling (`--json`) |
|
|
51
51
|
| `canon teach render` | Render a JSON block list (heading, paragraph, list, raw) from stdin to HTML through the shared components, refusing `bad-input` by block index (`--json`) |
|
|
52
52
|
| `canon records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
53
53
|
| `canon records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
@@ -9,11 +9,11 @@ What each finding from `canon context audit` means. The command surface, its fla
|
|
|
9
9
|
|
|
10
10
|
## Required sections
|
|
11
11
|
|
|
12
|
-
The required-section check reports what does not declare `## Overview` and `## Layout`, the two sections `standards/context.md` marks required.
|
|
12
|
+
The required-section check reports what does not declare the sections its folder's standard requires. A context entry owes `## Overview` and `## Layout`, the two sections `standards/context.md` marks required. A wireframe owes `## Regions`, `## States`, `## Copy`, and `## Not on this surface`, the sections answering the four questions `standards/wireframes.md` tests a surface against. `## Behavior` is expected of a wireframe rather than required, since a static surface has no interaction to state. The lists are held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
|
|
13
13
|
|
|
14
|
-
Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
|
|
14
|
+
Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it. A wireframe answers for itself in every folder, nested or not, since its standard keeps one surface per file and a subfolder of wireframes groups surfaces rather than splitting one.
|
|
15
15
|
|
|
16
|
-
It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections
|
|
16
|
+
It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections`, the lists per folder name as `checkpoints.requiredSectionsByFolder`, and the context list alone as `checkpoints.requiredSections`, the flat key it carried before wireframes owed a section.
|
|
17
17
|
|
|
18
18
|
`--gate` promotes the finding to a failing exit code, which the seed stage runs and no other caller does. That mode needs an answer to the sanctioned omission above, so a file declaring `stub: true` in its frontmatter is dropped before the check and reported nowhere. Both are described in `context-audit.md`.
|
|
19
19
|
|
|
@@ -127,11 +127,11 @@ All three wireframe findings are printed under a bare run. The states-mismatch f
|
|
|
127
127
|
|
|
128
128
|
## Which folders each check reaches
|
|
129
129
|
|
|
130
|
-
The provenance
|
|
130
|
+
The provenance and narration checks cover `canon/context/` alone, the reference-form check covers the split folders inside it, the required-section check covers `canon/context/` and `canon/wireframes/`, length and the table finding reach every audited folder, and the wireframe states check covers `canon/wireframes/` alone.
|
|
131
131
|
|
|
132
|
-
What narrows the
|
|
132
|
+
What narrows the first two is stated in `standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
|
|
133
133
|
|
|
134
|
-
Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason:
|
|
134
|
+
Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: each list is one standard's own, so it reaches the folder that standard governs. The context and wireframe standards each state a set, and the diagram standard states none.
|
|
135
135
|
|
|
136
136
|
The same test is what moved depth and bullet weight out of this command entirely. A rule stated at the attribute tier reaches every markdown file, and a check reaching every markdown file has no reason to require a folder that resolves.
|
|
137
137
|
|
|
@@ -88,7 +88,7 @@ The pattern spells both record-root prefixes, so a citation into a folder that h
|
|
|
88
88
|
|
|
89
89
|
## The wireframe states gate
|
|
90
90
|
|
|
91
|
-
Every entry under `canon/wireframes/` carrying a `## States` table is checked one-to-one against its evidence folders, matching the table's `State` and `Evidence` columns by header text rather than position, so a project reordering the template's own columns still resolves. An entry with no such table
|
|
91
|
+
Every entry under `canon/wireframes/` carrying a `## States` table is checked one-to-one against its evidence folders, matching the table's `State` and `Evidence` columns by header text rather than position, so a project reordering the template's own columns still resolves. An entry with no such table has no rows to match, and the required-section check reports it as short of `## States` instead, so a project on an older wireframe shape is told what it lacks rather than read as clean.
|
|
92
92
|
|
|
93
93
|
An evidence cell resolves as the literal path it names, taken from the project root. A state whose cited folder does not exist is a states mismatch, and so is a folder sitting under a cited evidence root that no row names. A `not captured` cell is the one exception the standard admits, and it excuses that row from both readings. A `plaintext` sketch still carried once a state's evidence exists on disk is a third finding this check adds, reading the entry as a whole rather than one row.
|
|
94
94
|
|
|
@@ -96,4 +96,4 @@ Sweep mode's findings carry a `heading` field alongside `file` and use the sweep
|
|
|
96
96
|
| `0` | the run completed, whatever the findings say |
|
|
97
97
|
| `1` | refused: a bad range, an unreadable file, or a malformed flag |
|
|
98
98
|
|
|
99
|
-
Findings never set a failing exit code, in either mode. Diff mode refuses on a git range it cannot resolve. Both modes refuse on a file the run could not read.
|
|
99
|
+
Findings never set a failing exit code, in either mode. `canon audits run` carries sweep mode as its placement entry for the same reason, pinned to `--backend off` so the tally it retains is the regex layer's on every machine. `docs/agents/audits.md` states why. Diff mode refuses on a git range it cannot resolve. Both modes refuse on a file the run could not read.
|
package/docs/agents/teach.md
CHANGED
|
@@ -192,6 +192,12 @@ One detector decides both regions. A lesson carrying `<button class="opt"`, the
|
|
|
192
192
|
|
|
193
193
|
Splicing the stepper into a lesson of either shape was measured against a browser and reverted. Its rules match a button question, which can never hold a checked radio, so a lesson given both hides every question past the first for good and outranks the class the script reveals feedback with. Nothing else caught that: the markup is valid, the splice succeeds, and the page renders.
|
|
194
194
|
|
|
195
|
+
### Reference pages
|
|
196
|
+
|
|
197
|
+
Each reference page is rendered to a `reference/<slug>.html` sibling carrying the same chrome, and the contents page links that sibling rather than the markdown, which `canon serve` hands over as plain text. The render drops the frontmatter, turns tables into `<table>` markup, and escapes raw HTML written in the markdown rather than passing it through. A body with no H1 gets one from the frontmatter `title`. A relative link to another reference page's markdown points at that page's `.html` sibling, and one to the workspace `GLOSSARY.md` points at the glossary on the contents page, `../index.html#gloss`. Every other link, such as `../RESOURCES.md`, stays as written. The markdown file is never touched.
|
|
198
|
+
|
|
199
|
+
Every rendered page carries a `<meta name="generator" content="canon teach nav">` tag. A `.html` file under `reference/` whose markdown is gone is deleted when it carries that tag, and a hand-written HTML file there, carrying none, is left alone. The record's `reference` field counts the pages rendered. On a Bun with no `Bun.markdown`, each reference page lands in `skipped` with `Bun.markdown` as what is missing, and the rest of the run goes on.
|
|
200
|
+
|
|
195
201
|
## Opening a workspace
|
|
196
202
|
|
|
197
203
|
No `canon teach` verb serves the workspace. `canon serve` does, taking the teach root as its directory and the workspace contents page as its entry:
|
package/docs/target-projects.md
CHANGED
|
@@ -86,13 +86,15 @@ 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
|
|
89
|
+
Escalate only for real web apps. The `target-setup` skill reads `package.json` and the framework 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
|
+
|
|
91
|
+
In a monorepo the skill reads each folder holding a `package.json`, `pyproject.toml`, `go.mod`, `composer.json`, or `Cargo.toml` to depth two, skipping what git ignores. Each of those folders gets its own tooling stack and a `canon tooling sync <stack> <folder> --skip base` in the preview. Governance still installs once at the root, since a rule's `paths:` globs reach every subfolder, so a subfolder's language rules join the root install through `--add`.
|
|
90
92
|
|
|
91
93
|
`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
94
|
|
|
93
95
|
Markdown-heavy projects, CLI tools, docs sites, research notebooks, and scripting repos stay on `base`. Escalation is a ceiling move, not a default.
|
|
94
96
|
|
|
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
|
|
97
|
+
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 ship tooling stacks but no governance stack, so a project in either takes the `go` or `php` tooling stack, lands on `base` for governance, and picks up its language and testing rules through `--add`, for example `canon gov install base --add 130-go,335-testing-go .`
|
|
96
98
|
|
|
97
99
|
Run `canon tooling list --json` and `canon gov list --json` to see the current catalogs. Never hardcode stack names.
|
|
98
100
|
|
package/package.json
CHANGED
package/src/audits/catalog.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SkillsAuditRefusal } from '@/claude/skills-audit'
|
|
|
2
2
|
import type { RankRefusal } from '@/claude/skills-rank'
|
|
3
3
|
import type { ReachRefusal } from '@/claude/skills-reach'
|
|
4
4
|
import type { ContextAuditRefusal } from '@/context/audit'
|
|
5
|
+
import type { SweepVerdict } from '@/context/classify/patterns'
|
|
5
6
|
import type { AuditRefusal } from '@/deps/audit'
|
|
6
7
|
import type { RestatedRefusal } from '@/gov/restated'
|
|
7
8
|
import type { LabelAuditRefusal } from '@/labels/audit'
|
|
@@ -199,6 +200,45 @@ function architectureCounts(
|
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The retained key for each verdict the sweep can return. A record rather than
|
|
205
|
+
* a list, so the compiler fails here when the classifier gains a verdict,
|
|
206
|
+
* instead of that verdict's sections dropping out of the tally.
|
|
207
|
+
*/
|
|
208
|
+
const PLACEMENT_KEYS: Record<SweepVerdict, string> = {
|
|
209
|
+
KEEP: 'keep',
|
|
210
|
+
REWRITE: 'rewrite',
|
|
211
|
+
MOVE: 'move',
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isPlacementVerdict(value: unknown): value is SweepVerdict {
|
|
215
|
+
return typeof value === 'string' && Object.hasOwn(PLACEMENT_KEYS, value)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Tallies the sweep's findings by verdict, with a zero for a verdict nothing
|
|
220
|
+
* returned, so a baseline reads the same three keys on every run.
|
|
221
|
+
*
|
|
222
|
+
* `KEEP` is retained beside the two that ask for a change. It moves when a
|
|
223
|
+
* section is added or split, which is what tells a falling `rewrite` count
|
|
224
|
+
* from a corpus that shrank.
|
|
225
|
+
*/
|
|
226
|
+
function placementCounts(record: unknown): Record<string, number> | undefined {
|
|
227
|
+
const findings = asObject(record)?.findings
|
|
228
|
+
if (!Array.isArray(findings)) return undefined
|
|
229
|
+
|
|
230
|
+
const counts: Record<string, number> = Object.fromEntries(
|
|
231
|
+
Object.values(PLACEMENT_KEYS).map((key) => [key, 0]),
|
|
232
|
+
)
|
|
233
|
+
for (const finding of findings) {
|
|
234
|
+
const verdict = asObject(finding)?.verdict
|
|
235
|
+
if (!isPlacementVerdict(verdict)) return undefined
|
|
236
|
+
counts[PLACEMENT_KEYS[verdict]] += 1
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return counts
|
|
240
|
+
}
|
|
241
|
+
|
|
202
242
|
function contextCounts(record: unknown): Record<string, number> | undefined {
|
|
203
243
|
const root = asObject(record)
|
|
204
244
|
if (root === undefined) return undefined
|
|
@@ -502,6 +542,18 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
502
542
|
absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
|
|
503
543
|
counts: contextCounts,
|
|
504
544
|
},
|
|
545
|
+
{
|
|
546
|
+
id: 'placement',
|
|
547
|
+
label: 'Context placement',
|
|
548
|
+
// Pinned to the regex layer rather than the resolved classifier setting.
|
|
549
|
+
// A count that moves with whichever model a machine runs is not
|
|
550
|
+
// comparable across clones, and the model layer turns a sub-second read
|
|
551
|
+
// into a multi-minute one. `document-health` is where the model reads.
|
|
552
|
+
argv: ['context', 'classify', 'sweep', '--backend', 'off', '--json'],
|
|
553
|
+
gatingExits: [],
|
|
554
|
+
corpus: 'tracked',
|
|
555
|
+
counts: placementCounts,
|
|
556
|
+
},
|
|
505
557
|
{
|
|
506
558
|
id: 'markdown',
|
|
507
559
|
label: 'Markdown corpus',
|
package/src/commands/context.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
missingSections,
|
|
15
15
|
PROVENANCE_FOLDER,
|
|
16
16
|
REQUIRED_SECTIONS,
|
|
17
|
+
REQUIRED_SECTIONS_BY_FOLDER,
|
|
18
|
+
requiredSections,
|
|
17
19
|
type SectionFinding,
|
|
18
20
|
} from '@/context/audit'
|
|
19
21
|
import {
|
|
@@ -129,8 +131,9 @@ export function register(program: Command): void {
|
|
|
129
131
|
'it holds more decisions than the cap. A record stating neither is',
|
|
130
132
|
'reported and never gated. --gate widens the gate to the other two',
|
|
131
133
|
'findings that are facts rather than judgments: a missing required',
|
|
132
|
-
'section and index',
|
|
133
|
-
'
|
|
134
|
+
'section and index drift. A context entry requires Overview and',
|
|
135
|
+
'Layout, and a wireframe requires Regions, States, Copy, and Not on',
|
|
136
|
+
'this surface. Entry length, reference form, table, provenance, narration,',
|
|
134
137
|
'and the record claim classification are judgments under both.',
|
|
135
138
|
'',
|
|
136
139
|
'Depth and bullet weight are stated over every markdown file rather',
|
|
@@ -740,6 +743,10 @@ async function runAudit(
|
|
|
740
743
|
renderWidth: RENDER_WIDTH,
|
|
741
744
|
provenanceFolder: PROVENANCE_FOLDER,
|
|
742
745
|
requiredSections: REQUIRED_SECTIONS,
|
|
746
|
+
// Keyed on the audited folder name. The flat key above stays for a
|
|
747
|
+
// caller that read it before wireframes owed a section, since turning
|
|
748
|
+
// it into this map would break that caller without an error.
|
|
749
|
+
requiredSectionsByFolder: REQUIRED_SECTIONS_BY_FOLDER,
|
|
743
750
|
// Three states rather than two, so a record showing no finding says
|
|
744
751
|
// which terms were looked for. The key is absent when the run never
|
|
745
752
|
// scanned, which is `--citations-only`, and null when it scanned and
|
|
@@ -946,7 +953,8 @@ function reportReferenceForm(
|
|
|
946
953
|
|
|
947
954
|
/**
|
|
948
955
|
* Names the path each finding belongs to, which is an entry in the folder named
|
|
949
|
-
* under `.claude
|
|
956
|
+
* under `.claude/`, the folder itself in a domain split across one, and the
|
|
957
|
+
* file in any wireframe folder. States
|
|
950
958
|
* the reach on every run for the reason the provenance report does.
|
|
951
959
|
*
|
|
952
960
|
* This prints ahead of the four readability measures because a missing section
|
|
@@ -959,19 +967,31 @@ function reportSections(
|
|
|
959
967
|
): void {
|
|
960
968
|
logStep('Sections')
|
|
961
969
|
|
|
962
|
-
const
|
|
963
|
-
|
|
970
|
+
const names = [
|
|
971
|
+
...new Set(
|
|
972
|
+
folders
|
|
973
|
+
.filter((folder) => requiredSections(folder).length > 0)
|
|
974
|
+
.map((folder) => folder.name),
|
|
975
|
+
),
|
|
976
|
+
]
|
|
977
|
+
if (names.length === 0) {
|
|
964
978
|
logInfo(
|
|
965
|
-
`Out of scope. The
|
|
979
|
+
`Out of scope. The lists are stated in the standards governing ${Object.keys(
|
|
980
|
+
REQUIRED_SECTIONS_BY_FOLDER,
|
|
981
|
+
)
|
|
982
|
+
.map((name) => `canon/${name}/`)
|
|
983
|
+
.join(' and ')}, and no audited folder is one of them.`,
|
|
966
984
|
)
|
|
967
985
|
return
|
|
968
986
|
}
|
|
969
987
|
|
|
988
|
+
for (const name of names) {
|
|
989
|
+
logInfo(
|
|
990
|
+
`canon/${name}/ requires ${REQUIRED_SECTIONS_BY_FOLDER[name].join(', ')}.`,
|
|
991
|
+
)
|
|
992
|
+
}
|
|
970
993
|
logInfo(
|
|
971
|
-
|
|
972
|
-
)
|
|
973
|
-
logInfo(
|
|
974
|
-
'A heading at any level counts. Each entry answers for itself, except in a domain split across a folder, where a sibling answers for the rest.',
|
|
994
|
+
'A heading at any level counts. Each entry answers for itself, except in a context domain split across a folder, where a sibling answers for the rest. A wireframe answers for itself in every folder.',
|
|
975
995
|
)
|
|
976
996
|
|
|
977
997
|
if (missing.length === 0) {
|
package/src/commands/teach.ts
CHANGED
|
@@ -797,6 +797,7 @@ function reportNav(
|
|
|
797
797
|
root: outcome.root,
|
|
798
798
|
contents: outcome.contents,
|
|
799
799
|
lessons: outcome.lessons,
|
|
800
|
+
reference: outcome.reference,
|
|
800
801
|
skipped: outcome.skipped,
|
|
801
802
|
})}\n`,
|
|
802
803
|
)
|
|
@@ -810,9 +811,11 @@ function reportNav(
|
|
|
810
811
|
for (const path of outcome.contents) logInfo(path)
|
|
811
812
|
logStep('Lessons rewritten')
|
|
812
813
|
logInfo(String(outcome.lessons))
|
|
814
|
+
logStep('Reference pages rendered')
|
|
815
|
+
logInfo(String(outcome.reference))
|
|
813
816
|
|
|
814
817
|
if (outcome.skipped.length > 0) {
|
|
815
|
-
logStep('Refused, missing a chrome marker')
|
|
818
|
+
logStep('Refused, missing a chrome marker or the markdown renderer')
|
|
816
819
|
for (const skip of outcome.skipped) {
|
|
817
820
|
logWarn(`${skip.file}: no ${skip.missing}`)
|
|
818
821
|
}
|
package/src/context/audit.ts
CHANGED
|
@@ -45,19 +45,35 @@ export const CATALOG_ROW_CHECKPOINT = 6
|
|
|
45
45
|
const CATALOG_NAMED_RATIO = 0.6
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Required sections per audited folder name, each in the order its standard
|
|
49
|
+
* states them.
|
|
49
50
|
*
|
|
50
|
-
* The
|
|
51
|
-
* checkpoints above quote their numbers. A parser over
|
|
51
|
+
* The lists are held here rather than read out of the standards, the way the
|
|
52
|
+
* four checkpoints above quote their numbers. A parser over a standard's prose
|
|
52
53
|
* would decide which sections are required from the wording around them, so it
|
|
53
54
|
* fails on a rewrite of that wording rather than on a defect in an entry.
|
|
54
55
|
*
|
|
55
56
|
* These names do not generalize the way a length threshold does, which is why
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* each list is keyed on the folder whose standard states it. `context.md`
|
|
58
|
+
* requires an overview and a layout. `wireframes.md` asks four questions of a
|
|
59
|
+
* surface and answers each in a section, so those four are required and
|
|
60
|
+
* `## Behavior` is not, since a static surface has no interaction to state. A
|
|
61
|
+
* diagram entry declares a heading per kind and its standard requires none.
|
|
59
62
|
*/
|
|
60
|
-
export const
|
|
63
|
+
export const REQUIRED_SECTIONS_BY_FOLDER: Readonly<
|
|
64
|
+
Record<string, readonly string[]>
|
|
65
|
+
> = {
|
|
66
|
+
context: ['Overview', 'Layout'],
|
|
67
|
+
wireframes: ['Regions', 'States', 'Copy', 'Not on this surface'],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The context standard's list, kept as its own export because the audit record
|
|
72
|
+
* has published it flat under `checkpoints.requiredSections` since before any
|
|
73
|
+
* other folder required a section.
|
|
74
|
+
*/
|
|
75
|
+
export const REQUIRED_SECTIONS: readonly string[] =
|
|
76
|
+
REQUIRED_SECTIONS_BY_FOLDER.context
|
|
61
77
|
|
|
62
78
|
const HEADING_TEXT = /^#{1,6}\s+(.+?)\s*$/
|
|
63
79
|
const TABLE_ROW = /^\s*\|/
|
|
@@ -226,7 +242,7 @@ export interface EntryReport {
|
|
|
226
242
|
readonly bareReferences: readonly BareReferenceFinding[]
|
|
227
243
|
/**
|
|
228
244
|
* Required sections this entry declares, in the standard's order, and empty
|
|
229
|
-
* outside
|
|
245
|
+
* outside a folder whose standard names them. What the folder is short of
|
|
230
246
|
* is `missingSections`, since one entry answers for its siblings.
|
|
231
247
|
*/
|
|
232
248
|
readonly sections: readonly string[]
|
|
@@ -550,17 +566,20 @@ function narration(
|
|
|
550
566
|
* Fenced blocks are skipped for the reason the scans above skip them. A
|
|
551
567
|
* standard quoted inside an example declares nothing about the entry quoting it.
|
|
552
568
|
*/
|
|
553
|
-
function declaredSections(
|
|
569
|
+
function declaredSections(
|
|
570
|
+
lines: readonly BodyLine[],
|
|
571
|
+
required: readonly string[],
|
|
572
|
+
): string[] {
|
|
554
573
|
const found = new Set<string>()
|
|
555
574
|
|
|
556
575
|
for (const line of lines) {
|
|
557
576
|
if (line.fenced) continue
|
|
558
577
|
|
|
559
578
|
const match = line.text.match(HEADING_TEXT)
|
|
560
|
-
if (match &&
|
|
579
|
+
if (match && required.includes(match[1])) found.add(match[1])
|
|
561
580
|
}
|
|
562
581
|
|
|
563
|
-
return
|
|
582
|
+
return required.filter((section) => found.has(section))
|
|
564
583
|
}
|
|
565
584
|
|
|
566
585
|
/**
|
|
@@ -611,7 +630,9 @@ function bareReferences(
|
|
|
611
630
|
* The caller passes jurisdiction rather than deriving it from `rel`, because a
|
|
612
631
|
* path prefix hardcodes what `--folder` exists to override and misses a domain
|
|
613
632
|
* split into `context/<sub-area>/`. Sibling names arrive the same way and for
|
|
614
|
-
* the same reason, since the folder an entry sits in is what holds them.
|
|
633
|
+
* the same reason, since the folder an entry sits in is what holds them. The
|
|
634
|
+
* required sections arrive apart from jurisdiction, since a wireframe owes
|
|
635
|
+
* sections under a standard that states no provenance rule.
|
|
615
636
|
*/
|
|
616
637
|
export function measureEntry(
|
|
617
638
|
rel: string,
|
|
@@ -619,6 +640,7 @@ export function measureEntry(
|
|
|
619
640
|
governsContent = true,
|
|
620
641
|
terms?: NarrationTerms,
|
|
621
642
|
siblings: readonly string[] = [],
|
|
643
|
+
required: readonly string[] = governsContent ? REQUIRED_SECTIONS : [],
|
|
622
644
|
): EntryReport {
|
|
623
645
|
const lines = bodyLines(source)
|
|
624
646
|
|
|
@@ -632,7 +654,7 @@ export function measureEntry(
|
|
|
632
654
|
provenance: governsContent ? provenance(lines) : [],
|
|
633
655
|
narration: governsContent && terms ? narration(lines, terms) : [],
|
|
634
656
|
bareReferences: bareReferences(lines, siblings),
|
|
635
|
-
sections:
|
|
657
|
+
sections: declaredSections(lines, required),
|
|
636
658
|
stub: isStubSeed(source),
|
|
637
659
|
governed: governsContent,
|
|
638
660
|
}
|
|
@@ -713,6 +735,7 @@ export async function measureFolders(
|
|
|
713
735
|
governsContent(folder),
|
|
714
736
|
terms,
|
|
715
737
|
names.filter((name) => name !== self),
|
|
738
|
+
requiredSections(folder),
|
|
716
739
|
),
|
|
717
740
|
)
|
|
718
741
|
}
|
|
@@ -726,6 +749,13 @@ export function governsContent(folder: AuditedFolder): boolean {
|
|
|
726
749
|
return folder.name === PROVENANCE_FOLDER
|
|
727
750
|
}
|
|
728
751
|
|
|
752
|
+
/** Names the sections the folder's standard requires, empty where it states none. */
|
|
753
|
+
export function requiredSections(folder: AuditedFolder): readonly string[] {
|
|
754
|
+
return Object.hasOwn(REQUIRED_SECTIONS_BY_FOLDER, folder.name)
|
|
755
|
+
? REQUIRED_SECTIONS_BY_FOLDER[folder.name]
|
|
756
|
+
: []
|
|
757
|
+
}
|
|
758
|
+
|
|
729
759
|
/**
|
|
730
760
|
* Reports whether a bare sibling name here is a reference by construction.
|
|
731
761
|
*
|
|
@@ -765,6 +795,11 @@ export function matchesSiblings(folder: AuditedFolder): boolean {
|
|
|
765
795
|
* The standard sanctions omitting `## Layout` from a domain owning no paths,
|
|
766
796
|
* which no measure can tell from an entry that forgot it. An entry of that
|
|
767
797
|
* shape therefore reports, which is a reason this is printed and never gated on.
|
|
798
|
+
*
|
|
799
|
+
* A wireframe folder answers per file whether nested or not, since its
|
|
800
|
+
* standard keeps one surface per file. A subfolder of wireframes groups
|
|
801
|
+
* surfaces the way the named context folder groups domains, so a conforming
|
|
802
|
+
* sibling there says nothing about the surface beside it.
|
|
768
803
|
*/
|
|
769
804
|
export function missingSections(
|
|
770
805
|
root: string,
|
|
@@ -774,11 +809,12 @@ export function missingSections(
|
|
|
774
809
|
const byRel = new Map(entries.map((entry) => [entry.rel, entry]))
|
|
775
810
|
const findings: SectionFinding[] = []
|
|
776
811
|
|
|
777
|
-
const shortOf = (declared: readonly string[]): string[] =>
|
|
778
|
-
REQUIRED_SECTIONS.filter((name) => !declared.includes(name))
|
|
779
|
-
|
|
780
812
|
for (const folder of folders) {
|
|
781
|
-
|
|
813
|
+
const required = requiredSections(folder)
|
|
814
|
+
if (required.length === 0 || folder.entries.length === 0) continue
|
|
815
|
+
|
|
816
|
+
const shortOf = (declared: readonly string[]): string[] =>
|
|
817
|
+
required.filter((name) => !declared.includes(name))
|
|
782
818
|
|
|
783
819
|
// A stub owes no sections, so it is dropped before either branch rather
|
|
784
820
|
// than inside them. Leaving one in the split-folder aggregate would let a
|
|
@@ -790,7 +826,7 @@ export function missingSections(
|
|
|
790
826
|
|
|
791
827
|
if (reports.length === 0) continue
|
|
792
828
|
|
|
793
|
-
if (folder.nested) {
|
|
829
|
+
if (folder.nested && governsContent(folder)) {
|
|
794
830
|
const missing = shortOf(reports.flatMap((entry) => entry.sections))
|
|
795
831
|
if (missing.length > 0) findings.push({ rel: folder.rel, missing })
|
|
796
832
|
continue
|