@journeykit/canon 0.6.0 → 0.8.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.
@@ -0,0 +1,199 @@
1
+ # canon-gate
2
+
3
+ `canon gate` is canon's trust spine: a two-layer evidence gate over a
4
+ repo's artifact corpus. It answers two DIFFERENT questions per artifact —
5
+ "does required evidence exist" (coverage) and "did the evidence pass, by
6
+ whom, how stale" (verdict-ledger) — never collapsing them into one
7
+ "done" boolean.
8
+
9
+ ## The trust ladder
10
+
11
+ Each artifact climbs a trust ladder as evidence accrues: an unreviewed
12
+ record is weaker than a reviewed one, which is weaker than a ratified
13
+ one. `policy.yaml` sets a `trust_required` level per artifact class; the
14
+ release-scoped check (below) enforces it. A human-only `flagged` overlay
15
+ overrides the ladder — a flagged artifact is never green regardless of
16
+ evidence. (Author `policy.yaml`'s trust/staleness fields via `canon-policy`.)
17
+
18
+ ## The eight failure classes
19
+
20
+ Every violation carries one of these stable, grep-able strings:
21
+
22
+ | Class | Meaning |
23
+ |---|---|
24
+ | `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. |
25
+ | `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record. |
26
+ | `trust-below-required` | Achieved trust level is below `policy.yaml`'s `trust_required` for its class — RELEASE-scoped only (`canon gate check --release`); never fires on an ordinary run. |
27
+ | `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. |
28
+ | `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
29
+ | `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
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. |
32
+
33
+ ## `canon gate check [--repo <dir>] [--release]`
34
+
35
+ Assembles coverage + ledger + staleness + the always-on trust-ladder
36
+ check, plus the opt-in spec-coverage check, over the resolved repo's
37
+ corpus and runs them, printing every violation grouped by failure class.
38
+ `--release` additionally engages the release-scoped
39
+ `trust-below-required` check; the trust-ladder check is never dropped
40
+ when `--release` is passed.
41
+
42
+ ```bash
43
+ canon gate check --repo . # ordinary evaluation
44
+ canon gate check --repo . --release # + trust-below-required
45
+ ```
46
+
47
+ Exit `0` clean, `1` gate-red (any violation), `2` usage/load failure
48
+ (unreadable ledger, corrupt `canon.yaml`). `--repo` (or its omission)
49
+ resolves through the nearest-ancestor `canon.yaml` walk, so it reads the
50
+ repo ROOT's `.canon/policy.yaml` and `.canon/ledger` from any subdirectory.
51
+
52
+ ### The spec-derived worklist (`spec_coverage`, opt-in)
53
+
54
+ Coverage groups the EVIDENCE corpus, so it can only ever enumerate
55
+ artifacts that already have evidence. A `.feature` scenario nobody has
56
+ attested to appears in no group and is invisible to it. `spec_coverage`
57
+ adds the other direction: it starts from the Scenario corpus
58
+ `canon inventory sync` materializes and left-joins evidence onto it, so
59
+ an unimplemented or mismatched spec becomes a reported violation.
60
+
61
+ ```yaml
62
+ # .canon/policy.yaml — ABSENT by default; absent means zero violations
63
+ spec_coverage:
64
+ require_evidence: true
65
+ scope: [building, verifying] # optional; omit for the whole corpus
66
+ ```
67
+
68
+ - **Unimplemented** — no `EvidenceRecord` carries the scenario's
69
+ `(project_id, scenario_id)`. Author one with
70
+ `canon evidence add --scenario-id <id> --project-id <root-id>`; both
71
+ flags are required together, because two spec roots may carry the same
72
+ scenario id.
73
+ - **Mismatched** — the folded divergence state is `open`,
74
+ `still-divergent`, or `resolved-invalid` (a resolution whose app sha
75
+ has moved), or the latest ledger verdict is `divergent`.
76
+ - **`scope`** narrows blocking to scenarios whose `@subject:<id>` tag
77
+ links them to a Subject in one of the named statuses. With a `scope`
78
+ set, an untagged scenario is out of scope; with `scope` omitted, every
79
+ scenario is in scope. A tag naming no Subject record is reported as a
80
+ dangling link, never silently skipped.
81
+
82
+ Two refusals rather than a silent pass, both surfacing as
83
+ `uncovered-cell` on the subject `spec_coverage`:
84
+
85
+ - A **malformed section** (unknown key, unknown `scope` status) refuses
86
+ at check time. It is never treated as absent — a typo must not be
87
+ silently equivalent to not opting in.
88
+ - A **corpus kind routed off the gate's rung** (`scenario`,
89
+ `divergence`, or `subject` sent anywhere but `local`) refuses, because
90
+ the gate reads one tier and would otherwise pass by seeing nothing.
91
+
92
+ Malformed rows in those three kinds do NOT surface as
93
+ `malformed-evidence`; they are kept off the evidence violation set on
94
+ purpose, so enabling nothing changes no existing verdict.
95
+
96
+ ## Effect-aware risk approvals (`risk_tiers`)
97
+
98
+ `risk_tiers` is absent by default and remains a no-op when absent. When it
99
+ is present, `canon gate check` selects the highest-ranked configured tier
100
+ whose current evidence binding matches a path or exact `effect:<kebab-slug>`.
101
+ The current generation is selected deterministically; approvals from an
102
+ older `evidence_sha`, a non-`faithful` verdict, or an unverified attestation
103
+ never count. A required tier also fails closed when the current record has
104
+ no usable `surface_ref`/effect binding, so a caller cannot suppress a
105
+ configured rule by declaring a safe path.
106
+
107
+ `canon evidence add --surface-ref src/auth/login.rs` (repeatable) or
108
+ `--surface-ref effect:secret-access` persists an explicitly validated
109
+ binding. This is still attestation input, not proof of the changed paths;
110
+ run/diff metadata is authoritative when available, and an unavailable
111
+ binding is a gate violation rather than a clean result.
112
+
113
+ Approval identity is separate from attestation text. `--approval-by` is
114
+ accepted as a verified approval only when it equals a non-empty
115
+ `CANON_ACTOR` environment identity and `--approval-role human` is supplied.
116
+ The record persists that verification bit for replay; arbitrary caller
117
+ strings, empty identities, and `agent` roles never satisfy a risk tier.
118
+ Malformed `risk_tiers` sections produce a stable `uncovered-cell` policy
119
+ violation (`risk_tiers policy-invalid: ...`) instead of degrading to an
120
+ empty clean map.
121
+
122
+ ## `canon gate task <task_id> [--repo <dir>]`
123
+
124
+ The evidence-gated task checkbox flip. Resolves `<task_id>`
125
+ (`<change_id>#<n>`) through `canon.yaml`'s `plans:` sources — the first
126
+ configured dialect whose `PlanWriteBack` locates the task's document
127
+ wins, so the flip is plan-dialect agnostic (`openspec` change dirs,
128
+ `superpowers` plan docs). A repo with no `plans:` section falls back to
129
+ the compat default `[{ dialect: openspec, root: <repo> }]`; no source
130
+ locating the task at all is a loud failure naming every source consulted.
131
+ It requires
132
+ a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
133
+ with an appended evidence note ONLY on a clean check. Every other path —
134
+ missing evidence, a `Divergent` verdict, a fabricated note — leaves the
135
+ row byte-unchanged and exits `1` with the blocking violation on stderr.
136
+ An already-`[x]` row is an idempotent no-op (exit `0`). An unknown
137
+ `task_id` is reported (exit `1`).
138
+
139
+ ```bash
140
+ canon gate task my-change#5.2 --repo .
141
+ ```
142
+
143
+ This is canon's own authority for the checkbox grammar — always flip
144
+ through `canon gate task`, never by hand.
145
+
146
+ ## `canon gate promote [--repo <dir>] [--dry-run]`
147
+
148
+ Staging → committed: every well-formed record under
149
+ `.canon/ledger/_staging/` is re-validated with the SAME checks the gate
150
+ applies, assigned a monotonic per-(role, surface) `run_seq` (gap-free
151
+ within one invocation, continuing from the committed max), and moved into
152
+ the committed ledger. A malformed or unpartitionable candidate is refused
153
+ — exit `1`, no `run_seq` consumed, the file left in place. `--dry-run`
154
+ prints the plan (target path + assigned `run_seq` per candidate) without
155
+ writing or deleting.
156
+
157
+ ```bash
158
+ canon gate promote --repo . # land every staged candidate
159
+ canon gate promote --repo . --dry-run # preview only
160
+ ```
161
+
162
+ ## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
163
+
164
+ Idempotent, diff-only hook-seam installation: merges one
165
+ `{matcher?, hooks: [{type: "command", command, timeout}]}` entry into
166
+ BOTH `<repo>/.claude/settings.json` and `<repo>/.codex/hooks.json` —
167
+ additive only, never touching an existing entry in the same matcher
168
+ group. Running it twice with no manual edits between reports "no diff"
169
+ and writes nothing. When neither file already carries a `canon
170
+ gate`-invoking command, it ALSO emits
171
+ `<repo>/.canon/scripts/canon-gate-pre-commit.sh` (advisory by default — set
172
+ `CANON_GATE_ADVISORY=0` to make it block the commit on a failing gate).
173
+
174
+ ```bash
175
+ canon gate install-hooks --repo .
176
+ # non-default wiring:
177
+ canon gate install-hooks --repo . --event PreToolUse --matcher Edit --command "canon gate check" --timeout 30
178
+ ```
179
+
180
+ Prefer this over hand-editing `settings.json`/`hooks.json`.
181
+
182
+ ## `canon gate selftest`
183
+
184
+ Runs the shipped fixture corpus — one fixture per failure class, each a
185
+ deliberately broken corpus proving that class fires and ONLY that class
186
+ (both under-detection and over-triggering fail the run). Takes no
187
+ `--repo`; self-contained. Run it before trusting any other `canon gate
188
+ check` run's green.
189
+
190
+ ```bash
191
+ canon gate selftest
192
+ ```
193
+
194
+ ## Reading a violation
195
+
196
+ Every printed line is `<failure-class> <subject> — <detail>`. `<subject>`
197
+ is the artifact's own join identity (`task_id` preferred, then
198
+ `scenario_id`, then `run_id`) — grep the ledger or `tasks.md` for it
199
+ directly, never guess.
@@ -0,0 +1,102 @@
1
+ # canon-inventory
2
+
3
+ `canon inventory sync` is the FIRST stage of canon's unified spec loop:
4
+ it turns a hand-authored, `canon format`-validated `.feature` corpus into
5
+ the scenario ledger-INDEX records every downstream stage joins against.
6
+
7
+ ## The unified loop
8
+
9
+ ```
10
+ author .feature corpus → canon format --check (validate)
11
+ │
12
+ ▼
13
+ canon inventory sync # THIS skill — materializes scenario index records
14
+ │
15
+ ▼
16
+ canon review add # attests a review — see canon-inventory downstream / canon-gate
17
+ canon divergence {stage,promote,resolve,defer,status} # tracks/resolves a divergence
18
+ canon gate check [--release] # trust-ladder / staleness / coverage — see canon-gate
19
+ │
20
+ ▼
21
+ canon ingest artifacts # with artifacts.native_records: true in canon.yaml —
22
+ # feeds review/divergence verdicts into the flywheel
23
+ # (see canon-artifact-ingest)
24
+ ```
25
+
26
+ Every stage after `sync` joins on the SAME `(project_id, scenario_id)`
27
+ pair `sync` materializes — one identity scheme, no second one.
28
+
29
+ ## `canon inventory sync [--repo <dir>] [--spec-root <dir>]`
30
+
31
+ ```bash
32
+ canon inventory sync # every canon.yaml specs.roots[] entry
33
+ canon inventory sync --repo ../svc # a specific repo
34
+ canon inventory sync --spec-root ./specs # ad hoc: ignore canon.yaml entirely
35
+ ```
36
+
37
+ Per configured root, in order:
38
+
39
+ 1. **Validate** — runs `canon format --check` over the root. ANY violation
40
+ (missing provenance, layout-grammar, …) ABORTS THE WHOLE ROOT: zero
41
+ records written for it, the violation(s) reported. Never a partial sync.
42
+ 2. **Scan** — walks `<root>/features/**/*.feature`, pairing each
43
+ `@<area>.<surface>.<nn>` tag with its header label as `title` and a
44
+ `source_digest` (sha256 over the raw `.feature` bytes). `@subject:<id>`
45
+ → `subject_id`, `@lane:<v>` → `lane` (first tag wins; a second, a
46
+ malformed value, or a value outside a declared `lane` enum is dropped
47
+ with a counted, non-fatal diagnostic). Any other `@name:value`
48
+ namespace is counted as a diagnostic so a dropped classification is
49
+ visible; plain tags (`@p2`) are ignored. The index derives from the
50
+ `.feature` corpus alone. See `canon-authoring` for the organizing rule.
51
+ 3. **Materialize** — upserts ONE scenario index record per
52
+ `(project_id, scenario_id)` via the append-only git-tier write.
53
+ Logically idempotent: an unchanged `source_digest` is a no-op; a
54
+ changed `.feature` file appends exactly one new record. A `.feature`
55
+ edit re-materializes every scenario in that file (file granularity).
56
+ 4. **Duplicate guard** — a `scenario_id` scanned more than once WITHIN
57
+ one root's corpus aborts that root (0 writes), reported as a sync
58
+ error. Two DIFFERENT roots sharing a `scenario_id` stay distinct —
59
+ `project_id` isolates them.
60
+
61
+ The scenario index shape is deliberately general: `title` +
62
+ `source_digest`, nothing else. It carries no coverage field — coverage
63
+ stays `canon gate check`'s own authority (see `canon-gate`).
64
+
65
+ ## `specs.roots[]` config (`canon.yaml`)
66
+
67
+ ```yaml
68
+ specs:
69
+ roots:
70
+ - id: app-a # STABLE LITERAL — never the checkout directory name
71
+ root: apps/a/specs
72
+ - id: app-b
73
+ root: apps/b/specs
74
+ ```
75
+
76
+ - **Absent `specs:` key** → the single default root `{id: root, root: specs}`.
77
+ - **Present `specs:` with an empty/missing `roots[]`** → fails LOUD; a
78
+ present-but-incomplete config never silently resolves to zero roots.
79
+ - **A malformed entry** (missing `id`, `roots` not a list, `id` not a
80
+ valid project id) → fails LOUD, never a silent fallback to the default.
81
+ - `--spec-root <dir>` bypasses `specs.roots[]` entirely and syncs
82
+ exactly that one directory under the same stable `root` id the
83
+ absent-`specs:` default uses.
84
+
85
+ ## Sync-abort causes
86
+
87
+ - **A `canon format --check` validation violation** in the root's corpus →
88
+ whole root aborts, 0 writes, violations reported.
89
+ - **A duplicate `scenario_id`** within one root → that root aborts, 0
90
+ writes, reported as a sync error (a distinct lane from fmt violations).
91
+
92
+ ## How the index feeds the rest
93
+
94
+ - **Review** — `canon review add` attests a verdict against a synced
95
+ scenario. (See `canon-inventory` downstream commands and each command's `--help`.)
96
+ - **Divergence** — `canon divergence {stage,promote,resolve,defer,status}`
97
+ tracks/resolves divergences against synced scenarios.
98
+ - **Gate** — `canon gate check` enforces trust-ladder / staleness /
99
+ coverage over the corpus (see `canon-gate`).
100
+ - **Ingest** — `canon ingest artifacts` with `artifacts.native_records:
101
+ true` in `canon.yaml` feeds review/divergence verdicts into the
102
+ reward flywheel (see `canon-artifact-ingest`).
@@ -0,0 +1,245 @@
1
+ # canon-learn
2
+
3
+ canon's role-namespaced strategy memory turns accumulated verdict
4
+ trajectories into retrievable, promotable insights. Two tiers:
5
+
6
+ - **Trajectories (warm, local).** Raw verdict-keyed rows under
7
+ `<repo>/.canon/learn/trajectories/`, written by ingest. Rebuildable,
8
+ gitignored, never hand-committed.
9
+ - **Strategies (durable, git).** Distilled, reviewable
10
+ `<repo>/.canon/strategies/<role>/<id>.md` files — the tier a promotion
11
+ writes into and a demotion soft-flags.
12
+
13
+ ## Rebuilding strategy memory
14
+
15
+ Strategy memory is re-derived from the current trajectory set as part of
16
+ `canon ingest artifacts` (see `canon-artifact-ingest`): after it
17
+ persists a repo's verdict trajectories, it re-derives the distilled
18
+ strategies. This is NON-DESTRUCTIVE — a rebuild never drops a
19
+ hand-promoted strategy the trajectory rows don't touch. There is no
20
+ standalone `canon learn` rebuild command; the only subcommand is
21
+ `promote`.
22
+
23
+ # `canon learn promote <strategy_id> [--repo <dir>] [--evaluation <path> --approval <path>] [--signature <path>] [--dry-run]`
24
+
25
+ The production activation path is a signed, paired-evaluation workflow:
26
+
27
+ 1. Run `canon ingest artifacts`. Ingest resolves covering verdicts and rebuilds
28
+ newly distilled strategies as `lifecycle: quarantined`.
29
+ 2. Check eligibility with the configured `occurrence` or `crn` gate. This is
30
+ only a prerequisite; occurrence alone MUST NOT activate a strategy.
31
+ 3. An honest external evaluator writes a `PromotionEvaluation` JSON bundle.
32
+ It MUST bind the candidate's id, version, quarantined lifecycle, strategy
33
+ digest, source trajectory ids, baseline and candidate result digests, and
34
+ non-empty corpus/evaluator/context/policy/model/tool provenance.
35
+ 4. Require paired evidence: baseline and candidate digest arrays MUST be
36
+ non-empty, equal-length, disjoint, and duplicate-free. The evaluation's
37
+ finite `paired_metrics` MUST include `pairs`, `uplift`, and `regressions`;
38
+ `pairs` MUST equal the array length, `uplift` MUST be strictly positive,
39
+ `regressions` MUST be zero, `passed` MUST match those derived checks, and
40
+ `decision` MUST be `pass`, `promote`, or `approved`. Missing or unknown
41
+ metrics are rejected; the repository currently publishes **no measured
42
+ uplift result**, so this command is not evidence that retrieval improves
43
+ agent outcomes.
44
+ 5. Produce the unsigned approval payload:
45
+
46
+ ```bash
47
+ canon learn approve <strategy_id> --evaluation evaluation.json \
48
+ --principal alice --repo .
49
+ # `request` is an exact alias:
50
+ canon learn request <strategy_id> --evaluation evaluation.json \
51
+ --principal alice --repo .
52
+ ```
53
+
54
+ The JSON output contains the evaluation digest, RFC3339 `approved_at`,
55
+ subject `strategy:<strategy_id>`, namespace
56
+ `canon-learning-approval-v1`, and `payload_hex`. `approve`/`request` never
57
+ read a private key and never set `verified`.
58
+ 6. Decode `payload_hex` to the exact canonical JSON bytes. The signed object
59
+ is:
60
+
61
+ ```json
62
+ {
63
+ "namespace": "canon-learning-approval-v1",
64
+ "subject": "strategy:<strategy_id>",
65
+ "project": null,
66
+ "artifact_sha": "<evaluation_digest>",
67
+ "run_id": null,
68
+ "surface": [],
69
+ "effects": [],
70
+ "actor": "<principal>",
71
+ "timestamp": "<RFC3339 nanosecond timestamp>"
72
+ }
73
+ ```
74
+
75
+ For rollback, the corresponding signed object uses subject
76
+ `strategy:<id>:rollback:<sha256(reason)>`, `artifact_sha` equal to that
77
+ reason digest, and the rollback actor/timestamp. Sign the decoded bytes
78
+ externally with an SSH key using the `canon-learning-approval-v1`
79
+ namespace (for example, `ssh-keygen -Y sign -n canon-learning-approval-v1
80
+ -f <private-key>`). Put the armored detached signature in
81
+ `PromotionApproval.signature` and set `signer_key` to the same principal.
82
+ The approval JSON has exactly these fields: `schema_version`,
83
+ `candidate_strategy_id`, `evaluation_digest`, `approver_identity`,
84
+ `approver_role` (`human`), `verified` (non-authoritative display data),
85
+ `approved_at`, optional `signature`, optional `signer_key`, and
86
+ `integrity_digest`.
87
+ 7. Configure the verifier in the repository policy:
88
+
89
+ ```yaml
90
+ approval:
91
+ allowed_signers: .canon/allowed_signers
92
+ ```
93
+
94
+ The path is policy-pinned (relative paths resolve from the repository).
95
+ The verifier runs `ssh-keygen -Y verify` against that file; an absent,
96
+ unreadable, or non-matching signer fails closed. A detached signature is
97
+ authentication of the configured principal, not proof that the principal
98
+ is a human or is otherwise authorized outside this policy.
99
+ 8. Activate only with both evidence files:
100
+
101
+ ```bash
102
+ canon learn promote <strategy_id> --repo . \
103
+ --evaluation evaluation.json --approval approval.json
104
+ # Or attach an armored signature file and recompute the approval digest:
105
+ canon learn promote <strategy_id> --repo . \
106
+ --evaluation evaluation.json --approval approval.json \
107
+ --signature approval.sig
108
+ ```
109
+
110
+ `--evaluation` and `--approval` MUST be supplied together. A quarantined
111
+ candidate without valid paired evaluation, policy-pinned SSH verification,
112
+ and human approval remains quarantined. `--dry-run` previews the render but
113
+ does not activate or write.
114
+
115
+ The command resolves the repo via the nearest-`canon.yaml`-ancestor walk,
116
+ evaluates the role gate before any write, and runs advisory lints
117
+ (content-length ceiling and literal-absolute-path rejection). There is no
118
+ `--force`. Promotion re-renders the whole git-tier file with YAML front matter
119
+ (`status`, `regime_key`, `role`, `title`, `source_trajectory_ids`,
120
+ `recorded_at`) plus evaluation and approval digests; a re-promote does not
121
+ preserve manual edits.
122
+
123
+ An unknown `strategy_id` fails nonzero. A candidate's lifecycle defaults to
124
+ `quarantined` when newly distilled and quarantine is excluded from retrieval.
125
+ The old public writer is retained only for already-`active` items; it is not a
126
+ way to activate new or legacy candidates.
127
+
128
+ ### The eligibility gates: `crn` vs `occurrence`
129
+
130
+ A role's eligibility gate is a `canon.yaml` `learn:`-section choice:
131
+
132
+ ```yaml
133
+ # canon.yaml
134
+ learn:
135
+ promotion:
136
+ dev:
137
+ mode: occurrence
138
+ n_min: 8 # optional — defaults to 5
139
+ window_days: 14 # optional — defaults to 30
140
+ sim:
141
+ mode: crn
142
+ demotion:
143
+ hard_delete: false # optional — soft-flag is the default
144
+ strategies_root: .canon/strategies # optional
145
+ ```
146
+
147
+ A role with no explicit `promotion.<role>` entry defaults to
148
+ `mode: occurrence, n_min: 5, window_days: 30`.
149
+
150
+ - **`occurrence`** — for roles whose domain does NOT support deterministic
151
+ replay. Within the trailing window, at least `n_min` resolved `Success`
152
+ trajectories for the same `regime_key` are required and no `Failure` or
153
+ `RolledBack` trajectory may be present. A contradiction resets the count;
154
+ it is never averaged away. This eligibility does not replace paired
155
+ evaluation or signed approval.
156
+ - **`crn`** — for roles that CAN run a deterministic simulator. A paired
157
+ common-random-number statistical eligibility gate uses the same
158
+ `crn:config=<label>` and `crn:panel=<index>` panels across compared
159
+ configurations. Insufficient panels/noise evidence is not a pass. CRN
160
+ eligibility still does not replace the external `PromotionEvaluation` and
161
+ signature.
162
+
163
+ Both gates read resolved `verdict_record.outcome`, not raw verdict rows.
164
+ `Pending` trajectories neither corroborate nor contradict. `canon ingest
165
+ artifacts` is therefore always first:
166
+
167
+ ```bash
168
+ canon ingest artifacts
169
+ canon learn promote <id> --evaluation evaluation.json --approval approval.json
170
+ ```
171
+
172
+ ### Demotion versus authenticated rollback
173
+
174
+ A later ordinary contradictory `Failure`/`RolledBack` trajectory demotes an
175
+ already-active strategy when a non-dry-run promotion observes it. The default
176
+ is append-only soft-flagging of the existing git file with `status: demoted`
177
+ and `reason:`; `demotion.hard_delete: true` deletes that file instead. This
178
+ ordinary contradiction path is not an approval and does not create a rollback
179
+ record.
180
+
181
+ An operator-requested rollback is a distinct authenticated action:
182
+
183
+ ```bash
184
+ canon learn rollback <strategy_id> --reason "<reason>" --actor alice \
185
+ --signature-file rollback.sig --approved-at 2026-10-02T12:00:00Z \
186
+ [--contradicting-trajectory-id <trajectory_id>] --repo .
187
+ ```
188
+
189
+ `--signature-file` is an armored SSH detached signature over the
190
+ `canon-learning-approval-v1` payload bound to
191
+ `strategy:<id>:rollback:<sha256(reason)>`, actor, and `--approved-at`.
192
+ `actor` MUST be listed in policy-pinned `approval.allowed_signers`; verification
193
+ uses that file and `ssh-keygen -Y verify`. Successful rollback writes durable
194
+ provenance, sets lifecycle `rolled_back`, and records the optional contradicting
195
+ trajectory. Missing policy, signer, timestamp, or signature fails closed.
196
+
197
+ ### What retrieval can and cannot serve
198
+
199
+ Retrieval serves only exact role/regime matches and excludes demoted,
200
+ quarantined, rejected, and rolled-back strategies. Legacy rows with
201
+ `lifecycle: null` remain readable for backward compatibility, but they have no
202
+ quarantine/evaluation/signature provenance and MUST NOT be treated as proof of
203
+ the current activation workflow. Retrieval is fail-soft and advisory; it does
204
+ not upgrade legacy rows or establish measured uplift.
205
+
206
+ ## How verdicts feed scoring
207
+
208
+ Verdicts arrive from `canon ingest artifacts` (see
209
+ `canon-artifact-ingest`), persisted regime-keyed
210
+ (`<role>/<repo>/<area>/<hash>`). Each verdict scores its covering
211
+ trajectory into `Success` / `Failure` / `RolledBack` / `Pending`; the
212
+ eligibility gate reads a regime's accumulated resolved verdicts, while the
213
+ paired evaluation and signed approval decide whether a quarantined strategy
214
+ may become active. A contradicting trajectory for an already-promoted
215
+ strategy triggers ordinary demotion; an explicit rollback is separate and
216
+ authenticated.
217
+
218
+ ## Reading a demoted or rolled-back strategy
219
+
220
+ An ordinary contradicting trajectory demotes an active strategy append-only:
221
+ the default soft-flag merges `status: demoted` + `reason: <text>` into the
222
+ existing front matter and leaves the body byte-unchanged. Set
223
+ `demotion.hard_delete: true` to delete the file instead. A strategy demoted
224
+ before it reached the git tier has no file to flag — that is not an error.
225
+ `canon retrieve` skips demoted and rolled-back strategies. An authenticated
226
+ rollback is different: it records signed operator provenance and sets the
227
+ stored lifecycle to `rolled_back`.
228
+
229
+ ## The flywheel
230
+
231
+ ```
232
+ ingest → resolved trajectories → quarantined strategies
233
+ ↑ │
234
+ verdicts eligibility + paired evaluation + SSH approval
235
+ │ (activate)
236
+ ▼
237
+ active git strategies → advisory retrieve
238
+ ```
239
+
240
+ ## What this skill does NOT cover
241
+
242
+ - Retrieval at dispatch time (`canon retrieve`, the pre-dispatch hook) —
243
+ see the `canon-retrieve` skill.
244
+ - Producing the verdict trajectories promotion reads — see the
245
+ `canon-artifact-ingest` skill.