From b8bba0950ab08ee6965dfa86d7a41b5125bdeffc Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:09:33 +0200 Subject: [PATCH] Add the mind's design record from Epiphany notes (docs/history) Seven notes copied verbatim from GameCult/Epiphany ef956865 notes/, each with a one-line provenance comment on top: huginn-organ-cut and the eureka-* and postmortem notes. Prose only. Co-Authored-By: Claude Opus 5.5 --- docs/history/eureka-body-plane-cut.md | 902 +++ docs/history/eureka-pipeline-state-cut.md | 6465 +++++++++++++++++ docs/history/eureka-pipeline-state-target.md | 171 + docs/history/eureka-query-prior-art.md | 524 ++ docs/history/eureka-read-side-cut.md | 1246 ++++ docs/history/huginn-organ-cut.md | 659 ++ .../history/postmortem-eureka-schema-phase.md | 226 + 7 files changed, 10193 insertions(+) create mode 100644 docs/history/eureka-body-plane-cut.md create mode 100644 docs/history/eureka-pipeline-state-cut.md create mode 100644 docs/history/eureka-pipeline-state-target.md create mode 100644 docs/history/eureka-query-prior-art.md create mode 100644 docs/history/eureka-read-side-cut.md create mode 100644 docs/history/huginn-organ-cut.md create mode 100644 docs/history/postmortem-eureka-schema-phase.md diff --git a/docs/history/eureka-body-plane-cut.md b/docs/history/eureka-body-plane-cut.md new file mode 100644 index 0000000..1731495 --- /dev/null +++ b/docs/history/eureka-body-plane-cut.md @@ -0,0 +1,902 @@ + +# The body plane for oversize answers: cut map + +Date: 2026-09-22. Imagination output (Opus). Nothing here is committed and no +repository was edited. Self places and commits this file; the natural homes +are a section of `F:\Projects\Epiphany\notes\eureka-pipeline-state-cut.md` +(the campaign that owns the ruling) with the CultLib half copied or linked +into `F:\Projects\CultLib\docs\`, because the cut spans the organ and +CultMesh. + +Status: cut map for three cuts, **BP-1** (CultLib: the content plane in the +Rust runtime), **BP-2** (the pin move that lets Huginn see it) and **BP-3** +(Huginn: an oversize answer is deferred to the body plane). + +**2026-09-29 (Self):** both questions in section 8 are ruled (Q-BP1 A, +Q-BP2 A, 2026-09-22). Selection Cut 1 merged to CultLib `main` at `dbee044`, +so section 6's collision is gone. **BP-1 is in Hands** (Sonnet) on CultLib +`cultmesh/rust-content-plane`, based on `268e0ef`, with two overrides to +section 3: no committed mutation entries (the operator retired hand-written +harnesses on 2026-09-22; each rule's test is shown red once by a scratch +mutant instead), and verification on Yggdrasil through the stopgap rather +than on the workstation. The Pins table below is as of 2026-09-22. + +**BP-1 landed** (Sonnet, 2026-09-29) on `cultmesh/rust-content-plane` at +`29c3e4c`, `11485fb` and `8a66b1d`. `cultnet-rs` went from 264 to 279 tests, +plus 3 C# vector tests, and all 22 listed mutants went red. Size: +486 source +lines against ~330 estimated, and +778 test lines against ~350. +**Soul (Opus) will merge it after a fix batch, now in Hands:** +- F1: a flood of zero-size chunks makes the client send unbounded requests; +- F2: malformed requests are refused at decode, so a Rust server answers + nothing where the reference answers `found:false`. **This was the map's + defect**: section 3 ordered decode-time request validation, which + contradicts "a failure is an answer, never silence". Refusals belong to the + answer function; +- F3: `BTreeMap` metadata breaks byte identity for keys that are not sorted; +- F4: hashes are normalised only after requests have gone out; +- F5: one error spelling differs from the reference; +- F6: 8 surviving mutants. +Held: byte identity in both directions beyond the fixture, verification, +serving, validation order, and one path. The empty-body deviation (one +zero-length chunk) is the reference's behaviour, so the spec was wrong there too. + +**The ruling this map is written under** (operator, 2026-09-17, recorded in +the Epiphany map at about line 368): an answer too large for the control +plane travels over CultMesh's content/body transfer. The control plane +carries a summary and a reference to the body. Raising the window +(1,228,800 bytes) and bounding documents smaller were both rejected. **The +typed oversize refusal stays as the backstop.** The ruling says the cut "spans +the organ and CultMesh", and nothing about it was in Cut 10 except the refusal. + +## Pins + +| Repo | Branch | HEAD | State | +|---|---|---|---| +| CultLib | `main` (checked out; the operator's shell is on `main`, and the gitStatus snapshot's `cultnet/selection-cut1` is the locked worktree `.claude/worktrees/agent-af580008561975a4c`, also at `b3d9cf7`) | `b3d9cf7` | Dirty in `native/GameCult.Mesh.Quic.Native/*` and `scripts/mutate-cultmesh.mjs` from the QUIC campaign. **Not touched.** `git diff --stat a0813c6 b3d9cf7 -- packages/cultcache-rs packages/cultnet-rs packages/cultmesh-rs` is **empty**, so every Rust anchor below holds at Huginn's pin as well. | +| CultLib selection Cut 1 | `cultnet/selection-cut1` | `b3d9cf7` + uncommitted work in the locked worktree | In Hands (another agent). The collision is in section 6. | +| Huginn | `eureka/memory-organ` | `202e5e3` | Clean. Cut 10 and its two fix batches have landed. | +| Epiphany | `codex/eureka-pipeline-state` | `2876a827` | Map read only. The leaf `epiphany-pipeline` is pinned by Huginn at `5cda0886` and pins `cultcache-rs` at `a0813c6`. | + +## 0b. Model page: identity, lifecycle and authority + +This table was written before any cut was mapped. Every persistent or +long-lived kind the cut touches has a row, and no cell is empty. + +| Kind | Identity (what names it) | Lifecycle (what happens to it over time) | Authority (who decides) | +|---|---|---|---| +| **Deferred body**: the encoded bytes of one `HuginnMindResponse` that did not fit one send | The SHA-256 of those bytes, lowercase hex: the manifest's `contentHash`. Two identical answers are one body. | Created when an answer's encoded envelope exceeds the window and the body is within the deferral bound. A second deferral of the same bytes refreshes it; it is never duplicated. Every chunk served also refreshes it (sliding expiry). It expires when it has been untouched for the TTL, and it is evicted least-recently-touched first when the retained total exceeds the budget. It is lost when the process exits, and the client asks again. It is never written to the mind, a file or any `.cc` store. | The daemon's serve module (`huginn-daemon`) decides to defer and holds the retention store. The mind decides what the answer *is* and knows nothing about deferral. | +| **Chunk** | The SHA-256 of the chunk's bytes. Its record key is `mesh:cdn:chunk:`, which is the C# reference's spelling (`CultMeshCdnArtifactChunk.CreateRecordKey`, `CultMeshCdn.cs:120`). | It lives exactly as long as some retained body references it. A chunk shared by two bodies is stored once and stays until both are gone. | Chunking (the boundaries and the hash) belongs to `cultnet-rs` `pack_content`. Retention belongs to the daemon's store. | +| **Reference (manifest)** | `gamecult.mesh.cdn_artifact_manifest.v1`, the reference's type, reused and not reinvented. `contentHash` identifies the body; `artifactId` and the other fields describe it and never identify it. | Transient: minted with the body, carried once inside the deferred response, and never stored by the daemon or the client. It stays valid while the body is retained. After that a chunk request answers `found: false` and the client re-asks the operation. | `cultnet-rs` owns the shape and the validation rules, with parity to `CultMeshCdn.ValidateManifestShape` (`CultMeshCdn.cs:499-526`). The daemon fills it in. | +| **Deferred response**: the control-plane answer that stands in for an oversize one | Correlated by the operation envelope's `messageId`, exactly like every other answer. | One per request, sent once, and never deferred itself. It always fits: at the deferral bound it is about 41 KB, far under the window. | Only the daemon's serve module produces it. `Mind` never does. | +| **Content chunk wire messages** `cultmesh.content_chunk_request.v1` / `.response.v1` | Their `schemaVersion` strings, both already defined by the reference (`CultNetSchemaMessages.cs:153,157`, classes `:1414-1446`). | Stateless: one request, one response. | The C# reference owns the spelling. `cultnet-rs` gains it with byte identity, pinned by vectors in both directions. | +| **Vector fixtures** `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` | File path plus the `label` of each vector. | Committed. Each writer regenerates its own file under `CULTNET_WRITE_VECTORS=1`. The file is expected to grow and is not pinned as final. | Each runtime owns its written file. Both runtimes' tests read both files. | +| **Mind documents and receipts** | Unchanged: leaf keys and receipt ids. | Unchanged. **No body-plane state enters the mind.** | Unchanged: `huginn-mind` admission. | + +Forks this page surfaced, which are sections 8's Q-BP1 and Q-BP2: + +- **Authority over the physical path.** CultMesh's own doctrine forbids + schema messages carrying bulk bodies and names the RUDP content path as + legacy. That is Q-BP1. +- **What the summary is.** The ruling says "a summary". Whether that means the + reference's own facts or a per-operation typed summary decides whether + `huginn-mind` grows new types. That is Q-BP2. + +**Identity cannot be the request.** Re-deriving a body per chunk request from +the mind would need no retention at all. But a chunk request names only a +hash, so the daemon could not know which answer to recompute without a +Huginn-specific request shape, and that would give up wire reuse with the +reference. Rejected for that reason, not asked. + +## 1. Body facts + +Each fact is backed by a source read at the pins above or by a probe that +ran code. The probes are in the scratchpad and nothing was committed: + +- `bodyplane-cs/`: a net10.0 console against the built reference DLLs + `bin/GameCult.{Networking,Mesh}/Debug/netstandard2.1/*.dll`, dated + 2026-09-16. The sources it depends on last moved at `f622ee1` (2026-08-18), + so the DLLs match `b3d9cf7`. It emitted real reference bytes to + `bodyplane-cs/out/`. +- `bodyplane-rs/`: a crate against `packages/cultnet-rs` by path, release + build. `src/main.rs` is the transport probe and `src/bin/parity.rs` the + byte-parity probe. +- `pinprobe/`: `cargo tree -d` over mixed CultLib git revisions. + +### F1. Only the C# reference implements body or content transfer. The Rust client Huginn uses has no path today. + +- **Source.** Across `packages/` there is no occurrence of + `content_chunk_request`, `body_read_request`, `cdn_artifact_manifest`, + `ContentChunk*` or `BodyRead*`, and no `cdn`, `chunkHash` or + `artifact_manifest` in any `.rs`, `.ts`, `.py` or `.kt` source. + `cultnet-rs` does not contain the word "body". What the other runtimes have + is the *local* stream-body negotiation enum (`SharedMemory`, + `CultCachePage`, `InlineBytes`, and so on): `cultmesh-rs/src/lib.rs:191-200`, + `cultnet-py/.../cultmesh_contracts.py:434`, and in `cultmesh-ts` and + `cultmesh-kotlin`. That is same-machine body mapping, not network transfer. +- **Probe A (decode).** `cultnet_rs::decode_cultnet_message_from_slice` over + bytes the C# reference emitted: + - `content_chunk_request` gives *unknown variant + `cultmesh.content_chunk_request.v1`*, because the closed enum + (`contracts.rs:252-392`) lists 20 `cultnet.*` variants. + - `content_chunk_response`, `body_read_response` and a real 256 KiB chunk + response give *invalid type: byte array, expected any valid JSON value*. + The non-raw decode path converts through `serde_json::Value` + (`contracts.rs:404-411`), which cannot hold MessagePack `bin`. So adding + the variants alone is not enough: both must ride the raw path that + `document_put_raw` uses (`contracts.rs:771-854`). +- **Probe E (on the wire).** A Rust hub sent the reference's own 262,357-byte + chunk response raw on the `schema` channel. The transport delivered it, and + the Rust client refused it in `receive_schema_message_once` with the same + byte-array error. + +**Conclusion:** no Rust runtime can request, answer, decode or verify a body +or a content chunk today. Rust gets its first content-plane code in BP-1. + +### F2. The reference has two planes, and only one of them fits this class of content. + +- **The content plane.** It carries large immutable artifacts + (`src/GameCult.Mesh/docs/content-sessions.md:1-3`). The reference is a + `CultMeshCdnArtifactManifest` (`CultMeshCdn.cs:127-190`) listing + content-addressed chunks (default 4 MiB, `CultMeshCdnPackOptions` + `:44`). The chunk protocol is one bounded response per chunk + (`content-sessions.md:44-46`). The transfer owner + (`CultMeshContentTransferService`, `CultMeshContentTransfer.cs:115`) is the + only owner of chunk verification, whole-body SHA-256 verification, resume + and promotion. The preferred connector is TCP: a typed header, then raw + bytes outside the envelope (`transport-planes.md:22-26`). It is plaintext + and **restricted to explicit loopback development** + (`transport-planes.md:103-106`). The RUDP content server + (`CultMeshLegacyRudpContentServer`, `CultMeshContentSessions.cs:14-87`) is + "basement priority and exists for compatibility and parity measurement" + (`transport-planes.md:93-96`), and its connector is never installed by + default (`CultMeshContentSessions.cs:97-116`, priority 10,000). +- **The body plane proper.** It carries live, lease-bound generations of + mutable bodies: `CultMeshNetworkBodyStore` (`CultMeshBodySessions.cs:19-219`, + a 16 MiB default bound, 8 retained generations, HMAC capability tokens, + expiry by lease). It is served **whole in one response** + (`CultMeshBodyServer.HandleAsync`, `:241-272`, payload inline). There is no + chunking. +- **Why that decides it.** A pipeline answer is an immutable artifact, which + is the content plane's stated class. The body plane's single response meets + exactly the wall the ruling is routing around: probe B's whole-body send + failed with `RUDP reliable send queue is full`. The operator's phrase + "content/body transfer" covers both planes, and the source says which one + this is. **This map uses the content plane's chunk protocol and manifest.** + It is recorded as a reading of the source, not asked, because the body + plane cannot carry this answer on any transport Huginn has. + +### F3. What the wire carries. Decoded from real bytes and pinned at byte level. + +| Message | Reference encoding (probe, hex prefix) | Fields in order | +|---|---|---| +| `cultmesh.content_chunk_request.v1` | named map `0x85`, 169 bytes for the sample | `schemaVersion`, `messageId`, `chunkHash` (64 hex), `recordKey` (may be empty), `expectedSizeBytes` (int32) | +| `cultmesh.content_chunk_response.v1` | named map `0x87`, 179 bytes for a 4-byte payload | `schemaVersion`, `messageId`, `found`, `chunkHash`, `sizeBytes` (int32), `payload` (**MessagePack `bin`**), `error` (string, empty on success) | +| `gamecult.mesh.cdn_artifact_manifest.v1` | **array** `0x9A` (10 integer-keyed fields), 1,097 bytes for 6 chunks | `[artifactId, kind, version, contentHash, sizeBytes(int64), mimeType, createdAtUtc, chunks[[chunkHash, offset(int64), sizeBytes(int32), recordKey]], tags[], metadata{}]` | +| `cultmesh.body_read_response.v1` (not used) | named map `0x8B`, 11 fields | whole generation in `payload` | + +Measured: + +- A 256 KiB chunk response encodes to **262,357 bytes**, 213 bytes of + overhead, so four fit in Huginn's 1,228,800-byte window. +- The manifest for the real 1,315,551-byte case at 256 KiB chunks is 6 refs + and 1,097 bytes. +- **Probe parity:** + - A Rust `rmpv` map written in the reference's key order is **byte-identical** + to the reference's request (169 = 169) and response (179 = 179). + - A Rust tuple-shaped manifest decodes the reference's bytes and re-encodes + them **byte-identically**. + - A Rust re-pack of the same body with SHA-256 at 256 KiB reproduces every + chunk reference (hash, offset, size, record key) and the content hash. + - `rmp_serde::to_vec_named`, the encoder Huginn uses for responses, still + writes a tuple-shaped type as an array (`0x9A`). So the reference's array + form survives inside Huginn's named-map payload **only if** the Rust type + serialises as a tuple. + +### F4. Chunked delivery works on Huginn's exact transport. Pipelining past the window does not. + +Probes B, C and D used a hub configured exactly as Huginn's `serve::bind` +(`serve.rs:135-143`: fragment 1200, pending 1024, one settled session) and the +real 1,315,551-byte body, with chunks carried as base64 inside operation +responses, the only bytes-bearing message Rust has today. + +- **One reply carrying the whole body** (a 1,754,254-byte envelope): + `RUDP reliable send queue is full`, returned to the sender only. +- **Sequential request, then reply, one chunk at a time** at 256, 512 and 768 + KiB: delivered whole and SHA-equal, in 1.56 s, 1.54 s and 1.45 s on loopback + (about 0.85 MB/s, paced by the 32-packet send window, + `rudp.rs:34`). **Chunk size barely moves throughput.** +- **900 KiB** as base64 is a 1,228,983-byte envelope, which exceeds the window, + so the send fails. +- **Six 256 KiB chunks sent without waiting:** four accepted, two refused + immediately, and all four accepted ones were delivered. This is Cut 10's F2 + (pipelined reads) reproduced at chunk scale. The window is per session and + counts unacknowledged packets. + +**Consequence for the design:** the fetch is sequential, one chunk in +flight, at 256 KiB. With `bin` payloads, as opposed to this probe's base64, +one chunk reply is 262,357 bytes, which leaves room for a control-plane +answer on the same session. + +### F5. Moving one CultLib pin duplicates `cultcache-rs` even when its content is identical. + +`pinprobe` (`cultcache-rs` at `a0813c6` beside `cultnet-rs` at `b3d9cf7`, +whose `cultcache-rs` content is byte-identical) reports **two** +`cultcache-rs v0.2.0` and two `cultcache-rs-derive` in `cargo tree -d`, +because Cargo keys a git source by revision. + +Huginn has three pins at `a0813c6`: `huginn-mind`'s `cultcache-rs`, +`huginn-daemon`'s `cultnet-rs`, and the Epiphany leaf `epiphany-pipeline`'s +`cultcache-rs` (at leaf `5cda0886`). All three must move together, or "one +crate pins one revision" breaks (see `huginn-daemon/Cargo.toml` and the Cut 8 +build check `cargo tree -p huginn-mind -e normal -d` with zero duplicates). +That is BP-2. + +### F6. Huginn today. + +The anchors in `huginn-daemon/src/serve.rs` at `202e5e3`: + +- The module doc's stated limit is at `:10-17`, and the pipelined-read + paragraphs are at `:32-44`. +- `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and `MAX_RESPONSE_BYTES` + are at `:124-131`, and `bind` is at `:135-143`. +- `answer` (`:165-194`) routes `OperationRequest` and `SchemaCatalogRequest`, + and answers every other family with `Error` (`:188-192`). +- **`within_window` (`:217-251`) is the single owner of the size decision + today.** It encodes the envelope, admits it at `<= MAX_RESPONSE_BYTES`, and + otherwise answers `Refused(ResponseTooLarge { bytes, limit })`. +- `run` (`:257-305`) reads the clock once per frame, at `:294`. + +Elsewhere in the organ: + +- `MindRefusal::ResponseTooLarge` is at `huginn-mind/src/refusal.rs:57-65`, and + its doc names the daemon as owner. +- `HuginnMindResponse` is at `huginn-mind/src/wire.rs:81-90`, and `status` is + at `:97-102`. +- The published schema is `schemas/cultnet/huginn.mind_response.v1.schema.json`, + pinned byte for byte by `published_wire_schemas_match_derivation` + (`wire.rs:210-216`). +- The tests that pin the gate are + `an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client` + (`serve.rs:548-656`) and + `the_gates_boundary_is_the_transports_boundary_byte_for_byte` + (`serve.rs:749-777`). +- The mutation entries D20, D20L through D20L5, D21 and D21L are in + `tools/eureka-cut10-mutations.psd1:250-330`. +- `huginn-mind` has **no** `cultnet-rs` dependency today. The selection + consumer cut adds one, because `Query { instance, selection }` carries the + substrate's type (`docs/cultnet-selection-cut.md` §12). +- `eureka-state/src/lib.rs` is empty. Cut 13 has not been built. + +## 2. The cut order + +``` +[selection Cut 1, Rust commit 3 lands on CultLib main] (in Hands now, not this map) + │ + ▼ +BP-1 CultLib the content plane in cultnet-rs (wire, manifest, pack, answer, verified fetch) + vectors both ways + │ + ▼ +BP-2 Epiphany leaf + Huginn one pin move to a CultLib rev carrying selection Cut 1 and BP-1 + │ + ├──► [Huginn selection consumer cut] (Epiphany map, unnumbered, not this map) + ▼ +BP-3 Huginn an oversize answer is deferred to the body plane; the refusal becomes the backstop + │ + ▼ +Cut 11 / Cut 13 (Cut 13's spec gains: resolve a Deferred answer) +``` + +BP-3 comes **after** the Huginn selection consumer cut by default, for two +reasons. That cut already adds `cultnet-rs` to `huginn-mind`, which BP-3's +response variant needs. It also deletes `open_items` and `history`, so BP-3 +tests fewer read operations. If the operator orders BP-3 first, BP-3 adds +that dependency itself (section 7, BP-3 "Adds") and nothing else changes. + +## 3. Cut BP-1. The content plane in `cultnet-rs` + +- **Repo and branch:** CultLib, `cultmesh/rust-content-plane`, from `main` + **after selection Cut 1's Rust commit (its commit 3) has landed on `main`**. + It depends on nothing else. See section 6 for why it cannot branch now. +- **First:** + - Record `packages/cultnet-rs` baseline tests (51 in `tests/cultnet.rs` + and 7 in `tests/rudp_server_hub.rs` at `b3d9cf7`; recount at the actual + base). + - Record the target-dir path count and size. + - Capture the reference's bytes for every vector with the C# writer + before writing any Rust. +- **Deletes first:** none. This cut fills an absence. No Rust code carries a + body today, so nothing is stood beside, and there is no compensator to cut. + This is stated rather than padded. +- **Keeps (untouched):** + - Every C# source file. The reference does not change: its chunk classes, + serializer arms (`CultNetSchemaMessageSerialization.cs:79-80`), CDN pack + and validation, and servers stay as they are. + - `CultNetDatabaseSubscribeMessage` and its `bodyIds`/`supportedBodyTransports`. + - `CultMeshBodyDemand.cs` and `CultMesh.cs`. + - `contracts/cultnet/*.schema.json` and `CultNetSchemaRegistry.cs`. + - `cultmesh-rs` (Huginn does not depend on it, and this cut does not make it). + - `rudp.rs` (the transport carries these as ordinary schema frames). +- **Adds:** + +| Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve | +|---|---|---|---|---| +| `contracts.rs`: `CultNetMessage::ContentChunkRequest { message_id, chunk_hash, record_key, expected_size_bytes: i32 }` and `ContentChunkResponse { message_id, found, chunk_hash, size_bytes: i32, payload: Vec, error }`, renamed to the reference's `schemaVersion`s, **both on the raw path** | `cultnet-rs` | Huginn's daemon (BP-3), `fetch_content` | **byte identity with the reference in both directions** | The closed enum cannot carry them (F1). The JSON-value path cannot carry `bin` (F1). | +| `src/content.rs`: `CultMeshCdnArtifactManifest` and `CultMeshCdnChunkRef` (the reference's names; named Rust fields; serde `into`/`from` a private tuple mirror, so every encoder writes the reference's array, F3) | `cultnet-rs` | BP-3's deferred response, the fetch | one reference vocabulary across runtimes; the array form survives `to_vec_named` | A Huginn-local reference type would be a second vocabulary for one thing | +| `normalize_hash` (mirrors `CultMeshCdn.NormalizeHash`, `CultMeshCdn.cs:549-560`: an optional case-insensitive `sha256:` prefix is stripped, then trimmed, then lowercased; empty is refused) | `cultnet-rs` | answer, fetch | one hash spelling | — | +| `pack_content(artifact_id, kind, version, mime_type, created_at_utc, bytes, chunk_size) -> (CultMeshCdnArtifactManifest, Vec)` (mirrors `CultMeshCdn.PackArtifact`, `:232-310`) | `cultnet-rs` | BP-3's store | **the same bytes and options produce the reference's manifest byte for byte** | — | +| `validate_manifest` (mirrors `ValidateManifestShape`, `:499-526`, **including its sort by offset before the contiguity check**) and a caller-supplied `max_bytes` refused **before any fetch** | `cultnet-rs` | `fetch_content` | a hostile or corrupt manifest never makes a client allocate or ask | — | +| `answer_content_chunk_request(request, lookup: impl Fn(&str) -> Option<&[u8]>) -> CultNetMessage` (mirrors `CultMeshLegacyRudpContentServer.HandleAsync`, `CultMeshContentSessions.cs:43-86`, **in its validation order**: message id, hash normalisation, record-key agreement with `mesh:cdn:chunk:`, lookup, size and hash of what is served; a failure is `found: false`, an empty payload, and `error` = `: ` in the reference's spelling) | `cultnet-rs` | BP-3's `serve::answer` | a server never serves bytes whose hash is not the requested one, and never answers a failure as silence | Rust has no `ICultNetSchemaServer`, so a pure function is the whole server | +| `fetch_content(manifest, max_bytes, ask: impl FnMut(CultNetMessage) -> Result) -> Result>`: sequential, one chunk in flight, in manifest-offset order; verifies each response's correlation id, `found`, size and SHA-256 against the reference, then the whole body's size and SHA-256 against `contentHash` | `cultnet-rs` | Huginn's tests now, `eureka-state` (Cut 13) later | **one owner of verification on the receiving side**, as `CultMeshContentTransferService` is in the reference; no caller re-derives a check | The reference's transfer service is durable and file-promoting, and a C# owner does not serve Rust. This is its in-memory subset, with no resume, no failover and no files | +| `lib.rs:21-39`: `mod content; pub use content::*;` | `cultnet-rs` | — | — | — | +| `tests/content.rs` (new) | `cultnet-rs` | CI | the rules below | — | +| `contracts/cultmesh/content-vectors.cs-written.json` and `content-vectors.rs-written.json`: request, response found, response not found, and manifest vectors. A **deterministic** body (`byte[i] = i % 251`, 1,315,551 bytes, the real oversize case) at 256 KiB. **The fixture must carry:** a hash given with an uppercase `SHA256:` prefix; a manifest whose chunk list is **out of offset order**, which the reference accepts; a final chunk shorter than the rest; a zero-length body (0 chunks); and a chunk response whose `chunkHash` differs from its payload's hash in the **last** hex digit only. Otherwise a loosening that compares prefixes cannot fail. | each runtime writes its own | `tests/content.rs`, the C# test below | **vectors the reference writes decode and re-encode identically in Rust, and the other way round** | — | +| `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` (**a new file**, not `NetworkingTests.cs`, which the selection cut is rewriting): writes the cs vectors under `CULTNET_WRITE_VECTORS=1`; asserts the committed cs vectors equal a fresh encode; decodes the rs-written vectors, asserts field equality and byte-identical re-encode; asserts `CultMeshCdn.PackArtifact` of the fixture body equals the fixture manifest | C# tests | CI | parity from the reference's side | — | +| Docs: `src/GameCult.Mesh/docs/transport-planes.md` cut line (`:93-101`) and `content-sessions.md` gain one paragraph each naming the Rust runtime's content path and its transport (the wording depends on **Q-BP1**); `docs/runtime-parity-scope.md` gets one row | docs | — | describe the live system | — | + +No new dependency: `sha2`, `serde_bytes` and `rmpv` are already in +`cultnet-rs/Cargo.toml:14-31`. No new package, binary, transport, store +format or JSON schema file (see section 5). + +- **Per-file changes** in `packages/cultnet-rs/src/contracts.rs` at `b3d9cf7`. + Re-anchor at the base, because selection Cut 1 moves this file. + - **`:252-392`**, the enum: two variants after `OperationResponse`. + `payload` is `#[serde(with = "serde_bytes")]`, although the raw path + never serialises it through serde. + - **`:398-418`**, `parse_cultnet_message`: both `schemaVersion`s join the + raw arm at `:405`. + - **`:420-436`**, `encode_cultnet_message_for_wire`: both join the raw arm + at `:427`. + - **`:458-643`**, `validate_message`: two arms. + - Request: `messageId` non-empty; `chunkHash` non-empty after + normalisation; `expectedSizeBytes >= 0`. + - Response: `messageId` non-empty. `found` implies + `payload.len() == sizeBytes` and an empty `error`. Not `found` implies + an empty payload. + - These are the reference's own invariants, as its constructors and + `HandleAsync` produce them. They are not new policy. + - **`:771-810`**, `parse_raw_cultnet_schema_message`: two arms, reading + `bin` via `as_slice` and ints via `as_i64` range-checked to `i32`. + - **`:812-854`**, `encode_raw_cultnet_schema_message`: two arms writing keys + **in the reference's declaration order** (F3), with the payload as + `rmpv::Value::Binary`. + - **`:964-1138`**, the `gamecult.networking.v0` path: unchanged. Its `_` + arms already refuse. A test pins that refusal. +- **Authority map:** + - **Owner:** `cultnet-rs::content` owns chunking, hashing, manifest shape, + manifest validation, answering a chunk request and verifying a fetched + body, for every Rust runtime. `contracts.rs` owns the two messages' + spelling. + - **Inputs:** bytes plus pack options (packing); a chunk request plus a + lookup (answering); a manifest, a size cap and a transport closure + (fetching). + - **Outputs:** a manifest and chunks; a chunk response message; verified + bytes or a typed error. + - **Derived state:** `recordKey` is derived from the hash and is never an + independent identity. `sizeBytes` and `contentHash` are derived from the + bytes. + - **Forbidden writers:** no consumer (Huginn, `eureka-state`) hashes, + chunks, validates a manifest or checks a chunk itself. No consumer + re-orders or retries chunks behind `fetch_content`. + - **Shared paths:** the daemon's answer and the client's fetch both call + `normalize_hash`. `pack_content` and `validate_manifest` share the one + contiguity rule. + - **Deletion line:** none. This is the first owner. +- **Verification:** + - **Builds** (PowerShell, `$env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'`): + `cargo test -p cultnet-rs --tests` from `packages/cultnet-rs`; then + `dotnet test tests/GameCult.Mesh.Tests --filter CultMeshContentVectorTests`. + - **Tests, each with the rule it pins and the mutants that must kill it** + (committed in `scripts/mutate-cultmesh.mjs`, CultLib's own runner, which + owes the Eureka harness contract by name, **once the QUIC campaign has + released that file**; see section 6): + - `reference_vectors_encode_byte_identically` pins byte identity of the + two messages and the manifest. + Mutants: + - swap two keys in `encode_raw`'s request arm (revert-shaped); + - write `payload` as a `str` or an array of ints (loosening); + - encode the manifest through a named-field struct, i.e. drop the tuple + mirror (loosening, killed because `to_vec_named` then writes a map). + - `rust_vectors_decode_in_the_reference` (C#) pins the direction the + reference reads. + Mutant: Rust writes `sizeBytes` as a `u64`, which MessagePack-CSharp + rejects into `int`. + - `a_chunk_response_carries_bin_through_the_raw_path` pins F1's failure + mode. + Mutant: remove the response from the raw arm at `:405`. It must fail + with the byte-array error. + - `normalize_hash_matches_the_reference` pins the reference's + normalisation. + Mutants: + - case-sensitive prefix strip; + - no lowercasing; + - no trim (each killed by the fixture's `SHA256:` uppercase vector). + - `a_manifest_out_of_offset_order_is_accepted_as_the_reference_accepts_it` + pins parity of the sort. + Mutant: drop the sort. It must fail, because the fixture's chunk list + is shuffled. + - `a_manifest_over_the_cap_is_refused_before_any_request` pins that the + cap precedes fetching. + Mutants: + - check the cap after the first chunk (the test counts closure calls, + expecting 0); + - compare `>` instead of `>=` at the boundary. + The cap's boundary vector is exactly `max_bytes` accepted and one byte + over refused. **At least one mutant must be a function of the input**: + `max_bytes * 2`. The probe sizes sit where doubling would admit them. + - `a_chunk_whose_hash_differs_in_the_last_digit_is_refused` pins whole-hash + comparison. + Mutants: + - compare the first 63 characters; + - compare lengths; + - compare prefixes. + All are killed only because the fixture pair differs in the last digit. + - `a_body_whose_chunks_verify_but_whole_hash_does_not_is_refused` pins the + whole-body check. + Mutant: skip the final `contentHash` comparison. The fixture substitutes + a manifest whose chunk refs are valid but whose `contentHash` is another + body's. + - `a_response_for_another_message_id_is_refused` pins correlation. + Mutant: skip the id check. + - `answer_refuses_a_record_key_that_disagrees_with_its_hash` pins parity + with `HandleAsync`'s third check. + Mutant: delete the record-key check. + - `answer_serves_found_false_with_the_reference_error_spelling` pins that a + failure is an answer. + Mutant: return `Error` instead. + - `gamecult_networking_contract_refuses_content_messages` pins that the + legacy contract does not grow them. + Mutant: add them to its encoder. + - A **no-op control** rewrites each target through the runner's I/O path + and must leave everything green. + - **Negative checks:** + - `git diff --stat main -- src/GameCult.Networking src/GameCult.Mesh/*.cs contracts/cultnet packages/cultmesh-rs packages/cultnet-rs/src/rudp.rs` + is empty. + - `rg -n "untagged" packages/cultnet-rs/src/content.rs` is empty. + - **Operator:** nothing to click. This cut's evidence is the two vector files. +- **Subtraction estimate:** removes 0 lines. Adds about 330: `content.rs` + about 190, `contracts.rs` about 90, `lib.rs` 2, and docs about 20. Tests add + about 260 Rust and 90 C#, plus two vector files. There are no dependencies, + targets or formats to remove or add. **The delta is net positive because it + buys an explicitly ruled capability with the smallest surface that carries + it: the reference's own wire and reference type, not a new one.** +- **Build budget:** + - Host and target are both the Windows workstation, debug. + - Rust: `cultnet-rs` library and tests only, in the warm codex target dir. + The probe's release build of the same crate by path added 945 files and + 0.32 GiB (12.31 to 12.63 GiB) cold. The expected debug delta is up to + +1,000 paths and about +0.4 GiB. Nothing is cleaned. + - C#: `GameCult.Mesh.Tests`, which rebuilds Networking, Mesh and Caching as + dependencies with no source change in them. + - No native build, no TypeScript, no Python, no workspace-wide build. + - C: had 258 GB free. + - The Linux target (Yggdrasil) is not exercised here. Nothing in this cut + is platform-specific, and Cut 14 builds on Yggdrasil. + +## 4. Cut BP-2. One pin move + +- **Repo and branch:** Epiphany `codex/eureka-pipeline-state` and Huginn + `eureka/memory-organ`. It depends on BP-1 **and** selection Cut 1 being on + CultLib `main`, pushed. +- **Why it is its own cut:** F5. Moving `cultnet-rs` alone duplicates + `cultcache-rs`. The selection consumer cut needs the same move, so **one** + move to one revision carrying both CultLib changes serves both Huginn cuts. + That saves a second leaf release and a second Huginn bump. Self schedules + it once, whichever Huginn cut runs first. +- **Changes:** + - Epiphany `epiphany-pipeline/Cargo.toml` `cultcache-rs` rev: from + `a0813c6…` to the new CultLib SHA. This is one commit. The leaf's own + tests must stay green. `cultcache-rs` content is unchanged across + `a0813c6..b3d9cf7`, so any change after that is itself a finding. + - Huginn: `crates/huginn-mind/Cargo.toml` `cultcache-rs` rev and + `epiphany-pipeline` rev to the new leaf SHA, and + `crates/huginn-daemon/Cargo.toml` `cultnet-rs` rev. This is one commit. +- **Verification:** + - `cargo tree -p huginn-daemon -e normal -d` shows zero `cultcache-rs` or + `cultnet-rs` duplicates. + - The full Huginn suite is green: 74 tests at `202e5e3`. + - The cut8, cut9 and cut10 mutation suites rerun clean. + - `rg "a0813c6" F:\Projects\Huginn\crates F:\Projects\Epiphany\epiphany-pipeline` + is empty. +- **Subtraction:** 0 or 0. Only four revision strings change. +- **Build budget:** Huginn workspace check and tests, and `epiphany-pipeline` + library tests. Each is a cold compile of three CultLib crates at a new + revision, about +300 to +600 paths. The same host as before. + +## 5. Cut BP-3. Huginn defers an oversize answer to the body plane + +- **Repo and branch:** Huginn `eureka/memory-organ`. It depends on BP-2 and, + by default, on the Huginn selection consumer cut (section 2). It needs + **Q-BP2** answered, and **Q-BP1** decides one paragraph of its module + documentation. +- **First:** rerun + `an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client` + and record the two sizes it prints. At `202e5e3` they are 1,315,551 bytes + for the wide view and 1,189,495 for the fitting one. They are the + before-and-after witness. +- **Deletes first:** + - `serve.rs:217-251` `within_window` is **deleted whole**. It is the one + owner of the size decision, and it is replaced by `deliver` (below), not + wrapped. A gate that refuses and a second gate that defers would be two + owners of one decision. + - `serve.rs:10-17`, the stated limit ("This is the current bound and not a + design target… expect the number to move"), is deleted. The number no + longer moves, because the window stops being the organ's delivery limit. + - In `tools/eureka-cut10-mutations.psd1:250-330`, entries D20, D20L, D20L2, + D20L3, D20L4, D20L5, D21 and D21L are **deleted from the cut10 suite and + re-authored in BP-3's suite** against the new spelling. Their `Old` + anchors name `within_window`'s lines, which no longer exist, so leaving + them would make the cut10 runner fail on anchor-not-found rather than on + a rule. +- **Keeps:** + - `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and + `MAX_RESPONSE_BYTES` (`:124-131`): the window is still what one send + carries. + - `MindRefusal::ResponseTooLarge { bytes, limit }` keeps its name and + shape; **its doc is re-scoped** (below). + - The pipelined-read paragraphs `:32-44`, unchanged and still true: + deferral does not fix F2. + - `encode_or_fail` (`:197-215`). + - The `Daemon` dispatch. `daemon.rs` is untouched: **the mind and the pure + dispatch know nothing about deferral.** +- **Adds:** + +| 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. 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 | — | +| `refusal.rs:57-65`, doc only: `ResponseTooLarge` is now "the answer exceeds the largest body the organ will deliver by any plane", `limit` is `MAX_DEFERRED_BODY_BYTES`, and `bytes` is the encoded answer (the payload, not the envelope). It is still the daemon's, and still never raised by the mind | `huginn-mind` | clients | **the backstop stays, per the ruling** | — | + +- **Authority map:** + - **Owner:** `huginn-daemon::serve::deliver` decides, per answer, between + delivering it whole, deferring it, or refusing it. `DeferredBodies` owns + retention. `cultnet-rs::content` owns chunking, hashing, the manifest and + chunk answering. + - **Inputs:** the encoded reply, the window constants, the deferral bound, + the store's state and the per-frame clock read (`serve.rs:294`, still the + crate's only one). + - **Outputs:** + - The reply, unchanged. + - `Deferred { manifest }` plus retained chunks. + - `Refused(ResponseTooLarge)`. + - `ContentChunkResponse` for each chunk request. + - **Derived state:** + - The manifest and the chunk hashes are derived from the payload bytes. + - The deferred body is **cache-only** (ephemeral transport state, like + the reference's `CultMeshNetworkBodyStore`, `CultMeshBodySessions.cs:15-18`, + "not a CultCache document store and not world truth"). + - The mind is not a writer or a reader of any of it. + - **Forbidden writers:** + - `Daemon::handle` and `Mind` never produce `Deferred` and never read the + window. + - No second gate: `within_window` is gone, and nothing else compares + against `MAX_RESPONSE_BYTES`. + - The client never re-derives a check: it calls `fetch_content`. + - No pagination or truncation behind the caller's back (the Cut 10 F1 + ruling still holds). + - **Shared paths:** every operation's reply passes through `deliver`, + including `Whoami` and `Admit`. There is no per-operation path to + deferral. Direct replies and chunk replies leave through the same + `hub.send_schema_message` at `:295`. + - **Deletion line:** `within_window` and the stated-limit paragraph are + deleted before `deliver` is added, and the cut10 entries that anchored + on them are removed in the same commit. +- **Verification:** + - **Builds** (PowerShell, the codex target dir): + - `cargo test -p huginn-mind --lib`; + - `cargo test -p huginn-daemon --lib`; + - `cargo check --workspace`; + - `cargo tree -p huginn-daemon -e normal -d`, which must show no + `cultcache-rs` or `cultnet-rs` duplicate. + - **Tests, each with its rule and mutants.** They are committed in a new + `tools/eureka-bodyplane-mutations.psd1` and run by the Eureka harness + `C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1 -Repo F:\Projects\Huginn`, + with a no-op control. + - `an_oversize_answer_arrives_deferred_and_fetches_to_the_same_answer` + rewrites `serve.rs:548-656` over a real loopback hub and a real mind. + The wide view, at 1,315,551 bytes, comes back `Deferred`. The client + runs `cultnet_rs::fetch_content` over the **same session**, one chunk + in flight, and decodes the bytes as `HuginnMindResponse`. The result + **equals** `daemon.handle` of the same request, and is `View(Some)` and + not `Deferred`. The fitting view and `Whoami` still arrive directly. + This pins the ruling. + Mutants: + - `deliver` refuses instead of deferring (revert to Cut 10's + behaviour); + - `deliver` defers at `MAX_RESPONSE_BYTES * 2` (a function of the input: + the wide answer sits between 1x and 2x, so doubling lets it through + whole and the send fails silently, and the test times out with no + reply). + - `the_delivery_boundary_is_the_transports_boundary_byte_for_byte` + rewrites `serve.rs:749-777`. An envelope of exactly `MAX_RESPONSE_BYTES` + goes direct; one byte more is `Deferred`, **not** refused. This pins + that the threshold is the window. + Mutants: + - `<` for `<=` (off by one); + - measure the payload and not the envelope (the old D20L5, re-authored); + - `MAX_RESPONSE_BYTES - MAX_FRAGMENT_BYTES` (a function of the constant + that a "safety margin" edit would produce). + - `an_answer_over_the_deferral_bound_is_refused_by_name` uses a synthetic + response sized exactly `MAX_DEFERRED_BODY_BYTES` (deferred) and one + byte more (refused, with `bytes` = payload length and `limit` = + `MAX_DEFERRED_BODY_BYTES`). This pins that the backstop stays and is + measured against the deferral bound. + Mutants: + - `if true` (never refuse); + - `limit: MAX_RESPONSE_BYTES` (the old limit leaking through); + - `bytes: envelope_len` (the wrong measure); + - `MAX_DEFERRED_BODY_BYTES * 4` (a function of the input). + - `a_deferred_answer_is_never_itself_deferred` pins a rule written + *inside* `deliver`: the deferred reply is always under the window, and + a body never decodes to `Deferred`. Encode the `Deferred` reply at the + bound (about 256 manifest refs) and assert it is `<= MAX_RESPONSE_BYTES`. + Mutant: recurse `deliver` on the deferred reply. + - `retention_is_idempotent_and_sliding` exercises `DeferredBodies` alone + with an injected `now`. The same body deferred twice is stored once, and + its budget use is counted once. A chunk served at `t = TTL - 1` + refreshes the body, so it survives to `2 * TTL - 2`. An untouched body + is gone at `TTL`. + Mutants: + - expiry from creation rather than last touch; + - `>` rather than `>=` at the TTL boundary; + - a TTL that is a function of the input (`ttl / 2`); + - count bytes per retain instead of per body. + - `eviction_is_least_recently_touched_and_bounded_in_bytes` uses three + bodies over the budget. The one touched longest ago goes first, and a + shared chunk survives while any owner lives. + Mutants: + - evict the newest; + - evict by insertion order (killed because the fixture touches the + oldest-inserted body last); + - free a shared chunk with its first owner. + - `an_evicted_body_answers_found_false_and_the_client_can_ask_again` + pins lifecycle end to end. + Mutant: `chunk()` returns stale bytes after eviction. That is + impossible by construction, so the mutant used is "do not refresh on + serve" at a TTL shorter than a fetch. + - `the_mind_never_sees_a_deferral` is a negative test: + `rg -n "Deferred|DeferredBodies|MAX_RESPONSE_BYTES" F:\Projects\Huginn\crates\huginn-mind\src` + matches only the `wire.rs` variant and its schema test. + - `published_wire_schemas_match_derivation` (`wire.rs:210-216`, kept) + pins that the regenerated schema equals the derivation. + - **Scenario sited at the decision:** the loopback test holds its client + on the `Deferred` reply **before** fetching and asserts the manifest's + `sizeBytes` equals the direct `daemon.handle` payload's length. That + observation is at the decision, not downstream of the fetch (the + skill's "put the observation where the rule is decided"). + - **Negative checks:** + - `rg -n "fn within_window" F:\Projects\Huginn\crates` is empty. + - `rg -n "sha2|Sha256" F:\Projects\Huginn\crates\huginn-daemon\src` is + empty, because hashing belongs to `cultnet-rs`. + - Verified to not collide: `huginn-mind`'s `sha2` is for receipts, in + another crate. + - **Operator:** none until Cut 14. The real-network check (WireGuard to + Yggdrasil, an oversize read end to end) is Cut 14's runbook step and is + recorded there. +- **Subtraction estimate:** + - 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 `schema_with` `$ref` about + 35. + - Tests are about +220 net over the rewritten two. + - Entries: 8 removed, about 20 added. + - No dependency is added: `cultnet-rs` is already a daemon dependency, and + `huginn-mind` already has it after the selection consumer cut. If BP-3 + runs first, it adds that dependency to `huginn-mind`, and that is the one + listed cost. +- **Build budget:** + - `huginn-mind` and `huginn-daemon` library and tests, warm after BP-2. + - Expected +50 to +200 paths. + - Host and target are both the workstation. The unix arm is not exercised, + and Cut 14 builds on Yggdrasil. + +## 6. Interaction with CultNet selection Cut 1, and sequencing + +Selection Cut 1 is in Hands on `cultnet/selection-cut1` in the locked worktree +`.claude/worktrees/agent-af580008561975a4c`, based at `b3d9cf7`. + +**What it touches, from its own map:** + +- `CultNetDatabaseSubscribeMessage`: v1 classes beside v0. It **keeps** the + body-plane demand fields `bodyIds` and `supportedBodyTransports`, which its + §5 lists as not selection. +- `CultMeshBodyDemand.cs:177-221` and `CultMesh.cs` body and snapshot + selector construction. +- `CultNetSchemaRegistry.cs:360-385`. +- `CultNetSchemaMessageSerialization.cs:9-85`. +- `NetworkingTests.cs`. +- In Rust: `packages/cultnet-rs/src/contracts.rs:314-331`, which gains + `SnapshotRequestV1`, `DatabaseSubscribeV1` and `SnapshotResponseRawV1`. The + last is a **raw** message, so it edits the same `parse_raw` and + `encode_raw` dispatch this map edits. +- `lib.rs` re-exports, the new `selection.rs`, and the vector writer pattern + under `CULTNET_WRITE_VECTORS=1`. + +**Semantic interaction: none.** `bodyIds` and `supportedBodyTransports` on the +subscribe message are demand for *live* bodies, the reference's body plane +proper (F2). This cut uses the content plane and never touches that message, +its schema, the body-demand code or the database subscription server and +client. A Huginn read is an operation request and not a subscription, so the +two cuts share no rule. + +**Textual collision: certain, in five places:** + +- the `CultNetMessage` enum; +- `parse_cultnet_message`'s raw arm; +- `encode_cultnet_message_for_wire`'s raw arm; +- `parse_raw` and `encode_raw`; +- `validate_message`; +- and `lib.rs`. + +Two Hands in one working tree is forbidden, and two branches editing one +dispatch list is a merge that falls to whoever lands second. + +**Sequence:** + +1. Selection Cut 1 lands its Rust commit (its commit 3) on `main`. +2. BP-1 branches from that `main`. The anchors in section 3 are re-taken at + that base, and the discrepancy is reported. +3. BP-1 lands. +4. BP-2 moves the pins once to a revision carrying both. +5. Then the Huginn selection consumer cut and BP-3, in that order by + default. + +BP-1 does **not** add JSON schema files or registry entries, so it does not +touch `CultNetSchemaRegistry.cs` or `contracts/cultnet/`, which selection +Cut 1 is editing. The absence of published schemas for the content messages +predates both cuts and is a follow-up (section 7). + +**The mutation runner:** `scripts/mutate-cultmesh.mjs` is modified in the +working tree by the live QUIC campaign. BP-1 adds its entries only after that +file is committed or released, and never onto someone else's uncommitted +edit. If the QUIC campaign still holds it when BP-1 is ready, BP-1 carries +its entries in a new `scripts/mutate-cultnet-content.mjs` under the same +contract, and Self collapses the two later. That is a default, not a +question. + +## 7. Deliberately not in scope + +- **Pipelined chunk fetch, and Cut 10's F2 (pipelined reads).** The fetch is + sequential. F4 shows the throughput barely depends on chunk size, and + pipelining is the known way to lose a reply silently. A window-aware sender + is a transport change in `rudp.rs`, not this cut. +- **Throughput.** About 0.85 MB/s on loopback (F4), set by the 32-packet + pacing window. A 64 MiB body takes about 75 s. Recorded, not addressed. +- **A TCP or QUIC content connector in Rust, and Rust session identity with + Odin-signed route certificates.** These are the reference's + `CultMeshSessionIdentityClient` and `transport-planes.md:73-86`. Huginn's + control plane has none of that today, and the body rides the same session + with the same (absent) trust. **Trust for both planes is Cut 14's** (D8, the + trust boundary). +- **Durable, resumable or file-promoted fetch** (the reference transfer + service's resume state and `.body` promotion). An answer is + re-asked, not resumed. +- **Publishing JSON schemas for `cultmesh.content_chunk_*`.** The reference + never published them: there is no file in `contracts/cultnet/` and no entry + at `CultNetSchemaRegistry.cs:360-385`. Follow-up: a CultLib parity fill for + every runtime, after selection Cut 1 has released the registry. +- **The TypeScript, Python and Kotlin content plane.** It follows when one of + them has a caller, the same rule the selection cut ruled. +- **The live body plane** (`body_read_*`, `CultMeshNetworkBodyStore`, body + demand) in Rust. +- **Persisting deferred bodies across a daemon restart.** +- **Per-operation summaries,** unless Q-BP2 B is ruled. Row-level summaries + are the selection vocabulary's projections. +- **Cut 13's client handling of `Deferred`.** Consequence for Cut 13's spec, + to be carried into its refresh: every read tool resolves `Deferred` through + `cultnet_rs::fetch_content` with a cap no larger than + `MAX_DEFERRED_BODY_BYTES`, and treats `ResponseTooLarge` as "narrow the + selection". +- **Consequence for Cut 14's spec:** the daemon's memory budget on Yggdrasil + grows by up to `DEFERRED_BUDGET_BYTES` (256 MiB). + +## 8. Operator questions + +Two real forks. Every other choice above (the content plane over the body +plane per F2, the reference's manifest, the ephemeral store, the constants, +the order) is a default with its reason stated. It is recorded, not asked. + +**Q-BP1: RULED A by the operator, 2026-09-22 ("A is fine here").** +- Chunk requests and responses travel on the session the client already + holds, byte-identical on the wire to the reference's + `CultMeshLegacyRudpContentServer`. +- BP-1 amends `transport-planes.md` to name this as Rust's *explicit* content + path, never an implicit default. +- It lasts until Rust has an authenticated content connector, TCP+TLS or + QUIC. That later move sits behind the same `fetch_content` owner and needs + no change to the reference type. + +*History, the question as asked:* **Q-BP1. Which physical path carries the body?** + +CultMesh's own transport doctrine says "Schema messages must not carry bulk +bodies" and that the RUDP content path is legacy, "compatibility and parity +measurement" only (`transport-planes.md:90-96`). But the only session +Huginn and its client share is CultNet RUDP, and the doctrine's preferred +content plane (TCP byte stream) is plaintext and loopback-only until TLS +(`:103-106`), while Huginn serves remote clients over WireGuard. + +- **A. Chunk request and response on the session the client already holds.** + This is wire-identical to the reference's `CultMeshLegacyRudpContentServer`. + `transport-planes.md` is amended to name it as the Rust runtime's + *explicit* content path, never an implicit default, until Rust has an + authenticated content connector. Cost: it re-legitimises, for Rust, a path + the C# doctrine retired, and it inherits RUDP's pacing (F4). +- **B. Port the TCP content plane to Rust** (`cultmesh-content+tcp://`, a + typed header and raw bytes). Faster, and outside the envelope. It is + blocked for Huginn by the doctrine's own loopback-only gate until TLS + exists, so it needs a TLS content connector as well: a much larger cut, and + Huginn waits on it. +- **C. Wait for authenticated QUIC in Rust**, which does not exist. Huginn's + oversize answers stay refused until then. + +**Recommended: A.** It is the only option that delivers what the ruling +asked for on the substrate Huginn has. It reuses the reference's wire byte +for byte, and the amendment keeps the doctrine honest rather than quietly +contradicted. B and C are where the body moves later, behind the same +`fetch_content` owner and without changing the wire's reference type. + +**What depends on it:** BP-1's documentation paragraph, whether BP-1 grows a +TCP connector (B), and whether BP-3 can land before a TLS campaign (B and C +block it). + +**Q-BP2: RULED A by the operator, 2026-09-22 ("I accept your +recommendations").** The deferred answer carries the reference's own facts: +the manifest (`sizeBytes`, `contentHash`, the chunk list) and the operation. +It carries no per-operation summary. Row-level facts come from asking again +with a header projection. `huginn-mind` gains no summary types. B can be added +later on the selection projection if Cut 13's tools show the need. + +*History, the question as asked:* **Q-BP2. What is "the summary" the control plane carries?** + +- **A. The reference's own facts.** The manifest carries `sizeBytes`, + `contentHash` and the chunk list, and the envelope carries the operation. + A caller decides whether to fetch on size alone. Anything row-level (ids, + statuses, `matched`) comes from asking again with a selection projection, + which the selection cut already provides. +- **B. A typed per-operation summary produced by the mind:** for a view, the + id, kind, status and admission facts without the document; for a page, + `matched` and row references. The caller can act without fetching at all. + Cost: new summary types and derivations in `huginn-mind`, one per read + operation, re-specified whenever the selection consumer cut reshapes the + reads, plus schema growth. + +**Recommended: A.** It keeps deferral a pure transport decision owned by one +function that knows nothing about operations. A per-operation summary is a +second projection vocabulary beside the one the operator just ruled into +CultNet. If Cut 13's tools show that agents need to decide without fetching, +B can be added then, on the selection projection rather than beside it. + +**What depends on it:** `DeferredAnswer`'s shape, whether `huginn-mind` +gains types, BP-3's size (B adds about 120 lines and a test per operation), +and Cut 13's tool output. + +## 9. What was probed, what was only read, and what is not settled + +- **Probed by running code:** F1 (decode refusals, plus the on-wire refusal), + F3 (real reference bytes, byte identity of both messages and the manifest + in Rust, the Rust re-pack equal to the reference's), F4 (whole-body send + failure; sequential delivery at three chunk sizes with SHA equality and + timings; the 900 KiB failure; the pipelining loss), and F5 (duplicate + `cultcache-rs` under mixed revisions). +- **Read only, not run:** + - C# `ValidateManifestShape` sorting and contiguity, `NormalizeHash`, and + the legacy server's validation order. These are the rules the vectors + will pin in BP-1. + - The reference's TCP connector behaviour and the loopback-only gate, from + `transport-planes.md`, not from running `CultMeshTcpContentTransport`. + - Selection Cut 1's exact edit sites in the worktree. I read its map, not + its uncommitted diff, and did not open the locked worktree's changes, to + avoid reading another agent's working tree mid-edit. +- **Not probed:** + - Throughput over WireGuard or to Yggdrasil (loopback only). + - The 60 s TTL against a real slow link. + - C# decoding **Rust-written** bytes. Byte identity makes it follow, but + BP-1's C# test is the first thing that will actually run it. + - Whether a hostile chunk flood can starve the control plane on one + session (the window is per session, so the likely answer is that it can + starve only the flooding session). Recorded for Cut 14's trust pass. +- **Substrate missing, recorded per the skill:** typed pipeline state. This + map, its model page and its questions live in markdown because Huginn, + which would hold them, is the thing being built. diff --git a/docs/history/eureka-pipeline-state-cut.md b/docs/history/eureka-pipeline-state-cut.md new file mode 100644 index 0000000..a0b21c9 --- /dev/null +++ b/docs/history/eureka-pipeline-state-cut.md @@ -0,0 +1,6465 @@ + +# Eureka pipeline state: cut map + +Date: 2026-09-15 (first Imagination pass), remapped 2026-09-16 after the +operator rejected the repo-owned store. + +Status: cut map. The ends are owned by `notes/eureka-pipeline-state-target.md`; +this document owns the means. Self updates this header in every landing commit. + +**Cuts 1-6, 6b, 6c and 6d are landed and closed inside Epiphany, and Cuts 7, +8 and 9 are landed and closed inside Huginn. Cut 10 is mapped and in Hands. +Cuts 11-16 are unbuilt, and none of them is in this repo.** The old Cuts 3b-7 +described a repo-owned store and are dead; they are +kept, clearly marked, under "History" at the end of this file. Nothing above +that section describes the old model. + +**Ordering against the other campaigns.** Operator rulings, 2026-09-16: + +- First ruling: the StreamPixels rescue is the portfolio piece, so CultLib's + TypeScript QUIC realtime plane goes ahead of Huginn, and the schema work + here stops at Cut 6c so that `epiphany-pipeline` is a clean leaf with + published schemas and no half-built service. +- Correction, same day, in the operator's words: "I'm fine with doing the + CultLib cut without Huginn, but I want the StreamPixels migration to run + with it. If it's to be our portfolio piece, we want full Eureka capabilities + demonstrated." So this campaign does **not** pause after Cut 6c. The QUIC + cut runs without Huginn; Huginn (Cuts 7-16) lands before the StreamPixels + migration starts, so that migration runs on typed pipeline state end to end. + Sequence: Cut 6c → QUIC (CultLib) and Cuts 7-16 (Huginn, a different repo, + may run alongside) → StreamPixels on Eureka with `eureka-state`. + +Self still writes a postmortem at the Cut 6c boundary, because it closes the +schema-ownership phase inside Epiphany, not because the campaign stops. + +- **Cut 1 landed** at `2b76c2e7`, `df82992c`. CultLib re-pinned to `a0813c6`. +- **Cut 2 landed** at `00991c1b`, `46460efc`, `cb6ef5d2`. One commit owner, + parameterised by a `TypedCommitStore` profile. +- **Cut 3a landed** at `a1473c45`, `ad18c385`, then fixes at `b4f88d29`, + `187e01e7`, `a317d4cf`. Ten document kinds, ten derived schemas, plus the + store opener, writer lease and git preconditions that **Cut 4 deleted**. +- **Cut 4 landed** at `ca275c7b`, `a9f06c2a`, fixes at `43a08baa`, `8a598ebb`, + `186dbc62`. The repo-owned store died whole. +- **Cut 5 landed** at `1758ad50`, `4c091f57`, fixes at `617c397d`. One commit + owner, no profile. +- **Cut 6 landed** at `7ab838f1`, `d17cc441`, `08c1c9d9`, fixes at `eb55efe9`, + `b6f6e802`, `80db5db6`, `3ee78e05`, second fix batch at `9b68d83c`, + `4cccb45c`, `4a654351`. `epiphany-pipeline` is a leaf library Huginn can + depend on without `epiphany-core`. +- **Cut 6b landed and closed** at `602ffd9f` (deletes), `1bddd2ac` + (grammar), `95ee551a` (mutation suite), with three fix batches at + `4b85dd2d`, `d03a32df`, `9370aa0f`, `f00062db`, `9d57a460`, `7cd1a38b`. + The second fix batch's Soul pass on Cut 6 had found the same invariant + open one level up for the third time, so the key grammar was redesigned + rather than patched a fourth time; Soul found no collision in 212,450 + adversarial keys. The shared mutation harness came out of its fix + batches. +- **Cut 6c landed and closed** at `4d6af409`, `13570e84`, `dddf9ede`, fix + batch `fcfbda3f`, `3f7d58d1`, harness repairs `b3bd4a82`, `aef0e1bf`. +- **Cut 6d leaf half landed** at `9699751f` (the sequences) and `b82c76df` + (entries M23-M27); 31 tests; exactly two schemas regenerated; every + earlier suite still killed. **Soul-closed on every code promise** (303 + distinct keys over sequences 0, 1, 9, 10 and the maximum, nested at each + level, no collision, every key reading back to its kind, subject and + sequence; a subject local ending in `n1` cannot be confused with a + sequence because the sequence is exactly one part and always present; + the prefix selects one subject's history; the bounds as stated; twelve + non-revert mutants dead). Two coverage gaps on the paths Cuts 9 and 12 + lean on, in Hands with the Huginn follow-up: a nested resolution's own + sequence was unpinned because every nested fixture used 1 at both + levels, and no test resolved a stewardship subject. Stated for the spec: + hand-built ids like `…n01` pass the grammar as labels, the leaf never + emits them, and admission treats them as missing referents; neither + refuses nor canonicalises. Tests delta was +181, not +173. The "additive, + no epoch" claim rests on "no store exists yet", not on a serde default; + the map records both definitions. The two tests landed at `d5a36c2a`, the + new pin. **The Huginn follow-up landed** at `3efc0dc` (pin, sequences, + recursive in-force, the four sequence and in-force refusals, the Q19 cap, + the collision arm deleted) and `fcb7fdd` (entries H40-H51 including the + six Cut 8 test gaps, README stubs); 33 tests each side; H1-H51 killed. + **Soul closed the leaf half and did not close the follow-up** (Fable; + `soul-cut6dh-*` and `soul_cut6dh_probe.rs` in the session scratchpad, + real redb over two stores). Held: every entry on both sides, the pin + exact, no `epiphany-core`, the greps, the docs; the reopen path (n1 + Answered, withdrawal, n2 Answered, n2 again refused out of sequence, n3 + refused already resolved, withdrawal of the withdrawal refused, in-force + n2, all readable after reopening the store); the transfer path over two + minds with the numbers Hands gave; the derived sequences after reopens. + Found, in a fix batch (Opus): **an exact replay of any batch with a + derived write is refused, not `AlreadyAdmitted`** (high; `derive` + recomputes `latest + 1` over an image that already holds the first + admission's derived record, so the re-derived document differs and A8 + refuses before A9; a regression from `30daff8`, where derived writes had + no sequence, and the replay test used only batches without derivations); + **withdrawing a withdrawal on a base kind reinstates an earlier record + beside a later one** (medium; two stewardships of one repo in force + after away, back, and a withdrawal of the first withdrawal; the same + shape for a target's supersession), closed by Self's rule above; + "exactly previous plus one" unpinned against gaps; the cap's and + `latest_resolution`'s batch scope unpinned; the hand-off's derived + withdrawal sequence unpinned. Observations: a reopened question is open + and the code agrees; a ruling stays in force after its `Answered` + resolution is withdrawn; a byte-identical resubmission after a reopen + is `AlreadyAdmitted`. + + **The fix batch landed** (Opus) at `f566fe6` (`derive` reproduces the + record it already made when image or batch holds one, found by content: + `Answered { by: this ruling }`, a withdrawal whose `reason` is the + hand-off key, an assignment whose `note` is; `WouldReinstateOverLater` + after the Q19 cap, scoped by `(instance, repo)`, subject, campaign, or + `(campaign, cut)` — Hands extended the rule to target and cut-spec bases + because Self's own test case had a target base with no scope named, and + Self kept it), `564d492` (entries H52-H61) and `7b67730` (H45/H46 + re-anchored). 41 tests, 61 entries killed. One deviation: Hands ran a + scoped `cargo clean` on the shared target dir against its brief, freeing + and partly retaking about 400 MB; Soul could not establish that anything + of another crate's was evicted, and the next brief forbids it by name. + **Soul's scoped second pass** (Fable, under the new brief shape) held the + content match between rulings, the receiving-side replay after a hand-on, + every F2 scope, and 61 of 61; and found: **the source-side hand-off + replay picks its subject by key order** once two stewardships are in + force under the replayed hand-off's exclusion, so from the tenth transfer + on (`…n10` sorts before `…n2`) the replay derives a withdrawal of the + wrong record and dies at A10 (low: refuses, nothing lands; but the + "AlreadyAdmitted for all three derived kinds" promise is false past nine + transfers); **the resolution-base arm of the reinstatement rule is dead**, + since the cap fires first (delete it: dead code with a test that cannot + reach it is the failure mode of subtraction); four suite gaps with the + code right by probe. Stated, defensible under ruling 20 and recorded as a + limit the views must not hide: **a replay refuses once its scope has + advanced** (a first hand-off replayed after a later one assigned the next + sequence is out of sequence; a first ruling replayed after a second + answered the reopened question is already resolved), with a refusal text + that reads oddly for a record already in the image, and asymmetric with + the source side. **The follow-up closes well enough to map Cut 9**; the + subject-selection fix and the dead arm landed at `1bcb7ce` (Opus): the + selection sits in `stewardship_of` itself, since `derive` is its only + order-sensitive caller; the colliding pair is `n11` before `n2` after + ten round trips, not `n10`; ten round trips alone do not expose the + loosening, so the test hands `n11` away, withdraws that withdrawal, and + replays; the dead arm is gone and Soul's Y7 is unexpressible, said in + the entries header; H62-H67 cover the two fixes and the four gaps. 46 + tests, 67 of 67 killed. **The follow-up is closed.** One tooling scar: + `CARGO_TARGET_DIR=C:\…` through the Bash tool collapses the backslashes + and cargo starts a from-scratch build into a path that does not exist; + nothing landed in the shared dir, and cargo on this host runs through + PowerShell only. Cut 9 in Hands. +- **Cut 8 landed**: the Epiphany half at `a65c6420` (Soul-closed) with the + Cut 10 prerequisite at `b4b17fc`; the Huginn half on `eureka/memory-organ` + at `946758f`, `a4c5b79`, `0bd7133`, `ca30d3e`, fix batch `1cfa81d`, + `acd32f3`, `30daff8`. **Closed as ruled today**; Cut 6d's Huginn + follow-up owns the key sequence, in-force ignoring withdrawals, the Q19 + cap and six test gaps. +- **Cut 9 landed and closed** at `5ae34f8` (every derivation moved whole + into `docs.rs`, one owner for in-force), `49cc6e3` (the read side) and + `8fc39b1` (34 entries). 56 tests; 67 + 34 killed. Soul closed it with + three bounded follow-ups in Hands (two loosenings the entries file called + unfailable are failable; one open-items fixture). D4, D6 and D7 are + superseded by the landed read side; each carries a banner naming its false + sentences. +- **The public ref validator landed** across both repos, the last of Cut 9's + follow-ups. Epiphany `5cda0886` opens one door onto the reference grammar, + `PipelineRef::validate_ref`, as an inherent method rather than a free + function, because the check concerns one public type this crate owns; not + the bare name `validate`, which would shadow the in-crate `Bounded::validate` + its own callers use and break the build. 34 tests, entries R1 (revert) and + R2 (loosening), no schema or epoch change. **`5cda0886` is the new leaf + pin.** Huginn `5ba7b0c6` moves that pin and validates at both read doors: + `view` refuses a malformed ref before answering, and `history` refuses a + malformed `Subject` scope. The refusal is the existing + `MindRefusal::Document(PipelineRefusal::InvalidFormat)` rather than a new + variant, since the grammar it breaks lives in the leaf and a second name + for one rule is a second authority; `MissingReference` stays admission's + "names a document that is not here", which is a different thing. The + `query` filters take `Slug`, `Label` and `OrgRepo`, not refs, so they were + untouched. 57 tests, entries V24 and V24L, and **V10L is demoted** out of + the suite to a recorded unfailable weakening: with the ref validated at the + door, a subject's kind is a function of its id on both sides, so comparing + the whole subject and comparing only its id cannot differ. Cut 9's spec + text still calls V10L failable and is stale there. +- **Cut 10 landed** on Huginn at `f7170fa` (the instance check gets one owner + and the dead serde mirror goes), `c4b2a0f` (the wire vocabulary and its two + published schemas), `6b619d6` (the CultNet surface itself), `a235304` + (documentation of the daemon that exists rather than the stub that did not) + and `f0fee0b` (31 entries over 16 rules). 60 mind tests and 10 daemon tests, + zero warnings, one each of the three GameCult crates in the dependency tree, + no Epiphany file touched and the leaf pin unmoved. M0 green on all seven + targets and every entry killed; cut8 and cut9 rerun clean against the final + spelling. No fork. **Soul in flight.** + + Nine spec discrepancies, all of them the map's fault rather than the Body's: + the connection id constant the spec named does not exist in CultLib at + `a0813c6`, so the entry uses a literal; the daemon must name the owned store + type after all, taken as a re-export through `huginn-mind` rather than a + second `cultcache-rs` dependency, so one crate still pins one revision; + `serde_json` was specified as a daemon dependency and is not one, and was + removed rather than kept unused; one refusal code is unreachable through the + door the spec routed it to and is pinned in the envelope tests instead; + `Options`, `parse_options` and `startup` live in `serve.rs` because a + binary's tests are unreachable, leaving `main.rs` at thirty-three lines that + call them; the mutant the spec stated for the bind-order rule could not fail, + because a hub bound before the refusal is dropped by `?` and releases the + port, so the test now holds the mind lock and the port at once; `std::env` + appears once, for process arguments, with no deployment variable read; + `Mind::status` had to travel with `wire.rs` rather than the first commit. + **Several loosening mutations the brief required could not fail and were + replaced with failable ones of the same rules** — that replacement is the + first thing Soul was told to attack, since a loosening swapped for an easier + target is a hole wearing a green tick. Two rules carry a single mutation with + the file saying why: a served connection id is one argument, so "serves more + ids" has no expressible shape, and the bind-order rule is an order between + two statements whose only weakening short of correctness is already an entry. + + Left for Soul: **the reliable-window limit is asserted nowhere**, so a + response larger than the transport carries may fail by truncation or silence + rather than by a typed refusal; the termination signal path is pinned only by + reading. Build budget missed: +1,743 paths and +1.02 GiB against an estimate + of +100 to +300 paths, because the probe's rlibs were cold and this pass + built library, binary and tests rather than one library. Lockfile 93 → 139 + packages. + + **Soul's pass, 2026-09-16 (Fable; probe crate and two client binaries in the + session scratchpad, a detached worktree, a real daemon over a real mind). + Cut 10 does not close.** Held, and these are the promises the cut exists + for: the daemon holds no rule of its own; every typed refusal crossed the + wire as itself, including a foreign instance, a batch bound, a missing + identity, a field bound, a malformed reference and a semantic + unavailability; the instance check cannot be bypassed by a second daemon, a + second bind, a copied store or an unwritable root, each exiting before it + serves; the published schemas equal derivation byte for byte, live over the + wire as well as on disk; a client that vanished mid-request did not disturb + a fresh one; two hostile datagrams were served past; the single-writer + attribution names CultCache's owned store and not redb. All 31 entries + rerun and killed, M0 green on all seven targets. Findings: + + - **F1, confirmed, High: a real-sized read is lost in silence.** The hub + window is 1024 packets of 1200 bytes and the send error is logged and + served past. Measured: a query returning 833 KB was delivered; every + larger one produced no reply at all. The spec's "documents of about 1200 + bytes" is not the leaf's bound, since one cut spec may carry 256 file + changes and the open-items and history queries are uncapped by design, so + no limit on document count can guarantee a response fits. Cut 13's tool + would ask for open items on a real campaign and hang to a timeout with + nothing to act on. The cut's own rule, that a refusal is typed and never a + transport failure, does not hold at size. + - **F2, confirmed, medium: two reads pipelined on one session lose the + second.** Three large queries sent without waiting yielded one reply and + two full-queue errors. + - **F3, confirmed, medium: the instance check was pinned by luck.** Mutants + comparing only the lengths of two names, only their first bytes, or + running the gate only when the declared name is longer all survived both + suites, because no fixture pair had ever shared a length. This is the + replaced loosening the brief predicted, and the rule had no defence while + the suite claimed two mutants for it. + - **F4, confirmed, medium: read refusals are not pinned as crossing + unchanged.** Mutants rewrapping a view refusal as unavailable, swallowing + one into an empty answer, and rewrapping a query refusal all survived. The + code is honest today and nothing forbids it flattening tomorrow. + - **F5, confirmed, low: the operation comparison is pinned by fixture + luck**, since the mismatch fixture compares two words that differ under + any comparison and both happen to be five letters. + - **F6-F9, deferred to stated limits**: an idle session times out at thirty + seconds on the daemon side with no disconnect while the client still + believes it is connected; an envelope failing the transport's own + validation is dropped rather than answered, because its id cannot be + echoed; a stop neither drains nor disconnects; one daemon-authored failure + code is unreachable in practice. + + **Self's ruling on F1, 2026-09-16.** The daemon compares the encoded + response against the window before attempting a send it can already see will + fail, and answers a typed refusal carrying the size and the limit, so the + caller narrows and retries. The window also rises to the leaf's worst case + for a single document, with the bound stated where a client author reads it. + Raising the window is not a substitute for the refusal; it only stops + ordinary reads from meeting it. No silent truncation, no pagination behind + the caller's back, and Cut 13 caps its default limit. F6-F9 land in the + module's stated limits rather than blocking. + + **The fix batch landed** at `4fba6cc` (the instance check pinned against + names resembling the mind's own: equal length differing in the last byte, + and a shared prefix, with the briefed length comparison back as a loosening + beside first-byte, gate-only-when-longer and prefix mutants), `25a841d` (a + read's refusal pinned as crossing the dispatch unchanged, with mutants that + rewrap and mutants that swallow each into the empty answer of its own + kind), `9bbe746` (the oversize refusal) and `44ed8a9` (its loosening). 72 + tests, 53 entries plus the control (recorded as 52 here at first; corrected + from the file itself), cut8 and cut9 rerun clean. The window + is unchanged at 1,228,800 bytes and documented as the current bound rather + than a design target, per the amendment sent mid-flight. + + **The measurement that changes the question.** One cut spec with every list + at the leaf's bound encodes to 1,315,551 bytes as a view response, over the + limit by 86,751, while a real one at 1,189,495 bytes is delivered whole. So + **a document admission accepts cannot be read back through this door**, which + is a write-and-read asymmetry rather than a size limit, and is the operator's + to rule. The body plane exists in CultMesh for exactly this class of content + and is the obvious candidate; nothing is designed for it yet. + + **Pipelined reads remain wrong and are documented as such**: the gate is + stateless, measuring one answer against the window rather than against what + the session still has room for, so a second large reply sent before the + first is acknowledged still fails to send and still only logs. + + **One entry was refused rather than faked.** The open-items dispatch arm + raises no refusal of its own and its only failing paths need a corrupt + store, so no mutant of it can fail a test here. Stated in the entries header + and the test's doc comment instead of substituted with an easier target, + which is the discipline the replaced loosenings broke. **Scoped Soul in + flight**, told to verify that claim rather than accept it, and to judge one + flagged placement: the oversize refusal is the daemon's rule living in the + mind's refusal type, because the response schema carries exactly one refusal. +- **Soul's pass on the Cut 10 fix batch, 2026-09-17** (Fable, banked and its + findings acted on directly rather than paying a successor to rewrite the + report; probe sources and a pristine detached worktree in the session + scratchpad). Held: 72 tests; all three suites green in a clean worktree; the + gate's boundary equal to the transport's own, byte for byte, at 1,228,800 + delivered and 1,228,801 refused; the oversize refusal itself 299 bytes, so + it always fits, which was worth checking rather than assuming; the + pipelined-read loss exactly as documented. + - **Both size measurements reproduced independently**, by the test's own + print and by Soul's probe: the wide document 1,315,551 bytes, the fitting + one 1,189,495, envelope overhead 231. The transport refused the wide reply + and accepted the fitting one. **The write-and-read asymmetry is fact, not + inference**: a document admission accepts cannot be read back. + - **The honest gap was wrong.** Hands had recorded that the open-items + dispatch arm could carry no mutation entry because its only failing paths + need a corrupt store. Soul built a store-integrity probe from the daemon + crate's public surface that kills both the swallowing and the rewrapping + mutant, and both survive the shipped suite. This is the first real + application of the rule adopted the same day: the probe gets committed, + because a kill living in an agent's scratchpad is not evidence anyone can + rerun. An honest gap beats a fabricated kill, and a falsified gap beats + both. + - Low: mutants measuring the payload rather than the envelope, and + off-by-one mutants at the comparison, survive; a case-folding comparison + of instance names survives, because the slug type permits uppercase and no + fixture pair differs only in case. + - Corrections: the suite has 53 entries, not 52. Informational: the window + is 1023 packets rather than 1024 until the acceptance is acknowledged, + which would make a naive boundary test flaky. + - **The second fix batch landed** at `a96c386` (Soul's probe committed as a + test, the arm pinned, and the two false paragraphs claiming it could not + be pinned deleted from both the code and the entries header), `731b542` + (the gate pinned on the boundary rather than near it, with the + 1023-packet transient documented beside the pipelined-read paragraph) and + `202e5e3` (an instance name differing in case alone). 74 tests. Three + suites green against the final spelling: 164 kills, none surviving, no + sidecars. **All six new entries survived the shipped suite before this + batch**, so none of them is a loosening that cannot fail. + - Hands declined Soul's route for the probe: Soul had added `cultcache-rs` + as a dev-dependency of the daemon, which duplicates the very revision pin + the daemon's manifest says it exists to avoid. It re-exported the two + traits from `huginn-mind::store` instead, and **named the cost out loud** + as a widening of that crate's public surface by two re-exports, bought + against no new dependency and no second revision. Correct trade, correctly + stated. + - **Count correction, Self's to make:** the entry count above was wrong. + The suite held **53 entries at `44ed8a9`**, not 52, and holds **59 as of + `202e5e3`**. +- **Soul's pass on the Cut 10 second fix batch, 2026-09-22** (Opus). Held: 164 + kills with none surviving (Cut 10 59, Cut 9 38, Cut 8 67), every M0 green, + every restore verified by hash, 74 tests. The boundary test passed 60 runs + out of 60 and is not flaky: `settled_session` absorbs the 1023-packet + transient. The open-items pin, the boundary pin and the case pin were each + killed here, and each survives the pre-batch suite. No false "unpinnable" + prose remains in the range. **Cut 10 does not close.** + - **F1, confirmed, medium: the instance check survives separator folding.** + `Slug` is dotted labels, so `_`, `-` and `.` are all legal, and + `thought-cage` and `thought_cage` are two different minds. Folding `_` or + `.` into `-` at `mind.rs:129` passes every suite. The failure: a batch that + declares `thought_cage` is admitted by `thought-cage`, which is a write + into the wrong mind. The cause is that the fixture instance `yggdrasil` + contains no separator. + - **F2, confirmed, low-medium: the size gate is pinned for one envelope + shape.** "Payload plus a fixed 231" survives, because every synthetic + reply uses `message_id` `m-0`. The reply echoes the client's id, so the + envelope's overhead is under the client's control. With a 40-byte id, a + reply up to about 36 bytes over the limit passes the gate, and the hub + then drops it silently. + - **F3, confirmed, low: the open-items pin drives one fault, and it sits + outside the campaign.** Two loosenings survive. One swallows an + `Unavailable` when the fault is inside the campaign asked about. The other + swallows every `Unavailable` except a missing receipt, so an undecodable + document (`docs.rs:46`) or a document written by two receipts + (`query.rs:145`) reads as "nothing open". + - **F4, low:** `fixtures.rs:33-34` describes `Label`'s grammar as though it + were the slug's grammar. That prose hides exactly the `.` equivalence F1 + exploits. + - **F5, low:** the entries headers for Cuts 8, 9 and 10 still name the + deleted Epiphany harness path. + - **F6, plausible, low:** a declared name is never checked against the slug + grammar on the read path, so a fullwidth fold survives. It aliases a name + rather than colliding with one. + - **F7, informational: a second writer already existed.** The two + re-exports opened nothing new, but only because `MindStore`'s supertrait + `CacheBackingStore` and the public `compare_and_swap_batch` already let + non-test code outside the crate write rows. The line "No non-test code + outside this crate touches a row directly" (`store.rs:18-19`) is prose, + not structure. The re-export is also a derive macro as well as two + traits, because the macro and the trait share a name. The prose is + corrected in the third fix batch. **Follow-up, recorded: seal the store + so admission is its only write path.** That is its own cut: it changes + the public trait surface, and the Cut 9 header's "no behaviour this Body + can reach" rests on the same false premise. + - **F8, process, confirmed:** two checkouts sharing + `C:\Users\Meta\.cargo-target-codex` produce the same artifact hash. A plain + `cargo test` in the checkout whose sources are older does not rebuild, and + it ran the other checkout's last mutant binary. The harness is immune, + because it bumps the time on every file it writes. Became a rule in the + Eureka skill. + - **The third fix batch goes to Hands (Sonnet): F1 through F6, plus F7's + prose.** + - **The third fix batch landed** (Sonnet), `7611d1b..7fc8ed3`, 77 tests. + - **F1:** separator-only fixture pairs `UNDERSCORE_INSTANCE` and + `DOTTED_INSTANCE`; D1L6/D1L7. + - **F2:** the boundary at a second `message_id` length; D20L6. + - **F3:** `open_items` refuses on three in-campaign faults: an orphan + receipt, an undecodable document, and a document written by two + receipts. S1 and S1d first anchored on `.views()?` and could not fail + for two of the three faults, because those faults arise in + `Reader::new`. Widened, and each now dies on all three. + - **F4 and F5:** prose corrected. + - **F6:** `require_grammatical_instance` on the read path. + - **F7:** prose corrected; the store is not sealed. + - Cut 10: 65 entries, 65 killed, M0 green. Cut 8 and Cut 9 were not rerun, + since only header text changed. + - **Flagged for Soul by Self:** F6's check is not in its owner. The slug + grammar (`Bounded`) is private to the `epiphany_pipeline` crate, so the + daemon wraps the declared name in a throwaway `PipelineInstance` and + calls `validate()` on it. That is a gap filled beside its owner, not in + it. It also applies only to the read path, while `require_instance` is + shared with admission. **Soul's pass dispatched** (Opus). + - **Soul's pass on the third fix batch, 2026-09-22** (Opus). **Held:** 77 + tests. Cut 10 65/65, Cut 9 38/38, Cut 8 67/67, so 170 killed, every M0 + green, every restore verified by hash. The Cut 8 and Cut 9 diffs are + comments only. The code in the range does what the ledger says. + **Nothing blocking in the range**, but it carries an older defect. + - **N1, confirmed, medium-low, predates the range: nothing checks the + mind's own name, and a name can put the store outside the state root.** + `serve.rs:108` builds `Slug` straight from the command line. + `Mind::open` and `open_with` (`mind.rs:116,137`) never validate it. + `path_for` (`mind.rs:108-110`) joins it as written. A probe: + `Daemon::open(outer/inner, Slug("..\\..\\escaped"))` returned `Ok` and + created `outer/escaped/mind.redb`. It then refused its own name on every + read. Admission's A1 (`admission.rs:109`) never checks the grammar + either, so one fullwidth name gets `ForeignInstance` from `Admit` and + `InvalidFormat` from `Query`. + - **N2, F6 placement:** it is the same grammar, by construction, and + neither direction of disagreement can be reached. The refusal still + names `instance.instance`, a field the client never sent. + - **N3, low-medium:** four separator mutants survive all 77 tests: + folding `_` and `.` into each other, a fold on the mind's side only, + `trim_matches('-')`, and collapsing `--`. The fixture minds contain + neither `_` nor `.`. + - **N4, low:** an overhead mutant hard-coded for `view` survives, and so + does a coincidental slope. Every fixture pins the operation to `view` + and the runtime id to `huginn-yggdrasil`. + - **N5, low:** the ledger's claim that S1d "dies on all three" is false. + S1d exempts the orphan fault by construction. + - **N6, low:** the Cut 9 header contradicts itself at lines 18–25, points + the wrong way, and says V1–V23 where the entries run to V24L. + - **Self's ruling on the F6 fork, 2026-09-22: fill the gap in its owner.** + The leaf gets a public slug check, on the pattern of + `PipelineRef::validate_ref`. Huginn moves its pin to that commit and calls + the check from `require_instance`, so reads and A1 refuse identically, and + from `Mind::open`, before `path_for` touches the filesystem. + `require_grammatical_instance` and its daemon-side branch are deleted. + This rejects the interim option of moving the probe without a pin move, + because that would leave a workaround living beside its owner. + **The fourth fix batch landed** (Sonnet): leaf `9d3a3efd`, Huginn + `cc71a56` and `4f7e3b5`. + - **Leaf.** It gains `Slug::validate_slug`. The name keeps it from + shadowing `Bounded::validate` inside the crate, the same reason + `validate_ref` is named as it is. + - **Huginn, N1.** Huginn moves its pin to the new leaf. `require_instance`, + `Mind::open` and `open_with` all go through the leaf's door. + `require_grammatical_instance` and its branch on the daemon side are + deleted. The escape probe is now refused, and no file is created. + `Admit` and `Query` refuse a fullwidth name identically. + - **Huginn, N3.** One generated test covers 30 separator variants. + - **Huginn, N4.** The size gate is exercised across operation and runtime + id. + - **N5 and N6.** Prose corrected. + - **Graph.** `cargo tree -d` shows one `cultcache-rs` (`a0813c6`). + - **Tests.** Leaf 34→35, mind 60→62, daemon 17→19. + - **Mutation suites.** Every entry killed, M0 green in each, restores + verified by hash: + + | Suite | Killed | + |---|---| + | leaf | 2/2 | + | Cut 8 | 67/67 | + | Cut 9 | 38/38 | + | Cut 10 | 71/71 | + + - **Correction owed here (N5): the earlier line "S1 and S1d … each now + dies on all three" is false.** S1d exempts the orphan fault by + construction and dies on the undecodable fault. S1 dies on the orphan. + - **A scar reported by Hands.** Hands edited a harness target while a + mutation run was in flight. At the end of the run the harness restored + the bytes it had captured at the start, which silently threw away the + edits. `git status` caught it before commit. + - **Soul's pass dispatched** (Opus). + - **Soul's pass on the fourth fix batch, 2026-09-22** (Opus). **Cut 10 + closes on its stated invariants.** Findings: + - **N1's escape is closed in every form tried.** Refused names were + `../x`, `..`, `..\..\x`, absolute paths, drive-relative paths, UNC and + `\\?\` paths, `a/b`, a leading dot, a trailing dot, `a..b`, fullwidth, + NUL, empty and overlong. The whole tree was walked after each attempt, + and no file or directory was created. + - `validate_slug` is `Bounded::validate` itself. + - `Admit` and all four reads share `require_instance`. + - All separator folds die, including a fold of two separators into a + third. + - There is one `cultcache-rs`, and the leaf's range touches only the door + and its test. + - Suites: leaf 2/2, Cut 8 67/67, Cut 9 38/38, Cut 10 71/71. M0 was green + in each, and restores were verified by hash. + - **Residue, not blocking. The residue batch goes to Hands (Sonnet):** + - **S1:** the gate fixtures still fit by coincidence: (3,4,16), (40,4,16) + and (3,10,19). A formula that ignores the runtime id survives, and so + does one that ignores the operation. Needs a point that varies the two + independently. + - **S2:** N1 has no mutation entry. Deleting the door in `open_with` + (`mind.rs:135`) survives all 81 tests. + - **S3:** `Mind::path_for` (`mind.rs:104`) is public and bypasses the + door. The daemon's tests construct stores through it. Make it private, + or make it fallible. + - **S4:** a Unicode hyphen fold placed before the check survives. + - **S5:** the leaf's door test does not pin the 64-byte bounds, whole-name + or per-label, nor NUL. Those mutants survive all 35 leaf tests. This + predates the leaf work. + - **S8:** the header of the leaf's entries file names the deleted harness + path. + - **Recorded, not fixed here:** + - **S6:** declared names used only as read filters (`open_items(campaign)`, + the `campaign` filter in `query`, `HistoryScope::Repo`) are never checked + against the grammar, and read as empty. `OrgRepo`'s doc claims a format + it does not enforce. Owner: the read-side consumer cut, which reshapes + these reads. + - **S7:** the grammar admits Windows device names. `NUL` fails only after + the parent directory has been created. This is informational: there is + no escape and no aliasing. Revisit if the organ ever runs on Windows in + production. + - **H1, medium, a harness defect.** The Eureka harness silently discards any + edit made to a target while a run is in flight, or between entries. It + 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. + - **Soul's check of the residue, 2026-09-22** (Opus). **The residue holds on + every invariant it claimed.** + - `path_for` is private (probe: `E0624`). + - `store_path_for` runs the grammar check in its own body, and refused 10 + escape forms. + - The old escape through `new` and then `open_with` is refused. + - `test-support` is not compiled into a release daemon. + - `getrandom`: all three versions are present both before and after. Only + a dev-only `tempfile` edge moved. + - There is one `cultcache-rs`. + - Suites, each with M0 green and every restore identical by hash: leaf + 5/5, Cut 8 67/67, Cut 9 38/38, Cut 10 77/77. + + Recorded, not blocking: + - Step-function gate formulas survive a 2×2 grid (F4). The grid proves the + two terms are independent, not that each is linear. Production measures + real bytes. + - `trim()`, a U+2011 fold and a space fold placed inside + `require_grammatical_slug` survive (F5). None of them reopens N1. + - S5's bound behaves as a byte count (F6). + - The `test-support` comment claims more than Cargo enforces, and does no + harm (F7). + + **Harness defects in H1, confirmed, and Self's to fix in Eureka:** + - **F1, medium.** After H1 fires, the sidecar is kept on purpose. The next + run's startup repair treats it as a crash: it writes the M0 original + over an operator's hand-cleaned file, calls it "died mid-mutation", and + finishes **green**. + - **F2, low-medium.** `.eureka-mutation-overwritten` is overwritten without + a check. + + Queued behind the RS Hands, which is using the harness now. + - **RS-2 and RS-1 landed** (Sonnet). + - **RS-2, `92ac16c`.** Removes `open_items`, `history` and the admission + window, plus `Reader::views()` and the `Reader` lifetime they alone + required. Net −543 against an estimate of about −440. The negative greps + are empty. Cut 9 27/27, Cut 10 65/65. + - V24L is dropped: with `history`'s door gone, "one door and not the + other" has no shape left. + - D13L is rewritten as `"queries"`. The harness judged the case-only + mutant `"Query"` a no-op, because it compares with PowerShell's + case-insensitive `-eq`. **That is a harness defect** and joins the + H1 follow-up. + - **RS-1, `0d6f9a6`.** + - Receipt v2 carries an `ordinal` equal to `head + 1`, left out of + the digest. + - `head` requires the ordinals to run exactly 1..N. + - The opener checks the epoch before the types. + - Five new tests. + - Readside mutations 9/9 (R1, R1L, R1L2, R3L, and R2/R5 with no + loosening, by the "binary rule" reasoning of D9/D12). + - H6 is re-anchored on the new gate order. + - Suites: Cut 8 67/67, Cut 9 27/27, Cut 10 65/65. + - **Soul's pass dispatched** (Opus). + - **Operator rulings, 2026-09-22:** Q-RS1 is B (one non-empty `Title` for + all four titles), Q-RS2 is A (`huginn-mind` links `cultnet-rs`; + FU-SelSplit recorded), Q-BP1 is A (chunks on the held session). Recorded + in their maps. + - **RS-L landed** (Sonnet) at `d74e36aa`. + - `Title`, 1 to 200 bytes and non-empty, is used by campaign, cut spec, + question and ruling. + - Epoch v2, with 13 type ids and schema files renamed. The negative grep + is empty. + - `validate_org_repo` and `validate_label` are added as doors. + - Leaf tests go from 35 to 37. + - Six new entries (L1, L1L, L2, L2L, L3, L3L), all killed. + - **Stale:** entry E1 in `tools/eureka-cut8-epiphany-mutations.psd1` + still anchors on the v1 epoch string, so that historical suite cannot + rerun. **Soul's pass dispatched**, and it includes E1. + - **Soul on RS-2 and RS-1, 2026-09-22** (Opus). Both build, and every suite + kills every mutant. + - **Held for RS-2:** nothing deleted is reachable. `resolutions_of` and + `assignments_of` survive and are pinned. Dropping V24L and + `Reader::views()` was right. N4 and D13 hold. + - **Held for RS-1:** every R2 loosening dies. The opener cases refuse as + the spec says. + - **Correct counts:** Cut 9 has **26** entries and Cut 10 has **71**. + The 27 and 65 in Hands' report were the map's estimate, not a run. + + Findings: + - **F1, medium: the read side never checks ordinal density.** + `AdmissionIndex::build` (`query.rs:113-130`) copies the ordinal without + calling `head`. `head`'s only caller is admission. With planted + duplicates, gaps, zeros or out-of-range chains, `query` and `view` both + answer `Ok`. The shipped code is exactly the spec's named mutant ("the + check runs only in admission"). R3L tests something else, and a test + named `…reads_alike` never reads. `head`'s doc claims every reader asks + it. The schema publishes `minimum: 0`. RS-3 makes this field the order. + - **F2, low-medium: a second in-process owner bricks admission.** + `Mind::open_with` is `pub` over a store whose clones share one lock. + Two Minds admitting concurrently produced `[1,2,2]`, and after that + every admission refuses. Before RS-1, the same misuse landed both + batches. + - **F3, low-medium: R5 is not binary.** Two loosenings survive: + - "types first when there is no epoch record" (R5b); + - "epoch first only when the record's key is foreign" (R5c). + - **F5, low: stale prose.** The gate step numbering in `mind.rs`, `docs.rs` + pointing at `history`, a test named for two doors, and the Cut 9 header. + - **Self's rulings, 2026-09-22 (RS fix batch):** + - **F1:** density is checked wherever the ordinal is read. The read index + is built through `head`, or through the same density function. A test + that runs `query` and `view` over a store with a duplicate, a gap, a + zero and ordinals above N must refuse. The mutant "check only in + admission" is added and must die. The schema says `minimum: 1`. + - **F2: the smallest seal.** `Mind::open_with` is gated behind the + `test-support` feature, like `store_path_for`. Production opens only + through `Mind::open`, which takes the per-path lock. The full store + seal stays a recorded follow-up. + - **F3:** fixtures pin R5b and R5c. A foreign store with no epoch record + must refuse as the spec's epoch-first order says. A store with the epoch + at the current key, a foreign value and a foreign type must refuse + `ForeignEpoch`. + - **F5:** fix the prose. + - **Soul on RS-L, 2026-09-22** (Opus). **What held:** + - The `Title` bound counts bytes, and the empty title is refused. + - Epoch v2 covers all 13 ids. + - The two doors delegate to the grammar admission uses, and L1L/L2L are + real loosenings. + - The `runtime_spine` stand-in is harmless. + - Leaf 37 tests. Readside-leaf 6/6. Cut 10 leaf 5/5. Cut 6b 17/17, + 6c 10/10, 6d 7/7, 6 10/10, 5 7/7. Restores identical by hash. + + **Findings:** + - **F1:** a 1-byte title is accepted by no test. `len() < 2` survives. + - **F2:** titles of `" "`, `"\0"`, `"\n"`, U+200B and U+FEFF all pass as + non-empty. A single space is still a bare placeholder, which is the + thing Q-RS1 was ruled to prevent. + - **F3:** the published `Title` schema has no `minLength`, and its + description ("1 to 200 bytes, never empty") contradicts its own + keywords. + - **F4:** ruling history is copied into four published schema + descriptions. + - **F5:** Cut 8's E1 anchors on `epoch.v1`, so that suite aborts. A copy + re-anchored runs 7/7. + - **F6:** the readside-leaf header names the old harness path. + - **F7:** the `OrgRepo` grammar behind the new door accepts `../..`, + `a/..`, spaces, control bytes, NUL and non-ASCII. It predates RS-L, and + RS-3 reads the organ's `repo` through it. + - **F8:** the spec's Huginn anchor has drifted to `daemon.rs:369`, + trivially. + - **Self's rulings on RS-L, 2026-09-22 (fix batch):** + - **F2: a `Title` holds at least one non-whitespace character and no + control characters** (C0, C1, U+2028/9, zero-width and BOM). That is + what "not a bare placeholder" means. It is free inside epoch v2 and + would cost another epoch later. **Accepted by the operator, + 2026-09-22** ("I accept your recommendations"). Q-BP2 was ruled A in the + same message. + - **F7: `OrgRepo` becomes GitHub's grammar, in the same epoch v2.** Both + the owner and the repo are `[A-Za-z0-9._-]+`. The owner has no dot or + underscore, is at most 39 bytes, and has no leading or trailing + hyphen. A repo is at most 100 bytes and is never `.` or `..`. Exactly + one `/`. + - **F1, F3, F4, F5, F6 are fixed as found.** The schema carries + `minLength: 1`. The `description` states the byte rule truthfully, and + admission remains the enforcer (schemas README). Ruling history moves + out of the doc comment. + - **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 + and N6. +- **The harness moved out of this repo, 2026-09-17.** The fourth of the + process changes the operator approved on cost review. It now lives in the + Eureka repo at `C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1` + (`GameCult/Eureka`, `9b43747`), and `F:\Projects\Epiphany\tools\eureka-mutations.ps1` + is deleted. It was only ever here because Epiphany needed it first, and + Huginn had been reaching across a repo boundary for it. It takes `-Repo`, so + one copy serves every campaign and a fix to it fixes all of them. **Every + brief and every earlier reference in this document that names the Epiphany + path is history and no longer resolves.** CultLib keeps its own JavaScript + runner, which cannot be this script, and owes the same contract by name. +- **Operator ruling on the oversize document, 2026-09-17: the body plane.** + A body too large for the control plane travels CultMesh's content and body + transfer, which exists for exactly this class of content, with the control + plane carrying the summary and a reference to it. Rejected: raising the + window, which moves a number we would then keep revising until it met the + same wall; and bounding documents smaller, which argues with what a cut spec + honestly is. **The typed refusal stays** as the backstop, because a caller + must still be told by name when an answer cannot be delivered, rather than + discovering it as silence. This is a cut of its own, to be mapped: it spans + the organ and CultMesh, and nothing about it is in Cut 10 beyond the refusal + that made the problem visible. +- **Operator rulings on the read side, 2026-09-17.** The generic selection + vocabulary does **not** land here: it fills the hole in CultNet, mapped at + `F:\Projects\CultLib\docs\cultnet-selection-cut.md`, and the organ becomes a + consumer. What stays with the organ is a smaller later cut: the row and + reference implementations, the roles, in-force, the summary, the snapshot + restriction, and the deletions below. Three rulings bind it. + - **Receipt ordinals: yes.** Each receipt records its admission ordinal, so + the order is exact and a cursor carries a snapshot position rather than a + guess. The stored type changes, the store format takes a new version, and + an existing store refuses to open rather than being silently misread. The + cost is zero today because the only store is ours, and it will not stay + zero. + - **`open_items` and `history` are deleted**, not kept as presets. Every + relation they hardcode maps onto one citation hop, and a preset would be a + second spelling of one answer. If a common question deserves a convenient + name, Cut 13 is where the name belongs. + - **The leaf gains titles**, reversing Self's recommendation. The operator's + words: bare leaves in the catalog is crazy. A catalog row that shows only + an identifier is not a catalog, so `question` and `ruling` gain a `Short` + title in `epiphany-pipeline`. This changes published schemas and the + documents already stored, so **it rides the same store version as the + receipt ordinal** rather than forcing a second migration. +- **Self's ruling on the boundary Hands pinned, 2026-09-16.** A resolution id + carrying no sequence part is well formed to the leaf and is not refused + there. The leaf owns the grammar of ids; per-kind local shape belongs to + admission, which is where the sequence rules already live. This costs + nothing on the write path, because a resolution's key is derived by + `pipeline_key` from the typed record and is never hand-authored, so a + sequence-less resolution id cannot reach admission at all. On the read side + such an id is well formed and simply names nothing. Recorded as a decision + rather than raised as an operator question, because no product meaning + turns on it. +- **Cut 7 landed and closed** on Huginn `eureka/memory-organ` at `1320fc4`, + `f63c0f2`, `e20c786`, fix `4094e68`; Eve `main` `e777e4c`, fix `167a2d3`; + EveConformance `main` `048ea2f`. Started in parallel on the operator's + instruction; it has no dependency on the Epiphany cuts. +- **Target rewritten** at `5fb4eb22`. + +**Cut 6c is the last cut inside Epiphany.** Cuts 7-16 build Huginn's Rust +workspace and the `eureka-state` MCP client; this repo's remaining stake is +schema ownership through `epiphany-pipeline`. + +Pins for this pass. Code anchors are `file:line` against Epiphany +`5fb4eb22`, tree clean, branch `codex/eureka-pipeline-state`. + +- CultLib `main` `a0813c6`. +- Huginn `main` `91b7fcf` at `F:\Projects\Huginn` (upstream public, default + branch `main`, last pushed 2026-06-21). +- VoidBot `main`. gamecult-ops `main`. +- The Eureka skill is now a git repo: `GameCult/Eureka`, public, `main` at + `6ca7882`, checked out at `~/.claude/skills/eureka`. + +## What the rewrite changed + +The first model made a repo own the store. Nothing owned the state, so the +machinery grew to compensate. The operator's correction is that **an instance +owns its mind**, a **service owns that state**, and **minds leave git**. + +The mechanical consequence, which drives Cut 4: every layer that existed only to +make a *repo-resident, git-committed, multi-clone* store safe is now dead +weight. That is the writer lease, the main-work-tree resolution, the committed +`.gitattributes` / `.gitignore` preconditions, the branch binding, and the merge +tool that was never built. + +## Rulings in force + +Numbering follows the target. Rulings 5, 6, 10, 11 and 13's store clauses are +superseded by 14-17; see History. + +1. **Epiphany owns the schemas** for pipeline state. +2. **Searchable state serves agents first.** Rehydration and precedent checks + come before operator browsing. +3. **Two campaigns.** This one covers schemas, the memory organ, admission, the + MCP client and a proof. Epiphany adopting Eureka's habits is the second. +4. **Re-pin first.** Landed. +7. **Eureka is published** as `GameCult/Eureka`, MIT. +8. **Q2 and Q5 survive the rewrite.** Additive schema changes keep the epoch and + a breaking bump refuses the old store (Q2). `schemars` is an unconditional + dependency and published JSON schemas are derived from the Rust types (Q5). + + **Amended 2026-09-16 by operator ruling: widening a `PipelineKind` enum is + additive and keeps the epoch.** Soul was right that a widened enum is additive + for a writer and a hard validation refusal for a reader pinned to the old + file. The ruling accepts that, because the readers of these schemas are ours: + a reader ships with the kinds it knows, and a document of a kind it has never + heard of is not addressed to it. The cost of the other answer decided it — + every new kind would rev the five schemas that embed `PipelineKind`, and + adding kinds is most of what this campaign still does. A reader that must + refuse an unknown kind refuses it on the kind, not on the epoch. +9. **The operator channel is the Claude Code session.** Eureka has no Persona. + Blocking questions may be pushed through any notification MCP; Eureka owns no + transport and names no provider. Answers come back in the session. +11. **Numeric attempt and pass.** `attempt` and `pass` are numeric, and attempts + count up per cut across spec revisions. (Survives; it is a key rule, not a + store rule.) +14. **An instance owns its mind.** A store is canonical to exactly one instance. + Identity lives in the state, not in a path. Admission refuses a write + carrying another instance's identity, whatever the transport. Stewardship + over repos is an assignment recorded in that mind, and one instance may + steward several repos. Reassignment is an explicit typed hand-off recorded + in both minds. +15. **A service owns the state, on Yggdrasil.** The memory organ is a daemon. No + per-clone lease, no git-attribute precondition, no divergence between clones. +16. **The organ depends on Qdrant directly, not on voidbot.** It owns its own + collections and indexes at admission time. Embeddings come from Ollama. +17. **Huginn is the memory organ.** The dormant `.cc`-to-Eve CLI is retired and + generic `.cc` inspection belongs to CultCache Studio in CultLib. +18. **Instance identity is declared, not signed** (Q6 A). On a single-operator + LAN this is attribution, not authentication. Hands does not invent a + credential system; the trust boundary is the network and the host. +19. **The workstation reaches the organ over WireGuard** (Q7 A), because the + organ speaks RUDP over UDP and the existing SSH forward carries TCP only. +20. **`TypedCommitStore` collapses** (Q8 A). Its second profile moved to another + repo, leaving a one-implementation abstraction. Cut 5 must preserve the Mind + path's fail-closed order and validation before replay (finding S6). +21. **The organ shares voidbot's Qdrant** (Q9 A), with the coupling declared as + an Idunn dependency rather than left implicit. + +## Landed + +**Cut 4 landed** at `ca275c7b` (deletion) and `a9f06c2a` (mutation suite). +Soul is verifying. + +- **Removed 1,302 lines, added 549.** `pipeline_store.rs` died whole (1,251), + plus 47 in `pipeline_documents.rs`, two `lib.rs` lines, `.gitattributes:1` and + `.gitignore:37`. Retired: one store format, one lease, two git preconditions. +- **Eight tests moved** into `pipeline_documents.rs` rather than waiting for + Cut 6, so no rule sits unpinned between cuts. Tests 168, 0 warnings, and all + eight mutations killed. +- **Orphans removed:** `MissingIdentity` and `Unavailable` lost their last + raiser with the opener and lease. The three named helpers all kept live + callers. + +Spec corrections from this cut, which stand against the sections above: + +1. **The local `PipelineRefusal` keeps five variants, not three.** + `ForeignStore` is raised by `decode` and `ForeignEpoch` by + `validate_pipeline_write_envelope`, so the spec's three-variant line does not + compile. Both leave with admission, as D2 already assigns them to + `huginn-mind`. +2. **The deleted module held 20 tests on Windows, not 15,** so the arithmetic is + 180 − 20 + 8 = 168, not 165. +3. **Cut 6's estimate must grow.** Its `epiphany-core` −623 should read about + −1,060, because the test module now travels with the code. +4. **`schemas/cultnet/README.md:30`** still describes a per-repo store path. + Cut 6 owns that rewrite, so Cut 4's negative grep cannot be empty yet. + +**Cut 4's Soul findings are closed** at `43a08baa` and `8a598ebb`. Soul is +verifying the fix batch. + +Soul found that the deletion had taken a live rule's only test with it: the +epoch guard survived a `if false` mutation, and no test could even construct an +identity envelope to reach it. Self ruled deletion over testing what nothing can +construct, since admission, identity and the epoch refusal belong to the organ +and D2 already assigns those refusals to `huginn-mind`. + +- **Deleted:** the epoch branch, `ForeignEpoch`, `EpiphanyPipelineIdentity`, + `EpiphanyPipelineProvenance`, `PipelineProvenance`, `Faculty`, and + `PIPELINE_SCHEMA_EPOCH`, whose only reader was that branch. Each was confirmed + producer-free first. **Cut 8 writes them in the organ, with tests that can + reach them.** +- **Pinned instead:** a foreign-typed envelope is refused on decode with + `ForeignStore` specifically, and the runtime spine cache refuses a pipeline + store. Both die under their own mutation. +- **No schema consequence:** identity and provenance were never published, and + all ten schemas still derive byte for byte. + +Further corrections to the sections above: + +5. **Cut 4's Keeps list is wrong in two places.** `PipelineProvenance` and + `Faculty` fell under "every value type", and are gone. +6. **`register_pipeline_document_types` is now `#[cfg(test)]`**, which Cut 6 + must account for when it moves the module. +7. **Test arithmetic is 170**, not 168. +8. **The pipeline write path is test-only, and now says so.** `186dbc62` + converted `prepare` and `validate_pipeline_write_envelope` from + `expect(dead_code)` waivers to `#[cfg(test)]`, after Soul proved both dead in + a non-test build. `decode` and the `CultCache` imports came with them: they + had looked live only because dead code mentioned them. + - **Cut 6 arithmetic:** it now relocates four `cfg(test)` items plus a gated + import, not two live ones. The Keeps list still holds, and + `validate_pipeline_write_envelope` still moves. + - **Cut 6 must refresh** its Keeps citation for + `validate_pipeline_write_envelope`; the line range no longer matches. + - **Refusal picture unchanged:** none of the four variants has a reachable + non-test raiser. The conversion made the existing state literal rather than + waiver-shaped. Cut 8 owns it. +9. **The mutation suite is runnable and its anchors are exact** (`06100d8e`). + It documents the interpreter this host actually has, refuses an unsupported + one up front, and asserts each anchor matches exactly once, since a replace + hits every match. All ten mutations still die by their own test, including + the two that mutate code inside the newly gated functions. + +**Cut 5 landed** at `1758ad50` (collapse) and `4c091f57` (mutation suite). Soul +is verifying. + +- **Deleted:** `TypedCommitStore`, `MIND_COMMIT_STORE`, `validate_mind_writes`, + the parameter and all five call-site arguments, plus one orphan import. No + part of the profile shape survived. One commit owner remains, and two receipt + construction sites: the owner's, and a test that plants one on purpose to pin + the replay rule. +- **Unchanged behaviour, proven against the base:** the pinned receipt digest + was checked by running the base test in a detached worktree at `cda36541`, + not by a value captured from the new code. +- **Tests 171, 0 warnings.** Two profile tests retired because they tested the + profile through the profile; their rules were re-expressed as three new tests. +- **Five mutations, all killed**, including both halves of S6: uniqueness + before validation, and validation before replay. + +Cut 5 corrections: + +10. **Ruling 20's second half had no named test.** The Keeps list named only + validation-before-replay, so the uniqueness-ordering half of S6 was + unpinned. Cut 5 added a test for it rather than leaving half the invariant + on trust. +11. **The Deletes table's line anchors were stale by 26 lines**, written against + `5fb4eb22`. Matching was done by content. Later cuts should re-anchor before + trusting a line number in this map. +12. **The subtraction estimate was −46/+8; the actual is +172/−122.** The + owner-side collapse matched; the overshoot is the test rewrite the ledger + never priced, because the Keeps list demanded three rules stay pinned while + the tests that pinned them died with the abstraction. Carrying cost still + falls: one struct, one const, one fn, one parameter and five arguments gone. + +**Soul's Cut 5 pass** confirmed the collapse changed no Mind behaviour: it read +the full owner diff, including the places the pinned digest is blind to +(`committed_at`, companions, conflict re-read, replay lookup), and found nothing +reordered or re-worded. Both renamed tests assert what their names claim, and +byte-identity on refusal is now asserted in three places rather than base's two. + +It found four gaps, which a Hands batch is closing: + +- **S7 (medium, a regression from base).** Re-deriving the backing store at the + CAS from the same path survives the suite. The replacement test only asserts + "one file in this directory", which a re-derivation satisfies; the retired + profile test had caught it by putting the store somewhere else. It matters + beyond style, because the planned redb store permits one writable handle per + path. +- **S8 (low-medium, pre-existing).** Validating only the first write of a batch + survives, because no test commits a batch of two distinct writes. +- **S9 (low).** The claim of exactly one receipt construction site is false: + there are two, the owner's and the planted receipt that makes the replay test + work. +- **S10 (cosmetic).** Profile vocabulary survives in a renamed test's fixture + data. + +**Cut 5's findings are closed** at `617c397d`. Tests 172, 0 warnings; seven +mutations, every one killed by its own test. + +- **S7:** a `RuntimeSpineBackingStore` carries only its path, so a same-path + re-derivation is invisible to any assertion about files, contents or + receipts. The test now counts resolutions through a `#[cfg(test)]` log and + requires exactly one per commit, with a comment saying why the file + assertions below it cannot see this. MS1 dies with `left: 2, right: 1`. +- **S8:** a new test commits two writes with distinct identities, the second + refused, and requires the store byte-identical, so the valid first write + lands nowhere. MS2 dies. M2's rule string was corrected: it pins that the + batch is validated at all, and MS2 owns "every write". +- **S9:** corrected above. Two construction sites, one deliberate. +- **S10:** fixture data renamed; the pinned digest does not depend on it. + +**Soul's second pass corrected Self's record.** Self had written that a +mutation reported a verdict it never earned. The committed script already threw +on a missing anchor, the affected mutation existed only in a working copy, and +every committed verdict was real. The Eureka changelog carries the correction. + +**Cut 6 landed** at `7ab838f1` (extract), `d17cc441` (docs) and `08c1c9d9` +(mutation suite). The last cut inside Epiphany. + +- **`epiphany-pipeline` exists.** Four direct dependencies, 36 transitive + against `epiphany-core`'s 93, and **zero new packages in the workspace lock** — + the lock gains exactly one entry, the member itself. `cargo tree` over normal, + dev and build finds no path back to `epiphany-core`. The leaf is a leaf. +- **`epiphany-core` net −1,034**, and lost a dependency: `schemars` left with the + code, the module having been its only user in the package. +- **Tests 12 and 163**, both 0 warnings, the arithmetic exactly 172 − 9 moved. + Twelve mutations, all killed. +- **The move is verbatim in the library code**, confirmed by Soul reading the + full base-to-head diff. + +Cut 6 corrections: + +13. **Cut 4's Keeps list said `schemars` stays. It did not.** The pipeline + documents were its only user, so it left with them. The Q5 reason to keep it + now lives in `epiphany-pipeline`. +14. **The add-side estimate was never raised.** Correction 3 raised the removal + figure to −1,060 when the test module joined the move, and left the addition + figure on its pre-Cut-4 623-line base: spec +700, actual +1,292. The two + numbers always move together. Second ledger drift after a correction this + campaign; the first was Cut 5's correction 12. +15. **`rg -n "pipeline" epiphany-core/src` cannot be empty**, and should not be. + Sixteen hits remain, all inside one `#[cfg(test)]` spine test that Cut 4's + fix batch put there deliberately. The intent holds — `epiphany-core` has no + pipeline *surface* — so the negative grep is the narrower one over the type + names. Hands refused to weaken the test to satisfy a grep, and Soul + confirmed that was the right call. +16. **Ten schemas changed, not three added.** The five that embed `PipelineKind` + gained three variants. Ruling 8's amendment settles that as additive. +17. **`epiphany-state.exe` "byte-identical" is withdrawn as false**, not merely + unverified. It is 94,720 bytes smaller at head, which is what taking ~1,000 + lines of schemars-derived types and the `schemars` dependency out of + `epiphany-core` should do. No receipt for the original claim exists anywhere. + Soul also showed the same source hashes three ways from three different + roots, so the honest replacement is no hash claim at all: that artifact's + hash is not a comparison this branch can make. +18. **`7ab838f1`'s message says three things changed in the move; the diff + carries five.** The two omissions are test fixture path literals, mechanical + and harmless. Self's first relay of this was wrong in its other half: the + `remove_dir_all` line was introduced by `08c1c9d9`, whose message devotes a + paragraph to it. + +**Cut 6's Soul findings are closed** at `eb55efe9`, `b6f6e802`, `80db5db6` and +`3ee78e05`. Tests 13, 0 warnings; the suite is now M1-M15 and every one is +killed by its own test, including both of Soul's survivors. + +- **S1, the escape was not injective.** `GameCult_Epiphany/thing` and + `GameCult/Epiphany_thing` both keyed to `GameCult_Epiphany_thing`; an instance + stewarding both would silently have lost one document. The test above it + claimed the property in a comment and checked one half of the pair. Both + special bytes are now escaped to two-byte codes starting with `_`, which is + reversible and therefore injective, and the test keys both halves. +- **S2, the instance key segment was unpinned.** The validation call was in the + source; nothing would have noticed its removal. Soul's survivor is now M14. +- **S3, `pipeline_key` made `Instance` a root and `pipeline_id` did not**, so an + instance could be written but never referenced and no resolution could take + one as its subject. The key writer had been extended for the new kind and the + id reader left behind. +- **S6, the derivation test's temp directory is per-run**, named by pid and + nanos. The best-effort clear and its `.ok()` are gone rather than hardened: + with no directory shared between runs there is nothing to defend. The + mutation suite had been leaving mutated schemas in the shared one. +- **Recorded, not fixed:** S5, the `ForeignDocument` type-id literal can drift + from `epiphany-core`'s real one and nothing connects them; the strong fix is a + dev-dependency that crosses this cut's deletion line. Cut 8 owns it. +- **Informational for Cut 8:** the leaf pulls `redb` and `fs2` through + `cultcache-rs`, so Huginn gets an embedded database in its graph from this + leaf whatever else it chooses. + +**The test shape was the common cause.** S2 and S3 were both invisible because +the three new kinds' tests asserted key strings and never parsed them back. +`keys_read_back_as_ids_of_their_kind` is the pin that would have caught both, +and it is now in the Keeps list for every kind added after this. + +**Cut 6's second fix batch landed** at `9b68d83c` (one composer, +`KeyParts::key`, for every non-root, non-resolution key, with `.` escaped to +`_d`), `4cccb45c` (a resolution reads back by the key it has; `declared_kind` +added) and `4a654351` (the per-run schema directory's cost stated). It closed +Soul's second-pass findings: + +- **F-A, the composed hand-off key was still not injective.** S1's fix made the + repo *segment* injective and left the *composition* open: a dotted + `to_instance` and a dotted date could trade bytes across the `.` separator. + Fixed at the composer, not the segment. +- **F-B, a resolution was unreferenceable.** Soul's mutation adding + `| PipelineKind::Resolution` to the root case survived, because nothing read + a resolution's key back. Fixed with a Resolution arm in `pipeline_id`. + +**Soul's third pass, on that batch, found the same invariant open again**, one +level higher, and Self stopped the patch sequence. Three consecutive fixes had +each made one composition site injective (segment, then composed local, then +root namespace) and left the site above it ambiguous. Per doctrine, escalating +guards mean the ownership is wrong. The three CONFIRMED High findings, all +`epiphany-pipeline/src/lib.rs` at `4a654351`, became Cut 6b: + +- **Campaign and instance share one root namespace** (`:693-700`, `:553-556`). + Campaign `yggdrasil` and instance `yggdrasil` key identically. Soul measured + 254 documents keying to 234 distinct keys, 20 colliding, 10 from this pair. +- **A resolution's key discards its subject's root kind** (`:703-707` with + `:529-538`). `declared_kind` hardcodes `Campaign` for a bare slug; a mutation + flipping it to `Instance` survived because the information was already gone. +- **A resolution of a resolution composes `resolution:resolution:…`** + (`:564-568`), which `pipeline_id` reads back as `InvalidFormat`. The comment + beside it names the path as live. + +Three medium survivors go into the same cut: the composed local's total length +check is unpinned (a hand-off local reaches about 140 bytes against a +documented 64); the root check inside the composer is unpinned (`pipeline_key` +is `pub` and a colon in a root yields four segments); and "a kind's tail arity +is fixed by the arm" is enforced by a doc comment, not the type. + +Cut 6 corrections, continued: + +19. **`` escapes three bytes, not two.** The Keys paragraph below + said `_` and `/`; `9b68d83c` added `.` → `_d` so that an escaped repo can + sit inside a `.`-separated local without trading bytes with its neighbours. + Cut 6b generalises the escape to every slug entering a local and the + paragraph is superseded there. +20. **The key table never stated the resolution key.** `resolution:` + was the shape since Cut 3a and appeared in no table. Pre-existing; Cut 6b's + grammar table is the first complete one. +21. **`rg -n "std::process" epiphany-pipeline/src` is not empty**, and should + not be: the per-run temp directory from S6 reads `std::process::id()` under + `#[cfg(test)]`. The intent (no process is spawned, no socket is opened) + holds; the grep is narrowed to `Command|UdpSocket|BackingStore`. Second + negative grep this campaign to drift from its intent; the first was + correction 15. +22. **The shared cargo target directory was left at 8,642 paths against an + 8,284 baseline, and that is ruled acceptable for this pass.** The growth is + one debug profile of this package's dependency tree, with no new package, + target, profile or platform, and deleting it would evict a cache the next + build re-creates. This is a judgment per pass, not a loosening of the rule: + the path list is still recorded and compared every time. (`find | wc -l` + counts the root directory and PowerShell's `Get-ChildItem -Recurse` does + not, so 8,642 and 8,641 are the same count.) +23. **Cut 5's Soul addendum carries a second "byte-identical" claim about + `epiphany-state.exe`.** Its other half is stronger than claimed and stands: + the negative greps are empty across the whole tree including untracked and + ignored files. But the binary claim has the shape correction 17 withdrew, + and that artifact's hash is build-root dependent. Marked suspect, not + evidence; no hash replaces it. +24. **`schemas/cultnet/README.md:5` promises the folder helps "foreign + consumers inspect the wire shape", and `:40-44` now says a widened enum is + additive because "these readers are ours".** Both sentences are true today + and they will contradict each other the day Eureka has an outside reader. + Recorded, not resolved: it needs a real answer if a foreign consumer + appears, and the answer is a versioning policy, not a README edit. + +**Mutation harness audit (Eyes, 2026-09-16).** Soul's third pass ran a no-op +control mutation and it "killed" `bounds_refuse_in_utf8_bytes`: the harness's +text round-trip had collapsed the test's `é` literal to one byte, so every +mutation through that path would have reported a kill whatever it changed. +Soul caught it only because it ran the control. Eyes then audited every suite +in this repo and in Ghostlight: + +- **No committed suite has the defect.** `tools/eureka-cut4-mutations.ps1` (at + `a9f06c2a`), `cut5` and `cut6` all use `[IO.File]::ReadAllText` / + `WriteAllText`, measured byte-identical on every live target. The corrupting + harness was Soul's own inline one and is not on disk anywhere; its text is + unrecoverable. That is the shape finding S9 under History already names as + Standing: a mutation with no artifact on disk is unverifiable. +- **Cut 1's M1-M3 are suspect for a different reason.** The ad-hoc + `eureka-c1-mutate.ps1` joined lines with `\n`, rewriting two CRLF files to LF + for the whole file while mutated; `core.autocrlf=true` hid the rewrite from + `git diff`. Whether that changed a verdict is unknown and was not re-run. + Cuts 3a, 4, 5, 6, the Cut 2 fix pass and all Ghostlight verdicts are not + suspect. +- **No suite in either repo has a no-op control**, and the committed suites + structurally forbid one: each throws when a replacement changes nothing + (`cut4:86`, `cut5:216`, `cut6:237`). Ghostlight has never had a scripted + harness; its mutations are hand edits restored by `git checkout`. +- **The measured rule** is encoding symmetry and end-of-line preservation, not + `-Encoding utf8` by itself: mixed-encoding round-trips corrupt the `é`, and + any non-`-Raw` `Get-Content` corrupts line endings regardless of encoding. + +Cut 6b's suite is the first with a control (M0) and the rule is now in the +Eureka skill. + +**Cut 6b landed** at `602ffd9f` (the three key-shape exceptions deleted; +does not build, by design), `1bddd2ac` (one grammar for every kind; 20 tests, +0 warnings) and `95ee551a` (`tools/eureka-cut6b-mutations.ps1`, the first +suite with a no-op control). Source outside tests 774 → 726, tests 734 → 881, +no schema, dependency, kind, field or epoch moved. Hands' spec discrepancies, +all kept: `local` takes `&[&str]` rather than a fixed array because the +Resolution and Finding arms carry a parent's parts; the whole-local bound +refuses `InvalidFormat` to match the reader; `key_segment` is a pure escape +and each caller validates its own field first; two "unchanged" tests carried +literal keys that moved; and `{CAMPAIGN}:resolution:R8` is now asserted +*accepted* with the Q11 rationale in its doc comment, so a future runtime +guard is visible. + +**Soul's first pass on the grammar** (Fable, script +`soul-cut6b-mutations.ps1` in the session scratchpad, M0 green twice) built a +whole-key injectivity probe: 212,450 distinct keys across all thirteen kinds +with adversarial slugs, repos, labels and dates and resolutions nested to +depth four, **zero collisions**, every key three segments, every key reading +back to its own kind and identity, 157,010 resolution keys recovering exactly +their subject. `self` collides with nothing as a slug, a label or a campaign. +Hands' M1-M6 all killed on rerun; Soul's own S1, S6, S7, S8, S9 killed; S3 +(`splitn(3, ':')`) survived as an equivalent mutant because `label_text` +refuses a colon in the third segment anyway. Three survived for real: + +- **F2 (medium): the escape's `_`→`__` half is pinned only through an + `OrgRepo`.** A mutant escaping only `.` when the value has no `/` survives, + and hand-offs to `a_db` and to `a.b` then share a key. That is the pass-2 + collision class, one edit from reopening. +- **F3 (medium-low): the composer's root check is pinned for one byte.** Under + `trim_end_matches('.')`, campaign slug `eureka-state.` composes a key the + reader refuses, and the write validator recomputes and compares without + reading back, so a document is admitted under an id no `PipelineRef` can + name. +- **F4 (medium-low): the 64-byte boundary is unpinned on both sides.** + Fixtures sit at 55 and 66+, so a `> 65` mutant survives and the writer + admits a 65-byte local the reader refuses. The code is right; the pin was + missing. + +And four findings that are not code defects: **F1**, the "depth at most five" +claim is false (corrected above); **F5**, a dotted finding label now refuses as +`finding.key` rather than `finding.label`, consistent with every other kind +and accepted as the behaviour; **F6**, the authority map overstated the escape +rule for `Date` (corrected above); **F7**, Hands' "twelve fixed call sites" is +eleven literal slices plus two computed vectors, the Finding one fixed in +practice by `parent_cut` pinning the verdict local to two parts. **F8**, +pre-existing and informational: a cut-report key drops the spec revision +(a standing ruling) and a verdict key drops the report attempt (implied, not +stated). `pipeline_id` accepting `c:campaign:notself` and the like is grammar +versus reachability, admission's job, as the spec says. + +The stale Cut 6 script entries M5 and M12-M15 match zero anchors at HEAD and +the script throws at its line 234 rather than reporting a kill. Loud, as the +rule requires. Target dir: Hands' growth to 9,279 was one debug profile; +Soul left 8,991 because cargo rotated out one of Hands' incremental sessions, +which cannot be recreated and is not a deletion. + +**The first fix batch landed** at `4b85dd2d` (F1-F4 pinned; Soul's S2, S4 and +S5 now die; the depth test computes its expectation from the formula) and +`d03a32df` (one harness, `tools/eureka-mutations.ps1`, with per-cut entries +files `eureka-cut5-`, `-cut6-`, `-cut6b-mutations.psd1`; the three old scripts +deleted; `tools/` net −24 lines). The harness runs each entry with +`--exact ` and reports `TEST NOT RUN` for a name that runs nothing, +where the old scripts would have said SURVIVED. Cut 6's stale entries M5 and +M12-M15 were deleted with notes, on Self's instruction, which was a mistake; +see F3 below. + +**Soul's second pass** (Fable; scripts `soul-cut6b-fix-*.ps1` in the session +scratchpad) attacked the harness directly and reran every entry with the +whole suite: no verdict was borrowed from a collateral kill. Held: S2/S4/S5 +die, the four pins are as promised, all three suites report the same verdicts +through the new harness, refusal `value` is the failing part and asserting +`field` alone cannot hide a wrong-reason refusal. Found: + +- **F1 (medium): the control could damage what it protects.** M0 wrote the + target before comparing, having read it as text, so on a broken harness or + a BOM-bearing target it reported "harness broken" and left the file + changed with no restore source but the checkout the rules forbid. +- **F2 (low): "exactly once" counted non-overlapping matches**, so a + self-overlapping anchor like `}\n}\n` in a run of three braces passed as + one. +- **F3 (low-medium): the deletion notes for Cut 6's M12/M13 named the wrong + pinning entry.** `key_segment` still exists and nothing in the committed + suite removes its escape or breaks its injectivity; the tests do pin the + rule (Soul's N7/N8 die), the suite claimed coverage it lacked. Self's brief + ordered "delete, do not re-anchor"; the coherent fix was to re-anchor. +- **F4 (medium): dotted roots were unpinned on writer and reader.** No test + used a campaign or instance slug with a dot, so `label_text` on the root in + either `pipeline_key` or `pipeline_id` survived the whole suite. +- **F5 (low): the reader's kind check was unpinned for root kinds.** A mutant + excusing `Instance` from the kind-segment comparison survived: exactly the + "reader excuses a root" shape Cut 6b existed to kill. +- **F6 (low): the depth test restated the literal 64** rather than + referencing the bound, so moving both to 60 passed it; the bound itself is + pinned by F4's fixtures. +- **F7 (recorded): `-Test` splits on whitespace**, so a path with spaces + cannot be expressed. Not reachable on this host. + +**The second fix batch landed** at `9370aa0f` (harness: the original is read +as bytes and is the restore source on every path; M0 compares the re-encoded +bytes to the original before writing anything, then still writes through and +hashes so a broken write path is caught, restoring the original bytes before +any throw; anchor occurrences are counted overlapping; the offset reverse-edit +path is deleted; a BOM-bearing target is now tolerated rather than refused) +and `f00062db` (`dotted_roots_key_and_read_back`; +`keys_read_back_as_ids_of_their_kind` extended by three root-kind asserts, its +first change since Cut 6; `const LOCAL_MAX: usize = 64` shared by `local` and +the depth test; entries N3, N6, N7, N8, N9, N10 in the 6b file, M4 and S5 +re-anchored on the constant; the Cut 6 notes corrected to M5→N3+M6, M12→N7, +M13→N8, M15→N6+M1). Tests 21, 0 warnings. Soul's attack script rerun against +the fixed harness: every variant leaves every target byte-identical and runs +no entry. + +**Soul's third pass** (Fable; `soul-cut6b-fix2-*` in the session scratchpad) +reran all three suites through the committed harness (every entry killed, M0 +green on every target, 0 warnings unmutated) and **closed the grammar +pins**: the dotted-roots test asserts exact tuples, the root-kind asserts are +appended to the read-back test with nothing weakened, a BOM-bearing target +round-trips byte-identically and rustc accepts a leading BOM, and its own +non-revert mutations died or were equivalent (swapped escape codes remain +injective; `chars().count()` equals `len()` for ASCII parts). What survived +is the harness under failure, not the grammar: + +- **The multi-target write loop sits outside the `try` whose `finally` + restores**, so a write that throws on the second target leaves the first + mutated with no message. Low for today's single-target entries, medium + against the harness's stated contract. +- **M0's write-through has no `finally`**: a write that dies after + truncation leaves a truncated target, and only the hash-mismatch branch + restores. +- **A killed process runs no `finally`**, and the harness has no timeout of + its own, so a tool-enforced timeout is exactly the path that leaves the + tree mutated. The next M0 would catch it without saying why. +- **The reader accepts a trailing dot on the root and on the local** + (`pipeline_id`, `:556-557`); only the writer refuses, so a `PipelineRef` + with trailing junk passes the reader against its own doc comment. One + fixture closes it. +- `LOCAL_MAX` names the writer's bound; the reader's is `dotted_text`'s + literal 64, which is also the `Slug` bound. Redundant, not split: the + writer refuses first. A comment at `:651` still says "the 64-byte bound". +- Hands' line counts were +6/−1 and +73/−6, not +5/−1 and +68/−6. + +**The third fix batch landed** at `9d57a460` (harness: every target write +inside the restoring `try`; M0's write-through in its own `try`/`finally`; a +shared restore that is hash-first, attempts every target, and prints +`RESTORE FAILED` with the original SHA-256 before rethrowing; a sidecar +`.eureka-mutation-original` written before any write and removed +after a verified restore, from which a run that died mid-mutation is +repaired at the next start with a message; `-TimeoutSeconds`, default 1800, +killing the process tree inside the command runner's own `finally` with the +verdict `TIMED OUT (no verdict)`) and `7cd1a38b` (the reader refuses +`c.:target:x`, `c:target:x.`, `c:target:.x` and `c:target:a..b`; X1 and X11 +are entries). Hands reproduced every attack: locked targets on either side +leave both unchanged; a tree killed mid-entry leaves the mutant and the +sidecar, and the rerun repairs and says so; a one-second timeout kills the +runner and leaves the target unchanged. Soul in flight on this batch +together with Cut 6c. + +## Probes and source reads this pass + +No cargo build ran this pass. Every new mechanism claim below was settled by a +source read; the one build-dependent claim (rmcp as a stdio server) was settled +by P4 in the first pass and is unchanged. No build outputs were created, so +none were deleted. Claims are marked **(source read)** or **(probe)**. + +| # | Claim | Evidence | +|---|---|---| +| R1 | **No Rust Qdrant client exists anywhere under `F:\Projects`.** | (source read) `grep -rn qdrant --include=Cargo.toml --include=Cargo.lock --include=*.rs` over `F:\Projects` returns nothing in any live tree. | +| R2 | **Epiphany had one, and deleted it.** `856648de` "Delete unused semantic projection subsystem" removed `semantic_backend.rs` (1,018 lines), "Typed boundary around the Qdrant and Ollama HTTP APIs", plus 8,800 more lines of projector. | (source read) `git show 856648de --stat`; `git show 856648de^:epiphany-core/src/semantic_backend.rs`. | +| R3 | **That client was plain `reqwest::blocking` against Qdrant's REST API**, not a Qdrant crate: `PUT/GET/DELETE {base}/collections/{name}`, `PUT {base}/collections/{name}/points`, `POST .../points/query`, `.../points/scroll`, `.../points/delete`. Embeddings were `POST {base}/api/embed` returning `.embeddings`. | (source read) `856648de^:.../semantic_backend.rs:116,130,157,207,244,262,287,318,351,397,535,560`; deps at `856648de^:epiphany-core/Cargo.toml:37` (`reqwest = { version = "0.12", features = ["blocking","json"] }`). | +| R4 | **Epiphany has no CultNet *server* for documents.** Its live `cultnet_rs::` usage is service identity and trust anchors only. Its only UDP binds are the Persona Discord permit issuer, the Persona delivery client and Atlas publication. | (source read) `grep cultnet_rs::` over `epiphany-core/src` yields only `ServiceIdentitySigner`, `GameCultServiceTrustAnchorRecord`, `open/enroll_service_identity_at`, `derive_service_identity_id`. `UdpSocket::bind` appears only at `atlas/transport.rs:245`, `bin/epiphany-persona-discord-permit.rs:38`, `persona_discord_crossing.rs:322`. | +| R5 | **The permit issuer is a hand-rolled request/response loop**, not a reusable document server: `serve_persona_discord_permit_rudp` loops `transport.receive_once()`, matches one `DocumentPutRaw`, and replies with another. | (source read) `persona_discord_permit.rs:330-400`. | +| R6 | **Odin is the real harness to copy.** `odin-daemon` runs `CultMeshRudpDocumentServer::new(socket, SinkHandle, SnapshotHandle, CultMeshSystemClock, options)` and a `poll_once` loop, with Idunn activation, a process write lease, signal handling and presence-health publication. | (source read) `Odin/crates/odin-daemon/src/main.rs:14-40,542-560,600-700`. | +| R7 | **CultMesh's document server is port-shaped and mockable.** `CultMeshRudpRawDocumentSink::accept_raw_document(receipt)` and `CultMeshRudpSnapshotSource::raw_snapshot(&query)` are traits with blanket impls for closures; the clock is a trait. | (source read) `CultLib/packages/cultmesh-rs/src/rudp_document_server.rs:51-95`. | +| R8 | **A lean CultNet daemon needs six dependencies.** `odin-daemon` is `signal-hook, anyhow, chrono, cultcache-rs, cultmesh-rs, cultnet-rs, fs2, rmp-serde, serde`, and Odin's whole lock file is 156 packages. Epiphany's is 295. | (source read) `Odin/crates/odin-daemon/Cargo.toml`; `grep -c '^\[\[package\]\]' Cargo.lock` in both repos. | +| R9 | **`epiphany-core` is 40,941 lines across 37 modules with 39 `mod` declarations**, and pulls Ghostlight, `ed25519-dalek`, `ignore`, `semver`, `cultmesh-rs`, `cultnet-rs` and `windows-sys`. | (source read) `wc -l epiphany-core/src/*.rs`; `epiphany-core/Cargo.toml`. | +| R10 | **Epiphany's commit owner and its profile are crate-private**, so no external crate can reuse admission as it stands. | (source read) `reasoning_context.rs:1590` `pub(crate) struct TypedCommitStore`, `:1606` `pub(crate) const MIND_COMMIT_STORE`, `:1613` `pub(crate) fn commit_authorized_mind_mutation`. | +| R11 | **`RedbMessagePackBackingStore` stores one redb row per `(type, key)`**, transactionally, and creates parent directories. redb permits one writable handle per path, and the CultCache lock owns the open/transaction/close interval. | (source read) `cultcache-rs/src/lib.rs:960-1013`, `:979-982`. | +| R12 | **Nothing consumes `@gamecult/huginn`.** The only `package.json` naming it is Huginn's own. Eve's Huginn entry points at a checked-in fixture file, not the package. | (source read) Grep over every `package.json` under `F:\Projects`; `Eve/web/local-provider-catalog.json:72-83` gives `"url": "./fixtures/huginn-cc-surface.eve"`. | +| R13 | **Eve's Huginn fixture is static and already stale.** It describes `E:\Projects\CultCacheTS\.voidbot\state\huginn.cc` and declares `"freshness": {"state": "fixture"}`, `"splitTarget": "Huginn"`. | (source read) `Eve/web/fixtures/huginn-cc-surface.eve:16`; `huginn-cc-surface.conformance.json`. | +| R14 | **CultCache Studio exists** as a Unity editor surface in CultLib. | (source read) `CultLib/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs`, `CultCacheStudioDrawers.cs`. | +| R15 | **Qdrant on Yggdrasil is voidbot's container**, `qdrant/qdrant:v1.17.1`, host network, bound `127.0.0.1`, storage `/srv/voidbot/qdrant`, started by `voidbot-retrieval.service`. | (source read) `gamecult-ops/compose/voidbot-retrieval.yggdrasil.yaml`; `systemd/voidbot-retrieval.service`. | +| R16 | **Two Ollama endpoints exist, and the Yggdrasil precedent is the local one.** `epiphany.service` embeds against `http://10.77.0.1:11435` with `qwen3-embedding:0.6b`; voidbot's indexer uses Nightwing `10.77.0.3:11434` with the same model. | (source read) `gamecult-ops/systemd/epiphany.service:14-15`; `runbooks/yggdrasil-replacement-2026-07.md:184,193`; `runbooks/voidbot-retrieval-recovery-yggdrasil.md:42-62`. | +| R17 | **Idunn v2 is recipe-plus-binding.** A repo publishes `deployment/idunn/recipe.toml` (`gamecult.idunn.target_declaration.v1`: steps, artifacts, `[service]`, `[state.slots]`, `[[provides]]`, `[[dependencies]]`); Yggdrasil admits a paired `gamecult.idunn.operator_binding.v2` naming runners, workload roots, route, brakes, rollout and placement. | (source read) `Odin/deployment/idunn/recipe.toml`; `Ghostlight/deployment/idunn/recipe.toml:168-187`; `gamecult-ops/idunn/yggdrasil/bindings/odin.toml.in`, `bindings/README.md`. | +| R18 | **Idunn brakes are typed and already separated.** `idunn.deployment_brake.v1` (scope `deployment`) and `idunn.lifecycle_brake.v1` (scope `continuity-restart`) are distinct records with distinct authorities. | (source read) `cultnet-rs/src/idunn_deployment_brake.rs:9-15`; `idunn_lifecycle_brake.rs:4-7`. | +| R19 | **The authority backup is an explicit path list, daily at 03:20 UTC.** It tars a fixed set including `var/lib/gamecult/epiphany` and `srv/voidbot/state`; a path not listed is not backed up. | (source read) `gamecult-ops/scripts/backup-gamecult-authority-yggdrasil.sh:80-105`; `systemd/gamecult-authority-backup.timer`. | +| R20 | **The workstation reaches Yggdrasil by a supervised SSH tunnel with a fixed forward table**, scheduled task `GameCult-Yggdrasil-Tunnel`. It already forwards `17875` (voidbot MCP) and `16333/16334` (Qdrant). | (source read) `gamecult-ops/scripts/start-yggdrasil-tunnel.ps1:13-26`; `runbooks/yggdrasil-ssh-tunnel.md:86-88`. | +| R21 | **Allocated `178xx` RUDP/service ports** are 17870 Idunn health, 17871 Odin rendezvous, 17873 VoidBot swarm publisher, 17874 Hermodr, 17875 voidbot MCP, 17876 Epiphany permit listener, 17877 Starfire permit requester, 17878 dings. **17872 and 17879 are unallocated.** | (source read) `gamecult-ops/inventory.md:247,455,502,537`; `runbooks/yggdrasil-replacement-2026-07.md:315-321`; `idunn/yggdrasil/bindings/*.in`. | + +Carried forward from the first pass and still load-bearing: + +- **P4 (probe).** rmcp 2.2.0 with `features = ["server","macros","transport-io"]` serves stdio JSON-RPC, handshakes at `2025-06-18`, emits both `inputSchema` and `outputSchema` from schemars, returns `structuredContent`, maps `ErrorData::invalid_params` to `-32602`, and spawns nothing. +- **P7 (probe).** cultcache-ts `inspectCultCacheBytes` decodes a Rust-written `cultcache.store.v1` record; Rust writes an empty member catalog, so the TS decode is generic. + +Superseded probes: P5, P6, P8, P9, P10 and P11 all measured git layout, store locking at session scope, or git attribute classification. They were evidence for a repo-committed store and no longer bear on any live design. P1-P3 were Cut 1 and landed. + +## D1. Package boundaries and where admission lives + +This is the decision the brief asks for, so it is stated first. + +**Ruling 1 says Epiphany owns the schemas. The target's End state assigns +admission, queries and the index to the organ** ("**The organ (Huginn).** One +service owns every mind it hosts: admission, with per-document rules and typed +refusals; ... queries ...; semantic search ...; a typed hand-off"). Those are +consistent, and together they settle the split: + +- **Epiphany owns the document types, their keys, their bounds and the published + JSON schemas.** +- **Huginn owns admission rules, receipts, storage, queries, the index, the + hand-off and the CultNet surface.** + +**Admission does not live in `epiphany-core`, and Huginn does not depend on it.** +Three source-grounded reasons: + +1. **It cannot.** The commit owner, its profile and the wrappers are all + `pub(crate)` (R10). Exposing them would publish Epiphany's Mind commit + machinery as a public API to make an unrelated service compile. +2. **The weight is absurd.** Huginn would compile 40,941 lines and 295 lock + packages, including Ghostlight and `ed25519-dalek`, to use about 2,000 lines + of it (R9). Odin's comparable daemon costs 156 packages total (R8). This is + exactly the build fan-out AGENTS.md's Source And Build Economy forbids. +3. **The rules are not the same rules.** Epiphany's admission is bound to a + scheduler: launch requests, sealed reasoning bases, decision contexts. + Pipeline admission is bound to an instance and a campaign. Sharing the + function would mean sharing none of the interesting part. + +**So a third package owns the shared half.** New leaf library +`epiphany-pipeline`, in the Epiphany repo, workspace member, `autobins = false`: + +| | | +|---|---| +| **Owner** | The ten document kinds plus the three new ones, their value types, bound aliases, format rules, key derivation, and the derived JSON schemas. | +| **Dependencies** | `cultcache-rs`, `schemars`, `serde`, `rmp-serde`, `chrono`, `anyhow`. Nothing else. | +| **Consumers** | Huginn's `huginn-mind` (admission) and `eureka-state` (typed tool schemas), both by git rev. Epiphany itself does **not** consume it in this campaign; that is campaign two. | +| **Why a new crate** | A live cross-repo consumer needs these types without Epiphany's 295-package graph. `F:\Projects\CLAUDE.md` says authority separation alone does not justify a crate; this is not authority separation, it is a named external consumer and a hard dependency boundary. | +| **Why in the Epiphany repo** | Ruling 1. The schema publication path, `schemas/cultnet/index.json` and the derivation test stay where they are. | + +`epiphany-core` keeps the schema-derivation test's *outputs* — the committed +`schemas/cultnet/epiphany.pipeline.*.v1.schema.json` files are Epiphany's +publication artifact — but the test that derives and compares them moves into +`epiphany-pipeline`, reading `../schemas/cultnet` the way it already does +(`pipeline_store.rs:1173`). + +**Huginn's package boundary**, applying AGENTS.md's one-package-per-production- +entrypoint rule. Two entrypoints, so two binary-owning packages plus one library: + +| Package | Kind | Owns | +|---|---|---| +| `huginn-mind` | library | Mind storage, admission rules, receipts, queries, derivations, the index port and the embedding port. No process, no socket. | +| `huginn-daemon` | binary `huginn-daemon` | The CultNet surface, the Idunn lifecycle, Qdrant and Ollama adapters, the serve loop. | +| `eureka-state` | binary `eureka-state` | The stdio MCP client. | + +**The daemon test (`F:\Projects\CLAUDE.md`).** `huginn-daemon` earns its process: +it outlives every Claude Code session, owns an independent resource dependency +(Qdrant, a redb store), and isolates a dead session from a corrupted mind. That +is lifecycle, resource and failure isolation, and it protects the named +invariant "exactly one writer per mind". `eureka-state` earns a *separate* +entrypoint but is **not** a daemon: it is a per-session stdio child with a +different dependency set (rmcp, tokio) and a different lifecycle. `huginn-mind` +earns no process at all. + +**Why `eureka-state` lives in Huginn, not Epiphany.** The client and the server +share the request and response document types. Putting the client in Epiphany +would make the wire contract have two owners in two repos. It also keeps rmcp +and tokio out of Epiphany's release bundle, which was the original Cut 4's +reason for a separate package anyway (`construction.rs:82-98` lists the nine +packaged binaries; none changes). + +## D2. Documents + +Unchanged from the landed Cut 3a except as noted: the value-wrapper pattern, the +`value_types!` single field list, the bound aliases (`Short` 200, `Line` 1,000, +`Para` 4,000 UTF-8 bytes), the format types (`Label`, `Slug`, `OrgRepo`, `Sha`, +`FullSha`, `Sha256Hex`, `Date`), key derivation, and the ten kinds all survive +verbatim. The epoch stays `epiphany.pipeline.epoch.v1`. Adding the three kinds +widens the `PipelineKind` enum in the five schemas that embed it, which ruling +8's 2026-09-16 amendment settles as additive. + +**Deleted from the set:** `PipelineWriterHolder` and its wrapper +`EpiphanyPipelineWriterHolder` (`pipeline_documents.rs:334-349,492-497`). They +were the display record of the per-clone lease. + +**Added: three kinds, each with a live consumer.** + +| Kind | Type id | Value fields | Live consumer | +|---|---|---|---| +| `instance` | `epiphany.pipeline.instance.v1` | `instance: Slug`, `display_name: Short`, `created_at: Date`, `host: Short` | The mind's identity document. Admission's identity check (ruling 14); `whoami` in `eureka-state`; the hand-off's `from`/`to`. | +| `stewardship` | `epiphany.pipeline.stewardship.v1` | `instance: Slug`, `repo: OrgRepo`, `assigned_on: Date`, `note: Line` | Ruling 14's "stewardship is an assignment". Query "which repos does this instance steward"; the Rehydrate brief; admission's campaign-repo check. | +| `hand_off` | `epiphany.pipeline.hand_off.v1` | `from_instance: Slug`, `to_instance: Slug`, `repo: OrgRepo`, `documents: Vec[256]`, `reason: Para`, `handed_on: Date` | Ruling 14's "reassignment is an explicit typed hand-off recorded in both minds". Cut 12's import path. | + +**Keys.** *Superseded by Cut 6b's grammar (correction 19, 20). Kept as the +shape Cut 6 landed; the live table is in Cut 6b.* + +| Kind | Key as landed by Cut 6 | Key after Cut 6b | +|---|---|---| +| `instance` | `` | `:instance:self` | +| `stewardship` | `:stewardship:` | unchanged | +| `hand_off` | `:hand_off:..` | `:hand_off:..` | +| campaign | `` | `:campaign:self` | +| resolution | `resolution:` (never stated here before) | `:resolution:.` | +| everything else | `::` | unchanged | + +`` is the `OrgRepo` with `_`, `/` and `.` escaped: `_` → `__`, +`/` → `_-`, `.` → `_d` (the third added at `9b68d83c`, correction 19). `/` is +not a `Label` byte, `.` is the local separator, and the key must segment +unambiguously. Every other byte passes through and is never `_`, so a reader +going left to right takes each `_` with the byte after it and never has a +choice: the encoding is reversible, therefore injective. Admission recomputes +it and refuses a mismatch with the existing `InvalidIdentity`. + +**Corrected 2026-09-16, after Soul.** This read "`/` replaced by `_`", which is +not injective: `GameCult_Epiphany/thing` and `GameCult/Epiphany_thing` are both +well-formed `OrgRepo` and both keyed to `GameCult_Epiphany_thing`. An instance +stewarding both would have silently lost one document. The doubling trick alone +(`_`→`__`, `/`→`_`) does not fix it either — `a_/b` and `a/_b` both give +`a___b`. Sample keys move accordingly: `…:stewardship:GameCult_-Epiphany`. + +**Resolution matrix additions.** `stewardship` resolves by +`Superseded{by: stewardship}` or `Withdrawn`. `instance` and `hand_off` are not +resolvable. Everything else is unchanged. + +**`PipelineRefusal` splits.** The landed enum (`pipeline_store.rs:41-55`) mixes +document refusals with store, git and lease refusals. It becomes two: + +- `epiphany-pipeline` keeps the document half: `FieldBound`, `InvalidFormat`, + `InvalidIdentity`. +- `huginn-mind` owns the service half: `MissingIdentity`, `ForeignEpoch`, + `ForeignStore`, plus every admission-rule refusal (`MissingReference`, + `WrongReferenceKind`, `IdentityCollision`, `AlreadyResolved`, + `IncompatibleResolution`, `CitesResolvedDocument`, `RevisionWithoutSupersession`, + `InvalidOptions`, `InvalidChoice`, `RepoNotInCampaign`, `DuplicateLabel`, + `CutReportWithoutSpec`, `RangeOutsideCommits`, `FalsifiedClaimWithoutConfirmedFinding`, + `UnprovenClaimWithConfirmedFinding`, `FindingWithoutRange`, `FindingWithoutEvidence`, + `UnknownInvariant`), plus `ForeignInstance { declared, mind }` (ruling 14) and + `Unavailable`. + +**Deleted refusals:** `NotRepoRoot`, `NoMainWorkTree`, `StoreNotMarkedBinary`, +`LockNotIgnored`, `WrongBranch`, `WriterLeaseHeld`. Every one of them is a +statement about a repo-resident store. + +## D3. The organ's storage and admission + +**One mind per instance, one redb store per mind.** +`/minds//mind.cc` as a `RedbMessagePackBackingStore` +(R11). Redb, not the single-file store, because a mind grows without bound and +the single-file backing store rewrites the whole snapshot on every write; that +is the monolithic-store footgun `~/.claude/CLAUDE.md` names under +Infrastructure. Odin uses the single-file store, but its topology store is small +and bounded. + +**Identity.** The first write to a mind carries both the +`EpiphanyPipelineIdentity` (epoch) and the `instance` document, in one batch. +The opener refuses records-without-identity (`MissingIdentity`), a foreign epoch +(`ForeignEpoch`) and any unregistered type (`ForeignStore`), exactly as the +landed `pipeline_cache` does (`pipeline_store.rs:87-120`) — that logic moves +almost verbatim; only its backing store changes. + +**Admission.** One public entry: + +``` +admit(&mut Mind, PipelineAdmissionBatch) -> PipelineAdmissionOutcome +``` + +with `PipelineAdmissionBatch { instance, provenance, documents (1..64) }` and the +outcome one of `Committed { receipt_id, committed_at, writes }`, +`AlreadyAdmitted { receipt_id }`, `Refused(PipelineRefusal)` or +`Conflict { identities }`. + +Steps, in order: + +1. **Identity check (ruling 14).** `batch.instance` must equal the mind's + `instance` document, or `ForeignInstance`. This is the check that replaces + the whole lease. +2. Validate bounds and formats. +3. Recompute keys. +4. Check references against the store image plus the batch. +5. Apply the per-kind rules (unchanged from the old D4, minus the campaign + `origin` check, which no longer has a repo to inspect — `repo` must instead + be one the instance stewards, giving `RepoNotInCampaign` a new source). +6. Derive writes (the `Answered` resolution for a ruling that answers a + question; the paired `stewardship` records on a `hand_off`). +7. Commit. + +**Receipts stay, and Huginn owns its own commit primitive.** `huginn-mind` +defines `HuginnCommitReceipt` with its own type id +`huginn.mind_commit_receipt.v1`, carrying the same shape that earned its keep in +Epiphany: authority, invariant owner, strong reads, writes, a content-digest +receipt id, and `committed_at`. It replays idempotently on an exact match and +returns typed `Conflict` on a lost CAS. + +This is deliberately **not** a reuse of `EpiphanyMindCommitReceipt`. The landed +Cut 3a reused it with `store_id = "epiphany-pipeline"` because both lived in one +crate; across a repo boundary that reuse would drag the whole crate (D1). +Roughly 120 lines of receipt/replay/CAS logic are re-implemented. That is a real +duplication and it is the honest price of the service boundary — it is named +here, in the subtraction ledger, and as follow-up FU-3, rather than hidden. + +**Admission time** lives on the receipt, never in a document, so exact replay +stays byte-identical. Unchanged. + +**Concurrency.** The organ is the only writer to any mind it hosts, and it is +single-process. CAS stays as defence in depth against its own bugs and against +the import path. The old session lease has no successor: there is nothing left +for it to exclude. + +## D4. Queries + +*Superseded by Cut 9 as landed (`49cc6e3`), 2026-09-16. Still true: one +owner for every derivation, status derived at read time, the open-items +sentence. Now false: `get` is `view`; `query` returns a +`PipelineQueryPage { items, matched }`, not a `Vec`; `rulings_in_force` and +`stewardship` were never built and are query presets; the view is +`{ id, document, admission, status }`, not the seven-field shape below; +`PipelineQuery` has no `status`, `outcome`, `text_contains` or `instance` +and has `in_force: Option`; `semantic` is refused typed until Cut 11; +"in force when no resolution names it" is the non-recursive form, and the +landed rule is "no resolution that is itself in force" (Q17 B); `history` +exists and is missing here. The Cut 9 section is the owner.* + +`huginn-mind` owns every derivation; no client re-derives status. + +- `get(id) -> Option` +- `query(&PipelineQuery) -> Vec` +- `open_items(campaign) -> PipelineOpenItems` +- `rulings_in_force(Option) -> Vec` +- `stewardship(instance) -> Vec` + +`PipelineDocumentView` is +`{ id, kind, document, resolution: Option<(id, PipelineResolution)>, receipt_id, admitted_at, faculty }`, +its admission fields joined from the receipts naming the document. + +`PipelineQuery` keeps its landed field set — `campaign`, `repo`, `cut`, `kinds`, +`status`, `outcome`, `faculty`, `admitted_after/before`, `text_contains`, +`limit` ≤ 200 — and gains `instance` and `semantic: Option<{ text, top_k }>`. +When `semantic` is set the query runs through the index (D5) and the hit ids are +resolved back through the typed store, so the store stays the truth. + +Derived state, never stored, unchanged from the landed design: a document is +**in force** when no resolution names it; **open items** are unresolved +questions, findings and follow-ups, in-force cut specs with no report, and cut +reports with no verdict. + +## D5. The index + +**Owner:** `huginn-daemon`, through two ports defined in `huginn-mind` so the +library stays testable without either service: + +``` +trait EmbeddingPort { fn embed(&self, texts: &[String]) -> Result>>; } +trait IndexPort { fn upsert(&self, points: Vec) -> Result<()>; + fn search(&self, vector: Vec, top_k: u32, filter: IndexFilter) -> Result>; + fn delete(&self, ids: Vec) -> Result<()>; } +``` + +**Adapters.** `QdrantIndex` and `OllamaEmbedding`, both plain `reqwest::blocking` +against the REST endpoints R3 establishes, ported in shape from the deleted +`semantic_backend.rs`. That module is the precedent: it confined JSON to one +xenos-facing boundary and treated Qdrant as "a rebuildable projection rather +than canonical authority", which is exactly this design's relationship to it. + +**Collections.** `huginn_pipeline_documents`, one point per document, vector +size 1,024 (`qwen3-embedding:0.6b`, R16). Payload `{ instance, campaign, repo, +kind, doc_id, admitted_at }`, with payload indexes on `instance`, `campaign`, +`repo` and `kind`. Receipts, identity and provenance are never indexed. +In-force status is never indexed; clients resolve it through `get`. + +**Indexing is at admission, in the same call, but not in the same transaction.** +The typed commit lands first; the index upsert follows. If the upsert fails the +admission still succeeded, and the organ records the document id in a +`pending_index` slot and retries on its next poll. The index is a projection, so +a stale index is a degraded read, never a lost write. The reverse order would +let an index failure reject an admitted document. + +**Rebuild.** `huginn-daemon --reindex ` drops and rebuilds the +collection from the typed store. This is the affordance that makes the +projection disposable, and it is the negative proof that the index is not truth. + +## D6. The CultNet surface + +*Partly superseded by Cut 9 and Q13 A, 2026-09-16: the `RulingsInForce` +and `Stewardship` operations are presets of `Query`; there is no `History` +operation, which Q17 B's obligation requires; the wire types are +`huginn-mind`'s, carried by the client for types only. The Cut 10 refresh +is the owner of the surface's shape.* + +**Copy Odin's harness (R6, R7).** `huginn-daemon` binds one loopback UDP socket +from `GAMECULT_IDUNN_CANDIDATE_BIND`, constructs +`CultMeshRudpDocumentServer::new(socket, sink, snapshot, CultMeshSystemClock::default(), options)`, +and runs a `poll_once` loop with `signal-hook` handling SIGTERM/SIGINT. + +Requests arrive as `DocumentPutRaw` and replies go back as `DocumentPutRaw`, +the shape Epiphany's permit issuer already uses (R5) and that the document +server routes natively. Two request documents and two response documents: + +| Document | Type id | Payload | +|---|---|---| +| request | `huginn.mind_request.v1` | `instance`, `operation: Admit \| Get \| Query \| OpenItems \| RulingsInForce \| Stewardship \| HandOff \| Import`, and the operation's typed argument | +| response | `huginn.mind_response.v1` | `request_id`, `outcome: Ok(payload) \| Refused(PipelineRefusal)` | + +Read operations are also served through `CultMeshRudpSnapshotSource`, so a +plain CultNet snapshot client can read a mind without speaking the request +document at all. That is the affordance that keeps the MCP surface swappable. + +**Schema publication.** `huginn.mind_request.v1` and `huginn.mind_response.v1` +are Huginn's contracts and are published from Huginn, not Epiphany. Ruling 1 +covers pipeline *state* schemas, which stay in `schemas/cultnet/`. The organ's +transport contracts belong to the provider that owns the boundary, which is +exactly what `schemas/cultnet/README.md:52-56` already says. + +**Port.** `rudp://10.77.0.1:17872`, the lower of the two unallocated `178xx` +ports (R21), with a private candidate range `27880-27887` by analogy with +Odin's `27872-27879`. + +## D7. `eureka-state` + +*Partly superseded by Cut 9 and Q13 A, 2026-09-16: the `query` output is a +page with `matched`; the `rulings_in_force` and `stewardship` tool rows are +presets; there is no `history` tool, without which the Q17 affordance +stops at the crate boundary; "types are the `epiphany-pipeline` types" is +false for the view and query types, which are `huginn-mind`'s. The Cut 13 +refresh is the owner.* + +A stdio MCP server that is a thin CultNet client. It owns no state, no cache and +no fallback. + +| Tool | Input | Output | +|---|---|---| +| `admit` | `{ instance, provenance, documents }` | `PipelineAdmissionOutcome` | +| `get` | `{ instance, id }` | `{ found, view }` | +| `query` | `{ instance, query: PipelineQuery }` | `{ views }` | +| `open_items` | `{ instance, campaign }` | `PipelineOpenItems` | +| `rulings_in_force` | `{ instance, campaign? }` | `{ views }` | +| `stewardship` | `{ instance }` | `{ repos }` | +| `whoami` | `{}` | `{ instance, organ_endpoint, reachable }` | + +Input and output types are the `epiphany-pipeline` types deriving `JsonSchema`, +returned as `Json`, so tool schemas equal the published schemas (P4). +Refusals are typed outcomes, not JSON-RPC errors; malformed input is +`invalid_params`. + +**No `repo_root` anywhere.** The old design threaded it through every tool +because the store was a file in a repo. The mind is now addressed by instance. + +**Configuration.** Two environment variables, `EUREKA_ORGAN_ENDPOINT` (default +`rudp://127.0.0.1:17872`) and `EUREKA_INSTANCE`. No config file. + +**Unreachable organ (target, "Availability is honest").** Every tool returns +`{ refused: Unavailable { detail } }` naming the endpoint and the failure. The +server never spools, never caches, never degrades to a local file. `whoami` +exists so an agent can check reachability in one call before starting a +campaign, and the Rehydrate brief calls it first. + +**Registration is the operator's, not Hands'.** After Hands reports the binary +path, the operator runs: + +``` +claude mcp add --scope user --transport stdio eureka-state --env EUREKA_INSTANCE= --env EUREKA_ORGAN_ENDPOINT=rudp://127.0.0.1:17872 -- C:\Users\Meta\.eureka\bin\eureka-state.exe +``` + +Unprobed: the exact `--env` spelling on CLI 2.1.268. Hands confirms it from +`claude mcp add --help` and reports it; the operator runs the confirmed line. + +## D8. The trust boundary + +**State it plainly: on a single-operator LAN there is no authentication here, +and the design does not pretend otherwise.** + +The organ binds loopback on Yggdrasil and is reached from the workstation over +the existing supervised SSH tunnel (R20). Anything that can open that socket can +declare any instance. `ForeignInstance` is therefore a **collision and +attribution** control — it stops instance A writing into B's mind by mistake, +and keeps history attributable — not an access control. + +This matters because the alternative is available and already used in this +codebase: `cultnet-rs` ships `ServiceIdentitySigner`, `enroll_service_identity_at` +and trust anchors, and Epiphany's permit path signs with them (R4). Enrolling a +per-instance identity would make `ForeignInstance` enforceable. It would also +mean distributing and rotating a key to every workstation that runs Claude Code. + +**This is a real fork; see Q6.** Do not let Hands invent a credential system. + +## Cut 4. Delete the repo-store, lease and git layers + +- **Repo/branch:** Epiphany `codex/eureka-pipeline-state`. No dependencies. +- **First:** confirm `git status` is clean at `5fb4eb22`. + +**Deletes first.** + +| Path | Lines | What dies | +|---|---:|---| +| `epiphany-core/src/pipeline_store.rs` | 1,251 | **The whole file.** See below. | +| `epiphany-core/src/pipeline_documents.rs:334-349` | 16 | `PipelineWriterHolder` and its `Bounded` impl | +| `epiphany-core/src/pipeline_documents.rs:492-497` | 6 | `EpiphanyPipelineWriterHolder` wrapper | +| `epiphany-core/src/pipeline_documents.rs:602-617` | 16 | `validate_pipeline_writes`, the commit-profile validator | +| `epiphany-core/src/lib.rs:21` | 1 | `mod pipeline_store;` | +| `epiphany-core/src/lib.rs:132` | 1 | `pub use pipeline_store::{...};` | +| `.gitattributes:1` | 1 | `/.epiphany/pipeline/pipeline.cc binary` | +| `.gitignore:37` | 1 | `/.epiphany/pipeline/*.lock` | + +**Yes, `pipeline_store.rs` dies whole.** Every one of its parts is a repo-store +part: + +- `PIPELINE_COMMIT_STORE` (`:76-81`) — the commit profile, dead with Cut 5. +- `pipeline_cache` (`:87-120`) — the epoch/foreign-type opener. Its *logic* + survives, but it is re-authored against redb in Cut 8; it is not moved, + because its signature takes a `RuntimeSpineBackingStore`. +- `git`, `git_line`, `committed_line` (`:125-162`) — the committed-blob checks. +- `require_repo_root`, `main_work_tree`, `common_dir_of` (`:165-201`) — ruling + 10's resolution. +- `PipelineStore` (`:204-226`) — the read path, now CultNet. +- `PipelineWriter`, `attach`, `require_branch`, `Drop` (`:230-331`) — the lease + and branch binding. +- `live_holder`, `current_holder`, `holder_cache`, `read/write_writer_holder` + (`:334-387`) — the holder record. +- `mod tests` (`:389-1251`, 863 lines) — 15 tests, all of which pin a deleted + rule. The four that pin *document* rules — `every_pipeline_kind_round_trips_through_named_slot_zero`, + `bounds_refuse_in_utf8_bytes`, `repo_fields_must_be_org_slash_repo`, + `keys_are_derived_and_mismatch_refuses`, `composed_keys_cannot_collide`, + `parent_ids_are_parsed_strictly`, `resolution_subject_is_a_full_id_of_its_kind` + — **move to Cut 6**, not deleted. They are listed here as moves so the count + is honest. + +**Keeps.** + +- Every document kind, value type, bound alias, format rule and key derivation + in `pipeline_documents.rs`. +- `validate_pipeline_write_envelope` (`:621-645`), bounds plus key recomputation. +- All ten `schemas/cultnet/epiphany.pipeline.*.v1.schema.json` files and their + `index.json` entries. Ruling 1 keeps them; Cut 6 adds three more. +- `.gitattributes:2`, `/schemas/cultnet/epiphany.pipeline.*.schema.json text eol=lf`. +- `process_observation::capture_process_instance` — other callers at + `bin/epiphany-mvp-coordinator.rs:257,1703` and `bin/epiphany-swarm.rs:211,723`. +- `repository_body_observer::repository_git_command` — other callers at + `repository_body_observer.rs:734,745,1041,1054,1063,1077`. +- `fs2` in `epiphany-core/Cargo.toml` — still used by + `packaged_release/construction.rs`. +- `schemars` — Q5 keeps it; Cut 6 needs it. + +**Per-file changes.** `pipeline_documents.rs:19` drops +`use crate::pipeline_store::PipelineRefusal;` and gains a local `PipelineRefusal` +holding only `FieldBound`, `InvalidFormat` and `InvalidIdentity`, with its +`Display` and `Error` impls carried from `pipeline_store.rs:57-63`. This is a +temporary home; Cut 6 moves the whole module out. + +**Authority map.** + +- **Owner:** nothing. This cut removes an owner and installs no replacement; the + organ becomes the owner in Cut 8. +- **Inputs / outputs:** none. +- **Derived state:** none. The holder file is gone. +- **Forbidden writers:** after this cut, no Epiphany code may open a pipeline + store, take a lease, or read git for pipeline purposes. +- **Shared paths:** none remain. +- **Deletion line:** the table above. + +**Verification.** + +- **Builds:** `cargo check -p epiphany-core --lib --tests`, then + `cargo check -p epiphany-release-bundle --bin epiphany-state`. One at a time, + `CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex`. +- **Tests:** `cargo test -p epiphany-core --lib`. Expect 180 − 15 = **165**, and + the report states the exact number. The surviving document tests are re-added + in Cut 6, so this cut legitimately reduces coverage; Soul checks that only the + named 15 disappeared. +- **Negative greps**, all empty: + - `rg -n "PipelineWriter|PipelineStore|WriterLeaseHeld|main_work_tree|check-attr|git-common-dir" epiphany-core/src` + - `rg -n "\.epiphany/pipeline" -- . ':!notes'` + - `rg -n "pipeline" .gitattributes .gitignore` returns only the schema + `eol=lf` line. +- **Operator:** none. + +**Subtraction ledger:** −1,291 lines, +about 20 (the reduced refusal enum). No +dependency, format or target change. + +## Cut 5. Collapse `TypedCommitStore` back into the Mind commit owner + +- **Repo/branch:** Epiphany, same branch. Depends on Cut 4. +- **Why this cut exists.** Cut 2 generalised the commit owner so a *second* + profile could use it. Cut 4 deletes that second profile, and D3 puts the + organ's receipts in another repo. `TypedCommitStore` is now a one-implementation + abstraction surviving only because it exists — a named STOP condition in + `~/.claude/CLAUDE.md`. Honest accounting: **Cut 2's generalisation did not pay + off**, and leaving it in place to avoid admitting that is the failure mode the + doctrine warns about. +- **Why it is a separate cut.** It is pure subtraction on the Mind path, and + Soul must be able to falsify "Mind behaviour is unchanged" without pipeline + code in the diff. + +**Deletes first.** + +| Path | Lines | What dies | +|---|---:|---| +| `reasoning_context.rs:1590-1597` | 8 | `struct TypedCommitStore` | +| `reasoning_context.rs:1599-1604` | 6 | `fn validate_mind_writes` (inlined) | +| `reasoning_context.rs:1606-1611` | 6 | `const MIND_COMMIT_STORE` | +| `reasoning_context.rs:1614` | 1 | the `store: &TypedCommitStore` parameter | +| `reasoning_context.rs:1435,1466,1516,1540,1566` | 5 | the `&MIND_COMMIT_STORE` argument at each of the five wrappers | +| `reasoning_context.rs:2521-2530`, `:2599-2610` | ~20 | `TEST_COMMIT_STORE`, `NOW_REFUSING` and the profile-parameterised test harness at `:2540` | + +**Per-file changes.** In `commit_authorized_mind_mutation` +(`reasoning_context.rs:1613-1622`), re-inline the three choices the profile +carried: `runtime_spine_backing_store` at `:1641`, +`crate::runtime_spine::open_runtime_spine_cache` at `:1642`, +`validate_mind_write_envelope` at `:1644`, and the literal `"epiphany-mind"` at +`:1665` and `:1669`. + +**Keeps — and this is the part that must not be lost.** Cut 2's *tests* proved +real rules and stay, re-expressed against the concrete owner: + +- the Mind epoch refusal is pinned (mutation M2b dropped the check and failed); +- validation-before-replay is pinned; +- the owner reads and writes one store, never two. + +Do **not** delete a verifier to make this cut smaller. AGENTS.md's Verification +Guardrails: preserve the claim in the smallest owning surface. + +**Authority map.** + +- **Owner:** `commit_authorized_mind_mutation`, still the only *production* code + that builds receipts, replays, and runs batch CAS — now concretely, for Mind + only. One test plants a receipt on purpose, which is how the replay rule is + pinned; that is the second construction site and it is deliberate. +- **Inputs:** authority, invariant owner, strong reads, writes, companions, time. +- **Outputs:** `EpiphanyMindCommitOutcome`. +- **Derived state:** `store_id` inside the receipt's document versions, now the + constant `"epiphany-mind"`. +- **Forbidden writers:** any new function building an `EpiphanyMindCommitReceipt` + or calling `compare_and_swap_batch` with a receipt. +- **Shared paths:** the five Mind wrappers. There is no sixth. +- **Deletion line:** the table above. + +**Verification.** + +- **Tests:** `cargo test -p epiphany-core --lib` passes at the Cut 4 count. + `disjoint_mind_mutations_merge_and_same_identity_conflicts` + (`reasoning_context.rs:2482`) pins Mind CAS and replay unchanged. +- **Behaviour-unchanged proof, per the Hands brief:** a Mind receipt id captured + by running the pin test **at base `5fb4eb22`** must equal the id after this + cut. A value captured from the new code is not evidence. +- **Mutations:** drop the epoch check from the opener; make the validator a + no-op; make the owner read one store and write another. Each must fail its + named test. +- **Negative greps:** `rg -n "TypedCommitStore|MIND_COMMIT_STORE" epiphany-core/src` + is empty. + +**Subtraction ledger:** −46 lines, +about 8 re-inlined. No dependency change. + +**If Soul or Hands finds a live second consumer this cut is wrong** — stop and +report rather than deleting. The map asserts there is none; `rg` is the check. + +## Cut 6. Extract `epiphany-pipeline` + +- **Repo/branch:** Epiphany, same branch. Depends on Cut 4. + +**Adds: package `epiphany-pipeline/`.** + +- `Cargo.toml`: `autobins = false`, `license-file = "../LICENSE"`, dependencies + `anyhow`, `chrono`, `cultcache-rs` (pinned `a0813c6`), `rmp-serde`, `schemars = "1"`, + `serde`. Dev-dependencies `serde_json`, `tempfile`. +- `src/lib.rs`: the whole of `pipeline_documents.rs` (623 lines after Cut 4), + moved verbatim apart from the module doc's cut-map references, plus the + reduced `PipelineRefusal`. +- The three new kinds from D2, their keys, and their entries in the + `pipeline_kinds!` macro. + +**Moves.** + +- `epiphany-core/src/pipeline_documents.rs` → `epiphany-pipeline/src/lib.rs`. +- The seven document tests named in Cut 4 → `epiphany-pipeline/src/lib.rs` + tests, minus their git-repo scaffolding (`campaign_repo`, `git_ok`, `attach`, + `holder`), which has nothing left to set up. +- `pipeline_published_schemas_match_derivation` (`pipeline_store.rs:1172-1203`) + → `epiphany-pipeline`, still reading `../schemas/cultnet` via + `env!("CARGO_MANIFEST_DIR")`. + +**Deletes.** `epiphany-core/src/lib.rs:20` (`mod pipeline_documents;`) and +`:131` (`pub use pipeline_documents::*;`). `epiphany-core` then has no pipeline +surface at all, which is correct: its consumption is campaign two. + +**Per-file changes.** + +- Root `Cargo.toml:2-8`: add `"epiphany-pipeline"` to workspace members. It is + **not** added to `[dependencies]`, and no `[[bin]]` references it. +- `schemas/cultnet/index.json`: three new entries in the existing shape + (`kind: document_payload`, `wireContracts: ["cultnet.schema.v0"]`) for + `instance`, `stewardship` and `hand_off`. +- Three new `schemas/cultnet/epiphany.pipeline..v1.schema.json` files, + derived, never hand-written. +- `schemas/cultnet/README.md:29-31`: the Main Families entry currently says + pipeline state is "stored per repo at `.epiphany/pipeline/pipeline.cc`". + Replace with: stored in an instance's mind, owned by the Huginn memory organ; + name the three added kinds. +- `schemas/cultnet/README.md:33-41`: the wire note says these contracts "cross to + Eureka MCP clients and the voidbot projection". Replace the voidbot half: they + cross to the Huginn organ and its `eureka-state` client. Keep the `[value]` + payload sentence, the derivation sentence and the Q2 evolution sentence. + +**Authority map.** + +- **Owner:** `epiphany-pipeline`, for document shape, bounds, formats, keys and + derived schemas. +- **Inputs:** none; it is a pure type library. +- **Outputs:** typed documents, derived JSON schemas, `pipeline_key`. +- **Derived state:** the published schema files. +- **Forbidden writers:** nothing in this package may open a store, spawn a + process, or reach a network. It has no `std::process`, no socket and no + backing-store dependency beyond `cultcache-rs` types. +- **Shared paths:** `huginn-mind` and `eureka-state`, both by git rev. +- **Deletion line:** `epiphany-core`'s two pipeline modules and their exports. + +**Verification.** + +- **Builds:** `cargo check -p epiphany-pipeline --lib --tests`, then + `cargo check -p epiphany-core --lib --tests`. +- **Tests:** `cargo test -p epiphany-pipeline --lib`. The nine moved tests + (seven plus the derivation test plus `decode_refuses_an_envelope_of_a_foreign_type`, + which Cut 4's fix batch added after this was written), plus four new ones: + `instance_stewardship_and_hand_off_round_trip`, + `stewardship_key_escapes_the_repo_slash`, `hand_off_names_both_instances`, and + `keys_read_back_as_ids_of_their_kind`. Thirteen in total. +- **Key⇄id round-tripping is the test shape this cut was missing.** The first + three key tests assert key strings and never parse them back, which is why + Soul found an unvalidated instance segment and a reader that refused the new + root kind. `keys_read_back_as_ids_of_their_kind` walks every sample, keys it + and parses it back. Resolution is the one kind whose key is not an id of + itself; the test states that rather than skipping it. +- **The derivation test is the schema gate.** On mismatch it writes the derived + file to a per-run directory under `std::env::temp_dir()` named by pid and + nanos, and fails naming the path. There is no bless flag, and no directory is + shared between runs. Thirteen schemas must now match. +- **Mutations:** add a variant to one value type's enum without regenerating its + schema — the derivation test fails. Change `` escaping to keep the + slash — the key test fails. (A *field* cannot be added to those macro-built + value types without failing the whole target, which would prove nothing about + the derivation test in particular; the variant changes exactly one derived + schema.) +- **Negative greps:** + - `rg -n "serde_json::Value|Vec" epiphany-pipeline/src` empty + (`serde_json` is a dev-dependency only). + - `rg -n "std::process|UdpSocket|BackingStore" epiphany-pipeline/src` empty. + *Corrected (21): `process::Command|UdpSocket|BackingStore`; the per-run + test directory reads `std::process::id()` under `#[cfg(test)]`.* + - `rg -n "pipeline" epiphany-core/src` empty. *Corrected (15): the narrower + grep over the type names.* +- **Operator:** none. + +**Subtraction ledger:** Epiphany-core −623 lines; new package +about 700 +including the three kinds, +about 400 derived JSON. Net repo change is small; +the point is the dependency boundary, not the line count. + +## Cut 6b. The key grammar + +Anchor: `epiphany-pipeline/src/lib.rs` at `4a654351`, 1,508 lines. Lands +**before** Cut 6c and does not absorb it. Spec by Imagination 2026-09-16; +in Hands. + +### Why a grammar and not a fourth fix + +Three passes each made one composition site injective and left the site above +it ambiguous. The key space had **four** shapes and no single reader: `` +for campaign and instance (two early returns at `:693-700`), `::` +for ten kinds (`KeyParts::key`, `:671-683`), `resolution:` (a third +early return at `:703-707`), and recursively `resolution:resolution:…`. Three of +the four are written by an early `return` that never reaches the composer, so +the reader `pipeline_id` (`:545-586`) needs a `match kind` with a root arm and a +recursive arm to undo them, plus `declared_kind` (`:529-538`) to guess the kind +of a string that does not carry one. Every confirmed defect lives in one of +those exceptions. A fourth pass would guard an arm that should not exist. + +### The grammar + +``` +key ::= root ":" kind ":" local +root ::= slug +kind ::= one of the thirteen `PipelineKind` names, literally +local ::= part ( "." part )* +part ::= label +slug ::= label ( "." label )* ; total <= 64 bytes +label ::= [A-Za-z0-9_-]{1,64} +``` + +Two byte facts already in the source: `label_text` (`:118-126`) admits neither +`.` nor `:`; `dotted_text` (`:131-139`) is `label ("." label)*` bounded at 64 +bytes with no empty parts. Two rules the cut adds: + +- **R1. Every key has exactly three segments.** Roots included. +- **R2. No local part is a `Slug`.** A `Slug` or `OrgRepo` entering a local is + escaped to one label first. So `local.split('.')` recovers its parts exactly + at any arity. + +Unambiguity then follows from the alphabet, not from checks: `:` is in no part, +so segment recovery is forced; the kind is a literal segment, so nothing is +inferred; `.` is in no part, so local recovery is exact. `declared_kind` has no +successor because there is nothing left to guess. + +| Kind | Key | +|---|---| +| campaign `c` | `c:campaign:self` | +| instance `y` | `y:instance:self` | +| resolution of `c:question:Q1` | `c:resolution:question.Q1` | +| resolution of campaign `c` | `c:resolution:campaign.self` | +| resolution of that resolution | `c:resolution:resolution.question.Q1` | +| hand-off | `:hand_off:..` | +| stewardship | `:stewardship:` | +| the nine others | `::`, unchanged | + +Roots take the reserved constant `self` as local; a reader recovers a root's +identity from segment 1 and never consults it. A resolution is keyed inside its +subject's root with the subject's kind and local as its own local. Nesting is +bounded by construction, not by a guard: a nested local is +`11 * (depth - 1) + len(subject local)` bytes against the 64-byte bound, so a +one-byte subject local nests six deep and `question.Q1` five, and +`pipeline_id` does not recurse. *(Corrected after Soul F1: this first said +"at most five"; the bound is the local, and the depth follows from the subject.)* + +**The one escape, generalised.** `repo_segment` (`:619-636`, `_`→`__`, +`/`→`_-`, `.`→`_d`, injectivity verified by Soul) becomes `key_segment` and +gains one caller: `hand_off.to_instance`, the only place a raw `Slug` reached a +local (`:720`) and exactly Soul's pass-2 collision pair, closed from the other +side. Its output is not assumed to be a label; `Game Cult/x` escapes and is +refused downstream by `label_text`, which is already tested. + +**Where the total length bound lives:** in one function, +`local(field, parts: [&str; N])`, which runs every part through +`label_text` and the join through one 64-byte `bound`. It is not a type fact and +is not dressed as one; it is pinned by M4. Today the total check (`:681`) is the +second of two `dotted_text` calls on overlapping data, which is why deleting it +was invisible to every sample. + +**Where tail arity lives:** `local` takes an array, not a chainable builder, so +a conditional tail cannot be written as a quiet extra chain call. That does +*not* prove one arm emits one arity, and after this cut arity is no longer +load-bearing: under R2 a local reads back at any arity. The property the doc +comment at `:663-670` defended is retired, and the invariant that replaces it, +"no local part carries the separator", has a runtime mutation (M6). + +### Deletes first + +| Path | Lines | What dies | +|---|---:|---| +| `lib.rs:526-538` | 13 | `declared_kind`. Defect 2's hardcoded `Campaign` dies with the function. | +| `lib.rs:550-570` | 21 | `pipeline_id`'s `match kind`: the root arm, the resolution arm and its recursion, and the comment defending them. | +| `lib.rs:638-684` | 47 | `KeyParts`, its constructors and `key`. Replaced by a free function, not renamed. | +| `lib.rs:692-707` | 16 | `pipeline_key`'s three early `return`s. The cut is not done until they are gone. | + +97 lines. No file, schema file or type dies. The deletes go **before** `local` +is written; if they coexist with the new composer at any commit the cut has +produced a fourth shape. + +### Keeps, moves, adds + +Keeps: every kind, type id, field, bound and format type; no `value_types!` or +`pipeline_kinds!` entry moves; `PipelineRef` and its `Bounded` impl +(`:365-370`) unchanged. `repo_segment` → `key_segment`, same body. +`parent_local` (`:589-600`) unchanged. `parent_cut` (`:605-617`) keeps both +rules; `rsplit_once('.')` becomes an index into the split, net −4. The +outcome-invariance assertion in `keys_are_derived_and_mismatch_refuses` +(`:1077-1084`) and the `!key.starts_with(":")` assertion in +`instance_stewardship_and_hand_off_round_trip` (`:1311-1315`) stay true. + +Adds: `const ROOT_LOCAL: &str = "self"` and `fn local`. `pipeline_id` is +**replaced whole**, one shape with no `match kind`: exactly three segments, kind +segment equals `kind.name()`, root through `dotted_text`, every local part +through `label_text`, local bounded whole, returns `(root, local)`. + +### Per-file changes, `epiphany-pipeline/src/lib.rs` + +| Line | Change | +|---|---| +| `:12-13` | Module doc states the grammar in four lines. | +| `:117` | `Label`'s doc comment retargets `KeyParts::key` to `local`. | +| `:526-538` | Delete `declared_kind`. | +| `:540-586` | Replace `pipeline_id` whole. | +| `:605-617` | `parent_cut`: index into `local.split('.')`. | +| `:619-636` | Rename `repo_segment` → `key_segment`; generalise its doc comment. | +| `:638-684` | Delete `KeyParts`; add `local` and `ROOT_LOCAL`. | +| `:687-752` | `pipeline_key`: thirteen arms, one exit. Campaign/Instance emit `local(field, [ROOT_LOCAL])`. Resolution parses its subject id once, then `local(field, [subject_kind.name(), subject_local_parts…])`. `HandOff` gains `key_segment` on `to_instance`. | +| `:780-781`, `:824-934` | Thirteen expected sample keys move. | +| `:1088-1166` | `composed_keys_cannot_collide`: the probe built `KeyParts` directly and moves to `local`; the hand-off pair keeps its conclusion with a dotted receiver now escaping to `thought-cage_dGameCult`. | +| `:1256-1291` | `a_resolution_is_named_by_the_key_it_has`: `{CAMPAIGN}:resolution:R8` becomes well-formed grammar and leaves the refused list; reachability is admission's rule. Said in the doc comment. | +| `:1329-1363` | `keys_read_back_as_ids_of_their_kind`: **body unchanged.** | +| `:1419-1455` | `hand_off_names_both_instances`: add a dotted-receiver assertion. | + +`schemas/cultnet/`: **no file changes.** `notes/eureka-pipeline-state-cut.md`: +the Keys table above is marked superseded and the scar at the old `:483-488` +stays. + +### Authority map + +- **Owner:** `pipeline_key` (`:687`), through the single composer `local`. No + arm has a path to a key the composer never saw. +- **Inputs:** the document's own fields. No clock, store, registry or counter. +- **Outputs:** one `String`, or a `PipelineRefusal` naming the field. +- **Derived state:** the key is derived, never stored. `pipeline_id` is the key + read backwards and holds no state. `declared_kind`'s inference is deleted. +- **Forbidden writers:** no arm of `pipeline_key` may `return` a key; no local + part may be a raw `Slug`, `Short` or `OrgRepo` (a validated `Date` is + `[0-9-]{10}`, already a label, and passes raw; Soul F6 corrected the first + wording, which claimed every non-label value was escaped); nothing may infer + a kind from an id's shape; reachability ("does this document exist", "may this + kind be resolved") stays with admission in `huginn-mind` per the resolution + matrix above. Cross-field rules stay out of `Bounded`. +- **Shared paths:** `pipeline_key`, `pipeline_id`, `PipelineRef::validate` + (`:365-370`), `validate_pipeline_write_envelope` (`:759-772`). Four paths, + one grammar, no fifth. +- **Deletion line:** the 97 lines above, before `local` is written. + +### Verification + +Builds: `cargo check -p epiphany-pipeline --lib --tests`, then +`cargo check -p epiphany-core --lib --tests` (untouched; its only mention of +the package is a doc comment at `runtime_spine.rs:8754-8758`). +`CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex`, path-list baseline. + +Tests: `cargo test -p epiphany-pipeline --lib`, thirteen existing plus six. + +| Test | Rule it pins | +|---|---| +| `every_key_has_exactly_three_segments` | R1, over every sample plus a nested resolution: three segments, segment 1 a `Slug`, segment 2 in `PipelineKind::ALL` by name, each local part a `Label`. Also asserts a `Target` whose campaign is `Slug("a:b")` is refused by the `pub` `pipeline_key` (M5). | +| `roots_of_different_kinds_do_not_share_a_key` | Defect 1: campaign `yggdrasil` and instance `yggdrasil`, `assert_ne!`. | +| `a_resolution_names_its_subjects_kind` | Defect 2: resolutions of `c:question:Q1` and `c:ruling:Q1` key differently, as do resolutions of campaign `c` and instance `c`, and each reads back recovering its subject kind. | +| `a_resolution_of_a_resolution_reads_back` | Defect 3: a resolution whose subject is a resolution validates, keys, reads back; five nestings key, six refuse on the local bound with `field == "resolution.key"`. | +| `a_composed_local_is_bounded_whole` | The total bound: a hand-off whose escaped receiver is 60 label bytes composes about 80 and is refused `InvalidFormat { field: "hand_off.key" }`. | +| `no_local_part_carries_the_separator` | R2: `local("probe", ["a.b"])` and `local("probe", ["a", "b.c"])` both refused; a hand-off with a dotted `to_instance` keys to an escaped local. | + +Unchanged in name and assertion: `keys_read_back_as_ids_of_their_kind`, +`keys_are_derived_and_mismatch_refuses`, +`every_pipeline_kind_round_trips_through_named_slot_zero`, +`pipeline_published_schemas_match_derivation`, +`decode_refuses_an_envelope_of_a_foreign_type`, `bounds_refuse_in_utf8_bytes`, +`repo_fields_must_be_org_slash_repo`, `parent_ids_are_parsed_strictly`, +`resolution_subject_is_a_full_id_of_its_kind`. + +Negative checks: `rg -n "declared_kind|KeyParts" epiphany-pipeline/src` empty; +no `return Ok(` inside `pipeline_key`; `rg -n "match kind"` empty; +`git diff --stat 4a654351 -- schemas/cultnet/` **empty**; +`rg -n "to_instance.0.clone\(\)"` empty. If Hands finds any consumer of a +pipeline key outside `epiphany-pipeline/src/lib.rs`, the cut is wrong; stop. + +### Mutations + +Committed as `tools/eureka-cut6b-mutations.ps1` with byte-exact UTF-8 I/O, +anchors that must match exactly once, and **M0, a no-op control** that rewrites +the file through the same path and must leave every test green. A redesign's +mutations restore the old permissiveness rather than break a check. + +| # | Survivor | Mutation, exactly | Killed by | +|---|---|---|---| +| M1 | Roots share a namespace | `Campaign` arm returns `Ok(value.slug.0.clone())` | `roots_of_different_kinds_do_not_share_a_key`; collaterally the three-segment test and the read-back test | +| M2 | Resolution key drops the subject kind | `Resolution` arm omits `subject_kind.name()` from the parts | `a_resolution_names_its_subjects_kind` (both pairs key `c:resolution:Q1`) | +| M3 | Nested resolution unnameable | `Resolution` arm returns `format!("resolution:{}", subject.id)` | `a_resolution_of_a_resolution_reads_back`; the three-segment test | +| M4 | Total bound unpinned | In `local`, delete the `bound` on the join, keep per-part `label_text` | `a_composed_local_is_bounded_whole` | +| M5 | Root check unpinned | In `pipeline_key`, drop `dotted_text` on the root | the refusal assertion in `every_key_has_exactly_three_segments` | +| M6 | Head may carry a dot | In `local`, `dotted_text` on `parts[0]` instead of `label_text` | `no_local_part_carries_the_separator` | + +M6's stated limit: the array type is a compile fact with no runtime mutation, +and one-arm-one-arity is not proven and no longer needs to be. + +### Ordering against Cut 6c + +Cut 6c's central keep ("no key moves") is false after this cut, its retyping of +`ResolutionOutcome`'s referents to `PipelineRef` rests on the `pipeline_id` +this cut replaces whole, and this cut is the smaller diff in the shared sample +block. So 6b lands first, and Imagination reissues 6c against the landed +grammar with three edits: re-anchor by content (everything from `:526` shifts +by about −60 lines); replace "no key moves" with "keys moved in 6b and are +settled; 6c moves none further, checked by `git diff -- schemas/cultnet/` +naming exactly six files"; and add one fixture the new grammar makes available, +a `Superseded` entry naming a resolution (`c:resolution:question.Q1`). + +### Subtraction estimate + +−97 source, +38 outside tests, +110 tests; net −59 outside tests. Zero schema +files, types, kinds, dependencies, targets, formats or epoch. **Every key +moves**, which is the cut's whole cost and is free exactly once: nothing reads +these keys yet. Once `huginn-mind` reads them, the same change is a +stored-document re-key. + +## Cut 6c. The Ghostlight shapes + +- **Repo/branch:** Epiphany `codex/eureka-pipeline-state`. Depends on Cut 6b + (landed at `1bddd2ac`, mutation suite at `95ee551a`) and its fix batch. + Blocks Cut 8. Lands before Cut 8 (ruling C) and is the last cut inside + Epiphany before the pause. +- **Anchors.** Every `file:line` below is against `epiphany-pipeline/src/lib.rs` + at `95ee551a`, 1,608 lines, twenty tests, re-found by content after the key + grammar shifted everything from the old `:526` down. The 6b fix batch lands + on `lib.rs` before Hands starts, so **re-anchor by content again**: every + anchor is a `struct`, `impl`, `fn` or sample opener that greps cleanly. +- **Reissued** 2026-09-16 by Imagination against the landed grammar. Three + edits from the first issue: anchors re-found; the Keeps claim about keys + rewritten; one fixture added that only the new grammar makes available, a + `Superseded` entry naming a resolution by the key it has. Deletes, adds, + `Promise`, the two unit enums, `StructuralDelta`, M17/M19/M20/M21, the + authority map and the subtraction estimate carry over; M16 and M18 keep + their rule and gain sharper forgeries; M22 is new. + +**Why this cut exists, and why now.** Ghostlight's independently run campaign +(D9) named six pains. Four were already built: finding identity, the ledger +derivation, cut status and the direction of the supersession relation are +covered by keys, joins and `PipelineResolution` as they stand. What is left is +one genuine hole (promises), two fields wearing one shape (ruling authority, +finding origin), and four retypings that are breaking, not additive. Under +ruling 8 a retyping costs an epoch bump the moment a pinned reader exists. +Nothing reads these schemas today: a grep for `SubtractionEstimate`, +`MutationRecord`, `ResolutionOutcome`, `VerdictClaim`, `StructuralDelta` and +`subtraction_estimate` over Epiphany finds `lib.rs`, the four schema files +that embed them, and one prose mention in this map; over `F:\Projects\Huginn` +and the Eureka skill checkout it finds nothing (source read). A whole-drive +search timed out, so any other tree is unchecked. Cut 8 pins a git rev and +closes the window. **The four retypings cost nothing today and an epoch +tomorrow.** + +**Rulings carried in:** A, Soul measures every promise and `Unproven` carries +the ones it could not reach; B, `Fixed { commit: Sha }` is admitted with its +referent outside the document set; C, this lands before Cut 8. +Multi-supersession is `Vec[8]`, cardinality being admission's +(Q10 asked against Cut 8). Q11 A: nesting stays. Q12: `OrgRepo` untouched; the +tightening is its own commit and is not folded in. No kind widens, so +`epiphany.pipeline.epoch.v1` holds. **Zero new kinds.** + +### Deletes first + +| Path | Lines | What dies | +|---|---:|---| +| `lib.rs:290-294` | 5 | `SubtractionEstimate` and the two-line comment on its list default. Its consumer becomes `StructuralDelta`. | +| `lib.rs:388-396` | 9 | `impl Bounded for ResolutionOutcome`. Replaced whole, not patched: every arm changes referent type, and a patched impl is how a length-only check survives. | +| `lib.rs:826-828` | 3 | The cut-spec sample's `SubtractionEstimate { … }` literal. | +| `lib.rs:296` | 1 | `MutationRecord`'s `mutation: Line` field, rewritten in place as `location`/`before`/`after`. | + +The comment at `:298-299` names the dead type and is reworded to stand alone. +No file or schema file dies; `index.json` is untouched because no kind is +added and no type id renamed. Deletes go before any field is added: if +`SubtractionEstimate` and `estimate: StructuralDelta` coexist at any commit, +the cut has two estimate shapes. + +### Keeps + +- **All thirteen kinds, `pipeline_key`, `pipeline_id`, `local`, `key_segment` + and the epoch.** Keys moved in Cut 6b and are settled; this cut moves none + further, because it adds, retypes or removes no field a key is derived from. + The check is mechanical: `git diff --stat -- schemas/cultnet/` at the + end names exactly the six regenerated schema files. `index.json` carries no + hashes (its entries are id, kind, wire contracts, version, type, title and + path, and the derivation test asserts that shape), so it is byte-identical. +- **`PipelineRef` and its `Bounded` impl (`:363-376`).** It becomes + load-bearing for four more referents. It already parses the id against its + declared kind through `pipeline_id`, whose kind-segment check (`:553-555`) + refuses a mismatch; that is why retyping 1 is cheap and what the new fixture + rides on. +- **The whole key suite**, `keys_read_back_as_ids_of_their_kind` included. All + twenty tests stay unchanged in name and assertion. They are blind to this + cut, which is why it carries its own tests. +- **`Short`** stays the type for full-id fields already parsed at key + derivation (`cut_report.cut_spec`, `verdict.cut_report`, `finding.verdict`, + `cut_spec.rulings`, `ruling.answers`). Retyping those is a larger cut. +- **The outcome-invariance assertion** in `keys_are_derived_and_mismatch_refuses` + (`:1029-1037`): a subject has one resolution key whatever the outcome. + +### Adds + +Two `unit_enums!` entries and one `value_types!` entry. No kind, module, +dependency or target. + +``` +RulingAuthority { Operator, Standing, Defaulted } +FindingOrigin { Introduced, PreExisting } +pub struct Promise { label: Label, text: Line } +``` + +`Promise` is not a document and has no key; it is identified by its report's +id plus its label, as `TargetInvariant` is by its target's (`:276`). It is not +resolvable and does not enter the resolution matrix. + +### Per-file changes, `epiphany-pipeline/src/lib.rs` + +| Line | Change | +|---|---| +| `:262-267` | Add `RulingAuthority` and `FindingOrigin` to the `unit_enums!` block. | +| `:290-294` | Delete `SubtractionEstimate` and its comment. | +| `:296` | `pub struct MutationRecord { label: Label, rule: Line, location: CodeLocation, before: Line, after: Line, commit: Sha, failed_as_expected: bool }` | +| `:298-299` | Reword the comment so it no longer names `SubtractionEstimate`. | +| after `:307` | `pub struct Promise { label: Label, text: Line }` beside `LandedName`. | +| `:308` | `VerdictClaim` gains `promise: Option