Merge eureka-substrate/readback-gate: one O(N) upgrade read-back gate in huginn-mind, pinned by CI fixtures (Soul s2 holds)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-26 15:27:17 +01:00
commit 2512c8a42f
5 changed files with 255 additions and 180 deletions

View file

@ -18,13 +18,6 @@ and every encoding-relevant variant against `golden/envelopes.txt`. A diff
there is a wire change; do not regenerate it to make the test pass. An additive
field adds golden lines and never changes an existing one.
Opt-in, against a real store: `stored_documents_read_back` decodes, validates
and keys every stored pipeline document and asserts that it re-encodes to the
stored key, type and payload bytes. Take a copy of the store (the test copies
the file again before opening it, so the copy may be read-only), never open the
live one:
```sh
sudo cp /var/lib/gamecult/huginn/minds/eureka/mind.redb "$SNAP/minds/eureka/mind.redb"
HUGINN_MIND_SNAPSHOT="$SNAP" cargo test -p eureka-pipeline -- --ignored --exact tests::stored_documents_read_back --nocapture
```
The real-store gate is huginn-mind's `#[ignore]` `query::tests::stored_documents_read_back`,
which checks these stored bytes and the read path together (the leaf cannot see the read
path); the Huginn Upgrade runbook in gamecult-ops runs it.

View file

@ -2747,79 +2747,6 @@ mod tests {
assert!(!Sha("5f98228a".into()).names_same_commit(&Sha("5f98228b".into())), "equal length, differing");
}
/// stored-documents-valid and stored-bytes-reproduce, on the real mind.
/// `HUGINN_MIND_SNAPSHOT` is a copy of a state root holding
/// `minds/eureka/mind.redb`; the test copies the store file into a scratch
/// directory first (redb opens read-write), so the snapshot may be
/// read-only. Read through cultcache-rs's own store. Each pipeline
/// document must re-prepare to its stored envelope: key, type and payload
/// bytes.
#[test]
#[ignore = "needs HUGINN_MIND_SNAPSHOT, a copy of a Huginn state root; see the crate README"]
fn stored_documents_read_back() {
use cultcache_rs::{CacheBackingStore, OwnedRedbMessagePackBackingStore};
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names the snapshot state root");
let scratch = std::env::temp_dir().join(format!("eureka-pipeline-readback-{}", std::process::id()));
std::fs::create_dir_all(&scratch).expect("scratch directory");
let working = scratch.join("mind.redb");
std::fs::copy(Path::new(&root).join("minds").join("eureka").join("mind.redb"), &working).expect("the snapshot copies");
let store = OwnedRedbMessagePackBackingStore::new(&working).expect("the snapshot opens");
let cache = schema_cache().expect("the registrar registers");
let envelopes = store.pull_all().expect("the snapshot reads");
let mut stored = Vec::new();
for envelope in &envelopes {
match PipelineDocument::decode(envelope) {
Ok(document) => {
assert_eq!(document.validate(), Ok(()), "{}: valid", envelope.key);
assert_eq!(pipeline_key(&document).as_deref(), Ok(envelope.key.as_str()), "{}: key derives byte for byte", envelope.key);
let again = document.prepare(&cache).unwrap_or_else(|error| panic!("{}: does not re-prepare: {error}", envelope.key));
assert_eq!(
(&again.key, &again.r#type, &again.payload),
(&envelope.key, &envelope.r#type, &envelope.payload),
"{}: re-encodes to the stored bytes",
envelope.key
);
stored.push((document.kind(), envelope.key.clone()));
}
Err(PipelineRefusal::ForeignStore { .. }) => {}
Err(error) => panic!("{}: does not decode: {error}", envelope.key),
}
}
assert!(stored.len() > 200, "{} pipeline documents read", stored.len());
let mut subjects = 0;
let resolvable = |kind: &PipelineKind| !matches!(kind, PipelineKind::Resolution | PipelineKind::Campaign | PipelineKind::Instance);
for (kind, key) in stored.iter().filter(|(kind, _)| resolvable(kind)) {
let one = resolved_at(*kind, key, 1).unwrap_or_else(|error| panic!("{key}: no resolution keys: {error}"));
let two = resolved_at(PipelineKind::Resolution, &one, 1).unwrap_or_else(|error| panic!("{key}: no withdrawal keys: {error}"));
for derived in [&one, &two] {
pipeline_id("read_back", derived, PipelineKind::Resolution).unwrap_or_else(|error| panic!("{derived}: does not parse: {error}"));
}
subjects += 1;
}
for (_, key) in stored.iter().filter(|(kind, _)| *kind == PipelineKind::Resolution) {
resolved_at(PipelineKind::Resolution, key, 1).unwrap_or_else(|error| panic!("{key}: cannot be withdrawn: {error}"));
}
// The six findings that could not be resolved before this cut.
let stuck = [
"cut-bifrost-retire-alarm.s2.verb-default-accepts-malformed",
"cut-ops-notice-deploy.s2.pinned-bifrost-predates-retry",
"cut-bifrost-notice-retry.s1.unknown-test-ignores-backoff",
"cut-bifrost-notice-retry.s2.closed-unknown-retry-unpinned",
"cut-bifrost-notice-retry.s2.flapping-clock-retries-every-tick",
"cut-idunn-topology-lock.s2.boot-reconcile-skipped-on-contention",
];
for label in stuck {
let found = stored
.iter()
.find(|(kind, key)| *kind == PipelineKind::Finding && key.ends_with(label))
.unwrap_or_else(|| panic!("{label}: not in the snapshot"));
let one = resolved_at(PipelineKind::Finding, &found.1, 1).unwrap_or_else(|error| panic!("{label}: still stuck: {error}"));
assert!(one.split(':').nth(2).is_some_and(|local| local.len() > SUBJECT_LOCAL_MAX), "{label}: the old bound refused it");
}
eprintln!("read back {} pipeline documents, {subjects} subjects", stored.len());
let _ = std::fs::remove_dir_all(&scratch);
}
/// The total bound, in the one place it lives: parts that are each a legal
/// label compose a local wider than 64 bytes and are refused as a whole.
#[test]

View file

@ -362,7 +362,6 @@ fn snapshot(cache: &CultCache) -> Vec<CultCacheEnvelope> {
#[cfg(test)]
mod tests {
use super::*;
use crate::admission::PipelineAdmissionOutcome;
use crate::fixtures::{INSTANCE, epoch, instance, prepare, slug};
use crate::store::test_stores::MemoryStore;
use cultcache_rs::CacheBackingStore;
@ -786,94 +785,4 @@ mod tests {
}))
);
}
/// stored-documents-valid and every-subject-resolvable, against the real
/// mind: every stored document views back unchanged; the six findings the
/// 64-byte bound left unresolvable take a resolution; and a stored
/// resolution whose withdrawal key is over 64 bytes is withdrawn. Run it
/// on a scratch copy of the state root only: it writes.
#[test]
#[ignore = "reads and writes the scratch copy of a live state root named by HUGINN_MIND_SNAPSHOT"]
fn snapshot_admits_and_reads_back() {
use crate::fixtures::{admit, r, resolution, withdrawn};
use eureka_pipeline::{PipelineRef, ResolutionOutcome, Short, pipeline_key};
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names a scratch copy of the state root");
let mut mind = Mind::open(Path::new(&root), &slug("eureka")).unwrap();
// The census is taken apart from the loop that checks each document.
let pipeline_envelopes = mind
.envelopes()
.iter()
.filter(|envelope| PipelineKind::ALL.iter().any(|kind| kind.type_id() == envelope.r#type))
.count();
assert!(pipeline_envelopes > 0, "the snapshot held no pipeline document");
let mut viewed = 0;
let mut resolutions = Vec::new();
for envelope in mind.envelopes() {
let Some(kind) = PipelineKind::ALL.iter().copied().find(|kind| kind.type_id() == envelope.r#type) else { continue };
let stored = PipelineDocument::decode(envelope).unwrap();
stored.validate().unwrap_or_else(|refusal| panic!("{} no longer validates: {refusal:?}", envelope.key));
let rekeyed = pipeline_key(&stored).unwrap_or_else(|refusal| panic!("{} no longer keys: {refusal:?}", envelope.key));
assert_eq!(rekeyed, envelope.key, "stored key differs from the key the leaf computes");
let view = mind
.view(&PipelineRef { kind, id: Short(envelope.key.clone()) })
.unwrap()
.unwrap_or_else(|| panic!("{} does not view", envelope.key));
assert_eq!(view.document, stored, "{}", envelope.key);
if let PipelineDocument::Resolution(held) = &stored
&& !matches!(held.outcome, eureka_pipeline::ResolutionOutcome::Withdrawn { .. })
&& !envelope.key.contains(":resolution:resolution.")
{
resolutions.push(envelope.key.clone());
}
viewed += 1;
}
assert_eq!(viewed, pipeline_envelopes, "every pipeline envelope validates, rekeys and views back");
println!("validated, rekeyed and viewed {viewed} stored documents unchanged");
let stuck = [
"cut-bifrost-retire-alarm.s2.verb-default-accepts-malformed",
"cut-ops-notice-deploy.s2.pinned-bifrost-predates-retry",
"cut-bifrost-notice-retry.s1.unknown-test-ignores-backoff",
"cut-bifrost-notice-retry.s2.closed-unknown-retry-unpinned",
"cut-bifrost-notice-retry.s2.flapping-clock-retries-every-tick",
"cut-idunn-topology-lock.s2.boot-reconcile-skipped-on-contention",
];
let mut resolved = 0;
for local in stuck {
let subject = r(PipelineKind::Finding, &format!("idunn-watchdog:finding:{local}"));
let outcome = admit(&mut mind, vec![resolution(subject, ResolutionOutcome::Recorded { reason: "snapshot read-back".into() })]);
match outcome {
PipelineAdmissionOutcome::Committed { writes, .. } => {
assert_eq!(writes.len(), 1, "{local}");
resolved += 1;
}
other => panic!("stuck finding {local} was not resolved: {other:?}"),
}
}
assert_eq!(resolved, stuck.len(), "every stuck finding resolves");
let mut candidates = Vec::new();
for key in resolutions {
let withdrawal = resolution(r(PipelineKind::Resolution, &key), withdrawn());
let withdrawal_key = pipeline_key(&withdrawal).unwrap_or_else(|refusal| panic!("withdrawal of {key} does not key: {refusal:?}"));
if withdrawal_key.rsplit(':').next().unwrap().len() > 64 {
candidates.push((key, withdrawal));
}
}
assert!(!candidates.is_empty(), "no stored resolution had a withdrawal key over 64 bytes");
let wanted = candidates.len();
let mut withdrew = 0;
for (key, withdrawal) in candidates {
match admit(&mut mind, vec![withdrawal]) {
PipelineAdmissionOutcome::Committed { .. } => withdrew += 1,
other => panic!("withdrawal of {key} was not committed: {other:?}"),
}
}
assert_eq!(withdrew, wanted, "every candidate withdraws");
println!("withdrew {withdrew} resolutions whose withdrawal keys exceed 64 bytes");
}
}

View file

@ -28,13 +28,13 @@ use cultnet_rs::{
use eureka_pipeline::{
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, OrgRepo,
PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
Short, Slug, Title,
Short, Slug, Title, pipeline_key,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::docs::{CitationRole, Docs, Held, citations};
use crate::mind::Mind;
use crate::docs::{CitationRole, Docs, Held, citations, kind_of_type};
use crate::mind::{Mind, schema_cache};
use crate::receipt::{self, PipelineProvenance};
use crate::refusal::MindRefusal;
use crate::rows::{SelectionRow, Vocabulary, refuse_values};
@ -394,6 +394,12 @@ impl Reader {
self.docs.image.iter().find(|held| held.kind == kind && held.key == key)
}
/// The lookup `Mind::view` answers through, so a caller holding one `Reader`
/// views many documents from one snapshot.
fn view(&self, id: &PipelineRef) -> Result<Option<PipelineDocumentView>, MindRefusal> {
self.held(id.kind, &id.id.0).map(|held| self.view_of(held)).transpose()
}
fn view_of(&self, held: &Held) -> Result<PipelineDocumentView, MindRefusal> {
let admission = self.facts.of(held)?;
let status = match self.docs.closing_resolution(held.kind, &held.key) {
@ -455,8 +461,57 @@ impl<S: MindStore> Mind<S> {
/// vocabulary for one rule.
pub fn view(&self, id: &PipelineRef) -> Result<Option<PipelineDocumentView>, MindRefusal> {
id.validate_ref()?;
Reader::at(self, receipt::head(self)?)?.view(id)
}
/// The upgrade gate (ruling readback-gate-and-materialized-image): every
/// stored pipeline document decodes, validates, rekeys, re-prepares to its
/// stored key, type and payload bytes, and views back unchanged through the
/// read path `view` uses; every pipeline write a receipt names is stored,
/// and every stored document has its receipt. Returns how many documents
/// it read back; an empty store is a fault. One `Reader` serves every view,
/// because `Reader::at` is the expensive build. Admits nothing. A fault
/// names the document's key and what failed, never its content.
pub fn read_back(&self) -> Result<usize, MindRefusal> {
self.read_back_through(Reader::view)
}
/// The gate's one body; `lookup` is the read path under test, which tests
/// replace with a lying one to prove the view comparison refuses.
fn read_back_through(
&self,
lookup: impl Fn(&Reader, &PipelineRef) -> Result<Option<PipelineDocumentView>, MindRefusal>,
) -> Result<usize, MindRefusal> {
let cache = schema_cache()?;
let reader = Reader::at(self, receipt::head(self)?)?;
reader.held(id.kind, &id.id.0).map(|held| reader.view_of(held)).transpose()
let mut count = 0;
for envelope in self.envelopes() {
let Some(kind) = kind_of_type(&envelope.r#type) else { continue };
let key = &envelope.key;
let stored = PipelineDocument::decode(envelope).map_err(|_| integrity(&envelope.r#type, key, "does not decode"))?;
stored.validate().map_err(|_| integrity(&envelope.r#type, key, "no longer validates"))?;
if pipeline_key(&stored).map_err(|_| integrity(&envelope.r#type, key, "no longer keys"))? != *key {
return Err(integrity(&envelope.r#type, key, "is stored under a key the leaf no longer computes"));
}
let again = stored.prepare(&cache).map_err(|_| integrity(&envelope.r#type, key, "does not re-prepare"))?;
if (&again.key, &again.r#type, &again.payload) != (key, &envelope.r#type, &envelope.payload) {
return Err(integrity(&envelope.r#type, key, "does not re-encode to its stored bytes"));
}
let viewed = lookup(&reader, &PipelineRef { kind, id: Short(key.clone()) })?;
if viewed.map(|view| view.document).as_ref() != Some(&stored) {
return Err(integrity(&envelope.r#type, key, "does not view back as stored"));
}
count += 1;
}
for (type_id, key) in reader.facts.0.keys() {
if kind_of_type(type_id).is_some() && self.raw_envelope(type_id, key).is_none() {
return Err(integrity(type_id, key, "is named by a receipt but not stored"));
}
}
if count == 0 {
return Err(MindRefusal::Unavailable { detail: "the store holds no pipeline document".into() });
}
Ok(count)
}
/// One typed selection over the mind, as of one snapshot.
@ -678,7 +733,6 @@ mod tests {
use crate::fixtures::*;
use crate::receipt::Faculty;
use cultnet_rs::{Citation, FieldPredicate, Incoming, RecordRef};
use crate::mind::schema_cache;
use crate::store::test_stores::MemoryStore;
use chrono::{TimeZone, Utc};
use eureka_pipeline::{
@ -2118,4 +2172,196 @@ mod tests {
assert_eq!(found.id, r(K::Question, &q1), "the id travels with the document");
assert_eq!(Some(found.document), mind.get(K::Question, &q1).unwrap());
}
/// A store of 135 pipeline documents: past any sampling cutoff, with one
/// resolution so a view carries a closing status. Returns its rows.
fn gate_rows() -> Vec<cultcache_rs::CultCacheEnvelope> {
let store = MemoryStore::new();
let mut mind = opened(store.clone(), INSTANCE);
seed(&mut mind);
for chunk in (1..=130).collect::<Vec<u32>>().chunks(10) {
committed(admit(&mut mind, chunk.iter().map(|n| question_n(*n)).collect()));
}
committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())]));
store.rows()
}
fn read_back_of(rows: Vec<cultcache_rs::CultCacheEnvelope>) -> Result<usize, MindRefusal> {
let store = MemoryStore::new();
for row in rows {
store.plant(row);
}
Mind::open_with(store, &slug(INSTANCE))?.read_back()
}
/// The stored question the faults land on: the last question in identity
/// order, with more than 100 pipeline documents before it, so a gate that
/// stops early or samples never reaches it.
fn last_question(rows: &[cultcache_rs::CultCacheEnvelope]) -> usize {
let at = rows.iter().rposition(|row| row.r#type == K::Question.type_id()).unwrap();
let before = rows[..at].iter().filter(|row| crate::docs::kind_of_type(&row.r#type).is_some()).count();
assert!(before > 100, "the fixture is past any cutoff: {before}");
at
}
fn refused_with(rows: Vec<cultcache_rs::CultCacheEnvelope>, fragment: &str, target: &str) {
match read_back_of(rows) {
Err(MindRefusal::Unavailable { detail }) => {
assert!(detail.contains(fragment), "refused for another reason: {detail}");
assert!(detail.contains(target) || fragment == "holds no pipeline document", "the fault names the key: {detail}");
}
other => panic!("expected an Unavailable refusal containing {fragment:?}, got {other:?}"),
}
}
/// The gate over a clean store reads back every pipeline document, and
/// says how many: the exact count, so a gate that stops early is seen.
#[test]
fn read_back_accepts_a_clean_store_and_counts_every_document() {
assert_eq!(read_back_of(gate_rows()).unwrap(), 135);
}
fn faulted(fault: impl FnOnce(&mut Vec<cultcache_rs::CultCacheEnvelope>, usize)) -> (Vec<cultcache_rs::CultCacheEnvelope>, String) {
let mut rows = gate_rows();
let at = last_question(&rows);
let key = rows[at].key.clone();
fault(&mut rows, at);
(rows, key)
}
/// The stored question, decoded, for a test to bend.
fn stored_question(row: &cultcache_rs::CultCacheEnvelope) -> eureka_pipeline::PipelineQuestion {
match D::decode(row).unwrap() {
D::Question(question) => question,
other => panic!("not a question: {other:?}"),
}
}
#[test]
fn read_back_refuses_an_undecodable_payload() {
let (rows, _) = faulted(|rows, at| rows[at].payload = vec![0xc1]);
assert!(matches!(read_back_of(rows), Err(MindRefusal::Document(_) | MindRefusal::Unavailable { .. })));
}
#[test]
fn read_back_refuses_a_field_past_its_bound() {
let (rows, key) = faulted(|rows, at| {
let mut question = stored_question(&rows[at]);
question.title = "x".repeat(300).as_str().into();
rows[at].payload = D::Question(question).prepare(&schema_cache().unwrap()).unwrap().payload;
});
refused_with(rows, "no longer validates", &key);
}
/// A document stored under a key other than the one the leaf computes for
/// it, with its receipt renamed to match, so only the re-key check can see it.
#[test]
fn read_back_refuses_a_changed_key() {
let (rows, key) = faulted(|rows, at| {
let old = rows[at].key.clone();
rows[at].key.push('x');
let mind = Mind::open_with(
{
let store = MemoryStore::new();
rows.iter().for_each(|row| store.plant(row.clone()));
store
},
&slug(INSTANCE),
)
.unwrap();
let mut receipt =
mind.receipts().unwrap().into_iter().find(|receipt| receipt.writes.iter().any(|write| write.document_key == old)).unwrap();
receipt.writes.iter_mut().filter(|write| write.document_key == old).for_each(|write| write.document_key.push('x'));
let renamed = schema_cache().unwrap().prepare_entry_named(&receipt.receipt_id, &receipt).unwrap().0;
let at = rows.iter().position(|row| row.r#type == renamed.r#type && row.key == renamed.key).unwrap();
rows[at] = renamed;
});
refused_with(rows, "key the leaf no longer computes", &key);
}
#[test]
fn read_back_refuses_a_trailing_byte() {
let (rows, key) = faulted(|rows, at| rows[at].payload.push(0x00));
refused_with(rows, "re-encode", &key);
}
#[test]
fn read_back_refuses_a_payload_that_decodes_but_was_encoded_differently() {
let (rows, key) = faulted(|rows, at| {
// The same array, its header in the wider array16 form: it decodes to the
// same document and encodes to different bytes.
let payload = &rows[at].payload;
assert_eq!(payload[0] & 0xf0, 0x90, "the payload opens with a fixarray header");
let mut wider = vec![0xdc, 0x00, payload[0] & 0x0f];
wider.extend_from_slice(&payload[1..]);
assert_eq!(D::decode(&cultcache_rs::CultCacheEnvelope { payload: wider.clone(), ..rows[at].clone() }).unwrap(), D::Question(stored_question(&rows[at])));
rows[at].payload = wider;
});
refused_with(rows, "re-encode", &key);
}
#[test]
fn read_back_refuses_a_foreign_type() {
let mut rows = gate_rows();
let at = last_question(&rows);
rows[at].r#type = "epiphany.pipeline.finding.v9".into();
assert!(matches!(read_back_of(rows), Err(MindRefusal::ForeignStore { .. })));
}
#[test]
fn read_back_refuses_a_document_stored_under_the_wrong_type() {
let (rows, _) = faulted(|rows, at| rows[at].r#type = K::Verdict.type_id().into());
assert!(matches!(read_back_of(rows), Err(MindRefusal::Document(_) | MindRefusal::Unavailable { .. })));
}
/// The read path is what the gate compares the stored bytes against: a path
/// that answers nothing, or another document, is refused.
#[test]
fn read_back_refuses_a_read_path_that_does_not_return_the_stored_document() {
let mind = |rows: Vec<cultcache_rs::CultCacheEnvelope>| {
let store = MemoryStore::new();
rows.into_iter().for_each(|row| store.plant(row));
Mind::open_with(store, &slug(INSTANCE)).unwrap()
};
let absent = mind(gate_rows()).read_back_through(|_, _| Ok(None));
assert!(matches!(&absent, Err(MindRefusal::Unavailable { detail }) if detail.contains("does not view back")), "{absent:?}");
let other = mind(gate_rows()).read_back_through(|reader, id| {
let first = reader.docs.image.iter().find(|held| held.kind == id.kind).unwrap();
reader.view_of(first).map(Some)
});
assert!(matches!(&other, Err(MindRefusal::Unavailable { detail }) if detail.contains("does not view back")), "{other:?}");
}
#[test]
fn read_back_refuses_an_empty_store() {
refused_with(Vec::new(), "holds no pipeline document", "");
}
#[test]
fn read_back_refuses_a_receipted_document_that_is_not_stored() {
let (rows, key) = faulted(|rows, at| {
rows.remove(at);
});
refused_with(rows, "named by a receipt but not stored", &key);
}
#[test]
fn read_back_refuses_a_stored_document_with_no_receipt() {
let (rows, _) = faulted(|rows, _| rows.push(prepare(&question_n(900))));
refused_with(rows, "has no commit receipt", "Q900");
}
/// The upgrade gate on a real store (ruling readback-gate-and-materialized-image):
/// the one function `Mind::read_back` over a scratch copy of a state root.
/// Opens the store read-write, so run it on a copy, never the live file.
/// Measured about 30 s in debug at 11,328 documents.
#[test]
#[ignore = "needs HUGINN_MIND_SNAPSHOT, a scratch copy of a Huginn state root"]
fn stored_documents_read_back() {
let root = std::env::var("HUGINN_MIND_SNAPSHOT").expect("HUGINN_MIND_SNAPSHOT names a scratch copy of the state root");
let started = std::time::Instant::now();
let mind = Mind::open(std::path::Path::new(&root), &Slug("eureka".into())).unwrap();
let count = mind.read_back().unwrap();
println!("read back {count} stored documents in {:?}", started.elapsed());
}
}

View file

@ -326,7 +326,7 @@ pipeline kinds are in Huginn `docs/eureka-substrate-map.md`.
| Kind | Identity (what names it) | Lifecycle (what happens over time) | Authority (who decides) |
| --- | --- | --- | --- |
| Pipeline wire ids | `epiphany.pipeline.<kind>.v2`, epoch `epiphany.pipeline.epoch.v2`, defined by the leaf, the Huginn crate `crates/eureka-pipeline` (lib `eureka_pipeline`), ids unchanged. | Renamed once to `eureka.pipeline.<kind>.v3` by one `mind.redb` migration, at the next breaking leaf change or before the first hosted mind (ruling `wire-names-rename-before-hosting`). Adding the `run` kind or the `Mind` authority variant is additive (old stored documents decode; no migration), so it does not trigger the rename; a field added with the leaf's `= absent` marker is additive too (leaf-read-anchors r2: old bytes decode, empty fields encode byte-identically, `golden/envelopes.txt` gains lines and loses none); a field added any other way would. | Huginn owns the leaf. The migration is a live-mind upgrade, which is the operator's (`mind-rules-which-forks`). |
| Leaf crate and schema catalogue | Huginn `crates/eureka-pipeline`, `schemas/cultnet/` (13 pipeline schemas, `gamecult.persona_state.v0`, index, README). `epiphany.work_organ_state.v0` dies: no reader outside Epiphany (grep, F1/F3). | Landed at Huginn `afc35f9` (leaf-into-huginn). Wire pinned by `crates/eureka-pipeline/golden/envelopes.txt` and the opt-in `stored_documents_read_back`. Huginn is AGPL-3.0-only (ruling `eureka-body:ruling:leaf-relicensed-agpl`). | Huginn. Forbidden: any Cargo source resolving into GameCult/Epiphany (`nothing-runs-from-epiphany`). |
| Leaf crate and schema catalogue | Huginn `crates/eureka-pipeline`, `schemas/cultnet/` (13 pipeline schemas, `gamecult.persona_state.v0`, index, README). `epiphany.work_organ_state.v0` dies: no reader outside Epiphany (grep, F1/F3). | Landed at Huginn `afc35f9` (leaf-into-huginn). Wire pinned by `crates/eureka-pipeline/golden/envelopes.txt` and huginn-mind's opt-in `query::tests::stored_documents_read_back`. Huginn is AGPL-3.0-only (ruling `eureka-body:ruling:leaf-relicensed-agpl`). | Huginn. Forbidden: any Cargo source resolving into GameCult/Epiphany (`nothing-runs-from-epiphany`). |
| `run` | `<instance>:run:<label>` (cut-run-kind r2). Label `mind-<YYYYMMDD>T<HHMMSS><mmm>Z` from mind-launch's `run_label`; two launches in one millisecond produce the same key with different content, which admission answers `Conflict`, so the name stays injective. Order is admission ordinal (F15), not label. | Each wake opens a PersonaTurn run with no claims (ruling `eureka-substrate:ruling:every-wake-a-run`); the Persona tool opens SelfRun runs with claims; both go through `open_and_launch`, and the unit starts only after the run commits (the run is the grant). The Self in a unit admits under its label and ends it `Recorded { reason }`. A run whose unit is not alive past a 120 s start grace is closed `Recorded { reason: "unit not active" }` by the next `open_and_launch` of the same turn, before it checks anything else. It is Recorded, not Withdrawn, so a unit that keeps dying counts as an empty ended run and trips the breaker. Only a unit whose start failed in the launch itself is Withdrawn. Cadence comes from the newest PersonaTurn run's `admitted_at` (any status), not `started_on` (a date cannot carry seconds). | Opening: `open_and_launch`, called by the waker for PersonaTurn and by the Persona tool for SelfRun, each with a `Grant` that only `read_effective` constructs. One live run of hers per turn and instance: huginn-mind admission refuses a second in-force run with `operated_by: Mind` of the same turn and instance as `AlreadyLive { run }` (ruling `one-live-self-run`, option `one-live-per-turn`; cut run-live-admission). `open_and_launch` answers `Busy` from that refusal, or earlier, when the holder it has just read is alive. Operator runs never count. `operated_by` is declared by the writer (follow-up `eureka-substrate:follow_up:operator-authority-needs-operator-credential`). Ending: the Self that worked it; a dead one is closed by the next launch of its turn (cut mind-launch r3). No sweep exists elsewhere. Forbidden: Hands, Soul, Imagination, Life. |
| Repetition breaker | No state of its own (rulings `self-waker`, `merge-gate-own-soul`). Trip condition derived from one selection: the documents citing the item, in any role. Counted runs are the item's ended SelfRun runs of hers (`operated_by: Mind`, not withdrawn) admitted after the newest question `raised_in` the item. Trip when the last 3 counted runs are empty, or 10 of the last 50. A run is empty when no citing document other than a run has an ordinal in its window (its admission up to the next run's, or now). Documents that cite something else never count, wherever they were admitted (F21). | Evaluated in `open_and_launch` when a Self run claims the item, not by the waker. When tripped, it admits one `question` labelled `breaker-<run label>` under the item's campaign, `raised_in` the item, and launches nothing. The run label is the label the refused launch would have used, so every trip has its own key. The existing blocked-spec recipe then removes the item from the queue. Answered by a ruling or withdrawn: either reopens the item, and that question is the boundary, so a reopened item starts a fresh count. | `open_and_launch` admits the question; any Self may answer it (substrate-internal, `mind-rules-which-forks`). |
| Brake | One document per instance in a root-owned CultCache store `/etc/gamecult/minds/<instance>/control.cc`, type `eureka.control.brake.v1 { released: bool, set_at, set_by }`. | Replaced whole by the operator's CLI; never appended. Absent or undecodable reads as held. `mind-control brake hold` writes it held and then, in the same command, stops her running units: `systemctl stop` (blocking) over `mind-self@<i>:*` and `mind-persona@<i>:*`, through `SystemdLauncher::stop_all` (cut brake-stop; ruling `self-run-actuator`; F18-F20). | Operator only (ruling `brake-dial-root-store-now`); her units read it, and the file is not writable by any user she runs as. The hold is the one stopper of her units. `read_effective` is the one reader. Its `Released` answer carries a `Grant` (cadence and run cap, private fields, not `Clone`), which `open_and_launch` takes by value, so no run opens without a released reading (cut mind-launch r3). |