@erclx/canon 4.81.0 → 4.82.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.81.0",
4
+ "version": "4.82.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -58,7 +58,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
58
58
  | `canon migrate rename` | Rewrite every unprotected `aitk` token to `canon` and move the paths that carry the name, reporting the plan without `--write` (`--scope`, `--json`) |
59
59
  | `canon migrate skill-names` | Move the prefixed skill folders onto their two-word names and rewrite every citation, reporting the plan without `--write` (`--root`, `--json`) |
60
60
  | `canon migrate records` | Move the gitignored session records to `.canon/` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
61
- | `canon migrate surface-roots` | Move the tracked surfaces (`context/`, `wireframes/`, the three loose documents, and the install stamp folder) from `.claude/` to `canon/` with `git mv` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
61
+ | `canon migrate surface-roots` | Move the tracked surfaces (`context/`, `decisions/`, `wireframes/`, the three loose documents, and the install stamp folder) from `.claude/` to `canon/` with `git mv` and repoint every tracked citation, reporting the plan without `--write` (`--root`, `--json`) |
62
62
  | `canon migrate record-tree` | Repoint the old-root citations inside the records themselves, scoped to the live folders and reporting every line without `--write` (`--root`, `--json`) |
63
63
  | `canon migrate rule-layout` | Move a target's installed rules from the flat `.claude/rules/<subdir>/` layout to `.claude/rules/canon/<subdir>/`, reporting the plan without `--write` (`--root`, `--json`) |
64
64
  | `canon migrate scratch-evidence` | Move a folder under `.tmp/` that a durable record cites as evidence to `.canon/review/evidence/`, repointing every citation live or archived, reporting the plan without `--write` (`--root`, `--json`) |
@@ -67,6 +67,10 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
67
67
  | `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
68
68
  | `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
69
69
  | `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
70
+ | `canon context classify diff` | Classify the chunks a git range changed, each with its enclosing section, as keep, replace, history, or move (`--base`, `--doc-types`, `--json`) |
71
+ | `canon context classify sweep` | Classify every section of the five canonical doc types, split at H3, as keep, rewrite, or move (`--doc-types`, `--json`) |
72
+ | `canon context classifier show` | Report the resolved classifier backend and model and which source decided them (`--json`) |
73
+ | `canon context classifier set` | Write the project classifier setting to `canon/config/classifier.toml` (`--backend`, `--model`, `--json`) |
70
74
  | `canon markdown audit` | Fail any markdown path on a banned character, word, or spelling, or a dead relative link, and report the structural checkpoints |
71
75
  | `canon claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
72
76
  | `canon standards audit` | Report the corpus against the success-criterion gate `standards/standard.md` states, failing only on a standard new to the branch (`--json`, `--arrivals-only`) |
@@ -143,7 +147,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
143
147
  | `intake` | `list`, `answer` |
144
148
  | `teach` | `list`, `open`, `resource`, `glossary`, `lesson`, `stylesheet`, `nav`, `render` |
145
149
  | `comments` | `scan` |
146
- | `context` | `audit` |
150
+ | `context` | `audit`, `classify diff`, `classify sweep`, `classifier show`, `classifier set` |
147
151
  | `markdown` | `audit` |
148
152
  | `secrets` | `scan` |
149
153
  | `deps` | `audit` |
@@ -169,7 +173,7 @@ Common patterns:
169
173
 
170
174
  A record folder already present at the destination is a refusal rather than a merge, and a line carrying `canon-keep-record-root`, or the nearest non-blank line below it, keeps the old spelling for prose that dates a decision. The records themselves are never swept: everything under `.canon/` and every `.claude/` record folder is passed over and reported as a count on its own line, which is what keeps the run that follows the ignore collapse touching the same files as one before it. Running it twice rewrites nothing, which is the check that the exclusions, the markers, and that skip all fired.
171
175
 
172
- `migrate surface-roots` moves the tracked surfaces from `.claude/` to `canon/`: `context/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the install stamp folder, respelled `canon/config/` since a project under `canon/` reserves the bare `canon` name for the CLI's own install. Every path is tracked, so each file moves with `git mv` and its history follows it, and every tracked citation of one is rewritten in the same run. `rules/`, `skills/`, `hooks/`, and `settings.json` stay where Claude Code reads them. It reports until `--write` is passed, and a destination already present under `canon/` refuses the whole plan rather than splitting a surface across both roots.
176
+ `migrate surface-roots` moves the tracked surfaces from `.claude/` to `canon/`: `context/`, `decisions/`, `wireframes/`, `ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`, and the install stamp folder, respelled `canon/config/` since a project under `canon/` reserves the bare `canon` name for the CLI's own install. Every path is tracked, so each file moves with `git mv` and its history follows it, and every tracked citation of one is rewritten in the same run. `rules/`, `skills/`, `hooks/`, and `settings.json` stay where Claude Code reads them. It reports until `--write` is passed, and a destination already present under `canon/` refuses the whole plan rather than splitting a surface across both roots.
173
177
 
174
178
  A line carrying `canon-keep-surface-root`, or the nearest non-blank line above it, keeps the old spelling. A rule's frontmatter `paths:` glob, a citation shaped like a path into another repository, and one inside a dated paragraph are reported by file and line and left alone. Running it twice rewrites nothing, which is the idempotence check. A project that never runs it keeps resolving each surface under `.claude/`, since every reader takes whichever root holds the entry.
175
179
 
@@ -29,7 +29,7 @@ canon context audit tooling/base/seeds --gate
29
29
 
30
30
  ## Folder scope
31
31
 
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.
32
+ Scope defaults to `context`, `decisions`, `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
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
 
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: Doc classifier
3
+ description: Classifying canonical-doc content as keep, replace/rewrite, history, or move in diff and sweep mode, the classifier setting pair, the record shape, and the exit codes
4
+ ---
5
+
6
+ # Doc classifier
7
+
8
+ `canon context classify` reads content from the five canonical doc types (`context/<domain>.md`, `ARCHITECTURE.md`, `wireframes/<surface>.md`, `DESIGN.md`, `REQUIREMENTS.md`) and reports whether each piece states the project as it stands or should be rewritten, narrated elsewhere, or moved to another surface. It reports and never gates: a finding is a judgment for a writer to act on, not a fact a push fails over. Wiring it into `docs-fold`, the merge gate, or `canon context audit` is separate work this verb does not do.
9
+
10
+ Two modes read different units. Diff mode reads the chunks a git range changed, each with the section it landed in, which is what a session checks right after an edit. Sweep mode reads every section of the five doc types, split at H3, which is what a cleanup pass or a periodic re-scan runs over a whole corpus.
11
+
12
+ ```bash
13
+ canon context classify diff
14
+ canon context classify diff --base origin/main --json
15
+ canon context classify diff --doc-types context,wireframes
16
+ canon context classify sweep
17
+ canon context classify sweep --doc-types design,requirements --json
18
+ ```
19
+
20
+ | Option | Behavior |
21
+ | -------------------- | -------------------------------------------------------------- |
22
+ | `--base <ref>` | Diff mode only. Far side of the range, defaulting to the trunk |
23
+ | `--doc-types <list>` | Comma-separated canonical doc types (default: all five) |
24
+ | `--backend <name>` | Override the resolved model backend for this run |
25
+ | `--model <name>` | Override the resolved model name for this run |
26
+ | `--json` | Add a machine-readable record on stdout |
27
+
28
+ ## Two layers
29
+
30
+ The regex layer always runs. It is free, needs nothing installed, and catches narration (a branch name, "closed on", "did not survive") and a source file path named inside a wireframe. It never answers REPLACE in diff mode, since telling a restated figure from a genuinely new one needs the section a hunk landed in, which the model layer reads and the regex layer does not.
31
+
32
+ The local Ollama layer runs only when configured, through `canon context classifier`. When it runs, its call is one chunk or section per request, never batched: the groundwork measurement behind this verb found that batching 16 hunks into one call returned KEEP for every one of them. Thinking is always off, since it was measured to catch nothing thinking-off missed while running roughly five times slower and, in sweep mode, losing three real flags by reasoning past them.
33
+
34
+ A finding carries both layers' readings when the model ran. The `verdict` field takes the model's reading when it ran and parsed, and falls back to the regex reading otherwise, since the model catches shapes the regex layer cannot reach structurally. `decidedBy` names which one won.
35
+
36
+ A configured-but-unreachable backend warns on stderr and falls back to the regex layer alone. The run still exits clean: the record's `modelLayer` field names why the model layer did not run (`off`, `skipped-no-model`, `skipped-unreachable`, or `ran`), so a clean regex-only report never reads as a clean model pass.
37
+
38
+ ## The classifier setting
39
+
40
+ ```bash
41
+ canon context classifier show
42
+ canon context classifier show --json
43
+ canon context classifier set --backend ollama --model qwen3.8:27b
44
+ canon context classifier set --backend off
45
+ ```
46
+
47
+ The backend and model resolve independently through the same four-tier precedence: a flag on the verb, then `CANON_CLASSIFIER_BACKEND` / `CANON_CLASSIFIER_MODEL`, then `canon/config/classifier.toml`, then off. `classifier show` reports the resolved backend and model and which tier decided them, without running a classification. There is no default model name: a model that resolves on one machine means nothing on another, so a backend configured with no model reports `no-model` from `show` and runs the regex layer alone from `classify`.
48
+
49
+ `classifier set` writes `canon/config/classifier.toml`, creating `canon/config/` when a project does not carry it yet:
50
+
51
+ ```toml
52
+ [classifier]
53
+ backend = "ollama"
54
+ model = "qwen3.8:27b"
55
+ ```
56
+
57
+ `--backend off` writes a file with no `model` line, which reads back as `off` at the same tier a missing file would default to, but with a source line saying the file decided it rather than the default.
58
+
59
+ ## The record
60
+
61
+ ```json
62
+ {
63
+ "decision": "ok",
64
+ "mode": "diff",
65
+ "backend": "ollama",
66
+ "model": "qwen3.8:27b",
67
+ "modelLayer": "ran",
68
+ "settingsSource": "file",
69
+ "findings": [
70
+ {
71
+ "file": "canon/context/retrieval.md",
72
+ "docType": "context",
73
+ "regex": {
74
+ "verdict": "KEEP",
75
+ "quote": "",
76
+ "reason": "no narration or wrong-surface pattern"
77
+ },
78
+ "model": {
79
+ "verdict": "REPLACE",
80
+ "quote": "the count moved",
81
+ "reason": "restates a figure the section already carries"
82
+ },
83
+ "verdict": "REPLACE",
84
+ "decidedBy": "model"
85
+ }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ Sweep mode's findings carry a `heading` field alongside `file` and use the sweep vocabulary (KEEP, REWRITE, MOVE) rather than diff mode's four (KEEP, REPLACE, HISTORY, MOVE). REWRITE stands in for both REPLACE and HISTORY, matching the sweep prompt's own three-verdict vocabulary: a whole section already shows its own history in view, so there is no diff-mode split left to make.
91
+
92
+ ## Exit codes
93
+
94
+ | Code | Meaning |
95
+ | ---- | ------------------------------------------------------------- |
96
+ | `0` | the run completed, whatever the findings say |
97
+ | `1` | refused: a bad range, an unreadable file, or a malformed flag |
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.
@@ -15,6 +15,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
15
15
  - [Comments](comments.md): Comment density by language and kind, the two structural exclusions, and how the degradation sweep finds its vocabulary
16
16
  - [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, the architecture record's length gate and claim coverage, which folders each check reaches, and what moved to the attribute tier
17
17
  - [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
18
+ - [Doc classifier](context-classify.md): Classifying canonical-doc content as keep, replace/rewrite, history, or move in diff and sweep mode, the classifier setting pair, the record shape, and the exit codes
18
19
  - [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
20
  - [Demo](demo.md): Compiling a screencast draft into a runnable plan, driving a served application to a recording and a still, reading numbered frames back out of a recording, the pointer the recording paints, and what each refusal reports
20
21
  - [Design board](design-board.md): Generating a static index over a project's design surfaces, what each panel reads, and which two stay toolkit-checkout-only
@@ -196,6 +196,21 @@ Run the second line once per stack the project holds, since the two hooks above
196
196
 
197
197
  The order carries the reason. A sync replaces each toolkit-owned copy with one already carrying the new names, and the sweep afterwards reaches the prose the project wrote itself. Sweeping first rewrites those installed files in place, which moves them off the hash the install recorded, so the next sync reads them as edited by the project and leaves them alone.
198
198
 
199
+ ### Move the surfaces off `.claude/`, once
200
+
201
+ Tracked surfaces moved out of `.claude/` and into a root of their own. `context/`, `decisions/`, `wireframes/`, the three loose documents (`ARCHITECTURE.md`, `REQUIREMENTS.md`, `DESIGN.md`), and the install stamp folder, respelled `canon/config/`, now live under `canon/`, and `rules/`, `skills/`, `hooks/`, and `settings.json` stay put since Claude Code reads those by path.
202
+
203
+ Run `canon upgrade` first if you have not, then run this from inside the project:
204
+
205
+ ```bash
206
+ canon migrate surface-roots --json
207
+ canon migrate surface-roots --write --json
208
+ ```
209
+
210
+ The first line reports the plan and the second applies it, moving each surface with `git mv` so its history follows and repointing every tracked citation of one in the same run. See `canon docs agents` for the full file list rather than reading it here.
211
+
212
+ Neither `canon sync --check` nor any other command notices a surface still sitting at `.claude/`, so running this is on you rather than on a prompt from the toolkit.
213
+
199
214
  ### Check first
200
215
 
201
216
  `canon sync --check <path>` reports what has drifted without writing anything. It splits each difference by cause, which is the question that decides what to do next.
@@ -20,7 +20,8 @@ canon/
20
20
  ├── ARCHITECTURE.md ← technical design decisions
21
21
  ├── DESIGN.md ← visual intent and token decisions (UI projects)
22
22
  ├── wireframes/ ← ASCII wireframes: layout, UI copy, and interaction rules (UI projects)
23
- └── context/ ← per-domain narrative loaded on demand via index.md
23
+ ├── context/ ← per-domain narrative loaded on demand via index.md
24
+ └── decisions/ ← decision history a canonical doc points at, never loaded eagerly
24
25
 
25
26
  .claude/
26
27
  └── rules/ ← path-scoped governance rules, written by canon gov install
@@ -33,7 +34,7 @@ canon/
33
34
  └── tmp/ ← deletable scratch, safe to remove without loss
34
35
  ```
35
36
 
36
- A project scaffolded before the move keeps its records under `.claude/`, and every command reads either root. `canon migrate records` moves one project across and repoints what cites it, and `canon migrate record-tree` follows it to reach the citations inside the records themselves, which the first verb passes over because it enumerates through git. A project scaffolded before the surface move keeps its context, wireframes, and loose documents under `.claude/` the same way, and `canon migrate surface-roots` moves those to `canon/` with the history following each file.
37
+ A project scaffolded before the move keeps its records under `.claude/`, and every command reads either root. `canon migrate records` moves one project across and repoints what cites it, and `canon migrate record-tree` follows it to reach the citations inside the records themselves, which the first verb passes over because it enumerates through git. A project scaffolded before the surface move keeps its context, decisions, wireframes, and loose documents under `.claude/` the same way, and `canon migrate surface-roots` moves those to `canon/` with the history following each file.
37
38
 
38
39
  Three tiers of context load with different cost: always-loaded (root `CLAUDE.md`, `canon/REQUIREMENTS.md`, `canon/ARCHITECTURE.md`), path-scoped lazy (`.claude/rules/<scope>.md` with `paths:` glob), and on-demand lookup (`canon/context/<domain>.md`, or `canon/context/<domain>/` once a domain outgrows one file, discovered via `canon/context/index.md`). See [the context model](../../canon/context/context-model.md) for the full picture.
39
40
 
@@ -0,0 +1,12 @@
1
+ ---
2
+ description: Route canon/decisions/ edits to the decisions standard for record shape and lifecycle
3
+ paths:
4
+ - 'canon/decisions/**'
5
+ - '.claude/decisions/**'
6
+ ---
7
+
8
+ # Decisions standards
9
+
10
+ ## Authority
11
+
12
+ - Follow the decisions standard for folder layout, the ordinal filename, frontmatter, record sections, and the append-only lifecycle. It is the single source. Read it with `canon standards decisions`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.81.0",
4
+ "version": "4.82.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -20,6 +20,7 @@ const HOOKS = 'hooks'
20
20
  export const SUBDIRS: readonly string[] = [
21
21
  HOOKS,
22
22
  'context',
23
+ 'decisions',
23
24
  'diagrams',
24
25
  'memory',
25
26
  'tasks',
@@ -61,6 +61,7 @@ import { copyPreservingMode } from '@/copy'
61
61
  import { execScript } from '@/exec'
62
62
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
63
63
  import { recordDir } from '@/record-root'
64
+ import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
64
65
  import { isDirectory, resolveTarget } from '@/target'
65
66
  import { injectGitignore, pruneGitignore } from '@/tooling/inject'
66
67
  import {
@@ -148,7 +149,27 @@ const SEEDED_FILES: readonly string[] = [
148
149
  'REQUIREMENTS.md',
149
150
  'DESIGN.md',
150
151
  ]
151
- const SEEDED_DIRS: readonly string[] = ['memory', 'tasks', 'wireframes']
152
+ const SEEDED_DIRS: readonly string[] = [
153
+ 'decisions',
154
+ 'memory',
155
+ 'tasks',
156
+ 'wireframes',
157
+ ]
158
+
159
+ /**
160
+ * Where a `SEEDED_DIRS` entry resolves for the sync presence check.
161
+ *
162
+ * `wireframes` and `decisions` are tracked surface entries, resolved under
163
+ * `canon/` ahead of `.claude/`, while `memory` and `tasks` are session records
164
+ * resolved under `.canon/` ahead of `.claude/`. Routing every name through
165
+ * `recordDir` reported a migrated target's `canon/wireframes/` or
166
+ * `canon/decisions/` as missing, since that resolver never checks `canon/`.
167
+ */
168
+ export function seededDirPath(resolved: string, name: string): string {
169
+ return SURFACE_ENTRIES.includes(name)
170
+ ? surfaceDir(resolved, name)
171
+ : recordDir(resolved, name)
172
+ }
152
173
  const USER_DIR = join('tooling', 'claude', 'user')
153
174
  const STATUSLINE = 'statusline-command.sh'
154
175
  const PLUGIN_MANIFEST = join('claude', '.claude-plugin', 'plugin.json')
@@ -580,16 +601,16 @@ async function runSync(target: string): Promise<number> {
580
601
 
581
602
  // A record folder resolves at either root, so a migrated target is reported as
582
603
  // seeded rather than sent to `canon claude init` to re-create records it
583
- // already holds. The three seeded files and `wireframes` are tracked and stay
584
- // at `.claude/`, which the resolver answers for them anyway, since nothing
585
- // ever creates a second root copy for a name that does not move.
604
+ // already holds. `wireframes` and `decisions` are tracked surface entries
605
+ // instead, resolved through `surfaceDir`, since a migrated target holds them
606
+ // at `canon/` rather than under either record root.
586
607
  logStep('Seeded')
587
608
  for (const name of SEEDED_FILES) {
588
609
  if (existsSync(join(resolved, '.claude', name))) logInfo(name)
589
610
  else logWarn(`${name} missing. Run \`canon claude init\``)
590
611
  }
591
612
  for (const name of SEEDED_DIRS) {
592
- if (isDirectory(recordDir(resolved, name))) logInfo(`${name}/`)
613
+ if (isDirectory(seededDirPath(resolved, name))) logInfo(`${name}/`)
593
614
  else logWarn(`${name}/ missing. Run \`canon claude init\``)
594
615
  }
595
616