@journeykit/canon 0.17.0 → 0.18.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.
@@ -36,6 +36,14 @@ a feature, a one-line request included. A human is needed once per scope:
36
36
  stays in the approved change: add it as tasks in that change's
37
37
  `tasks.md`, never with `canon change new`, because a new change is a new
38
38
  scope and needs the human's approval.
39
+ - **A follow-up on a shipped subject reopens it.** Reopen it with
40
+ `canon subject status <id> verifying --reason "<why>" --actor-id <unit>`
41
+ (no human inside the approved scope; the reason and your id are
42
+ recorded), add the follow-up as tasks in the approved change's
43
+ `tasks.md`, then its scenario, evidence and an independent review, and
44
+ ship again with `canon subject status <id> shipped`, which runs every
45
+ guard again. `canon status` flags a shipped subject whose gate went red
46
+ and names the reopen.
39
47
 
40
48
  When no human can answer one of the brief's questions, write the question
41
49
  into the brief, state the assumption you will build on, and say so in your
@@ -59,6 +67,9 @@ work inside an approved scope, never to the approval itself.
59
67
  `canon change new <slug> --subject <id> --title "…"`, and put the brief in
60
68
  the `.canon/changes/<slug>/brief.md` it writes. For a new subject or a
61
69
  scope change, this is where you stop until the human answers (step 1).
70
+ A change that also serves another subject is adopted there too,
71
+ `canon subject adopt <slug> --subject <other>`: it then belongs to both,
72
+ and its open blockers gate both.
62
73
  3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
63
74
  `canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
64
75
  per behavior, with failure paths (`--case failure`) as well as `happy`
@@ -88,12 +99,20 @@ work inside an approved scope, never to the approval itself.
88
99
  `canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
89
100
  with `--report junit:<path>` or `--artifact <path>` when files prove it.
90
101
  canon stores those bytes in `.canon/artifacts/sha256/` (commit it with the
91
- ledger) and `canon gate check` re-checks them. For a scenario about what
102
+ ledger) and `canon gate check` re-checks them; the bound copy is the one
103
+ kept, so the working-tree report itself may be gitignored. For a scenario about what
92
104
  users see, bind the screenshot or recording that shows it
93
105
  (`--artifact <path>`). Under `write: { promote: auto }` (what
94
106
  `canon init` writes) the record is final when the command returns; only
95
107
  in a repo with `write.promote: manual` (or no `write:` key) follow it
96
108
  with `canon gate promote`.
109
+ One test run that proves several scenarios is bound once:
110
+ `canon evidence add --project-id <root-id> --all-scenarios --report <junit|cucumber|swift-testing>:<path> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`
111
+ writes one record per indexed scenario owning a case, each with the
112
+ verdict its own cases show; any refusal writes none. When a new record
113
+ makes a signed review stale, the add prints the reviewer and the
114
+ `canon review add … --pin <sha>` that re-pins it; the reviewer looks
115
+ again before running it.
97
116
  An external evaluator's audit is recorded with `canon evidence add --kind
98
117
  audit`; canon never generates one.
99
118
  Evidence for a plan task (`--task <id>`, before `canon gate task`) must
@@ -102,16 +121,24 @@ work inside an approved scope, never to the approval itself.
102
121
  change's `tasks.md` as it is now, so a task row you add needs no
103
122
  `canon ingest plans` first.
104
123
  7. **Independent review.** A different session from the one that wrote the
105
- code and evidence reviews the change. The reviewer records every defect
124
+ code and evidence reviews the change. The review session must be able
125
+ to run the checks it signs off (the build, the tests, the app); a
126
+ reviewer that cannot run the tests says so and does not sign off. The
127
+ reviewer records every defect
106
128
  the review finds as a finding:
107
129
  `canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`;
130
+ `canon finding list --change-id <slug> [--open] [--json]` shows the
131
+ change's findings (round, seq, severity, disposition, reviewer, summary),
132
+ so the next seq and the open blockers are one command away;
108
133
  once fixed,
109
134
  `canon finding close --change-id <slug> --round <n> --seq <n> --disposition fixed --resolution-sha <sha>`
110
135
  (a short sha is fine; canon resolves it). With `write.promote: manual`,
111
136
  follow each with `canon gate promote`. Every bug fix adds or extends a scenario. The
112
137
  reviewer signs off each scenario:
113
138
  `canon review add --project-id <root-id> --scenario-id <id> --reviewer <who> --actor-id <who> --role reviewer --session-id <review-session> --pin <sha> --upstream-ref <ref>`
114
- (written directly; nothing to promote). A reviewer of user-visible
139
+ (written directly; nothing to promote). Repeat `--scenario-id`, or pass
140
+ `--scenario-ids-from <file>`, to sign off several scenarios in one
141
+ call, all or nothing. A reviewer of user-visible
115
142
  behavior attaches what it looked at with `--artifact <path>`
116
143
  (repeatable); canon stores and re-checks those bytes and never compares
117
144
  images.
@@ -133,7 +160,10 @@ work inside an approved scope, never to the approval itself.
133
160
  `shipped` yourself once `canon subject status <id> shipped` passes its
134
161
  guard, unless the brief says the human ships. `canon status` shows
135
162
  whether each subject's brief is approved, by the same check, and names
136
- the next step.
163
+ the next step. `shipped → verifying` reopens a shipped subject for a
164
+ follow-up (see the working loop above): it needs `--reason`, and it is
165
+ refused, naming the brief, when the subject's newest change is not
166
+ approved.
137
167
 
138
168
  Every write command says whether the record is final (`written directly`,
139
169
  or `staged and promoted in this run`) or staged; only a staged record
@@ -151,9 +181,17 @@ canon init --providers=claude,codex # harnesses for the canon skill; wi
151
181
  canon skills install # follows canon.yaml skills.providers (else the install lock's, else the detected ones; none detected installs nothing)
152
182
  canon skills install --providers=claude,codex,omp,pi # any subset or none; rewrites skills.providers
153
183
  canon skills check # read-only drift check
154
- canon skills doctor # diagnostics, incl. a stray canon projection for an unselected provider; never deletes user files
184
+ canon skills doctor # diagnostics, incl. a stray canon projection for an unselected provider and a projection a formatter rewrote; never deletes user files
155
185
  ```
156
186
 
187
+ `install` and `doctor` end with `owned-paths:`, the globs of every path
188
+ canon writes. Add them to the repo's formatter and linter ignore lists; a
189
+ formatter that rewrites a projection or a ledger record makes canon read
190
+ drift. Specs, `policy.yaml` and the changes are written by hand and are not
191
+ on the list. `doctor` reports a projection whose bytes changed
192
+ but whose content did not as `formatter-rewritten` and names the ignore
193
+ entry to add; a changed file stays `stale`.
194
+
157
195
  The harness list lives in `canon.yaml` as `skills: { providers: [...] }`.
158
196
  `.agents` alone never selects Codex; `.codex` does. Change the list with
159
197
  `canon skills install --providers=<list>`, never by assuming a default.
@@ -28,7 +28,7 @@ Every violation carries one of these stable, grep-able strings:
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
- | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) or a bare `verified` claim with no attached command result. |
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
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`. |
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
 
@@ -391,6 +391,8 @@ canon evidence add --scenario-id cart.add.04 --project-id root \
391
391
  # a JUnit XML or Cucumber JSON report: also records the matched case + outcome
392
392
  canon evidence add ... --report junit:target/nextest/default/junit.xml
393
393
  canon evidence add ... --report cucumber:reports/cucumber.json
394
+ # a Swift Testing event stream (swift test --experimental-event-stream-output <path>)
395
+ canon evidence add ... --report swift-testing:.build/events.jsonl
394
396
  ```
395
397
 
396
398
  A case is bound when the scenario id is its OWN: the first
@@ -408,6 +410,12 @@ verdict when ANY bound case failed or was skipped refuses (exit 1). Files
408
410
  must be inside the repository, are recorded by relative path, and are
409
411
  stored (see Bound evidence stays provable).
410
412
 
413
+ A Swift Testing case is named by its display name (`@Test("cart.add.04
414
+ refuses an out-of-stock item")`), else its function name
415
+ (`cart_add_04_refuses()`). A test with a failing issue failed, a skipped
416
+ test was skipped, a test that started and ended without one passed, and
417
+ a test that started and never ended failed.
418
+
411
419
  Strength, weakest first: `attested` (no attachment) < `artifact` (any
412
420
  bound file) < `report` (a parsed report whose case passed). The policy
413
421
  decides whether the gate cares:
@@ -431,6 +439,55 @@ violation in every mode. Canon still cannot know whether the bound test
431
439
  actually exercises the scenario — binding narrows the trust gap, it does
432
440
  not close it.
433
441
 
442
+ ### One report, many scenarios (0.18)
443
+
444
+ One test run proves many scenarios. Bind it once:
445
+
446
+ ```bash
447
+ canon evidence add --project-id root --all-scenarios \
448
+ --report junit:target/nextest/default/junit.xml \
449
+ --kind test-run --ref "cargo nextest run" --role implementer
450
+ ```
451
+
452
+ Each indexed scenario of `--project-id` that owns a case (the rule above)
453
+ gets its own record, with only its own cases. Its verdict comes from
454
+ them: `faithful` when every case passed, `divergent` when one failed,
455
+ `not-applicable` when one was skipped and none failed. `--verdict <v>`
456
+ claims the same verdict for every record instead. A case whose id is not
457
+ indexed is named and skipped (`skipped <id> — not an indexed scenario`);
458
+ a report no indexed scenario owns is refused (exit 2). The records share
459
+ the stored report and every `--artifact`, and the output lists each
460
+ scenario with its verdict on its own line.
461
+
462
+ It is all or nothing: a claimed `faithful` verdict over a failed case,
463
+ a fabrication marker in `--summary`, an unapproved brief for any
464
+ scenario's subject, or a scenario whose latest record binds a file the
465
+ batch does not refuses the whole batch, and nothing is written.
466
+ `--scenario-id`, `--task`, `--report-case`, `--kind audit`,
467
+ `--drop-attachment` and the approval flags name one subject and are
468
+ refused with `--all-scenarios`.
469
+
470
+ A task's evidence with `--report` and no `--scenario-id` binds EVERY case
471
+ in the report, so a `faithful` verdict needs all of them to pass.
472
+
473
+ ### A write that makes a review stale says so (0.18)
474
+
475
+ When new evidence (one record or a batch) supersedes the evidence a
476
+ current pinned review saw, `canon evidence add` prints one line per
477
+ review it makes stale, with the command that re-pins it:
478
+
479
+ ```
480
+ 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
481
+ ```
482
+
483
+ The decision is the gate's own (the same stale-review rule `canon gate
484
+ check` reports). The command repeats the review's reviewer, actor, role,
485
+ session, provenance ref, runtime labels and artifacts, pinned at `HEAD` (`<sha>`
486
+ when the repository has no commit). A record that is only staged says
487
+ the review "goes stale once this evidence is promoted": promote first,
488
+ then re-pin. The reviewer re-reviews before re-pinning; several
489
+ scenarios re-pin in one `canon review add` (see `canon-review`).
490
+
434
491
  ## Bound evidence stays provable (the artifact store)
435
492
 
436
493
  `canon evidence add --artifact/--report` copies each bound file into
@@ -468,20 +525,29 @@ have its bindings checked. A superseded record is not checked —
468
525
  re-attesting is the fix.
469
526
 
470
527
  Files bound to a review (`canon review add --artifact`, see
471
- `canon-review`) are checked the same way for every in-effect review (the
472
- latest per `(project_id, scenario_id, reviewer)`), with one difference:
473
- they were stored when the review was written, so only an intact blob is
474
- clean. A missing or altered blob is `stale-evidence` on the review's
475
- scenario, and the detail names the review: ``review by `<reviewer>`
476
- (actor `<id>`, <time>)``. The working-tree file is reported but never
477
- vouches for it.
528
+ `canon-review`) are checked by the same rule for every CURRENT in-effect
529
+ review: the latest per `(project_id, scenario_id, reviewer)`, less any
530
+ that went stale against its scenario (its spec or evidence pin moved, or
531
+ a review from before pinning is stale at its commit). A stale review is
532
+ superseded — the gate reads the newer review, or reports the missing
533
+ one as `unreviewed-promotion` — so its files are not re-checked, as a
534
+ superseded evidence record's are not. A missing blob whose working-tree
535
+ file still matches is an advisory, listed with the reviewer:
536
+
537
+ ```
538
+ unstored cart.add.01 reports/cart-badge.png (sha256 9848…, review by `reviewer-2`) is verified against the working tree only
539
+ ```
540
+
541
+ Anything else is `stale-evidence` on the review's scenario, and the
542
+ detail names the review: ``review by `<reviewer>` (actor `<id>`,
543
+ <time>)``.
478
544
 
479
545
  `canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
480
- existing records: every attachment whose working-tree file still matches
481
- its digest is copied in, and everything else is listed as not stored,
482
- with both digests. It exits `0` whenever it ran (the gate decides whether
483
- an unstored file matters), `2` when the ledger cannot be read or a write
484
- fails.
546
+ existing evidence records and current reviews (a superseded review's files are not vaulted): every attachment whose
547
+ working-tree file still matches its digest is copied in, and everything
548
+ else is listed as not stored, with both digests. It exits `0` whenever it
549
+ ran (the gate decides whether an unstored file matters), `2` when the
550
+ ledger cannot be read or a write fails.
485
551
 
486
552
  ## Effect-aware risk approvals (`risk_tiers`)
487
553
 
@@ -135,7 +135,8 @@ fixed finding reopened with `--disposition open` counts as open again.
135
135
  ## `canon review add`
136
136
 
137
137
  ```bash
138
- canon review add --project-id <root-id> --scenario-id <id> \
138
+ canon review add --project-id <root-id> --scenario-id <id> [--scenario-id <id>]... \
139
+ [--scenario-ids-from <file|->] \
139
140
  --reviewer <who> --pin <sha-or-ref> --role <role> \
140
141
  (--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
141
142
  [--outcome signed|indeterminate|unavailable] \
@@ -143,7 +144,7 @@ canon review add --project-id <root-id> --scenario-id <id> \
143
144
  [--artifact <path>]... [--max-artifact-mib <MiB>]
144
145
  ```
145
146
 
146
- One attributed Review record for one scenario, joined on
147
+ One attributed Review record per scenario, joined on
147
148
  `(project_id, scenario_id)` — both are required because two spec roots
148
149
  may carry the same scenario id. Exactly one provenance ref is required.
149
150
  `--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
@@ -153,6 +154,18 @@ own. It is written directly to the ledger, with no staging step, and says
153
154
  so: `canon review add: wrote <path> (new) — written directly; nothing to
154
155
  promote`.
155
156
 
157
+ ### Several scenarios in one call (0.18)
158
+
159
+ `--scenario-id` is repeatable, and `--scenario-ids-from <file>` (`-` reads
160
+ stdin) adds one id per line; `#` starts a comment and blank lines are
161
+ skipped. One reviewer, pin, provenance ref and set of artifacts cover
162
+ every review; each review pins its own scenario and evidence, and each
163
+ gets its own `pinned` and `wrote` line. The artifacts are stored once.
164
+ It is all or nothing: a scenario with no record, or a line in the file
165
+ that is not a scenario id, refuses the call (exit 2) and writes no review.
166
+ This is also the batch re-pin after `canon evidence add` reports reviews
167
+ it made stale (see `canon-gate`).
168
+
156
169
  ### What the reviewer looked at (artifacts)
157
170
 
158
171
  `--artifact <path>` (repeatable) binds a file the reviewer looked at — a
@@ -173,19 +186,25 @@ The field is absent when no `--artifact` is given. `review add` prints
173
186
  .canon/artifacts/sha256/ (<k> newly stored) — commit them with the
174
187
  ledger`.
175
188
 
176
- `canon gate check` re-hashes the stored blob of every file bound to an
177
- in-effect review (the latest per `(project_id, scenario_id, reviewer)`).
178
- A missing or altered blob is `stale-evidence` on the review's scenario,
179
- naming the review, even when the working-tree file still matches (see
180
- `canon-gate`, "Bound evidence stays provable"):
189
+ `canon gate check` re-checks every file bound to a current in-effect
190
+ review (the latest per `(project_id, scenario_id, reviewer)`, unless it
191
+ went stale against its scenario) by the evidence rule (see `canon-gate`,
192
+ "Bound evidence stays provable"). A missing blob whose working-tree file
193
+ still matches is an advisory naming `canon evidence vault`, which stores
194
+ it back. A missing blob with a changed or missing working-tree file, or
195
+ an altered blob, is `stale-evidence` on the review's scenario, naming
196
+ the review:
181
197
 
182
198
  ```
183
- stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
199
+ stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <other hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
184
200
  ```
185
201
 
186
202
  The reviewer's later review replaces the earlier one, so its files are
187
- the ones checked. Canon binds and re-checks bytes only; it never compares
188
- images or judges what a screenshot shows.
203
+ the ones checked. A review that went stale (its scenario's spec or
204
+ evidence moved since) is not checked either: it no longer counts, and the
205
+ scenario's newer review is the one whose files are checked. Canon binds
206
+ and re-checks bytes only; it never compares images or judges what a
207
+ screenshot shows.
189
208
 
190
209
  ### What the review saw (pins)
191
210
 
@@ -104,11 +104,12 @@ canon query --kind trajectory --json
104
104
 
105
105
  canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
106
106
  builds its numbers from the `mart_` views. Those read ONLY local roots
107
- — the git ledger and a LOCAL `.canon/r2` parquet mirror. `canon report`
107
+ — the git ledger, the sqlite `hot` db file (local, no credential; read
108
+ since 0.18) and a LOCAL `.canon/r2` parquet mirror. `canon report`
108
109
  never opens a live DB connection or the live bucket, so:
109
110
 
110
- - Kinds routed to `hot` (sqlite/postgres) do NOT appear in `canon
111
- report`.
111
+ - Kinds routed to a postgres `hot` rung do NOT appear in `canon
112
+ report`; kinds routed to `canon init`'s default sqlite `hot` rung do.
112
113
  - Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
113
114
  has been separately materialized — there is no automatic sync.
114
115
 
@@ -103,7 +103,7 @@ this subject and domain — the exact memory step 2 retrieves next time.
103
103
 
104
104
  ## Status lifecycle + the shipped evidence gate
105
105
 
106
- `canon subject status <id> <state> [--override-reason <text>] [--actor-id <id>]`
106
+ `canon subject status <id> <state> [--override-reason <text>] [--reason <text>] [--actor-id <id>]`
107
107
  performs a policy-gated transition.
108
108
  The legal chain:
109
109
 
@@ -111,9 +111,10 @@ The legal chain:
111
111
  proposed → specced → building → verifying → shipped
112
112
  ```
113
113
 
114
- plus **any state → retired** (a subject can be retired from anywhere).
115
- Any other transition is rejected by failure class, exits 1, and leaves
116
- the record UNCHANGED (fail closed).
114
+ plus **shipped → verifying** to reopen a shipped subject (with
115
+ `--reason`, below) and **any state → retired** (a subject can be retired
116
+ from anywhere). Any other transition is rejected by failure class, exits
117
+ 1, and leaves the record UNCHANGED (fail closed).
117
118
 
118
119
  ```bash
119
120
  canon subject status subject-domain-loop specced
@@ -242,6 +243,39 @@ raised later, a scenario tagged later) still fails the gate. Without
242
243
  `require_review`, nothing is printed and nothing changes, except that
243
244
  `shipped` still runs the brief check above.
244
245
 
246
+ ### Reopening a shipped subject (0.18)
247
+
248
+ A follow-up on a shipped subject (a fix, a review round, a scenario its
249
+ gate now misses) reopens it: reopen, add tasks to the approved change,
250
+ scenario, evidence, review, ship.
251
+
252
+ ```bash
253
+ canon subject status demo-subject verifying \
254
+ --reason "follow-up: the retry path leaks a file handle" --actor-id impl
255
+ ```
256
+
257
+ ```
258
+ canon subject status: reopened `demo-subject` from shipped, recorded by `impl`: follow-up: the retry path leaks a file handle; add the follow-up as tasks in its approved change `c-demo`, then scenario, evidence and review; shipping again runs every guard
259
+ canon subject status: demo-subject → verifying — written directly; nothing to promote
260
+ ```
261
+
262
+ - `--reason "<one line>"` is required for `shipped → verifying` and
263
+ refused on any other transition (exit 2, as is a blank one). The new
264
+ record carries it with the actor and the change the work goes into:
265
+ `reopen: { reason: …, change: c-demo, actor: { agent_id: impl } }`. The
266
+ next status write clears it, as it clears a waiver.
267
+ - Inside the approved scope it needs no human: the subject's newest
268
+ adopted change's brief is approved, or there is no brief to answer.
269
+ Otherwise it refuses, exit 1, naming the brief to approve, and
270
+ `--override-reason` does not waive it:
271
+ ``unreviewed-promotion demo-subject — shipped → verifying: reopening needs the approved scope, and the brief is not approved — .canon/changes/c-next/brief.md has no `Approved:` line; …``.
272
+ - The review guard does not run on a reopen (it prints `skipped … — a
273
+ reopen moves the subject back to fix its gaps; shipping again runs
274
+ it`). `verifying → shipped` afterwards runs every guard above again.
275
+ - `canon status` flags a shipped subject whose gate went red (whatever
276
+ `verifying → shipped` would now refuse it on) and its only step is
277
+ this reopen.
278
+
245
279
  ## Adopt flow
246
280
 
247
281
  Planning docs written outside canon become change/task records via
@@ -261,19 +295,33 @@ and a flip re-imports that change itself. Run `canon ingest plans` for a
261
295
  plan written outside canon, or when `canon query --kind task` must list
262
296
  rows nobody has flipped yet.
263
297
 
264
- `adopt` stamps the change's `subject_id` and adds it to the Subject's
265
- `change_ids`, so `canon query --kind change --change-id …` and `canon
266
- query --kind subject` agree on the link from both ends. It writes only
267
- the side that lacks the link, so rerunning it on a linked pair writes
268
- nothing and exits `0`.
298
+ `adopt` adds the subject to the change's `subject_ids` and the change to
299
+ the Subject's `change_ids`, so `canon query --kind change --change-id …`
300
+ and `canon query --kind subject` agree on the link from both ends. It
301
+ writes only the side that lacks the link, so rerunning it on a linked
302
+ pair writes nothing and exits `0`.
303
+
304
+ A change may belong to several subjects (0.18). Adopting it under a
305
+ second subject adds that subject, never re-points the change, and says
306
+ so:
307
+
308
+ ```
309
+ canon subject adopt: linked change `refactor-core` to subject `editing` as well; the change now belongs to `perception` and `editing` — written directly; nothing to promote
310
+ ```
311
+
312
+ Every reader joins a subject to its changes from both sides (the
313
+ subject's `change_ids`, and every change whose `subject_ids` names it):
314
+ the open-blocker rule, the status guard, `canon status`, the brief and
315
+ the evidence refusal. An open blocker on a shared change gates each
316
+ subject it belongs to. A change written before 0.18 names one subject as
317
+ `subject_id`; it reads, and `canon query --kind change --json` shows it,
318
+ as a one-element `subject_ids`.
269
319
 
270
320
  The two records cannot be written atomically, so the subject is written
271
- first: its `change_ids` is what the gate reads a subject's adopted
272
- changes from (`open-blocker`, the status guard), so a half-done adoption
273
- still has that change's open blockers counted. If the change write then
274
- fails, `adopt` exits `2`, names the subject that now lists the change,
275
- and prints `canon subject adopt <change_id> --subject <id>` to complete
276
- it.
321
+ first: a half-done adoption still has that change's open blockers
322
+ counted. If the change write then fails, `adopt` exits `2`, names the
323
+ subject that now lists the change, and prints `canon subject adopt
324
+ <change_id> --subject <id>` to complete it.
277
325
 
278
326
  ### Starting a new change: `canon change new`
279
327
 
@@ -393,6 +441,14 @@ uses, so status and the gate cannot disagree about a gap:
393
441
  canon brief and no line it reads
394
442
  ``brief: not approved (<brief> has no `Approved:` line)``.
395
443
  There is no record kind for it; the file is the record.
444
+ - **Red shipped subject** (0.18) — a `shipped` subject that the
445
+ `verifying → shipped` guard would now refuse, by the guard's own checks
446
+ (owned scenarios, verdicts, required cases, audits, and, where
447
+ `require_review` covers `shipped`, reviews and open blockers under
448
+ `block_on_findings`; a gap its waiver recorded is not counted), prints
449
+ ``gate: red — <n> <class> (first: <subject>), …; reopen with `canon subject status <id> verifying --reason "<why it reopens>" --actor-id <unit>` ``,
450
+ carries `red: [...]` in `--json`, and that reopen is its only `next:`
451
+ step. Open blockers count against every subject a change belongs to.
396
452
  - **Unowned** — scenarios with no `@subject:` tag.
397
453
  - **Audits** — how many scenarios are sampled for
398
454
  `spec_coverage.require_audit`, confirmed, owed (no audit or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.17.0",
3
+ "version": "0.18.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.17.0",
34
- "@journeykit/canon-core-linux-x64": "0.17.0",
35
- "@journeykit/canon-core-win32-x64": "0.17.0"
33
+ "@journeykit/canon-core-darwin-arm64": "0.18.0",
34
+ "@journeykit/canon-core-linux-x64": "0.18.0",
35
+ "@journeykit/canon-core-win32-x64": "0.18.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",