@journeykit/canon 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skills/SKILL.src.md +92 -30
- package/dist/skills/reference/canon-authoring.md +10 -3
- package/dist/skills/reference/canon-gate.md +238 -19
- package/dist/skills/reference/canon-inventory.md +35 -8
- package/dist/skills/reference/canon-policy.md +114 -7
- package/dist/skills/reference/canon-review.md +198 -21
- package/dist/skills/reference/canon-storage.md +124 -1
- package/dist/skills/reference/canon-subject.md +173 -45
- package/package.json +4 -4
|
@@ -26,21 +26,94 @@ diagnostic and reports `clean: false`.
|
|
|
26
26
|
## The starter policy `canon init` writes
|
|
27
27
|
|
|
28
28
|
A fresh `canon init` writes `.canon/policy.yaml` (skip it with
|
|
29
|
-
`--no-policy`; an existing file is never overwritten)
|
|
29
|
+
`--no-policy`; an existing file is never overwritten). `--policy
|
|
30
|
+
light|standard|strict` picks how much it asks for; the default is
|
|
31
|
+
`strict`:
|
|
32
|
+
|
|
33
|
+
| Level | `spec_coverage` |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `light` | `require_evidence: true` — every scenario needs evidence |
|
|
36
|
+
| `standard` | light, plus `require_cases: [failure]` — every feature surface needs a `@case:failure` scenario |
|
|
37
|
+
| `strict` | standard, plus `require_review: {}` — verifying/shipped subjects need an independent review |
|
|
30
38
|
|
|
31
39
|
```yaml
|
|
40
|
+
# canon policy level: strict
|
|
32
41
|
spec_coverage:
|
|
33
42
|
require_evidence: true # every scenario needs evidence
|
|
34
43
|
require_cases: [failure] # every feature surface needs a @case:failure scenario
|
|
35
44
|
require_review: {} # verifying/shipped subjects need an independent review
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
The
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
The first line names the level. The file carries comments explaining
|
|
48
|
+
each field, and a `light` or `standard` starter carries the next levels'
|
|
49
|
+
keys as commented lines: to raise the level, uncomment them and change
|
|
50
|
+
the level line, and a human approves the edit (next section). An
|
|
51
|
+
unknown `--policy` value exits 2, and so does `--policy` with `--no-policy`.
|
|
52
|
+
|
|
53
|
+
`canon status` prints the level under the policy settings
|
|
54
|
+
(`level: strict`, or `level: light — raise it by …` with how to raise
|
|
55
|
+
it) and in `--json` as `policy.level`. It reads the level line when the
|
|
56
|
+
file's leading comment block has one naming a known level; otherwise it
|
|
57
|
+
infers the level from the `spec_coverage` section, which must match a
|
|
58
|
+
level exactly (no `scope`, `exclude_lanes`, `require_audit`, or
|
|
59
|
+
non-default `require_review` settings); anything else is `custom`.
|
|
60
|
+
`policy.level` is `null` when there is no policy.
|
|
61
|
+
|
|
62
|
+
Every level loads with zero diagnostics. The policy is what `canon gate
|
|
63
|
+
check` grades the work against, so a human should review and approve
|
|
64
|
+
it, and every later change to it (next section). The agent doing the
|
|
65
|
+
work should not write its own grading policy. Narrow it with `scope:
|
|
66
|
+
[building, verifying]` to check evidence only for subjects in those
|
|
67
|
+
statuses (see `canon-gate`).
|
|
68
|
+
|
|
69
|
+
## Approving the policy
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
canon policy approve --by <who> [--note <text>] [--repo <dir>]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A human approves the policy after reviewing `.canon/policy.yaml`, in the
|
|
76
|
+
same answer that approves a subject's brief (0.16; see the skill's working
|
|
77
|
+
loop). The human runs this, or the agent runs it right after that answer
|
|
78
|
+
as `canon policy approve --by <the human> --note "<their words>"`. It
|
|
79
|
+
writes `.canon/policy.approval.json` directly (nothing to promote):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"policy_digest": "sha256:<sha256 of the policy.yaml bytes>",
|
|
84
|
+
"approved_by": "<who>",
|
|
85
|
+
"approved_at": "2026-10-11T09:00:00Z",
|
|
86
|
+
"note": "<text, or null>"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Commit it with the policy: the file is tracked, and neither canon's
|
|
91
|
+
`.gitignore` nor the one `canon init` writes ignores it. Running the
|
|
92
|
+
command again replaces the record. With no `.canon/policy.yaml` it is
|
|
93
|
+
refused (exit `2`), and so is an empty `--by`.
|
|
94
|
+
|
|
95
|
+
While a policy exists with no approval, `canon gate check` prints after
|
|
96
|
+
its result:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
advisory: policy not approved: .canon/policy.yaml has no approval record (.canon/policy.approval.json); a human reviews it and runs `canon policy approve --by <who>`
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Once the policy's bytes no longer match the approved digest, it prints
|
|
103
|
+
this instead:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
advisory: policy changed since approval: .canon/policy.yaml is sha256:<now>, but `<who>` approved sha256:<then> at <time>; a human reviews it and runs `canon policy approve --by <who>`
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
An approval file that does not parse reads
|
|
110
|
+
`advisory: policy approval unreadable: …`.
|
|
111
|
+
`canon status` prints the same text as a `warning:` line (and in
|
|
112
|
+
`--json` `warnings`). The advisory never changes an exit code: canon
|
|
113
|
+
cannot tell whether a human or an agent ran the command, so the record
|
|
114
|
+
names who approved which bytes, and the advisory says when that no
|
|
115
|
+
longer holds. An agent runs `canon policy approve` only to record a
|
|
116
|
+
human's answer, never without one and never on its own behalf.
|
|
44
117
|
|
|
45
118
|
## The closed CEL profile
|
|
46
119
|
|
|
@@ -124,6 +197,40 @@ question rather than adding an expression field. Conditional logic about
|
|
|
124
197
|
what a record *means* lives in `policy.yaml`/`applies_when:`, evaluated
|
|
125
198
|
against the record's fields, never inside the record.
|
|
126
199
|
|
|
200
|
+
## Sampled audits: `spec_coverage.require_audit`
|
|
201
|
+
|
|
202
|
+
Optional; absent means no audit is ever owed (a recorded `refuted` audit
|
|
203
|
+
is still `false-success` regardless — see `canon-gate`).
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
spec_coverage:
|
|
207
|
+
require_audit:
|
|
208
|
+
rate: 0.1 # required: a number in [0, 1]
|
|
209
|
+
seed: "2026-q4" # optional string; default ""
|
|
210
|
+
scope: [verifying, shipped] # optional subject statuses; this is the default; [] = every scenario
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`spec_coverage.exclude_lanes` applies. A malformed `require_audit` —
|
|
214
|
+
unknown key, missing `rate`, `rate` out of range or a string, bad
|
|
215
|
+
`scope`, `null` or a non-mapping — poisons the whole `spec_coverage`
|
|
216
|
+
section: the gate reports `uncovered-cell spec_coverage — ... unusable`
|
|
217
|
+
rather than checking anything under it.
|
|
218
|
+
|
|
219
|
+
Sampling is deterministic. A scenario is in the sample iff the first 8
|
|
220
|
+
bytes of
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
sha256(seed || 0x00 || project_id || 0x00 || scenario_id)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
read as a big-endian u64, are less than `rate · 2^64` — computed exactly,
|
|
227
|
+
as a comparison against `ceil(rate · 2^64)`, so `rate: 0` samples nothing
|
|
228
|
+
and `rate: 1` samples everything. A sampled in-scope scenario whose
|
|
229
|
+
latest audit is missing or `indeterminate` is `uncovered-cell`; with
|
|
230
|
+
`shipped` in `scope`, `canon subject status <id> shipped` also refuses
|
|
231
|
+
it. `canon context` and the `canon status` policy line show
|
|
232
|
+
`require_audit=(rate=… seed=`…` scope=…)`.
|
|
233
|
+
|
|
127
234
|
## Effect-aware risk tiers
|
|
128
235
|
|
|
129
236
|
The optional `risk_tiers` section is not CEL. Absent means no risk-approval
|
|
@@ -17,8 +17,10 @@ state they are in.
|
|
|
17
17
|
The order an agent follows to claim a subject done:
|
|
18
18
|
|
|
19
19
|
1. **Test-run evidence** per owned scenario (`canon evidence add
|
|
20
|
-
--scenario-id <id> --project-id <root-id> ... --session-id <session
|
|
21
|
-
|
|
20
|
+
--scenario-id <id> --project-id <root-id> ... --session-id <session>`;
|
|
21
|
+
final when it returns under `write: { promote: auto }`, which
|
|
22
|
+
`canon init` writes, and followed by `canon gate promote` only under
|
|
23
|
+
`write.promote: manual`). See `canon-gate`.
|
|
22
24
|
2. **Independent review** of the change(s) the subject adopted. Record
|
|
23
25
|
each issue the reviewer raises as a Finding, fix it, then close it:
|
|
24
26
|
|
|
@@ -26,15 +28,17 @@ The order an agent follows to claim a subject done:
|
|
|
26
28
|
canon finding add --change-id c-demo --round 1 --seq 1 \
|
|
27
29
|
--severity blocker --disposition open --reviewer reviewer-2 \
|
|
28
30
|
--summary "unbounded subprocess wait" --session-id review-session-1
|
|
29
|
-
canon gate promote
|
|
30
31
|
# ... fix, commit ...
|
|
31
32
|
canon finding close --change-id c-demo --round 1 --seq 1 \
|
|
32
33
|
--disposition fixed --resolution-sha "$(git rev-parse HEAD)"
|
|
33
|
-
canon gate promote
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Under `write.promote: manual` (or no `write:` key), run
|
|
37
|
+
`canon gate promote` after each of the two.
|
|
38
|
+
|
|
36
39
|
3. **One Review per scenario**, by someone other than the evidence actor,
|
|
37
|
-
from a session other than the one that attested the evidence
|
|
40
|
+
from a session other than the one that attested the evidence, after
|
|
41
|
+
the evidence is in (the review pins what it saw):
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
44
|
canon review add --project-id root --scenario-id world.demo.01 \
|
|
@@ -43,7 +47,13 @@ The order an agent follows to claim a subject done:
|
|
|
43
47
|
--pin "$(git rev-parse HEAD)" --upstream-ref "pr#41"
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
`review add` writes directly; there is nothing to promote.
|
|
50
|
+
`review add` writes directly; there is nothing to promote. If the
|
|
51
|
+
reviewer could not reach a verdict, record that instead of signing:
|
|
52
|
+
`--outcome indeterminate` (or `unavailable` when it could not run).
|
|
53
|
+
Changing the scenario or re-attesting it afterwards makes the review
|
|
54
|
+
stale; review it again. A reviewer of user-visible behavior attaches
|
|
55
|
+
the screenshots or recordings it looked at with `--artifact <path>`
|
|
56
|
+
(below).
|
|
47
57
|
|
|
48
58
|
4. **Move the subject**: `canon subject status <id> verifying`, later
|
|
49
59
|
`shipped`. See `canon-subject`.
|
|
@@ -65,22 +75,36 @@ canon finding close --change-id <c> --round <n> --seq <n> \
|
|
|
65
75
|
--disposition fixed --resolution-sha <sha> [--session-id <id>] # or rejected / deferred / open
|
|
66
76
|
```
|
|
67
77
|
|
|
68
|
-
Both stage a candidate
|
|
69
|
-
|
|
78
|
+
Both stage a candidate. Under `write: { promote: auto }` (what
|
|
79
|
+
`canon init` writes) the same run promotes it and the success line names
|
|
80
|
+
the committed record (``wrote kind=finding/… — staged and promoted in this
|
|
81
|
+
run; nothing to promote``); under `write.promote: manual`
|
|
82
|
+
`canon gate promote` commits it, and the line says so (``staged … — run
|
|
83
|
+
`canon gate promote` to commit it``). `finding close`
|
|
70
84
|
changes only the disposition (and `resolution_sha` with it); severity,
|
|
71
85
|
reviewer, summary, and refs are copied from the committed record. It
|
|
72
86
|
appends a second version at the same natural key — the first stays. Its
|
|
73
87
|
`--actor-id` and `--session-id` name who authored the transition; the
|
|
74
88
|
raiser's stay on the first version.
|
|
75
89
|
|
|
90
|
+
Every sha flag (`--reviewed-sha`, `--resolution-sha`, `--introduced-by`)
|
|
91
|
+
takes a full 40-character sha or a short one (4 or more lowercase hex
|
|
92
|
+
characters). A short sha is resolved with `git rev-parse --verify
|
|
93
|
+
<sha>^{commit}` in the repo, and the record stores the full sha. It is
|
|
94
|
+
refused (exit `2`) when it names no commit, when more than one commit
|
|
95
|
+
starts with it (pass more characters), when it only resolves by peeling a
|
|
96
|
+
tag, or outside a git work tree.
|
|
97
|
+
|
|
76
98
|
### A defect the author found in its own work
|
|
77
99
|
|
|
78
|
-
Record it like any other finding,
|
|
79
|
-
|
|
80
|
-
the
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
Record it like any other finding, at the moment you find it and before
|
|
101
|
+
review, with `--reviewer` and `--actor-id` set to your own id; never ask
|
|
102
|
+
the reviewer to record it later under theirs. Nothing refuses a finding
|
|
103
|
+
whose reviewer is also the implementer, and an open `blocker` the author
|
|
104
|
+
raised blocks under `block_on_findings` exactly like a reviewer's.
|
|
105
|
+
`--introduced-by` names the commit that introduced the defect, not who
|
|
106
|
+
found it: `--introduced-by self` is a usage error (exit 2) that points at
|
|
107
|
+
`--reviewer`.
|
|
84
108
|
|
|
85
109
|
### Severities
|
|
86
110
|
|
|
@@ -113,19 +137,101 @@ fixed finding reopened with `--disposition open` counts as open again.
|
|
|
113
137
|
```bash
|
|
114
138
|
canon review add --project-id <root-id> --scenario-id <id> \
|
|
115
139
|
--reviewer <who> --pin <sha-or-ref> --role <role> \
|
|
116
|
-
(--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>]
|
|
140
|
+
(--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
|
|
141
|
+
[--outcome signed|indeterminate|unavailable] \
|
|
142
|
+
[--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>] \
|
|
143
|
+
[--artifact <path>]... [--max-artifact-mib <MiB>]
|
|
117
144
|
```
|
|
118
145
|
|
|
119
146
|
One attributed Review record for one scenario, joined on
|
|
120
147
|
`(project_id, scenario_id)` — both are required because two spec roots
|
|
121
148
|
may carry the same scenario id. Exactly one provenance ref is required.
|
|
122
149
|
`--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
|
|
123
|
-
A Review carries no verdict; the `verifying → shipped`
|
|
124
|
-
refuses missing or `Divergent` evidence verdicts on its
|
|
125
|
-
written directly to the ledger, with no staging step, and says
|
|
126
|
-
`canon review add: wrote <path> (new) — written directly; nothing to
|
|
150
|
+
A Review carries no verdict about the behavior; the `verifying → shipped`
|
|
151
|
+
ship gate still refuses missing or `Divergent` evidence verdicts on its
|
|
152
|
+
own. It is written directly to the ledger, with no staging step, and says
|
|
153
|
+
so: `canon review add: wrote <path> (new) — written directly; nothing to
|
|
127
154
|
promote`.
|
|
128
155
|
|
|
156
|
+
### What the reviewer looked at (artifacts)
|
|
157
|
+
|
|
158
|
+
`--artifact <path>` (repeatable) binds a file the reviewer looked at — a
|
|
159
|
+
screenshot, a recording, a trace — exactly as `canon evidence add
|
|
160
|
+
--artifact` binds one: the path must resolve to a file inside the
|
|
161
|
+
repository, a file over 25 MiB is refused unless `--max-artifact-mib <N>`
|
|
162
|
+
raises the limit, and the bytes are copied into
|
|
163
|
+
`.canon/artifacts/sha256/<hex>` (commit it with the ledger) before the
|
|
164
|
+
review is written. Any refusal exits 2 and writes and stores nothing. The
|
|
165
|
+
review records each distinct file once, in the order given:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"artifacts": [{"path": "reports/title.png", "sha256": "<64 hex>"}]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The field is absent when no `--artifact` is given. `review add` prints
|
|
172
|
+
`canon review add: artifact store holds <n> bound file(s) under
|
|
173
|
+
.canon/artifacts/sha256/ (<k> newly stored) — commit them with the
|
|
174
|
+
ledger`.
|
|
175
|
+
|
|
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"):
|
|
181
|
+
|
|
182
|
+
```
|
|
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>`
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
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.
|
|
189
|
+
|
|
190
|
+
### What the review saw (pins)
|
|
191
|
+
|
|
192
|
+
`review add` reads the corpus and records `pins` on the review; the
|
|
193
|
+
reviewer never types them:
|
|
194
|
+
|
|
195
|
+
- `scenario_digest` — the scenario record's `scenario_digest`: a sha256
|
|
196
|
+
over that scenario alone, as `canon inventory sync` stores it. It
|
|
197
|
+
covers the scenario's tags and block (header, steps, doc strings,
|
|
198
|
+
tables, `Examples:`) and the context it inherits: the Feature's tags,
|
|
199
|
+
the Feature `Background:`, and an enclosing `Rule:` with its tags and
|
|
200
|
+
`Background:`. Layout does not count: comments, blank lines, line
|
|
201
|
+
indentation, tag order, and table alignment (cells are compared
|
|
202
|
+
trimmed and unescaped). Values do: any step, tag or cell change, and
|
|
203
|
+
whitespace inside a doc string after the delimiter's own indentation
|
|
204
|
+
is removed. Other scenarios never count. A record synced before 0.15 has no
|
|
205
|
+
`scenario_digest`; then the review pins its `source_digest` (the
|
|
206
|
+
sha256 of the whole `.feature` file) and records
|
|
207
|
+
`scenario_digest_scope: file`;
|
|
208
|
+
- `evidence` — the scenario's latest evidence record per role, each as
|
|
209
|
+
`{key, digest}`: the ledger file `kind=evidence_record/<key>__<digest>.json`.
|
|
210
|
+
A scenario with no evidence yet is pinned with an empty list.
|
|
211
|
+
|
|
212
|
+
It prints `canon review add: pinned <id> at scenario digest <12 hex> and
|
|
213
|
+
<n> latest evidence record(s); …` (`at source digest` for a file pin). A
|
|
214
|
+
scenario with no record (never synced) is refused with exit 2 and nothing
|
|
215
|
+
is written: there is nothing to pin.
|
|
216
|
+
|
|
217
|
+
The gate compares each pin like for like: a scenario pin with the
|
|
218
|
+
scenario's current `scenario_digest`, a file pin with its current
|
|
219
|
+
`source_digest`. Adding or editing another scenario in the file leaves a
|
|
220
|
+
scenario-pinned review current; editing this scenario, or a Background
|
|
221
|
+
or Rule it inherits, makes it stale. A file-pinned review stays a file
|
|
222
|
+
pin until it is reviewed again. Review after the spec and evidence are
|
|
223
|
+
final.
|
|
224
|
+
|
|
225
|
+
### The reviewer's outcome and runtime
|
|
226
|
+
|
|
227
|
+
`--outcome` records how the reviewer's run ended: `signed` (the default:
|
|
228
|
+
an approval), `indeterminate` (it ran and reached no verdict) or
|
|
229
|
+
`unavailable` (it could not run). Only `signed` counts; the other two
|
|
230
|
+
are recorded so a failed review is visible, never mistaken for a
|
|
231
|
+
sign-off or a rejection, and `review add` says so. `--reviewer-provider`,
|
|
232
|
+
`--reviewer-model` and `--reviewer-harness` record the runtime that
|
|
233
|
+
produced the review, as given. An empty or padded label is a usage error.
|
|
234
|
+
|
|
129
235
|
`--session-id` (on `evidence add`, `review add`, `finding add` and
|
|
130
236
|
`finding close`) takes one opaque token: non-empty, no surrounding
|
|
131
237
|
whitespace, no control characters. Anything else is a usage error
|
|
@@ -169,9 +275,64 @@ the starter policy `canon init` writes.
|
|
|
169
275
|
|
|
170
276
|
## How it meets the gate
|
|
171
277
|
|
|
278
|
+
### Which review counts
|
|
279
|
+
|
|
280
|
+
The reviews of a scenario are folded to the **latest per `reviewer`**
|
|
281
|
+
first (by `at`, then schema, then content digest — the fold every reader
|
|
282
|
+
uses). A reviewer's later review replaces their earlier one: a reviewer
|
|
283
|
+
who signed and later recorded `indeterminate` no longer counts as
|
|
284
|
+
signing, and a later `signed` review counts again. Reviews by different
|
|
285
|
+
reviewers never replace each other.
|
|
286
|
+
|
|
287
|
+
The scenario is reviewed when at least one of those latest reviews:
|
|
288
|
+
|
|
289
|
+
1. is `signed`;
|
|
290
|
+
2. still matches its pins — the scenario record's current
|
|
291
|
+
`source_digest`, and its current latest evidence record per role —
|
|
292
|
+
when it has pins; or, written before 0.15 without pins, still
|
|
293
|
+
describes its scenario at the commit it pinned (below);
|
|
294
|
+
3. passes `distinct_actor`, when that is on.
|
|
295
|
+
|
|
296
|
+
### Reviews written before pinning
|
|
297
|
+
|
|
298
|
+
A review written before 0.15 has no pins, only `pin`: the commit it
|
|
299
|
+
reviewed. The gate reads the scenario's `.feature` file at that commit
|
|
300
|
+
(`git`, with the same per-scenario normalization `canon inventory sync`
|
|
301
|
+
uses for `scenario_digest`; if the scenario was in another file then, the
|
|
302
|
+
spec root's files at that commit are searched for its id) and decides:
|
|
303
|
+
|
|
304
|
+
- **current** when that digest equals the scenario's current one and no
|
|
305
|
+
declared evidence record of the scenario is newer than the review: it
|
|
306
|
+
counts under rules 1 and 3;
|
|
307
|
+
- **stale** when the scenario changed since the commit (`its spec changed
|
|
308
|
+
since <pin>`) or has evidence newer than the review (`it has declared
|
|
309
|
+
evidence newer than the review`): it no longer counts, exactly like a
|
|
310
|
+
stale pinned review;
|
|
311
|
+
- **undecidable** when the pin is not a commit in this clone (shallow
|
|
312
|
+
clone, rewritten history) or the scenario id is absent at it: it keeps
|
|
313
|
+
counting, as in 0.15. A missing commit never becomes a violation.
|
|
314
|
+
|
|
315
|
+
`canon gate check` prints one advisory line with the split (exit code
|
|
316
|
+
unchanged by the line itself). A stale one whose scenario already has
|
|
317
|
+
another qualifying review is counted as superseded and not named:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
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
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`canon status` prints the same line as a warning, and `canon subject
|
|
324
|
+
status` prints it on stderr for the subject's own scenarios; all three
|
|
325
|
+
make the same decision. Re-review exactly the scenarios the line names:
|
|
326
|
+
the new review pins what it saw and replaces the reviewer's legacy one.
|
|
327
|
+
In CI, check out full history (`fetch-depth: 0`) for the job that runs
|
|
328
|
+
the gate, or every legacy review is undecidable.
|
|
329
|
+
|
|
172
330
|
### `unreviewed-promotion`
|
|
173
331
|
|
|
174
|
-
An in-scope scenario with no qualifying review
|
|
332
|
+
An in-scope scenario with no qualifying review. When several latest
|
|
333
|
+
reviews fail for different reasons, the detail names the one closest to
|
|
334
|
+
counting: a current signed review failing only `distinct_actor`, then a
|
|
335
|
+
stale review, then non-signed outcomes.
|
|
175
336
|
|
|
176
337
|
```
|
|
177
338
|
unreviewed-promotion world.demo.01 — no review record for this scenario; spec_coverage.require_review wants an independent review (`canon review add`) once its subject is claimed done
|
|
@@ -190,6 +351,22 @@ evidence:
|
|
|
190
351
|
unreviewed-promotion world.demo.01 — every review of this scenario is by an actor that also attested its evidence (`impl-agent`) or from a session that did (`sess-impl`); spec_coverage.require_review.distinct_actor wants a reviewer other than the evidence actor, in a session other than any evidence session
|
|
191
352
|
```
|
|
192
353
|
|
|
354
|
+
or, when the scenario's spec, its evidence, or both changed after the
|
|
355
|
+
review (`its spec changed`, `its evidence changed`, `its spec and its
|
|
356
|
+
evidence changed`):
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
unreviewed-promotion world.demo.01 — stale review by `reviewer-2`: its evidence changed (the latest evidence records are not the pinned ones) since the review; a pinned review counts only while what it saw is current — review it again (`canon review add`)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
or, when every latest review recorded no sign-off:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
unreviewed-promotion world.demo.01 — no signed review: the latest review recorded no sign-off (`reviewer-2` indeterminate); an indeterminate or unavailable review is a gap, never an approval — run the review again and record `--outcome signed`
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
A non-signed review is a gap even without `distinct_actor`.
|
|
369
|
+
|
|
193
370
|
### `open-blocker`
|
|
194
371
|
|
|
195
372
|
With `block_on_findings`, a Finding whose latest version is severity
|
|
@@ -222,7 +399,7 @@ prints on stderr which review checks it ran or skipped:
|
|
|
222
399
|
|
|
223
400
|
```
|
|
224
401
|
canon subject status: review guard for building → verifying (spec_coverage.require_review, scope: verifying, shipped)
|
|
225
|
-
ran unreviewed-promotion — every owned scenario needs a review by an actor, and from a session, other than its evidence's
|
|
402
|
+
ran unreviewed-promotion — every owned scenario needs a signed review, current with the spec and evidence it pinned, by an actor, and from a session, other than its evidence's
|
|
226
403
|
ran open-blocker — 1 adopted change(s) checked for open blocker findings
|
|
227
404
|
```
|
|
228
405
|
|
|
@@ -146,7 +146,130 @@ query:
|
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Every query, export, and purge appends an actor-stamped record to
|
|
149
|
-
`.canon/audit/access.jsonl` (mode `0600` on creation)
|
|
149
|
+
`.canon/audit/access.jsonl` (mode `0600` on creation) in the repository
|
|
150
|
+
whose `canon.yaml` it read (through `--repo` or `--canon-yaml`), never in
|
|
151
|
+
the current directory; with no `canon.yaml` there is no audit and nothing
|
|
152
|
+
is written. The log is local operator state: `canon init` keeps
|
|
153
|
+
`.canon/audit/` out of git (below). `CANON_ACTOR` takes
|
|
150
154
|
precedence over the OS username. Audit rows contain operation, kind, scope,
|
|
151
155
|
sensitive flag, counts, and timestamp only—never query text, secrets, or
|
|
152
156
|
record bodies.
|
|
157
|
+
|
|
158
|
+
## Staged writes and promotion
|
|
159
|
+
|
|
160
|
+
Four writes stage their record instead of committing it: `canon evidence
|
|
161
|
+
add`, `canon finding add` and `canon finding close` (under
|
|
162
|
+
`.canon/ledger/_staging/`, committed by `canon gate promote`) and `canon
|
|
163
|
+
divergence stage` (committed by `canon divergence promote`). `canon.yaml`'s
|
|
164
|
+
`write:` section decides whether they promote in the same run:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
write:
|
|
168
|
+
promote: auto # or manual
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `auto` — the command stages, then promotes that one record through the
|
|
172
|
+
same validation and assignment `canon gate promote` (or `canon
|
|
173
|
+
divergence promote`) applies. Records other runs staged stay staged;
|
|
174
|
+
only `canon gate promote` / `canon divergence promote` commit
|
|
175
|
+
everything staged. The one success line names the committed record
|
|
176
|
+
(`wrote kind=…/….json`; promotion renames it, so the staged file is not
|
|
177
|
+
named) and ends `— staged and promoted in this run; nothing to
|
|
178
|
+
promote`. `canon init` writes `auto`.
|
|
179
|
+
- `manual` — the record stays staged and the line ends ``— run `canon
|
|
180
|
+
gate promote` to commit it``. An absent `write:` key (or `promote:`
|
|
181
|
+
field) is `manual`, so a repo that never set it is unchanged.
|
|
182
|
+
|
|
183
|
+
`--promote` / `--no-promote` decide for one write and win over the config
|
|
184
|
+
without reading it; both at once is a usage error (exit `2`). If the
|
|
185
|
+
promotion refuses the command's own record, the record stays staged, the
|
|
186
|
+
line ends ``— run `canon gate promote` to commit it`` (`canon divergence
|
|
187
|
+
promote` for a divergence), and the command exits `1`. With neither flag,
|
|
188
|
+
a malformed `write:` section (anything but `promote: auto | manual`)
|
|
189
|
+
refuses the write, exit `2`, before anything is staged. `canon init
|
|
190
|
+
--check-config` reports `[PASS] write (promote: auto)`, `[not configured]
|
|
191
|
+
write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
|
|
192
|
+
works under `auto` and is idempotent: with nothing staged it prints
|
|
193
|
+
`nothing staged` and exits `0`.
|
|
194
|
+
|
|
195
|
+
## The scenario index
|
|
196
|
+
|
|
197
|
+
`canon inventory sync` materializes one `Scenario` record per
|
|
198
|
+
`(project_id, scenario_id)` under `.canon/ledger/kind=scenario/`. The
|
|
199
|
+
index is derived — a pure function of the `.feature` files — so the
|
|
200
|
+
commands that read it rebuild it first: `canon gate check`, `canon
|
|
201
|
+
status`, `canon subject status`, `canon review add` and `canon evidence
|
|
202
|
+
add`. The index is stale when a `.feature` file holds a scenario with no
|
|
203
|
+
live index record, or one whose recorded source digest differs from the
|
|
204
|
+
file's current bytes, or when the index holds a scenario of a spec root
|
|
205
|
+
that no `.feature` file of it declares any more. The reader then runs
|
|
206
|
+
`canon inventory sync`'s code on the changed files (validation still
|
|
207
|
+
covers the whole root) and prints one stderr line:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/features/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A deleted scenario is not erased: `canon inventory sync` — and a reader's
|
|
214
|
+
refresh, identically — appends a tombstone, a new generation of its
|
|
215
|
+
record with `removed: true`. The gate, `canon status`, `canon query` and
|
|
216
|
+
the report panels then treat it as absent; re-adding the scenario writes
|
|
217
|
+
a live generation that supersedes the tombstone. A root that fails to
|
|
218
|
+
sync writes no tombstone either.
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
canon status: refreshed the scenario index from 1 scenario(s) no longer in any feature file (cart.pay.01): 1 record(s) written
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`canon inventory sync` and every refresh hold an OS file lock on
|
|
225
|
+
`.canon/scenario-index.lock` while they write, so concurrent runs never
|
|
226
|
+
index one change twice; a refresh that waited checks staleness again and
|
|
227
|
+
is silent if another run already did the work. The lock is released when
|
|
228
|
+
its holder exits, however it exits, so it never goes stale; a writer
|
|
229
|
+
gives up after 30 s with a message naming the file (`canon inventory
|
|
230
|
+
sync` exits `2`; a reader skips the refresh and still runs).
|
|
231
|
+
|
|
232
|
+
A fresh index prints nothing. A root that fails to sync (format
|
|
233
|
+
violations, a duplicate id) is named and left as it was, and the command
|
|
234
|
+
still runs; `canon gate check` then fails with `uncovered-cell <root>`,
|
|
235
|
+
the `verifying → shipped` guard refuses, and `canon status` lists only
|
|
236
|
+
`canon inventory sync`, while the reporting readers (`query`, `context`)
|
|
237
|
+
keep only this line:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
canon gate check: found the scenario index stale for 1 changed feature file(s) (…): 0 record(s) written; NOT refreshed: root `root` (2 format violation(s), 0 sync error(s)) — `canon inventory sync` lists them
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Nothing is written in a directory without `canon.yaml`. `canon inventory
|
|
244
|
+
sync` stays the explicit command, and the one that lists violations and
|
|
245
|
+
tag diagnostics.
|
|
246
|
+
|
|
247
|
+
## The artifact store lock
|
|
248
|
+
|
|
249
|
+
Every write to the evidence artifact store (`.canon/artifacts/sha256/`:
|
|
250
|
+
`canon evidence add`, `canon review add`, `canon evidence vault`) holds an
|
|
251
|
+
OS file lock on `.canon/artifact-store.lock` from its "already stored?"
|
|
252
|
+
check through its last rename, so a write that fails and rolls back never
|
|
253
|
+
removes a blob another writer published or counted as stored. A rollback
|
|
254
|
+
removes only the blobs its own write renamed into place; a blob already
|
|
255
|
+
stored intact counts as stored and is not written again. The lock is
|
|
256
|
+
released when its holder exits, however it exits, so it never goes stale;
|
|
257
|
+
a writer gives up after 30 s with a message naming the file and saying to
|
|
258
|
+
retry (the command exits `2` and writes no record).
|
|
259
|
+
|
|
260
|
+
## What `canon init` keeps out of git
|
|
261
|
+
|
|
262
|
+
A fresh `canon init` writes these lines to `.gitignore` (each under a
|
|
263
|
+
comment, only the missing ones):
|
|
264
|
+
|
|
265
|
+
- `.canon/hot.db*` — the sqlite hot tier and its WAL/SHM sidecars.
|
|
266
|
+
- `.canon/audit/` — the query/export/purge access log.
|
|
267
|
+
- `.canon/ledger/kind=scenario/` — the scenario index; every reader
|
|
268
|
+
rebuilds it.
|
|
269
|
+
- `.canon/scenario-index.lock` — the lock serializing index rebuilds.
|
|
270
|
+
- `.canon/artifact-store.lock` — the lock serializing artifact store
|
|
271
|
+
writes.
|
|
272
|
+
|
|
273
|
+
A rerun on a repo that already has `canon.yaml` only refreshes
|
|
274
|
+
`AGENTS.md`, so an existing repo's `.gitignore` is unchanged; add the
|
|
275
|
+
lines by hand to adopt them.
|