# 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, 2026-09-29 (Self): **RS-L, RS-2 and RS-1 are landed and closed** (2026-09-22; the record is in `eureka-pipeline-state-cut.md`, "RS-L closes", "RS-2 and RS-1 are closed"). **P-1 and P-2** were folded into selection's R-G and R-M, and selection Cut 1 merged to CultLib `main` at `dbee044` (2026-09-23); the RS-3 brief confirms both are in that merge. **Both operator questions in §9 are ruled** (2026-09-22: Q-RS1 B, one non-empty `Title`; Q-RS2 A, `huginn-mind` links `cultnet-rs`). **Next:** BP-2 once BP-1 lands (BP-1 in Hands 2026-09-29), then **RS-3**. The Pins table below is as of 2026-09-22 and stale; each brief re-takes its anchors. **2026-09-29: BP-2 and RS-3 landed and closed.** BP-2 is at Huginn `e770fb9` and leaf `4891f2e5`, with pins at CultLib `6251cbc`. RS-3 is at `511afa2`, `eb57bdb` and `9673ab8`: huginn-mind 91 tests, daemon 19, and nothing reimplemented. Soul closed it; in-force, the snapshot restriction and the single owner of repo identity all held under probe. Recorded: - one orphan record takes every read offline, failing closed on a corruption the single writer cannot produce; - a cursor that dies with a restart is refused with the substrate's message about a "selection digest". That is CultLib's wording to fix; - the selection `in_force=true` + `cited answers exists=false` misses a question reopened by a withdrawal, because the withdrawn answer still cites it. Cut 15's recipes must say so; - header pages are bounded per header, not per page, and `ResponseTooLarge` stays reachable. **Next: BP-3**, in Hands. *As written 2026-09-22:* 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. **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-