@journeykit/canon 0.22.0 → 0.24.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.
@@ -24,7 +24,7 @@ a feature, a one-line request included. A human is needed once per scope:
24
24
  answer until you report: the brief is that report. The one answer
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
- `canon brief approve --change <slug> --by <the human> --note "<their words>"`.
27
+ `canon brief approve --change-id <slug> --by <the human> --note "<their words>"`.
28
28
  Never edit the policy to make the gate pass: a policy that differs from
29
29
  the approved one fails closed (`POLICY_CHANGED_SINCE_APPROVAL`, not
30
30
  waivable); stop and ask the human. A brief edited after its
@@ -39,7 +39,9 @@ a feature, a one-line request included. A human is needed once per scope:
39
39
  new answer. A follow-up on a shipped subject reopens it
40
40
  (`canon subject status <id> --to verifying --reason "<why>"`); a fix
41
41
  closed `fixed`, and an approved chore, then need the whole suite evidenced
42
- again (`evidence add --all-scenarios`) and re-reviewed before it ships.
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-id <slug>`).
43
45
  - When no human can answer one of the brief's questions, write your
44
46
  assumption into the brief's Questions and assumptions, build on it, and say
45
47
  so in your report. That covers the brief's questions and work inside an
@@ -58,6 +60,24 @@ Every write command says whether the record is final (`written directly`,
58
60
  or `staged and promoted in this run`) or staged; only a staged record
59
61
  needs `canon gate promote`, and only `write.promote: manual` leaves one.
60
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
+
61
81
  ## Know your role
62
82
 
63
83
  canon cannot detect a role; you declare it. Pass `--as <role>` or set
@@ -93,6 +113,10 @@ A role is guidance, not enforcement: `--role` on `evidence add` and
93
113
  on `error.code`, fix the option `field` names using the `expected` values,
94
114
  and follow `error.hint`. Every refusal carries a concrete `hint`: follow
95
115
  it. The codes are stable; `canon schema --errors --json` lists them.
116
+ - Flags that name an id are spelled `--<noun>-id` everywhere (`--change-id`,
117
+ `--project-id`, `--subject-id`, `--scenario-id`, `--actor-id`,
118
+ `--session-id`), and `--kind` values carry no `_record` suffix
119
+ (`--kind evidence`).
96
120
  - `canon status` sends the planner to write the brief while `brief check`
97
121
  has blockers; the human stop appears only when the brief is clean and
98
122
  unapproved.
@@ -84,7 +84,7 @@ enum, like `lane`.
84
84
  ## Subject pinning
85
85
 
86
86
  Every scenario belongs to a Subject (the durable product unit, see
87
- `canon-subject`). Write the Subject first, then pin with `--subject`;
87
+ `canon-subject`). Write the Subject first, then pin with `--subject-id`;
88
88
  the scenario index carries the tag onto the index record and
89
89
  `spec_coverage.scope` filters on it. An unpinned scenario is invisible to
90
90
  a scoped gate.
@@ -94,9 +94,9 @@ a scoped gate.
94
94
  ```bash
95
95
  canon feature new checkout.cart --title "Cart" # once per surface
96
96
  canon scenario new checkout.cart.01 --title "Adding an item raises the count" \
97
- --subject checkout-core --lane behavior --case happy # once per behavior
97
+ --subject-id checkout-core --lane behavior --case happy # once per behavior
98
98
  canon scenario new checkout.cart.04 --title "Adding an out-of-stock item is refused" \
99
- --subject checkout-core --lane behavior --case failure # and its failure path
99
+ --subject-id checkout-core --lane behavior --case failure # and its failure path
100
100
  ```
101
101
 
102
102
  There is no format or sync step after these: the scaffold writes a
@@ -108,7 +108,7 @@ one of them prints `NOT refreshed`, the corpus has a format violation
108
108
  duplicate id; `canon inventory sync` (or `canon format <root>`) names it.
109
109
  Fix it and the next command indexes the files (see `canon-inventory`).
110
110
 
111
- - `--actor <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
111
+ - `--actor-id <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
112
112
  provenance line. Default is `canon-scaffold`.
113
113
  - **Keep the `# canon:` lines when you fill in the scenario.** Dropping one
114
114
  makes `canon inventory sync` abort on `missing-provenance`; restore it
@@ -146,7 +146,7 @@ Fix it and the next command indexes the files (see `canon-inventory`).
146
146
  - **A Feature title that is a theme.** "Fidelity of the original
147
147
  experience" names no surface. Name the screen.
148
148
  - **Hand-stamped provenance.** Eighty lines with one timestamp and the
149
- same `agent_id`. Use `canon scenario new --actor`.
149
+ same `agent_id`. Use `canon scenario new --actor-id`.
150
150
  - **Coverage by splitting.** One behavior as two scenarios so two rows go
151
151
  green. One behavior, one id.
152
152
  - **Runner tags as classification.** `@design`, `@p2`, `@wip` are
@@ -226,9 +226,13 @@ from shipping until every one of its evidenced scenarios has evidence
226
226
  recorded after the close or the approval. The refusal names the scenarios
227
227
  and the batch command: `canon evidence add --project-id <root-id>
228
228
  --all-scenarios --report <format>:<path> --kind test-run --role
229
- implementer --ref "<test command>" …`. That evidence stales the pinned
230
- reviews, so a re-review follows (and a new `divergence accept`, where the
231
- gap remains).
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-id <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).
232
236
 
233
237
  ### Audit records (`--kind audit`)
234
238
 
@@ -263,33 +267,16 @@ canon evidence add --kind audit --project-id root --scenario-id cart.add.01 \
263
267
  - `canon status` shows an `audits` section (sampled / confirmed / owed /
264
268
  refuted) and suggests `canon evidence add --kind audit …` in `next:`.
265
269
 
266
- ### Carrying and dropping bindings (`--drop-attachment`)
267
-
268
- `canon evidence add` for a scenario carries forward what the scenario's
269
- current latest record of the same fold (declared records, or audit
270
- records for an audit; committed ledger plus `_staging`) bound: every
271
- file the new write does not name again is bound into the new record
272
- unchanged (same path, digest and stored blob), and `--json` lists it as
273
- `carried: [paths]`. A file named again (`--artifact`/`--report`, new
274
- bytes at the same path included) replaces its earlier binding. A suite
275
- batch (`--all-scenarios`) therefore never costs a scenario its
276
- screenshots. A carried file whose blob the artifact store does not
277
- hold intact is refused (exit `2`, `EVIDENCE_CARRY_BLOB_MISSING`, nothing
278
- written): re-bind it, run `canon evidence vault`, or drop it. The audit
279
- fold (`--kind audit`) does not carry: an audit that leaves out a file
280
- the latest audit bound is refused (exit `2`) unless it names the file
281
- again or drops it with `--drop-attachment`, so a counterexample never
282
- rides along into a confirming audit.
283
-
284
- The only way to lose a binding is `--drop-attachment <path>`
285
- (repeatable) with one single-line `--drop-reason <text>`, recorded as
286
- `attachment_drop: {paths, reason}`. With `--all-scenarios` the drop
287
- applies to each scenario that binds the path. Refused (`2`): a drop
288
- without a reason, a reason without a drop, a drop path that no
289
- scenario of the write leaves out, a multi-line reason, or a drop
290
- without `--scenario-id`/`--all-scenarios` (task-only evidence has no
291
- carry or drop). `canon status` lists recorded drops (`dropped bindings
292
- (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.
293
280
 
294
281
  ### Nothing is attested before the brief is approved (`--override-reason`)
295
282
 
@@ -308,7 +295,7 @@ check `canon status` and the ship guard use; see
308
295
  `--task`:
309
296
 
310
297
  ```
311
- canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line, so the human has not answered it; once they do, record their words with `canon brief approve --change c-demo --by <the human> --note "<their words>"`
298
+ canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line, so the human has not answered it; once they do, record their words with `canon brief approve --change-id c-demo --by <the human> --note "<their words>"`
312
299
  canon evidence add: refused — stop, report the brief to the human and wait for their answer: nothing is built or attested for a subject before its brief is approved. Record their answer with `canon brief approve --by <the human> --note "<their words>"` only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
313
300
  ```
314
301
 
@@ -319,7 +306,7 @@ openspec/changes/c-ext/proposal.md, which canon reads and never edits, so
319
306
  its approval lives in that canon file, so the human has not answered it;
320
307
  once they do, create .canon/changes/c-ext/brief.md holding a `Source:
321
308
  openspec/changes/c-ext/proposal.md` line, then record their words with
322
- `canon brief approve --change c-ext --by <the human> --note "<their words>"` ``. An `Approved:` line in the
309
+ `canon brief approve --change-id c-ext --by <the human> --note "<their words>"` ``. An `Approved:` line in the
323
310
  external `proposal.md` itself is never read.
324
311
 
325
312
  A subject with no brief to answer (no adopted change, or a newest change
@@ -376,51 +363,43 @@ the stop is at the brief. `canon gate check` does not grade briefs, so a
376
363
  waived ship brief prints no advisory here; `canon status` keeps showing
377
364
  the brief as not approved.
378
365
 
379
- ### Reviews that predate pinning (checked against their pin)
380
-
381
- With `require_review` on, the reviews in effect (latest per reviewer and
382
- scenario) that carry no pins were written before 0.15. Each still names
383
- `pin`, the commit it reviewed, and the gate checks it there (0.16):
384
-
385
- - **current** — the scenario's digest at that commit (read with `git`,
386
- normalized exactly as `canon inventory sync` computes
387
- `scenario_digest`) equals its current digest, and no declared evidence
388
- record of the scenario is newer than the review. It counts. When the
389
- scenario lived in another `.feature` file at that commit, the spec
390
- root's files at that commit are searched for its id, then the files
391
- under each of the root's `former_roots` (where `canon migrate` moved
392
- it from, e.g. `specs` for a root now at `.canon/specs`). Only the
393
- root's own directories are searched, never another root's.
394
- - **stale** — the scenario changed since the commit, or it has newer
395
- evidence. It no longer counts, and an in-scope scenario left with no
396
- other qualifying review is `unreviewed-promotion`:
397
-
398
- ```
399
- 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`)
400
- ```
401
-
402
- A spec change reads `its spec changed since <pin> (the scenario's
403
- digest at that commit is not its current one)`.
404
- - **undecidable** — the pin is not a commit in this clone (a shallow
405
- clone, rewritten history), or no feature file of the scenario's root
406
- holds its id at that commit. It keeps counting, as in 0.15: a missing
407
- commit never becomes a violation.
366
+ ### Reviews that predate 0.23 pinning (`canon migrate`)
408
367
 
409
- One advisory line after the violations counts the three. A stale one
410
- whose scenario already has another qualifying review (current, signed,
411
- distinct) is counted as superseded, since nothing is owed for it; only
412
- the scenarios a stale one leaves without a qualifying review are named
413
- 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:
414
375
 
415
376
  ```
416
- 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
417
378
  ```
418
379
 
419
- A review of a scenario no feature file declares any more is not counted.
420
- In CI, check out full history (`actions/checkout` with `fetch-depth: 0`)
421
- for the job that runs `canon gate check`, or every legacy review is
422
- undecidable. Each pinned commit is read once per run through one `git
423
- 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.
424
403
 
425
404
  ## Experimental: evidence binding (`experimental.evidence_binding`, off by default)
426
405
 
@@ -503,15 +482,20 @@ them: `faithful` when every case passed, `divergent` when one failed,
503
482
  claims the same verdict for every record instead. A case whose id is not
504
483
  indexed is named and skipped (`skipped <id> — not an indexed scenario`);
505
484
  a report no indexed scenario owns is refused (exit 2). The records share
506
- the stored report and every `--artifact`, and the output lists each
507
- scenario with its verdict on its own line.
485
+ the stored report, and the output lists each scenario with its verdict on
486
+ its own line. Each record binds the attachments its own cases name in the
487
+ report (a JUnit test case's `[[ATTACHMENT|path]]` lines, which Playwright's
488
+ JUnit reporter writes; a Cucumber JSON embedding), so a test's screenshot
489
+ stays with its scenario. A file the report does not name goes with
490
+ `--artifact <scenario-id>=<path>`; a bare `--artifact <path>` in a batch is
491
+ refused (`EVIDENCE_INVALID_ARGUMENT`). A report attachment that is missing,
492
+ outside the repo or oversized is refused.
508
493
 
509
494
  It is all or nothing: a claimed `faithful` verdict over a failed case,
510
495
  a fabrication marker in `--summary`, an unapproved brief for any
511
- scenario's subject, or a carried attachment whose blob is missing from the
512
- store refuses the whole batch, and nothing is written. Each scenario's
513
- earlier attachments are carried into its new record (see above), and
514
- `--drop-attachment` applies per scenario. `--scenario-id`, `--task`,
496
+ scenario's subject refuses the whole batch, and nothing is written. Each
497
+ record binds only what the batch names (nothing is carried forward).
498
+ `--scenario-id`, `--task`,
515
499
  `--report-case`, `--kind audit` and the approval flags name one subject
516
500
  and are refused with `--all-scenarios`.
517
501
 
@@ -522,10 +506,10 @@ in the report, so a `faithful` verdict needs all of them to pass.
522
506
 
523
507
  When new evidence (one record or a batch) supersedes the evidence a
524
508
  current pinned review saw, `canon evidence add` prints one line per
525
- review it makes stale, with the command that re-pins it:
509
+ review it makes stale (its verdict or checks differ from what the review saw), with the command that re-pins it:
526
510
 
527
511
  ```
528
- 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
512
+ 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
529
513
  ```
530
514
 
531
515
  The decision is the gate's own (the same stale-review rule `canon gate
@@ -545,6 +529,16 @@ again. The directory is tracked in git (`canon init` never ignores it):
545
529
  commit it with the ledger. A file larger than 25 MiB is refused (exit 2);
546
530
  raise the limit with `--max-artifact-mib <N>`.
547
531
 
532
+ Where those bytes live is `canon.yaml`'s `artifact_store: { backend: local |
533
+ <s3 tier>, prefix: artifacts/ }` (default `local`: the directory above,
534
+ git-tracked, no credentials). With an object-store tier, `evidence add`,
535
+ `review add` and `evidence vault` upload each blob under
536
+ `<prefix>sha256/<digest>` and keep nothing locally. `canon gate check` and
537
+ `canon artifacts verify [--full]` ask the store (a HEAD per digest; `--full`
538
+ downloads and re-hashes) and refuse, never pass, when it cannot be reached.
539
+ `canon artifacts migrate --to <tier> [--dry-run]` moves an existing local
540
+ store; it is the repo owner's decision. See `canon-storage`.
541
+
548
542
  `canon gate check` re-checks every file bound to a LATEST evidence record
549
543
  (the latest per task and per `(project_id, scenario_id)`):
550
544
 
@@ -575,8 +569,7 @@ re-attesting is the fix.
575
569
  Files bound to a review (`canon review add --artifact`, see
576
570
  `canon-review`) are checked by the same rule for every CURRENT in-effect
577
571
  review: the latest per `(project_id, scenario_id, reviewer)`, less any
578
- that went stale against its scenario (its spec or evidence pin moved, or
579
- a review from before pinning is stale at its commit). A stale review is
572
+ that went stale against its scenario (its spec or evidence pin moved). A stale review is
580
573
  superseded — the gate reads the newer review, or reports the missing
581
574
  one as `unreviewed-promotion` — so its files are not re-checked, as a
582
575
  superseded evidence record's are not. A missing blob whose working-tree
@@ -715,13 +708,12 @@ staged. A record another run staged is not touched: only `gate promote`
715
708
  commits everything staged. It stays safe to run anyway: with nothing
716
709
  staged it prints
717
710
  `nothing staged` and exits `0`. `divergence add` stages for `canon
718
- divergence promote` and names it, under the same `write:` rule. Every
719
- other write — `review add`, `divergence
720
- resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
711
+ gate promote` and names it, under the same `write:` rule. Every
712
+ other write — `review add`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
721
713
  `gate task` (it edits the plan document and re-ingests it)
722
714
  — is final when it returns, and its success line ends `— written
723
715
  directly; nothing to promote`. Full promote-on-write contract:
724
- `canon-storage` (Staged writes and promotion).
716
+ `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`.
725
717
 
726
718
  ## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
727
719
 
@@ -179,7 +179,7 @@ record.
179
179
  An operator-requested rollback is a distinct authenticated action:
180
180
 
181
181
  ```bash
182
- canon learn rollback <strategy_id> --reason "<reason>" --actor alice \
182
+ canon learn rollback <strategy_id> --reason "<reason>" --actor-id alice \
183
183
  --signature-file rollback.sig --approved-at 2026-10-02T12:00:00Z \
184
184
  [--contradicting-trajectory-id <trajectory_id>] --repo .
185
185
  ```
@@ -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-id <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
@@ -167,14 +169,30 @@ that is not a scenario id, refuses the call (exit 2) and writes no review.
167
169
  This is also the batch re-pin after `canon evidence add` reports reviews
168
170
  it made stale (see `canon-gate`).
169
171
 
172
+ ### A chore is reviewed as a change (`--change-id`, 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-id
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-id` takes a
184
+ chore's slug and refuses a feature change.
185
+
170
186
  ### What the reviewer looked at (artifacts)
171
187
 
172
188
  `--artifact <path>` (repeatable) binds a file the reviewer looked at — a
173
189
  screenshot, a recording, a trace — exactly as `canon evidence add
174
190
  --artifact` binds one: the path must resolve to a file inside the
175
191
  repository, a file over 25 MiB is refused unless `--max-artifact-mib <N>`
176
- raises the limit, and the bytes are copied into
177
- `.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
178
196
  review is written. Any refusal exits 2 and writes and stores nothing. The
179
197
  review records each distinct file once, in the order given:
180
198
 
@@ -225,12 +243,16 @@ reviewer never types them:
225
243
  `scenario_digest`; then the review pins its `source_digest` (the
226
244
  sha256 of the whole `.feature` file) and records
227
245
  `scenario_digest_scope: file`;
228
- - `evidence` — the scenario's latest evidence record per role, each as
229
- `{key, digest}`: the ledger file `kind=evidence_record/<key>__<digest>.json`.
230
- A scenario with no evidence yet is pinned with an empty list.
231
-
232
- It prints `canon review add: pinned <id> at scenario digest <12 hex> and
233
- <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
234
256
  scenario with no record (never synced) is refused with exit 2 and nothing
235
257
  is written: there is nothing to pin.
236
258
 
@@ -308,44 +330,25 @@ The scenario is reviewed when at least one of those latest reviews:
308
330
 
309
331
  1. is `signed`;
310
332
  2. still matches its pins — the scenario record's current
311
- `source_digest`, and its current latest evidence record per role —
312
- when it has pins; or, written before 0.15 without pins, still
313
- 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);
314
336
  3. passes `distinct_actor`, when that is on.
315
337
 
316
- ### Reviews written before pinning
317
-
318
- A review written before 0.15 has no pins, only `pin`: the commit it
319
- reviewed. The gate reads the scenario's `.feature` file at that commit
320
- (`git`, with the same per-scenario normalization `canon inventory sync`
321
- uses for `scenario_digest`; if the scenario was in another file then, the
322
- spec root's files at that commit are searched for its id) and decides:
323
-
324
- - **current** when that digest equals the scenario's current one and no
325
- declared evidence record of the scenario is newer than the review: it
326
- counts under rules 1 and 3;
327
- - **stale** when the scenario changed since the commit (`its spec changed
328
- since <pin>`) or has evidence newer than the review (`it has declared
329
- evidence newer than the review`): it no longer counts, exactly like a
330
- stale pinned review;
331
- - **undecidable** when the pin is not a commit in this clone (shallow
332
- clone, rewritten history) or the scenario id is absent at it: it keeps
333
- counting, as in 0.15. A missing commit never becomes a violation.
334
-
335
- `canon gate check` prints one advisory line with the split (exit code
336
- unchanged by the line itself). A stale one whose scenario already has
337
- another qualifying review is counted as superseded and not named:
338
-
339
- ```
340
- 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
341
- ```
342
-
343
- `canon status` prints the same line as a warning, and `canon subject
344
- status` prints it on stderr for the subject's own scenarios; all three
345
- make the same decision. Re-review exactly the scenarios the line names:
346
- the new review pins what it saw and replaces the reviewer's legacy one.
347
- In CI, check out full history (`fetch-depth: 0`) for the job that runs
348
- 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.
349
352
 
350
353
  ### `unreviewed-promotion`
351
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 --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
@@ -69,12 +69,12 @@ canon subject new subject-domain-loop \
69
69
  ### 2. Retrieve subject-scoped strategy memory BEFORE working
70
70
 
71
71
  ```bash
72
- canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
72
+ canon retrieve --role dev --domain dev --subject-id subject-domain-loop --k 5
73
73
  ```
74
74
 
75
- - `--regime <key>` XOR the derived pair `--domain <d> [--subject <id>]`:
75
+ - `--regime <key>` XOR the derived pair `--domain <d> [--subject-id <id>]`:
76
76
  give the full regime key directly, OR let retrieval derive it. Never both.
77
- - With `--domain`/`--subject`, retrieval tries `<domain>-<subject_id>`
77
+ - With `--domain`/`--subject-id`, retrieval tries `<domain>-<subject_id>`
78
78
  first, then `<domain>` — a subject's own lessons win, falling back to
79
79
  the domain's shared memory when the subject is new.
80
80
  - Fail-soft: an empty/missing store degrades to an empty guidance list,
@@ -82,7 +82,7 @@ canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
82
82
 
83
83
  ### 3. Work → pin every artifact to `subject_id`
84
84
 
85
- - **Scenarios**: `canon scenario new <tag> --subject <subject-id>` emits
85
+ - **Scenarios**: `canon scenario new <tag> --subject-id <subject-id>` emits
86
86
  the `@subject:<subject-id>` tag; the scenario index maps the tag onto
87
87
  the scenario's `subject_id` the next time a command reads it (no sync
88
88
  step; see `canon-inventory`). A malformed/absent tag leaves it unset
@@ -172,7 +172,7 @@ a policy. The refusal is an `unreviewed-promotion` naming the subject, and
172
172
  says what to do:
173
173
 
174
174
  ```
175
- canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line; a human must answer the brief, and only from that answer the agent will record their words with `canon brief approve --change c-demo --by <the human> --note "<their words>"`
175
+ canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line; a human must answer the brief, and only from that answer the agent will record their words with `canon brief approve --change-id c-demo --by <the human> --note "<their words>"`
176
176
  canon subject status: refused — get the human's answer to the brief and record it with `canon brief approve`, or pass --override-reason <text> to move anyway; the waiver is recorded on the subject
177
177
  ```
178
178
 
@@ -181,7 +181,7 @@ For an external change with no canon brief yet, the reason reads
181
181
  the external plan <proposal>, which canon reads and never edits, so its
182
182
  approval lives in that canon file`` and the agent will ``create
183
183
  .canon/changes/<c>/brief.md holding a `Source: <proposal>` line, then
184
- record their words with `canon brief approve --change <c> --by <the human> --note "<their words>"` ``.
184
+ record their words with `canon brief approve --change-id <c> --by <the human> --note "<their words>"` ``.
185
185
 
186
186
  Never record the approval without the human's answer. When there is no brief to
187
187
  answer (no adopted change, or a newest change with neither a canon
@@ -306,7 +306,7 @@ Planning docs written outside canon become change/task records via
306
306
  ```bash
307
307
  canon ingest plans --repo . # import → change/task rows
308
308
  canon subject adopt subject-domain-loop-plan \
309
- --subject subject-domain-loop # link change → subject
309
+ --subject-id subject-domain-loop # link change → subject
310
310
  ```
311
311
 
312
312
  A change made with `canon change new` (below) is recorded and adopted
@@ -343,7 +343,7 @@ The two records cannot be written atomically, so the subject is written
343
343
  first: a half-done adoption still has that change's open blockers
344
344
  counted. If the change write then fails, `adopt` exits `2`, names the
345
345
  subject that now lists the change, and prints `canon subject adopt
346
- <change_id> --subject <id>` to complete it.
346
+ <change_id> --subject-id <id>` to complete it.
347
347
 
348
348
  ### Starting a new change: `canon change new`
349
349
 
@@ -351,7 +351,7 @@ For a change that does not exist yet, one command writes it and records
351
351
  it adopted:
352
352
 
353
353
  ```bash
354
- canon change new add-login --subject auth --title "Add login"
354
+ canon change new add-login --subject-id auth --title "Add login"
355
355
  ```
356
356
 
357
357
  ```
@@ -387,7 +387,7 @@ canon change new: recorded change `add-login` adopted into subject `auth` — wr
387
387
  cannot be written atomically. If the change record fails after the
388
388
  subject record was written, the command exits `2`, keeps the change
389
389
  dir and that one subject record, and prints the repair, `canon ingest
390
- plans && canon subject adopt <slug> --subject <id>`, which completes
390
+ plans && canon subject adopt <slug> --subject-id <id>`, which completes
391
391
  the link and is safe to rerun.
392
392
  - **Refuses** with exit `2`, writing nothing, when the subject does not
393
393
  exist, or when the slug already has a `.canon/changes/<slug>` dir
@@ -411,12 +411,12 @@ it, so the external source keeps the change, its summary and its tasks.
411
411
  ### The brief: `canon brief`, `canon brief check`, `canon brief approve`
412
412
 
413
413
  ```bash
414
- canon brief [--change <slug>] # state and the next one to three steps; never edits
415
- canon brief check [--change <slug>] [--json] # findings; exit 0 clean, 2 blockers, 1 error
416
- canon brief approve --change <slug> --by <the human> --note "<their words>"
414
+ canon brief [--change-id <slug>] # state and the next one to three steps; never edits
415
+ canon brief check [--change-id <slug>] [--json] # findings; exit 0 clean, 2 blockers, 1 error
416
+ canon brief approve --change-id <slug> --by <the human> --note "<their words>"
417
417
  ```
418
418
 
419
- Without `--change`, the repo's only live change is read. The brief is the
419
+ Without `--change-id`, the repo's only live change is read. The brief is the
420
420
  PRD `canon change new` writes; sections are found by their
421
421
  `<!-- canon:section <key> role=… -->` marker, never by heading text. A
422
422
  brief without markers (written before 0.19) checks as `brief.legacy`, an
@@ -453,9 +453,10 @@ built or kept, never what it does. Its brief is intent, category,
453
453
  invariants, checks, out of scope; it adds no scenarios, and its evidence
454
454
  is the subject's suite run again at the chore commit with
455
455
  `canon evidence add --all-scenarios --report <kind>:<path> …`. That
456
- supersedes the old evidence, which makes the pinned reviews stale, so an
457
- independent re-review follows. Work that changes behavior is a feature
458
- 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-id <slug> …` before it ships. Work that changes
459
+ behavior is a feature change, never a chore.
459
460
 
460
461
  ## Where are we: `canon status`
461
462
 
@@ -488,8 +489,7 @@ uses, so status and the gate cannot disagree about a gap:
488
489
  stale or whose latest reviews are all `indeterminate`/`unavailable`
489
490
  (see `canon-review`), and the `canon review add` step names both. With
490
491
  `require_review` on, a `warning:` line counts the in-effect reviews that
491
- predate review pinning (0.15), checked against the commit each pinned:
492
- current, stale (named) and undecidable.
492
+ predate 0.23 review pinning and wait for `canon migrate`.
493
493
  - **Brief** (0.16, 0.17, 0.19) — for each `building`, `verifying` or `shipped`
494
494
  subject, and each `proposed` subject whose newest adopted change has a
495
495
  brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
@@ -545,8 +545,6 @@ uses, so status and the gate cannot disagree about a gap:
545
545
  refuted audit adds a `next:` step suggesting `canon evidence add
546
546
  --kind audit …` (recorded from an external evaluator; canon never runs
547
547
  one).
548
- - **Dropped bindings** — `dropped bindings (N):` lists every recorded
549
- `--drop-attachment` (scenario, paths, reason); `drops[]` in `--json`.
550
548
  - **Brief overrides** — a `warning:` line per evidence record attested
551
549
  with `canon evidence add --override-reason` while its subject's brief
552
550
  was not approved: what it attests, who, when, the subject and the
@@ -562,7 +560,7 @@ uses, so status and the gate cannot disagree about a gap:
562
560
  first, then building, specced, proposed, shipped: an unapproved brief
563
561
  (building and later, and proposed once its change has a brief)
564
562
  → the human step below, which is then the subject's
565
- only step; no scenario → `canon scenario new … --subject <id>` (and, when no
563
+ only step; no scenario → `canon scenario new … --subject-id <id>` (and, when no
566
564
  `.feature` file holds a scenario yet, `canon feature new` first, unless
567
565
  every subject waits on its brief); a missing case → `canon scenario new
568
566
  <surface>.<next nn> … --case <case>`; (building and later) an
@@ -574,12 +572,12 @@ uses, so status and the gate cannot disagree about a gap:
574
572
  Last, unowned scenarios → `canon status`, after tagging them in their
575
573
  `.feature` files (status re-indexes the edit; no sync step). With more
576
574
  than one `specs.roots[]` entry, every suggested `canon scenario new`
577
- carries `--project`: the owning root for a missing case, `<root-id>`
575
+ carries `--project-id`: the owning root for a missing case, `<root-id>`
578
576
  (the reason lists the roots) for a subject that owns no scenario yet.
579
577
  - **The one human step.** Every step is the agent's to run except one,
580
578
  and it comes only once the brief is clean: while `canon brief check
581
- --change <slug>` has blockers, the subject gets the planner step
582
- `write the brief: canon brief check --change <slug> lists N blockers`
579
+ --change-id <slug>` has blockers, the subject gets the planner step
580
+ `write the brief: canon brief check --change-id <slug> lists N blockers`
583
581
  (`role: planner`) instead. A subject whose brief is enforced (its
584
582
  newest change's canon
585
583
  `brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
@@ -591,10 +589,10 @@ uses, so status and the gate cannot disagree about a gap:
591
589
  and the approval is never assumed: `canon evidence add` refuses <id>'s
592
590
  scenarios and tasks until it is recorded; once the human answers,
593
591
  <record> (one call records the brief and the policy approval)``, where
594
- `<record>` is ``record their words with `canon brief approve --change
592
+ `<record>` is ``record their words with `canon brief approve --change-id
595
593
  <c> --by <the human> --note "<their words>"` ``, or, for an external
596
594
  change, ``create <brief> holding a `Source: <proposal>` line, then
597
- record their words with `canon brief approve --change <c> --by <the
595
+ record their words with `canon brief approve --change-id <c> --by <the
598
596
  human> --note "<their words>"` ``; for a brief edited after its
599
597
  approval, ``re-approve with `canon brief approve …` ``.
600
598
  It is the subject's only step, whatever its status: nothing suggests
@@ -640,7 +638,7 @@ Status fails closed rather than suggest progress the gate would refuse:
640
638
  ],
641
639
  "unowned": ["cart.promo.01"],
642
640
  "next": [
643
- { "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: .canon/changes/c-promo/brief.md does not exist yet: change `c-promo` comes from the external plan openspec/changes/c-promo/proposal.md, which canon reads and never edits, so its approval lives in that canon file (an older change's approval does not cover a new scope), and the approval is never assumed: … once the human answers, create .canon/changes/c-promo/brief.md holding a `Source: openspec/changes/c-promo/proposal.md` line, then record their words with `canon brief approve --change c-promo --by <the human> --note \"<their words>\"` (one call records the brief and the policy approval)", "actor": "human" },
641
+ { "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: .canon/changes/c-promo/brief.md does not exist yet: change `c-promo` comes from the external plan openspec/changes/c-promo/proposal.md, which canon reads and never edits, so its approval lives in that canon file (an older change's approval does not cover a new scope), and the approval is never assumed: … once the human answers, create .canon/changes/c-promo/brief.md holding a `Source: openspec/changes/c-promo/proposal.md` line, then record their words with `canon brief approve --change-id c-promo --by <the human> --note \"<their words>\"` (one call records the brief and the policy approval)", "actor": "human" },
644
642
  { "command": "canon finding close --change-id c-cart --round 1 --seq 1 --disposition fixed --resolution-sha <sha> --actor-id <unit> --session-id <session>", "why": "cart: 1 open blocker finding(s) (first: …); fix it, commit, then close it", "actor": "agent" },
645
643
  { "command": "canon review add --project-id root --scenario-id cart.add.02 --reviewer <reviewer> --actor-id <reviewer> --session-id <review-session> --role reviewer --pin <sha> --original-spec-ref \"<feature file>\"", "why": "cart: 1 of 2 scenario(s) lack a qualifying review; …", "actor": "agent" },
646
644
  { "command": "canon status", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); add `@subject:<id>` above each in its .feature file, then check again: …", "actor": "agent" }
@@ -14,13 +14,16 @@ a `specs.roots[]` entry in `canon.yaml` (`root` by default).
14
14
  ```bash
15
15
  canon status --as implementer --json # once, at the start
16
16
  canon feature new <area>.<surface> --title "<title>" --as implementer --json
17
- canon scenario new <area>.<surface>.<nn> --subject <subject-id> --flow <F-id> --case <happy|edge|failure> --title "<title>" --as implementer --json
18
- canon scenario new --from - --as implementer --json # many at once: NDJSON on stdin, one {"id","title","subject","flow","case",…} per line
17
+ canon scenario new <area>.<surface>.<nn> --subject-id <subject-id> --flow <F-id> --case <happy|edge|failure> --title "<title>" --as implementer --json
18
+ canon scenario new --from - --as implementer --json # many at once: NDJSON on stdin, one {"id","title","subject-id","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
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
22
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
23
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)
24
27
  canon gate promote --as implementer --json # only with write.promote: manual
25
28
  ```
26
29
 
@@ -52,10 +55,23 @@ After the first `status`, follow `meta.next` of each write; do not call
52
55
  `--all-scenarios`: one record per indexed scenario owning a case, each with
53
56
  the verdict its own cases show; any refusal writes none. For one scenario
54
57
  use `--scenario-id <id>` (repeatable) with `--report` or `--artifact`.
55
- canon stores the bytes in `.canon/artifacts/sha256/` (commit it with the
56
- ledger) and `canon gate check` re-checks them, so the working-tree report
57
- may be gitignored. For a scenario about what users see, bind the screenshot
58
- 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, let the test report carry its
69
+ screenshots: Playwright's JUnit reporter attaches, per test case, what a
70
+ test attaches (`testInfo.attach(name, {path})`, or `screenshot: 'on'`), and
71
+ one `evidence add --all-scenarios --report junit:<path>` binds to each
72
+ record only its own tests' files. A file the report does not name goes
73
+ with `--artifact <scenario-id>=<path>`; a bare `--artifact` in a batch is
74
+ refused. An external evaluator's audit is
59
75
  `--kind audit`; canon never generates one. Evidence for a task of an
60
76
  imported plan (`--task <id>`) must bind the artifact or report that proves
61
77
  the task; a canon command is never that proof. When the behavior cannot
@@ -67,16 +83,21 @@ After the first `status`, follow `meta.next` of each write; do not call
67
83
  the record is final when `evidence add` returns. Only with
68
84
  `write.promote: manual` (or no `write:` key) follow it with
69
85
  `gate promote`; `meta.next` names it.
70
- 6. **A new record makes a signed review stale.** The add prints the reviewer
71
- and the `canon review add … --pin <sha>` that re-pins it; that is the
72
- reviewer's call. Do not run it yourself.
86
+ 6. **A changed result makes a signed review stale.** A review pins the
87
+ verdict and the checks (report format and matched case names, or artifact
88
+ digests) of the evidence it saw, not which record it was. Recording the
89
+ same verdict from the same checks keeps every review current; a different
90
+ verdict, different checks or an edited scenario stales it. The add prints
91
+ the reviewer and the `canon review add … --pin <sha>` that re-pins it; that
92
+ is the reviewer's call. Do not run it yourself.
73
93
  7. **After a fix or a chore, record the whole suite again.** A blocker the
74
94
  reviewer closed `fixed`, and an approved chore, are not done until every
75
95
  evidenced scenario has evidence newer than the close or the approval:
76
96
  run the suite and bind it with `canon evidence add --all-scenarios …`
77
- (the shipping refusal names the scenarios and this command). The
78
- re-review follows; a scenario whose gap was accepted is accepted again
79
- by the reviewer once new evidence supersedes the old.
97
+ (the shipping refusal names the scenarios and this command). Reviews stay
98
+ current while the verdicts and checks are unchanged; a scenario whose gap
99
+ was accepted is accepted again by the reviewer once new evidence says
100
+ something different.
80
101
 
81
102
  ## Retry safety
82
103
 
@@ -88,6 +109,8 @@ response, run it again.
88
109
 
89
110
  When every scenario has evidence, the review is the reviewer's session, in a
90
111
  separate process: do not review your own change. A chore adds no scenarios:
91
- run the suite again and bind it with `--all-scenarios`, which supersedes the
92
- old evidence and makes pinned reviews stale; shipping needs that evidence
93
- newer than the chore's approval. Reference: `reference/canon-gate.md`.
112
+ once it is approved, run the suite and bind it with `--all-scenarios`;
113
+ shipping needs that evidence newer than the chore's approval, plus the
114
+ reviewer's one change-level review of the chore (`review add --change-id`).
115
+ Scenario reviews stay current when the verdicts and checks are unchanged.
116
+ Reference: `reference/canon-gate.md`.
@@ -10,11 +10,14 @@ Every line is complete; fill the `<placeholders>`.
10
10
 
11
11
  ```bash
12
12
  canon status --as orchestrator --json --fields next,waitingOn # at the start and when resuming
13
- canon brief approve --change <slug> --by "<the human>" --note "<their words>" --as orchestrator --json
13
+ canon brief approve --change-id <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
16
  canon policy show --as orchestrator --json # policy digest and its approval: approved, changed, unreadable or unapproved
17
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
18
21
  ```
19
22
 
20
23
  ## Read the state
@@ -78,10 +81,27 @@ brief says the human ships. A follow-up on a shipped subject goes back to
78
81
  change, then `--to shipped` again. A chore on a shipped subject (`change
79
82
  new … --kind chore` names the reopen command) is approved first, then
80
83
  reopened the same way; shipping again needs the whole suite evidenced after
81
- the chore's approval (`evidence add --all-scenarios`) and a re-review. A
82
- blocker closed `fixed` needs the same. The refusal names the scenarios and
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-id
86
+ <slug>`): the order is approval, evidence, review. A blocker closed
87
+ `fixed` needs the suite again. The refusal names the scenarios and
83
88
  the command; never override them. `canon status` flags a shipped subject
84
89
  whose gate went red. A bad `--to` fails with `error.field` `--to` and
85
90
  `error.expected` listing the states.
86
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
+
87
107
  Reference: `reference/canon-subject.md`.
@@ -12,17 +12,17 @@ Every line is complete; fill the `<placeholders>`.
12
12
  ```bash
13
13
  canon status --as planner --json # once, at the start
14
14
  canon subject new <subject-id> --domain <domain> --title "<title>" --actor-id <unit> --as planner --json
15
- canon change new <slug> --subject <subject-id> --title "<title>" --as planner --json
16
- canon change new <slug> --subject <subject-id> --kind chore --title "<title>" --as planner --json
17
- canon subject adopt <slug> --subject <other-subject-id> --as planner --json
18
- canon brief check --change <slug> --as planner --json
19
- canon brief approve --change <slug> --by "<the human>" --note "<their words>" --as planner --json
15
+ canon change new <slug> --subject-id <subject-id> --title "<title>" --as planner --json
16
+ canon change new <slug> --subject-id <subject-id> --kind chore --title "<title>" --as planner --json
17
+ canon subject adopt <slug> --subject-id <other-subject-id> --as planner --json
18
+ canon brief check --change-id <slug> --as planner --json
19
+ canon brief approve --change-id <slug> --by "<the human>" --note "<their words>" --as planner --json
20
20
  ```
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
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>
25
+ send you to write it (`write the brief: canon brief check --change-id <slug>
26
26
  lists N blockers`); the human step appears only once the brief is clean and
27
27
  unapproved. A policy that no longer matches its approval fails closed
28
28
  (`POLICY_CHANGED_SINCE_APPROVAL`): never edit it; the human re-approves.
@@ -79,7 +79,8 @@ can observe: `change new … --kind chore`. Its brief is short (intent,
79
79
  category, invariants, checks, out of scope) and approved like any brief, one
80
80
  human stop. A chore adds no scenarios: the
81
81
  implementer runs the subject's suite again and binds it with
82
- `canon evidence add --all-scenarios …`, then a re-review follows. On a
82
+ `canon evidence add --all-scenarios …`, then the reviewer records one
83
+ change-level review of the chore (`canon review add --change-id <slug>`). On a
83
84
  shipped subject `change new` prints the reopen command for the orchestrator
84
85
  (`subject status <id> --to verifying --reason "<why>"`); the subject cannot
85
86
  ship again until the suite is evidenced after the chore's approval. Work that
@@ -10,14 +10,17 @@ Every line is complete; fill the `<placeholders>`. `<root-id>` is the `id` of
10
10
  a `specs.roots[]` entry in `canon.yaml` (`root` by default).
11
11
 
12
12
  ```bash
13
- canon brief check --change <slug> --as reviewer --json
13
+ canon brief check --change-id <slug> --as reviewer --json
14
14
  canon gate check --as reviewer --json # once, before review add
15
15
  canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "<defect>" --as reviewer --json
16
16
  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-id <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
20
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 --history --as reviewer --json # every version, not only the current record
21
24
  ```
22
25
 
23
26
  `review add` needs `--upstream-ref <ref>` or `--original-spec-ref <path>`;
@@ -25,6 +28,14 @@ add `--artifact <path>` (repeatable) for user-visible behavior. A short
25
28
  `<sha>` is fine. With `write.promote: manual`, follow `finding add` and
26
29
  `finding close` with `canon gate promote --as reviewer --json`.
27
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
+
28
39
  Run `gate check` once, before `review add`: its `unreviewed-promotion`
29
40
  violations are the subjects and scenarios that need your review. Do not loop
30
41
  it. After `review add`, read its `meta.next` instead of calling `gate check`
@@ -87,9 +98,16 @@ ship guard and stays listed as `accepted-divergence` in `status` and
87
98
  directly; nothing to promote. canon stores and re-checks attached bytes
88
99
  and never compares images.
89
100
  - A blocker you close `fixed` stays blocking until the implementer records
90
- the whole suite again (`evidence add --all-scenarios`); re-review after
91
- it, since new evidence makes your pinned review stale.
92
- - Fixes you ask for return as a re-review of the same change. When a new
93
- evidence record makes your review stale, look again before you re-pin.
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-id <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.
94
112
 
95
113
  Reference: `reference/canon-review.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.22.0",
3
+ "version": "0.24.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.22.0",
34
- "@journeykit/canon-core-linux-x64": "0.22.0",
35
- "@journeykit/canon-core-win32-x64": "0.22.0"
33
+ "@journeykit/canon-core-darwin-arm64": "0.24.0",
34
+ "@journeykit/canon-core-linux-x64": "0.24.0",
35
+ "@journeykit/canon-core-win32-x64": "0.24.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",