diff --git a/notes/eureka-body-plane-cut.md b/notes/eureka-body-plane-cut.md index d33e7a9a..bf9b34e5 100644 --- a/notes/eureka-body-plane-cut.md +++ b/notes/eureka-body-plane-cut.md @@ -515,7 +515,7 @@ format or JSON schema file (see section 5). | Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve | |---|---|---|---|---| -| `huginn-mind/src/wire.rs:81-90`: `HuginnMindResponse::Deferred(DeferredAnswer)` with `DeferredAnswer { manifest: CultMeshCdnArtifactManifest }` under Q-BP2 A, or plus the typed summary under B; `status()` (`:97-102`) answers `accepted` for it. A hand-written `JsonSchema` for the manifest inside Huginn, as the selection consumer cut does for `Selection`. `schemas/cultnet/huginn.mind_response.v1.schema.json` is regenerated. **The schema id stays `v1`**, because no client of it exists (Cut 13 is unbuilt) | `huginn-mind` (types only) | the daemon, Cut 13 | the reference rides the response schema, so there is no second vocabulary for a client | `HuginnMindResponse` is the only response a client decodes | +| `huginn-mind/src/wire.rs:81-90`: `HuginnMindResponse::Deferred(DeferredAnswer)` with `DeferredAnswer { manifest: CultMeshCdnArtifactManifest }` under Q-BP2 A, or plus the typed summary under B; `status()` (`:97-102`) answers `accepted` for it. The manifest's schema is embedded through a `#[schemars(schema_with)]` field attribute returning a `$ref` to its published schema, as the selection consumer cut now does for `Selection` (corrected 2026-09-22: a hand-written `impl JsonSchema` for a foreign type is `E0117`, the orphan rule). `schemas/cultnet/huginn.mind_response.v1.schema.json` is regenerated. **The schema id stays `v1`**, because no client of it exists (Cut 13 is unbuilt) | `huginn-mind` (types only) | the daemon, Cut 13 | the reference rides the response schema, so there is no second vocabulary for a client | `HuginnMindResponse` is the only response a client decodes | | `huginn-daemon/src/bodies.rs` (new), `DeferredBodies`: a map from chunk hash to bytes with reference counts, and a map from content hash to `(chunk hashes, bytes, last_touched)`. `retain(manifest, chunks, now)`, which is idempotent on the content hash and refreshes it; `chunk(hash, now) -> Option<&[u8]>`, which refreshes the owning bodies; `expire(now)`; eviction least-recently-touched first when the total is over budget. Constants: `DEFERRED_CHUNK_BYTES = 256 * 1024`; `MAX_DEFERRED_BODY_BYTES = 64 MiB`; `DEFERRED_BUDGET_BYTES = 256 MiB`; `DEFERRED_TTL = 60 s`, which is twice `session_timeout`, and is a field of `ServeOptions` (`:70-79`) with that default | `huginn-daemon::serve` | `deliver`, `answer` | retention is bounded in bytes and time, and a body is never evicted while its chunks are being served within the TTL | The mind must not hold transport artifacts. `cultnet-rs` owns no retention policy, and should not, because the policy is this service's memory budget | | `serve.rs`: `deliver(reply, message_id, operation, runtime_id, bodies, now)` replaces `within_window`. It encodes the envelope. If the envelope is `<= MAX_RESPONSE_BYTES`, the reply is sent unchanged. Otherwise it takes the **payload bytes**, the named MessagePack of the response. If they are `<= MAX_DEFERRED_BODY_BYTES`, it calls `pack_content("huginn.mind_response", "package", "", "application/vnd.gamecult.huginn.mind-response+msgpack", now, payload, DEFERRED_CHUNK_BYTES)`, then `bodies.retain`, and answers `Deferred`. Otherwise it answers `Refused(ResponseTooLarge { bytes: payload_len, limit: MAX_DEFERRED_BODY_BYTES })` | `huginn-daemon::serve` | `answer` | **the one owner of how an answer is delivered.** Three outcomes, in that order, and nothing else decides | — | | `serve.rs:165-194`, `answer`: a new `CultNetMessage::ContentChunkRequest` arm calls `cultnet_rs::answer_content_chunk_request(request, \|h\| bodies.chunk(h, now))`. The error text at `:188-192` names the third family. The signature gains `bodies: &mut DeferredBodies`, and `run` (`:257-305`) owns one and passes it | `huginn-daemon::serve` | the client's fetch | a chunk is answered on the requester's own session, like every other frame | — | @@ -651,7 +651,7 @@ format or JSON schema file (see section 5). - Removes about 35 lines (`within_window` 35, the stated-limit doc 8) and 8 mutation entries. - Adds about 190 lines: `bodies.rs` about 110, `deliver` about 35, the - `answer` arm 8, the wire variant with its hand-written `JsonSchema` about + `answer` arm 8, the wire variant with its `schema_with` `$ref` about 35. - Tests are about +220 net over the rewritten two. - Entries: 8 removed, about 20 added. diff --git a/notes/eureka-pipeline-state-cut.md b/notes/eureka-pipeline-state-cut.md index d1d1cdd2..297541f0 100644 --- a/notes/eureka-pipeline-state-cut.md +++ b/notes/eureka-pipeline-state-cut.md @@ -545,6 +545,53 @@ schema-ownership phase inside Epiphany, not because the campaign stops. restores the bytes it captured at M0, and never checks whether the current bytes are the mutant it wrote (`eureka-mutations.ps1:163-175, 393, 415`). Fixed in the Eureka repository in the same batch. + - **The residue batch landed** (Sonnet). Commits: + - Huginn `65c46aa`: seals `path_for`, adds N1 entries on all three sites + (`open_with` now dies), the U+2010 fold, and gate fixtures that vary the + operation and runtime id independently. + - Huginn `f7e0d02`: moves the leaf pin to `b3ae787b`, which carries S5 + (64-byte bounds and NUL pinned) and S8. The lockfile also picked up a + `getrandom` 0.4.3→0.3.4 change as a side effect. + - Eureka `5d2bd3e`, H1: the harness saves a mid-run edit as + `.eureka-mutation-overwritten`, prints `EDIT LOST`, and fails the run. + Demonstrated before and after. + + Tests: mind 65, daemon 20, leaf 35. Every suite is fully killed: + + | Suite | Killed | + |---|---| + | leaf | 5/5 | + | Cut 8 | 67/67 | + | Cut 9 | 38/38 | + | Cut 10 | 77/77 | + + There is one `cultcache-rs`. **Soul's check of the residue is + dispatched.** It covers two things. First, `path_for` became + `store_path_for` behind a test-support feature, and a consumer that turns + the feature on might reach the bypass again. Second, the lockfile side + effect. + - **The read-side consumer cut is mapped** at `notes/eureka-read-side-cut.md` + (Imagination, Opus). Cuts in order: + - **P-1 and P-2:** CultLib, before merge. Now folded into selection's + R-G and R-M. + - **RS-L:** leaf epoch v2, titles, and the `OrgRepo` and `Label` doors. + - **RS-2:** subtraction. + - **RS-1:** receipt v2 with ordinals, and the epoch gate moved ahead of + the type gate. + - **BP-2, widened:** one pin move. + - **RS-3:** the consumer, including the value door for S6. + + Its appendix carries the summary type, copied from a session scratchpad + that will not survive. + + Probed facts: + - The seven old relations each equal one selection. + - Admission order cannot be recovered from the clock: 31 receipts carry + 5 distinct seconds. + - Today's opener refuses an old store by the wrong gate. + + **Operator questions Q-RS1 (titles) and Q-RS2 (whether `huginn-mind` may + link `cultnet-rs`) are open.** - *History:* **Cut 10 stays open until the fourth fix batch lands** (Hands, Sonnet): N1 through the leaf check, one generated separator test for N3, fixtures that vary the operation and the runtime id for N4, and the prose fixes for N5 diff --git a/notes/eureka-read-side-cut.md b/notes/eureka-read-side-cut.md new file mode 100644 index 00000000..fe4f6fb9 --- /dev/null +++ b/notes/eureka-read-side-cut.md @@ -0,0 +1,1211 @@ +# Huginn read side: the consumer cut map + +Date: 2026-09-22. Imagination output (Opus). Nothing here is committed and no +repository was edited. Self places and commits this file. Its natural home is a +section of `F:\Projects\Epiphany\notes\eureka-pipeline-state-cut.md`, the +campaign that owns the read-side rulings, with a pointer from §12 of +`F:\Projects\CultLib\docs\cultnet-selection-cut.md`. + +Status: cut map for five cuts and two CultLib prerequisites. **P-1** and **P-2** +go to the selection campaign's next fix batch, before `cultnet/selection-cut1` +merges. **RS-L** is the leaf (Epiphany). **RS-1**, **RS-2** and **RS-3** are +Huginn. **BP-2** belongs to the body-plane map and is widened here by one +mechanical clause. None is started. Two operator questions are open (§9). + +**Rulings this map is written under.** + +- **Operator, 2026-09-17** (Epiphany map, "Operator rulings on the read side"): + - the generic vocabulary lives in CultNet, and the organ consumes it; + - receipt ordinals: yes. The stored type changes, the store takes a new + version, and an existing store refuses to open rather than being misread; + - `open_items` and `history` are deleted, not kept as presets; + - `question` and `ruling` gain a `Short` title in the leaf, and it rides the + same store version as the ordinal. +- **R-3 (selection map §16):** `admitted_after` and `admitted_before` are deleted + from the organ's wire. The latest N is `descending, limit: N`. +- **Q13 A (Epiphany map, 2026-09-16):** the wire types live in `huginn-mind`, + and the client depends on that crate for types only. +- **What stays with the organ:** the row and reference implementations, the + roles, in force, the summary, the snapshot restriction and the deletions. +- **S6 (Cut 10 residue, recorded 2026-09-22) is owned here:** + - declared names used only as read filters are never checked against the + grammar; + - `OrgRepo` claims a format it does not enforce. + +## Pins + +| Repo | Branch | HEAD | State | +|---|---|---|---| +| Huginn | `eureka/memory-organ` | `4f7e3b5` | **Dirty: the Cut 10 residue batch is in Hands.** It modifies seven files: `huginn-mind/Cargo.toml` (+8, a `test-support` feature), `mind.rs` (+74), `admission.rs` (±6), `daemon.rs`, `serve.rs`, `huginn-daemon/Cargo.toml` and the cut10 entries file. Anchors below are against `4f7e3b5`. **Hands re-takes them after the residue lands:** `mind.rs` lines shift, and `admission.rs` lines may shift. This map was not written against the uncommitted diff. | +| Epiphany | `codex/eureka-pipeline-state` | `b3ae787b` | Clean. The leaf changed after `9d3a3efd` only in tests (+32 lines at `lib.rs:2484`), so leaf anchors hold at both. Huginn pins the leaf at `9d3a3efd`. | +| CultLib | `main` | `070fac0` | The selection branch is not merged. `origin/main` is 14 commits past the merge base `b3d9cf7`. | +| CultLib | `cultnet/selection-cut1` | `00f4c02` | Selection Cut 1 finished executing. Its final Soul pass is running. Rust anchors are against `git show 00f4c02:packages/cultnet-rs/src/selection.rs`. | + +**Pins in use:** + +- Huginn: `cultcache-rs` and `cultnet-rs` at `a0813c6`, and the leaf at `9d3a3efd`. +- The leaf: `cultcache-rs` at `a0813c6`. + +`git diff --stat a0813c6 00f4c02 -- packages/cultcache-rs` is **empty**. In the +same range, `cultnet-rs` gains selection (+3,111/−21 over 6 files). + +## 0b. Model page: identity, lifecycle and authority + +Every persistent or long-lived kind this cut touches has a row. No cell is empty. + +| Kind | Identity (what names it) | Lifecycle | Authority (who decides) | +|---|---|---|---| +| **Commit receipt** (`huginn.mind_commit_receipt.v2`) | `receipt_id` = `mind-commit-` + sha256 of `(instance, strong_reads, writes)`. **`ordinal` is not part of the identity.** It is excluded from the digest, as `committed_at` and `provenance` are (`receipt.rs:10-14`), so an exact replay still finds its stored receipt (A9). | Written once in its batch's own swap. It never changes and is never deleted. A replay answers `AlreadyAdmitted` with the stored receipt. | `receipt.rs`. `candidate` (`:160-178`) is the one construction site. `commit` (`:211-245`) is the one swap. | +| **Admission ordinal** (a field on the receipt) | A `u64` position in this mind's admission history. It starts at 1 and is dense: the receipts carry exactly `{1..=N}`. **Head** means `N`, the number of receipts. | Assigned when a batch is admitted, as `head + 1`, under the single writer: `&mut Mind` plus redb's lifetime lock. It is never reused. A lost swap lands nothing, so it leaves no gap. | `receipt.rs` owns both `head()` and assignment. Admission asks it for `head + 1`. The read side asks it for `head` as the current `asOf`. Both refuse a chain that is not dense, with `Unavailable`. No other code computes an ordinal. | +| **Store version** | The epoch record `huginn.mind_epoch.v1`, keyed by and naming the leaf's `PIPELINE_SCHEMA_EPOCH` (`mind.rs:25-42`), together with every stored type id. Each id carries its own version. | The epoch moves when the leaf breaks its schema. A type id moves when its owner breaks that type. An old store is refused by the opener and never migrated. That is the operator's ruling, and it costs nothing today (F5). | The leaf owns the epoch string and its thirteen type ids. Huginn owns the receipt's type id and the gates. **The epoch gate runs first** (RS-1), so a store written at an old epoch is refused as `ForeignEpoch`. A foreign type in a store at the current epoch stays `ForeignStore`. | +| **Question and ruling title** | The field `title` on `PipelineQuestion` and `PipelineRuling`. Its type depends on Q-RS1. | Written by the author, immutable like every leaf field, and required. | The leaf owns the field and its bound. Admission validates it through `PipelineDocument::validate`, as it does every other field. | +| **Selection cursor** | An opaque string minted by `cultnet_rs::Cursor::mint`. It carries `asOf`, the last `(ordinal, schemaId, recordKey)`, and a digest of the selection with `cursor` and `limit` cleared. | One per page that has a next page. It lives as long as the client keeps it. **It never goes stale on Huginn:** the mind is append-only with one writer, so the organ can always present rows as of any `asOf ≤ head`. | The substrate mints it, parses it and checks its digest. The organ decides which `asOf` to answer at: the cursor's own, and never `head`, because the substrate refuses `cursor_stale` whenever the two differ (F7). An `asOf > head` is refused `CursorInvalid`: this mind could not have minted it. | +| **Snapshot image** (the rows as of `asOf`) | `asOf`. | Built per read call and thrown away with it. Nothing is stored. | `query.rs` `Reader::at(mind, as_of)` builds it. It holds the documents whose writing receipt has `ordinal ≤ asOf`, and **every derivation runs over that restricted image**: in force, the closing resolution, citations and base. | +| **Declared vocabulary**: aliases, roles and their value domains | Alias and role names, which are the organ's closed lists (RS-3 §"Vocabulary"). | Changes only with code. There is no catalog. A new alias or role is a normal change. | `huginn-mind` `rows.rs`, through `RowSet`. The substrate door checks the **names**. The organ's door checks the **values**, which is S6. | +| **Citation edge** | `(from: PipelineRef, role: CitationRole, to: PipelineRef)`. | Derived from the leaf fields each time it is read. Never stored. | `docs.rs` `citations()` is the one edge list. Admission's A7 (`admission.rs:242-285` today) and the read side's `Row::references` both read it. | +| **Header (summary)** | `PipelineDocumentSummary`, keyed by the row's `id`. | Derived each time it is read, and bounded by `SUMMARY_MAX_BYTES`. | `query.rs`. It is the organ's meaning of `projection: header` (selection §2 "Projection"). | +| **Published schemas** | The leaf's thirteen `epiphany.pipeline..v2.schema.json` files plus `index.json`. Huginn's `huginn.mind_request.v1` and `huginn.mind_response.v1`. | They are regenerated whenever the derivation changes, and pinned byte for byte by a test (leaf `lib.rs:2324`; Huginn `wire.rs:210-216`). The request schema `$ref`s CultLib's published selection schema by its `$id` (F9). | The leaf owns its thirteen. Huginn owns its two. CultLib owns `cultnet.selection.schema.json`, and nothing copies it. | +| **Mutation suites** | Files: `tools/eureka-cut9-mutations.psd1` (V entries), `tools/eureka-cut10-mutations.psd1` (D/S entries), and the new `tools/eureka-readside-mutations.psd1` (Huginn) and `tools/eureka-readside-leaf-mutations.psd1` (Epiphany). | The entries whose rule died with a deletion are **deleted** (RS-2). The entries whose rule survives under a new spelling are **re-authored** in the readside suite (RS-3). Every file keeps its M0 control. | Hands writes them. The harness is `~/.claude/skills/eureka/tools/eureka-mutations.ps1`. | + +Forks this page surfaced: **Q-RS1** (what a title is) and **Q-RS2** (whether +`huginn-mind` may link `cultnet-rs`). Both are in §9. + +## 1. Body facts + +Each fact comes from a source read at a named HEAD, and wherever a mechanism +claim needed it, from a run. The probes are in the session scratchpad: + +- `readside-probe/` was built against `cultnet-rs@00f4c02` and `huginn-mind` by + path. That path was the `4f7e3b5` tree plus the residue batch's uncommitted + edits, read only; see "Not probed". +- `storev2/` holds copies of the leaf and of `huginn-mind`, emulating the + version bump. +- `schemaprobe/`. + +The target directory is `C:\Users\Meta\.cargo-target-codex\imag-readside`: 941 +files, 0.37 GiB, cold build 3m07s. Nothing was cleaned. + +### F1. The four reads today (`query.rs` at `4f7e3b5`) + +**What every read shares.** Every read builds `Reader::new` +(`query.rs:206-209`). That decodes **the whole image** +(`docs.rs:42-52`) and **every receipt** (`mind.rs:186-196`, then +`AdmissionIndex::build`, `query.rs:134-150`), on every call. A document that no +receipt wrote refuses as `Unavailable`. A document that two receipts wrote +refuses the same way. + +**`view`** (`:323-327`). It asks `validate_ref` first (V24), then finds the +`(kind, key)` by a linear search, then joins the facts and the status. + +**`query`** (`:331-350`): +- It refuses `semantic` before the reader exists (V8/V8L). +- It scans every held document. +- `Reader::matches` (`:249-290`) applies these filters: + - `campaign` = the key's root segment (V18/V18L: the root, not a field); + - `repo` and `cut`, read through `base()` (`:237-246`), which walks + resolutions down to their subject; + - `kinds`; + - `in_force`; + - `faculty`; + - `admitted_after` and `admitted_before`, compared as strings. +- It orders by `(admitted_at, id)` (`:307-311`). +- It clamps `limit` into `1..=200` (`QUERY_LIMIT_MAX`, `:32`), and reports + `matched` before the cap. + +**`open_items`** (`:355-391`). It filters all views to the campaign root, then +builds five lists: +- in-force questions, findings and follow-ups; +- in-force cut specs that no `cut_report.cut_spec` names; +- cut reports that no `verdict.cut_report` names. + +It is uncapped. + +**`history`** (`:401-435`): +- `Subject(ref)` asks `validate_ref`, reads `resolutions_of`, and orders by + sequence. +- `Repo(org_repo)` reads `assignments_of(mind, repo)` and orders by sequence. + **Nothing checks the `OrgRepo`.** The doc comment at `:399-400` claims "the + type already holds [it] to its own format". It does not: `Bounded` is + crate-private to the leaf, and a deserialised `OrgRepo` is never validated. + This is S6. + +**Wire.** `HuginnMindRequest` (`wire.rs:42-50`) and `HuginnMindResponse` +(`:81-90`) carry one variant each per read. **Daemon.** The arms are at +`daemon.rs:84-99`. The instance gate at `:66-71` runs for every read. + +**Probe, run.** A real redb mind built through the organ's public API +reproduces the `open_items` scenario of `query.rs:943-1003` and three answer +cycles for `history`. Every list the organ answered was then re-answered +through `cultnet_rs::select` over rows built from the organ's own views. **All +seven relations match, in members and in order:** the five open lists, +`history(Subject)` and `history(Repo)`. + +`specs_without_report` came back `[cut-9.r2, cut-12.r2]`. That is admission +order, because the probe's clock advances per batch. The Cut 9 test pins +`[cut-12.r2, cut-9.r2]`, the id order that results when every batch shares one +`now()`. **Tests that pin id order under a single clock change their +expectation once the order becomes the ordinal.** Hands expects this; it is +not a regression. + +### F2. Where receipt ordinals come from + +There is no ordinal today. +- `candidate` (`receipt.rs:160-178`) is the one place a receipt is constructed. + `commit` (`:211-245`) makes one compare-and-swap. Its expectation is the + strong reads, and its insert-only replacements are the writes plus the + receipt. +- **The swap does not guard an ordinal.** Receipts are insert-only under their + digest key, so two receipts claiming one ordinal would both insert (the store + semantics are at `store.rs:135-159`). Uniqueness therefore rests on the single + writer: `admit` takes `&mut self`, and the owned store holds a lifetime lock + (ruling 15). F7 of the Cut 10 pass already recorded that the store is not + sealed. +- The map's answer is to derive the ordinal as `head + 1` and to **validate + density wherever it is read**. It does not add an insert-only ordinal row, + which would be a new persistent kind that guards only against this organ's own + code. Sealing the store is the recorded follow-up that closes the gap + structurally. + +**Probe, run: the order cannot be recovered from the clock.** A real store, +written by Cut 10's Soul probe (copied from +`…F--Projects-Aetheria…/cut10-probe-run/state-1789593994318/minds/yggdrasil/mind.redb`), +decodes to 31 receipts with **5 distinct `committed_at` values**. A store +therefore cannot be migrated by deriving ordinals from time. That confirms the +ruling to refuse rather than migrate. + +### F3. The payload shapes, decoded from the real store + +**The receipt** is a **positional 7-element MessagePack array** (`0x97`). That +holds even though it is written through `prepare_entry_named` +(`cultcache-rs@a0813c6 lib.rs:2181-2197`). The positions are the +`#[cultcache(key = N)]` slots. + +A v1 receipt decoded under a v2 shape that adds `ordinal` at key 7 gives: +- **strict:** `Err("invalid length 7, expected struct … with 8 elements")`, + which surfaces as `Unavailable` at read time, deep inside every read; +- **`#[serde(default)]`:** `Ok(0)`. That is a **silent misread**: every old + receipt gets ordinal 0. + +**So the receipt's type id must move.** Adding the field alone is either a late +refusal or a silent lie. + +**Leaf documents** are `[value]` (`0x91`), where `value` is a named map +(`schemas/cultnet/README.md:36-38`). A required `title` is therefore a +breaking change to the leaf. Without a default, an old payload fails with +"missing field", which follows from serde and was not probed. With a default, +the change is additive and keeps the epoch, per the leaf's own rule (`README.md:40-46`). + +### F4. What the store version bump touches + +**Leaf (`epiphany-pipeline` at `b3ae787b`):** +- `PIPELINE_SCHEMA_EPOCH` `lib.rs:720` changes `v1 → v2`. Its doc at `:713-719` + stays true. +- **All thirteen** type ids in the `pipeline_kinds!` invocation (`:596-630`) + move, because `every_kind_is_at_the_epochs_version` (`:2381-2400`) pins every + kind at the epoch's version. Two kinds change shape, and thirteen ids move. +- Thirteen schema files in `schemas/cultnet/` are renamed `v1 → v2`, and + `question` and `ruling` gain `title`. +- `schemas/cultnet/index.json` (`schemaId`, `schemaVersion`, `documentType`, + `path` ×13) is updated. +- `schemas/cultnet/README.md:41` (the epoch named in prose) is updated. + +**Huginn:** +- `receipt.rs:27` (`RECEIPT_SCHEMA_VERSION`), `:95` (the type id) and `:348` + (a test literal) change `v1 → v2`. `HuginnCommitReceipt` gains + `#[cultcache(key = 7)] pub ordinal: u64`. +- The daemon test literals `"epiphany.pipeline.epoch.v1"` at `daemon.rs:380` + and `envelope.rs:183` change, and so does the `"epiphany.pipeline.instance.v1/…"` + detail at `daemon.rs:645`. +- Both published wire schemas are regenerated, because they embed + `PipelineDocument` (the new titles) and `AdmissionFacts` (the new ordinal). +- `HuginnMindEpoch` (`huginn.mind_epoch.v1`) is **unchanged**. Its shape does + not move, and it is keyed by whatever the epoch string is. + +**Untouched, checked:** +- `epiphany-core/src/runtime_spine.rs:8761` uses + `"epiphany.pipeline.campaign.v1"` as a stand-in for a *foreign* type id. Its + rule is "an id the spine never registered", which any string satisfies. It + does not depend on the leaf; that is stated at `:8754-8757`. + +### F5. How an existing store refuses to open (probe, run) + +The `storev2/` probe copies the leaf and applies the bump: +- the epoch becomes v2 and all thirteen type ids become `.v2`; +- `question` and `ruling` gain `title`. + +It also copies `huginn-mind`, with the receipt at v2 carrying `ordinal`, and +opens a copy of the real v1 store: + +``` +gate order today (types, then epoch): Some(ForeignStore { type: "epiphany.pipeline.campaign.v1" }) +gate order epoch first: Some(ForeignEpoch { found: "epiphany.pipeline.epoch.v1", expected: "epiphany.pipeline.epoch.v2" }) +v2 opener refuses: ForeignStore { type: "epiphany.pipeline.campaign.v1" } +``` + +**Today's opener refuses the old store, but by the wrong gate.** A real epoch +bump always moves the type ids, by the leaf's rule. The type gate +(`mind.rs:137`, `:220-225`) therefore always fires before the epoch gate +(`:138`, `:235-257`). **The epoch gate is unreachable for the one case it +exists for.** Its doc at `mind.rs:21-24` promises "a breaking schema bump +refuses the old store". + +The fix swaps the two calls at `:137-138`. Checked against the opener test +(`mind.rs:345-413`): **none of its eight cases changes its expected refusal** +under the swap. +- The "runtime store passed by mistake" case carries a current epoch record, so + it still reaches the type gate. +- A foreign store with **no** epoch record becomes `MissingIdentity` instead of + `ForeignStore`. No test pins that case. It is honest: such a store has no + mind identity at all. + +### F6. `select`'s snapshot contract, run + +On a probe row set: +- A cursor minted at `asOf 21` answers page 2 at `asOf 21`. +- The same cursor answered with `asOf 22` gives + `CursorStale { as_of: 21, current: 22 }` (`selection.rs:1131-1136`). +- **So "Huginn never refuses `cursor_stale`" (selection map §2 "Snapshot") is + the organ's job:** it must answer as of the cursor's `asOf`, and pass that + `asOf`. It learns `asOf` through `Cursor::parse` (public, `:1326-1346`) + **before** calling `select`, so that it can restrict the rows. This is a + dependency on near-final code (§10). + +### F7. The substrate door checks names, never values (S6 lives in the organ), run + +`cultnet_rs::validate` (`selection.rs:756-862`) refuses these: +- empty lists and blank entries; +- a bad `op`; +- `values` and `number` present in the wrong combination; +- an undeclared `index` on the reachable schemas; +- a comparison on a non-numeric alias; +- undeclared roles. + +**It never looks at a value.** Probe: +- a `root` value `"eureka state/../x"` validates, and answers `[]`; +- a `cites.target` of `{schemaId: ruling, recordKey: }` + validates, and answers `[]`. + +An unknown `schemas` id is not refused either. `reachable_schemas` (`:864-873`) +filters it away silently. This is the S6 class exactly: a malformed filter reads +as an empty answer. Selection map D1 already assigns value refusal to "the row +owner … at the door". **The organ's value door is RS-3's.** + +### F8. `Evaluation` carries no `matched` (source read) + +`Evaluation { rows, edges, next_cursor }` (`selection.rs:1080-1085`) has no +count. `select` computes the full matched list (`:1120-1152`) and discards its +length. The page's `matched` is on the wire (`contracts.rs@00f4c02:365-368`), +but a Rust consumer cannot fill it without re-running `select`, and even then +only up to the 200 cap. + +This is a gap in the owner: **P-1**. No Rust server answers v1 today +(`snapshot_query.rs` serves v0), so Huginn is the first caller to feel it. + +### F9. Publishing Huginn's request schema with a substrate type in it (probe, run) + +`impl JsonSchema for cultnet_rs::Selection` inside a consumer crate fails with +**`E0117`**, the orphan rule. So §12's "hand-maintained `JsonSchema` impl for +the `cultnet-rs` types inside Huginn" **cannot be written**. The same applies +to BP-3's plan for the manifest. + +What works, and was run, is +`#[schemars(schema_with = "selection_ref")]` on Huginn's own field, returning +`{"$ref": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json"}`, +which is the `$id` published at `00f4c02`. This gives one owner for the +selection's shape and no mirror type in Huginn. + +`Edge` has no `$defs` entry in CultLib's response schema; it is inline under +`properties.edges`. Huginn therefore types its edges in its own vocabulary +instead (RS-3). + +### F10. `Selection` does not derive `Eq` (probe, run) + +`#[derive(PartialEq, Eq)]` on a struct carrying `cultnet_rs::Selection` fails +with `E0277`. `Selection` and `FieldPredicate` derive `PartialEq` only +(`selection.rs:400`, `:435`). `HuginnMindRequest` derives `Eq` (`wire.rs:42`). +This is a gap in the owner: **P-2**. Every field is a string, bool, integer or +vector, so `Eq` is derivable. + +### F11. `huginn-mind` has no CultNet dependency, and a check says so + +`huginn-mind/Cargo.toml` depends on `cultcache-rs` and the leaf, and says "No +network, no socket, no index". The Cut 8 verification +(`eureka-pipeline-state-cut.md:3701`) requires this grep to come back empty: + +`rg -n "reqwest|UdpSocket|qdrant|ollama|cultnet|cultmesh|IndexPort|EmbeddingPort|pending_index" crates/huginn-mind` + +**Carrying `Selection` in `wire.rs`, which is where Q13 A put the wire types, +breaks that check.** + +`cultnet-rs` brings `socket2`, `aes-gcm`, `ed25519-dalek`, `rand`, `uuid` and +`windows-sys`. It adds **zero** packages to the workspace build, because +`huginn-daemon` already pulls all of them. `selection.rs` itself uses only +`serde`, `serde_bytes`, `base64` and `sha2`. This is Q-RS2. + +### F12. Record keys are unique across kinds + +- The leaf derives every key as `::` + (`query.rs:165-174` reads it that way). +- The probe image had 36 distinct keys over 36 views. + +This matters because `select` indexes rows by record key alone +(`selection.rs:1095`). Huginn is safe under that indexing whichever way the +selection Soul pass settles it. + +## 2. CultLib prerequisites (routed to the selection campaign, not this map's Hands) + +These fill gaps in their owner. Self adds them to the selection branch's next +fix batch, so that they land **before the merge** that BP-2 pins to. + +- **P-1. `Evaluation.matched: u32`**, the length of the ordered list before + cursor and limit. + - Rule to pin: `matched` is the count before paging, unchanged across a + walk's pages. + - Revert: drop it. Loosening: `matched = page.len()` must die on a fixture + with 3 matches and `limit: 1`. + - The C# evaluator's page already carries it. The parity vectors should + compare `matched`; check whether they already do. +- **P-2. `Eq` on `Selection`, `FieldPredicate`, `Citation`, `Incoming` and + `Edge`.** + - `RecordRef` already has it. + - No test is needed beyond a compile use. Fallback, if the selection campaign + refuses: Huginn drops `Eq` from `HuginnMindRequest`. That costs nothing + today, because its tests use `assert_eq!`, which needs only `PartialEq`. + +## 3. Cut order + +``` +[Cut 10 residue batch lands on Huginn] (in Hands now) + │ + ├──► RS-2 Huginn: delete open_items, history, the admission window (no pin move) + │ │ + │ ▼ + │ RS-1 Huginn: the receipt ordinal, receipt v2, epoch-first opener (no pin move) + │ + ├──► RS-L Epiphany leaf: titles, epoch v2, OrgRepo and Label doors (parallel: another repo) + │ +[P-1, P-2 → selection Soul → selection merges to CultLib main] + ▼ +BP-1 CultLib content plane (body-plane map; Q-BP1) + ▼ +BP-2 one pin move: leaf cultcache → X; Huginn cultcache, cultnet → X; Huginn leaf → RS-L tip + │ (widened here: the mechanical adoption of RS-L's titles and epoch in Huginn's fixtures and literals) + ▼ +RS-3 Huginn: the consumer (Row/RowSet, value door, snapshot, page, summary, edges) + ▼ +BP-3 Huginn: deferral to the body plane (body-plane map; Q-BP2) + ▼ +Cut 11, Cut 13 +``` + +**Why RS-2 and RS-1 run early.** Neither needs a new pin. Both are plain Huginn +work that can land while CultLib finishes selection. RS-2 comes first because +it is pure subtraction, and it leaves RS-1 and RS-3 fewer read paths to carry. + +**What the gap costs.** Between RS-2 and RS-3 the organ answers `view`, +`query`, `whoami` and `admit`. The two relations that RS-2 unpins (R-B, D6) are +re-pinned as selections in RS-3. No client exists, because Cut 13 is unbuilt. + +**RS-L can land at any time**, but **before BP-2**, so that Huginn moves its +leaf pin once (§4). + +## 4. The pin sequence + +- **CultLib revision X** is the first `main` commit that carries both of these: + - the selection merge, including P-1 and P-2; + - BP-1. +- **The leaf** pins `cultcache-rs` at X. BP-2 makes this the leaf's commit, + **after** RS-L, so the leaf's tip carries both changes. +- **Huginn**, in one commit (BP-2's Huginn half): + - `huginn-mind` pins `cultcache-rs` at X; + - `huginn-daemon` pins `cultnet-rs` at X; + - `huginn-mind` pins the leaf at the leaf's tip. + + `huginn-mind` gains `cultnet-rs` at **X** in RS-3. That adds an edge at the + same revision, not a pin move. +- **Result:** every CultLib pin moves once, and the leaf pin moves once. + `cargo tree -d` shows one `cultcache-rs` and one `cultnet-rs`. The probe build + shows two `cultcache-rs` when the revisions differ, confirming the body-plane + map's F5. + +**BP-2 is widened by one clause.** Adopting RS-L changes the leaf's API: two +required fields and a new epoch. BP-2's Huginn commit is therefore **not** +revision strings only. It also: +- adds `title` in `fixtures.rs:162-187` and in its own probe; +- updates the three version literals of F4; +- regenerates the two published wire schemas. + +It changes no behaviour. It is verified by: +- the full suite green; +- the cut8, cut9 and cut10 mutation suites rerun clean; +- `cargo tree -p huginn-daemon -e normal -d` clean. + +The body-plane map's "74 tests at `202e5e3`" is stale. Re-count after the +residue lands. + +**Conditional default: this depends on Q-BP1, and nothing here re-asks it.** +- Under **Q-BP1 A**, BP-1 is small and starts now, so the pins wait for it and + move once. +- Under **B or C**, BP-1 becomes a TLS or QUIC campaign. Waiting would stall + RS-3 on an unrelated transport. In that case BP-2 splits: + - it moves to the selection merge now, and RS-3 proceeds; + - a second CultLib-only pin move follows BP-1. The leaf pin does not move + again unless the leaf's `cultcache-rs` changes. + + Self decides at that point. It is not an operator question: a pin move costs + four strings and one `cargo tree` check. + +## 5. The cuts + +### Cut RS-L. The leaf: titles, the epoch, and two grammar doors + +- **Repo/branch:** Epiphany `codex/eureka-pipeline-state` from `b3ae787b`. + Depends on Q-RS1 (the type of `title`). No pin moves. +- **Deletes first:** none. The v1 schema files are **renamed**, not kept + beside v2, so there is no second copy. +- **Changes, by anchor (`lib.rs` at `b3ae787b`):** + - `:349-352` `PipelineQuestion` and `:353-356` `PipelineRuling` each gain + `title: `, where `` depends on Q-RS1. Under Q-RS1 B, `:344` campaign + and `:358` cut spec move from `Short` to the same ``. + - `:720` `PIPELINE_SCHEMA_EPOCH` becomes `"epiphany.pipeline.epoch.v2"`. + - The thirteen type ids in `:596-630` go to `.v2`. The test at `:2381` + forces it. + - Two public doors are added beside `Slug::validate_slug` (`:214-227`), on its + pattern: + - `OrgRepo::validate_org_repo(&self)` delegates to `org_repo_text` + (`:157-166`), through `Bounded::validate(self, "org_repo")`; + - `Label::validate_label(&self)` delegates to `label_text` (`:134-142`). + + Each delegates, so the grammar keeps one owner. + - `schemas/cultnet/`: rename the thirteen files, regenerate them, update + `index.json`, and update `README.md:41`. +- **Authority:** the leaf owns every grammar. The organ calls doors and + re-derives nothing. That is the precedent set by `validate_ref` and + `validate_slug`. +- **Verification:** + - builds: `cargo test -p epiphany-pipeline --lib`, with the new target + subdirectory given in §7. + - tests: + - `pipeline_published_schemas_match_derivation` pins the renamed and + regenerated files byte for byte. + - `every_kind_is_at_the_epochs_version` pins v2 throughout. + - A new test, `the_org_repo_and_label_doors_are_the_grammar`, runs these + inputs: + + | Door | Refuses | Accepts | + |---|---|---| + | `OrgRepo` | `"GameCult"`, `"/Repo"`, `"GameCult/"`, `"a/b/c"`, 201 bytes | `"GameCult/Epiphany"` | + | `Label` | `""`, `"a.b"`, 65 bytes, `"é"` | `"cut-10"` | + - A new test, `question_and_ruling_carry_a_title`, round-trips each kind + through `prepare`/`decode`. Under Q-RS1 A or B it also asserts that an + empty title refuses. + - mutations (`tools/eureka-readside-leaf-mutations.psd1`, M0 control): + + | Entry | Kind | Mutation | + |---|---|---| + | `L1` | revert | `validate_org_repo` returns `Ok(())` | + | `L1L` | loosening, a function of the input | `org_repo_text` accepts when `contains('/')` | + | `L2` | revert | `validate_label` returns `Ok(())` | + | `L2L` | loosening, the wrong grammar | `validate_label` delegates to `dotted_text`; the fixture `"a.b"` is a valid slug and an invalid label | + | `L3` | revert, Q-RS1 A/B only | the title's non-empty check is removed | + - negative: `git grep -n 'epiphany\.pipeline\.[a-z_]*\.v1' -- epiphany-pipeline schemas` + is empty. Checked for collisions: `notes/` and the `runtime_spine` + stand-in are outside that scope, deliberately. +- **Subtraction estimate:** about +45 source lines (4 fields, 2 doors with docs) + and about +60 test lines. The schema files churn by rename, plus about 2 fields + each on two files. No dependency changes. + +### Cut RS-2. Huginn: delete `open_items`, `history`, and the admission window + +- **Repo/branch:** Huginn `eureka/memory-organ`, after the residue batch lands. + No pin move. Subtraction only, so that Soul can falsify it on its own. +- **First:** re-take every anchor below against the post-residue HEAD. +- **Deletes (anchors at `4f7e3b5`):** + - **`query.rs`:** + + | Lines | What is deleted | Size | + |---|---|---| + | `:107-117` | `PipelineOpenItems` | 11 | + | `:119-125` | `HistoryScope` | 7 | + | `:352-391` | `open_items` | 40 | + | `:393-435` | `history` | 43 | + | `:89-92` | the fields `admitted_after`/`admitted_before` | | + | `:279-288` | their filters | | + | `:570-663` | tests: both history tests | about 93 | + | `:942-1003` | tests: the open-items test | about 61 | + | in `:765-886` | tests: the window lines of `query_filters_each_select_by_one_field` | | + - **`wire.rs`:** + - `:48-49` (the variants), `:60-61` and `:72-73` (the arms) and `:87-88` + (the response variants); + - the test fixtures at `:155-159` and `:172-173`; + - the operation names asserted at `:184`. + - **`lib.rs:41`:** the re-exports of `HistoryScope` and `PipelineOpenItems`. + - **`daemon.rs`:** + - `:92-99`, the two arms; + - the tests that exercise them only: + - `a_refusal_open_items_raised_is_the_answer_the_dispatch_returns_whole` (`:624`); + - `open_items_refuses_on_a_fault_inside_the_requested_campaign` (`:660`); + - the `OpenItems`/`History` lines in the instance-gate tests at + `:401-416`, `:455-459`, `:518-522` and `:581`. + + Each instance-gate test keeps its `query` and `view` cases. + - **`serve.rs`:** + - `:591` (`"m-o"`); + - `:855` (`"open_items"` as the long operation name). + + N4 needs two operation names of different lengths. **Replace `open_items` + with `whoami`, six bytes against `view`'s four.** Do not delete the case: + the rule that the gate is exercised across operations survives. + - **`README.md:51`:** the clause "a campaign's open work, and a subject's or a + repo's history". + - **Entries:** + - cut9: V9, V10, V11, V12, V13, V13L, V14, V14L, V15, V22 and V22L (11); + - cut10: D19, D19L, D23, D23L, S1 and S1d (6). + + D13 and D13L anchor on the deleted `operation()` arms: re-anchor them on the + surviving arms (`"view"`/`"query"`), with the same rule. + + Each deleted entry's rule is either **ruled away** (the window, R-3) or + **re-pinned in RS-3** under the selection spelling. That second group is + R-B, D6, V9-V15, D19 and D23: RS-3 re-authors them by name. +- **Keeps:** + - `docs.rs` `resolutions_of` (`:97-99`) and `assignments_of` (`:104-106`). + Admission's sequence rules still read both, through `latest_*`, + `derived_*` and `stewardships_of`. + - `semantic` and its refusal, as-is: Cut 11 owns "the `semantic` branch of + `query`" (Epiphany map `:5321`). +- **Authority:** no ownership moves. Two public operations stop existing. +- **Verification:** + - builds: `cargo test -p huginn-mind -p huginn-daemon`. + - negative, all over `crates` and `README.md`: each of the following + patterns is empty, and each has been checked for collisions: + + | Pattern | Checked against | + |---|---| + | `open_items`, `OpenItems`, `PipelineOpenItems` | — | + | `HistoryScope` | — | + | `fn history\b` | prose uses "history" in comments | + | `admitted_after`, `admitted_before` | — | + - mutations: cut9 (27 remaining) and cut10 (65 remaining) rerun, with every + M0 green. +- **Subtraction estimate:** about −110 source lines and about −330 test lines. + 17 entries are removed and 2 re-anchored. No dependency or format changes. + +### Cut RS-1. Huginn: the receipt ordinal, receipt v2, and the epoch gate first + +- **Repo/branch:** Huginn, after RS-2. No pin move. The leaf is still at + epoch v1 here. Its epoch arrives with BP-2, before any mind is deployed: + Cut 14 is unbuilt. The one store version any deployed mind ever carries is + therefore (leaf epoch v2, receipt v2), which is the ruling's single migration. +- **Deletes first:** none. +- **Changes (anchors at `4f7e3b5`):** + - **`receipt.rs`:** + - `:27` and `:95` change `v1 → v2`. `HuginnCommitReceipt` (`:94-111`) + gains `#[cultcache(key = 7)] pub ordinal: u64`. + - `candidate` (`:160-178`) takes `ordinal: u64`. + - `digest` (`:116-120`) is **unchanged**: the ordinal is not digested. The + header doc (`:10-14`) names the ordinal beside `committed_at` and + `provenance`. + - `validate` (`:122-142`) refuses `ordinal == 0`. + - New: `pub(crate) fn head(mind) -> Result` decodes the + receipts and requires their ordinals to be exactly `{1..=N}`. Anything + else is `Unavailable { detail: "receipt ordinals are not 1..=N: …" }`. + Admission calls it for `head + 1`. Nothing else computes an ordinal. + - **`admission.rs`:** the one `candidate(…)` call passes `head(self)? + 1`. + - **`query.rs`:** `AdmissionFacts` (`:36-41`) gains `ordinal: u64`, and + `AdmissionIndex::build` (`:134-150`) fills it. **The order is not switched + here.** `ordered` (`:307-311`) is deleted in RS-3, not rewritten twice. + - **`mind.rs:137-138`:** swap to `refuse_foreign_epoch`, then + `refuse_foreign_types`. The doc at `:129-133` is updated to match. + - **`wire.rs`:** regenerate the response schema, because `AdmissionFacts` + changes. +- **Authority map:** + - **Owner:** `receipt.rs` owns the ordinal, meaning both its assignment and + its validity. + - **Inputs:** the image's receipts. + - **Outputs:** `head`; and `ordinal` on each receipt and on `AdmissionFacts`. + - **Derived state:** `AdmissionFacts.ordinal`, which is display-only until + RS-3 makes it the order. + - **Forbidden writers:** no code outside `receipt.rs` counts receipts to + produce an ordinal. `MindStatus.receipts` (`wire.rs:106-114`) counts them + for display and is not an ordinal. + - **Shared paths:** `admit` and `admit_prepared`, and so the daemon's sink + and Cut 12's import, all end in the one `candidate` call. + - **Deletion line:** none. The ordinal is new. +- **Verification (tests; each rule has a revert and a loosening that fail):** + + | Test | Rule it pins | Mutants that must die | + |---|---|---| + | `ordinals_are_admission_order_not_clock_or_id_order` | ordinal is `head + 1` at admission. The fixture has **three batches admitted at the same `now`**, whose receipt ids sort opposite to admission order; the probe's real store shows 31 receipts in 5 seconds, so this is the live case | revert `ordinal: 1` for every receipt (dies on the density check); **function-of-input loosening:** the ordinal is the rank of `(committed_at, receipt_id)` among receipts, which the same-second fixture kills; off-by-one `head` | + | `an_exact_replay_keeps_its_ordinal` | the ordinal is not digested (A9) | the ordinal inside the digest makes the replay commit again or refuse, and must die | + | `a_chain_that_is_not_dense_refuses_admission_and_reads_alike` | density is validated wherever the ordinal is read. The fixture plants a duplicate and a gap through `MemoryStore` | `head` returns `len()` without checking; the check runs only in admission | + | `a_v1_receipt_store_is_refused_at_open` | receipt v2 is a new type. The fixture plants a v1-shaped 7-slot receipt under `huginn.mind_commit_receipt.v1` | adding the field with `#[serde(default)]` and keeping the v1 id; F3 shows that this decodes to 0 silently | + | `a_store_written_at_the_previous_epoch_is_refused_by_the_epoch_gate` | epoch first. The fixture has an epoch record at `epoch.v0` and type ids at `.v0` | swapping the gates back makes it `ForeignStore`, which must die. The existing eight cases in `mind.rs:345-413` stay green unchanged, and that is part of the check | + + - Entries: `tools/eureka-readside-mutations.psd1`, R1-R5 with L variants + and an M0. + - Cut8's A9 replay entries rerun, because `candidate`'s signature changed. +- **Subtraction estimate:** about +45 source lines and +160 test lines. No + dependency changes. **Formats:** one moves, receipt v1 → v2. + +### Cut RS-3. Huginn: the consumer + +- **Repo/branch:** Huginn, after BP-2 (pins at X, leaf at RS-L's tip) and after + RS-1. Depends on Q-RS2, and on P-1 and P-2 being in X. +- **Commits, in order.** Each builds and passes on its own. + 1. **The edge list moves to its owner (ownership only).** It moves from + `admission.rs:175-178` (`kind_of_id`) and `:223-285` + (`outcome_references`, `references`) into + `docs.rs::citations(document) -> Vec<(CitationRole, PipelineRef)>` and + `CitationRole`: the fifteen roles of the earlier read-side spec's D3 (list + below). + - Admission's `references` becomes a map over `citations`, carrying the + `Missing` classification (`:203-207`) and the hand-off source-side + filter. It has the same refusals in the same order. + - Cut8's `H17` entry (`refs.extend(outcome_references(…))`) is + re-anchored into `docs.rs`, with the rule unchanged. + - Verified by: the cut8 suite 67 of 67 (plus H17 re-anchored) and the + full admission test set, unchanged. + 2. **Deletes first:** + - `PipelineQuery` (`query.rs:77-97`) and `PipelineQueryPage` (`:101-105`); + - `QUERY_LIMIT_MAX` (`:32`), which is replaced by + `cultnet_rs::LIMIT_MAX`, the substrate's clamp; + - `Reader::matches` (`:249-290`) and `ordered` (`:307-311`); + - `root_and_local`'s role as a filter. It survives as the row's value + source for `root` and `cut`; + - `repo_matches` (`:178-195`), which moves to the row's `repo` values; + - `lib.rs:40-43`, the re-exports of all of the above. + 3. **The consumer:** `rows.rs` (new), `query.rs` rewritten around it, + `wire.rs`, `refusal.rs`, and `daemon.rs:88-91`. Details below. + 4. **Tests and the readside entries.** +- **New dependency:** `huginn-mind` gains + `cultnet-rs = { git = …, rev = X, package = "cultnet-rs" }` (Q-RS2 A). +- **Vocabulary: the organ's closed lists.** + - **Schemas.** These are the thirteen leaf type ids. `schemas` entries are + matched exactly: Rust carries no alias matching (`selection.rs:438-440`). + - **Aliases.** Each row answers `values(alias)` with strings. Every value is + checked at the organ's door against the domain in the last column. + + | Alias | Declared on | Value | Door (domain) | + |---|---|---|---| + | `root` | all | the key's root segment. This is V18/V18L's rule, renamed from `campaign` because a stewardship's root is an instance and a resolution's is its subject's | `Slug::validate_slug` | + | `in_force` | all | `"true"`/`"false"` | exactly those two | + | `faculty` | all | `Faculty` variant name | the closed set | + | `repo` | campaign (each listed repo), cut_spec, cut_report, follow_up, stewardship, hand_off, resolution (read through `base`: V17/V17L) | `OrgRepo` text | `OrgRepo::validate_org_repo` (RS-L) | + | `cut` | cut_spec, cut_report, verdict, finding, resolution (through `base`) | the label in `cut-