@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,214 @@
1
+ # canon-session-ingest
2
+
3
+ `canon ingest sessions` turns every agent CLI's on-disk transcript store
4
+ into `Session`/`Run`/`Event` records on canon's join spine, scanning four
5
+ adapters, normalizing, and persisting through the same tier resolution
6
+ `canon query` / `canon tier age` use. The default corpus is scoped to the
7
+ current PROJECT; the watermark gate is per-file; user directives are
8
+ metadata-only unless explicitly opted in by bounded privacy config.
9
+
10
+ **Quick start needs zero docker/services.** `canon init` scaffolds `hot`
11
+ as a local sqlite file (`.canon/hot.db`) with `session`/`run`/`event`
12
+ already routed to it — a fresh repo's FIRST `canon ingest sessions`
13
+ persists real records with no `docker compose` stack, no `CANON_PG_DSN`,
14
+ no live database. Swap `hot` to postgres (the commented stanza `canon
15
+ init` ships beside the sqlite one) once team-scale multi-agent
16
+ concurrency outgrows sqlite's single-writer mode.
17
+
18
+ ## `canon ingest sessions [--watch] [--interval-secs N] [--home <dir>] [--canon-yaml <path>] [--full] [--all-workspaces]`
19
+
20
+ ```bash
21
+ canon ingest sessions # one pass over this project's sources
22
+ canon ingest sessions --watch # poll every 30s (see --interval-secs)
23
+ canon ingest sessions --full # ignore the watermark; re-parse every in-scope file
24
+ canon ingest sessions --all-workspaces # machine-wide scan
25
+ canon ingest sessions --home /custom # override the scan-root home ($HOME)
26
+ ```
27
+
28
+ ## The four adapters
29
+
30
+ Registered in order: **`omp`** (omp/pi sessions), **`hermes`**,
31
+ **`claude`** (`~/.claude/projects/**/*.jsonl`), **`codex`**
32
+ (`${CODEX_HOME:-~/.codex}/sessions/` unioned with `archived_sessions/`).
33
+ Each resolves its default root and honors an env override
34
+ (`CANON_INGEST_OMP_SESSIONS_DIR`, `CANON_INGEST_CLAUDE_SESSIONS_DIR`,
35
+ `CODEX_HOME`, `HERMES_HOME`). An absent source root is a zero-record,
36
+ non-fatal skip.
37
+
38
+ ### Per-source root overrides (`canon.yaml`)
39
+
40
+ ```yaml
41
+ ingest:
42
+ sources:
43
+ omp:
44
+ roots: [/data/omp-sessions] # relative paths resolve against the canon.yaml dir
45
+ codex:
46
+ roots: [] # explicit empty = scan ZERO (not the default)
47
+ ```
48
+
49
+ A source with a **present** `roots` scans exactly those (explicit
50
+ `roots: []` scans nothing); a source whose key or `roots` is **absent**
51
+ keeps its env-override + default resolution. Unlike artifacts (where
52
+ unconfigured = no scan), sessions default to their home roots when
53
+ unconfigured. A PRESENT but broken `ingest:` section fails **loud** (a
54
+ typo like `root:` for `roots:`, an unknown source id, or a non-YAML
55
+ canon.yaml) rather than silently scanning the wrong corpus; a
56
+ missing/unreadable canon.yaml stays a soft no-config.
57
+
58
+ ## Project scope — the default corpus
59
+
60
+ `canon ingest sessions` defaults to THIS PROJECT's sessions, never the
61
+ whole machine: the repo's main `git worktree` root plus every linked
62
+ worktree (`git worktree list --porcelain`). Outside a git repo, the scope
63
+ fails soft to the repo root alone.
64
+
65
+ - **omp/pi and Claude Code are cwd-partitioned on disk** — one
66
+ subdirectory per project. Out-of-scope subdirectories are pruned at
67
+ enumerate time — never read or counted.
68
+ - **Codex and Hermes are not partitioned** — rows are filtered
69
+ post-parse by workspace membership; a row with no captured workspace is
70
+ kept, fail-soft.
71
+ - **`--all-workspaces`** restores the machine-wide scan. Scope is still
72
+ resolved (so `project_key` stamping stays correct); only the
73
+ pruning/filtering are disabled.
74
+ - Every ingested session whose workspace resolves into the active project
75
+ gets `project_key` stamped to the MAIN worktree's key, so `canon query
76
+ --kind session` aggregates a whole project's sessions regardless of
77
+ which worktree recorded them.
78
+ - The run summary's first line always names the active scope:
79
+ `scope: project /repo/root (2 roots)` or `scope: all workspaces`.
80
+
81
+ ### Provenance, digests, and parser generations
82
+
83
+ The parser semantics are compared against Tokscale upstream at
84
+ https://github.com/junhoyeo/tokscale (revision
85
+ `fe72e1f9b5a2b1a927108b353a9730a5c631a196`); the cache-accounting comparison
86
+ reference is commit
87
+ [`2cd6c648df026fb43d37e492efa9c344e2d681ee`](https://github.com/junhoyeo/tokscale/commit/2cd6c648df026fb43d37e492efa9c344e2d681ee).
88
+ Canon ports the selected parser semantics, including Claude's nested
89
+ cache-write buckets, but does **not** vendor Tokscale's pricing or cache
90
+ service.
91
+
92
+ Three identities are intentionally distinct:
93
+
94
+ * the raw file SHA-256 watermark identifies unchanged transcript bytes;
95
+ * the normalized content digest is canonical JSON over the complete
96
+ normalized event and changes when normalized token detail changes; and
97
+ * the parse-generation cursor identity combines adapter id and parse version,
98
+ forcing a re-read when parser semantics change even if raw bytes do not.
99
+
100
+ ## The watermark — per-file, incremental `--watch`
101
+
102
+ Each pass content-digests every present (post-pruning) file and diffs it
103
+ against its source's persisted cursor (under
104
+ `<repo>/.canon/ingest/cursors/`, gitignored), written after a durable
105
+ pass. A file whose digest matches its cursor is SKIPPED — never parsed; a
106
+ new or changed file is (re-)parsed. One session derives from exactly one
107
+ file, so a single growing transcript re-parses alone instead of dragging
108
+ the whole source back through parse/persist.
109
+
110
+ - The gate is sound: a file is skipped only when its digest matches the
111
+ cursor, so a new, changed, copied, or restored file is always
112
+ re-scanned.
113
+ - Correctness never depends on the cursor: a missing/corrupt cursor
114
+ treats every present file as new, and the digest-idempotent write path
115
+ keeps any rescan from double-writing.
116
+ - A cursor's identity carries its adapter's PARSE VERSION as well as its
117
+ files, so changing an adapter's normalization re-reads that adapter's
118
+ transcripts instead of reporting them unchanged — no `--full`, no
119
+ cursor deletion. Version `1` IS the bare `client_id` (`omp.json`), so a
120
+ cursor already on disk is never invalidated just by installing the
121
+ mechanism; a bump appends the suffix (`claude-code-v3.json`) and that
122
+ `omp` is at `2` because the logical adapter now deduplicates forks
123
+ across files; `hermes` remains at `1`; `codex` is at `2` because its
124
+ reasoning-token accounting is split; and `claude-code` is at `3` because
125
+ cache accounting now recognizes `cache_write_1h` buckets. Each changed
126
+ adapter re-reads once to backfill the normalization change that the old
127
+ watermark would otherwise hide.
128
+
129
+ **`--full`** ignores the cursors and re-parses every present in-scope
130
+ file (a full rescan / cursor reset) — safe because a byte-identical
131
+ resubmission is a no-op; cursors re-advance afterward.
132
+
133
+ ## User directives
134
+
135
+ Every adapter (except Hermes, whose format carries no user-turn text)
136
+ emits one parsed `DirectiveRow` per USER-role message. Parsing retains
137
+ source text only in memory; the shared post-adapter privacy boundary
138
+ controls whether it becomes a durable event. The default when
139
+ `ingest.sessions.privacy` is absent is metadata-only: no
140
+ `user_directive` event reaches normalized output.
141
+
142
+ Repositories that explicitly opt in MUST set a positive bound:
143
+
144
+ ```yaml
145
+ ingest:
146
+ sessions:
147
+ privacy:
148
+ capture_user_directives: true
149
+ max_directive_chars: 4096
150
+ ```
151
+
152
+ Captured text is classified and redacted BEFORE the Unicode-scalar capture
153
+ bound is applied. The conservative classifier replaces known API
154
+ key/token/password assignments, bearer credentials, PEM private keys, and
155
+ email addresses with deterministic `[REDACTED_SECRET_N]` /
156
+ `[REDACTED_PII_N]` placeholders and includes `redaction_counts`. The
157
+ detail's `redacted` is true only when a known value was replaced;
158
+ `truncated` is independent and may be true for otherwise unredacted text.
159
+ This classifier is not a guarantee against every kind of secret or PII.
160
+ `workspace_key`/`workspace_label` remain metadata. The source transcript
161
+ remains the source of truth and is not rewritten, purged, or replaced by
162
+ Canon's normalized event.
163
+
164
+ The source cursor identity includes the privacy mode and positive bound, so
165
+ toggling capture or changing its bound re-scans unchanged source bytes.
166
+ Re-scans retain stable event sequence/key ordinals; they do not purge
167
+ previously stored normalized records. Operators owning pre-existing raw
168
+ data MUST handle purge/migration separately.
169
+
170
+ These events interleave with `token_usage` events in one deterministic
171
+ order (directive-before-token on a timestamp tie). Query them with:
172
+
173
+ ```bash
174
+ canon query --kind event # filter detail.label == "user_directive"
175
+ ```
176
+
177
+ ## Reading the run summary
178
+
179
+ Each pass prints the active scope, one line per adapter, then totals:
180
+
181
+ ```
182
+ scope: project /Users/me/Workspace/canon (2 roots)
183
+ omp: 3 file(s) scanned, 1 reparsed, 2 skipped unchanged (watermark), 4 row(s) parsed, 0 malformed record(s)
184
+ claude-code: 1 file(s) scanned, 0 reparsed, 1 skipped unchanged (watermark), 0 row(s) parsed, 0 malformed record(s)
185
+ sessions normalized: 4
186
+ malformed records (corrupt line/db, counted as violations): 0
187
+ rows skipped (malformed session_id): 0
188
+ runs written: 4
189
+ events written: 37
190
+ ```
191
+
192
+ - **scope** — the active project scope (`--all-workspaces` names it).
193
+ - **file(s) scanned** — every present file this source matched, after
194
+ pruning, before the per-file gate.
195
+ - **reparsed** — the subset actually (re-)parsed this pass (`0` on a
196
+ steady-state pass).
197
+ - **skipped unchanged (watermark)** — files skipped because that file's
198
+ digest was byte-identical to its cursor entry.
199
+ - **malformed records** — corrupt lines / dbs an adapter hit but could
200
+ not extract a row from (counted as a violation, never crashing).
201
+ - **store tiers unreachable** — if `canon.yaml`'s tiers aren't reachable
202
+ (e.g. `tiers.pg` set but `CANON_PG_DSN` unset) or `session`/`run`/
203
+ `event` aren't routed, the pass prints a metadata-only JSON fallback
204
+ instead of persisting — never a partial write. The fallback contains
205
+ scope, adapter counts, record counts, IDs/digests, and a stable
206
+ routing/tier failure class/reason; it never contains directive text,
207
+ task/context prose, or raw event detail.
208
+
209
+ ## What this skill does NOT cover
210
+
211
+ - Artifact/verdict ingestion (`canon ingest artifacts`) — a different
212
+ pipeline; see the `canon-artifact-ingest` skill.
213
+ - Cost-parity computation — omp/pi's own cost is `0.0`/`Unknown` per the
214
+ ported behavior.
@@ -0,0 +1,152 @@
1
+ # canon-storage
2
+
3
+ canon stores every record in one of three capability **rungs**:
4
+
5
+ - `local` — diffable files in the repo (git-backed), zero network.
6
+ - `hot` — live-queryable state (sqlite or postgres).
7
+ - `cold` — bulk archive (s3).
8
+
9
+ A rung is a *role*; which backend implements it is a separate
10
+ declaration. `routing:`/`aging:` name a rung (`local`/`hot`/`cold`),
11
+ never a backend name. `tiers.<rung>.backend:` names the backend.
12
+
13
+ ## Backends and their config
14
+
15
+ | backend | rung role | `tiers.<rung>` config | credentials / network |
16
+ | ---------- | --------- | ---------------------------------- | --------------------- |
17
+ | `git` | local | `root:` (relative to canon.yaml) | none — zero network |
18
+ | `sqlite` | hot | `path:` (relative to canon.yaml) | none |
19
+ | `postgres` | hot | `dsn_env:` + `schema:` | live network + creds |
20
+ | `s3` | cold | `bucket_env:` + `prefix:` | live network + creds |
21
+
22
+ `canon init` scaffolds `sqlite` as the zero-dependency `hot` default
23
+ (WAL journal + busy timeout, fine for one operator's batch ingest).
24
+ sqlite is a **single-writer store**: for heavy multi-agent concurrent
25
+ writers, swap the `hot` rung to `postgres` — same hot role, a one-block
26
+ canon.yaml swap (the scaffold ships the postgres stanza commented right
27
+ beside the sqlite one). A postgres/s3 rung that can't attach fails loud
28
+ at startup — never a silent fallback to local files.
29
+
30
+ ```yaml
31
+ tiers:
32
+ local: { backend: git, root: .canon/ledger }
33
+ hot: { backend: sqlite, path: .canon/hot.db }
34
+ # hot: { backend: postgres, dsn_env: CANON_PG_DSN, schema: canon }
35
+ cold: { backend: s3, bucket_env: CANON_BUCKET, prefix: canon/ }
36
+ ```
37
+
38
+ Using a backend name (`git`/`pg`/`s3`) where a rung is expected is
39
+ rejected with a hint:
40
+
41
+ ```
42
+ canon.yaml TierPolicy: `git` is a BACKEND name, not a rung — canon.yaml's
43
+ `routing`/`aging`/`tiers` keys now name a capability rung
44
+ (local/hot/cold); declare the backend separately via
45
+ `tiers.<rung>.backend: git`
46
+ ```
47
+
48
+ ## Routing a record kind
49
+
50
+ `routing:` is the only place a kind's rung is decided. Keys are the
51
+ stable snake_case kind wire strings (`change`, `evidence_record`,
52
+ `strategy_item`, …) — the same string the record's own `kind` field
53
+ serializes to. Values are `local`/`hot`/`cold`.
54
+
55
+ ```yaml
56
+ routing:
57
+ scenario: local # change this to move FUTURE `scenario` writes
58
+ ```
59
+
60
+ Changing a routing value moves future writes only; re-run reads
61
+ resolve the policy live. An unknown kind or rung name is a loud config
62
+ parse error. Every record kind needs a `routing:` entry — an unrouted
63
+ kind fails hard at its first write/query, never silently dropped.
64
+
65
+ ## Aging a kind to another rung
66
+
67
+ `aging:` moves records from their routed rung to a `to:` rung once
68
+ their age exceeds `after:`:
69
+
70
+ ```yaml
71
+ aging:
72
+ handoff: { after: 30d, to: cold } # <n>d/h/m/s — one integer, one unit
73
+ ```
74
+
75
+ `canon tier age` applies every `aging:` rule once. It is
76
+ content-digest idempotent: re-running finds nothing left to move
77
+ (`moved == 0`); a partial run (destination written, source not deleted)
78
+ re-selects the record but the destination write is a no-op. A kind
79
+ split across two rungs (some aged, some not) still reads correctly —
80
+ `canon query` fans out.
81
+
82
+ ## Running `canon tier age` / `canon query`
83
+
84
+ Both take `--canon-yaml <path>` (default `canon.yaml` in the current
85
+ directory).
86
+
87
+ ```bash
88
+ # Preview what would move (read-only; writes/deletes nothing).
89
+ canon tier age --dry-run
90
+
91
+ # Apply every `aging:` rule once (the destructive move+delete).
92
+ canon tier age
93
+
94
+ # Read a kind, fanning out across its routed rung AND its aging
95
+ # destination, merged by time. --kind is the kind wire string — the
96
+ # same vocabulary as routing:/aging: (`change`, `handoff`, …).
97
+ canon query --kind handoff --since 2026-06-01T00:00:00Z
98
+
99
+ # Machine-readable output instead of the human table.
100
+ canon query --kind trajectory --json
101
+ ```
102
+
103
+ ## What `canon report` does and doesn't read
104
+
105
+ canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
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`
108
+ never opens a live DB connection or the live bucket, so:
109
+
110
+ - Kinds routed to `hot` (sqlite/postgres) do NOT appear in `canon
111
+ report`.
112
+ - Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
113
+ has been separately materialized — there is no automatic sync.
114
+
115
+ These kinds are not lost: read them with `canon query --kind <kind>`.
116
+ The gap is named loud, not under-counted — when any routed kind isn't
117
+ report-readable, `canon report` renders a `## Kinds not read directly`
118
+ section listing them and emits a matching stderr `WARN`.
119
+
120
+ ## Retention, export, and access audit
121
+
122
+ `canon purge --kind <kind> --before <RFC3339> [--repo <dir>] [--dry-run] [--json]`
123
+ is the canonical data-lifecycle boundary. The only purgeable kinds are
124
+ `session`, `run`, `event`, and `trajectory`; the cutoff applies to every
125
+ routed rung and every configured aging destination, including retained
126
+ historical versions. For `trajectory`, the command also purges the
127
+ operator-local Parquet learn store under `learn.root` in `canon.yaml`;
128
+ an unavailable configured learn root is an explicit error, never a
129
+ successful zero-row purge. The operation is idempotent. Apply mode
130
+ preflights all attached destinations before the first delete, but storage
131
+ backends are not one transaction: a failure after a prior delete returns
132
+ an explicit `partial` result with matched/deleted counts and a nonzero
133
+ status. Purge never rewrites Git history or deletes source transcript
134
+ files.
135
+
136
+ `canon export --kind <kind> [--before <RFC3339>] [--after <RFC3339>]
137
+ [--out <file>] [--json]` emits a versioned manifest containing source scope,
138
+ counts, digests, and redacted metadata. It does not authorize backend access;
139
+ credentials remain an operator prerequisite. New output files are mode
140
+ `0600`. Raw event detail text is excluded unless `--include-sensitive` is
141
+ passed and `.canon/policy.yaml` explicitly contains:
142
+
143
+ ```yaml
144
+ query:
145
+ allow_sensitive: true
146
+ ```
147
+
148
+ Every query, export, and purge appends an actor-stamped record to
149
+ `.canon/audit/access.jsonl` (mode `0600` on creation). `CANON_ACTOR` takes
150
+ precedence over the OS username. Audit rows contain operation, kind, scope,
151
+ sensitive flag, counts, and timestamp only—never query text, secrets, or
152
+ record bodies.
@@ -0,0 +1,161 @@
1
+ # canon-subject
2
+
3
+ A **Subject** is canon's first-class handle on the durable product unit a
4
+ team plans, designs, builds, verifies, and ships across MANY changes and
5
+ MANY scenarios. This skill covers the subject-write-first contract every
6
+ domain agent follows.
7
+
8
+ ## Vocabulary rule (non-negotiable)
9
+
10
+ - **"feature docs" / ".feature files" = Gherkin behavior specs** — the
11
+ `.feature` corpus `canon format`/`canon inventory sync` validate. "Read
12
+ the feature docs" ALWAYS means the Gherkin files, never a Subject.
13
+ - **"subject" = the management record** authored by `canon subject new`.
14
+ A Subject is the product unit; its `.feature` files are its behavior
15
+ specs. Never the same word.
16
+
17
+ A Subject links, but does not contain, its work: `change_ids` (adopted
18
+ plan units) and `scenario_ids` (behavior specs) are join links.
19
+
20
+ ## The 4-step loop
21
+
22
+ Every substantial piece of work starts at a subject write.
23
+
24
+ ### 1. Context → author or update the Subject FIRST
25
+
26
+ Write (or update) the Subject before touching anything else, so every
27
+ downstream artifact has a `subject_id` to pin to.
28
+
29
+ ```bash
30
+ canon context --repo . # record kinds, enum domains, policy requirements
31
+ canon subject new subject-domain-loop \
32
+ --domain dev --title "Subject domain loop" \
33
+ --summary "The durable product unit + its per-domain loop" \
34
+ --owner-role implementer
35
+ ```
36
+
37
+ - `<id>` is a kebab-case slug (`[a-z0-9]+(-[a-z0-9]+)*`) — the durable
38
+ `subject_id` join key, never renumbered once assigned.
39
+ - `--domain <d>` is checked twice at write: SHAPE (a kebab-case slug),
40
+ then MEMBERSHIP against the domain set THIS repo has activated.
41
+ Canon ships no opinion about how a team slices its work — the set is
42
+ resolved from the vocabulary, so a studio declaring
43
+ `combat`/`economy`/`live-ops` authors against those and canon's base
44
+ set (`planning`, `design`, `dev`, `data`, `test`, from `canon.core`)
45
+ carries no authority there. Extend or replace it in your own
46
+ `.canon/vocab/<id>/enums.yaml` under the `domain` key, activated by a
47
+ `canon.project.yaml` profile (see `canon-vocab`) — never a canon code
48
+ change. A non-member is refused with the legal set named
49
+ (`expected one of: …`), so a typo cannot silently mint a category that
50
+ then splits every `--domain`-filtered read. A repo that declares no
51
+ `domain` enum has no constraint and accepts any slug. Run
52
+ `canon context` to see the set in force.
53
+ - `--summary` is optional; `--owner-role` defaults to `implementer`;
54
+ `--actor-id` defaults to `canon`. A freshly-authored Subject with no
55
+ `change_ids`/`scenario_ids` is a valid minimal record and starts at
56
+ status `proposed`. `--json` prints the written record.
57
+
58
+ ### 2. Retrieve subject-scoped strategy memory BEFORE working
59
+
60
+ ```bash
61
+ canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
62
+ ```
63
+
64
+ - `--regime <key>` XOR the derived pair `--domain <d> [--subject <id>]`:
65
+ give the full regime key directly, OR let retrieval derive it. Never both.
66
+ - With `--domain`/`--subject`, retrieval tries `<domain>-<subject_id>`
67
+ first, then `<domain>` — a subject's own lessons win, falling back to
68
+ the domain's shared memory when the subject is new.
69
+ - Fail-soft: an empty/missing store degrades to an empty guidance list,
70
+ never a nonzero exit. See `canon-retrieve` for the full contract.
71
+
72
+ ### 3. Work → pin every artifact to `subject_id`
73
+
74
+ - **Scenarios**: `canon scenario new <tag> --subject <subject-id>` emits
75
+ the `@subject:<subject-id>` tag; `canon inventory sync` maps the tag
76
+ onto the scenario's `subject_id`. A malformed/absent tag leaves it
77
+ unset (fail-soft), never an error. See `canon-authoring`.
78
+ - **Changes**: adopt imported plan changes under the Subject (see
79
+ [Adopt flow](#adopt-flow)).
80
+ - **Reviews / evidence / divergences**: authored as usual (`canon review
81
+ add`, `canon gate`, `canon divergence …`) against the Subject's
82
+ scenarios — the join spine carries them back through `scenario_id`.
83
+
84
+ ### 4. Verdicts / trajectories → learn (knowledge out)
85
+
86
+ As work is reviewed, verdicts ingest under the subject-scoped regime
87
+ (`canon ingest artifacts`), so `canon learn` distills strategies keyed to
88
+ this subject and domain — the exact memory step 2 retrieves next time.
89
+
90
+ ## Status lifecycle + the shipped evidence gate
91
+
92
+ `canon subject status <id> <state>` performs a policy-gated transition.
93
+ The legal chain:
94
+
95
+ ```
96
+ proposed → specced → building → verifying → shipped
97
+ ```
98
+
99
+ plus **any state → retired** (a subject can be retired from anywhere).
100
+ Any other transition is rejected by failure class, exits 1, and leaves
101
+ the record UNCHANGED (fail closed).
102
+
103
+ ```bash
104
+ canon subject status subject-domain-loop specced
105
+ canon subject status subject-domain-loop building
106
+ canon subject status subject-domain-loop verifying
107
+ canon subject status subject-domain-loop shipped # gated — see below
108
+ ```
109
+
110
+ On success the status updates in place; `--json` prints the updated
111
+ record. On a gate block it prints violations by failure class to stderr,
112
+ exits 1, and the record is unchanged.
113
+
114
+ **Corpus-wide, the same join:** `canon gate check` with
115
+ `.canon/policy.yaml`'s opt-in `spec_coverage` section generalizes this
116
+ gate past one transition — it left-joins the WHOLE Scenario corpus
117
+ against evidence, so an unimplemented or mismatched spec is reported
118
+ whether or not a Subject links it. `spec_coverage.scope` narrows
119
+ blocking to subjects in named statuses, and `@subject:<id>` tagging is
120
+ what brings a scenario into that scope. See `canon-gate`.
121
+
122
+ **The `verifying → shipped` evidence gate:** shipping additionally
123
+ requires that EVERY linked `scenario_ids` entry carries a latest,
124
+ non-`Divergent` verdict in the ledger (the same last-wins rule `canon
125
+ gate check` uses). If any linked scenario is uncovered or its latest
126
+ verdict is `Divergent`, the transition prints the violating scenarios by
127
+ failure class, exits 1, and the status stays `verifying`. `retired` is
128
+ not gated.
129
+
130
+ ## Adopt flow
131
+
132
+ Planning docs become change/task records via `canon ingest plans` (see
133
+ `canon-plan-import`), but that stops at import. `canon subject adopt`
134
+ lifts an imported change into a managed Subject:
135
+
136
+ ```bash
137
+ canon ingest plans --repo . # import → change/task rows
138
+ canon subject adopt subject-domain-loop-plan \
139
+ --subject subject-domain-loop # link change → subject
140
+ ```
141
+
142
+ `adopt` stamps the change's `subject_id` and adds it to the Subject's
143
+ `change_ids`, so `canon query --kind change --change-id …` and `canon
144
+ query --kind subject` agree on the link from both ends.
145
+
146
+ ## Reading the per-domain management view
147
+
148
+ ```bash
149
+ canon query --kind subject --domain dev --status building
150
+ canon report --repo . # renders the "Subjects" panel
151
+ ```
152
+
153
+ - `canon query --kind subject [--domain <d>] [--status <s>]` — the
154
+ per-domain management view; `--domain`/`--status` are subject-only
155
+ filters.
156
+ - `canon report`'s **Subjects** panel is a per-domain rollup: one row per
157
+ subject (`domain`, `subject_id`, `title`, `status`, `scenario_count`,
158
+ `covered_scenarios`), where `covered_scenarios` counts linked scenarios
159
+ carrying a latest non-`Divergent` verdict — the same coverage the
160
+ shipped gate enforces, surfaced read-only. Also exported by `canon
161
+ report --snapshot` (see `canon-report-dashboard`).
@@ -0,0 +1,115 @@
1
+ # canon-vocab
2
+
3
+ Canon's typed authoring layer replaces a freeform `tasks.md` checkbox's
4
+ implicit evidence with a structured, checker-validated, policy-resolved
5
+ attribute — and gives handoff bodies a declared, rendered template. You
6
+ declare a vocabulary in a `.canon/vocab/<id>/` plugin, then author atoms
7
+ that the checker validates against it.
8
+
9
+ ## Run `canon context` FIRST
10
+
11
+ Before authoring any atom, run `canon context` (or `--json`) and read its
12
+ `vocab` section: it lists every declarable directive + enum for the repo
13
+ plus a content-hash version. Authoring against a directive/enum
14
+ `canon context` does not list means the checker will reject it — the
15
+ surface is the source of truth for "what can I declare here?" See
16
+ `canon-context`.
17
+
18
+ ## Declaring a directive or enum
19
+
20
+ A vocabulary plugin lives at `.canon/vocab/<id>/`:
21
+
22
+ ```yaml
23
+ # .canon/vocab/<id>/plugin.yaml
24
+ id: my.plugin
25
+ version: "0.1.0"
26
+ kind: project # `core` for canon.core; `project` for a consumer plugin
27
+ exports:
28
+ directives: directives/ # a dir of *.yaml directive files
29
+ enums: enums.yaml
30
+ ```
31
+
32
+ ```yaml
33
+ # .canon/vocab/<id>/directives/thing.yaml
34
+ directives:
35
+ - name: thing
36
+ attrs:
37
+ - name: desc
38
+ type: string # scalar
39
+ required: true
40
+ - name: status
41
+ type: { domain: task-status } # value must be a member of the enum
42
+ required: true
43
+ - name: tags
44
+ type: { list: string } # a YAML list of that element type
45
+ required: false
46
+ - name: evidence
47
+ type: evidence # SPECIAL: {kind, ref}; `kind`'s domain
48
+ required: true # resolves LIVE from .canon/policy.yaml,
49
+ # never a locally-declared enum
50
+ ```
51
+
52
+ ```yaml
53
+ # .canon/vocab/<id>/enums.yaml
54
+ enums:
55
+ task-status: [open, done]
56
+ ```
57
+
58
+ - **Type kinds**: `string`; `{ domain: <enum> }` (validated against
59
+ `enums.yaml`); `{ list: <type> }`; and `evidence` (the `{kind, ref}`
60
+ shape whose `kind` domain comes from `.canon/policy.yaml`'s evidence
61
+ kinds, NOT a local enum).
62
+ - The evidence-kind domain is DELIBERATELY absent from `enums.yaml` — do
63
+ not add it there; it resolves dynamically from policy.
64
+
65
+ ## Authoring a typed task atom
66
+
67
+ An atom file is a YAML list of `{ id, tag, attrs }` records. A task atom
68
+ tags `task` and must satisfy the `task` directive's attrs:
69
+
70
+ ```yaml
71
+ - id: my-change#12.3 # <change_id>#<n> (the task_id join key)
72
+ tag: task
73
+ attrs:
74
+ desc: "wire the checker against the real manifest"
75
+ owner: alice # optional
76
+ status: open # must be a task-status enum member
77
+ evidence:
78
+ kind: test-run # must be a policy.yaml evidence kind
79
+ ref: "cargo test"
80
+ ```
81
+
82
+ The atom is validated against the resolved vocabulary and compiled to a
83
+ canon `Task`. A wrong tag, an unknown attr, a missing required attr, an
84
+ out-of-domain `status`, or an evidence `kind` absent from `policy.yaml`
85
+ each produces a checker diagnostic (the "expected one of: …" shape) — the
86
+ atom is rejected at author time, never compiled into a malformed Task.
87
+
88
+ ## Authoring a vocabulary-defined handoff body
89
+
90
+ A handoff body tags `handoff-<domain>` (the domain selects the directive,
91
+ NOT an attr value) and satisfies that directive's attrs:
92
+
93
+ ```yaml
94
+ - id: my-handoff-1
95
+ tag: handoff-dev # dev | design | content | test (canon.core)
96
+ attrs:
97
+ title: "wire the vocabulary into the CLI"
98
+ summary: "the foundation is ready for CLI integration"
99
+ verification-steps:
100
+ - "cargo build"
101
+ - "cargo test"
102
+ ```
103
+
104
+ It's validated and compiled to a canon handoff body. Each
105
+ `handoff-<domain>` directive declares its own attrs (e.g. `design` /
106
+ `content` domains may require `acceptance-criteria` instead of
107
+ `verification-steps`) — read the domain's directive (or `canon context`)
108
+ for the exact required set.
109
+
110
+ ## What this skill does NOT cover
111
+
112
+ - The `canon context` surface itself — see `canon-context`; this skill
113
+ covers authoring AGAINST it.
114
+ - `policy.yaml` / CEL evidence-requirement authoring — see `canon-policy`;
115
+ here you only reference an evidence `kind` policy already declares.