@journeykit/canon 0.21.1 → 0.23.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.
@@ -25,7 +25,9 @@ a feature, a one-line request included. A human is needed once per scope:
25
25
  approves the brief and the policy, `.canon/policy.yaml`, which is what
26
26
  the gate grades the work against. Record it only from the human's answer:
27
27
  `canon brief approve --change <slug> --by <the human> --note "<their words>"`.
28
- Never edit the policy to make the gate pass. A brief edited after its
28
+ Never edit the policy to make the gate pass: a policy that differs from
29
+ the approved one fails closed (`POLICY_CHANGED_SINCE_APPROVAL`, not
30
+ waivable); stop and ask the human. A brief edited after its
29
31
  approval is not approved (`BRIEF_CHANGED_SINCE_APPROVAL`): it needs a new
30
32
  answer. The ship guard and `canon evidence add` refuse an unapproved or
31
33
  edited brief, but those are backstops: the stop itself is at step 1.
@@ -35,7 +37,11 @@ a feature, a one-line request included. A human is needed once per scope:
35
37
  implement, evidence, independent review. A follow-up that adds or changes
36
38
  a step of the brief edits the brief: that is a scope change and needs a
37
39
  new answer. A follow-up on a shipped subject reopens it
38
- (`canon subject status <id> --to verifying --reason "<why>"`).
40
+ (`canon subject status <id> --to verifying --reason "<why>"`); a fix
41
+ closed `fixed`, and an approved chore, then need the whole suite evidenced
42
+ again (`evidence add --all-scenarios`, for a chore after its approval)
43
+ before it ships, and a chore also needs one change-level review
44
+ (`review add --change <slug>`).
39
45
  - When no human can answer one of the brief's questions, write your
40
46
  assumption into the brief's Questions and assumptions, build on it, and say
41
47
  so in your report. That covers the brief's questions and work inside an
@@ -54,6 +60,24 @@ Every write command says whether the record is final (`written directly`,
54
60
  or `staged and promoted in this run`) or staged; only a staged record
55
61
  needs `canon gate promote`, and only `write.promote: manual` leaves one.
56
62
 
63
+ The ledger is latest-only. It holds the current record of each key; writing a
64
+ newer version moves what it supersedes to `.canon/archive`, and `status`,
65
+ `gate check`, the guards and `query` read the ledger only. Earlier versions
66
+ are one flag away: `canon query --kind <k> --history`. A ledger written
67
+ before 0.23, or one whose move was interrupted, is brought to one file per
68
+ key by `canon ledger compact` (`canon migrate` runs it). A record binds only
69
+ what its own write names: evidence carries nothing forward from an earlier
70
+ record, and a review stays current while the verdict and checks of the
71
+ evidence it saw are unchanged.
72
+
73
+ Where artifact bytes live is the repo's choice: `canon.yaml`'s `artifact_store:`
74
+ block defaults to the local, git-tracked `.canon/artifacts/sha256/`, or names
75
+ an object-store tier, in which case `evidence add`, `review add` and
76
+ `evidence vault` upload by digest and keep nothing locally. The gate and
77
+ `canon artifacts verify [--full]` ask the store and refuse, never pass, when
78
+ it cannot be reached. Moving an existing store with `canon artifacts migrate`
79
+ is the repo owner's decision, not a step of the loop.
80
+
57
81
  ## Know your role
58
82
 
59
83
  canon cannot detect a role; you declare it. Pass `--as <role>` or set
@@ -87,10 +111,11 @@ A role is guidance, not enforcement: `--role` on `evidence add` and
87
111
  - Pass `--json` and read stdout as one document. On failure it is
88
112
  `{"error":{"code","message","hint","retryable","field","expected"}}`: branch
89
113
  on `error.code`, fix the option `field` names using the `expected` values,
90
- and follow `error.hint`. The codes are stable; `canon schema --errors --json`
91
- lists them.
92
- - A renamed command still runs for one release, warns on stderr and sets
93
- `meta.deprecated`; use the new name it prints.
114
+ and follow `error.hint`. Every refusal carries a concrete `hint`: follow
115
+ it. The codes are stable; `canon schema --errors --json` lists them.
116
+ - `canon status` sends the planner to write the brief while `brief check`
117
+ has blockers; the human stop appears only when the brief is clean and
118
+ unapproved.
94
119
  - Page long lists with `--limit <n>` and `--cursor <nextCursor>` (`--all`
95
120
  for everything), and project fields with `--fields a,b` (`status`, `export`,
96
121
  `query`, `finding list`, `divergence status`, `context`).
@@ -21,15 +21,15 @@ Every violation carries one of these stable, grep-able strings:
21
21
 
22
22
  | Class | Meaning |
23
23
  |---|---|
24
- | `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched, or a feature surface missing a `require_cases` case. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. |
25
- | `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record — or, with `spec_coverage.require_review` set, an in-scope scenario has no qualifying `Review`: none at all, only self-reviews, only stale reviews (the spec or evidence it pinned changed since), or only `indeterminate`/`unavailable` ones. See `canon-review`. |
24
+ | `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched, or a feature surface missing a `require_cases` case. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. At `verifying → shipped`, a `Divergent` latest verdict is `uncovered-cell` unless a reviewer accepted it (see Accepted divergence below), and a chore's approval, like a blocker closed `fixed`, needs every evidenced scenario evidenced again afterwards (see Re-verification below). |
25
+ | `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record — or, with `spec_coverage.require_review` set, an in-scope scenario has no qualifying `Review`: none at all, only self-reviews, only stale reviews (the spec or evidence it pinned changed since), or only `indeterminate`/`unavailable` ones. Also, subject `policy`: the policy changed since its approval or its approval is unreadable (fails closed; see `canon-policy`). See `canon-review`. |
26
26
  | `trust-below-required` | Achieved trust level is below `policy.yaml`'s `trust_required` for its class — RELEASE-scoped only (`canon gate check --release`); never fires on an ordinary run. |
27
27
  | `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. Also: a file bound to a latest evidence record whose bytes are gone — no intact blob in `.canon/artifacts/sha256/` and no working-tree copy with the recorded digest — or a file bound to an in-effect review with no intact blob (see Bound evidence stays provable below). |
28
28
  | `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
29
29
  | `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
30
30
  | `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
31
31
  | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) standing as a whole word — never inside a path, a file name, a URL or a longer word, so `pack.json/atlas` is not `n/a` — or a bare `verified` claim with no attached command result. `canon evidence add` refuses such a note before writing (`canon evidence add: refused — fabricated-evidence …`, exit 1). |
32
- | `open-blocker` | With `spec_coverage.require_review` set (and `block_on_findings`, the default), a Finding whose latest version is severity `blocker`, disposition `open`, on a change an in-scope subject adopted. Subject string `<change_id>#<round>.<seq>`. See `canon-review`. |
32
+ | `open-blocker` | With `spec_coverage.require_review` set (and `block_on_findings`, the default), a Finding whose latest version is severity `blocker`, disposition `open`, on a change an in-scope subject adopted — or closed `fixed` while an evidenced scenario of the subject has no evidence recorded after the close (re-verification). Subject string `<change_id>#<round>.<seq>`. See `canon-review`. |
33
33
  | `false-success` | The latest audit record (`canon evidence add --kind audit`) of a scenario says `refuted`: an external evaluator found a counterexample to the claimed success. Always on, regardless of policy or sampling, and takes precedence over any declared faithful verdict; only a newer audit (`confirmed` or `indeterminate`) clears it. The detail names the evaluator, its version, its sha256 and the counterexample paths. |
34
34
 
35
35
  ## `canon gate check [--repo <dir>] [--release]`
@@ -51,11 +51,12 @@ Exit `0` clean, `1` gate-red (any violation), `2` usage/load failure
51
51
  resolves through the nearest-ancestor `canon.yaml` walk, so it reads the
52
52
  repo ROOT's `.canon/policy.yaml` and `.canon/ledger` from any subdirectory.
53
53
 
54
- When `.canon/policy.yaml` exists but has no approval, or its bytes no
55
- longer match the approved digest, the gate prints `advisory: policy not
56
- approved: …` or `advisory: policy changed since approval: …` after its
57
- result, naming `canon policy approve --by <who>`; the exit code is
58
- unchanged (see `canon-policy`).
54
+ When `.canon/policy.yaml` exists but has no approval, the gate prints
55
+ `advisory: policy not approved: …` after its result, naming
56
+ `canon policy approve --by <who>`; the exit code is unchanged. When its
57
+ bytes no longer match the approved digest, or the approval cannot be read,
58
+ the gate fails closed: the violation is reported and the exit is `1` (see
59
+ `canon-policy`).
59
60
 
60
61
  Before it reads the scenario index, the gate rebuilds any stale part of
61
62
  it from the changed `.feature` files (the same code `canon inventory
@@ -200,6 +201,39 @@ Malformed rows in those three kinds do NOT surface as
200
201
  `malformed-evidence`; they are kept off the evidence violation set on
201
202
  purpose, so enabling nothing changes no existing verdict.
202
203
 
204
+ ### Accepted divergence and re-verification
205
+
206
+ ```bash
207
+ canon divergence accept --project-id <root-id> --scenario-id <id> --reason "<why>" --tracking <issue-url-or-ref> [--expiry YYYY-MM-DD] --reviewer <me> --actor-id <me> --session-id <session> --as reviewer
208
+ ```
209
+
210
+ A reviewer accepts a scenario whose latest evidence is `divergent` as a
211
+ known gap limited by something outside the change; never to skip a fix.
212
+ Refused (`DIVERGENCE_ACCEPT_REFUSED`) unless the latest verdict is
213
+ divergent and the session and actor differ from the evidence's. The record
214
+ pins that evidence: new evidence for the scenario, or `--expiry` passing,
215
+ ends it. While it holds, the ship guard counts the cell covered and
216
+ `status` and `gate check` keep listing it, never silently:
217
+
218
+ ```
219
+ accepted divergences: 1 known gap(s) — not failing the gate:
220
+ accepted-divergence demo.page.02 — https://example.test/issues/1 (no expiry)
221
+ ```
222
+
223
+ Re-verification is one rule with two triggers: a blocker whose latest
224
+ disposition is `fixed`, and a chore change once approved, keep the subject
225
+ from shipping until every one of its evidenced scenarios has evidence
226
+ recorded after the close or the approval. The refusal names the scenarios
227
+ and the batch command: `canon evidence add --project-id <root-id>
228
+ --all-scenarios --report <format>:<path> --kind test-run --role
229
+ implementer --ref "<test command>" …`. Reviews pin what the evidence says
230
+ (its verdict and the identity of its checks), not which record it is, so
231
+ recording the suite again with the same verdicts from the same cases leaves
232
+ every scenario review current. A chore instead needs one change-level
233
+ review of the chore itself (`canon review add --change <slug> …`) before
234
+ `--to shipped`; a changed verdict or case set stales that scenario's
235
+ review as before (and a new `divergence accept`, where the gap remains).
236
+
203
237
  ### Audit records (`--kind audit`)
204
238
 
205
239
  canon never generates or runs an audit. An external evaluator does, and
@@ -233,33 +267,16 @@ canon evidence add --kind audit --project-id root --scenario-id cart.add.01 \
233
267
  - `canon status` shows an `audits` section (sampled / confirmed / owed /
234
268
  refuted) and suggests `canon evidence add --kind audit …` in `next:`.
235
269
 
236
- ### Carrying and dropping bindings (`--drop-attachment`)
237
-
238
- `canon evidence add` for a scenario carries forward what the scenario's
239
- current latest record of the same fold (declared records, or audit
240
- records for an audit; committed ledger plus `_staging`) bound: every
241
- file the new write does not name again is bound into the new record
242
- unchanged (same path, digest and stored blob), and `--json` lists it as
243
- `carried: [paths]`. A file named again (`--artifact`/`--report`, new
244
- bytes at the same path included) replaces its earlier binding. A suite
245
- batch (`--all-scenarios`) therefore never costs a scenario its
246
- screenshots. A carried file whose blob the artifact store does not
247
- hold intact is refused (exit `2`, `EVIDENCE_CARRY_BLOB_MISSING`, nothing
248
- written): re-bind it, run `canon evidence vault`, or drop it. The audit
249
- fold (`--kind audit`) does not carry: an audit that leaves out a file
250
- the latest audit bound is refused (exit `2`) unless it names the file
251
- again or drops it with `--drop-attachment`, so a counterexample never
252
- rides along into a confirming audit.
253
-
254
- The only way to lose a binding is `--drop-attachment <path>`
255
- (repeatable) with one single-line `--drop-reason <text>`, recorded as
256
- `attachment_drop: {paths, reason}`. With `--all-scenarios` the drop
257
- applies to each scenario that binds the path. Refused (`2`): a drop
258
- without a reason, a reason without a drop, a drop path that no
259
- scenario of the write leaves out, a multi-line reason, or a drop
260
- without `--scenario-id`/`--all-scenarios` (task-only evidence has no
261
- carry or drop). `canon status` lists recorded drops (`dropped bindings
262
- (N):`; `drops[]` in `--json`).
270
+ ### A record binds what its own write names
271
+
272
+ `canon evidence add` binds exactly the files the write names
273
+ (`--artifact`, `--report`, `--report-case`). Nothing is carried forward
274
+ from an earlier record, in a single add or in a suite batch
275
+ (`--all-scenarios`), and no drop is recorded: history keeps the earlier
276
+ records, and a new record is reviewed on its own files. An audit
277
+ (`--kind audit`) replaces a refutation the same way, so a counterexample
278
+ never rides along into a confirming audit. `--drop-attachment` and
279
+ `--drop-reason` no longer exist.
263
280
 
264
281
  ### Nothing is attested before the brief is approved (`--override-reason`)
265
282
 
@@ -346,51 +363,43 @@ the stop is at the brief. `canon gate check` does not grade briefs, so a
346
363
  waived ship brief prints no advisory here; `canon status` keeps showing
347
364
  the brief as not approved.
348
365
 
349
- ### Reviews that predate pinning (checked against their pin)
350
-
351
- With `require_review` on, the reviews in effect (latest per reviewer and
352
- scenario) that carry no pins were written before 0.15. Each still names
353
- `pin`, the commit it reviewed, and the gate checks it there (0.16):
354
-
355
- - **current** — the scenario's digest at that commit (read with `git`,
356
- normalized exactly as `canon inventory sync` computes
357
- `scenario_digest`) equals its current digest, and no declared evidence
358
- record of the scenario is newer than the review. It counts. When the
359
- scenario lived in another `.feature` file at that commit, the spec
360
- root's files at that commit are searched for its id, then the files
361
- under each of the root's `former_roots` (where `canon migrate` moved
362
- it from, e.g. `specs` for a root now at `.canon/specs`). Only the
363
- root's own directories are searched, never another root's.
364
- - **stale** — the scenario changed since the commit, or it has newer
365
- evidence. It no longer counts, and an in-scope scenario left with no
366
- other qualifying review is `unreviewed-promotion`:
366
+ ### Reviews that predate 0.23 pinning (`canon migrate`)
367
367
 
368
- ```
369
- unreviewed-promotion gate.check.15 — stale review by `canon-reviewer-g1` (written before review pinning; checked against commit b556ab7529a332aa0b28d0a0f98bdd62fcbb9a63): it has declared evidence newer than the review; a review counts only while what it saw is current — review it again (`canon review add`)
370
- ```
371
-
372
- A spec change reads `its spec changed since <pin> (the scenario's
373
- digest at that commit is not its current one)`.
374
- - **undecidable** — the pin is not a commit in this clone (a shallow
375
- clone, rewritten history), or no feature file of the scenario's root
376
- holds its id at that commit. It keeps counting, as in 0.15: a missing
377
- commit never becomes a violation.
378
-
379
- One advisory line after the violations counts the three. A stale one
380
- whose scenario already has another qualifying review (current, signed,
381
- distinct) is counted as superseded, since nothing is owed for it; only
382
- the scenarios a stale one leaves without a qualifying review are named
383
- for re-review. The exit code is unchanged by the line itself:
368
+ A review pins what its scenario's evidence says: the scenario's spec
369
+ digest and, per role, the verdict and the identity of the checks the latest
370
+ evidence bound (`ReviewPins.checks`). Reviews written before 0.23 pin
371
+ something the gate no longer compares: 0.15–0.22 reviews pin evidence
372
+ record files, and reviews written before 0.15 pin only `pin`, the commit
373
+ they reviewed. Such a review does not count until `canon migrate` rewrites
374
+ it; the gate says so:
384
375
 
385
376
  ```
386
- advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
377
+ unreviewed-promotion gate.check.15 — review by `canon-reviewer-g1` predates 0.23 review pinning (it pins evidence records, or nothing): `canon migrate` rewrites it to what the evidence says, and a review that is not migrated does not count
387
378
  ```
388
379
 
389
- A review of a scenario no feature file declares any more is not counted.
390
- In CI, check out full history (`actions/checkout` with `fetch-depth: 0`)
391
- for the job that runs `canon gate check`, or every legacy review is
392
- undecidable. Each pinned commit is read once per run through one `git
393
- cat-file --batch` process, and never fetched over the network.
380
+ One advisory line counts the scenarios whose review is not migrated.
381
+ `canon migrate` converts each review (and accepted divergence) that was
382
+ current under the rule it was written under, in place, keeping its
383
+ reviewer, actor, session and artifacts; one that was already stale stays
384
+ stale and its scenario needs a new review.
385
+
386
+ - **0.15–0.22 reviews** are current when the spec pin matches and the
387
+ latest evidence record per role is the one the review pinned.
388
+ - **pre-0.15 reviews** are current when the scenario's digest at the pinned
389
+ commit (read with `git`, normalized as `canon inventory sync` computes
390
+ `scenario_digest`) equals its current digest and no declared evidence is
391
+ newer than the review. When the scenario lived in another `.feature`
392
+ file at that commit, the spec root's files at that commit are searched
393
+ for its id, then the files under each of the root's `former_roots`
394
+ (where `canon migrate` moved it from). Only the root's own directories
395
+ are searched, never another root's. A pin that is not a commit in this
396
+ clone, or a scenario no file holds at that commit, is undecidable: the
397
+ review is pinned as the scenario stands (it counted unchecked before
398
+ 0.23), and the migration says how many. In CI, check out full history
399
+ (`actions/checkout` with `fetch-depth: 0`) for the job that runs
400
+ `canon migrate` to check them.
401
+
402
+ A review of a scenario no feature file declares any more is left alone.
394
403
 
395
404
  ## Experimental: evidence binding (`experimental.evidence_binding`, off by default)
396
405
 
@@ -478,10 +487,9 @@ scenario with its verdict on its own line.
478
487
 
479
488
  It is all or nothing: a claimed `faithful` verdict over a failed case,
480
489
  a fabrication marker in `--summary`, an unapproved brief for any
481
- scenario's subject, or a carried attachment whose blob is missing from the
482
- store refuses the whole batch, and nothing is written. Each scenario's
483
- earlier attachments are carried into its new record (see above), and
484
- `--drop-attachment` applies per scenario. `--scenario-id`, `--task`,
490
+ scenario's subject refuses the whole batch, and nothing is written. Each
491
+ record binds only what the batch names (nothing is carried forward).
492
+ `--scenario-id`, `--task`,
485
493
  `--report-case`, `--kind audit` and the approval flags name one subject
486
494
  and are refused with `--all-scenarios`.
487
495
 
@@ -492,10 +500,10 @@ in the report, so a `faithful` verdict needs all of them to pass.
492
500
 
493
501
  When new evidence (one record or a batch) supersedes the evidence a
494
502
  current pinned review saw, `canon evidence add` prints one line per
495
- review it makes stale, with the command that re-pins it:
503
+ review it makes stale (its verdict or checks differ from what the review saw), with the command that re-pins it:
496
504
 
497
505
  ```
498
- canon evidence add: review by `reviewer-2` of cart.add.01 is now stale (it pinned the evidence this record supersedes); re-pin it: canon review add --project-id root --scenario-id cart.add.01 --reviewer reviewer-2 --actor-id reviewer-2 --role reviewer --pin <HEAD sha> --original-spec-ref specs/kind=feature/area=cart/add.feature
506
+ canon evidence add: review by `reviewer-2` of cart.add.01 is now stale (this record's verdict or checks differ from what it saw); re-pin it: canon review add --project-id root --scenario-id cart.add.01 --reviewer reviewer-2 --actor-id reviewer-2 --role reviewer --pin <HEAD sha> --original-spec-ref specs/kind=feature/area=cart/add.feature
499
507
  ```
500
508
 
501
509
  The decision is the gate's own (the same stale-review rule `canon gate
@@ -515,6 +523,16 @@ again. The directory is tracked in git (`canon init` never ignores it):
515
523
  commit it with the ledger. A file larger than 25 MiB is refused (exit 2);
516
524
  raise the limit with `--max-artifact-mib <N>`.
517
525
 
526
+ Where those bytes live is `canon.yaml`'s `artifact_store: { backend: local |
527
+ <s3 tier>, prefix: artifacts/ }` (default `local`: the directory above,
528
+ git-tracked, no credentials). With an object-store tier, `evidence add`,
529
+ `review add` and `evidence vault` upload each blob under
530
+ `<prefix>sha256/<digest>` and keep nothing locally. `canon gate check` and
531
+ `canon artifacts verify [--full]` ask the store (a HEAD per digest; `--full`
532
+ downloads and re-hashes) and refuse, never pass, when it cannot be reached.
533
+ `canon artifacts migrate --to <tier> [--dry-run]` moves an existing local
534
+ store; it is the repo owner's decision. See `canon-storage`.
535
+
518
536
  `canon gate check` re-checks every file bound to a LATEST evidence record
519
537
  (the latest per task and per `(project_id, scenario_id)`):
520
538
 
@@ -545,8 +563,7 @@ re-attesting is the fix.
545
563
  Files bound to a review (`canon review add --artifact`, see
546
564
  `canon-review`) are checked by the same rule for every CURRENT in-effect
547
565
  review: the latest per `(project_id, scenario_id, reviewer)`, less any
548
- that went stale against its scenario (its spec or evidence pin moved, or
549
- a review from before pinning is stale at its commit). A stale review is
566
+ that went stale against its scenario (its spec or evidence pin moved). A stale review is
550
567
  superseded — the gate reads the newer review, or reports the missing
551
568
  one as `unreviewed-promotion` — so its files are not re-checked, as a
552
569
  superseded evidence record's are not. A missing blob whose working-tree
@@ -685,13 +702,12 @@ staged. A record another run staged is not touched: only `gate promote`
685
702
  commits everything staged. It stays safe to run anyway: with nothing
686
703
  staged it prints
687
704
  `nothing staged` and exits `0`. `divergence add` stages for `canon
688
- divergence promote` and names it, under the same `write:` rule. Every
689
- other write — `review add`, `divergence
690
- resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
705
+ gate promote` and names it, under the same `write:` rule. Every
706
+ other write — `review add`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
691
707
  `gate task` (it edits the plan document and re-ingests it)
692
708
  — is final when it returns, and its success line ends `— written
693
709
  directly; nothing to promote`. Full promote-on-write contract:
694
- `canon-storage` (Staged writes and promotion).
710
+ `canon-storage` (Staged writes and promotion). `divergence resolve|defer|accept` stage and promote their own record in the same run and end `— staged and promoted in this run; nothing to promote`.
695
711
 
696
712
  ## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
697
713
 
@@ -72,7 +72,7 @@ scenario index # refreshed by every reader; `canon inventory sy
72
72
  │
73
73
  ▼
74
74
  canon review add # attests a review — see canon-inventory downstream / canon-gate
75
- canon divergence {add,promote,resolve,defer,status} # tracks/resolves a divergence
75
+ canon divergence {add,promote,resolve,defer,accept,status} # tracks/resolves a divergence
76
76
  canon gate check [--release] # trust-ladder / staleness / coverage — see canon-gate
77
77
  │
78
78
  ▼
@@ -158,7 +158,7 @@ specs:
158
158
 
159
159
  - **Review** — `canon review add` attests a verdict against a synced
160
160
  scenario. (See `canon-inventory` downstream commands and each command's `--help`.)
161
- - **Divergence** — `canon divergence {add,promote,resolve,defer,status}`
161
+ - **Divergence** — `canon divergence {add,promote,resolve,defer,accept,status}`
162
162
  tracks/resolves divergences against synced scenarios.
163
163
  - **Gate** — `canon gate check` enforces trust-ladder / staleness /
164
164
  coverage over the corpus (see `canon-gate`).
@@ -101,21 +101,43 @@ its result:
101
101
  advisory: policy not approved: .canon/policy.yaml has no approval record (.canon/policy.approval.json); a human reviews it and runs `canon policy approve --by <who>`
102
102
  ```
103
103
 
104
- Once the policy's bytes no longer match the approved digest, it prints
105
- this instead:
104
+ Once the policy's bytes no longer match the approved digest, or the
105
+ approval file cannot be read, the gate fails closed instead:
106
106
 
107
107
  ```
108
- advisory: policy changed since approval: .canon/policy.yaml is sha256:<now>, but `<who>` approved sha256:<then> at <time>; a human reviews it and runs `canon policy approve --by <who>`
108
+ unreviewed-promotion policy — policy changed since approval: .canon/policy.yaml is sha256:<now>, but `<who>` approved sha256:<then> at <time>; … (--override-reason does not waive it); the human re-approves the policy in force with `canon policy approve --by <human> --note "<words>"`, or restore the approved .canon/policy.yaml (and .canon/policy.approval.json) from git
109
109
  ```
110
110
 
111
- An approval file that does not parse reads
112
- `advisory: policy approval unreadable: …`.
113
- `canon status` prints the same text as a `warning:` line (and in
114
- `--json` `warnings`). The advisory never changes an exit code: canon
115
- cannot tell whether a human or an agent ran the command, so the record
116
- names who approved which bytes, and the advisory says when that no
117
- longer holds. An agent runs `canon policy approve` only to record a
118
- human's answer, never without one and never on its own behalf.
111
+ `canon gate check` exits `1`; `canon subject status` (the ship guard and
112
+ the reopen) refuses with `POLICY_CHANGED_SINCE_APPROVAL`, or
113
+ `POLICY_APPROVAL_UNREADABLE` for an unreadable approval, and
114
+ `--override-reason` does not waive either. A policy never approved stays
115
+ an advisory. `canon status` prints the same text as a `warning:` line (and
116
+ in `--json` `warnings`). canon cannot tell whether a human or an agent
117
+ ran `policy approve`, so the record names who approved which bytes and
118
+ the gate fails when that no longer holds. An agent never edits the policy
119
+ to pass the gate: it stops and asks the human, who re-approves or restores
120
+ it. An agent runs `canon policy approve` only to record a human's answer,
121
+ never without one and never on its own behalf.
122
+
123
+ ## `canon policy show [--json]`
124
+
125
+ Prints the policy path and current digest, and the approval state
126
+ (`approved`, `changed`, `unreadable` or `unapproved`) with the approver,
127
+ time and approved digest:
128
+
129
+ ```
130
+ policy: .canon/policy.yaml
131
+ digest: sha256:37ea…
132
+ approval: approved
133
+ approved by: Ann
134
+ approved at: 2026-10-17T04:57:45Z
135
+ approved digest: sha256:37ea…
136
+ note: ok
137
+ ```
138
+
139
+ For `changed` and `unreadable` it adds `gate: fails closed (…)` with the
140
+ human's way out. Read-only; it writes nothing.
119
141
 
120
142
  ## The closed CEL profile
121
143
 
@@ -142,6 +142,8 @@ canon review add --project-id <root-id> --scenario-id <id> [--scenario-id <id>].
142
142
  [--outcome signed|indeterminate|unavailable] \
143
143
  [--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>] \
144
144
  [--artifact <path>]... [--max-artifact-mib <MiB>]
145
+ canon review add --project-id <root> --change <slug> --reviewer <who> --pin <sha-or-ref> \
146
+ --role reviewer (--upstream-ref <ref> | --original-spec-ref <ref>) … # a chore, below
145
147
  ```
146
148
 
147
149
  One attributed Review record per scenario, joined on
@@ -150,7 +152,8 @@ may carry the same scenario id. Exactly one provenance ref is required.
150
152
  `--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
151
153
  A Review carries no verdict about the behavior; the `verifying → shipped`
152
154
  ship gate still refuses missing or `Divergent` evidence verdicts on its
153
- own. It is written directly to the ledger, with no staging step, and says
155
+ own (a `Divergent` one passes only while a reviewer's `canon divergence
156
+ accept` holds). It is written directly to the ledger, with no staging step, and says
154
157
  so: `canon review add: wrote <path> (new) — written directly; nothing to
155
158
  promote`.
156
159
 
@@ -166,14 +169,30 @@ that is not a scenario id, refuses the call (exit 2) and writes no review.
166
169
  This is also the batch re-pin after `canon evidence add` reports reviews
167
170
  it made stale (see `canon-gate`).
168
171
 
172
+ ### A chore is reviewed as a change (`--change`, 0.23)
173
+
174
+ A chore adds no scenarios, and recording the suite again after it leaves
175
+ every scenario review current (a review pins the evidence's verdict and
176
+ checks, so the same verdicts from the same cases change nothing). What the
177
+ chore needs instead is one change-level review: the reviewer signs its
178
+ invariants and checks against the diff with `canon review add --change
179
+ <slug> …`, in place of `--scenario-id`. The review pins the chore's brief
180
+ (its digest, less the `Approved:` line), so an edit to the brief stales it,
181
+ and `--to shipped` refuses a chore with no signed, current change-level
182
+ review (`spec_coverage.require_review.distinct_actor` asks it of a reviewer
183
+ and session that did not attest the subject's evidence). `--change` takes a
184
+ chore's slug and refuses a feature change.
185
+
169
186
  ### What the reviewer looked at (artifacts)
170
187
 
171
188
  `--artifact <path>` (repeatable) binds a file the reviewer looked at — a
172
189
  screenshot, a recording, a trace — exactly as `canon evidence add
173
190
  --artifact` binds one: the path must resolve to a file inside the
174
191
  repository, a file over 25 MiB is refused unless `--max-artifact-mib <N>`
175
- raises the limit, and the bytes are copied into
176
- `.canon/artifacts/sha256/<hex>` (commit it with the ledger) before the
192
+ raises the limit, and the bytes are stored by digest (in
193
+ `.canon/artifacts/sha256/<hex>` unless `canon.yaml`'s `artifact_store:` names an
194
+ object store, where they are uploaded and nothing is kept locally; commit
195
+ the local store with the ledger) before the
177
196
  review is written. Any refusal exits 2 and writes and stores nothing. The
178
197
  review records each distinct file once, in the order given:
179
198
 
@@ -224,12 +243,16 @@ reviewer never types them:
224
243
  `scenario_digest`; then the review pins its `source_digest` (the
225
244
  sha256 of the whole `.feature` file) and records
226
245
  `scenario_digest_scope: file`;
227
- - `evidence` — the scenario's latest evidence record per role, each as
228
- `{key, digest}`: the ledger file `kind=evidence_record/<key>__<digest>.json`.
229
- A scenario with no evidence yet is pinned with an empty list.
230
-
231
- It prints `canon review add: pinned <id> at scenario digest <12 hex> and
232
- <n> latest evidence record(s); …` (`at source digest` for a file pin). A
246
+ - `checks` — the scenario's latest evidence per role, each as `{role,
247
+ verdict, checks}`: its verdict and the identity of what it checked (the
248
+ report format and the matched case names, as `junit:<case>`, or the
249
+ artifact digests). It names no evidence record, so recording the same
250
+ verdict from the same checks again leaves the review current. A scenario
251
+ with no evidence yet is pinned with an empty list.
252
+
253
+ It prints `canon review add: pinned <id> at scenario digest <12 hex> and the
254
+ verdict and checks of its <n> role(s) of evidence; …` (`at source digest` for
255
+ a file pin). A
233
256
  scenario with no record (never synced) is refused with exit 2 and nothing
234
257
  is written: there is nothing to pin.
235
258
 
@@ -307,44 +330,25 @@ The scenario is reviewed when at least one of those latest reviews:
307
330
 
308
331
  1. is `signed`;
309
332
  2. still matches its pins — the scenario record's current
310
- `source_digest`, and its current latest evidence record per role —
311
- when it has pins; or, written before 0.15 without pins, still
312
- describes its scenario at the commit it pinned (below);
333
+ `scenario_digest` (or `source_digest` for a file pin), and the verdict and
334
+ checks of its latest evidence per role — when it has pins; reviews
335
+ written before 0.23 count only after `canon migrate` (below);
313
336
  3. passes `distinct_actor`, when that is on.
314
337
 
315
- ### Reviews written before pinning
316
-
317
- A review written before 0.15 has no pins, only `pin`: the commit it
318
- reviewed. The gate reads the scenario's `.feature` file at that commit
319
- (`git`, with the same per-scenario normalization `canon inventory sync`
320
- uses for `scenario_digest`; if the scenario was in another file then, the
321
- spec root's files at that commit are searched for its id) and decides:
322
-
323
- - **current** when that digest equals the scenario's current one and no
324
- declared evidence record of the scenario is newer than the review: it
325
- counts under rules 1 and 3;
326
- - **stale** when the scenario changed since the commit (`its spec changed
327
- since <pin>`) or has evidence newer than the review (`it has declared
328
- evidence newer than the review`): it no longer counts, exactly like a
329
- stale pinned review;
330
- - **undecidable** when the pin is not a commit in this clone (shallow
331
- clone, rewritten history) or the scenario id is absent at it: it keeps
332
- counting, as in 0.15. A missing commit never becomes a violation.
333
-
334
- `canon gate check` prints one advisory line with the split (exit code
335
- unchanged by the line itself). A stale one whose scenario already has
336
- another qualifying review is counted as superseded and not named:
337
-
338
- ```
339
- advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
340
- ```
341
-
342
- `canon status` prints the same line as a warning, and `canon subject
343
- status` prints it on stderr for the subject's own scenarios; all three
344
- make the same decision. Re-review exactly the scenarios the line names:
345
- the new review pins what it saw and replaces the reviewer's legacy one.
346
- In CI, check out full history (`fetch-depth: 0`) for the job that runs
347
- the gate, or every legacy review is undecidable.
338
+ ### Reviews written before 0.23 pinning
339
+
340
+ A review written before 0.23 pins something the gate no longer compares: a
341
+ 0.15–0.22 review pins evidence record files, and a review written before
342
+ 0.15 has no pins, only `pin`, the commit it reviewed. Such a review does not
343
+ count until `canon migrate` rewrites it to what the evidence says (see
344
+ `canon-gate`). A review that was current under the rule it was written under
345
+ becomes a current review; one that was stale stays stale, and its scenario
346
+ needs a new `canon review add`, which pins what it saw and replaces the
347
+ reviewer's earlier one. The gate, `canon status` and `canon subject status`
348
+ print one advisory naming how many scenarios are waiting for `canon
349
+ migrate`. In CI, check out full history (`fetch-depth: 0`) for the job that
350
+ runs `canon migrate`, or a pre-0.15 review whose commit is missing is pinned
351
+ as the scenario stands instead of being checked.
348
352
 
349
353
  ### `unreviewed-promotion`
350
354
 
@@ -105,6 +105,43 @@ canon query --kind trajectory --json --limit 50 --fields at,kind
105
105
  canon query --kind trajectory --json --limit 50 --cursor <nextCursor>
106
106
  ```
107
107
 
108
+ ## The ledger is latest-only; the archive holds history
109
+
110
+ The `local` rung's directory (`.canon/ledger`) holds the current record of
111
+ each key. Appending a newer version (evidence for a scenario and role, a
112
+ closed finding, a rewritten scenario, a subject's next status) moves the
113
+ versions it supersedes to `.canon/archive`: same `kind=<kind>/…` layout and
114
+ file names, tracked in git. `status`, the gate, the guards and `canon query`
115
+ read the ledger only.
116
+
117
+ ```bash
118
+ canon query --kind evidence_record --history # every version, archive included
119
+ canon ledger compact --dry-run # report; writes nothing
120
+ canon ledger compact # pre-0.23 ledger or interrupted move; idempotent; `canon migrate` runs it
121
+ ```
122
+
123
+ A crash between the write and the move leaves a readable ledger that the next
124
+ append or `canon ledger compact` finishes. The archive is local, not routed
125
+ through `tiers:`; a `tiers.local.root` whose directory is not named `ledger`
126
+ has no archive and keeps every version.
127
+
128
+ ## Where artifact bytes live (`artifact_store:`)
129
+
130
+ ```yaml
131
+ artifact_store: { backend: local } # default
132
+ # artifact_store: { backend: cold, prefix: artifacts/ } # an `s3` tier of `tiers:`
133
+ ```
134
+
135
+ `local` is `.canon/artifacts/sha256/`, git-tracked and credential-free. An
136
+ object-store tier gets `evidence add`, `review add` and `evidence vault`
137
+ uploads under `<prefix>sha256/<digest>` and keeps nothing locally. The gate
138
+ and `canon artifacts verify [--full]` ask the store and refuse, never pass,
139
+ when it cannot be reached. `canon artifacts migrate --to <tier> [--dry-run]`
140
+ uploads, reads back and verifies every local blob, then writes the
141
+ `artifact_store:` block, removes the local copies and ignores
142
+ `.canon/artifacts/`; commit the removals, `.gitignore` and `canon.yaml`. It is
143
+ resumable and a second run finds nothing to do.
144
+
108
145
  ## What `canon report` does and doesn't read
109
146
 
110
147
  canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
@@ -173,9 +210,11 @@ record bodies.
173
210
  ## Staged writes and promotion
174
211
 
175
212
  Four writes stage their record instead of committing it: `canon evidence
176
- add`, `canon finding add` and `canon finding close` (under
177
- `.canon/ledger/_staging/`, committed by `canon gate promote`) and `canon
178
- divergence add` (committed by `canon divergence promote`). `canon.yaml`'s
213
+ add`, `canon finding add`, `canon finding close` and `canon divergence add`
214
+ (under `.canon/ledger/_staging/`, one staging shape for every kind,
215
+ committed by `canon gate promote`; `canon divergence promote` commits only
216
+ the staged divergences). `canon divergence resolve|defer|accept` stage and
217
+ promote their own record in the same run. `canon.yaml`'s
179
218
  `write:` section decides whether they promote in the same run:
180
219
 
181
220
  ```yaml
@@ -184,9 +223,8 @@ write:
184
223
  ```
185
224
 
186
225
  - `auto` — the command stages, then promotes that one record through the
187
- same validation and assignment `canon gate promote` (or `canon
188
- divergence promote`) applies. Records other runs staged stay staged;
189
- only `canon gate promote` / `canon divergence promote` commit
226
+ same validation and assignment `canon gate promote` applies. Records
227
+ other runs staged stay staged; only `canon gate promote` commits
190
228
  everything staged. The one success line names the committed record
191
229
  (`wrote kind=…/….json`; promotion renames it, so the staged file is not
192
230
  named) and ends `— staged and promoted in this run; nothing to
@@ -146,7 +146,14 @@ owns must also carry a scenario of each required `@case:` (e.g. one
146
146
  `@case:failure`) — attested golden-path scenarios alone do not ship. A
147
147
  Subject with no tagged scenario is refused rather than shipped on an
148
148
  empty set; so is one whose `scenario` records route away from the
149
- gate's rung. Each refusal prints by failure class (`uncovered-cell`),
149
+ gate's rung. A `Divergent` latest verdict is refused as `uncovered-cell`
150
+ unless a reviewer accepted it (`canon divergence accept`, see
151
+ `canon-gate`): the hint names both ways out. Re-verification: a blocker
152
+ closed `fixed` keeps blocking, and a chore's approval keeps the subject
153
+ from shipping, until every evidenced scenario has evidence recorded after
154
+ the close or the approval; the refusal names the scenarios and the
155
+ `canon evidence add --all-scenarios …` command. Each refusal prints by
156
+ failure class (`uncovered-cell`), carries a `hint`,
150
157
  exits 1, and the status stays `verifying`. `retired` is not gated.
151
158
 
152
159
  **The brief (0.16, 0.17, 0.19):** `verifying → shipped` also refuses where a brief
@@ -280,6 +287,12 @@ canon subject status: demo-subject → verifying — written directly; nothing t
280
287
  - The review guard does not run on a reopen (it prints `skipped … — a
281
288
  reopen moves the subject back to fix its gaps; shipping again runs
282
289
  it`). `verifying → shipped` afterwards runs every guard above again.
290
+ - A policy that fails closed (`POLICY_CHANGED_SINCE_APPROVAL`,
291
+ `POLICY_APPROVAL_UNREADABLE`: see `canon-policy`) refuses the reopen and
292
+ the ship, and `--override-reason` does not waive it.
293
+ - `canon change new` on a shipped subject prints the reopen command
294
+ (`canon subject status <id> --to verifying --reason "<why it reopens>"`)
295
+ in its output and `meta.next`.
283
296
  - `canon status` flags a shipped subject whose gate went red (whatever
284
297
  `verifying → shipped` would now refuse it on) and its only step is
285
298
  this reopen.
@@ -440,9 +453,10 @@ built or kept, never what it does. Its brief is intent, category,
440
453
  invariants, checks, out of scope; it adds no scenarios, and its evidence
441
454
  is the subject's suite run again at the chore commit with
442
455
  `canon evidence add --all-scenarios --report <kind>:<path> …`. That
443
- supersedes the old evidence, which makes the pinned reviews stale, so an
444
- independent re-review follows. Work that changes behavior is a feature
445
- change, never a chore.
456
+ supersedes the old evidence; the pinned reviews stay current while the
457
+ verdicts and checks are the same, so the chore itself is reviewed once with
458
+ `canon review add --change <slug> …` before it ships. Work that changes
459
+ behavior is a feature change, never a chore.
446
460
 
447
461
  ## Where are we: `canon status`
448
462
 
@@ -475,8 +489,7 @@ uses, so status and the gate cannot disagree about a gap:
475
489
  stale or whose latest reviews are all `indeterminate`/`unavailable`
476
490
  (see `canon-review`), and the `canon review add` step names both. With
477
491
  `require_review` on, a `warning:` line counts the in-effect reviews that
478
- predate review pinning (0.15), checked against the commit each pinned:
479
- current, stale (named) and undecidable.
492
+ predate 0.23 review pinning and wait for `canon migrate`.
480
493
  - **Brief** (0.16, 0.17, 0.19) — for each `building`, `verifying` or `shipped`
481
494
  subject, and each `proposed` subject whose newest adopted change has a
482
495
  brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
@@ -532,8 +545,6 @@ uses, so status and the gate cannot disagree about a gap:
532
545
  refuted audit adds a `next:` step suggesting `canon evidence add
533
546
  --kind audit …` (recorded from an external evaluator; canon never runs
534
547
  one).
535
- - **Dropped bindings** — `dropped bindings (N):` lists every recorded
536
- `--drop-attachment` (scenario, paths, reason); `drops[]` in `--json`.
537
548
  - **Brief overrides** — a `warning:` line per evidence record attested
538
549
  with `canon evidence add --override-reason` while its subject's brief
539
550
  was not approved: what it attests, who, when, the subject and the
@@ -563,10 +574,14 @@ uses, so status and the gate cannot disagree about a gap:
563
574
  than one `specs.roots[]` entry, every suggested `canon scenario new`
564
575
  carries `--project`: the owning root for a missing case, `<root-id>`
565
576
  (the reason lists the roots) for a subject that owns no scenario yet.
566
- - **The one human step.** Every step is the agent's to run except one:
567
- a subject whose brief is enforced (its newest change's canon
577
+ - **The one human step.** Every step is the agent's to run except one,
578
+ and it comes only once the brief is clean: while `canon brief check
579
+ --change <slug>` has blockers, the subject gets the planner step
580
+ `write the brief: canon brief check --change <slug> lists N blockers`
581
+ (`role: planner`) instead. A subject whose brief is enforced (its
582
+ newest change's canon
568
583
  `brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
569
- and comes from an external openspec `proposal.md`) gets
584
+ and comes from an external openspec `proposal.md`) and clean gets
570
585
  `human: stop and report the brief in <brief>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
571
586
  where `<brief>` is the canon `brief.md`, or the external `proposal.md`
572
587
  when no canon brief exists yet. Its reason reads ``<id>: <why
@@ -18,8 +18,12 @@ canon scenario new <area>.<surface>.<nn> --subject <subject-id> --flow <F-id> --
18
18
  canon scenario new --from - --as implementer --json # many at once: NDJSON on stdin, one {"id","title","subject","flow","case",…} per line
19
19
  canon evidence add --project-id <root-id> --all-scenarios --report <junit|cucumber|swift-testing>:<path> --kind test-run --role implementer --ref "<test command>" --actor-id <unit> --session-id <session> --as implementer --json
20
20
  canon evidence add --project-id <root-id> --scenario-id <id> --artifact <path> --kind test-run --role implementer --ref "<what it shows>" --actor-id <unit> --session-id <session> --as implementer --json
21
+ canon evidence add --project-id <root-id> --scenario-id <id> --verdict divergent --artifact <path> --kind test-run --role implementer --ref "<the external limit, and where it is tracked>" --actor-id <unit> --session-id <session> --as implementer --json # honest divergent evidence
22
+ canon artifacts verify --full --as implementer --json # bound files are in the artifact store (omit --full to trust the store's size and digest metadata); refuses when an object store is unreachable
21
23
  canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <unit> --actor-id <unit> --session-id <session> --summary "<defect>" --as implementer --json
22
24
  canon inventory sync --as implementer --json # only after a write printed NOT refreshed
25
+ canon query --kind <kind> --history --as implementer --json # earlier versions too; without --history only the current record of each key
26
+ canon ledger compact --as implementer --json # only for a pre-0.23 ledger or an interrupted move (add --dry-run to preview)
23
27
  canon gate promote --as implementer --json # only with write.promote: manual
24
28
  ```
25
29
 
@@ -51,20 +55,46 @@ After the first `status`, follow `meta.next` of each write; do not call
51
55
  `--all-scenarios`: one record per indexed scenario owning a case, each with
52
56
  the verdict its own cases show; any refusal writes none. For one scenario
53
57
  use `--scenario-id <id>` (repeatable) with `--report` or `--artifact`.
54
- canon stores the bytes in `.canon/artifacts/sha256/` (commit it with the
55
- ledger) and `canon gate check` re-checks them, so the working-tree report
56
- may be gitignored. For a scenario about what users see, bind the screenshot
57
- or recording (`--artifact <path>`). An external evaluator's audit is
58
+ A record binds exactly what its own write names (`--report`,
59
+ `--artifact`): nothing is carried forward from an earlier record, so name
60
+ every file the record should prove again. canon stores the bytes by digest
61
+ in the artifact store (by default `.canon/artifacts/sha256/`, committed
62
+ with the ledger; an object store when `canon.yaml`'s `artifact_store:` block
63
+ names one, in which case nothing is kept locally) and `canon gate check`
64
+ re-checks them, so the working-tree report may be gitignored. Run
65
+ `canon artifacts verify` to ask the store yourself; it refuses, never
66
+ passes, when an object store cannot be reached. Moving an existing store
67
+ (`canon artifacts migrate`) is the repo owner's call: do not run it.
68
+ For a scenario about what users see, bind its own screenshot or recording
69
+ with that scenario: `--scenario-id <id> --artifact <path>`. A batch
70
+ (`--all-scenarios`) binds every file it names to every scenario, so give
71
+ a batch only the reports; never loop all screenshots into it. An external evaluator's audit is
58
72
  `--kind audit`; canon never generates one. Evidence for a task of an
59
73
  imported plan (`--task <id>`) must bind the artifact or report that proves
60
- the task; a canon command is never that proof.
74
+ the task; a canon command is never that proof. When the behavior cannot
75
+ match the scenario because of something outside this change, record the
76
+ evidence `divergent` and say why in `--ref`; never record `faithful` to
77
+ pass. A reviewer may accept the tracked gap (`canon divergence accept`);
78
+ fixing the behavior and recording again is the other way out.
61
79
  5. **Promote.** Under `write: { promote: auto }` (what `canon init` writes)
62
80
  the record is final when `evidence add` returns. Only with
63
81
  `write.promote: manual` (or no `write:` key) follow it with
64
82
  `gate promote`; `meta.next` names it.
65
- 6. **A new record makes a signed review stale.** The add prints the reviewer
66
- and the `canon review add … --pin <sha>` that re-pins it; that is the
67
- reviewer's call. Do not run it yourself.
83
+ 6. **A changed result makes a signed review stale.** A review pins the
84
+ verdict and the checks (report format and matched case names, or artifact
85
+ digests) of the evidence it saw, not which record it was. Recording the
86
+ same verdict from the same checks keeps every review current; a different
87
+ verdict, different checks or an edited scenario stales it. The add prints
88
+ the reviewer and the `canon review add … --pin <sha>` that re-pins it; that
89
+ is the reviewer's call. Do not run it yourself.
90
+ 7. **After a fix or a chore, record the whole suite again.** A blocker the
91
+ reviewer closed `fixed`, and an approved chore, are not done until every
92
+ evidenced scenario has evidence newer than the close or the approval:
93
+ run the suite and bind it with `canon evidence add --all-scenarios …`
94
+ (the shipping refusal names the scenarios and this command). Reviews stay
95
+ current while the verdicts and checks are unchanged; a scenario whose gap
96
+ was accepted is accepted again by the reviewer once new evidence says
97
+ something different.
68
98
 
69
99
  ## Retry safety
70
100
 
@@ -76,6 +106,8 @@ response, run it again.
76
106
 
77
107
  When every scenario has evidence, the review is the reviewer's session, in a
78
108
  separate process: do not review your own change. A chore adds no scenarios:
79
- run the suite again and bind it with `--all-scenarios`, which supersedes the
80
- old evidence and makes pinned reviews stale. Reference:
81
- `reference/canon-gate.md`.
109
+ once it is approved, run the suite and bind it with `--all-scenarios`;
110
+ shipping needs that evidence newer than the chore's approval, plus the
111
+ reviewer's one change-level review of the chore (`review add --change`).
112
+ Scenario reviews stay current when the verdicts and checks are unchanged.
113
+ Reference: `reference/canon-gate.md`.
@@ -13,7 +13,11 @@ canon status --as orchestrator --json --fields next,waitingOn # at the start a
13
13
  canon brief approve --change <slug> --by "<the human>" --note "<their words>" --as orchestrator --json
14
14
  canon subject status <subject-id> --to <proposed|specced|building|verifying|shipped> --actor-id <unit> --as orchestrator --json
15
15
  canon subject status <subject-id> --to verifying --reason "<why>" --actor-id <unit> --as orchestrator --json # reopen a shipped subject (--reason is required here; any other transition accepts it as a note)
16
+ canon policy show --as orchestrator --json # policy digest and its approval: approved, changed, unreadable or unapproved
16
17
  canon gate check --as orchestrator --json
18
+ canon migrate --dry-run --as orchestrator --json # on a repo upgraded from an earlier canon: the plan, nothing written
19
+ canon migrate --as orchestrator --json # applies it (see "Upgraded repo")
20
+ canon artifacts verify --as orchestrator --json # the artifact store holds every bound file; refuses when an object store is unreachable
17
21
  ```
18
22
 
19
23
  ## Read the state
@@ -57,9 +61,14 @@ When `status` or `meta.next` shows a step with `actor: human` (the brief's
57
61
  approval), stop everything for that scope, report the brief to the human in
58
62
  its language, and wait. Do not start the implementer before the answer.
59
63
  Record the answer with `brief approve`. Never approve it yourself and never
60
- edit the policy to pass the gate. Inside an approved scope never stop for a
61
- human: fixes, follow-ups and re-reviews go straight to the implementer and
62
- reviewer in the same change.
64
+ edit the policy to pass the gate. A policy that no longer matches the
65
+ approval fails closed (`POLICY_CHANGED_SINCE_APPROVAL`, or
66
+ `POLICY_APPROVAL_UNREADABLE`): the ship guard and the reopen refuse, and
67
+ `--override-reason` does not waive it. Run `canon policy show`, then stop and
68
+ ask the human to re-approve (`canon policy approve --by <human>`) or restore
69
+ the approved policy from git; never edit the policy yourself. Inside an
70
+ approved scope never stop for a human: fixes, follow-ups and re-reviews go
71
+ straight to the implementer and reviewer in the same change.
63
72
 
64
73
  ## Move the subject
65
74
 
@@ -69,8 +78,30 @@ evidence or reviews, open blockers, an unapproved brief); fix the gap rather
69
78
  than overriding. Move to `shipped` yourself once its guard passes, unless the
70
79
  brief says the human ships. A follow-up on a shipped subject goes back to
71
80
  `verifying` with `--reason`, then the implementer and reviewer in the same
72
- change, then `--to shipped` again. `canon status` flags a shipped subject
81
+ change, then `--to shipped` again. A chore on a shipped subject (`change
82
+ new … --kind chore` names the reopen command) is approved first, then
83
+ reopened the same way; shipping again needs the whole suite evidenced after
84
+ the chore's approval (the implementer's `evidence add --all-scenarios`) and
85
+ one change-level review of the chore from the reviewer (`review add --change
86
+ <slug>`): the order is approval, evidence, review. A blocker closed
87
+ `fixed` needs the suite again. The refusal names the scenarios and
88
+ the command; never override them. `canon status` flags a shipped subject
73
89
  whose gate went red. A bad `--to` fails with `error.field` `--to` and
74
90
  `error.expected` listing the states.
75
91
 
92
+ ## Upgraded repo
93
+
94
+ After a repo moves to a newer canon, run `canon migrate --dry-run`, then
95
+ `canon migrate`. Besides the layout, it compacts the ledger (moves every
96
+ superseded version to `.canon/archive`, as `canon ledger compact` does) and
97
+ re-pins the reviews written before 0.23, which do not count until then (the
98
+ gate, `status` and `subject status` print one advisory naming it). Commit
99
+ what it changes. In CI run it with full history (`fetch-depth: 0`).
100
+
101
+ `canon artifacts migrate --to <tier>` moves the local artifact store to an
102
+ object store and removes the local blobs. It is the repo owner's decision,
103
+ not a step of the loop: never run it yourself. If the repo is large, the
104
+ store is slow or the human has not asked for it, ask the human first, and
105
+ run it only on their answer (`--dry-run` previews it).
106
+
76
107
  Reference: `reference/canon-subject.md`.
@@ -21,7 +21,11 @@ canon brief approve --change <slug> --by "<the human>" --note "<their words>" --
21
21
 
22
22
  After the first `status`, follow `meta.next` of each write. `brief check`
23
23
  lists blockers and advisories; bare `canon brief --as planner` names the next
24
- steps.
24
+ steps. While the brief has blockers, `status` and `change new`'s `meta.next`
25
+ send you to write it (`write the brief: canon brief check --change <slug>
26
+ lists N blockers`); the human step appears only once the brief is clean and
27
+ unapproved. A policy that no longer matches its approval fails closed
28
+ (`POLICY_CHANGED_SINCE_APPROVAL`): never edit it; the human re-approves.
25
29
 
26
30
  ## Steps
27
31
 
@@ -75,7 +79,11 @@ can observe: `change new … --kind chore`. Its brief is short (intent,
75
79
  category, invariants, checks, out of scope) and approved like any brief, one
76
80
  human stop. A chore adds no scenarios: the
77
81
  implementer runs the subject's suite again and binds it with
78
- `canon evidence add --all-scenarios …`, then a re-review follows. Work that
82
+ `canon evidence add --all-scenarios …`, then the reviewer records one
83
+ change-level review of the chore (`canon review add --change <slug>`). On a
84
+ shipped subject `change new` prints the reopen command for the orchestrator
85
+ (`subject status <id> --to verifying --reason "<why>"`); the subject cannot
86
+ ship again until the suite is evidenced after the chore's approval. Work that
79
87
  changes behavior is never a chore: it is a feature change.
80
88
 
81
89
  ## Hand-off
@@ -17,6 +17,10 @@ canon finding list --change-id <slug> --open --as reviewer --json
17
17
  canon finding close --change-id <slug> --round <n> --seq <n> --disposition fixed --resolution-sha <sha> --as reviewer --json
18
18
  canon review add --project-id <root-id> --scenario-id <id> --scenario-id <id> --reviewer <me> --actor-id <me> --role reviewer --session-id <session> --pin <sha> --upstream-ref <ref> --as reviewer --json
19
19
  canon review add --project-id <root-id> --scenario-ids-from - --reviewer <me> --actor-id <me> --role reviewer --session-id <session> --pin <sha> --upstream-ref <ref> --as reviewer --json # ids on stdin, one per line
20
+ canon review add --project-id <root-id> --change <chore-slug> --reviewer <me> --actor-id <me> --role reviewer --session-id <session> --pin <sha> --upstream-ref <ref> --as reviewer --json # a chore: one change-level review of its invariants and checks against the diff
21
+ canon divergence accept --project-id <root-id> --scenario-id <id> --reason "<why the gap stays>" --tracking <issue-url-or-ref> [--expiry YYYY-MM-DD] --reviewer <me> --actor-id <me> --session-id <session> --as reviewer --json
22
+ canon artifacts verify --full --as reviewer --json # the files the evidence and reviews bind are in the artifact store; refuses when an object store is unreachable
23
+ canon query --kind evidence_record --history --as reviewer --json # every version, not only the current record
20
24
  ```
21
25
 
22
26
  `review add` needs `--upstream-ref <ref>` or `--original-spec-ref <path>`;
@@ -24,6 +28,14 @@ add `--artifact <path>` (repeatable) for user-visible behavior. A short
24
28
  `<sha>` is fine. With `write.promote: manual`, follow `finding add` and
25
29
  `finding close` with `canon gate promote --as reviewer --json`.
26
30
 
31
+ A scenario review pins what you saw: the scenario's digest plus the verdict
32
+ and checks (report format and matched case names, or artifact digests) of its
33
+ latest evidence per role, not which record held them. It stays current when
34
+ the implementer records the same verdict from the same checks again, and
35
+ goes stale when the verdict or the checks differ or the scenario changes.
36
+ Reviews written before 0.23 do not count until `canon migrate` re-pins them
37
+ (an advisory names it); that is the orchestrator's step, not yours.
38
+
27
39
  Run `gate check` once, before `review add`: its `unreviewed-promotion`
28
40
  violations are the subjects and scenarios that need your review. Do not loop
29
41
  it. After `review add`, read its `meta.next` instead of calling `gate check`
@@ -56,6 +68,27 @@ what you could not run: a reviewer that cannot run the tests says so and
56
68
  does not sign off; record that instead (`--outcome indeterminate`, or
57
69
  `unavailable` when you could not run).
58
70
 
71
+ ## Check the checks
72
+
73
+ Passing is not enough: read each bound test against its scenario's Then
74
+ steps. Record a finding (`finding add`) when a check pins behavior the
75
+ scenario does not require, or skips a Then step the scenario states: such a
76
+ test passes without proving the scenario. Do this before you sign.
77
+
78
+ ## Accept a known gap
79
+
80
+ `divergence accept` is for a scenario whose honest evidence is `divergent`
81
+ and whose gap is limited by something outside this change (an upstream
82
+ limit, a platform bug) and is tracked there: give the reason and the
83
+ `--tracking` reference, and an `--expiry` when the limit should be gone by
84
+ a date. Never use it to skip a fix you can make in this change. It is your
85
+ step, from a session that did not attest the evidence
86
+ (`DIVERGENCE_ACCEPT_REFUSED` otherwise). The acceptance pins that evidence:
87
+ new evidence for the scenario, or its expiry, ends it, and you accept (or
88
+ the implementer fixes) again. An accepted gap counts as covered for the
89
+ ship guard and stays listed as `accepted-divergence` in `status` and
90
+ `gate check`.
91
+
59
92
  ## Record
60
93
 
61
94
  - Every defect is a finding; `finding list` shows round, seq, severity and
@@ -64,7 +97,17 @@ does not sign off; record that instead (`--outcome indeterminate`, or
64
97
  - Sign off in one batch with `review add`, all or nothing. It writes
65
98
  directly; nothing to promote. canon stores and re-checks attached bytes
66
99
  and never compares images.
67
- - Fixes you ask for return as a re-review of the same change. When a new
68
- evidence record makes your review stale, look again before you re-pin.
100
+ - A blocker you close `fixed` stays blocking until the implementer records
101
+ the whole suite again (`evidence add --all-scenarios`); your pinned review
102
+ stays current when the new evidence says the same, otherwise look again.
103
+ - Fixes you ask for return as a re-review of the same change. When new
104
+ evidence changes a verdict or the checks and so stales your review, look
105
+ again before you re-pin.
106
+ A chore has no scenarios: review it with one change-level review, `canon
107
+ review add --project-id <root-id> --change <slug> --reviewer <me> --role
108
+ reviewer --pin <sha> --upstream-ref <ref> …`. It signs the chore's
109
+ invariants and checks against the diff and pins its brief, so editing the
110
+ brief stales it. The chore also needs evidence recorded after its approval
111
+ (the implementer's `--all-scenarios` run) before it ships.
69
112
 
70
113
  Reference: `reference/canon-review.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
4
4
  "description": "canon — harness knowledge substrate. JS launcher: platform detection + native binary resolution.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {},
32
32
  "optionalDependencies": {
33
- "@journeykit/canon-core-darwin-arm64": "0.21.1",
34
- "@journeykit/canon-core-linux-x64": "0.21.1",
35
- "@journeykit/canon-core-win32-x64": "0.21.1"
33
+ "@journeykit/canon-core-darwin-arm64": "0.23.0",
34
+ "@journeykit/canon-core-linux-x64": "0.23.0",
35
+ "@journeykit/canon-core-win32-x64": "0.23.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",