Admit a batch through one ordered path with every rule in it
Cut 8, Huginn half, commit 3 of 4: `admission.rs`. `Mind::admit`
prepares typed documents through the leaf and calls `admit_prepared`,
the single commit path for this cut, Cut 10's sink, Cut 12's hand-off and
import, and Cut 13's tools. The steps run in order and the first failing
step is the outcome: the batch names this mind (A1), one to sixty-four
envelopes (A2), the leaf's bounds, formats and key recomputation (A3),
unique identities (A4), every instance-carrying document names this mind
(A5), an empty mind's first batch carries exactly one `instance` document
and a batch carrying one derives the epoch record (A6), every reference
resolves in image or batch by `(kind, id)` (A7), the derived writes pass
A3-A7 themselves, the per-kind rules and the resolution matrix (A8), an
exact replay answers with the stored receipt only after validation (A9),
no write collides with the image (A10), the commit (A11).
Derived writes: a ruling that answers derives the `Answered` resolution
of its question; a hand-off derives the stewardship withdrawal on its
source side and the stewardship assignment on its receiving side. "In
force" is computed at rule time from the resolutions in image and batch
and never stored; the two derivations are the only resolutions a rule
ignores when judging its own document.
Two places the leaf is thinner than the spec assumed. `FindingWithoutRange`
is unreachable: a finding envelope without `range` fails the leaf's decode
as `Document(InvalidFormat { field: "payload" })` before any organ rule,
and the test asserts that. A campaign with no repos has no refusal of its
own in the spec's list; it is refused as `Document(FieldBound)` on
`campaign.repos` with a minimum of one. `PipelineAdmissionBatch` cannot
derive `JsonSchema` because the leaf's `PipelineDocument` does not; the
outcome and refusal types do.
The epoch record is derived whenever the batch carries the `instance`
document, which only the first batch may, so a replay of the first batch
digests to the same receipt and answers `AlreadyAdmitted` instead of
colliding.
Tests: 26 (the spec's 25 plus the replay-planting test, the receipt's
deliberate second construction site). `cargo check -p huginn-mind --lib
--tests` and `cargo test -p huginn-mind --lib` pass with zero warnings;
`cargo check --workspace` passes. README says what the crate does now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a4c5b79d52
commit
0bd71337fd
5 changed files with 1662 additions and 8 deletions
10
README.md
10
README.md
|
|
@ -33,8 +33,14 @@ A Cargo workspace of three crates:
|
|||
`huginn-mind` is live: it opens one instance's store (an owned redb CultCache
|
||||
at `<state_root>/minds/<instance>/mind.redb`, locked for the mind's lifetime),
|
||||
and refuses a store whose `instance` document names another instance, whose
|
||||
epoch record is foreign, or whose types are not a mind's. Document shape and
|
||||
keys come from `epiphany-pipeline`; the store is CultLib's Rust CultCache.
|
||||
epoch record is foreign, or whose types are not a mind's. It admits batches
|
||||
of pipeline documents through one commit path: the leaf's bounds, formats and
|
||||
keys, then the organ's cross-document rules (references, in-force status,
|
||||
the resolution matrix, derived resolutions and stewardships), then one
|
||||
compare-and-swap that lands the batch whole with a receipt naming the exact
|
||||
bytes it read and wrote. An exact replay answers with the stored receipt.
|
||||
Document shape and keys come from `epiphany-pipeline`; the store is CultLib's
|
||||
Rust CultCache.
|
||||
`huginn-daemon` and `eureka-state` are stubs; the
|
||||
campaign's cut map in `Epiphany/notes/eureka-pipeline-state-cut.md` owns what
|
||||
each crate must do next.
|
||||
|
|
|
|||
1340
crates/huginn-mind/src/admission.rs
Normal file
1340
crates/huginn-mind/src/admission.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,31 @@
|
|||
//! Test fixtures shared by the modules' tests: one valid document of each
|
||||
//! kind the tests need, prepared through the leaf against the organ's cache.
|
||||
//! kind, prepared through the leaf against the organ's cache, and a seeded
|
||||
//! mind to admit into.
|
||||
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use cultcache_rs::CultCacheEnvelope;
|
||||
use epiphany_pipeline::{Date, PipelineDocument, PipelineInstance, Short, Slug};
|
||||
use epiphany_pipeline::{
|
||||
AuthorityMap, ClaimOutcome, CodeLocation, CommitRange, CutVerification, Date, DocRef, Evidence, EvidenceKind,
|
||||
FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MutationRecord, OrgRepo, PipelineCampaign,
|
||||
PipelineCutReport, PipelineCutSpec, PipelineDocument, PipelineFinding, PipelineFollowUp, PipelineHandOff,
|
||||
PipelineInstance, PipelineKind, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRuling,
|
||||
PipelineStewardship, PipelineTarget, PipelineVerdict, Promise, QuestionOption, ReportCommit, ResolutionOutcome,
|
||||
RulingAuthority, Sha, Short, Slug, StructuralDelta, TargetInvariant, VerdictClaim,
|
||||
};
|
||||
|
||||
use crate::mind::{HuginnMindEpoch, schema_cache};
|
||||
use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome};
|
||||
use crate::mind::{HuginnMindEpoch, Mind, schema_cache};
|
||||
use crate::receipt::{Faculty, PipelineProvenance};
|
||||
use crate::refusal::MindRefusal;
|
||||
use crate::store::MindStore;
|
||||
use crate::store::test_stores::MemoryStore;
|
||||
|
||||
pub(crate) const INSTANCE: &str = "yggdrasil";
|
||||
pub(crate) const OTHER_INSTANCE: &str = "thought-cage";
|
||||
pub(crate) const CAMPAIGN: &str = "eureka-state";
|
||||
pub(crate) const REPO: &str = "GameCult/Epiphany";
|
||||
pub(crate) const OTHER_REPO: &str = "GameCult/Huginn";
|
||||
pub(crate) const INVARIANT: &str = "mind-admits";
|
||||
|
||||
pub(crate) fn slug(value: &str) -> Slug {
|
||||
value.into()
|
||||
|
|
@ -16,10 +35,59 @@ pub(crate) fn s(value: &str) -> Short {
|
|||
value.into()
|
||||
}
|
||||
|
||||
pub(crate) fn l(value: &str) -> Label {
|
||||
value.into()
|
||||
}
|
||||
|
||||
pub(crate) fn repo(value: &str) -> OrgRepo {
|
||||
value.into()
|
||||
}
|
||||
|
||||
pub(crate) fn date() -> Date {
|
||||
Date("2026-09-16".into())
|
||||
}
|
||||
|
||||
pub(crate) fn sha() -> Sha {
|
||||
Sha("5f98228d".into())
|
||||
}
|
||||
|
||||
pub(crate) fn id(kind: &str, local: &str) -> String {
|
||||
format!("{CAMPAIGN}:{kind}:{local}")
|
||||
}
|
||||
|
||||
pub(crate) fn r(kind: PipelineKind, id: &str) -> PipelineRef {
|
||||
PipelineRef { kind, id: s(id) }
|
||||
}
|
||||
|
||||
fn range() -> CommitRange {
|
||||
CommitRange { base: sha(), head: sha() }
|
||||
}
|
||||
|
||||
fn location() -> CodeLocation {
|
||||
CodeLocation { path: s("crates/huginn-mind/src/admission.rs"), line: 1, end_line: None }
|
||||
}
|
||||
|
||||
pub(crate) fn evidence() -> Evidence {
|
||||
Evidence { kind: EvidenceKind::Test, locator: "cargo test".into(), result: "ok".into() }
|
||||
}
|
||||
|
||||
fn doc_ref() -> DocRef {
|
||||
DocRef { path: s("notes/eureka-pipeline-state-target.md"), start_line: 1, end_line: 9, commit: sha() }
|
||||
}
|
||||
|
||||
fn delta() -> StructuralDelta {
|
||||
StructuralDelta {
|
||||
lines_added: 0,
|
||||
lines_removed: 0,
|
||||
dependencies_added: vec![],
|
||||
dependencies_removed: vec![],
|
||||
formats_added: vec![],
|
||||
formats_removed: vec![],
|
||||
targets_added: vec![],
|
||||
targets_removed: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn instance(name: &str) -> PipelineDocument {
|
||||
PipelineDocument::Instance(PipelineInstance {
|
||||
instance: slug(name),
|
||||
|
|
@ -29,6 +97,205 @@ pub(crate) fn instance(name: &str) -> PipelineDocument {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn stewardship(instance: &str, repo_name: &str) -> PipelineDocument {
|
||||
PipelineDocument::Stewardship(PipelineStewardship {
|
||||
instance: slug(instance),
|
||||
repo: repo(repo_name),
|
||||
assigned_on: date(),
|
||||
note: "assigned".into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn campaign(repos: &[&str]) -> PipelineDocument {
|
||||
PipelineDocument::Campaign(PipelineCampaign {
|
||||
slug: slug(CAMPAIGN),
|
||||
title: s("Eureka pipeline state"),
|
||||
repos: repos.iter().map(|name| repo(name)).collect(),
|
||||
working_branch: s("codex/eureka-pipeline-state"),
|
||||
target_doc: doc_ref(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn target(revision: u32, invariants: &[&str]) -> PipelineDocument {
|
||||
PipelineDocument::Target(PipelineTarget {
|
||||
campaign: slug(CAMPAIGN),
|
||||
revision,
|
||||
invariants: invariants
|
||||
.iter()
|
||||
.map(|label| TargetInvariant { label: l(label), statement: "Only admission writes.".into() })
|
||||
.collect(),
|
||||
not_in_scope: vec![],
|
||||
canonical_implementations: vec![],
|
||||
doc: doc_ref(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn question(label: &str, options: &[&str], recommended: &str) -> PipelineDocument {
|
||||
PipelineDocument::Question(PipelineQuestion {
|
||||
campaign: slug(CAMPAIGN),
|
||||
label: l(label),
|
||||
question: "Who owns the state?".into(),
|
||||
options: options.iter().map(|option| QuestionOption { label: l(option), text: "an option".into() }).collect(),
|
||||
recommended: l(recommended),
|
||||
depends: vec![],
|
||||
raised_in: None,
|
||||
asked_on: date(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ruling(label: &str) -> PipelineRuling {
|
||||
PipelineRuling {
|
||||
campaign: slug(CAMPAIGN),
|
||||
label: l(label),
|
||||
answers: None,
|
||||
choice: None,
|
||||
ruling: "An instance owns its mind.".into(),
|
||||
operator_quote: None,
|
||||
ruled_on: date(),
|
||||
precedents: vec![],
|
||||
authority: RulingAuthority::Standing,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cut_spec(cut: &str, revision: u32) -> PipelineCutSpec {
|
||||
PipelineCutSpec {
|
||||
campaign: slug(CAMPAIGN),
|
||||
cut: l(cut),
|
||||
revision,
|
||||
title: s("A cut"),
|
||||
repo: repo(REPO),
|
||||
branch: s("codex/eureka-pipeline-state"),
|
||||
base: sha(),
|
||||
depends_on: vec![],
|
||||
first: vec![],
|
||||
deletes: vec![],
|
||||
keeps_moves: vec![],
|
||||
adds: vec![],
|
||||
file_changes: vec![],
|
||||
authority_map: None::<AuthorityMap>,
|
||||
verification: CutVerification { builds: vec![], tests: vec![], negative: vec![], operator: vec![] },
|
||||
estimate: delta(),
|
||||
rulings: vec![],
|
||||
questions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cut_report(cut: &str, attempt: u32) -> PipelineCutReport {
|
||||
PipelineCutReport {
|
||||
campaign: slug(CAMPAIGN),
|
||||
cut_spec: s(&id("cut_spec", &format!("cut-{cut}.r1"))),
|
||||
attempt,
|
||||
repo: repo(REPO),
|
||||
branch: s("codex/eureka-pipeline-state"),
|
||||
commits: vec![ReportCommit { sha: sha(), subject: "Land the cut".into(), builds: true }],
|
||||
range: range(),
|
||||
verification: vec![evidence()],
|
||||
mutations: vec![MutationRecord {
|
||||
label: l("M1"),
|
||||
rule: "the rule".into(),
|
||||
location: location(),
|
||||
before: "before".into(),
|
||||
after: "after".into(),
|
||||
commit: sha(),
|
||||
failed_as_expected: true,
|
||||
}],
|
||||
deviations: vec![],
|
||||
forks: vec![],
|
||||
structural_delta: delta(),
|
||||
landed_names: vec![],
|
||||
undone: vec![],
|
||||
promises: vec![Promise { label: l("P1"), text: "One derived key per document.".into() }],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn claim(outcome: ClaimOutcome, findings: &[&str], promise: Option<&str>, mutations: &[&str]) -> VerdictClaim {
|
||||
VerdictClaim {
|
||||
claim: Line(format!("{outcome:?} claim")),
|
||||
outcome,
|
||||
evidence: vec![evidence()],
|
||||
findings: findings.iter().map(|finding| s(finding)).collect(),
|
||||
promise: promise.map(l),
|
||||
mutations: mutations.iter().map(|label| l(label)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verdict(cut: &str, pass: u32, claims: Vec<VerdictClaim>) -> PipelineDocument {
|
||||
PipelineDocument::Verdict(PipelineVerdict {
|
||||
campaign: slug(CAMPAIGN),
|
||||
cut_report: s(&id("cut_report", &format!("cut-{cut}.h1"))),
|
||||
pass,
|
||||
range: range(),
|
||||
claims,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn finding(cut: &str, pass: u32, label: &str, confidence: FindingConfidence) -> PipelineFinding {
|
||||
PipelineFinding {
|
||||
campaign: slug(CAMPAIGN),
|
||||
verdict: s(&id("verdict", &format!("cut-{cut}.s{pass}"))),
|
||||
label: l(label),
|
||||
range: range(),
|
||||
confidence,
|
||||
severity: FindingSeverity::High,
|
||||
claim: "A dotted label composes two keys.".into(),
|
||||
invariants: vec![l(INVARIANT)],
|
||||
locations: vec![location()],
|
||||
failure_scenario: "Two documents claim one key.".into(),
|
||||
evidence: vec![evidence()],
|
||||
precedents: vec![],
|
||||
origin: FindingOrigin::Introduced,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn follow_up(label: &str, source: PipelineRef) -> PipelineDocument {
|
||||
PipelineDocument::FollowUp(PipelineFollowUp {
|
||||
campaign: slug(CAMPAIGN),
|
||||
label: l(label),
|
||||
source,
|
||||
repo: repo(REPO),
|
||||
locations: vec![location()],
|
||||
item: "Per-kind admission rules.".into(),
|
||||
why_it_can_wait: "The organ owns admission.".into(),
|
||||
owner: s("Hands"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn resolution(subject: PipelineRef, outcome: ResolutionOutcome) -> PipelineDocument {
|
||||
PipelineDocument::Resolution(PipelineResolution {
|
||||
subject,
|
||||
outcome,
|
||||
rationale: "Resolved.".into(),
|
||||
resolved_on: date(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn withdrawn() -> ResolutionOutcome {
|
||||
ResolutionOutcome::Withdrawn { reason: "moot".into() }
|
||||
}
|
||||
|
||||
pub(crate) fn superseded(by: &[PipelineRef]) -> ResolutionOutcome {
|
||||
ResolutionOutcome::Superseded { by: by.to_vec() }
|
||||
}
|
||||
|
||||
pub(crate) fn hand_off(from: &str, to: &str, repo_name: &str, documents: &[&str]) -> PipelineDocument {
|
||||
PipelineDocument::HandOff(PipelineHandOff {
|
||||
from_instance: slug(from),
|
||||
to_instance: slug(to),
|
||||
repo: repo(repo_name),
|
||||
documents: documents.iter().map(|document| s(document)).collect(),
|
||||
reason: "The workstation mind takes the campaign.".into(),
|
||||
handed_on: date(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn provenance(faculty: Faculty) -> PipelineProvenance {
|
||||
PipelineProvenance { faculty, agent: s("claude"), session: s("session-1"), tool: s("admit") }
|
||||
}
|
||||
|
||||
pub(crate) fn now() -> DateTime<Utc> {
|
||||
Utc.with_ymd_and_hms(2026, 9, 16, 12, 0, 0).unwrap()
|
||||
}
|
||||
|
||||
/// The leaf's envelope for a document, keyed and encoded as the organ stores
|
||||
/// it.
|
||||
pub(crate) fn prepare(document: &PipelineDocument) -> CultCacheEnvelope {
|
||||
|
|
@ -40,3 +307,40 @@ pub(crate) fn prepare(document: &PipelineDocument) -> CultCacheEnvelope {
|
|||
pub(crate) fn epoch() -> CultCacheEnvelope {
|
||||
HuginnMindEpoch::envelope(&schema_cache().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn opened<S: MindStore>(store: S, instance: &str) -> Mind<S> {
|
||||
Mind::open_with(store, &slug(instance)).unwrap()
|
||||
}
|
||||
|
||||
/// Admits as Hands at the fixed clock.
|
||||
pub(crate) fn admit<S: MindStore>(mind: &mut Mind<S>, documents: Vec<PipelineDocument>) -> PipelineAdmissionOutcome {
|
||||
let batch = PipelineAdmissionBatch { instance: mind.instance().clone(), provenance: provenance(Faculty::Hands), documents };
|
||||
mind.admit(batch, now())
|
||||
}
|
||||
|
||||
/// The first write into a fresh yggdrasil mind: its identity, stewardship of
|
||||
/// the campaign repo, the campaign and a first target with one invariant.
|
||||
pub(crate) fn seed<S: MindStore>(mind: &mut Mind<S>) {
|
||||
let outcome = admit(mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO]), target(1, &[INVARIANT])]);
|
||||
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
|
||||
}
|
||||
|
||||
pub(crate) fn seeded() -> Mind<MemoryStore> {
|
||||
let mut mind = opened(MemoryStore::new(), INSTANCE);
|
||||
seed(&mut mind);
|
||||
mind
|
||||
}
|
||||
|
||||
pub(crate) fn refusal(outcome: PipelineAdmissionOutcome) -> MindRefusal {
|
||||
match outcome {
|
||||
PipelineAdmissionOutcome::Refused(refusal) => refusal,
|
||||
other => panic!("expected a refusal, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn committed(outcome: PipelineAdmissionOutcome) -> (String, Vec<PipelineRef>) {
|
||||
match outcome {
|
||||
PipelineAdmissionOutcome::Committed { receipt_id, writes, .. } => (receipt_id, writes),
|
||||
other => panic!("expected a commit, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
//! type, a foreign epoch, a missing identity or a foreign identity before it
|
||||
//! attaches anything. `receipt::commit` owns "did a batch enter, whole, with a
|
||||
//! receipt": one compare-and-swap over the store, a receipt naming the exact
|
||||
//! bytes read and written, and a typed conflict when the swap loses. The
|
||||
//! admission path (Cut 8's third commit) owns "may this batch enter".
|
||||
//! bytes read and written, and a typed conflict when the swap loses.
|
||||
//! `Mind::admit_prepared` owns "may this batch enter": every cross-field and
|
||||
//! cross-document rule, every derived write, and the replay check, in one
|
||||
//! ordered path that `admit`, the daemon's sink and the hand-off all share.
|
||||
//!
|
||||
//! Document shape, bounds, formats and keys are `epiphany-pipeline`'s; the
|
||||
//! organ registers, prepares, decodes and validates through the leaf's four
|
||||
|
|
@ -15,6 +17,7 @@
|
|||
//! invariant's mechanism. No function here reads a clock or the environment:
|
||||
//! `now` is passed in.
|
||||
|
||||
pub mod admission;
|
||||
pub mod mind;
|
||||
pub mod receipt;
|
||||
pub mod refusal;
|
||||
|
|
@ -23,6 +26,7 @@ pub mod store;
|
|||
#[cfg(test)]
|
||||
pub(crate) mod fixtures;
|
||||
|
||||
pub use admission::{BATCH_MAX, PipelineAdmissionBatch, PipelineAdmissionOutcome};
|
||||
pub use mind::{HuginnMindEpoch, Mind};
|
||||
pub use receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION};
|
||||
pub use refusal::MindRefusal;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! The commit receipt: what a batch read and what it wrote, byte for byte.
|
||||
//!
|
||||
//! This is the bounded duplicate of `epiphany-core`'s receipt (FU-3), smaller
|
||||
//! This is the bounded duplicate of Epiphany's Mind receipt (FU-3), smaller
|
||||
//! by design: no authority enum, no companion documents, no `invariant_owner`,
|
||||
//! no `store_id`. Provenance is a field on the receipt, not a companion, and
|
||||
//! the digest excludes it, so an exact replay across sessions and faculties
|
||||
|
|
|
|||
Loading…
Reference in a new issue