From 9298371f95317f87a39c3dfe2780d005571011a5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 17:38:04 +0200 Subject: [PATCH 01/12] Target gains repos: a campaign's repo set changes by target revision PipelineTarget.repos (additive, `= absent`, epoch stays v2) names the campaign's whole repo set from that revision on; repos_in_force is the one owner of cut_spec membership and replaces the direct campaign.repos check; a revision past the first may not empty the set its predecessor named (EmptyRepos on the target id). Golden line and regenerated schemas follow in the next commit (ruling campaign-repo-set-home). Co-Authored-By: Claude Sonnet 5.5 --- crates/eureka-pipeline/src/lib.rs | 75 +++++++++++++++++++++---- crates/huginn-mind/src/admission.rs | 87 ++++++++++++++++++++++++++++- crates/huginn-mind/src/docs.rs | 19 ++++++- crates/huginn-mind/src/fixtures.rs | 1 + crates/huginn-mind/src/refusal.rs | 1 + crates/mind-body/src/testkit.rs | 1 + crates/mind-body/tests/wake.rs | 1 + 7 files changed, 173 insertions(+), 12 deletions(-) diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 07c6857..d19d192 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -491,16 +491,17 @@ macro_rules! unit_enums { )*}; } -/// RS-L closing fix 4: `campaign.repos` refuses two entries naming the same +/// RS-L closing fix 4: a repo list refuses two entries naming the same /// repository by identity, not by raw spelling, so `GameCult/Epiphany` and /// `gamecult/epiphany` in one list refuse exactly as one entry twice would. -/// This is a cross-element invariant on `PipelineCampaign` alone, not a -/// per-type format rule any other `value_types!` struct shares, so it is -/// wired in as that struct's one `=> extra` hook rather than folded into -/// `Bounded for OrgRepo` or the list bound in `bounded_field!`. -fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result<(), PipelineRefusal> { +/// This is a cross-element invariant on the lists alone, not a per-type format +/// rule any other `value_types!` struct shares, so it is wired in as the +/// `=> extra` hook of the two structs that carry a repo list +/// (`PipelineCampaign.repos` and `PipelineTarget.repos`) rather than folded +/// into `Bounded for OrgRepo` or the list bound in `bounded_field!`. +fn repos_are_distinct(repos: &[OrgRepo], at: &str) -> Result<(), PipelineRefusal> { let mut seen = std::collections::HashSet::new(); - for repo in &campaign.repos { + for repo in repos { if !seen.insert(repo.identity()) { return Err(PipelineRefusal::DuplicateRepo { field: format!("{at}.repos"), @@ -511,6 +512,14 @@ fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result< Ok(()) } +fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result<(), PipelineRefusal> { + repos_are_distinct(&campaign.repos, at) +} + +fn target_repos_are_distinct(target: &PipelineTarget, at: &str) -> Result<(), PipelineRefusal> { + repos_are_distinct(&target.repos, at) +} + /// The most lines one read anchor may span. const READ_WINDOW_MAX_LINES: u32 = 100; @@ -625,10 +634,12 @@ value_types! { } pub struct PipelineCampaign { slug: Slug, title: Title, repos: Vec[8], working_branch: Short, target_doc: DocRef } => campaign_repos_are_distinct + /// `repos`, when non-empty, is the campaign's whole repo set from this + /// revision on; empty keeps the previous set (ruling campaign-repo-set-home). pub struct PipelineTarget { campaign: Slug, revision: u32, invariants: Vec[32], not_in_scope: Vec[32], - canonical_implementations: Vec[16], doc: DocRef, - } + canonical_implementations: Vec[16], repos: Vec[16] = absent, doc: DocRef, + } => target_repos_are_distinct pub struct PipelineQuestion { campaign: Slug, label: Label, title: Title, question: Para, options: Vec[8], recommended: Label, depends: Vec[8], raised_in: Option, asked_on: Date, @@ -1238,7 +1249,7 @@ mod tests { (D::Target(PipelineTarget { campaign: slug(CAMPAIGN), revision: 2, invariants: vec![TargetInvariant { label: l("mind-admits"), statement: "Only admission writes.".into() }], - not_in_scope: vec!["Eve browsing".into()], canonical_implementations: vec!["CultLib".into()], doc: doc_ref(), + not_in_scope: vec!["Eve browsing".into()], canonical_implementations: vec!["CultLib".into()], repos: vec![], doc: doc_ref(), }), format!("{CAMPAIGN}:target:r2")), (D::Question(PipelineQuestion { campaign: slug(CAMPAIGN), label: l("Q1"), title: t("Who owns the state?"), question: "Who owns the state?".into(), @@ -1506,6 +1517,9 @@ mod tests { let mut f = finding_sample(); f.locations[0].symbol = Some("&1153090498".into()); out.push(("finding.location-symbol".into(), D::Finding(f))); + let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() }; + target.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("GameCult/Huginn".into())]; + out.push(("target.repos".into(), D::Target(target))); out } @@ -3163,6 +3177,47 @@ mod tests { assert_eq!(PipelineDocument::Finding(finding).validate(), Ok(())); } + #[test] + fn target_repos_absent_decode_and_round_trip() -> Result<()> { + // The MCP door: a JSON target with no `repos` key decodes to an empty list. + let PipelineDocument::Target(sample) = samples().remove(1).0 else { unreachable!() }; + let mut json = serde_json::to_value(PipelineDocument::Target(sample.clone()))?; + assert!(json["value"].get("repos").is_none(), "an empty repos is not written"); + let PipelineDocument::Target(decoded) = serde_json::from_value::(json.clone())? else { unreachable!() }; + assert!(decoded.repos.is_empty()); + assert!(!rmp_serde::to_vec_named(&PipelineDocument::Target(sample.clone()))?.windows(5).any(|window| window == b"repos")); + + let mut named = sample; + named.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("GameCult/Huginn".into())]; + let document = PipelineDocument::Target(named); + document.validate()?; + json = serde_json::to_value(&document)?; + assert_eq!(serde_json::from_value::(json)?, document); + let packed = rmp_serde::to_vec_named(&document)?; + assert_eq!(rmp_serde::from_slice::(&packed)?, document); + assert!(packed.windows(5).any(|window| window == b"repos"), "the repos key is written"); + Ok(()) + } + + #[test] + fn target_repos_bounded_and_distinct() { + let with_repos = |repos: Vec| { + let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() }; + target.repos = repos; + PipelineDocument::Target(target).validate() + }; + let distinct = |count: usize| (0..count).map(|index| OrgRepo(format!("GameCult/Repo{index}"))).collect::>(); + assert_eq!(with_repos(distinct(16)), Ok(())); + assert_eq!( + with_repos(distinct(17)), + Err(PipelineRefusal::FieldBound { field: "target.repos".into(), limit: 16, actual: 17 }) + ); + assert_eq!( + with_repos(vec![OrgRepo("GameCult/Huginn".into()), OrgRepo("gamecult/huginn".into())]), + Err(PipelineRefusal::DuplicateRepo { field: "target.repos".into(), repo: "gamecult/huginn".into() }) + ); + } + /// The marker needs an `Absent` spelling; the two impls say what absent is. #[test] fn absent_is_none_and_empty() { diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 8d55f90..01eb96e 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -456,6 +456,15 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { D::Target(target) => { let predecessor = D::Target(eureka_pipeline::PipelineTarget { revision: target.revision.wrapping_sub(1), ..target.clone() }); revision_rule(docs, K::Target, key, target.revision, &predecessor)?; + // A revision past the first may not empty the repo set its predecessor + // named: empty means "keep the previous set", and that would silently + // narrow it to the campaign's opening set (ruling campaign-repo-set-home). + if target.repos.is_empty() && target.revision > 1 { + let named = matches!(docs.find(K::Target, &pipeline_key(&predecessor)?), Some(D::Target(previous)) if !previous.repos.is_empty()); + if named { + return Err(MindRefusal::EmptyRepos { campaign: key.into() }); + } + } unique_labels("target.invariants", target.invariants.iter().map(|invariant| invariant.label.0.as_str())) } D::Question(question) => { @@ -492,7 +501,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { let Some(campaign) = campaign else { return Err(MindRefusal::MissingReference { kind: K::Campaign, id: spec.campaign.0.clone() }); }; - if !campaign.repos.contains(&spec.repo) { + if !docs.repos_in_force(campaign).contains(&spec.repo) { return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() }); } for ruling in &spec.rulings { @@ -2704,6 +2713,82 @@ mod tests { assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); } + /// A target revision naming repos, and the supersession of its predecessor. + fn repo_revision(revision: u32, repos: &[&str]) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!() }; + named.repos = repos.iter().map(|name| repo(name)).collect(); + let previous = r(K::Target, &id("target", &format!("r{}", revision - 1))); + let this = r(K::Target, &id("target", &format!("r{revision}"))); + vec![D::Target(named), resolution(previous, superseded(&[this]))] + } + + fn spec_on(cut: &str, repo_name: &str) -> D { + let mut spec = cut_spec(cut, 1); + spec.repo = repo(repo_name); + D::CutSpec(spec) + } + + #[test] + fn a_target_revision_adds_a_repo_a_spec_may_name() { + let mut mind = seeded(); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + + // The same revision, supersession and spec in one batch. + let mut fresh = seeded(); + let mut batch = repo_revision(2, &[REPO, OTHER_REPO]); + batch.push(spec_on("1", OTHER_REPO)); + committed(admit(&mut fresh, batch)); + } + + #[test] + fn a_target_revision_removes_a_repo() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + committed(admit(&mut mind, repo_revision(2, &[OTHER_REPO]))); + let spec_id = id("cut_spec", "cut-1.r1"); + let spec_view = mind.view(&r(K::CutSpec, &spec_id)).expect("a valid ref").expect("the spec is held"); + assert_eq!(spec_view.status, crate::query::PipelineStatus::InForce, "spec 1 r1 stays in force"); + committed(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &spec_id), withdrawn())])); + + let mut second = cut_spec("1", 2); + second.repo = repo(REPO); + let revised = id("cut_spec", "cut-1.r2"); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(second), resolution(r(K::CutSpec, &spec_id), superseded(&[r(K::CutSpec, &revised)]))])), + MindRefusal::RepoNotInCampaign { repo: REPO.into() } + ); + committed(admit(&mut mind, vec![spec_on("2", OTHER_REPO)])); + } + + #[test] + fn a_target_revision_cannot_silently_drop_its_repos() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + refusal(admit(&mut mind, repo_revision(3, &[]))), + MindRefusal::EmptyRepos { campaign: id("target", "r3") } + ); + // Nothing was written: the restating revision takes the same key. + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + // An empty revision whose predecessor named none keeps the campaign's set. + let mut plain = seeded(); + committed(admit(&mut plain, repo_revision(2, &[]))); + } + + #[test] + fn a_repo_revision_replays_idempotently() { + let mut mind = seeded(); + let (first, _) = committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO])), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } + ); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + } + #[test] fn a_hand_off_derives_this_minds_side_only() { let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index 2427338..c124719 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -7,7 +7,7 @@ use cultcache_rs::CultCacheEnvelope; use eureka_pipeline::{ - OrgRepo, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, + OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RunOperator, Short, Slug, }; use schemars::JsonSchema; @@ -182,6 +182,23 @@ impl Docs { }) } + /// The repos a campaign's cuts may name: the repo set of the highest-revision + /// in-force target of `campaign.slug` that names one, else the campaign's own + /// opening set. The one owner of membership (ruling campaign-repo-set-home). + pub(crate) fn repos_in_force<'a>(&'a self, campaign: &'a PipelineCampaign) -> &'a [OrgRepo] { + self.of_kind(PipelineKind::Target) + .filter_map(|(key, document)| match document { + PipelineDocument::Target(target) + if target.campaign == campaign.slug && !target.repos.is_empty() && self.in_force(PipelineKind::Target, key) => + { + Some(target) + } + _ => None, + }) + .max_by_key(|target| target.revision) + .map_or(campaign.repos.as_slice(), |target| target.repos.as_slice()) + } + /// The resolution that closes the document, if any: the views' status, /// derived where admission derives whether there is one at all. pub(crate) fn closing_resolution(&self, kind: PipelineKind, id: &str) -> Option<(&str, &PipelineResolution)> { diff --git a/crates/huginn-mind/src/fixtures.rs b/crates/huginn-mind/src/fixtures.rs index 86cb827..77dbd45 100644 --- a/crates/huginn-mind/src/fixtures.rs +++ b/crates/huginn-mind/src/fixtures.rs @@ -155,6 +155,7 @@ pub(crate) fn target(revision: u32, invariants: &[&str]) -> PipelineDocument { .collect(), not_in_scope: vec![], canonical_implementations: vec![], + repos: vec![], doc: doc_ref(), }) } diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index e85113a..150f652 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -41,6 +41,7 @@ pub enum MindRefusal { InvalidOptions { question: String }, InvalidChoice { ruling: String, choice: String }, QuoteWithoutOperator { ruling: String }, + /// `campaign` names the refused document: a campaign, or a target revision that empties its predecessor's repos. EmptyRepos { campaign: String }, RepoNotInCampaign { repo: String }, UnknownDependency { cut: String }, diff --git a/crates/mind-body/src/testkit.rs b/crates/mind-body/src/testkit.rs index d00db01..2c27bf6 100644 --- a/crates/mind-body/src/testkit.rs +++ b/crates/mind-body/src/testkit.rs @@ -214,6 +214,7 @@ fn target() -> PipelineDocument { invariants: vec![TargetInvariant { label: "run-is-the-grant".into(), statement: "A run is the grant.".into() }], not_in_scope: vec![], canonical_implementations: vec![], + repos: vec![], doc: doc_ref(), }) } diff --git a/crates/mind-body/tests/wake.rs b/crates/mind-body/tests/wake.rs index c535391..58ffe12 100644 --- a/crates/mind-body/tests/wake.rs +++ b/crates/mind-body/tests/wake.rs @@ -116,6 +116,7 @@ fn seed() -> Vec { invariants: vec![TargetInvariant { label: "run-is-the-grant".into(), statement: "A run is the grant.".into() }], not_in_scope: vec![], canonical_implementations: vec![], + repos: vec![], doc: doc_ref(), }), ] From 6e5fb9153c98ebcf3b8ddc2bbb1b199471b8b76f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 17:40:14 +0200 Subject: [PATCH 02/12] Regenerate golden and schemas for target.repos One appended golden line (target.repos) dumped by golden_dump, one header comment line; the target, mind_request and mind_response schemas regenerated from the derivation. No existing golden line changed. Co-Authored-By: Claude Sonnet 5.5 --- crates/eureka-pipeline/golden/envelopes.txt | 2 ++ .../cultnet/epiphany.pipeline.target.v2.schema.json | 13 +++++++++++++ schemas/cultnet/huginn.mind_request.v1.schema.json | 8 ++++++++ schemas/cultnet/huginn.mind_response.v1.schema.json | 9 +++++++++ 4 files changed, 32 insertions(+) diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index a5e6fc8..cbd66ac 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -10,6 +10,7 @@ # Epiphany ef956865 modulo that rename (verdict ...s2). Never regenerate to make # a failing test pass: a diff here is a wire change and needs its own ruling. # cut_spec.reads-and-symbol and finding.location-symbol were appended after 44c010c added CodeLocation.symbol and PipelineCutSpec.reads (ruling context-pack-read-anchors); no earlier line changed. +# target.repos was appended after 9298371 added PipelineTarget.repos (ruling campaign-repo-set-home); no earlier line changed. sample.campaign eureka-state:campaign:self epiphany.pipeline.campaign.v2 EpiphanyPipelineCampaignDocument 9185a4736c7567ac657572656b612d7374617465a57469746c65b5457572656b6120706970656c696e65207374617465a57265706f7391b147616d6543756c742f4570697068616e79ae776f726b696e675f6272616e6368bb636f6465782f657572656b612d706970656c696e652d7374617465aa7461726765745f646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 sample.target eureka-state:target:r2 epiphany.pipeline.target.v2 EpiphanyPipelineTargetDocument 9186a863616d706169676eac657572656b612d7374617465a87265766973696f6e02aa696e76617269616e74739182a56c6162656cab6d696e642d61646d697473a973746174656d656e74b64f6e6c792061646d697373696f6e207772697465732eac6e6f745f696e5f73636f706591ac4576652062726f7773696e67b963616e6f6e6963616c5f696d706c656d656e746174696f6e7391a743756c744c6962a3646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 sample.question eureka-state:question:Q1 epiphany.pipeline.question.v2 EpiphanyPipelineQuestionDocument 9189a863616d706169676eac657572656b612d7374617465a56c6162656ca25131a57469746c65b357686f206f776e73207468652073746174653fa87175657374696f6eb357686f206f776e73207468652073746174653fa76f7074696f6e739282a56c6162656ca141a474657874ab616e20696e7374616e636582a56c6162656ca142a474657874a661207265706fab7265636f6d6d656e646564a141a7646570656e647391a6437574203361a97261697365645f696e82a46b696e64a743757453706563a26964bf657572656b612d73746174653a6375745f737065633a6375742d33612e7231a861736b65645f6f6eaa323032362d30392d3135 @@ -43,3 +44,4 @@ finding.severity-low eureka-state:finding:cut-3a.s2.F4 epiphany.pipeline.finding ruling.authority-mind eureka-state:ruling:R8 epiphany.pipeline.ruling.v2 EpiphanyPipelineRulingDocument 918aa863616d706169676eac657572656b612d7374617465a56c6162656ca25238a57469746c65b9416e20696e7374616e6365206f776e7320697473206d696e64a7616e7377657273c0a663686f696365c0a672756c696e67ba416e20696e7374616e6365206f776e7320697473206d696e642eae6f70657261746f725f71756f7465c0a872756c65645f6f6eaa323032362d30392d3135aa707265636564656e74739185a47265706fb147616d6543756c742f4165746865726961a6636f6d6d6974d92861616161616161616161616161616161616161616161616161616161616161616161616161616161a46b696e64a652756c696e67a26964b363756c7463616368653a72756c696e673a5231ae7061796c6f61645f736861323536d94062626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262a9617574686f72697479a44d696e64 cut_spec.reads-and-symbol eureka-state:cut_spec:cut-3a.r1 epiphany.pipeline.cut_spec.v2 EpiphanyPipelineCutSpecDocument 91de0013a863616d706169676eac657572656b612d7374617465a3637574a23361a87265766973696f6e01a57469746c65b2506970656c696e6520646f63756d656e7473a47265706fb147616d6543756c742f4570697068616e79a66272616e6368bb636f6465782f657572656b612d706970656c696e652d7374617465a462617365a83566393832323864aa646570656e64735f6f6e91a132a5666972737491ae526561642074686520737065632ea764656c657465739183a470617468a66f6c642e7273a56c696e657303a46e6f7465a464656164ab6b656570735f6d6f76657391ac636f6d6d6974206f776e6572a46164647391b5706970656c696e655f646f63756d656e74732e7273ac66696c655f6368616e6765739182a86c6f636174696f6e84a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509a673796d626f6cab76616c75655f7479706573a66368616e6765a3616464a572656164739282a86c6f636174696f6e84a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e650aa8656e645f6c696e6514a673796d626f6caf506970656c696e6543757453706563a3776879b274686520646f63756d656e7420736861706582a86c6f636174696f6e83a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e651ea8656e645f6c696e65c0a3776879a86f6e65206c696e65ad617574686f726974795f6d617087a56f776e6572a4636f7265a6696e7075747391af747970656420646f63756d656e7473a76f75747075747391a9656e76656c6f706573ad646572697665645f737461746591ac64657269766564206b657973b1666f7262696464656e5f7772697465727391a34d4350ac7368617265645f706174687391a961646d697373696f6ead64656c6574696f6e5f6c696e65a36e2f61ac766572696669636174696f6e84a66275696c647391ab636172676f20636865636ba574657374739182a46e616d65a46b657973a470696e73af6f6e652064657269766564206b6579a86e656761746976659182a77061747465726ea75665633c75383ea573636f7065a9646f63756d656e7473a86f70657261746f7291a46e6f6e65a8657374696d61746588ab6c696e65735f6164646564cd0384ad6c696e65735f72656d6f76656400b2646570656e64656e636965735f616464656491a8736368656d617273b4646570656e64656e636965735f72656d6f76656490ad666f726d6174735f616464656491b66570697068616e792e706970656c696e652e2a2e7631af666f726d6174735f72656d6f76656490ad746172676574735f616464656490af746172676574735f72656d6f76656490a772756c696e677391b6657572656b612d73746174653a72756c696e673a5238a97175657374696f6e7391b8657572656b612d73746174653a7175657374696f6e3a5131 finding.location-symbol eureka-state:finding:cut-3a.s2.F4 epiphany.pipeline.finding.v2 EpiphanyPipelineFindingDocument 918da863616d706169676eac657572656b612d7374617465a776657264696374be657572656b612d73746174653a766572646963743a6375742d33612e7332a56c6162656ca24634a572616e676582a462617365a83566393832323864a468656164a83566393832323864aa636f6e666964656e6365a9436f6e6669726d6564a87365766572697479a448696768a5636c61696dd9214120646f74746564206c6162656c20636f6d706f7365732074776f206b6579732eaa696e76617269616e747391ab6d696e642d61646d697473a96c6f636174696f6e739184a470617468d9216372617465732f657572656b612d706970656c696e652f7372632f6c69622e7273a46c696e6501a8656e645f6c696e6509a673796d626f6cab2631313533303930343938b06661696c7572655f7363656e6172696fbc54776f20646f63756d656e747320636c61696d206f6e65206b65792ea865766964656e63659183a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6baa707265636564656e747390a66f726967696eaa496e74726f6475636564 +target.repos eureka-state:target:r2 epiphany.pipeline.target.v2 EpiphanyPipelineTargetDocument 9187a863616d706169676eac657572656b612d7374617465a87265766973696f6e02aa696e76617269616e74739182a56c6162656cab6d696e642d61646d697473a973746174656d656e74b64f6e6c792061646d697373696f6e207772697465732eac6e6f745f696e5f73636f706591ac4576652062726f7773696e67b963616e6f6e6963616c5f696d706c656d656e746174696f6e7391a743756c744c6962a57265706f7392b147616d6543756c742f4570697068616e79af47616d6543756c742f487567696e6ea3646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864 diff --git a/schemas/cultnet/epiphany.pipeline.target.v2.schema.json b/schemas/cultnet/epiphany.pipeline.target.v2.schema.json index 32a5ab3..ee7fd68 100644 --- a/schemas/cultnet/epiphany.pipeline.target.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.target.v2.schema.json @@ -37,6 +37,11 @@ "maxLength": 1000, "type": "string" }, + "OrgRepo": { + "description": "A GitHub repository as `Org/Repo`, at most 140 UTF-8 bytes (39 + 1 +\n100). The owner is 1 to 39 `[A-Za-z0-9-]` bytes with no leading or\ntrailing hyphen; the repo is 1 to 100 `[A-Za-z0-9._-]` bytes, is never\n`.` or `..`, and does not end in `.git`, ASCII-case-insensitively.\nIdentity is case-insensitive: `GameCult/Epiphany` and `gamecult/epiphany`\nare the same repository.", + "maxLength": 140, + "type": "string" + }, "Sha": { "description": "A git commit id: 7-40 lowercase hex characters.", "maxLength": 40, @@ -70,6 +75,7 @@ }, "$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v2.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "`repos`, when non-empty, is the campaign's whole repo set from this\nrevision on; empty keeps the previous set (ruling campaign-repo-set-home).", "properties": { "campaign": { "$ref": "#/$defs/Slug" @@ -98,6 +104,13 @@ "maxItems": 32, "type": "array" }, + "repos": { + "items": { + "$ref": "#/$defs/OrgRepo" + }, + "maxItems": 16, + "type": "array" + }, "revision": { "format": "uint32", "minimum": 0, diff --git a/schemas/cultnet/huginn.mind_request.v1.schema.json b/schemas/cultnet/huginn.mind_request.v1.schema.json index c075586..77ee834 100644 --- a/schemas/cultnet/huginn.mind_request.v1.schema.json +++ b/schemas/cultnet/huginn.mind_request.v1.schema.json @@ -1542,6 +1542,7 @@ ] }, "PipelineTarget": { + "description": "`repos`, when non-empty, is the campaign's whole repo set from this\nrevision on; empty keeps the previous set (ruling campaign-repo-set-home).", "type": "object", "properties": { "campaign": { @@ -1571,6 +1572,13 @@ }, "maxItems": 32 }, + "repos": { + "type": "array", + "items": { + "$ref": "#/$defs/OrgRepo" + }, + "maxItems": 16 + }, "revision": { "type": "integer", "format": "uint32", diff --git a/schemas/cultnet/huginn.mind_response.v1.schema.json b/schemas/cultnet/huginn.mind_response.v1.schema.json index 4fd5482..28d1fa2 100644 --- a/schemas/cultnet/huginn.mind_response.v1.schema.json +++ b/schemas/cultnet/huginn.mind_response.v1.schema.json @@ -1152,6 +1152,7 @@ ] }, { + "description": "`campaign` names the refused document: a campaign, or a target revision that empties its predecessor's repos.", "type": "object", "properties": { "EmptyRepos": { @@ -3801,6 +3802,7 @@ ] }, "PipelineTarget": { + "description": "`repos`, when non-empty, is the campaign's whole repo set from this\nrevision on; empty keeps the previous set (ruling campaign-repo-set-home).", "type": "object", "properties": { "campaign": { @@ -3830,6 +3832,13 @@ }, "maxItems": 32 }, + "repos": { + "type": "array", + "items": { + "$ref": "#/$defs/OrgRepo" + }, + "maxItems": 16 + }, "revision": { "type": "integer", "format": "uint32", From bb4ef404dc01c75c7183c9a85b854ac755512189 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 18:16:25 +0200 Subject: [PATCH 03/12] Resolution matrix: a target is superseded only by a target of its own campaign A lone foreign supersession closed a repo-naming target and narrowed membership to the opening set. Refuses IncompatibleResolution, as the cut_spec supersession rule does. Includes the admission tests (not yet verified). Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 01eb96e..44ff49c 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -804,6 +804,16 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M } } } + ResolutionOutcome::Superseded { by } if subject_kind == K::Target => { + let Some(D::Target(subject)) = docs.find(K::Target, subject_id) else { return Ok(()) }; + for supersessor in by { + if let Some(D::Target(target)) = docs.find(K::Target, &supersessor.id.0) + && target.campaign != subject.campaign + { + return Err(incompatible()); + } + } + } ResolutionOutcome::Answered { by } => { let Some(D::Ruling(ruling)) = docs.find(K::Ruling, &by.id.0) else { return Ok(()) }; if ruling.answers.as_ref().map(|answers| answers.0.as_str()) != Some(subject_id) { @@ -2789,6 +2799,38 @@ mod tests { committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); } + /// Another campaign, opened with `repos`, and its own target revision 1 naming `named`. + fn other_campaign(repos: &[&str], named: &[&str]) -> Vec { + let D::Campaign(mut campaign) = campaign(repos) else { panic!("a campaign") }; + campaign.slug = "other-campaign".into(); + let D::Target(mut target) = target(1, &[INVARIANT]) else { panic!("a target") }; + target.campaign = "other-campaign".into(); + target.repos = named.iter().map(|name| repo(name)).collect(); + vec![D::Campaign(campaign), D::Target(target)] + } + + #[test] + fn another_campaigns_target_does_not_widen_membership() { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[REPO, OTHER_REPO]))); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + } + + #[test] + fn a_target_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let foreign = r(K::Target, "other-campaign:target:r1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[foreign]))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Target, outcome: "Superseded".into() } + ); + // Nothing was written: r2 still names the repos, and its own revision still lands. + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + } + #[test] fn a_hand_off_derives_this_minds_side_only() { let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); From 86adbc85bd7f90fda128f06827aa62e96dd39f6c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 18:16:26 +0200 Subject: [PATCH 04/12] repos_in_force: tests pin the campaign filter and the in-force filter Docs-level image holds a closed higher revision and another campaign's higher target; admission-level test for the foreign target. HM3 (min revision) is equivalent and noted in the test. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/docs.rs | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index c124719..2ff49b5 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -501,3 +501,51 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel } edges } + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::*; + use PipelineDocument as D; + use PipelineKind as K; + + fn held(kind: K, key: String, document: D) -> Held { + Held { kind, key, document } + } + + fn named(campaign: &str, revision: u32, repos: &[&str]) -> (String, D) { + let D::Target(mut target) = target(revision, &[INVARIANT]) else { panic!("a target") }; + target.campaign = campaign.into(); + target.repos = repos.iter().map(|name| repo(name)).collect(); + (format!("{campaign}:target:r{revision}"), D::Target(target)) + } + + /// Membership is the highest-revision target of this campaign that is in + /// force and names repos. The image is built by hand because admission + /// cannot reach it: a revision above 1 supersedes its predecessor in its + /// own batch, a target is superseded only within its campaign, and an + /// empty revision after a repo-naming one is refused, so through admission + /// a closed repo-naming target is always older than the in-force one. + /// + /// Taking the lowest revision instead of the highest changes nothing there + /// either (one target per campaign is in force), so that mutation is + /// equivalent and not pinned. + #[test] + fn repos_in_force_is_this_campaigns_in_force_naming_target() { + let D::Campaign(this) = campaign(&[REPO]) else { panic!("a campaign") }; + let (in_force_key, in_force) = named(CAMPAIGN, 1, &[REPO, OTHER_REPO]); + let (closed_key, closed) = named(CAMPAIGN, 2, &["GameCult/Closed"]); + let (foreign_key, foreign) = named("other-campaign", 3, &["GameCult/Foreign"]); + let closing = resolution(r(K::Target, &closed_key), superseded(&[r(K::Target, &in_force_key)])); + let docs = Docs { + image: vec![ + held(K::Target, in_force_key, in_force), + held(K::Target, closed_key, closed), + held(K::Target, foreign_key, foreign), + held(K::Resolution, id("resolution", "target.r2.n1"), closing), + ], + batch: Vec::new(), + }; + assert_eq!(docs.repos_in_force(&this), [repo(REPO), repo(OTHER_REPO)], "neither the closed higher revision nor another campaign's higher target decides"); + } +} From 528a7f7e5f39caf6e746ea1c483475aab6cc89e6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 18:38:22 +0200 Subject: [PATCH 05/12] Supersession: one lineage rule for targets, cut_specs and rulings; tests pin every path same_lineage replaces the Target and CutSpec arms. A target is superseded only within its campaign, a cut_spec within its campaign and cut, a ruling by any ruling (ruling supersession-lineage). Tests cover same-batch, first/last supersessor, subject in batch, cut_spec cross-campaign, and ruling cross-campaign. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 99 +++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 44ff49c..3395f43 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -794,21 +794,11 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M } } match &resolution.outcome { - ResolutionOutcome::Superseded { by } if subject_kind == K::CutSpec => { - let Some(D::CutSpec(subject)) = docs.find(K::CutSpec, subject_id) else { return Ok(()) }; + ResolutionOutcome::Superseded { by } => { + let Some(subject) = docs.find(subject_kind, subject_id) else { return Ok(()) }; for supersessor in by { - if let Some(D::CutSpec(spec)) = docs.find(K::CutSpec, &supersessor.id.0) - && spec.cut != subject.cut - { - return Err(incompatible()); - } - } - } - ResolutionOutcome::Superseded { by } if subject_kind == K::Target => { - let Some(D::Target(subject)) = docs.find(K::Target, subject_id) else { return Ok(()) }; - for supersessor in by { - if let Some(D::Target(target)) = docs.find(K::Target, &supersessor.id.0) - && target.campaign != subject.campaign + if let Some(next) = docs.find(subject_kind, &supersessor.id.0) + && !same_lineage(subject, next) { return Err(incompatible()); } @@ -825,6 +815,17 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M Ok(()) } +/// The one supersession lineage rule (ruling `supersession-lineage`). A target is +/// superseded only by a target of its campaign, a cut_spec only by a cut_spec of its +/// campaign and cut, and a ruling by any ruling: operator directions cut across campaigns. +fn same_lineage(subject: &D, supersessor: &D) -> bool { + match (subject, supersessor) { + (D::Target(old), D::Target(new)) => old.campaign == new.campaign, + (D::CutSpec(old), D::CutSpec(new)) => old.campaign == new.campaign && old.cut == new.cut, + _ => true, + } +} + /// A10: a write whose identity the image already holds. Every kind answers /// the same way now that a resolution key carries a sequence: the identity is /// taken. Whether a subject is closed is A8's question, not this one's. @@ -2831,6 +2832,76 @@ mod tests { committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); } + /// A target of the first campaign (`CAMPAIGN`) at `revision`, naming no repos. + fn own_target(revision: u32) -> D { + target(revision, &[INVARIANT]) + } + + const FOREIGN_TARGET: &str = "other-campaign:target:r1"; + + fn supersession_refused(outcome: PipelineAdmissionOutcome, subject_kind: K) -> bool { + refusal(outcome) == MindRefusal::IncompatibleResolution { subject_kind, outcome: "Superseded".into() } + } + + #[test] + fn a_foreign_target_in_the_same_batch_does_not_supersede() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, FOREIGN_TARGET)]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn every_supersessor_of_a_target_is_checked_whichever_is_foreign() { + for foreign_first in [false, true] { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let own = r(K::Target, &id("target", "r2")); + let foreign = r(K::Target, FOREIGN_TARGET); + let by = if foreign_first { [foreign, own] } else { [own, foreign] }; + let batch = vec![own_target(2), resolution(r(K::Target, &id("target", "r1")), superseded(&by))]; + assert!(supersession_refused(admit(&mut mind, batch), K::Target), "foreign first: {foreign_first}"); + } + } + + #[test] + fn a_new_campaigns_target_in_its_own_batch_is_not_closed_by_a_foreign_one() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, FOREIGN_TARGET), superseded(&[r(K::Target, &id("target", "r1"))]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn a_cut_spec_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + let mut batch = other_campaign(&[REPO], &[]); + let D::CutSpec(mut foreign) = spec_on("1", REPO) else { panic!("a spec") }; + foreign.campaign = "other-campaign".into(); + batch.push(D::CutSpec(foreign)); + committed(admit(&mut mind, batch)); + let lone = vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, "other-campaign:cut_spec:cut-1.r1")]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::CutSpec)); + // Nothing was written: this campaign's own revision still lands. + let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; + next.revision = 2; + let this = r(K::CutSpec, &id("cut_spec", "cut-1.r2")); + committed(admit(&mut mind, vec![D::CutSpec(next), resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[this]))])); + } + + #[test] + fn a_ruling_is_superseded_by_a_ruling_of_any_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + let mut batch = other_campaign(&[REPO], &[]); + let mut foreign = ruling("new"); + foreign.campaign = "other-campaign".into(); + batch.push(D::Ruling(foreign)); + batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, "other-campaign:ruling:new")]))); + committed(admit(&mut mind, batch)); + } + #[test] fn a_hand_off_derives_this_minds_side_only() { let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); From adda2bf9aeba1f037878100cc78a3f9357d9db0f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 18:40:19 +0200 Subject: [PATCH 06/12] same_lineage: name PipelineDocument locally (528a7f7 does not build) Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 3395f43..4ac638b 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -818,7 +818,8 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M /// The one supersession lineage rule (ruling `supersession-lineage`). A target is /// superseded only by a target of its campaign, a cut_spec only by a cut_spec of its /// campaign and cut, and a ruling by any ruling: operator directions cut across campaigns. -fn same_lineage(subject: &D, supersessor: &D) -> bool { +fn same_lineage(subject: &PipelineDocument, supersessor: &PipelineDocument) -> bool { + use PipelineDocument as D; match (subject, supersessor) { (D::Target(old), D::Target(new)) => old.campaign == new.campaign, (D::CutSpec(old), D::CutSpec(new)) => old.campaign == new.campaign && old.cut == new.cut, From 5d9a845e697cc0957e97a2669da9d2c1afd36783 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 20:04:43 +0200 Subject: [PATCH 07/12] Supersession: line_of(record) is the one lineage; same_lineage and later_than call it Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 5963 +++++++++++++-------------- crates/huginn-mind/src/docs.rs | 63 +- 2 files changed, 3025 insertions(+), 3001 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 4ac638b..6383e00 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -1,2987 +1,2976 @@ -//! Admission: the single door into a mind. -//! -//! `admit` prepares typed documents through the leaf and calls -//! `admit_prepared`, the one commit path for Cut 8, 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: -//! -//! - A1 the batch names this mind, and attributes itself to a named agent and -//! session (`refuse_provenance`); A2 one to sixty-four envelopes; -//! - A3 every envelope passes the leaf's bounds, formats and key -//! recomputation; A4 identities are unique in the batch; A5 every document -//! carrying an instance names this mind; -//! - A6 an empty mind's first batch carries exactly one `instance` document, -//! and a batch carrying one derives the epoch record; -//! - A7 every reference names a document present in the image or the batch, -//! looked up by `(kind, id)`; the ones resolved in the image become the -//! receipt's strong reads; -//! - the derived writes (a ruling's `Answered` resolution, a hand-off's -//! stewardship withdrawal or assignment) are appended and pass A3-A7; -//! - A8 the per-kind rules, below; A9 an exact replay answers with the stored -//! receipt, after validation and never before it (ruling 20); A10 no write -//! collides with the image; A11 the commit. -//! -//! Every cross-field and cross-document rule lives here and nowhere else: -//! the leaf never gains one, the daemon and the client never re-derive one. -//! In force, the sequences and the scopes are `docs.rs`'s, shared with the -//! views (Cut 9). - -use std::collections::BTreeSet; - -use chrono::{DateTime, Utc}; -use cultcache_rs::{CultCache, CultCacheEnvelope}; -use eureka_pipeline::{ - ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal, - PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key, - validate_pipeline_write_envelope, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::docs::{CitationRole, Docs, Staged, citations, kind_of_id, outcome_citations}; -use crate::mind::{HuginnMindEpoch, Mind, unavailable}; -use crate::persona::{self, HuginnPersonaEntry, PersonaPutOutcome}; -use crate::receipt::{self, CommitOutcome, PipelineProvenance}; -use crate::refusal::{MindRefusal, RunId}; -use crate::store::MindStore; - -/// The most envelopes one batch may carry. -pub const BATCH_MAX: usize = 64; - -/// One admission request: the mind it is for, who asks, and the documents. -/// Rides the wire whole (`wire::HuginnMindRequest::Admit`). -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct PipelineAdmissionBatch { - pub instance: Slug, - pub provenance: PipelineProvenance, - pub documents: Vec, -} - -/// How admission ended. `Committed.writes` names every document the batch -/// landed, derived writes included; Cut 11 indexes from it after `admit` -/// returns, and never inside it. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum PipelineAdmissionOutcome { - Committed { receipt_id: String, committed_at: String, writes: Vec }, - AlreadyAdmitted { receipt_id: String }, - Refused(MindRefusal), - Conflict { identities: Vec }, -} - -/// Keys a document through the leaf, then encodes it. A key the leaf cannot -/// compose is the document's fault and is refused as a document; only the -/// cache's encode fault is the organ's. -fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result { - pipeline_key(document).map_err(MindRefusal::Document)?; - document.prepare(cache).map_err(unavailable) -} - -impl Mind { - /// Validates and prepares every document through the leaf, then admits - /// the envelopes. - pub fn admit(&mut self, batch: PipelineAdmissionBatch, now: DateTime) -> PipelineAdmissionOutcome { - let mut envelopes = Vec::with_capacity(batch.documents.len()); - for document in &batch.documents { - if let Err(refusal) = document.validate() { - return PipelineAdmissionOutcome::Refused(MindRefusal::Document(refusal)); - } - match prepare(document, self.cache()) { - Ok(envelope) => envelopes.push(envelope), - Err(refusal) => return PipelineAdmissionOutcome::Refused(refusal), - } - } - self.admit_prepared(&batch.instance, batch.provenance, envelopes, now) - } - - /// The single commit path: envelopes prepared by anyone, validated here - /// whoever prepared them. - pub fn admit_prepared( - &mut self, - instance: &Slug, - provenance: PipelineProvenance, - envelopes: Vec, - now: DateTime, - ) -> PipelineAdmissionOutcome { - match self.admit_steps(instance, provenance, envelopes, now) { - Ok(outcome) => outcome, - Err(refusal) => PipelineAdmissionOutcome::Refused(refusal), - } - } - - fn admit_steps( - &mut self, - instance: &Slug, - provenance: PipelineProvenance, - envelopes: Vec, - now: DateTime, - ) -> Result { - // A1 - self.require_instance(instance)?; - refuse_provenance(&provenance)?; - // A2 - if envelopes.is_empty() || envelopes.len() > BATCH_MAX { - return Err(MindRefusal::BatchSize { actual: envelopes.len() as u32 }); - } - let mind = self.instance().clone(); - let mut docs = Docs::from_image(self.envelopes())?; - // A3, A5, then A4 on push. - for envelope in envelopes { - docs.push(stage(envelope, &mind)?)?; - } - // A6 - let carries_instance = docs.batch.iter().filter(|staged| staged.kind == PipelineKind::Instance).count() == 1; - if self.is_empty() && !carries_instance { - return Err(MindRefusal::MissingIdentity); - } - // A7 - let mut strong = BTreeSet::new(); - for staged in &docs.batch { - resolve(&docs, staged, &mind, &mut strong)?; - } - // Derived writes pass A3-A7 themselves. - let originals = docs.batch.len(); - for document in derive(&docs, &mind) { - document.validate()?; - let envelope = prepare(&document, self.cache())?; - docs.push(stage(envelope, &mind)?)?; - } - for staged in &docs.batch[originals..] { - resolve(&docs, staged, &mind, &mut strong)?; - } - // The pins: cited image bytes, and every write. - let strong_reads = strong - .iter() - .filter_map(|(type_id, key)| self.raw_envelope(type_id, key).cloned()) - .collect::>(); - let mut writes = docs.batch.iter().map(|staged| staged.envelope.clone()).collect::>(); - if carries_instance { - writes.push(HuginnMindEpoch::envelope(self.cache())?); - } - let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; - // A8 - for staged in &docs.batch { - check(&docs, staged, &mind)?; - } - // A9 - if let Some(existing) = receipt::replay(self, &candidate)? { - return Ok(PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: existing.receipt_id }); - } - // A10 - refuse_collisions(&docs)?; - // A11 - let landed = docs.batch.iter().map(|staged| PipelineRef { kind: staged.kind, id: Short(staged.key.clone()) }).collect(); - match receipt::commit(self, candidate, strong_reads, writes)? { - CommitOutcome::Committed(receipt) => Ok(PipelineAdmissionOutcome::Committed { - receipt_id: receipt.receipt_id, - committed_at: receipt.committed_at, - writes: landed, - }), - CommitOutcome::Conflict(identities) => Ok(PipelineAdmissionOutcome::Conflict { identities }), - } - } - - /// Replaces the mind's persona document whole, or refuses. One door for - /// the one document a mind holds, outside every pipeline batch: the same - /// instance and provenance checks as `admit`, then the persona's five - /// rules in order (schema-valid, `personaId` is this mind, canonical - /// provenance, `expected_updated_at` is the stored `updatedAt`, the new - /// `updatedAt` is later), then the same commit primitive. The stored - /// document is the commit's strong read, so the swap fails if it changed. - pub fn put_persona( - &mut self, - instance: &Slug, - state: Value, - expected_updated_at: Option, - provenance: PipelineProvenance, - now: DateTime, - ) -> PersonaPutOutcome { - match self.persona_steps(instance, state, expected_updated_at, provenance, now) { - Ok(receipt_id) => PersonaPutOutcome::Committed { receipt_id }, - Err(refusal) => PersonaPutOutcome::Refused(refusal), - } - } - - fn persona_steps( - &mut self, - instance: &Slug, - state: Value, - expected_updated_at: Option, - provenance: PipelineProvenance, - now: DateTime, - ) -> Result { - self.require_instance(instance)?; - refuse_provenance(&provenance)?; - // The persona rides a mind that has its identity (A6's analogue). - if self.is_empty() { - return Err(MindRefusal::MissingIdentity); - } - // P1 - persona::validate_shape(&state)?; - // P2 - let mind = self.instance().clone(); - if state.get("personaId").and_then(Value::as_str) != Some(mind.0.as_str()) { - return Err(MindRefusal::PersonaForeign { instance: mind.0 }); - } - // P3 - if state.pointer("/provenance/authority").and_then(Value::as_str) != Some("canonical") { - return Err(MindRefusal::PersonaNotCanonical); - } - // P4 - let stored = self.stored_updated_at()?; - if stored != expected_updated_at { - return Err(MindRefusal::PersonaStale { stored }); - } - // P5 - if let Some(stored) = &stored { - let instant = |stamp: &str| { - DateTime::parse_from_rfc3339(stamp).map_err(|_| MindRefusal::PersonaInvalid { - path: "/updatedAt".into(), - message: "is not an RFC 3339 date-time".into(), - }) - }; - let offered = state.get("updatedAt").and_then(Value::as_str).map(instant).transpose()?; - if offered <= Some(instant(stored)?) { - return Err(MindRefusal::PersonaInvalid { - path: "/updatedAt".into(), - message: "must be later than the stored updatedAt".into(), - }); - } - } - let (envelope, _) = self - .cache() - .prepare_entry_named(mind.0.clone(), &HuginnPersonaEntry { value: state }) - .map_err(unavailable)?; - let strong_reads = self.persona_envelope().cloned().into_iter().collect::>(); - let writes = vec![envelope]; - let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; - match receipt::commit(self, candidate, strong_reads, writes)? { - CommitOutcome::Committed(receipt) => Ok(receipt.receipt_id), - CommitOutcome::Conflict(_) => Err(MindRefusal::PersonaStale { stored: self.stored_updated_at()? }), - } - } -} - -/// The bound `eureka_pipeline::Short` declares, in UTF-8 bytes. The leaf -/// exposes no validator for it, so `short_bound_is_the_leafs_own` pins this to -/// the `maxLength` its schema publishes. -const SHORT_MAX: usize = 200; - -/// Who admitted a batch is the receipt's only record of it, so the mind checks -/// it here and every writer, whatever its transport, gets the check: the agent, -/// the session and the tool follow the leaf's `Title` rule (one alphanumeric -/// character, no control or line-break character, no bidi control) within -/// `Short`'s bound. The leaf keeps that rule private, so -/// `provenance_text_is_the_leafs_title_rule` pins this to it. Refused as the -/// leaf's own refusals, since the fields are the leaf's own type. -fn refuse_provenance(provenance: &PipelineProvenance) -> Result<(), MindRefusal> { - let bidi = |c: char| { - matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') - }; - let control = |c: char| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'); - for (field, value) in [ - ("provenance.agent", &provenance.agent), - ("provenance.session", &provenance.session), - ("provenance.tool", &provenance.tool), - ] { - let text = value.0.as_str(); - if !text.chars().any(char::is_alphanumeric) || text.contains(control) || text.contains(bidi) { - return Err(PipelineRefusal::InvalidFormat { field: field.into(), value: text.into() }.into()); - } - if text.len() > SHORT_MAX { - return Err(PipelineRefusal::FieldBound { field: field.into(), limit: SHORT_MAX as u32, actual: text.len() as u32 }.into()); - } - } - Ok(()) -} - -/// A3 through the leaf, then A5. -fn stage(envelope: CultCacheEnvelope, mind: &Slug) -> Result { - validate_pipeline_write_envelope(&envelope)?; - let document = PipelineDocument::decode(&envelope)?; - refuse_foreign_instance(&document, mind)?; - Ok(Staged { kind: document.kind(), key: envelope.key.clone(), document, envelope }) -} - -/// A5: every document carrying an instance field names this mind (a run -/// included); a hand-off names it on one side. -fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(), MindRefusal> { - let foreign = |declared: &Slug| MindRefusal::ForeignInstance { declared: declared.0.clone(), mind: mind.0.clone() }; - match document { - PipelineDocument::Instance(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => { - Err(foreign(&value.from_instance)) - } - _ => Ok(()), - } -} - -/// What a missing referent is called, by the field that cited it. -enum Missing { - Reference, - SpecOfReport(String), - Supersessor, -} - -struct Reference { - kind: PipelineKind, - id: String, - missing: Missing, -} - -/// A7's field list is `docs::citations`: every `PipelineRef` and every -/// full-id `Short` field, each carrying the refusal its absence earns. A -/// hand-off's documents are cited on the source side only, and each must read -/// as an id of a kind; the receiving side imports them afterwards (Cut 12). -fn references(staged: &Staged, mind: &Slug) -> Result, MindRefusal> { - if let PipelineDocument::HandOff(hand_off) = &staged.document { - if hand_off.from_instance != *mind { - return Ok(Vec::new()); - } - if let Some(document) = hand_off.documents.iter().find(|document| kind_of_id(&document.0).is_none()) { - return Err(PipelineRefusal::InvalidFormat { - field: "hand_off.documents".into(), - value: document.0.clone(), - } - .into()); - } - } - Ok(citations(&staged.document) - .into_iter() - .map(|(role, target)| Reference { - kind: target.kind, - id: target.id.0, - missing: match role { - CitationRole::CutSpec => Missing::SpecOfReport(staged.key.clone()), - CitationRole::SupersededBy => Missing::Supersessor, - _ => Missing::Reference, - }, - }) - .collect()) -} - -/// A7: every referent is in the batch or the image; the image ones are the -/// receipt's strong reads. -fn resolve(docs: &Docs, staged: &Staged, mind: &Slug, strong: &mut BTreeSet<(String, String)>) -> Result<(), MindRefusal> { - for reference in references(staged, mind)? { - if docs.in_batch(reference.kind, &reference.id).is_some() { - continue; - } - if docs.in_image(reference.kind, &reference.id).is_some() { - strong.insert((reference.kind.type_id().to_string(), reference.id)); - continue; - } - return Err(match reference.missing { - Missing::Reference => MindRefusal::MissingReference { kind: reference.kind, id: reference.id }, - Missing::SpecOfReport(report) => MindRefusal::CutReportWithoutSpec { report }, - Missing::Supersessor => MindRefusal::UnknownSupersessor { id: reference.id }, - }); - } - Ok(()) -} - -/// The writes admission derives from the batch: 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. One the batch already carries is not -/// derived again. -fn derive(docs: &Docs, mind: &Slug) -> Vec { - use PipelineDocument as D; - use PipelineKind as K; - let mut derived = Vec::new(); - for staged in &docs.batch { - match &staged.document { - D::Ruling(ruling) => { - if let Some(question) = &ruling.answers { - let subject = PipelineRef { kind: K::Question, id: question.clone() }; - let sequence = docs.derived_resolution_sequence(&subject, |outcome| { - matches!(outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == staged.key) - }); - derived.push(D::Resolution(PipelineResolution { - sequence, - subject, - outcome: ResolutionOutcome::Answered { by: PipelineRef { kind: K::Ruling, id: Short(staged.key.clone()) } }, - rationale: ruling.ruling.clone(), - resolved_on: ruling.ruled_on.clone(), - })); - } - } - D::HandOff(hand_off) => { - if hand_off.from_instance == *mind - && let Some((stewardship_key, _)) = docs.stewardship_of(mind, &hand_off.repo, Some(&staged.key)) - { - let subject = PipelineRef { kind: K::Stewardship, id: Short(stewardship_key.to_string()) }; - let sequence = docs.derived_resolution_sequence(&subject, |outcome| { - matches!(outcome, ResolutionOutcome::Withdrawn { reason } if reason.0 == staged.key) - }); - derived.push(D::Resolution(PipelineResolution { - sequence, - subject, - outcome: ResolutionOutcome::Withdrawn { reason: Line(staged.key.clone()) }, - rationale: hand_off.reason.clone(), - resolved_on: hand_off.handed_on.clone(), - })); - } - if hand_off.to_instance == *mind { - derived.push(D::Stewardship(PipelineStewardship { - instance: mind.clone(), - sequence: docs.derived_stewardship_sequence(mind, &hand_off.repo, &staged.key), - repo: hand_off.repo.clone(), - assigned_on: hand_off.handed_on.clone(), - note: Line(staged.key.clone()), - })); - } - } - _ => {} - } - } - derived.retain(|document| !docs.batch.iter().any(|staged| staged.document == *document)); - derived -} - -/// A8: the per-kind rules. -fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { - use PipelineDocument as D; - use PipelineKind as K; - let key = staged.key.as_str(); - match &staged.document { - D::Campaign(campaign) => { - if campaign.repos.is_empty() { - return Err(MindRefusal::EmptyRepos { campaign: key.into() }); - } - Ok(()) - } - D::Target(target) => { - let predecessor = D::Target(eureka_pipeline::PipelineTarget { revision: target.revision.wrapping_sub(1), ..target.clone() }); - revision_rule(docs, K::Target, key, target.revision, &predecessor)?; - // A revision past the first may not empty the repo set its predecessor - // named: empty means "keep the previous set", and that would silently - // narrow it to the campaign's opening set (ruling campaign-repo-set-home). - if target.repos.is_empty() && target.revision > 1 { - let named = matches!(docs.find(K::Target, &pipeline_key(&predecessor)?), Some(D::Target(previous)) if !previous.repos.is_empty()); - if named { - return Err(MindRefusal::EmptyRepos { campaign: key.into() }); - } - } - unique_labels("target.invariants", target.invariants.iter().map(|invariant| invariant.label.0.as_str())) - } - D::Question(question) => { - if question.options.len() < 2 || !question.options.iter().any(|option| option.label == question.recommended) { - return Err(MindRefusal::InvalidOptions { question: key.into() }); - } - unique_labels("question.options", question.options.iter().map(|option| option.label.0.as_str())) - } - D::Ruling(ruling) => { - if ruling.operator_quote.is_some() && ruling.authority != RulingAuthority::Operator { - return Err(MindRefusal::QuoteWithoutOperator { ruling: key.into() }); - } - let Some(question_id) = &ruling.answers else { return Ok(()) }; - let Some(D::Question(question)) = docs.find(K::Question, &question_id.0) else { - return Err(MindRefusal::MissingReference { kind: K::Question, id: question_id.0.clone() }); - }; - let own = |resolution: &PipelineResolution| { - matches!(&resolution.outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == key) - }; - if !docs.in_force_unless(K::Question, &question_id.0, own) { - return Err(MindRefusal::AlreadyResolved { subject: question_id.0.clone() }); - } - let choice = ruling.choice.as_ref().map_or("", |choice| choice.0.as_str()); - if !question.options.iter().any(|option| option.label.0 == choice) { - return Err(MindRefusal::InvalidChoice { ruling: key.into(), choice: choice.into() }); - } - Ok(()) - } - D::CutSpec(spec) => { - let campaign = docs.of_kind(K::Campaign).find_map(|(_, document)| match document { - D::Campaign(campaign) if campaign.slug == spec.campaign => Some(campaign), - _ => None, - }); - let Some(campaign) = campaign else { - return Err(MindRefusal::MissingReference { kind: K::Campaign, id: spec.campaign.0.clone() }); - }; - if !docs.repos_in_force(campaign).contains(&spec.repo) { - return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() }); - } - for ruling in &spec.rulings { - if !docs.in_force(K::Ruling, &ruling.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); - } - } - for dependency in &spec.depends_on { - // A cut naming itself is no ordering (ruling self-dep). - if dependency.0 == spec.cut.0 { - return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); - } - let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { - matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) - }); - if !names_a_cut { - return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); - } - } - let predecessor = D::CutSpec(eureka_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); - revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) - } - D::CutReport(report) => { - let Some(D::CutSpec(spec)) = docs.find(K::CutSpec, &report.cut_spec.0) else { - return Err(MindRefusal::CutReportWithoutSpec { report: key.into() }); - }; - if !docs.in_force(K::CutSpec, &report.cut_spec.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: report.cut_spec.0.clone() }); - } - if report.repo != spec.repo { - return Err(MindRefusal::SpecMismatch { field: "repo".into() }); - } - if report.branch != spec.branch { - return Err(MindRefusal::SpecMismatch { field: "branch".into() }); - } - if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { - return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); - } - Ok(()) - } - D::Verdict(verdict) => { - let Some(D::CutReport(report)) = docs.find(K::CutReport, &verdict.cut_report.0) else { - return Err(MindRefusal::MissingReference { kind: K::CutReport, id: verdict.cut_report.0.clone() }); - }; - for claim in &verdict.claims { - let confirmed = claim.findings.iter().filter_map(|finding| docs.find(K::Finding, &finding.0)).any(|document| { - matches!(document, D::Finding(finding) if finding.confidence == FindingConfidence::Confirmed) - }); - match claim.outcome { - ClaimOutcome::Falsified if !confirmed => { - return Err(MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: claim.claim.0.clone() }); - } - ClaimOutcome::Unproven if confirmed => { - return Err(MindRefusal::UnprovenClaimWithConfirmedFinding { claim: claim.claim.0.clone() }); - } - _ => {} - } - for label in &claim.mutations { - if !report.mutations.iter().any(|mutation| mutation.label == *label) { - return Err(MindRefusal::UnknownMutationLabel { label: label.0.clone() }); - } - } - } - for promise in &report.promises { - let count = verdict.claims.iter().filter(|claim| claim.promise.as_ref() == Some(&promise.label)).count(); - if count != 1 { - return Err(MindRefusal::PromiseWithoutVerdict { promise: promise.label.0.clone() }); - } - } - Ok(()) - } - D::Finding(finding) => { - if finding.evidence.is_empty() || finding.locations.is_empty() { - return Err(MindRefusal::FindingWithoutEvidence); - } - let target = docs.of_kind(K::Target).find_map(|(target_key, document)| match document { - D::Target(target) if target.campaign == finding.campaign && docs.in_force(K::Target, target_key) => Some(target), - _ => None, - }); - for label in &finding.invariants { - if !target.is_some_and(|target| target.invariants.iter().any(|invariant| invariant.label == *label)) { - return Err(MindRefusal::UnknownInvariant { label: label.0.clone() }); - } - } - Ok(()) - } - D::Resolution(resolution) => resolution_rule(docs, resolution), - D::Stewardship(stewardship) => { - let expected = docs.latest_stewardship(&stewardship.instance, &stewardship.repo, Some(stewardship)) + 1; - if stewardship.sequence != expected { - return Err(MindRefusal::StewardshipOutOfSequence { - repo: stewardship.repo.0.clone(), - expected, - actual: stewardship.sequence, - }); - } - if docs.stewardships_of(mind, &stewardship.repo, None).iter().any(|(other, _)| *other != key) { - return Err(MindRefusal::AlreadyStewarded { repo: stewardship.repo.0.clone() }); - } - Ok(()) - } - D::HandOff(hand_off) => { - if hand_off.from_instance == *mind && docs.stewardship_of(mind, &hand_off.repo, Some(key)).is_none() { - return Err(MindRefusal::NotStewarded { repo: hand_off.repo.0.clone() }); - } - Ok(()) - } - // run-is-the-grant: the claim is the one consumption fact. A claim must - // be in force, and no other run in force may hold it. A run already in - // the image under this key, byte for byte, is a replay and owes its - // claims no fresh freedom, so the replay still answers AlreadyAdmitted - // after the work it claimed has moved on. One live run of hers - // per instance and turn (ruling one-live-self-run) is decided by the same - // rule. - D::Run(run) => { - let replay = docs.in_image(K::Run, key) == Some(&staged.document); - run_rule(docs, key, run, replay) - } - D::FollowUp(_) | D::Instance(_) => Ok(()), - } -} - -/// The one rule for a run opening or reinstated by withdrawing its close: every -/// claim in force (a replay of an admitted run owes none) and held by no other -/// live run, then at most one live run of hers per instance and turn. -fn run_rule(docs: &Docs, run_key: &str, run: &PipelineRun, replay: bool) -> Result<(), MindRefusal> { - for claim in &run.claims { - if !replay && !docs.in_force(claim.kind, &claim.id.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: claim.kind, id: claim.id.0.clone() }); - } - if let Some(holder) = docs.claim_holder(run_key, claim) { - return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: RunId::held(holder) }); - } - } - if !replay && docs.in_force(PipelineKind::Run, run_key) && let Some(holder) = docs.live_holder(run_key, run) { - return Err(MindRefusal::AlreadyLive { run: RunId::held(holder) }); - } - Ok(()) -} - -/// Revision 1 stands alone; revision N needs a resolution in the batch that -/// supersedes revision N-1 by this document. The predecessor's key is the -/// leaf's, from the same document one revision back. -fn revision_rule(docs: &Docs, kind: PipelineKind, key: &str, revision: u32, predecessor: &PipelineDocument) -> Result<(), MindRefusal> { - if revision == 1 { - return Ok(()); - } - let refusal = || MindRefusal::RevisionWithoutSupersession { kind, revision }; - if revision == 0 { - return Err(refusal()); - } - let predecessor_key = pipeline_key(predecessor)?; - let superseded = docs.batch.iter().any(|staged| { - matches!(&staged.document, PipelineDocument::Resolution(resolution) - if resolution.subject.kind == kind - && resolution.subject.id.0 == predecessor_key - && matches!(&resolution.outcome, ResolutionOutcome::Superseded { by } - if by.iter().any(|supersessor| supersessor.kind == kind && supersessor.id.0 == key))) - }); - if superseded { Ok(()) } else { Err(refusal()) } -} - -fn unique_labels<'a>(field: &str, labels: impl Iterator) -> Result<(), MindRefusal> { - let mut seen = BTreeSet::new(); - for label in labels { - if !seen.insert(label) { - return Err(MindRefusal::DuplicateLabel { field: field.into(), label: label.into() }); - } - } - Ok(()) -} - -fn outcome_name(outcome: &ResolutionOutcome) -> &'static str { - match outcome { - ResolutionOutcome::Superseded { .. } => "Superseded", - ResolutionOutcome::Answered { .. } => "Answered", - ResolutionOutcome::Fixed { .. } => "Fixed", - ResolutionOutcome::Deferred { .. } => "Deferred", - ResolutionOutcome::Recorded { .. } => "Recorded", - ResolutionOutcome::Withdrawn { .. } => "Withdrawn", - } -} - -/// The resolution matrix: which outcomes a subject kind admits, and of which -/// kinds its referents must be. Admission's, never the leaf's. -/// -/// A resolution is itself resolvable, by withdrawal alone: the operator's -/// ruling on Q17 keeps a withdrawn resolution attached to its subject rather -/// than erasing it, and a subject whose resolution is withdrawn may be -/// resolved again at the next sequence, which the leaf's key now carries. The -/// chain stops there: Q19 A caps it at depth two, in `resolution_rule`, since -/// a withdrawal that could itself be withdrawn would leave two closures -/// standing over one subject. -fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { - use PipelineKind as K; - use ResolutionOutcome as O; - let all = |by: &[PipelineRef], kind: K| by.iter().all(|supersessor| supersessor.kind == kind); - match (subject, outcome) { - (K::Target, O::Superseded { by }) => all(by, K::Target), - (K::Question, O::Answered { by }) => by.kind == K::Ruling, - (K::Question, O::Withdrawn { .. }) => true, - (K::Ruling, O::Superseded { by }) => all(by, K::Ruling), - (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), - (K::CutSpec, O::Withdrawn { .. }) => true, - (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), - (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, - (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), - (K::FollowUp, O::Withdrawn { .. }) => true, - (K::Stewardship, O::Superseded { by }) => all(by, K::Stewardship), - (K::Stewardship, O::Withdrawn { .. }) => true, - (K::Run, O::Recorded { .. } | O::Withdrawn { .. }) => true, - (K::Resolution, O::Withdrawn { .. }) => true, - _ => false, - } -} - -/// The resolution row: the sequence, the in-force subject, the matrix, the -/// Q19 cap and the reinstatement rule it rests on, a non-empty supersession, -/// every referent in force, and the two -/// coherence rules (an `Answered` ruling answers this question; a cut spec is -/// superseded within its cut). -fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), MindRefusal> { - use PipelineDocument as D; - use PipelineKind as K; - let subject_kind = resolution.subject.kind; - let subject_id = resolution.subject.id.0.as_str(); - if let ResolutionOutcome::Superseded { by } = &resolution.outcome && by.is_empty() { - return Err(MindRefusal::EmptySupersession); - } - // The sequence is the writer's and is checked against the image, as a - // revision is: the previous plus one, counting every record of this - // subject other than this document itself. - let expected = docs.latest_resolution(&resolution.subject, Some(resolution)) + 1; - if resolution.sequence != expected { - return Err(MindRefusal::ResolutionOutOfSequence { - subject: subject_id.into(), - expected, - actual: resolution.sequence, - }); - } - // A8: a subject with a resolution still in force is closed. A withdrawn - // one is not in force, so the subject is open and this is its next record. - if !docs.in_force_unless(subject_kind, subject_id, |other| other == resolution) { - return Err(MindRefusal::AlreadyResolved { subject: subject_id.into() }); - } - let incompatible = || MindRefusal::IncompatibleResolution { subject_kind, outcome: outcome_name(&resolution.outcome).into() }; - if !matrix(subject_kind, &resolution.outcome) { - return Err(incompatible()); - } - // Q19 A: the chain stops at depth two. A withdrawal cannot be withdrawn; - // to reinstate a closure, resolve the subject again at the next sequence. - if subject_kind == K::Resolution - && let Some(D::Resolution(subject)) = docs.find(K::Resolution, subject_id) - && subject.subject.kind == K::Resolution - { - return Err(incompatible()); - } - // Q19 A's rationale one step out from the cap: a withdrawal never - // re-raises an earlier record over a later one. Withdrawing a resolution - // puts its subject back in force, so refuse while a later record of that - // subject's own scope stands -- a repo's next assignment, a document's - // next revision. - if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) - && subject_kind == K::Resolution - && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) - && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) - && let Some(later) = docs.later_in_force(base) - { - return Err(MindRefusal::WouldReinstateOverLater { - subject: reinstating.subject.id.0.clone(), - later: later.into(), - }); - } - // Withdrawing a run's closure puts the run back in force, and a run in - // force holds its claims and its turn's slot: refuse while another run in force - // holds one of the claims or another run of hers of its turn is live. - if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) - && subject_kind == K::Resolution - && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) - && reinstating.subject.kind == K::Run - && let Some(D::Run(run)) = docs.find(K::Run, &reinstating.subject.id.0) - { - run_rule(docs, &reinstating.subject.id.0, run, false)?; - } - for (_, referent) in outcome_citations(&resolution.outcome) { - if !docs.in_force(referent.kind, &referent.id.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 }); - } - } - match &resolution.outcome { - ResolutionOutcome::Superseded { by } => { - let Some(subject) = docs.find(subject_kind, subject_id) else { return Ok(()) }; - for supersessor in by { - if let Some(next) = docs.find(subject_kind, &supersessor.id.0) - && !same_lineage(subject, next) - { - return Err(incompatible()); - } - } - } - ResolutionOutcome::Answered { by } => { - let Some(D::Ruling(ruling)) = docs.find(K::Ruling, &by.id.0) else { return Ok(()) }; - if ruling.answers.as_ref().map(|answers| answers.0.as_str()) != Some(subject_id) { - return Err(incompatible()); - } - } - _ => {} - } - Ok(()) -} - -/// The one supersession lineage rule (ruling `supersession-lineage`). A target is -/// superseded only by a target of its campaign, a cut_spec only by a cut_spec of its -/// campaign and cut, and a ruling by any ruling: operator directions cut across campaigns. -fn same_lineage(subject: &PipelineDocument, supersessor: &PipelineDocument) -> bool { - use PipelineDocument as D; - match (subject, supersessor) { - (D::Target(old), D::Target(new)) => old.campaign == new.campaign, - (D::CutSpec(old), D::CutSpec(new)) => old.campaign == new.campaign && old.cut == new.cut, - _ => true, - } -} - -/// A10: a write whose identity the image already holds. Every kind answers -/// the same way now that a resolution key carries a sequence: the identity is -/// taken. Whether a subject is closed is A8's question, not this one's. -fn refuse_collisions(docs: &Docs) -> Result<(), MindRefusal> { - for staged in &docs.batch { - if docs.in_image(staged.kind, &staged.key).is_some() { - return Err(MindRefusal::IdentityCollision { kind: staged.kind, id: staged.key.clone() }); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::fixtures::*; - use crate::fixtures::prepare; - use crate::mind::schema_cache; - use crate::receipt::{DocumentVersion, Faculty, HuginnCommitReceipt}; - use crate::store::test_stores::{MemoryStore, RefusingStore, SwapCommand}; - use cultcache_rs::DatabaseEntry; - use eureka_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn}; - use sha2::{Digest, Sha256}; - - fn receipt_count(mind: &Mind) -> usize { - mind.receipts().unwrap().len() - } - - #[test] - fn an_empty_store_opens_and_the_first_write_must_carry_the_instance() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - assert!(mind.is_empty()); - assert_eq!(refusal(admit(&mut mind, vec![stewardship(INSTANCE, REPO), campaign(&[REPO])])), MindRefusal::MissingIdentity); - assert!(store.rows().is_empty()); - let (receipt_id, writes) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - assert!(writes.contains(&r(K::Instance, &format!("{INSTANCE}:instance:self")))); - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts.len(), 1); - assert_eq!(receipts[0].receipt_id, receipt_id); - let identities = receipts[0].writes.iter().map(DocumentVersion::identity).collect::>(); - assert!(identities.contains(&(K::Instance.type_id(), format!("{INSTANCE}:instance:self").as_str()))); - assert!(identities.contains(&(HuginnMindEpoch::TYPE, PIPELINE_SCHEMA_EPOCH)), "the epoch record is derived: {identities:?}"); - assert!(receipts[0].strong_reads.is_empty()); - // The store now opens again as this instance, and as nothing else. - assert!(!Mind::open_with(store.clone(), &slug(INSTANCE)).unwrap().is_empty()); - assert!(Mind::open_with(store, &slug(OTHER_INSTANCE)).is_err()); - } - - /// The identity the index labels its collection with: none before the - /// first write, then the first receipt's id and never a later one. - #[test] - fn the_genesis_receipt_id_is_the_first_admissions_and_stays_put() { - let mut mind = opened(MemoryStore::new(), INSTANCE); - assert_eq!(mind.genesis_receipt_id().unwrap(), None); - let (first, _) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first.clone())); - let (second, _) = committed(admit(&mut mind, vec![campaign(&[REPO])])); - assert_ne!(first, second); - assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first)); - } - - fn admit_attributed(agent: &str, session: &str) -> (Mind, PipelineAdmissionOutcome) { - admit_signed(agent, session, "admit") - } - - fn admit_signed(agent: &str, session: &str, tool: &str) -> (Mind, PipelineAdmissionOutcome) { - let mut mind = opened(MemoryStore::new(), INSTANCE); - let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(agent), session: s(session), tool: s(tool) }; - let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance, documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO)] }; - let outcome = mind.admit(batch, now()); - (mind, outcome) - } - - /// Who admitted a batch is validated by the mind, so no writer can commit - /// a blank, multi-line or oversized attribution, and a refused batch leaves - /// nothing behind. - #[test] - fn provenance_names_an_agent_a_session_and_a_tool_on_one_clean_line_within_short() { - let over = "x".repeat(SHORT_MAX + 1); - let exact = "y".repeat(SHORT_MAX); - for bad in [ - "", - " \t ", - "\u{2003}", - "\u{200B}", - "line\nbreak", - "line\rbreak", - "a\u{0B}b", - "a\u{0C}b", - "a\u{85}b", - "a\u{2028}b", - "a\u{2029}b", - "a\0b", - "a\u{1B}[31mb", - "a\u{202E}b", - "a\u{2066}b", - "a\u{200F}b", - ] { - for field in ["provenance.agent", "provenance.session", "provenance.tool"] { - let (mind, outcome) = match field { - "provenance.agent" => admit_signed(bad, "session-1", "admit"), - "provenance.session" => admit_signed("claude", bad, "admit"), - _ => admit_signed("claude", "session-1", bad), - }; - let refused = refusal(outcome); - assert!( - matches!(&refused, MindRefusal::Document(PipelineRefusal::InvalidFormat { field: named, .. }) if named == field), - "{field}={bad:?}: {refused:?}" - ); - assert!(mind.is_empty(), "{field}={bad:?}: a refused batch wrote"); - } - } - for (field, outcome) in [ - ("provenance.agent", admit_signed(&over, "session-1", "admit")), - ("provenance.session", admit_signed("claude", &over, "admit")), - ("provenance.tool", admit_signed("claude", "session-1", &over)), - ] { - let (mind, outcome) = outcome; - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::FieldBound { field: field.into(), limit: 200, actual: 201 }) - ); - assert!(mind.is_empty()); - } - // The bound is in bytes, not characters: 101 two-byte characters are 202 bytes. - let (_, outcome) = admit_attributed(&"é".repeat(101), "session-1"); - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::FieldBound { field: "provenance.agent".into(), limit: 200, actual: 202 }) - ); - let (_, outcome) = admit_signed("claude", "session-1", &"é".repeat(101)); - assert!(matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::FieldBound { actual: 202, .. }))); - let (_, outcome) = admit_attributed(&"é".repeat(100), "session-1"); - committed(outcome); - // The bound is inclusive and a name with inner spaces is a name. - let (mind, outcome) = admit_signed(&exact, "session 7", "admit tool"); - committed(outcome); - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts[0].provenance.agent, s(&exact)); - assert_eq!(receipts[0].provenance.session, s("session 7")); - assert_eq!(receipts[0].provenance.tool, s("admit tool")); - } - - /// The read bounds are the leaf's: admission applies the leaf's validation - /// and adds none. Sixteen anchors of a hundred lines admit and read back; - /// a seventeenth refuses at `cut_spec.reads` and nothing is written. - #[test] - fn reads_admit_at_the_bound_and_refuse_over_it() { - let anchors = |count: usize| { - (0..count) - .map(|n| eureka_pipeline::ReadAnchor { - location: eureka_pipeline::CodeLocation { - path: s("crates/eureka-pipeline/src/lib.rs"), - line: 1, - end_line: Some(100), - symbol: Some(format!("symbol_{n}").as_str().into()), - }, - why: s("read"), - }) - .collect::>() - }; - let mut mind = seeded(); - let mut spec = cut_spec("1", 1); - spec.reads = anchors(16); - committed(admit(&mut mind, vec![D::CutSpec(spec.clone())])); - let held = mind.view(&r(K::CutSpec, &id("cut_spec", "cut-1.r1"))).unwrap().expect("the spec is held"); - assert_eq!(held.document, D::CutSpec(spec)); - - let mut over = cut_spec("2", 1); - over.reads = anchors(17); - let receipts = mind.receipts().unwrap().len(); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(over)])), - MindRefusal::Document(PipelineRefusal::FieldBound { field: "cut_spec.reads".into(), limit: 16, actual: 17 }) - ); - assert_eq!(mind.receipts().unwrap().len(), receipts); - assert_eq!(mind.view(&r(K::CutSpec, &id("cut_spec", "cut-2.r1"))).unwrap(), None, "nothing is written"); - } - - /// The leaf's `Title` rule is private, so this holds the provenance rule to - /// it: the two accept and refuse the same text. - #[test] - fn provenance_text_is_the_leafs_title_rule() { - let leaf_accepts = |text: &str| { - let D::Campaign(mut campaign) = campaign(&[REPO]) else { unreachable!() }; - campaign.title = eureka_pipeline::Title(text.into()); - D::Campaign(campaign).validate().is_ok() - }; - let mine_accepts = |text: &str| { - let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(text), session: s("s"), tool: s("t") }; - refuse_provenance(&provenance).is_ok() - }; - for text in [ - "claude", "session 7", "", " ", "\t", "\u{200B}", "\u{2003}", "!!!", "a\nb", "a\u{0B}b", "a\u{85}b", "a\u{2028}b", - "a\0b", "a\u{7F}b", "a\u{202E}b", "a\u{061C}b", "a\u{2069}b", "a\u{200D}b", "é", "日本", "a\u{FEFF}b", - &"x".repeat(200), &"x".repeat(201), &"é".repeat(100), &"é".repeat(101), - ] { - assert_eq!(mine_accepts(text), leaf_accepts(text), "{text:?}"); - } - } - - #[test] - fn short_bound_is_the_leafs_own() { - let schema = serde_json::to_value(schemars::schema_for!(Short)).unwrap(); - assert_eq!(schema["maxLength"], serde_json::json!(SHORT_MAX)); - } - - #[test] - fn admission_refuses_a_foreign_instance_whatever_the_transport() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let foreign = MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }; - let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; - assert_eq!( - refusal(mind.admit_prepared(&slug(OTHER_INSTANCE), provenance(Faculty::Hands), envelopes, now())), - foreign - ); - assert_eq!(refusal(admit(&mut mind, vec![stewardship(OTHER_INSTANCE, OTHER_REPO)])), foreign); - assert_eq!( - refusal(admit(&mut mind, vec![hand_off("a", "b", REPO, &[])])), - MindRefusal::ForeignInstance { declared: "a".into(), mind: INSTANCE.into() } - ); - assert_eq!(refusal(admit(&mut mind, vec![instance(OTHER_INSTANCE)])), foreign); - assert_eq!(store.rows(), before); - } - - #[test] - fn two_minds_in_one_state_root_stay_separate() { - let root = tempfile::tempdir().unwrap(); - let mut yggdrasil = Mind::open(root.path(), &slug(INSTANCE)).unwrap(); - let mut thought_cage = Mind::open(root.path(), &slug(OTHER_INSTANCE)).unwrap(); - assert_ne!( - Mind::store_path_for(root.path(), &slug(INSTANCE)).unwrap(), - Mind::store_path_for(root.path(), &slug(OTHER_INSTANCE)).unwrap() - ); - committed(admit(&mut yggdrasil, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - committed(admit(&mut thought_cage, vec![instance(OTHER_INSTANCE)])); - let key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert!(yggdrasil.envelope(K::Stewardship, &key).is_some()); - assert!(thought_cage.envelope(K::Stewardship, &key).is_none()); - assert_eq!(yggdrasil.envelopes().len(), 4); - assert_eq!(thought_cage.envelopes().len(), 3); - } - - #[test] - fn keys_are_recomputed_and_a_forged_key_refuses() { - let mut mind = seeded(); - let mut envelope = prepare(&question("Q1", &["A", "B"], "A")); - let expected = envelope.key.clone(); - envelope.key = format!("{expected}-forged"); - let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), vec![envelope.clone()], now()); - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::InvalidIdentity { kind: K::Question, key: envelope.key, expected }) - ); - } - - #[test] - fn references_must_exist_in_image_or_batch() { - let mut mind = seeded(); - let absent_question = id("question", "Q9"); - let mut answering = ruling("R1"); - answering.answers = Some(s(&absent_question)); - answering.choice = Some(l("A")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering)])), - MindRefusal::MissingReference { kind: K::Question, id: absent_question } - ); - let absent_finding = id("finding", "cut-1.s1.F9"); - assert_eq!( - refusal(admit(&mut mind, vec![follow_up("FU-1", r(K::Finding, &absent_finding))])), - MindRefusal::MissingReference { kind: K::Finding, id: absent_finding } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])])])), - MindRefusal::MissingReference { kind: K::CutReport, id: id("cut_report", "cut-1.h1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])), - MindRefusal::MissingReference { kind: K::Question, id: id("question", "Q1") } - ); - // A well-formed id of another kind is a missing reference of the - // cited kind, not a wrong-kind refusal. - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("question", "Q1"))]; - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(spec)])), - MindRefusal::MissingReference { kind: K::Ruling, id: id("question", "Q1") } - ); - } - - #[test] - fn batch_is_all_or_nothing() { - let store = RefusingStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let receipts = receipt_count(&mind); - let mut quoted = ruling("R1"); - quoted.operator_quote = Some("go ahead".into()); - let batch = vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "B"), D::Ruling(quoted)]; - assert_eq!(refusal(admit(&mut mind, batch)), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - - store.command(SwapCommand::Lose); - let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); - assert!(matches!(outcome, PipelineAdmissionOutcome::Conflict { .. }), "{outcome:?}"); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - - store.command(SwapCommand::Fail); - assert!(matches!(refusal(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])), MindRefusal::Unavailable { .. })); - assert_eq!(store.rows(), before); - - store.command(SwapCommand::Delegate); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - assert_eq!(receipt_count(&mind), receipts + 1); - } - - #[test] - fn exact_replay_returns_already_admitted_across_provenance() { - let mut mind = seeded(); - let receipts = receipt_count(&mind); - let (first, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let batch = PipelineAdmissionBatch { - instance: slug(INSTANCE), - provenance: provenance(Faculty::Soul), - documents: vec![question("Q1", &["A", "B"], "A")], - }; - let later = now() + chrono::Duration::hours(1); - assert_eq!(mind.admit(batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first.clone() }); - assert_eq!(receipt_count(&mind), receipts + 1); - // The first write, epoch record and all, replays too. - let seed_batch = PipelineAdmissionBatch { - instance: slug(INSTANCE), - provenance: provenance(Faculty::Imagination), - documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO]), target(1, &[INVARIANT])], - }; - assert!(matches!(mind.admit(seed_batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { .. })); - assert_eq!(receipt_count(&mind), receipts + 1); - } - - /// RS-1: the ordinal is `head + 1` at admission, not a rank derived from - /// the clock or the receipt id. Three batches land at one `now`, so a - /// mutant that ranked by `(committed_at, receipt_id)` would fall back to - /// id order among them; the ids are checked not to already sort in - /// admission order, so that fallback provably disagrees with the truth. - #[test] - fn ordinals_are_admission_order_not_clock_or_id_order() { - let mut mind = seeded(); - assert_eq!(receipt::head(&mind).unwrap(), 1, "the seed batch is ordinal 1"); - let same_time = now(); - let (id1, _) = committed(admit_at(&mut mind, vec![question_n(1)], same_time)); - let (id2, _) = committed(admit_at(&mut mind, vec![question_n(2)], same_time)); - let (id3, _) = committed(admit_at(&mut mind, vec![question_n(3)], same_time)); - - let mut by_id = [id1.clone(), id2.clone(), id3.clone()]; - by_id.sort(); - assert_ne!( - by_id, - [id1.clone(), id2.clone(), id3.clone()], - "the fixture needs receipt ids that do not already sort in admission order" - ); - - let receipts = mind.receipts().unwrap(); - let ordinal_of = |id: &str| receipts.iter().find(|receipt| receipt.receipt_id == id).unwrap().ordinal; - assert_eq!(ordinal_of(&id1), 2); - assert_eq!(ordinal_of(&id2), 3); - assert_eq!(ordinal_of(&id3), 4); - assert_eq!(receipt::head(&mind).unwrap(), 4); - } - - /// RS-1, A9: the ordinal is not digested, so an exact replay finds the - /// receipt it first landed with and keeps that receipt's ordinal, even - /// when the head has moved since and a fresh candidate would be assigned - /// a different one. - #[test] - fn an_exact_replay_keeps_its_ordinal() { - let mut mind = seeded(); - let (first_id, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let first_ordinal = - mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == first_id).unwrap().ordinal; - - // Move the head, so a replay's own candidate would carry a different - // ordinal than the one already on the stored receipt. - committed(admit(&mut mind, vec![question("Q2", &["A", "B"], "A")])); - - let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); - assert_eq!(outcome, PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first_id.clone() }); - - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts.iter().filter(|receipt| receipt.receipt_id == first_id).count(), 1, "no second receipt lands"); - let stored = receipts.iter().find(|receipt| receipt.receipt_id == first_id).unwrap(); - assert_eq!(stored.ordinal, first_ordinal, "the replay does not overwrite the ordinal"); - } - - /// A batch that derives a write replays like any other: the second - /// admission re-derives the record the first one landed, sequence and - /// all, so A9 recognises the digest and answers with the first receipt - /// instead of the derived write colliding with itself. - #[test] - fn an_exact_replay_re_derives_the_rulings_resolution() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering.clone())])); - assert!(writes.contains(&r(K::Resolution, &id("resolution", "question.Q1.n1")))); - let receipts = receipt_count(&mind); - assert_eq!( - admit(&mut mind, vec![D::Ruling(answering)]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id } - ); - assert_eq!(receipt_count(&mind), receipts, "a replay writes no second receipt"); - } - - /// The same on both sides of a hand-off: the source re-derives the - /// withdrawal it landed, the receiving mind the assignment. - #[test] - fn an_exact_replay_re_derives_both_sides_of_a_hand_off() { - let away = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]); - let mut source = seeded(); - let (receipt_id, _) = committed(admit(&mut source, vec![away.clone()])); - let receipts = receipt_count(&source); - assert_eq!(admit(&mut source, vec![away.clone()]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); - assert_eq!(receipt_count(&source), receipts); - - let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); - let (receipt_id, _) = committed(admit(&mut receiving, vec![away.clone()])); - let receipts = receipt_count(&receiving); - assert_eq!(admit(&mut receiving, vec![away]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); - assert_eq!(receipt_count(&receiving), receipts); - } - - /// The sequence a ruling's answer takes is the one that ruling's own - /// answer carries, not whatever answer the question already has. A - /// question reopened by withdrawing its first answer is answered again at - /// the next sequence, so the first answer stays readable under it. - #[test] - fn a_ruling_answering_a_reopened_question_takes_the_next_sequence() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut first = ruling("R1"); - first.answers = Some(s(&id("question", "Q1"))); - first.choice = Some(l("A")); - committed(admit(&mut mind, vec![D::Ruling(first)])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); - let mut second = ruling("R2"); - second.answers = Some(s(&id("question", "Q1"))); - second.choice = Some(l("B")); - let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(second)])); - assert_eq!(writes, vec![ - r(K::Ruling, &id("ruling", "R2")), - r(K::Resolution, &id("resolution", "question.Q1.n2")), - ]); - assert!(mind.envelope(K::Resolution, &id("resolution", "question.Q1.n1")).is_some()); - } - - #[test] - fn a_refused_batch_is_not_answered_from_a_stored_receipt() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let envelopes = vec![prepare(&D::CutSpec(spec))]; - committed(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes.clone(), now())); - committed(admit(&mut mind, vec![ - D::Ruling(ruling("R2")), - resolution(r(K::Ruling, &id("ruling", "R1")), superseded(&[r(K::Ruling, &id("ruling", "R2"))])), - ])); - assert_eq!( - refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes, now())), - MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R1") } - ); - } - - #[test] - fn a_receipt_names_the_exact_bytes_it_read_and_wrote() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); - let cited = mind.envelope(K::Ruling, &id("ruling", "R1")).unwrap().clone(); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited)]); - let written = mind.envelope(K::CutSpec, &id("cut_spec", "cut-1.r1")).unwrap(); - assert_eq!(receipt.writes, vec![DocumentVersion::from_envelope(written)]); - for version in receipt.strong_reads.iter().chain(&receipt.writes) { - assert_eq!(version.payload_sha256, format!("{:x}", Sha256::digest(&version.payload_msgpack))); - } - let digest = rmp_serde::to_vec_named(&(&receipt.instance, &receipt.strong_reads, &receipt.writes)).unwrap(); - assert_eq!(receipt_id, format!("mind-commit-{:x}", Sha256::digest(&digest))); - - // The same content admitted by another faculty in another mind has - // the same id: provenance is not digested. - let mut twin = opened(MemoryStore::new(), INSTANCE); - seed(&mut twin); - committed(admit(&mut twin, vec![D::Ruling(ruling("R1"))])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance: provenance(Faculty::Soul), documents: vec![D::CutSpec(spec)] }; - let (twin_id, _) = committed(twin.admit(batch, now() + chrono::Duration::days(1))); - assert_eq!(twin_id, receipt_id); - let twin_receipt = twin.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == twin_id).unwrap(); - assert_ne!(twin_receipt.provenance, receipt.provenance); - } - - #[test] - fn a_document_is_written_once_and_superseded_by_resolution() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R8"))])); - let mut rewritten = ruling("R8"); - rewritten.ruling = "A repo owns its mind.".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(rewritten)])), - MindRefusal::IdentityCollision { kind: K::Ruling, id: id("ruling", "R8") } - ); - committed(admit(&mut mind, vec![ - D::Ruling(ruling("R9")), - resolution(r(K::Ruling, &id("ruling", "R8")), superseded(&[r(K::Ruling, &id("ruling", "R9"))])), - ])); - assert_eq!(mind.get(K::Ruling, &id("ruling", "R8")).unwrap(), Some(D::Ruling(ruling("R8")))); - } - - /// A8: one resolution of a subject is in force at a time, and the next - /// record of that subject is the next sequence. The two rules are - /// separate refusals: a second closure at the right sequence is refused - /// because the first still stands, and one at a taken sequence is refused - /// before the question of standing arises. - #[test] - fn a_subject_with_a_resolution_in_force_refuses_another() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject.clone(), 2, ResolutionOutcome::Withdrawn { reason: "again".into() })])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(subject.clone(), ResolutionOutcome::Withdrawn { reason: "again".into() })])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 2, actual: 1 } - ); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering)])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - } - - /// Q17 B and Q19 A together: withdrawing a resolution reopens its subject - /// without erasing the record. The withdrawn closure is still readable by - /// key, the subject takes its next resolution at the next sequence, and - /// the chain stops at depth two -- a withdrawal cannot be withdrawn. - #[test] - fn a_withdrawn_resolution_reopens_its_subject_and_stays_readable() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - let first = id("resolution", "question.Q1.n1"); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - - // The withdrawal of that closure: one nesting out, under the - // resolution it withdraws. - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); - let withdrawal = id("resolution", "resolution.question.Q1.n1.n1"); - assert!(mind.get(K::Resolution, &withdrawal).unwrap().is_some()); - - // The subject is open again, and its next record is `n2`. The first - // one is still there, outcome and all: this is a log, not an erasure. - committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); - let Some(D::Resolution(kept)) = mind.get(K::Resolution, &first).unwrap() else { panic!() }; - assert_eq!(kept.outcome, withdrawn()); - assert_eq!(kept.sequence, 1); - - // `n2` stands, so a third is refused, and the withdrawal cannot - // itself be withdrawn to re-raise `n1` over it (Q19 A). - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject, 3, withdrawn())])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), - MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } - ); - - // And a ruling may answer a question whose first closure was - // withdrawn: the derived resolution takes the next sequence. - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!(writes, vec![ - r(K::Ruling, &id("ruling", "R1")), - r(K::Resolution, &id("resolution", "question.Q1.n2")), - ]); - } - - /// The sequence is exactly the previous plus one: a gap refuses as loudly - /// as a taken sequence, on both sequenced kinds, and before the question - /// of what still stands arises. - #[test] - fn a_sequence_gap_refuses_on_both_sequenced_kinds() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); - committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject, 4, withdrawn())])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 4 } - ); - - let mut steward = seeded(); - committed(admit(&mut steward, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - committed(admit(&mut steward, vec![D::HandOff(back)])); - assert_eq!( - refusal(admit(&mut steward, vec![stewardship_n(INSTANCE, REPO, 4)])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 4 } - ); - } - - /// The sequence counts the batch as well as the image: two records of one - /// subject landing together are the second and the third, never two - /// firsts, and the first of them is refused for the sequence rather than - /// for what the other closes. - #[test] - fn the_latest_resolution_counts_the_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - assert_eq!( - refusal(admit(&mut mind, vec![ - resolution_n(subject.clone(), 1, withdrawn()), - resolution_n(subject, 2, ResolutionOutcome::Withdrawn { reason: "again".into() }), - ])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 1 } - ); - } - - /// The Q19 cap reads the batch too: a chain three deep landing in one - /// batch is capped exactly as one landing over three admissions. - #[test] - fn the_cap_reads_a_chain_landing_in_one_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![ - resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn()), - resolution(r(K::Resolution, &id("resolution", "resolution.question.Q1.n1.n1")), withdrawn()), - ])), - MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } - ); - } - - /// Q18 A and Q20 A at admission: a repo is stewarded by one record at a - /// time on a mind, and a repo handed away and handed back is a second - /// record rather than a collision or an overwrite. - #[test] - fn a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer() { - let mut mind = seeded(); - let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 2)])), - MindRefusal::AlreadyStewarded { repo: REPO.into() } - ); - let D::Stewardship(mut retaken) = stewardship(INSTANCE, REPO) else { panic!() }; - retaken.note = "assigned again".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Stewardship(retaken)])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 2, actual: 1 } - ); - - // Handing the repo away withdraws the assignment, and the withdrawal - // is a resolution of that record, sequence and all. The stewardship it - // reads to derive that is an image document, so it is pinned. - let (receipt_id, writes) = committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let away = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.{}", date().0); - assert_eq!(writes, vec![ - r(K::HandOff, &away), - r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1")), - ]); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - let stewardship_envelope = mind.envelope(K::Stewardship, &first).unwrap().clone(); - assert!( - receipt.strong_reads.contains(&DocumentVersion::from_envelope(&stewardship_envelope)), - "the derived withdrawal's own citation is pinned: {:?}", - receipt.strong_reads - ); - - // Nothing stewards the repo now, and a campaign over it still admits: - // campaign admission reads no stewardship (ruling stewardship-rule). - let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; - second.slug = slug("second"); - committed(admit(&mut mind, vec![D::Campaign(second)])); - - // Handed back, the mind stewards it again, as `n2`. No field names a - // return: this is the same hand-off shape with the instances swapped. - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(back)])); - let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); - assert_eq!(writes, vec![ - r(K::HandOff, &format!("{OTHER_INSTANCE}:hand_off:{INSTANCE}.gamecult_-epiphany.2026-09-17")), - r(K::Stewardship, &again), - ]); - let Some(D::Stewardship(taken)) = mind.get(K::Stewardship, &again).unwrap() else { panic!() }; - assert_eq!(taken.sequence, 2); - assert_eq!(taken.assigned_on, eureka_pipeline::Date("2026-09-17".into())); - assert!(mind.envelope(K::Stewardship, &first).is_some(), "the first assignment is still readable by key"); - } - - /// Q19 A's rationale, which the cap alone does not carry: a withdrawal - /// never re-raises an earlier record over a later one. The repo went away - /// and came back, so withdrawing the hand-off's withdrawal would put the - /// first assignment back in force beside the second. - #[test] - fn a_withdrawal_does_not_reinstate_a_stewardship_over_a_later_one() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - committed(admit(&mut mind, vec![D::HandOff(back)])); - let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), - MindRefusal::WouldReinstateOverLater { subject: first, later: again.clone() } - ); - let docs = Docs::from_image(mind.envelopes()).unwrap(); - let in_force = docs.stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| *key).collect::>(); - assert_eq!(in_force, vec![again.as_str()], "one assignment stands, and it is the later one"); - } - - /// The same rule one kind over: withdrawing a supersession while the - /// revision that superseded stands would leave two revisions in force. - #[test] - fn a_withdrawal_does_not_reinstate_a_revision_over_a_later_one() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - target(2, &[INVARIANT]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "target.r1.n1")), withdrawn())])), - MindRefusal::WouldReinstateOverLater { subject: id("target", "r1"), later: id("target", "r2") } - ); - assert!(docs_of(&mind).in_force(K::Target, &id("target", "r2"))); - assert!(!docs_of(&mind).in_force(K::Target, &id("target", "r1"))); - } - - fn docs_of(mind: &Mind) -> Docs { - Docs::from_image(mind.envelopes()).unwrap() - } - - /// The withdrawal a hand-off derives is a record of its subject like any - /// other: the second time the repo leaves, it takes the next sequence. - #[test] - fn a_hand_offs_derived_withdrawal_takes_the_next_sequence() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - // Nothing later stands over the first assignment, so withdrawing the - // withdrawal puts it back in force and the repo is this mind's again. - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let D::HandOff(mut again) = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]) else { panic!() }; - again.handed_on = eureka_pipeline::Date("2026-09-17".into()); - let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(again)])); - assert_eq!(writes, vec![ - r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-17")), - r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n2")), - ]); - } - - fn hand_off_on(from: &str, to: &str, day: &str) -> D { - let D::HandOff(mut hand_off) = hand_off(from, to, REPO, &[]) else { panic!() }; - hand_off.handed_on = eureka_pipeline::Date(day.into()); - D::HandOff(hand_off) - } - - fn stewardship_key(sequence: u32) -> String { - format!("{INSTANCE}:stewardship:gamecult_-epiphany.n{sequence}") - } - - /// Which assignment a hand-off's derived withdrawal names is a question of - /// content, not of order. While the withdrawal is being derived the - /// hand-off's own is excluded from what stands, so two assignments are in - /// force at once, and after ten transfers the one that stewards the repo - /// now sorts first: `n11` before `n2`. The replay must still withdraw the - /// assignment it withdrew the first time, or it derives a second - /// withdrawal of a standing assignment and collides instead of answering - /// with its receipt. - #[test] - fn a_source_side_replay_withdraws_the_assignment_it_withdrew() { - let mut mind = seeded(); - let mut aways = Vec::new(); - for transfer in 1..=10u32 { - let day = format!("2026-09-{transfer:02}"); - let away = hand_off_on(INSTANCE, OTHER_INSTANCE, &day); - let (receipt_id, _) = committed(admit(&mut mind, vec![away.clone()])); - aways.push((away, receipt_id)); - committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, &day)])); - } - let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); - assert_eq!(in_force, vec![stewardship_key(11)]); - - for index in [1usize, 8] { - let (away, receipt_id) = &aways[index]; - assert_eq!( - admit(&mut mind, vec![away.clone()]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() }, - "the replay of transfer {} re-derives its own withdrawal", - index + 1 - ); - } - - // The standing assignment is handed away and reinstated, so it too - // carries a withdrawal -- of another hand-off. Sorting first, it is - // what a match on the outcome alone would pick. - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-11")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n11.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let (away, receipt_id) = &aways[1]; - assert_eq!( - admit(&mut mind, vec![away.clone()]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() } - ); - } - - /// `later_in_force` counts what stands, not what exists: the repo went - /// away, came back and went away again, so the second assignment is later - /// than the first but withdrawn, and reinstating the first leaves one - /// assignment in force rather than two. - #[test] - fn a_withdrawn_later_assignment_does_not_block_a_reinstatement() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); - committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17")])); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-18")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); - assert_eq!(in_force, vec![stewardship_key(1)]); - } - - /// And it reads the batch: the assignment that stands later than the one - /// a withdrawal would reinstate may be landing beside that withdrawal, - /// derived by a hand-off in the same batch. - #[test] - fn a_reinstatement_is_blocked_by_an_assignment_in_its_own_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![ - hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17"), - resolution(r(K::Resolution, &withdrawal), withdrawn()), - ])), - MindRefusal::WouldReinstateOverLater { subject: stewardship_key(1), later: stewardship_key(2) } - ); - } - - /// A cut spec's revisions are sequenced within its cut, so another cut's - /// later revision is not what a withdrawal reads: once cut A's second - /// revision is gone, reinstating its first commits while cut B's second - /// stands. - #[test] - fn a_reinstatement_reads_the_revisions_of_its_own_cut() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("A", 1)), D::CutSpec(cut_spec("B", 1))])); - for cut in ["A", "B"] { - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec(cut, 2)), - resolution( - r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))), - superseded(&[r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r2")))]), - ), - ])); - } - let supersession = id("resolution", "cut_spec.cut-A.r1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])), - MindRefusal::WouldReinstateOverLater { - subject: id("cut_spec", "cut-A.r1"), - later: id("cut_spec", "cut-A.r2"), - } - ); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-A.r2")), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])); - assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-A.r1"))); - assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-B.r2"))); - } - - /// A seeded mind with one document of every resolvable kind, and the - /// non-resolvable ones beside them. - fn world() -> Mind { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - stewardship(INSTANCE, OTHER_REPO), - question("Q1", &["A", "B"], "A"), - D::Ruling(ruling("R1")), - D::CutSpec(cut_spec("1", 1)), - D::CutReport(cut_report("1", 1)), - verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&id("finding", "cut-1.s1.F1")], Some("P1"), &["M1"])]), - D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)), - follow_up("FU-1", r(K::Finding, &id("finding", "cut-1.s1.F1"))), - hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[]), - ])); - mind - } - - #[test] - fn the_resolution_matrix_is_admissions() { - let incompatible = |kind: K, outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: kind, outcome: outcome.into() }; - let fixed = || ResolutionOutcome::Fixed { commit: sha(), by: None }; - let target_ref = r(K::Target, &id("target", "r1")); - let target_r2 = r(K::Target, &id("target", "r2")); - let question_ref = r(K::Question, &id("question", "Q1")); - let ruling_ref = r(K::Ruling, &id("ruling", "R1")); - let spec_ref = r(K::CutSpec, &id("cut_spec", "cut-1.r1")); - let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); - let follow_up_ref = r(K::FollowUp, &id("follow_up", "FU-1")); - let stewardship_ref = r(K::Stewardship, &format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")); - let campaign_ref = r(K::Campaign, &id("campaign", "self")); - let report_ref = r(K::CutReport, &id("cut_report", "cut-1.h1")); - let verdict_ref = r(K::Verdict, &id("verdict", "cut-1.s1")); - let instance_ref = r(K::Instance, &format!("{INSTANCE}:instance:self")); - let hand_off_ref = r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-huginn.2026-09-16")); - - // Accepted, one per resolvable kind. - committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(std::slice::from_ref(&target_r2)))])); - committed(admit(&mut world(), vec![resolution(question_ref.clone(), withdrawn())])); - committed(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(ruling_ref.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])); - committed(admit(&mut world(), vec![resolution(spec_ref.clone(), withdrawn())])); - committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Recorded { reason: "noted".into() })])); - committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to: follow_up_ref.clone() })])); - committed(admit(&mut world(), vec![resolution(follow_up_ref.clone(), fixed())])); - committed(admit(&mut world(), vec![resolution(stewardship_ref.clone(), withdrawn())])); - let mut mind = world(); - committed(admit(&mut mind, vec![resolution(question_ref.clone(), withdrawn())])); - let nested = r(K::Resolution, &id("resolution", "question.Q1.n1")); - committed(admit(&mut mind, vec![resolution(nested.clone(), withdrawn())])); - - // Refused, one per kind, and every non-resolvable kind. - assert_eq!(refusal(admit(&mut world(), vec![resolution(target_ref.clone(), withdrawn())])), incompatible(K::Target, "Withdrawn")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(question_ref.clone(), fixed())])), incompatible(K::Question, "Fixed")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(ruling_ref.clone(), withdrawn())])), incompatible(K::Ruling, "Withdrawn")); - // A kind admits the outcomes of its own row and no others: a question - // is answered or withdrawn, never superseded; a target is superseded, - // never answered. - assert_eq!( - refusal(admit(&mut world(), vec![ - question("Q2", &["A", "B"], "A"), - resolution(question_ref, superseded(&[r(K::Question, &id("question", "Q2"))])), - ])), - incompatible(K::Question, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(target_ref, ResolutionOutcome::Answered { by: ruling_ref })])), - incompatible(K::Target, "Answered") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(spec_ref.clone(), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), - incompatible(K::CutSpec, "Answered") - ); - // A cut spec superseded across cuts is refused too. - let mut other_cut = cut_spec("2", 1); - other_cut.cut = l("2"); - assert_eq!( - refusal(admit(&mut world(), vec![D::CutSpec(other_cut), resolution(spec_ref, superseded(&[r(K::CutSpec, &id("cut_spec", "cut-2.r1"))]))])), - incompatible(K::CutSpec, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(std::slice::from_ref(&finding_ref)))])), - incompatible(K::Finding, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(follow_up_ref, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), - incompatible(K::FollowUp, "Answered") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(stewardship_ref, ResolutionOutcome::Recorded { reason: "no".into() })])), - incompatible(K::Stewardship, "Recorded") - ); - let mut mind = world(); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(nested.clone(), superseded(&[nested]))])), - incompatible(K::Resolution, "Superseded") - ); - for (subject, kind) in [(campaign_ref, K::Campaign), (report_ref, K::CutReport), (verdict_ref, K::Verdict), (instance_ref, K::Instance), (hand_off_ref, K::HandOff)] { - assert_eq!(refusal(admit(&mut world(), vec![resolution(subject, withdrawn())])), incompatible(kind, "Withdrawn")); - } - } - - #[test] - fn supersession_names_each_supersessor_and_none_is_empty() { - let subject = r(K::Ruling, &id("ruling", "R1")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(subject.clone(), superseded(&[]))])), MindRefusal::EmptySupersession); - let (_, writes) = committed(admit(&mut world(), vec![ - D::Ruling(ruling("R2")), - D::Ruling(ruling("R3")), - resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R3"))])), - ])); - assert_eq!(writes.len(), 3); - assert_eq!( - refusal(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R4"))]))])), - MindRefusal::UnknownSupersessor { id: id("ruling", "R4") } - ); - let mut mind = world(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R2")), D::Ruling(ruling("R3")), resolution(r(K::Ruling, &id("ruling", "R2")), superseded(&[r(K::Ruling, &id("ruling", "R3"))]))])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(subject, superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])), - MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R2") } - ); - } - - /// refusals-typed: a document the leaf cannot key is the client's fault. - /// The finding's local is 64 bytes of label under `cut-1.s1.`; the - /// resolution's subject is a resolution whose own local leaves no room for - /// a third nesting. Both pass `validate` and fail only at the key. - fn unkeyable() -> Vec { - let wide = D::Finding(finding("1", 1, &"w".repeat(64), FindingConfidence::Confirmed)); - let deep_subject = id("resolution", &format!("{}.{}", "x".repeat(60), "y".repeat(40))); - vec![wide, resolution(r(K::Resolution, &deep_subject), withdrawn())] - } - - #[test] - fn an_unkeyable_document_is_refused_as_a_document() { - let store = RefusingStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let receipts = receipt_count(&mind); - for (document, field) in unkeyable().into_iter().zip(["finding.key", "resolution.key"]) { - document.validate().unwrap(); - // The leaf owns what a key error says; admission must carry it - // unchanged, field and offending value included. - let leaf = pipeline_key(&document).expect_err("the fixture must not key"); - assert!( - matches!(&leaf, PipelineRefusal::InvalidFormat { field: named, value } if named == field && !value.is_empty()), - "{leaf:?}" - ); - assert_eq!(refusal(admit(&mut mind, vec![document])), MindRefusal::Document(leaf), "{field}"); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - } - } - - /// The derived path (`admit_steps`) shares the helper. No derivable - /// document can fail to key after the leaf's per-kind bound (a derived - /// resolution is depth one or two over a subject of at most 64 bytes), so - /// the helper is pinned directly. - #[test] - fn prepare_classifies_a_key_refusal_as_a_document_and_encodes_the_rest() { - let cache = schema_cache().unwrap(); - let keyable = question("Q1", &["A", "B"], "A"); - assert_eq!(super::prepare(&keyable, &cache).unwrap(), prepare(&keyable)); - for document in unkeyable() { - let leaf = pipeline_key(&document).expect_err("the fixture must not key"); - assert_eq!(super::prepare(&document, &cache).unwrap_err(), MindRefusal::Document(leaf)); - } - } - - #[test] - fn a_ruling_answering_a_question_derives_the_answered_resolution_atomically() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("C")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering.clone())])), - MindRefusal::InvalidChoice { ruling: id("ruling", "R1"), choice: "C".into() } - ); - answering.choice = Some(l("A")); - let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!(writes, vec![r(K::Ruling, &id("ruling", "R1")), r(K::Resolution, &id("resolution", "question.Q1.n1"))]); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.writes.len(), 2); - let Some(D::Resolution(derived)) = mind.get(K::Resolution, &id("resolution", "question.Q1.n1")).unwrap() else { panic!() }; - assert_eq!(derived.outcome, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); - let mut again = ruling("R2"); - again.answers = Some(s(&id("question", "Q1"))); - again.choice = Some(l("B")); - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(again)])), MindRefusal::AlreadyResolved { subject: id("question", "Q1") }); - - // An explicit `Answered` must name the ruling that actually answers - // its subject: a ruling answering Q1 does not resolve Q2. - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let wrong = resolution(r(K::Question, &id("question", "Q2")), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering.clone()), wrong.clone()])), - MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } - ); - // The coherence is checked whether the ruling is in the batch or in - // the image: a later batch citing the stored R1 is refused the same - // way, and the derived resolution of Q1 is not what saves it. - committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!( - refusal(admit(&mut mind, vec![wrong])), - MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } - ); - } - - #[test] - fn a_batch_is_one_to_sixty_four_documents_each_with_its_own_identity() { - let mut mind = seeded(); - // A2 at both ends, and at the boundary the batch is admitted whole. - assert_eq!(refusal(admit(&mut mind, vec![])), MindRefusal::BatchSize { actual: 0 }); - let over = (0..65).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); - assert_eq!(refusal(admit(&mut mind, over)), MindRefusal::BatchSize { actual: 65 }); - let full = (0..BATCH_MAX).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); - let (_, writes) = committed(admit(&mut mind, full)); - assert_eq!(writes.len(), BATCH_MAX); - // A4: one identity, once, whatever the two documents say. - assert_eq!( - refusal(admit(&mut mind, vec![question("Z", &["A", "B"], "A"), question("Z", &["A", "B"], "B")])), - MindRefusal::IdentityCollision { kind: K::Question, id: id("question", "Z") } - ); - } - - #[test] - fn a_question_offers_at_least_two_options_and_recommends_one_of_them() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q7", &["A"], "A")])), - MindRefusal::InvalidOptions { question: id("question", "Q7") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q8", &["A", "B"], "C")])), - MindRefusal::InvalidOptions { question: id("question", "Q8") } - ); - committed(admit(&mut mind, vec![question("Q9", &["A", "B"], "B")])); - } - - #[test] - fn a_label_names_one_option_and_one_invariant() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q1", &["A", "A"], "A")])), - MindRefusal::DuplicateLabel { field: "question.options".into(), label: "A".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![ - target(2, &["x", "x"]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])), - MindRefusal::DuplicateLabel { field: "target.invariants".into(), label: "x".into() } - ); - } - - /// Every A7 reference the image resolved is pinned, not the first of - /// them, so a batch citing three documents cannot commit over a change to - /// the other two. - #[test] - fn every_cited_image_document_is_a_strong_read() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), D::Ruling(ruling("R2")), D::Ruling(ruling("R3"))])); - let cited = ["R1", "R2", "R3"].map(|label| mind.envelope(K::Ruling, &id("ruling", label)).unwrap().clone()); - let mut spec = cut_spec("1", 1); - spec.rulings = cited.iter().map(|envelope| s(&envelope.key)).collect(); - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads.len(), 3); - for envelope in &cited { - assert!(receipt.strong_reads.contains(&DocumentVersion::from_envelope(envelope)), "{}", envelope.key); - } - - // Two batch documents citing one image document pin it once: the pins - // are a set, so a second citation neither duplicates the pin nor - // cancels it. - let mut twice = cut_spec("2", 1); - twice.rulings = vec![s(&id("ruling", "R1"))]; - let D::Question(mut raised) = question("Q1", &["A", "B"], "A") else { panic!() }; - raised.raised_in = Some(r(K::Ruling, &id("ruling", "R1"))); - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(twice), D::Question(raised)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited[0])], "cited twice, pinned once"); - } - - #[test] - fn an_operator_quote_requires_operator_authority() { - let mut mind = seeded(); - let mut quoted = ruling("R1"); - quoted.operator_quote = Some("all recommendations, go ahead".into()); - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - quoted.authority = RulingAuthority::Mind; - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - assert!(mind.get(K::Ruling, &id("ruling", "R1")).unwrap().is_none(), "a refused batch leaves the store unchanged"); - quoted.authority = RulingAuthority::Operator; - committed(admit(&mut mind, vec![D::Ruling(quoted)])); - } - - #[test] - fn a_mind_ruling_without_a_quote_is_admitted() { - let mut mind = seeded(); - let mut own = ruling("R1"); - own.authority = RulingAuthority::Mind; - committed(admit(&mut mind, vec![D::Ruling(own)])); - } - - #[test] - fn a_revision_requires_its_predecessors_supersession_in_the_batch() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![target(2, &[INVARIANT])])), - MindRefusal::RevisionWithoutSupersession { kind: K::Target, revision: 2 } - ); - committed(admit(&mut mind, vec![ - target(2, &[INVARIANT]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 2))])), - MindRefusal::RevisionWithoutSupersession { kind: K::CutSpec, revision: 2 } - ); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 2)), - resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), - ])); - } - - #[test] - fn a_cut_report_cites_an_in_force_spec_and_agrees_with_it() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), - MindRefusal::CutReportWithoutSpec { report: id("cut_report", "cut-1.h1") } - ); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - let mut report = cut_report("1", 1); - report.branch = s("another-branch"); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); - // A branch is compared as written. Git's refs are case-sensitive, and - // a report of `Codex/...` is a report of another branch or of none. - let mut report = cut_report("1", 1); - report.branch = s("CODEX/eureka-pipeline-state"); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); - let mut report = cut_report("1", 1); - report.repo = repo(OTHER_REPO); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "repo".into() }); - let mut report = cut_report("1", 1); - report.range.head = eureka_pipeline::Sha("abcdef0".into()); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { head: "abcdef0".into() }); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } - ); - } - - fn spec_ref(cut: &str) -> PipelineRef { - r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))) - } - - fn run_key(label: &str) -> String { - format!("{INSTANCE}:run:{label}") - } - - /// A seeded mind holding cut specs 1 and 2. - fn with_specs() -> Mind { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutSpec(cut_spec("2", 1))])); - mind - } - - fn recorded(label: &str) -> PipelineDocument { - resolution(r(K::Run, &run_key(label)), ResolutionOutcome::Recorded { reason: "finished".into() }) - } - - /// run-is-the-grant: the claim is the only consumption fact. A second run - /// on a claim a live run holds is AlreadyClaimed, naming the claim and the - /// holder; another claim is free; once the holder is Recorded the claim is - /// free, and withdrawing that record makes the holder live again. - #[test] - fn a_second_run_on_a_live_claim_is_already_claimed() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) }; - assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), claimed); - assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("2"), spec_ref("1")])])), claimed, "any one held claim refuses the run"); - committed(admit(&mut mind, vec![run("b", &[spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); - - let mut reopened = with_specs(); - committed(admit(&mut reopened, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut reopened, vec![recorded("a")])); - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - committed(admit(&mut reopened, vec![resolution(closure, withdrawn())])); - assert_eq!(refusal(admit(&mut reopened, vec![run("d", &[spec_ref("1")])])), claimed); - } - - /// one-live-self-run at admission: a second run of hers of one turn is - /// AlreadyLive naming the holder, whether the holder is claimless or - /// claiming, and a claim the holder holds is still AlreadyClaimed first. - #[test] - fn a_second_live_run_of_hers_of_one_turn_is_already_live() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - let live_a = MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), live_a, "a claimless holder"); - assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])), live_a); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])); - let live_b = MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("2")])])), live_b, "a claiming holder"); - let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("b")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("1")])])), claimed, "claims answer first"); - // The holder is named by its literal key, not by the constructor that made the expectation above. - let MindRefusal::AlreadyLive { run } = refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])) else { panic!("not live") }; - assert_eq!(run.as_str(), format!("{INSTANCE}:run:b")); - assert_eq!(run.to_ref().id.0, format!("{INSTANCE}:run:b")); - } - - /// The rule counts only in-force runs of hers of the same turn: an - /// operator's run, a run of the other turn and a run after the holder is - /// Recorded are admitted, and one live PersonaTurn and one live SelfRun of - /// hers coexist. - #[test] - fn an_operators_run_another_turn_and_a_run_after_the_holder_closed_are_admitted() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![run("op1", &[])])); - committed(admit(&mut mind, vec![run("op2", &[])])); - committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])])); - let live_p = MindRefusal::AlreadyLive { run: RunId::held(&run_key("p")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("p2", RunTurn::PersonaTurn, &[])])), live_p); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("b")])); - committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); - - let mut operator_only = with_specs(); - committed(admit(&mut operator_only, vec![run("op", &[])])); - committed(admit(&mut operator_only, vec![mind_run("a", RunTurn::SelfRun, &[])])); - } - - /// The slot is keyed on instance and turn alone: a holder that started - /// earlier, on another host and under another budget still blocks. - #[test] - fn a_holder_that_differs_in_start_host_and_budget_still_holds_the_slot() { - let mut mind = with_specs(); - let D::Run(mut holder) = mind_run("a", RunTurn::SelfRun, &[]) else { unreachable!() }; - holder.started_on = eureka_pipeline::Date("2026-01-01".into()); - holder.host = s("elsewhere"); - holder.budget_usd = s("9"); - committed(admit(&mut mind, vec![D::Run(holder)])); - assert_eq!( - refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) } - ); - } - - /// A run opened and closed in one batch is never live, so a live holder - /// of its turn does not refuse it; it holds no slot afterwards either. - #[test] - fn a_run_born_closed_beside_a_live_holder_is_admitted() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[]), recorded("b")])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); - } - - /// Two runs of hers of one turn in one batch hold nothing: the batch is - /// checked against itself, refused whole, and nothing is written. - #[test] - fn two_live_runs_of_hers_in_one_batch_are_already_live_and_nothing_commits() { - let mut mind = with_specs(); - let refused = refusal(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[]), mind_run("b", RunTurn::SelfRun, &[])])); - assert!(matches!(refused, MindRefusal::AlreadyLive { .. }), "{refused:?}"); - assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); - } - - /// Withdrawing the close of a run of hers reinstates it, and a reinstated - /// run is a live one: refused while another run of its turn is live, - /// allowed when none is. - #[test] - fn reinstating_a_run_of_hers_over_a_live_holder_is_already_live() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) } - ); - committed(admit(&mut mind, vec![recorded("b")])); - committed(admit(&mut mind, vec![resolution(closure, withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }, - "the reinstated run holds the slot" - ); - } - - /// A retry of an admitted run answers AlreadyAdmitted even when another - /// run of its turn has since become the live one. - #[test] - fn replaying_an_admitted_run_of_hers_after_another_took_the_turn_is_already_admitted() { - let mut mind = with_specs(); - let (first, _) = committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - assert_eq!( - admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } - ); - } - - /// Two openers on one claim in one batch: the claim is held by neither, so - /// the batch is refused whole and nothing is written. One opener at a time - /// is `a_second_run_on_a_live_claim_is_already_claimed`. - #[test] - fn two_openers_in_one_batch_hold_nothing() { - let mut mind = with_specs(); - let refused = refusal(admit(&mut mind, vec![run("a", &[spec_ref("1")]), run("b", &[spec_ref("1")])])); - assert!(matches!(refused, MindRefusal::AlreadyClaimed { .. }), "{refused:?}"); - assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); - } - - /// Every claim names a document that exists and is in force. - #[test] - fn a_runs_claims_must_exist_and_be_in_force() { - let mut mind = with_specs(); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("9")])])), - MindRefusal::MissingReference { kind: K::CutSpec, id: spec_ref("9").id.0 } - ); - committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } - ); - assert!(mind.envelope(K::Run, &run_key("a")).is_none()); - } - - /// A replay of an admitted run is AlreadyAdmitted even when its claim has - /// since been closed: a retry owes nothing to the work moving on. A - /// different run on that closed claim is still refused. - #[test] - fn replaying_an_admitted_run_after_its_claim_closed_is_already_admitted() { - let mut mind = with_specs(); - let (first, _) = committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); - assert_eq!(admit(&mut mind, vec![run("a", &[spec_ref("1")])]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first }); - assert_eq!( - refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } - ); - } - - /// Withdrawing a run's Recorded resolution puts the run back in force, so - /// it goes through the same exclusivity as an opening: refused while - /// another live run holds one of its claims, allowed when none does. - #[test] - fn reinstating_a_run_over_a_live_holder_is_already_claimed() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("c")) } - ); - - let mut free = with_specs(); - committed(admit(&mut free, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut free, vec![recorded("a")])); - committed(admit(&mut free, vec![resolution(closure, withdrawn())])); - } - - /// Reinstatement finds a contested claim anywhere in the run's list: a - /// reinstated run of two claims where only the second is held is refused, - /// and the first alone being contested is refused too. - #[test] - fn reinstating_a_run_finds_a_contested_second_claim() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - for (held, expected) in [("2", "2"), ("1", "1")] { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref(held)])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyClaimed { item: spec_ref(expected).id.0, run: RunId::held(&run_key("c")) } - ); - } - } - - /// Reinstating a run needs every claim in force, as opening it does: once - /// a claimed item has resolved the withdrawal is refused, and while all - /// stay in force it is allowed. - #[test] - fn reinstating_a_run_needs_its_claims_in_force() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } - ); - - let mut live = with_specs(); - committed(admit(&mut live, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut live, vec![recorded("a")])); - committed(admit(&mut live, vec![resolution(closure, withdrawn())])); - } - - /// A holder is found by any of its claims, not only the first. - #[test] - fn a_holder_is_found_by_its_second_claim() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("2"), spec_ref("1")])])); - assert_eq!( - refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), - MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) } - ); - } - - /// A key already holding a run is taken: the same key with different - /// content is an IdentityCollision, never a replay; only byte-identical - /// content answers AlreadyAdmitted. - #[test] - fn a_run_key_reused_with_different_content_is_an_identity_collision() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; - changed.budget_usd = "6".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(changed)])), - MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), - MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } - ); - } - - /// The replay clause is byte identity, not the key: a changed run under an - /// admitted key whose claim has since closed is not a replay, so the closed - /// claim refuses it. - #[test] - fn a_changed_run_under_an_admitted_key_is_not_a_replay() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); - let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; - changed.budget_usd = "6".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(changed)])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } - ); - } - - /// A5 for runs: a run of another instance is a ForeignInstance. - #[test] - fn a_run_of_another_mind_is_a_foreign_instance() { - let mut mind = with_specs(); - let D::Run(mut foreign) = run("a", &[]) else { panic!() }; - foreign.instance = slug(OTHER_INSTANCE); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(foreign)])), - MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() } - ); - } - - /// A run ends: Recorded or Withdrawn, and no other outcome. - #[test] - fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() { - let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Run, outcome: outcome.into() }; - let subject = || r(K::Run, &run_key("a")); - let open = || { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), run("a", &[spec_ref("1")]), run("z", &[])])); - mind - }; - committed(admit(&mut open(), vec![recorded("a")])); - committed(admit(&mut open(), vec![resolution(subject(), withdrawn())])); - let other_run = r(K::Run, &run_key("z")); - let refused = [ - (superseded(std::slice::from_ref(&other_run)), "Superseded"), - (ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"), - (ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"), - (ResolutionOutcome::Deferred { to: other_run }, "Deferred"), - ]; - for (outcome, name) in refused { - assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}"); - } - } - - #[test] - fn depends_on_names_cuts() { - let depending = |cut: &str, on: &[&str]| { - let mut spec = cut_spec(cut, 1); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - let mut mind = seeded(); - // An unknown label, and a spec id (a revision, not a cut), are refused. - assert_eq!( - refusal(admit(&mut mind, vec![depending("2", &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), - MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } - ); - // The same batch carries the cut it depends on. - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); - // The image carries it, and any revision of the cut names it. - committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); - // A cut of another campaign is not this campaign's cut. - let mut other = cut_spec("9", 1); - other.campaign = slug("elsewhere"); - let mut elsewhere = campaign(&[REPO]); - if let D::Campaign(campaign) = &mut elsewhere { - campaign.slug = slug("elsewhere"); - } - committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("4", &["9"])])), - MindRefusal::UnknownDependency { cut: "9".into() } - ); - } - - #[test] - fn depends_on_matches_cut_labels_whole() { - let depending = |cut: &str, on: &[&str]| { - let mut spec = cut_spec(cut, 1); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - // The image carries cut 10: neither its prefix nor an extension of it names it. - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); - for near in ["1", "100"] { - assert_eq!( - refusal(admit(&mut mind, vec![depending("11", &[near])])), - MindRefusal::UnknownDependency { cut: near.into() } - ); - } - // The same holds when the cut arrives in the same batch. - for near in ["2", "200"] { - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("20", 1)), depending("21", &[near])])), - MindRefusal::UnknownDependency { cut: near.into() } - ); - } - committed(admit(&mut mind, vec![depending("12", &["10"])])); - } - - #[test] - fn a_cut_may_not_depend_on_itself() { - let depending = |cut: &str, revision: u32, on: &[&str]| { - let mut spec = cut_spec(cut, revision); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - let mut mind = seeded(); - // Alone in the batch, and beside another cut it legitimately names. - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 1, &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("2", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 1, &["2", "1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - // An earlier revision of the same cut in the image does not make it a real dependency. - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 2, &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - } - - #[test] - fn a_report_head_may_spell_its_commit_short_or_full() { - let full = "5f98228d0123456789abcdef0123456789abcdef"; - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - // The commit is spelled short and the head full, and the reverse. - let mut report = cut_report("1", 1); - report.range.head = eureka_pipeline::Sha(full.into()); - committed(admit(&mut mind, vec![D::CutReport(report)])); - let mut report = cut_report("1", 2); - report.commits[0].sha = eureka_pipeline::Sha(full.into()); - committed(admit(&mut mind, vec![D::CutReport(report)])); - // A head that is no commit's prefix, long or short, is outside. - let mut report = cut_report("1", 3); - report.range.head = eureka_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); - assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); - let mut report = cut_report("1", 3); - report.commits[0].sha = eureka_pipeline::Sha(full.into()); - report.range.head = eureka_pipeline::Sha("5f98229".into()); - assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); - } - - #[test] - fn a_finding_defers_to_a_cut_spec() { - let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); - let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); - committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); - // A spec superseded since is no longer a place to defer to. - let mut mind = world(); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 2)), - resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } - ); - assert_eq!( - refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), - MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } - ); - } - - #[test] - fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutReport(cut_report("1", 1))])); - let finding_id = id("finding", "cut-1.s1.F1"); - let confirmed = D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[], Some("P1"), &[])])])), - MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: "Falsified claim".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![ - verdict("1", 1, vec![claim(ClaimOutcome::Unproven, &[&finding_id], Some("P1"), &[])]), - confirmed.clone(), - ])), - MindRefusal::UnprovenClaimWithConfirmedFinding { claim: "Unproven claim".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], None, &[])])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![ - claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), - claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), - ])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - // A promise is measured by the claim that names it, not by any claim - // that names a promise: the report promises P1 and nothing else. - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P2"), &[])])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &["M9"])])])), - MindRefusal::UnknownMutationLabel { label: "M9".into() } - ); - committed(admit(&mut mind, vec![ - verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&finding_id], Some("P1"), &["M1"])]), - confirmed, - ])); - } - - #[test] - fn a_finding_names_evidence_locations_and_known_invariants() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 1)), - D::CutReport(cut_report("1", 1)), - verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])]), - ])); - let mut bare = finding("1", 1, "F1", FindingConfidence::Plausible); - bare.evidence = vec![]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(bare)])), MindRefusal::FindingWithoutEvidence); - // Locations are evidence's other half: a finding that names no place - // in the code is refused the same way. - let mut placeless = finding("1", 1, "F1", FindingConfidence::Plausible); - placeless.locations = vec![]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(placeless)])), MindRefusal::FindingWithoutEvidence); - let mut unknown = finding("1", 1, "F1", FindingConfidence::Plausible); - unknown.invariants = vec![l("nope")]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(unknown)])), MindRefusal::UnknownInvariant { label: "nope".into() }); - - - // A raw envelope without `range` cannot be built from the type, and - // it is the leaf's decode that refuses it, before any organ rule. - #[derive(serde::Serialize)] - struct Rangeless { - campaign: Slug, - verdict: Short, - label: eureka_pipeline::Label, - confidence: FindingConfidence, - severity: eureka_pipeline::FindingSeverity, - claim: Line, - invariants: Vec, - locations: Vec, - failure_scenario: eureka_pipeline::Para, - evidence: Vec, - precedents: Vec, - origin: eureka_pipeline::FindingOrigin, - } - let whole = finding("1", 1, "F1", FindingConfidence::Plausible); - let rangeless = Rangeless { - campaign: whole.campaign.clone(), - verdict: whole.verdict.clone(), - label: whole.label.clone(), - confidence: whole.confidence, - severity: whole.severity, - claim: whole.claim.clone(), - invariants: whole.invariants.clone(), - locations: whole.locations.clone(), - failure_scenario: whole.failure_scenario.clone(), - evidence: whole.evidence.clone(), - precedents: vec![], - origin: whole.origin, - }; - let mut envelope = prepare(&D::Finding(whole)); - envelope.payload = rmp_serde::to_vec_named(&(rangeless,)).unwrap(); - let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Soul), vec![envelope], now()); - assert!( - matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::InvalidFormat { field, .. }) if field == "payload") - ); - - // The in-force target owns the invariant vocabulary. Supersede r1 with - // an r2 that drops its label, and the label is unknown again; r2's own - // label is what a finding may cite. The target is looked for in image - // and batch, like every other citation, so a finding may cite the - // vocabulary of the revision landing beside it: `other` is admitted in - // r2's own batch, before r2 is anywhere but here. - let mut fresh = finding("1", 1, "F4", FindingConfidence::Plausible); - fresh.invariants = vec![l("other")]; - assert_eq!( - refusal(admit(&mut mind, vec![D::Finding(fresh.clone())])), - MindRefusal::UnknownInvariant { label: "other".into() } - ); - committed(admit(&mut mind, vec![ - target(2, &["other"]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - D::Finding(fresh), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![D::Finding(finding("1", 1, "F2", FindingConfidence::Plausible))])), - MindRefusal::UnknownInvariant { label: INVARIANT.into() } - ); - let mut current = finding("1", 1, "F3", FindingConfidence::Plausible); - current.invariants = vec![l("other")]; - committed(admit(&mut mind, vec![D::Finding(current)])); - } - - #[test] - fn a_campaign_needs_no_stewardship() { - let mut mind = opened(MemoryStore::new(), INSTANCE); - committed(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])); - let D::Campaign(mut empty) = campaign(&[]) else { panic!() }; - empty.slug = slug("other"); - assert_eq!( - refusal(admit(&mut mind, vec![D::Campaign(empty)])), - MindRefusal::EmptyRepos { campaign: "other:campaign:self".into() } - ); - let mut spec = cut_spec("1", 1); - spec.repo = repo(OTHER_REPO); - assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - } - - /// A target revision naming repos, and the supersession of its predecessor. - fn repo_revision(revision: u32, repos: &[&str]) -> Vec { - let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!() }; - named.repos = repos.iter().map(|name| repo(name)).collect(); - let previous = r(K::Target, &id("target", &format!("r{}", revision - 1))); - let this = r(K::Target, &id("target", &format!("r{revision}"))); - vec![D::Target(named), resolution(previous, superseded(&[this]))] - } - - fn spec_on(cut: &str, repo_name: &str) -> D { - let mut spec = cut_spec(cut, 1); - spec.repo = repo(repo_name); - D::CutSpec(spec) - } - - #[test] - fn a_target_revision_adds_a_repo_a_spec_may_name() { - let mut mind = seeded(); - assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - - // The same revision, supersession and spec in one batch. - let mut fresh = seeded(); - let mut batch = repo_revision(2, &[REPO, OTHER_REPO]); - batch.push(spec_on("1", OTHER_REPO)); - committed(admit(&mut fresh, batch)); - } - - #[test] - fn a_target_revision_removes_a_repo() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![spec_on("1", REPO)])); - committed(admit(&mut mind, repo_revision(2, &[OTHER_REPO]))); - let spec_id = id("cut_spec", "cut-1.r1"); - let spec_view = mind.view(&r(K::CutSpec, &spec_id)).expect("a valid ref").expect("the spec is held"); - assert_eq!(spec_view.status, crate::query::PipelineStatus::InForce, "spec 1 r1 stays in force"); - committed(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &spec_id), withdrawn())])); - - let mut second = cut_spec("1", 2); - second.repo = repo(REPO); - let revised = id("cut_spec", "cut-1.r2"); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(second), resolution(r(K::CutSpec, &spec_id), superseded(&[r(K::CutSpec, &revised)]))])), - MindRefusal::RepoNotInCampaign { repo: REPO.into() } - ); - committed(admit(&mut mind, vec![spec_on("2", OTHER_REPO)])); - } - - #[test] - fn a_target_revision_cannot_silently_drop_its_repos() { - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - assert_eq!( - refusal(admit(&mut mind, repo_revision(3, &[]))), - MindRefusal::EmptyRepos { campaign: id("target", "r3") } - ); - // Nothing was written: the restating revision takes the same key. - committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); - // An empty revision whose predecessor named none keeps the campaign's set. - let mut plain = seeded(); - committed(admit(&mut plain, repo_revision(2, &[]))); - } - - #[test] - fn a_repo_revision_replays_idempotently() { - let mut mind = seeded(); - let (first, _) = committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - assert_eq!( - admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO])), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } - ); - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - } - - /// Another campaign, opened with `repos`, and its own target revision 1 naming `named`. - fn other_campaign(repos: &[&str], named: &[&str]) -> Vec { - let D::Campaign(mut campaign) = campaign(repos) else { panic!("a campaign") }; - campaign.slug = "other-campaign".into(); - let D::Target(mut target) = target(1, &[INVARIANT]) else { panic!("a target") }; - target.campaign = "other-campaign".into(); - target.repos = named.iter().map(|name| repo(name)).collect(); - vec![D::Campaign(campaign), D::Target(target)] - } - - #[test] - fn another_campaigns_target_does_not_widen_membership() { - let mut mind = seeded(); - committed(admit(&mut mind, other_campaign(&[REPO], &[REPO, OTHER_REPO]))); - assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - } - - #[test] - fn a_target_is_superseded_only_within_its_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - committed(admit(&mut mind, other_campaign(&[REPO], &[]))); - let foreign = r(K::Target, "other-campaign:target:r1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[foreign]))])), - MindRefusal::IncompatibleResolution { subject_kind: K::Target, outcome: "Superseded".into() } - ); - // Nothing was written: r2 still names the repos, and its own revision still lands. - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); - } - - /// A target of the first campaign (`CAMPAIGN`) at `revision`, naming no repos. - fn own_target(revision: u32) -> D { - target(revision, &[INVARIANT]) - } - - const FOREIGN_TARGET: &str = "other-campaign:target:r1"; - - fn supersession_refused(outcome: PipelineAdmissionOutcome, subject_kind: K) -> bool { - refusal(outcome) == MindRefusal::IncompatibleResolution { subject_kind, outcome: "Superseded".into() } - } - - #[test] - fn a_foreign_target_in_the_same_batch_does_not_supersede() { - let mut mind = seeded(); - let mut batch = other_campaign(&[REPO], &[]); - batch.push(resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, FOREIGN_TARGET)]))); - assert!(supersession_refused(admit(&mut mind, batch), K::Target)); - } - - #[test] - fn every_supersessor_of_a_target_is_checked_whichever_is_foreign() { - for foreign_first in [false, true] { - let mut mind = seeded(); - committed(admit(&mut mind, other_campaign(&[REPO], &[]))); - let own = r(K::Target, &id("target", "r2")); - let foreign = r(K::Target, FOREIGN_TARGET); - let by = if foreign_first { [foreign, own] } else { [own, foreign] }; - let batch = vec![own_target(2), resolution(r(K::Target, &id("target", "r1")), superseded(&by))]; - assert!(supersession_refused(admit(&mut mind, batch), K::Target), "foreign first: {foreign_first}"); - } - } - - #[test] - fn a_new_campaigns_target_in_its_own_batch_is_not_closed_by_a_foreign_one() { - let mut mind = seeded(); - let mut batch = other_campaign(&[REPO], &[]); - batch.push(resolution(r(K::Target, FOREIGN_TARGET), superseded(&[r(K::Target, &id("target", "r1"))]))); - assert!(supersession_refused(admit(&mut mind, batch), K::Target)); - } - - #[test] - fn a_cut_spec_is_superseded_only_within_its_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![spec_on("1", REPO)])); - let mut batch = other_campaign(&[REPO], &[]); - let D::CutSpec(mut foreign) = spec_on("1", REPO) else { panic!("a spec") }; - foreign.campaign = "other-campaign".into(); - batch.push(D::CutSpec(foreign)); - committed(admit(&mut mind, batch)); - let lone = vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, "other-campaign:cut_spec:cut-1.r1")]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::CutSpec)); - // Nothing was written: this campaign's own revision still lands. - let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; - next.revision = 2; - let this = r(K::CutSpec, &id("cut_spec", "cut-1.r2")); - committed(admit(&mut mind, vec![D::CutSpec(next), resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[this]))])); - } - - #[test] - fn a_ruling_is_superseded_by_a_ruling_of_any_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); - let mut batch = other_campaign(&[REPO], &[]); - let mut foreign = ruling("new"); - foreign.campaign = "other-campaign".into(); - batch.push(D::Ruling(foreign)); - batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, "other-campaign:ruling:new")]))); - committed(admit(&mut mind, batch)); - } - - #[test] - fn a_hand_off_derives_this_minds_side_only() { - let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); - let campaign_key = id("campaign", "self"); - let stewardship_key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - - let mut source = seeded(); - assert_eq!( - refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[])])), - MindRefusal::NotStewarded { repo: OTHER_REPO.into() } - ); - assert_eq!( - refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&id("ruling", "R1")])])), - MindRefusal::MissingReference { kind: K::Ruling, id: id("ruling", "R1") } - ); - let (_, writes) = committed(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); - let withdrawal = id_of(&stewardship_key); - assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Resolution, &withdrawal)]); - let Some(D::Resolution(derived)) = source.get(K::Resolution, &withdrawal).unwrap() else { panic!() }; - assert_eq!(derived.outcome, ResolutionOutcome::Withdrawn { reason: Line(key.clone()) }); - assert_eq!(derived.subject, r(K::Stewardship, &stewardship_key)); - - let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); - let (_, writes) = committed(admit(&mut receiving, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); - let assigned = format!("{OTHER_INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Stewardship, &assigned)]); - let Some(D::Stewardship(derived)) = receiving.get(K::Stewardship, &assigned).unwrap() else { panic!() }; - assert_eq!(derived.note, Line(key.clone())); - assert_eq!(derived.instance, slug(OTHER_INSTANCE)); - // The stewardship starts the day the repo was handed over, not on some - // clock of admission's own. - assert_eq!(derived.assigned_on, date()); - assert_eq!(source.envelope(K::HandOff, &key).map(|e| &e.payload), receiving.envelope(K::HandOff, &key).map(|e| &e.payload)); - - // A derived write passes A3-A7 and A8 like any other, and it counts as - // a record of its scope. A batch carrying its own assignment of the - // repo beside the hand-off is two assignments: the derived one takes - // the sequence after the batch's, and the batch's own is then out of - // sequence -- a typed refusal, not a duplicate identity the store - // complains about. - let mut colliding = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut colliding, vec![instance(OTHER_INSTANCE)])); - assert_eq!( - refusal(admit(&mut colliding, vec![ - hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]), - stewardship(OTHER_INSTANCE, REPO), - ])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 1 } - ); - } - - /// The key of the first resolution of a stewardship: the subject's root, - /// then its kind and local -- the subject's own sequence included -- then - /// this resolution's own sequence. - fn id_of(stewardship_key: &str) -> String { - let (root, rest) = stewardship_key.split_once(':').unwrap(); - format!("{root}:resolution:{}.n1", rest.replace(':', ".")) - } - - /// The second construction site of a receipt, deliberately: a receipt - /// planted through the store must be found by replay and must match. - #[test] - fn a_planted_receipt_with_other_content_is_refused_not_replayed() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; - let ordinal = receipt::head(&mind).unwrap() + 1; - let candidate = - receipt::candidate(&slug(INSTANCE), provenance(Faculty::Hands), &[], &envelopes, ordinal, now()).unwrap(); - let mut planted = HuginnCommitReceipt { writes: vec![], ..candidate.clone() }; - planted.writes = candidate.strong_reads.clone(); - planted.receipt_id = candidate.receipt_id.clone(); - store.plant(mind.cache().prepare_entry_named(&planted.receipt_id, &planted).unwrap().0); - let mut mind = opened(store, INSTANCE); - assert!(matches!( - refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), envelopes, now())), - MindRefusal::Unavailable { .. } - )); - } -} +//! Admission: the single door into a mind. +//! +//! `admit` prepares typed documents through the leaf and calls +//! `admit_prepared`, the one commit path for Cut 8, 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: +//! +//! - A1 the batch names this mind, and attributes itself to a named agent and +//! session (`refuse_provenance`); A2 one to sixty-four envelopes; +//! - A3 every envelope passes the leaf's bounds, formats and key +//! recomputation; A4 identities are unique in the batch; A5 every document +//! carrying an instance names this mind; +//! - A6 an empty mind's first batch carries exactly one `instance` document, +//! and a batch carrying one derives the epoch record; +//! - A7 every reference names a document present in the image or the batch, +//! looked up by `(kind, id)`; the ones resolved in the image become the +//! receipt's strong reads; +//! - the derived writes (a ruling's `Answered` resolution, a hand-off's +//! stewardship withdrawal or assignment) are appended and pass A3-A7; +//! - A8 the per-kind rules, below; A9 an exact replay answers with the stored +//! receipt, after validation and never before it (ruling 20); A10 no write +//! collides with the image; A11 the commit. +//! +//! Every cross-field and cross-document rule lives here and nowhere else: +//! the leaf never gains one, the daemon and the client never re-derive one. +//! In force, the sequences and the scopes are `docs.rs`'s, shared with the +//! views (Cut 9). + +use std::collections::BTreeSet; + +use chrono::{DateTime, Utc}; +use cultcache_rs::{CultCache, CultCacheEnvelope}; +use eureka_pipeline::{ + ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal, + PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key, + validate_pipeline_write_envelope, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::docs::{CitationRole, Docs, Staged, citations, kind_of_id, line_of, outcome_citations}; +use crate::mind::{HuginnMindEpoch, Mind, unavailable}; +use crate::persona::{self, HuginnPersonaEntry, PersonaPutOutcome}; +use crate::receipt::{self, CommitOutcome, PipelineProvenance}; +use crate::refusal::{MindRefusal, RunId}; +use crate::store::MindStore; + +/// The most envelopes one batch may carry. +pub const BATCH_MAX: usize = 64; + +/// One admission request: the mind it is for, who asks, and the documents. +/// Rides the wire whole (`wire::HuginnMindRequest::Admit`). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct PipelineAdmissionBatch { + pub instance: Slug, + pub provenance: PipelineProvenance, + pub documents: Vec, +} + +/// How admission ended. `Committed.writes` names every document the batch +/// landed, derived writes included; Cut 11 indexes from it after `admit` +/// returns, and never inside it. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum PipelineAdmissionOutcome { + Committed { receipt_id: String, committed_at: String, writes: Vec }, + AlreadyAdmitted { receipt_id: String }, + Refused(MindRefusal), + Conflict { identities: Vec }, +} + +/// Keys a document through the leaf, then encodes it. A key the leaf cannot +/// compose is the document's fault and is refused as a document; only the +/// cache's encode fault is the organ's. +fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result { + pipeline_key(document).map_err(MindRefusal::Document)?; + document.prepare(cache).map_err(unavailable) +} + +impl Mind { + /// Validates and prepares every document through the leaf, then admits + /// the envelopes. + pub fn admit(&mut self, batch: PipelineAdmissionBatch, now: DateTime) -> PipelineAdmissionOutcome { + let mut envelopes = Vec::with_capacity(batch.documents.len()); + for document in &batch.documents { + if let Err(refusal) = document.validate() { + return PipelineAdmissionOutcome::Refused(MindRefusal::Document(refusal)); + } + match prepare(document, self.cache()) { + Ok(envelope) => envelopes.push(envelope), + Err(refusal) => return PipelineAdmissionOutcome::Refused(refusal), + } + } + self.admit_prepared(&batch.instance, batch.provenance, envelopes, now) + } + + /// The single commit path: envelopes prepared by anyone, validated here + /// whoever prepared them. + pub fn admit_prepared( + &mut self, + instance: &Slug, + provenance: PipelineProvenance, + envelopes: Vec, + now: DateTime, + ) -> PipelineAdmissionOutcome { + match self.admit_steps(instance, provenance, envelopes, now) { + Ok(outcome) => outcome, + Err(refusal) => PipelineAdmissionOutcome::Refused(refusal), + } + } + + fn admit_steps( + &mut self, + instance: &Slug, + provenance: PipelineProvenance, + envelopes: Vec, + now: DateTime, + ) -> Result { + // A1 + self.require_instance(instance)?; + refuse_provenance(&provenance)?; + // A2 + if envelopes.is_empty() || envelopes.len() > BATCH_MAX { + return Err(MindRefusal::BatchSize { actual: envelopes.len() as u32 }); + } + let mind = self.instance().clone(); + let mut docs = Docs::from_image(self.envelopes())?; + // A3, A5, then A4 on push. + for envelope in envelopes { + docs.push(stage(envelope, &mind)?)?; + } + // A6 + let carries_instance = docs.batch.iter().filter(|staged| staged.kind == PipelineKind::Instance).count() == 1; + if self.is_empty() && !carries_instance { + return Err(MindRefusal::MissingIdentity); + } + // A7 + let mut strong = BTreeSet::new(); + for staged in &docs.batch { + resolve(&docs, staged, &mind, &mut strong)?; + } + // Derived writes pass A3-A7 themselves. + let originals = docs.batch.len(); + for document in derive(&docs, &mind) { + document.validate()?; + let envelope = prepare(&document, self.cache())?; + docs.push(stage(envelope, &mind)?)?; + } + for staged in &docs.batch[originals..] { + resolve(&docs, staged, &mind, &mut strong)?; + } + // The pins: cited image bytes, and every write. + let strong_reads = strong + .iter() + .filter_map(|(type_id, key)| self.raw_envelope(type_id, key).cloned()) + .collect::>(); + let mut writes = docs.batch.iter().map(|staged| staged.envelope.clone()).collect::>(); + if carries_instance { + writes.push(HuginnMindEpoch::envelope(self.cache())?); + } + let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; + // A8 + for staged in &docs.batch { + check(&docs, staged, &mind)?; + } + // A9 + if let Some(existing) = receipt::replay(self, &candidate)? { + return Ok(PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: existing.receipt_id }); + } + // A10 + refuse_collisions(&docs)?; + // A11 + let landed = docs.batch.iter().map(|staged| PipelineRef { kind: staged.kind, id: Short(staged.key.clone()) }).collect(); + match receipt::commit(self, candidate, strong_reads, writes)? { + CommitOutcome::Committed(receipt) => Ok(PipelineAdmissionOutcome::Committed { + receipt_id: receipt.receipt_id, + committed_at: receipt.committed_at, + writes: landed, + }), + CommitOutcome::Conflict(identities) => Ok(PipelineAdmissionOutcome::Conflict { identities }), + } + } + + /// Replaces the mind's persona document whole, or refuses. One door for + /// the one document a mind holds, outside every pipeline batch: the same + /// instance and provenance checks as `admit`, then the persona's five + /// rules in order (schema-valid, `personaId` is this mind, canonical + /// provenance, `expected_updated_at` is the stored `updatedAt`, the new + /// `updatedAt` is later), then the same commit primitive. The stored + /// document is the commit's strong read, so the swap fails if it changed. + pub fn put_persona( + &mut self, + instance: &Slug, + state: Value, + expected_updated_at: Option, + provenance: PipelineProvenance, + now: DateTime, + ) -> PersonaPutOutcome { + match self.persona_steps(instance, state, expected_updated_at, provenance, now) { + Ok(receipt_id) => PersonaPutOutcome::Committed { receipt_id }, + Err(refusal) => PersonaPutOutcome::Refused(refusal), + } + } + + fn persona_steps( + &mut self, + instance: &Slug, + state: Value, + expected_updated_at: Option, + provenance: PipelineProvenance, + now: DateTime, + ) -> Result { + self.require_instance(instance)?; + refuse_provenance(&provenance)?; + // The persona rides a mind that has its identity (A6's analogue). + if self.is_empty() { + return Err(MindRefusal::MissingIdentity); + } + // P1 + persona::validate_shape(&state)?; + // P2 + let mind = self.instance().clone(); + if state.get("personaId").and_then(Value::as_str) != Some(mind.0.as_str()) { + return Err(MindRefusal::PersonaForeign { instance: mind.0 }); + } + // P3 + if state.pointer("/provenance/authority").and_then(Value::as_str) != Some("canonical") { + return Err(MindRefusal::PersonaNotCanonical); + } + // P4 + let stored = self.stored_updated_at()?; + if stored != expected_updated_at { + return Err(MindRefusal::PersonaStale { stored }); + } + // P5 + if let Some(stored) = &stored { + let instant = |stamp: &str| { + DateTime::parse_from_rfc3339(stamp).map_err(|_| MindRefusal::PersonaInvalid { + path: "/updatedAt".into(), + message: "is not an RFC 3339 date-time".into(), + }) + }; + let offered = state.get("updatedAt").and_then(Value::as_str).map(instant).transpose()?; + if offered <= Some(instant(stored)?) { + return Err(MindRefusal::PersonaInvalid { + path: "/updatedAt".into(), + message: "must be later than the stored updatedAt".into(), + }); + } + } + let (envelope, _) = self + .cache() + .prepare_entry_named(mind.0.clone(), &HuginnPersonaEntry { value: state }) + .map_err(unavailable)?; + let strong_reads = self.persona_envelope().cloned().into_iter().collect::>(); + let writes = vec![envelope]; + let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; + match receipt::commit(self, candidate, strong_reads, writes)? { + CommitOutcome::Committed(receipt) => Ok(receipt.receipt_id), + CommitOutcome::Conflict(_) => Err(MindRefusal::PersonaStale { stored: self.stored_updated_at()? }), + } + } +} + +/// The bound `eureka_pipeline::Short` declares, in UTF-8 bytes. The leaf +/// exposes no validator for it, so `short_bound_is_the_leafs_own` pins this to +/// the `maxLength` its schema publishes. +const SHORT_MAX: usize = 200; + +/// Who admitted a batch is the receipt's only record of it, so the mind checks +/// it here and every writer, whatever its transport, gets the check: the agent, +/// the session and the tool follow the leaf's `Title` rule (one alphanumeric +/// character, no control or line-break character, no bidi control) within +/// `Short`'s bound. The leaf keeps that rule private, so +/// `provenance_text_is_the_leafs_title_rule` pins this to it. Refused as the +/// leaf's own refusals, since the fields are the leaf's own type. +fn refuse_provenance(provenance: &PipelineProvenance) -> Result<(), MindRefusal> { + let bidi = |c: char| { + matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') + }; + let control = |c: char| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'); + for (field, value) in [ + ("provenance.agent", &provenance.agent), + ("provenance.session", &provenance.session), + ("provenance.tool", &provenance.tool), + ] { + let text = value.0.as_str(); + if !text.chars().any(char::is_alphanumeric) || text.contains(control) || text.contains(bidi) { + return Err(PipelineRefusal::InvalidFormat { field: field.into(), value: text.into() }.into()); + } + if text.len() > SHORT_MAX { + return Err(PipelineRefusal::FieldBound { field: field.into(), limit: SHORT_MAX as u32, actual: text.len() as u32 }.into()); + } + } + Ok(()) +} + +/// A3 through the leaf, then A5. +fn stage(envelope: CultCacheEnvelope, mind: &Slug) -> Result { + validate_pipeline_write_envelope(&envelope)?; + let document = PipelineDocument::decode(&envelope)?; + refuse_foreign_instance(&document, mind)?; + Ok(Staged { kind: document.kind(), key: envelope.key.clone(), document, envelope }) +} + +/// A5: every document carrying an instance field names this mind (a run +/// included); a hand-off names it on one side. +fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(), MindRefusal> { + let foreign = |declared: &Slug| MindRefusal::ForeignInstance { declared: declared.0.clone(), mind: mind.0.clone() }; + match document { + PipelineDocument::Instance(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => { + Err(foreign(&value.from_instance)) + } + _ => Ok(()), + } +} + +/// What a missing referent is called, by the field that cited it. +enum Missing { + Reference, + SpecOfReport(String), + Supersessor, +} + +struct Reference { + kind: PipelineKind, + id: String, + missing: Missing, +} + +/// A7's field list is `docs::citations`: every `PipelineRef` and every +/// full-id `Short` field, each carrying the refusal its absence earns. A +/// hand-off's documents are cited on the source side only, and each must read +/// as an id of a kind; the receiving side imports them afterwards (Cut 12). +fn references(staged: &Staged, mind: &Slug) -> Result, MindRefusal> { + if let PipelineDocument::HandOff(hand_off) = &staged.document { + if hand_off.from_instance != *mind { + return Ok(Vec::new()); + } + if let Some(document) = hand_off.documents.iter().find(|document| kind_of_id(&document.0).is_none()) { + return Err(PipelineRefusal::InvalidFormat { + field: "hand_off.documents".into(), + value: document.0.clone(), + } + .into()); + } + } + Ok(citations(&staged.document) + .into_iter() + .map(|(role, target)| Reference { + kind: target.kind, + id: target.id.0, + missing: match role { + CitationRole::CutSpec => Missing::SpecOfReport(staged.key.clone()), + CitationRole::SupersededBy => Missing::Supersessor, + _ => Missing::Reference, + }, + }) + .collect()) +} + +/// A7: every referent is in the batch or the image; the image ones are the +/// receipt's strong reads. +fn resolve(docs: &Docs, staged: &Staged, mind: &Slug, strong: &mut BTreeSet<(String, String)>) -> Result<(), MindRefusal> { + for reference in references(staged, mind)? { + if docs.in_batch(reference.kind, &reference.id).is_some() { + continue; + } + if docs.in_image(reference.kind, &reference.id).is_some() { + strong.insert((reference.kind.type_id().to_string(), reference.id)); + continue; + } + return Err(match reference.missing { + Missing::Reference => MindRefusal::MissingReference { kind: reference.kind, id: reference.id }, + Missing::SpecOfReport(report) => MindRefusal::CutReportWithoutSpec { report }, + Missing::Supersessor => MindRefusal::UnknownSupersessor { id: reference.id }, + }); + } + Ok(()) +} + +/// The writes admission derives from the batch: 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. One the batch already carries is not +/// derived again. +fn derive(docs: &Docs, mind: &Slug) -> Vec { + use PipelineDocument as D; + use PipelineKind as K; + let mut derived = Vec::new(); + for staged in &docs.batch { + match &staged.document { + D::Ruling(ruling) => { + if let Some(question) = &ruling.answers { + let subject = PipelineRef { kind: K::Question, id: question.clone() }; + let sequence = docs.derived_resolution_sequence(&subject, |outcome| { + matches!(outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == staged.key) + }); + derived.push(D::Resolution(PipelineResolution { + sequence, + subject, + outcome: ResolutionOutcome::Answered { by: PipelineRef { kind: K::Ruling, id: Short(staged.key.clone()) } }, + rationale: ruling.ruling.clone(), + resolved_on: ruling.ruled_on.clone(), + })); + } + } + D::HandOff(hand_off) => { + if hand_off.from_instance == *mind + && let Some((stewardship_key, _)) = docs.stewardship_of(mind, &hand_off.repo, Some(&staged.key)) + { + let subject = PipelineRef { kind: K::Stewardship, id: Short(stewardship_key.to_string()) }; + let sequence = docs.derived_resolution_sequence(&subject, |outcome| { + matches!(outcome, ResolutionOutcome::Withdrawn { reason } if reason.0 == staged.key) + }); + derived.push(D::Resolution(PipelineResolution { + sequence, + subject, + outcome: ResolutionOutcome::Withdrawn { reason: Line(staged.key.clone()) }, + rationale: hand_off.reason.clone(), + resolved_on: hand_off.handed_on.clone(), + })); + } + if hand_off.to_instance == *mind { + derived.push(D::Stewardship(PipelineStewardship { + instance: mind.clone(), + sequence: docs.derived_stewardship_sequence(mind, &hand_off.repo, &staged.key), + repo: hand_off.repo.clone(), + assigned_on: hand_off.handed_on.clone(), + note: Line(staged.key.clone()), + })); + } + } + _ => {} + } + } + derived.retain(|document| !docs.batch.iter().any(|staged| staged.document == *document)); + derived +} + +/// A8: the per-kind rules. +fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { + use PipelineDocument as D; + use PipelineKind as K; + let key = staged.key.as_str(); + match &staged.document { + D::Campaign(campaign) => { + if campaign.repos.is_empty() { + return Err(MindRefusal::EmptyRepos { campaign: key.into() }); + } + Ok(()) + } + D::Target(target) => { + let predecessor = D::Target(eureka_pipeline::PipelineTarget { revision: target.revision.wrapping_sub(1), ..target.clone() }); + revision_rule(docs, K::Target, key, target.revision, &predecessor)?; + // A revision past the first may not empty the repo set its predecessor + // named: empty means "keep the previous set", and that would silently + // narrow it to the campaign's opening set (ruling campaign-repo-set-home). + if target.repos.is_empty() && target.revision > 1 { + let named = matches!(docs.find(K::Target, &pipeline_key(&predecessor)?), Some(D::Target(previous)) if !previous.repos.is_empty()); + if named { + return Err(MindRefusal::EmptyRepos { campaign: key.into() }); + } + } + unique_labels("target.invariants", target.invariants.iter().map(|invariant| invariant.label.0.as_str())) + } + D::Question(question) => { + if question.options.len() < 2 || !question.options.iter().any(|option| option.label == question.recommended) { + return Err(MindRefusal::InvalidOptions { question: key.into() }); + } + unique_labels("question.options", question.options.iter().map(|option| option.label.0.as_str())) + } + D::Ruling(ruling) => { + if ruling.operator_quote.is_some() && ruling.authority != RulingAuthority::Operator { + return Err(MindRefusal::QuoteWithoutOperator { ruling: key.into() }); + } + let Some(question_id) = &ruling.answers else { return Ok(()) }; + let Some(D::Question(question)) = docs.find(K::Question, &question_id.0) else { + return Err(MindRefusal::MissingReference { kind: K::Question, id: question_id.0.clone() }); + }; + let own = |resolution: &PipelineResolution| { + matches!(&resolution.outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == key) + }; + if !docs.in_force_unless(K::Question, &question_id.0, own) { + return Err(MindRefusal::AlreadyResolved { subject: question_id.0.clone() }); + } + let choice = ruling.choice.as_ref().map_or("", |choice| choice.0.as_str()); + if !question.options.iter().any(|option| option.label.0 == choice) { + return Err(MindRefusal::InvalidChoice { ruling: key.into(), choice: choice.into() }); + } + Ok(()) + } + D::CutSpec(spec) => { + let campaign = docs.of_kind(K::Campaign).find_map(|(_, document)| match document { + D::Campaign(campaign) if campaign.slug == spec.campaign => Some(campaign), + _ => None, + }); + let Some(campaign) = campaign else { + return Err(MindRefusal::MissingReference { kind: K::Campaign, id: spec.campaign.0.clone() }); + }; + if !docs.repos_in_force(campaign).contains(&spec.repo) { + return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() }); + } + for ruling in &spec.rulings { + if !docs.in_force(K::Ruling, &ruling.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); + } + } + for dependency in &spec.depends_on { + // A cut naming itself is no ordering (ruling self-dep). + if dependency.0 == spec.cut.0 { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { + matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) + }); + if !names_a_cut { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + } + let predecessor = D::CutSpec(eureka_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); + revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) + } + D::CutReport(report) => { + let Some(D::CutSpec(spec)) = docs.find(K::CutSpec, &report.cut_spec.0) else { + return Err(MindRefusal::CutReportWithoutSpec { report: key.into() }); + }; + if !docs.in_force(K::CutSpec, &report.cut_spec.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: report.cut_spec.0.clone() }); + } + if report.repo != spec.repo { + return Err(MindRefusal::SpecMismatch { field: "repo".into() }); + } + if report.branch != spec.branch { + return Err(MindRefusal::SpecMismatch { field: "branch".into() }); + } + if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { + return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); + } + Ok(()) + } + D::Verdict(verdict) => { + let Some(D::CutReport(report)) = docs.find(K::CutReport, &verdict.cut_report.0) else { + return Err(MindRefusal::MissingReference { kind: K::CutReport, id: verdict.cut_report.0.clone() }); + }; + for claim in &verdict.claims { + let confirmed = claim.findings.iter().filter_map(|finding| docs.find(K::Finding, &finding.0)).any(|document| { + matches!(document, D::Finding(finding) if finding.confidence == FindingConfidence::Confirmed) + }); + match claim.outcome { + ClaimOutcome::Falsified if !confirmed => { + return Err(MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: claim.claim.0.clone() }); + } + ClaimOutcome::Unproven if confirmed => { + return Err(MindRefusal::UnprovenClaimWithConfirmedFinding { claim: claim.claim.0.clone() }); + } + _ => {} + } + for label in &claim.mutations { + if !report.mutations.iter().any(|mutation| mutation.label == *label) { + return Err(MindRefusal::UnknownMutationLabel { label: label.0.clone() }); + } + } + } + for promise in &report.promises { + let count = verdict.claims.iter().filter(|claim| claim.promise.as_ref() == Some(&promise.label)).count(); + if count != 1 { + return Err(MindRefusal::PromiseWithoutVerdict { promise: promise.label.0.clone() }); + } + } + Ok(()) + } + D::Finding(finding) => { + if finding.evidence.is_empty() || finding.locations.is_empty() { + return Err(MindRefusal::FindingWithoutEvidence); + } + let target = docs.of_kind(K::Target).find_map(|(target_key, document)| match document { + D::Target(target) if target.campaign == finding.campaign && docs.in_force(K::Target, target_key) => Some(target), + _ => None, + }); + for label in &finding.invariants { + if !target.is_some_and(|target| target.invariants.iter().any(|invariant| invariant.label == *label)) { + return Err(MindRefusal::UnknownInvariant { label: label.0.clone() }); + } + } + Ok(()) + } + D::Resolution(resolution) => resolution_rule(docs, resolution), + D::Stewardship(stewardship) => { + let expected = docs.latest_stewardship(&stewardship.instance, &stewardship.repo, Some(stewardship)) + 1; + if stewardship.sequence != expected { + return Err(MindRefusal::StewardshipOutOfSequence { + repo: stewardship.repo.0.clone(), + expected, + actual: stewardship.sequence, + }); + } + if docs.stewardships_of(mind, &stewardship.repo, None).iter().any(|(other, _)| *other != key) { + return Err(MindRefusal::AlreadyStewarded { repo: stewardship.repo.0.clone() }); + } + Ok(()) + } + D::HandOff(hand_off) => { + if hand_off.from_instance == *mind && docs.stewardship_of(mind, &hand_off.repo, Some(key)).is_none() { + return Err(MindRefusal::NotStewarded { repo: hand_off.repo.0.clone() }); + } + Ok(()) + } + // run-is-the-grant: the claim is the one consumption fact. A claim must + // be in force, and no other run in force may hold it. A run already in + // the image under this key, byte for byte, is a replay and owes its + // claims no fresh freedom, so the replay still answers AlreadyAdmitted + // after the work it claimed has moved on. One live run of hers + // per instance and turn (ruling one-live-self-run) is decided by the same + // rule. + D::Run(run) => { + let replay = docs.in_image(K::Run, key) == Some(&staged.document); + run_rule(docs, key, run, replay) + } + D::FollowUp(_) | D::Instance(_) => Ok(()), + } +} + +/// The one rule for a run opening or reinstated by withdrawing its close: every +/// claim in force (a replay of an admitted run owes none) and held by no other +/// live run, then at most one live run of hers per instance and turn. +fn run_rule(docs: &Docs, run_key: &str, run: &PipelineRun, replay: bool) -> Result<(), MindRefusal> { + for claim in &run.claims { + if !replay && !docs.in_force(claim.kind, &claim.id.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: claim.kind, id: claim.id.0.clone() }); + } + if let Some(holder) = docs.claim_holder(run_key, claim) { + return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: RunId::held(holder) }); + } + } + if !replay && docs.in_force(PipelineKind::Run, run_key) && let Some(holder) = docs.live_holder(run_key, run) { + return Err(MindRefusal::AlreadyLive { run: RunId::held(holder) }); + } + Ok(()) +} + +/// Revision 1 stands alone; revision N needs a resolution in the batch that +/// supersedes revision N-1 by this document. The predecessor's key is the +/// leaf's, from the same document one revision back. +fn revision_rule(docs: &Docs, kind: PipelineKind, key: &str, revision: u32, predecessor: &PipelineDocument) -> Result<(), MindRefusal> { + if revision == 1 { + return Ok(()); + } + let refusal = || MindRefusal::RevisionWithoutSupersession { kind, revision }; + if revision == 0 { + return Err(refusal()); + } + let predecessor_key = pipeline_key(predecessor)?; + let superseded = docs.batch.iter().any(|staged| { + matches!(&staged.document, PipelineDocument::Resolution(resolution) + if resolution.subject.kind == kind + && resolution.subject.id.0 == predecessor_key + && matches!(&resolution.outcome, ResolutionOutcome::Superseded { by } + if by.iter().any(|supersessor| supersessor.kind == kind && supersessor.id.0 == key))) + }); + if superseded { Ok(()) } else { Err(refusal()) } +} + +fn unique_labels<'a>(field: &str, labels: impl Iterator) -> Result<(), MindRefusal> { + let mut seen = BTreeSet::new(); + for label in labels { + if !seen.insert(label) { + return Err(MindRefusal::DuplicateLabel { field: field.into(), label: label.into() }); + } + } + Ok(()) +} + +fn outcome_name(outcome: &ResolutionOutcome) -> &'static str { + match outcome { + ResolutionOutcome::Superseded { .. } => "Superseded", + ResolutionOutcome::Answered { .. } => "Answered", + ResolutionOutcome::Fixed { .. } => "Fixed", + ResolutionOutcome::Deferred { .. } => "Deferred", + ResolutionOutcome::Recorded { .. } => "Recorded", + ResolutionOutcome::Withdrawn { .. } => "Withdrawn", + } +} + +/// The resolution matrix: which outcomes a subject kind admits, and of which +/// kinds its referents must be. Admission's, never the leaf's. +/// +/// A resolution is itself resolvable, by withdrawal alone: the operator's +/// ruling on Q17 keeps a withdrawn resolution attached to its subject rather +/// than erasing it, and a subject whose resolution is withdrawn may be +/// resolved again at the next sequence, which the leaf's key now carries. The +/// chain stops there: Q19 A caps it at depth two, in `resolution_rule`, since +/// a withdrawal that could itself be withdrawn would leave two closures +/// standing over one subject. +fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { + use PipelineKind as K; + use ResolutionOutcome as O; + let all = |by: &[PipelineRef], kind: K| by.iter().all(|supersessor| supersessor.kind == kind); + match (subject, outcome) { + (K::Target, O::Superseded { by }) => all(by, K::Target), + (K::Question, O::Answered { by }) => by.kind == K::Ruling, + (K::Question, O::Withdrawn { .. }) => true, + (K::Ruling, O::Superseded { by }) => all(by, K::Ruling), + (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), + (K::CutSpec, O::Withdrawn { .. }) => true, + (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), + (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), + (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, + (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), + (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), + (K::FollowUp, O::Withdrawn { .. }) => true, + (K::Stewardship, O::Superseded { by }) => all(by, K::Stewardship), + (K::Stewardship, O::Withdrawn { .. }) => true, + (K::Run, O::Recorded { .. } | O::Withdrawn { .. }) => true, + (K::Resolution, O::Withdrawn { .. }) => true, + _ => false, + } +} + +/// The resolution row: the sequence, the in-force subject, the matrix, the +/// Q19 cap and the reinstatement rule it rests on, a non-empty supersession, +/// every referent in force, and the two +/// coherence rules (an `Answered` ruling answers this question; a cut spec is +/// superseded within its cut). +fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), MindRefusal> { + use PipelineDocument as D; + use PipelineKind as K; + let subject_kind = resolution.subject.kind; + let subject_id = resolution.subject.id.0.as_str(); + if let ResolutionOutcome::Superseded { by } = &resolution.outcome && by.is_empty() { + return Err(MindRefusal::EmptySupersession); + } + // The sequence is the writer's and is checked against the image, as a + // revision is: the previous plus one, counting every record of this + // subject other than this document itself. + let expected = docs.latest_resolution(&resolution.subject, Some(resolution)) + 1; + if resolution.sequence != expected { + return Err(MindRefusal::ResolutionOutOfSequence { + subject: subject_id.into(), + expected, + actual: resolution.sequence, + }); + } + // A8: a subject with a resolution still in force is closed. A withdrawn + // one is not in force, so the subject is open and this is its next record. + if !docs.in_force_unless(subject_kind, subject_id, |other| other == resolution) { + return Err(MindRefusal::AlreadyResolved { subject: subject_id.into() }); + } + let incompatible = || MindRefusal::IncompatibleResolution { subject_kind, outcome: outcome_name(&resolution.outcome).into() }; + if !matrix(subject_kind, &resolution.outcome) { + return Err(incompatible()); + } + // Q19 A: the chain stops at depth two. A withdrawal cannot be withdrawn; + // to reinstate a closure, resolve the subject again at the next sequence. + if subject_kind == K::Resolution + && let Some(D::Resolution(subject)) = docs.find(K::Resolution, subject_id) + && subject.subject.kind == K::Resolution + { + return Err(incompatible()); + } + // Q19 A's rationale one step out from the cap: a withdrawal never + // re-raises an earlier record over a later one. Withdrawing a resolution + // puts its subject back in force, so refuse while a later record of that + // subject's own scope stands -- a repo's next assignment, a document's + // next revision. + if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) + && subject_kind == K::Resolution + && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) + && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) + && let Some(later) = docs.later_in_force(base) + { + return Err(MindRefusal::WouldReinstateOverLater { + subject: reinstating.subject.id.0.clone(), + later: later.into(), + }); + } + // Withdrawing a run's closure puts the run back in force, and a run in + // force holds its claims and its turn's slot: refuse while another run in force + // holds one of the claims or another run of hers of its turn is live. + if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) + && subject_kind == K::Resolution + && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) + && reinstating.subject.kind == K::Run + && let Some(D::Run(run)) = docs.find(K::Run, &reinstating.subject.id.0) + { + run_rule(docs, &reinstating.subject.id.0, run, false)?; + } + for (_, referent) in outcome_citations(&resolution.outcome) { + if !docs.in_force(referent.kind, &referent.id.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 }); + } + } + match &resolution.outcome { + ResolutionOutcome::Superseded { by } => { + let Some(subject) = docs.find(subject_kind, subject_id) else { return Ok(()) }; + let line = line_of(subject); + for supersessor in by { + if let Some(next) = docs.find(subject_kind, &supersessor.id.0) + && (line.is_none() || line != line_of(next)) + { + return Err(incompatible()); + } + } + } + ResolutionOutcome::Answered { by } => { + let Some(D::Ruling(ruling)) = docs.find(K::Ruling, &by.id.0) else { return Ok(()) }; + if ruling.answers.as_ref().map(|answers| answers.0.as_str()) != Some(subject_id) { + return Err(incompatible()); + } + } + _ => {} + } + Ok(()) +} + +/// A10: a write whose identity the image already holds. Every kind answers +/// the same way now that a resolution key carries a sequence: the identity is +/// taken. Whether a subject is closed is A8's question, not this one's. +fn refuse_collisions(docs: &Docs) -> Result<(), MindRefusal> { + for staged in &docs.batch { + if docs.in_image(staged.kind, &staged.key).is_some() { + return Err(MindRefusal::IdentityCollision { kind: staged.kind, id: staged.key.clone() }); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::*; + use crate::fixtures::prepare; + use crate::mind::schema_cache; + use crate::receipt::{DocumentVersion, Faculty, HuginnCommitReceipt}; + use crate::store::test_stores::{MemoryStore, RefusingStore, SwapCommand}; + use cultcache_rs::DatabaseEntry; + use eureka_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn}; + use sha2::{Digest, Sha256}; + + fn receipt_count(mind: &Mind) -> usize { + mind.receipts().unwrap().len() + } + + #[test] + fn an_empty_store_opens_and_the_first_write_must_carry_the_instance() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + assert!(mind.is_empty()); + assert_eq!(refusal(admit(&mut mind, vec![stewardship(INSTANCE, REPO), campaign(&[REPO])])), MindRefusal::MissingIdentity); + assert!(store.rows().is_empty()); + let (receipt_id, writes) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + assert!(writes.contains(&r(K::Instance, &format!("{INSTANCE}:instance:self")))); + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts.len(), 1); + assert_eq!(receipts[0].receipt_id, receipt_id); + let identities = receipts[0].writes.iter().map(DocumentVersion::identity).collect::>(); + assert!(identities.contains(&(K::Instance.type_id(), format!("{INSTANCE}:instance:self").as_str()))); + assert!(identities.contains(&(HuginnMindEpoch::TYPE, PIPELINE_SCHEMA_EPOCH)), "the epoch record is derived: {identities:?}"); + assert!(receipts[0].strong_reads.is_empty()); + // The store now opens again as this instance, and as nothing else. + assert!(!Mind::open_with(store.clone(), &slug(INSTANCE)).unwrap().is_empty()); + assert!(Mind::open_with(store, &slug(OTHER_INSTANCE)).is_err()); + } + + /// The identity the index labels its collection with: none before the + /// first write, then the first receipt's id and never a later one. + #[test] + fn the_genesis_receipt_id_is_the_first_admissions_and_stays_put() { + let mut mind = opened(MemoryStore::new(), INSTANCE); + assert_eq!(mind.genesis_receipt_id().unwrap(), None); + let (first, _) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first.clone())); + let (second, _) = committed(admit(&mut mind, vec![campaign(&[REPO])])); + assert_ne!(first, second); + assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first)); + } + + fn admit_attributed(agent: &str, session: &str) -> (Mind, PipelineAdmissionOutcome) { + admit_signed(agent, session, "admit") + } + + fn admit_signed(agent: &str, session: &str, tool: &str) -> (Mind, PipelineAdmissionOutcome) { + let mut mind = opened(MemoryStore::new(), INSTANCE); + let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(agent), session: s(session), tool: s(tool) }; + let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance, documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO)] }; + let outcome = mind.admit(batch, now()); + (mind, outcome) + } + + /// Who admitted a batch is validated by the mind, so no writer can commit + /// a blank, multi-line or oversized attribution, and a refused batch leaves + /// nothing behind. + #[test] + fn provenance_names_an_agent_a_session_and_a_tool_on_one_clean_line_within_short() { + let over = "x".repeat(SHORT_MAX + 1); + let exact = "y".repeat(SHORT_MAX); + for bad in [ + "", + " \t ", + "\u{2003}", + "\u{200B}", + "line\nbreak", + "line\rbreak", + "a\u{0B}b", + "a\u{0C}b", + "a\u{85}b", + "a\u{2028}b", + "a\u{2029}b", + "a\0b", + "a\u{1B}[31mb", + "a\u{202E}b", + "a\u{2066}b", + "a\u{200F}b", + ] { + for field in ["provenance.agent", "provenance.session", "provenance.tool"] { + let (mind, outcome) = match field { + "provenance.agent" => admit_signed(bad, "session-1", "admit"), + "provenance.session" => admit_signed("claude", bad, "admit"), + _ => admit_signed("claude", "session-1", bad), + }; + let refused = refusal(outcome); + assert!( + matches!(&refused, MindRefusal::Document(PipelineRefusal::InvalidFormat { field: named, .. }) if named == field), + "{field}={bad:?}: {refused:?}" + ); + assert!(mind.is_empty(), "{field}={bad:?}: a refused batch wrote"); + } + } + for (field, outcome) in [ + ("provenance.agent", admit_signed(&over, "session-1", "admit")), + ("provenance.session", admit_signed("claude", &over, "admit")), + ("provenance.tool", admit_signed("claude", "session-1", &over)), + ] { + let (mind, outcome) = outcome; + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::FieldBound { field: field.into(), limit: 200, actual: 201 }) + ); + assert!(mind.is_empty()); + } + // The bound is in bytes, not characters: 101 two-byte characters are 202 bytes. + let (_, outcome) = admit_attributed(&"é".repeat(101), "session-1"); + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::FieldBound { field: "provenance.agent".into(), limit: 200, actual: 202 }) + ); + let (_, outcome) = admit_signed("claude", "session-1", &"é".repeat(101)); + assert!(matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::FieldBound { actual: 202, .. }))); + let (_, outcome) = admit_attributed(&"é".repeat(100), "session-1"); + committed(outcome); + // The bound is inclusive and a name with inner spaces is a name. + let (mind, outcome) = admit_signed(&exact, "session 7", "admit tool"); + committed(outcome); + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts[0].provenance.agent, s(&exact)); + assert_eq!(receipts[0].provenance.session, s("session 7")); + assert_eq!(receipts[0].provenance.tool, s("admit tool")); + } + + /// The read bounds are the leaf's: admission applies the leaf's validation + /// and adds none. Sixteen anchors of a hundred lines admit and read back; + /// a seventeenth refuses at `cut_spec.reads` and nothing is written. + #[test] + fn reads_admit_at_the_bound_and_refuse_over_it() { + let anchors = |count: usize| { + (0..count) + .map(|n| eureka_pipeline::ReadAnchor { + location: eureka_pipeline::CodeLocation { + path: s("crates/eureka-pipeline/src/lib.rs"), + line: 1, + end_line: Some(100), + symbol: Some(format!("symbol_{n}").as_str().into()), + }, + why: s("read"), + }) + .collect::>() + }; + let mut mind = seeded(); + let mut spec = cut_spec("1", 1); + spec.reads = anchors(16); + committed(admit(&mut mind, vec![D::CutSpec(spec.clone())])); + let held = mind.view(&r(K::CutSpec, &id("cut_spec", "cut-1.r1"))).unwrap().expect("the spec is held"); + assert_eq!(held.document, D::CutSpec(spec)); + + let mut over = cut_spec("2", 1); + over.reads = anchors(17); + let receipts = mind.receipts().unwrap().len(); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(over)])), + MindRefusal::Document(PipelineRefusal::FieldBound { field: "cut_spec.reads".into(), limit: 16, actual: 17 }) + ); + assert_eq!(mind.receipts().unwrap().len(), receipts); + assert_eq!(mind.view(&r(K::CutSpec, &id("cut_spec", "cut-2.r1"))).unwrap(), None, "nothing is written"); + } + + /// The leaf's `Title` rule is private, so this holds the provenance rule to + /// it: the two accept and refuse the same text. + #[test] + fn provenance_text_is_the_leafs_title_rule() { + let leaf_accepts = |text: &str| { + let D::Campaign(mut campaign) = campaign(&[REPO]) else { unreachable!() }; + campaign.title = eureka_pipeline::Title(text.into()); + D::Campaign(campaign).validate().is_ok() + }; + let mine_accepts = |text: &str| { + let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(text), session: s("s"), tool: s("t") }; + refuse_provenance(&provenance).is_ok() + }; + for text in [ + "claude", "session 7", "", " ", "\t", "\u{200B}", "\u{2003}", "!!!", "a\nb", "a\u{0B}b", "a\u{85}b", "a\u{2028}b", + "a\0b", "a\u{7F}b", "a\u{202E}b", "a\u{061C}b", "a\u{2069}b", "a\u{200D}b", "é", "日本", "a\u{FEFF}b", + &"x".repeat(200), &"x".repeat(201), &"é".repeat(100), &"é".repeat(101), + ] { + assert_eq!(mine_accepts(text), leaf_accepts(text), "{text:?}"); + } + } + + #[test] + fn short_bound_is_the_leafs_own() { + let schema = serde_json::to_value(schemars::schema_for!(Short)).unwrap(); + assert_eq!(schema["maxLength"], serde_json::json!(SHORT_MAX)); + } + + #[test] + fn admission_refuses_a_foreign_instance_whatever_the_transport() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let foreign = MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }; + let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; + assert_eq!( + refusal(mind.admit_prepared(&slug(OTHER_INSTANCE), provenance(Faculty::Hands), envelopes, now())), + foreign + ); + assert_eq!(refusal(admit(&mut mind, vec![stewardship(OTHER_INSTANCE, OTHER_REPO)])), foreign); + assert_eq!( + refusal(admit(&mut mind, vec![hand_off("a", "b", REPO, &[])])), + MindRefusal::ForeignInstance { declared: "a".into(), mind: INSTANCE.into() } + ); + assert_eq!(refusal(admit(&mut mind, vec![instance(OTHER_INSTANCE)])), foreign); + assert_eq!(store.rows(), before); + } + + #[test] + fn two_minds_in_one_state_root_stay_separate() { + let root = tempfile::tempdir().unwrap(); + let mut yggdrasil = Mind::open(root.path(), &slug(INSTANCE)).unwrap(); + let mut thought_cage = Mind::open(root.path(), &slug(OTHER_INSTANCE)).unwrap(); + assert_ne!( + Mind::store_path_for(root.path(), &slug(INSTANCE)).unwrap(), + Mind::store_path_for(root.path(), &slug(OTHER_INSTANCE)).unwrap() + ); + committed(admit(&mut yggdrasil, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + committed(admit(&mut thought_cage, vec![instance(OTHER_INSTANCE)])); + let key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert!(yggdrasil.envelope(K::Stewardship, &key).is_some()); + assert!(thought_cage.envelope(K::Stewardship, &key).is_none()); + assert_eq!(yggdrasil.envelopes().len(), 4); + assert_eq!(thought_cage.envelopes().len(), 3); + } + + #[test] + fn keys_are_recomputed_and_a_forged_key_refuses() { + let mut mind = seeded(); + let mut envelope = prepare(&question("Q1", &["A", "B"], "A")); + let expected = envelope.key.clone(); + envelope.key = format!("{expected}-forged"); + let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), vec![envelope.clone()], now()); + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::InvalidIdentity { kind: K::Question, key: envelope.key, expected }) + ); + } + + #[test] + fn references_must_exist_in_image_or_batch() { + let mut mind = seeded(); + let absent_question = id("question", "Q9"); + let mut answering = ruling("R1"); + answering.answers = Some(s(&absent_question)); + answering.choice = Some(l("A")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering)])), + MindRefusal::MissingReference { kind: K::Question, id: absent_question } + ); + let absent_finding = id("finding", "cut-1.s1.F9"); + assert_eq!( + refusal(admit(&mut mind, vec![follow_up("FU-1", r(K::Finding, &absent_finding))])), + MindRefusal::MissingReference { kind: K::Finding, id: absent_finding } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])])])), + MindRefusal::MissingReference { kind: K::CutReport, id: id("cut_report", "cut-1.h1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])), + MindRefusal::MissingReference { kind: K::Question, id: id("question", "Q1") } + ); + // A well-formed id of another kind is a missing reference of the + // cited kind, not a wrong-kind refusal. + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("question", "Q1"))]; + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(spec)])), + MindRefusal::MissingReference { kind: K::Ruling, id: id("question", "Q1") } + ); + } + + #[test] + fn batch_is_all_or_nothing() { + let store = RefusingStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let receipts = receipt_count(&mind); + let mut quoted = ruling("R1"); + quoted.operator_quote = Some("go ahead".into()); + let batch = vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "B"), D::Ruling(quoted)]; + assert_eq!(refusal(admit(&mut mind, batch)), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + + store.command(SwapCommand::Lose); + let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); + assert!(matches!(outcome, PipelineAdmissionOutcome::Conflict { .. }), "{outcome:?}"); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + + store.command(SwapCommand::Fail); + assert!(matches!(refusal(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])), MindRefusal::Unavailable { .. })); + assert_eq!(store.rows(), before); + + store.command(SwapCommand::Delegate); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + assert_eq!(receipt_count(&mind), receipts + 1); + } + + #[test] + fn exact_replay_returns_already_admitted_across_provenance() { + let mut mind = seeded(); + let receipts = receipt_count(&mind); + let (first, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let batch = PipelineAdmissionBatch { + instance: slug(INSTANCE), + provenance: provenance(Faculty::Soul), + documents: vec![question("Q1", &["A", "B"], "A")], + }; + let later = now() + chrono::Duration::hours(1); + assert_eq!(mind.admit(batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first.clone() }); + assert_eq!(receipt_count(&mind), receipts + 1); + // The first write, epoch record and all, replays too. + let seed_batch = PipelineAdmissionBatch { + instance: slug(INSTANCE), + provenance: provenance(Faculty::Imagination), + documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO]), target(1, &[INVARIANT])], + }; + assert!(matches!(mind.admit(seed_batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { .. })); + assert_eq!(receipt_count(&mind), receipts + 1); + } + + /// RS-1: the ordinal is `head + 1` at admission, not a rank derived from + /// the clock or the receipt id. Three batches land at one `now`, so a + /// mutant that ranked by `(committed_at, receipt_id)` would fall back to + /// id order among them; the ids are checked not to already sort in + /// admission order, so that fallback provably disagrees with the truth. + #[test] + fn ordinals_are_admission_order_not_clock_or_id_order() { + let mut mind = seeded(); + assert_eq!(receipt::head(&mind).unwrap(), 1, "the seed batch is ordinal 1"); + let same_time = now(); + let (id1, _) = committed(admit_at(&mut mind, vec![question_n(1)], same_time)); + let (id2, _) = committed(admit_at(&mut mind, vec![question_n(2)], same_time)); + let (id3, _) = committed(admit_at(&mut mind, vec![question_n(3)], same_time)); + + let mut by_id = [id1.clone(), id2.clone(), id3.clone()]; + by_id.sort(); + assert_ne!( + by_id, + [id1.clone(), id2.clone(), id3.clone()], + "the fixture needs receipt ids that do not already sort in admission order" + ); + + let receipts = mind.receipts().unwrap(); + let ordinal_of = |id: &str| receipts.iter().find(|receipt| receipt.receipt_id == id).unwrap().ordinal; + assert_eq!(ordinal_of(&id1), 2); + assert_eq!(ordinal_of(&id2), 3); + assert_eq!(ordinal_of(&id3), 4); + assert_eq!(receipt::head(&mind).unwrap(), 4); + } + + /// RS-1, A9: the ordinal is not digested, so an exact replay finds the + /// receipt it first landed with and keeps that receipt's ordinal, even + /// when the head has moved since and a fresh candidate would be assigned + /// a different one. + #[test] + fn an_exact_replay_keeps_its_ordinal() { + let mut mind = seeded(); + let (first_id, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let first_ordinal = + mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == first_id).unwrap().ordinal; + + // Move the head, so a replay's own candidate would carry a different + // ordinal than the one already on the stored receipt. + committed(admit(&mut mind, vec![question("Q2", &["A", "B"], "A")])); + + let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); + assert_eq!(outcome, PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first_id.clone() }); + + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts.iter().filter(|receipt| receipt.receipt_id == first_id).count(), 1, "no second receipt lands"); + let stored = receipts.iter().find(|receipt| receipt.receipt_id == first_id).unwrap(); + assert_eq!(stored.ordinal, first_ordinal, "the replay does not overwrite the ordinal"); + } + + /// A batch that derives a write replays like any other: the second + /// admission re-derives the record the first one landed, sequence and + /// all, so A9 recognises the digest and answers with the first receipt + /// instead of the derived write colliding with itself. + #[test] + fn an_exact_replay_re_derives_the_rulings_resolution() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering.clone())])); + assert!(writes.contains(&r(K::Resolution, &id("resolution", "question.Q1.n1")))); + let receipts = receipt_count(&mind); + assert_eq!( + admit(&mut mind, vec![D::Ruling(answering)]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id } + ); + assert_eq!(receipt_count(&mind), receipts, "a replay writes no second receipt"); + } + + /// The same on both sides of a hand-off: the source re-derives the + /// withdrawal it landed, the receiving mind the assignment. + #[test] + fn an_exact_replay_re_derives_both_sides_of_a_hand_off() { + let away = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]); + let mut source = seeded(); + let (receipt_id, _) = committed(admit(&mut source, vec![away.clone()])); + let receipts = receipt_count(&source); + assert_eq!(admit(&mut source, vec![away.clone()]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); + assert_eq!(receipt_count(&source), receipts); + + let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); + let (receipt_id, _) = committed(admit(&mut receiving, vec![away.clone()])); + let receipts = receipt_count(&receiving); + assert_eq!(admit(&mut receiving, vec![away]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); + assert_eq!(receipt_count(&receiving), receipts); + } + + /// The sequence a ruling's answer takes is the one that ruling's own + /// answer carries, not whatever answer the question already has. A + /// question reopened by withdrawing its first answer is answered again at + /// the next sequence, so the first answer stays readable under it. + #[test] + fn a_ruling_answering_a_reopened_question_takes_the_next_sequence() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut first = ruling("R1"); + first.answers = Some(s(&id("question", "Q1"))); + first.choice = Some(l("A")); + committed(admit(&mut mind, vec![D::Ruling(first)])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); + let mut second = ruling("R2"); + second.answers = Some(s(&id("question", "Q1"))); + second.choice = Some(l("B")); + let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(second)])); + assert_eq!(writes, vec![ + r(K::Ruling, &id("ruling", "R2")), + r(K::Resolution, &id("resolution", "question.Q1.n2")), + ]); + assert!(mind.envelope(K::Resolution, &id("resolution", "question.Q1.n1")).is_some()); + } + + #[test] + fn a_refused_batch_is_not_answered_from_a_stored_receipt() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let envelopes = vec![prepare(&D::CutSpec(spec))]; + committed(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes.clone(), now())); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("R2")), + resolution(r(K::Ruling, &id("ruling", "R1")), superseded(&[r(K::Ruling, &id("ruling", "R2"))])), + ])); + assert_eq!( + refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes, now())), + MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R1") } + ); + } + + #[test] + fn a_receipt_names_the_exact_bytes_it_read_and_wrote() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); + let cited = mind.envelope(K::Ruling, &id("ruling", "R1")).unwrap().clone(); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited)]); + let written = mind.envelope(K::CutSpec, &id("cut_spec", "cut-1.r1")).unwrap(); + assert_eq!(receipt.writes, vec![DocumentVersion::from_envelope(written)]); + for version in receipt.strong_reads.iter().chain(&receipt.writes) { + assert_eq!(version.payload_sha256, format!("{:x}", Sha256::digest(&version.payload_msgpack))); + } + let digest = rmp_serde::to_vec_named(&(&receipt.instance, &receipt.strong_reads, &receipt.writes)).unwrap(); + assert_eq!(receipt_id, format!("mind-commit-{:x}", Sha256::digest(&digest))); + + // The same content admitted by another faculty in another mind has + // the same id: provenance is not digested. + let mut twin = opened(MemoryStore::new(), INSTANCE); + seed(&mut twin); + committed(admit(&mut twin, vec![D::Ruling(ruling("R1"))])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance: provenance(Faculty::Soul), documents: vec![D::CutSpec(spec)] }; + let (twin_id, _) = committed(twin.admit(batch, now() + chrono::Duration::days(1))); + assert_eq!(twin_id, receipt_id); + let twin_receipt = twin.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == twin_id).unwrap(); + assert_ne!(twin_receipt.provenance, receipt.provenance); + } + + #[test] + fn a_document_is_written_once_and_superseded_by_resolution() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R8"))])); + let mut rewritten = ruling("R8"); + rewritten.ruling = "A repo owns its mind.".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(rewritten)])), + MindRefusal::IdentityCollision { kind: K::Ruling, id: id("ruling", "R8") } + ); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("R9")), + resolution(r(K::Ruling, &id("ruling", "R8")), superseded(&[r(K::Ruling, &id("ruling", "R9"))])), + ])); + assert_eq!(mind.get(K::Ruling, &id("ruling", "R8")).unwrap(), Some(D::Ruling(ruling("R8")))); + } + + /// A8: one resolution of a subject is in force at a time, and the next + /// record of that subject is the next sequence. The two rules are + /// separate refusals: a second closure at the right sequence is refused + /// because the first still stands, and one at a taken sequence is refused + /// before the question of standing arises. + #[test] + fn a_subject_with_a_resolution_in_force_refuses_another() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject.clone(), 2, ResolutionOutcome::Withdrawn { reason: "again".into() })])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(subject.clone(), ResolutionOutcome::Withdrawn { reason: "again".into() })])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 2, actual: 1 } + ); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering)])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + } + + /// Q17 B and Q19 A together: withdrawing a resolution reopens its subject + /// without erasing the record. The withdrawn closure is still readable by + /// key, the subject takes its next resolution at the next sequence, and + /// the chain stops at depth two -- a withdrawal cannot be withdrawn. + #[test] + fn a_withdrawn_resolution_reopens_its_subject_and_stays_readable() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + let first = id("resolution", "question.Q1.n1"); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + + // The withdrawal of that closure: one nesting out, under the + // resolution it withdraws. + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); + let withdrawal = id("resolution", "resolution.question.Q1.n1.n1"); + assert!(mind.get(K::Resolution, &withdrawal).unwrap().is_some()); + + // The subject is open again, and its next record is `n2`. The first + // one is still there, outcome and all: this is a log, not an erasure. + committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); + let Some(D::Resolution(kept)) = mind.get(K::Resolution, &first).unwrap() else { panic!() }; + assert_eq!(kept.outcome, withdrawn()); + assert_eq!(kept.sequence, 1); + + // `n2` stands, so a third is refused, and the withdrawal cannot + // itself be withdrawn to re-raise `n1` over it (Q19 A). + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject, 3, withdrawn())])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), + MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } + ); + + // And a ruling may answer a question whose first closure was + // withdrawn: the derived resolution takes the next sequence. + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!(writes, vec![ + r(K::Ruling, &id("ruling", "R1")), + r(K::Resolution, &id("resolution", "question.Q1.n2")), + ]); + } + + /// The sequence is exactly the previous plus one: a gap refuses as loudly + /// as a taken sequence, on both sequenced kinds, and before the question + /// of what still stands arises. + #[test] + fn a_sequence_gap_refuses_on_both_sequenced_kinds() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); + committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject, 4, withdrawn())])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 4 } + ); + + let mut steward = seeded(); + committed(admit(&mut steward, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + committed(admit(&mut steward, vec![D::HandOff(back)])); + assert_eq!( + refusal(admit(&mut steward, vec![stewardship_n(INSTANCE, REPO, 4)])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 4 } + ); + } + + /// The sequence counts the batch as well as the image: two records of one + /// subject landing together are the second and the third, never two + /// firsts, and the first of them is refused for the sequence rather than + /// for what the other closes. + #[test] + fn the_latest_resolution_counts_the_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + assert_eq!( + refusal(admit(&mut mind, vec![ + resolution_n(subject.clone(), 1, withdrawn()), + resolution_n(subject, 2, ResolutionOutcome::Withdrawn { reason: "again".into() }), + ])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 1 } + ); + } + + /// The Q19 cap reads the batch too: a chain three deep landing in one + /// batch is capped exactly as one landing over three admissions. + #[test] + fn the_cap_reads_a_chain_landing_in_one_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![ + resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn()), + resolution(r(K::Resolution, &id("resolution", "resolution.question.Q1.n1.n1")), withdrawn()), + ])), + MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } + ); + } + + /// Q18 A and Q20 A at admission: a repo is stewarded by one record at a + /// time on a mind, and a repo handed away and handed back is a second + /// record rather than a collision or an overwrite. + #[test] + fn a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer() { + let mut mind = seeded(); + let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 2)])), + MindRefusal::AlreadyStewarded { repo: REPO.into() } + ); + let D::Stewardship(mut retaken) = stewardship(INSTANCE, REPO) else { panic!() }; + retaken.note = "assigned again".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Stewardship(retaken)])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 2, actual: 1 } + ); + + // Handing the repo away withdraws the assignment, and the withdrawal + // is a resolution of that record, sequence and all. The stewardship it + // reads to derive that is an image document, so it is pinned. + let (receipt_id, writes) = committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let away = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.{}", date().0); + assert_eq!(writes, vec![ + r(K::HandOff, &away), + r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1")), + ]); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + let stewardship_envelope = mind.envelope(K::Stewardship, &first).unwrap().clone(); + assert!( + receipt.strong_reads.contains(&DocumentVersion::from_envelope(&stewardship_envelope)), + "the derived withdrawal's own citation is pinned: {:?}", + receipt.strong_reads + ); + + // Nothing stewards the repo now, and a campaign over it still admits: + // campaign admission reads no stewardship (ruling stewardship-rule). + let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; + second.slug = slug("second"); + committed(admit(&mut mind, vec![D::Campaign(second)])); + + // Handed back, the mind stewards it again, as `n2`. No field names a + // return: this is the same hand-off shape with the instances swapped. + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(back)])); + let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + assert_eq!(writes, vec![ + r(K::HandOff, &format!("{OTHER_INSTANCE}:hand_off:{INSTANCE}.gamecult_-epiphany.2026-09-17")), + r(K::Stewardship, &again), + ]); + let Some(D::Stewardship(taken)) = mind.get(K::Stewardship, &again).unwrap() else { panic!() }; + assert_eq!(taken.sequence, 2); + assert_eq!(taken.assigned_on, eureka_pipeline::Date("2026-09-17".into())); + assert!(mind.envelope(K::Stewardship, &first).is_some(), "the first assignment is still readable by key"); + } + + /// Q19 A's rationale, which the cap alone does not carry: a withdrawal + /// never re-raises an earlier record over a later one. The repo went away + /// and came back, so withdrawing the hand-off's withdrawal would put the + /// first assignment back in force beside the second. + #[test] + fn a_withdrawal_does_not_reinstate_a_stewardship_over_a_later_one() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + committed(admit(&mut mind, vec![D::HandOff(back)])); + let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), + MindRefusal::WouldReinstateOverLater { subject: first, later: again.clone() } + ); + let docs = Docs::from_image(mind.envelopes()).unwrap(); + let in_force = docs.stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| *key).collect::>(); + assert_eq!(in_force, vec![again.as_str()], "one assignment stands, and it is the later one"); + } + + /// The same rule one kind over: withdrawing a supersession while the + /// revision that superseded stands would leave two revisions in force. + #[test] + fn a_withdrawal_does_not_reinstate_a_revision_over_a_later_one() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + target(2, &[INVARIANT]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "target.r1.n1")), withdrawn())])), + MindRefusal::WouldReinstateOverLater { subject: id("target", "r1"), later: id("target", "r2") } + ); + assert!(docs_of(&mind).in_force(K::Target, &id("target", "r2"))); + assert!(!docs_of(&mind).in_force(K::Target, &id("target", "r1"))); + } + + fn docs_of(mind: &Mind) -> Docs { + Docs::from_image(mind.envelopes()).unwrap() + } + + /// The withdrawal a hand-off derives is a record of its subject like any + /// other: the second time the repo leaves, it takes the next sequence. + #[test] + fn a_hand_offs_derived_withdrawal_takes_the_next_sequence() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + // Nothing later stands over the first assignment, so withdrawing the + // withdrawal puts it back in force and the repo is this mind's again. + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let D::HandOff(mut again) = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]) else { panic!() }; + again.handed_on = eureka_pipeline::Date("2026-09-17".into()); + let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(again)])); + assert_eq!(writes, vec![ + r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-17")), + r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n2")), + ]); + } + + fn hand_off_on(from: &str, to: &str, day: &str) -> D { + let D::HandOff(mut hand_off) = hand_off(from, to, REPO, &[]) else { panic!() }; + hand_off.handed_on = eureka_pipeline::Date(day.into()); + D::HandOff(hand_off) + } + + fn stewardship_key(sequence: u32) -> String { + format!("{INSTANCE}:stewardship:gamecult_-epiphany.n{sequence}") + } + + /// Which assignment a hand-off's derived withdrawal names is a question of + /// content, not of order. While the withdrawal is being derived the + /// hand-off's own is excluded from what stands, so two assignments are in + /// force at once, and after ten transfers the one that stewards the repo + /// now sorts first: `n11` before `n2`. The replay must still withdraw the + /// assignment it withdrew the first time, or it derives a second + /// withdrawal of a standing assignment and collides instead of answering + /// with its receipt. + #[test] + fn a_source_side_replay_withdraws_the_assignment_it_withdrew() { + let mut mind = seeded(); + let mut aways = Vec::new(); + for transfer in 1..=10u32 { + let day = format!("2026-09-{transfer:02}"); + let away = hand_off_on(INSTANCE, OTHER_INSTANCE, &day); + let (receipt_id, _) = committed(admit(&mut mind, vec![away.clone()])); + aways.push((away, receipt_id)); + committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, &day)])); + } + let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); + assert_eq!(in_force, vec![stewardship_key(11)]); + + for index in [1usize, 8] { + let (away, receipt_id) = &aways[index]; + assert_eq!( + admit(&mut mind, vec![away.clone()]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() }, + "the replay of transfer {} re-derives its own withdrawal", + index + 1 + ); + } + + // The standing assignment is handed away and reinstated, so it too + // carries a withdrawal -- of another hand-off. Sorting first, it is + // what a match on the outcome alone would pick. + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-11")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n11.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let (away, receipt_id) = &aways[1]; + assert_eq!( + admit(&mut mind, vec![away.clone()]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() } + ); + } + + /// `later_in_force` counts what stands, not what exists: the repo went + /// away, came back and went away again, so the second assignment is later + /// than the first but withdrawn, and reinstating the first leaves one + /// assignment in force rather than two. + #[test] + fn a_withdrawn_later_assignment_does_not_block_a_reinstatement() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); + committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17")])); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-18")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); + assert_eq!(in_force, vec![stewardship_key(1)]); + } + + /// And it reads the batch: the assignment that stands later than the one + /// a withdrawal would reinstate may be landing beside that withdrawal, + /// derived by a hand-off in the same batch. + #[test] + fn a_reinstatement_is_blocked_by_an_assignment_in_its_own_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![ + hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17"), + resolution(r(K::Resolution, &withdrawal), withdrawn()), + ])), + MindRefusal::WouldReinstateOverLater { subject: stewardship_key(1), later: stewardship_key(2) } + ); + } + + /// A cut spec's revisions are sequenced within its cut, so another cut's + /// later revision is not what a withdrawal reads: once cut A's second + /// revision is gone, reinstating its first commits while cut B's second + /// stands. + #[test] + fn a_reinstatement_reads_the_revisions_of_its_own_cut() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("A", 1)), D::CutSpec(cut_spec("B", 1))])); + for cut in ["A", "B"] { + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec(cut, 2)), + resolution( + r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))), + superseded(&[r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r2")))]), + ), + ])); + } + let supersession = id("resolution", "cut_spec.cut-A.r1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])), + MindRefusal::WouldReinstateOverLater { + subject: id("cut_spec", "cut-A.r1"), + later: id("cut_spec", "cut-A.r2"), + } + ); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-A.r2")), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])); + assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-A.r1"))); + assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-B.r2"))); + } + + /// A seeded mind with one document of every resolvable kind, and the + /// non-resolvable ones beside them. + fn world() -> Mind { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + stewardship(INSTANCE, OTHER_REPO), + question("Q1", &["A", "B"], "A"), + D::Ruling(ruling("R1")), + D::CutSpec(cut_spec("1", 1)), + D::CutReport(cut_report("1", 1)), + verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&id("finding", "cut-1.s1.F1")], Some("P1"), &["M1"])]), + D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)), + follow_up("FU-1", r(K::Finding, &id("finding", "cut-1.s1.F1"))), + hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[]), + ])); + mind + } + + #[test] + fn the_resolution_matrix_is_admissions() { + let incompatible = |kind: K, outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: kind, outcome: outcome.into() }; + let fixed = || ResolutionOutcome::Fixed { commit: sha(), by: None }; + let target_ref = r(K::Target, &id("target", "r1")); + let target_r2 = r(K::Target, &id("target", "r2")); + let question_ref = r(K::Question, &id("question", "Q1")); + let ruling_ref = r(K::Ruling, &id("ruling", "R1")); + let spec_ref = r(K::CutSpec, &id("cut_spec", "cut-1.r1")); + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let follow_up_ref = r(K::FollowUp, &id("follow_up", "FU-1")); + let stewardship_ref = r(K::Stewardship, &format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")); + let campaign_ref = r(K::Campaign, &id("campaign", "self")); + let report_ref = r(K::CutReport, &id("cut_report", "cut-1.h1")); + let verdict_ref = r(K::Verdict, &id("verdict", "cut-1.s1")); + let instance_ref = r(K::Instance, &format!("{INSTANCE}:instance:self")); + let hand_off_ref = r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-huginn.2026-09-16")); + + // Accepted, one per resolvable kind. + committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(std::slice::from_ref(&target_r2)))])); + committed(admit(&mut world(), vec![resolution(question_ref.clone(), withdrawn())])); + committed(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(ruling_ref.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])); + committed(admit(&mut world(), vec![resolution(spec_ref.clone(), withdrawn())])); + committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Recorded { reason: "noted".into() })])); + committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to: follow_up_ref.clone() })])); + committed(admit(&mut world(), vec![resolution(follow_up_ref.clone(), fixed())])); + committed(admit(&mut world(), vec![resolution(stewardship_ref.clone(), withdrawn())])); + let mut mind = world(); + committed(admit(&mut mind, vec![resolution(question_ref.clone(), withdrawn())])); + let nested = r(K::Resolution, &id("resolution", "question.Q1.n1")); + committed(admit(&mut mind, vec![resolution(nested.clone(), withdrawn())])); + + // Refused, one per kind, and every non-resolvable kind. + assert_eq!(refusal(admit(&mut world(), vec![resolution(target_ref.clone(), withdrawn())])), incompatible(K::Target, "Withdrawn")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(question_ref.clone(), fixed())])), incompatible(K::Question, "Fixed")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(ruling_ref.clone(), withdrawn())])), incompatible(K::Ruling, "Withdrawn")); + // A kind admits the outcomes of its own row and no others: a question + // is answered or withdrawn, never superseded; a target is superseded, + // never answered. + assert_eq!( + refusal(admit(&mut world(), vec![ + question("Q2", &["A", "B"], "A"), + resolution(question_ref, superseded(&[r(K::Question, &id("question", "Q2"))])), + ])), + incompatible(K::Question, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(target_ref, ResolutionOutcome::Answered { by: ruling_ref })])), + incompatible(K::Target, "Answered") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(spec_ref.clone(), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), + incompatible(K::CutSpec, "Answered") + ); + // A cut spec superseded across cuts is refused too. + let mut other_cut = cut_spec("2", 1); + other_cut.cut = l("2"); + assert_eq!( + refusal(admit(&mut world(), vec![D::CutSpec(other_cut), resolution(spec_ref, superseded(&[r(K::CutSpec, &id("cut_spec", "cut-2.r1"))]))])), + incompatible(K::CutSpec, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(std::slice::from_ref(&finding_ref)))])), + incompatible(K::Finding, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(follow_up_ref, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), + incompatible(K::FollowUp, "Answered") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(stewardship_ref, ResolutionOutcome::Recorded { reason: "no".into() })])), + incompatible(K::Stewardship, "Recorded") + ); + let mut mind = world(); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(nested.clone(), superseded(&[nested]))])), + incompatible(K::Resolution, "Superseded") + ); + for (subject, kind) in [(campaign_ref, K::Campaign), (report_ref, K::CutReport), (verdict_ref, K::Verdict), (instance_ref, K::Instance), (hand_off_ref, K::HandOff)] { + assert_eq!(refusal(admit(&mut world(), vec![resolution(subject, withdrawn())])), incompatible(kind, "Withdrawn")); + } + } + + #[test] + fn supersession_names_each_supersessor_and_none_is_empty() { + let subject = r(K::Ruling, &id("ruling", "R1")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(subject.clone(), superseded(&[]))])), MindRefusal::EmptySupersession); + let (_, writes) = committed(admit(&mut world(), vec![ + D::Ruling(ruling("R2")), + D::Ruling(ruling("R3")), + resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R3"))])), + ])); + assert_eq!(writes.len(), 3); + assert_eq!( + refusal(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R4"))]))])), + MindRefusal::UnknownSupersessor { id: id("ruling", "R4") } + ); + let mut mind = world(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R2")), D::Ruling(ruling("R3")), resolution(r(K::Ruling, &id("ruling", "R2")), superseded(&[r(K::Ruling, &id("ruling", "R3"))]))])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(subject, superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])), + MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R2") } + ); + } + + /// refusals-typed: a document the leaf cannot key is the client's fault. + /// The finding's local is 64 bytes of label under `cut-1.s1.`; the + /// resolution's subject is a resolution whose own local leaves no room for + /// a third nesting. Both pass `validate` and fail only at the key. + fn unkeyable() -> Vec { + let wide = D::Finding(finding("1", 1, &"w".repeat(64), FindingConfidence::Confirmed)); + let deep_subject = id("resolution", &format!("{}.{}", "x".repeat(60), "y".repeat(40))); + vec![wide, resolution(r(K::Resolution, &deep_subject), withdrawn())] + } + + #[test] + fn an_unkeyable_document_is_refused_as_a_document() { + let store = RefusingStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let receipts = receipt_count(&mind); + for (document, field) in unkeyable().into_iter().zip(["finding.key", "resolution.key"]) { + document.validate().unwrap(); + // The leaf owns what a key error says; admission must carry it + // unchanged, field and offending value included. + let leaf = pipeline_key(&document).expect_err("the fixture must not key"); + assert!( + matches!(&leaf, PipelineRefusal::InvalidFormat { field: named, value } if named == field && !value.is_empty()), + "{leaf:?}" + ); + assert_eq!(refusal(admit(&mut mind, vec![document])), MindRefusal::Document(leaf), "{field}"); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + } + } + + /// The derived path (`admit_steps`) shares the helper. No derivable + /// document can fail to key after the leaf's per-kind bound (a derived + /// resolution is depth one or two over a subject of at most 64 bytes), so + /// the helper is pinned directly. + #[test] + fn prepare_classifies_a_key_refusal_as_a_document_and_encodes_the_rest() { + let cache = schema_cache().unwrap(); + let keyable = question("Q1", &["A", "B"], "A"); + assert_eq!(super::prepare(&keyable, &cache).unwrap(), prepare(&keyable)); + for document in unkeyable() { + let leaf = pipeline_key(&document).expect_err("the fixture must not key"); + assert_eq!(super::prepare(&document, &cache).unwrap_err(), MindRefusal::Document(leaf)); + } + } + + #[test] + fn a_ruling_answering_a_question_derives_the_answered_resolution_atomically() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("C")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering.clone())])), + MindRefusal::InvalidChoice { ruling: id("ruling", "R1"), choice: "C".into() } + ); + answering.choice = Some(l("A")); + let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!(writes, vec![r(K::Ruling, &id("ruling", "R1")), r(K::Resolution, &id("resolution", "question.Q1.n1"))]); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.writes.len(), 2); + let Some(D::Resolution(derived)) = mind.get(K::Resolution, &id("resolution", "question.Q1.n1")).unwrap() else { panic!() }; + assert_eq!(derived.outcome, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); + let mut again = ruling("R2"); + again.answers = Some(s(&id("question", "Q1"))); + again.choice = Some(l("B")); + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(again)])), MindRefusal::AlreadyResolved { subject: id("question", "Q1") }); + + // An explicit `Answered` must name the ruling that actually answers + // its subject: a ruling answering Q1 does not resolve Q2. + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let wrong = resolution(r(K::Question, &id("question", "Q2")), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering.clone()), wrong.clone()])), + MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } + ); + // The coherence is checked whether the ruling is in the batch or in + // the image: a later batch citing the stored R1 is refused the same + // way, and the derived resolution of Q1 is not what saves it. + committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!( + refusal(admit(&mut mind, vec![wrong])), + MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } + ); + } + + #[test] + fn a_batch_is_one_to_sixty_four_documents_each_with_its_own_identity() { + let mut mind = seeded(); + // A2 at both ends, and at the boundary the batch is admitted whole. + assert_eq!(refusal(admit(&mut mind, vec![])), MindRefusal::BatchSize { actual: 0 }); + let over = (0..65).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); + assert_eq!(refusal(admit(&mut mind, over)), MindRefusal::BatchSize { actual: 65 }); + let full = (0..BATCH_MAX).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); + let (_, writes) = committed(admit(&mut mind, full)); + assert_eq!(writes.len(), BATCH_MAX); + // A4: one identity, once, whatever the two documents say. + assert_eq!( + refusal(admit(&mut mind, vec![question("Z", &["A", "B"], "A"), question("Z", &["A", "B"], "B")])), + MindRefusal::IdentityCollision { kind: K::Question, id: id("question", "Z") } + ); + } + + #[test] + fn a_question_offers_at_least_two_options_and_recommends_one_of_them() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q7", &["A"], "A")])), + MindRefusal::InvalidOptions { question: id("question", "Q7") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q8", &["A", "B"], "C")])), + MindRefusal::InvalidOptions { question: id("question", "Q8") } + ); + committed(admit(&mut mind, vec![question("Q9", &["A", "B"], "B")])); + } + + #[test] + fn a_label_names_one_option_and_one_invariant() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q1", &["A", "A"], "A")])), + MindRefusal::DuplicateLabel { field: "question.options".into(), label: "A".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![ + target(2, &["x", "x"]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])), + MindRefusal::DuplicateLabel { field: "target.invariants".into(), label: "x".into() } + ); + } + + /// Every A7 reference the image resolved is pinned, not the first of + /// them, so a batch citing three documents cannot commit over a change to + /// the other two. + #[test] + fn every_cited_image_document_is_a_strong_read() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), D::Ruling(ruling("R2")), D::Ruling(ruling("R3"))])); + let cited = ["R1", "R2", "R3"].map(|label| mind.envelope(K::Ruling, &id("ruling", label)).unwrap().clone()); + let mut spec = cut_spec("1", 1); + spec.rulings = cited.iter().map(|envelope| s(&envelope.key)).collect(); + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads.len(), 3); + for envelope in &cited { + assert!(receipt.strong_reads.contains(&DocumentVersion::from_envelope(envelope)), "{}", envelope.key); + } + + // Two batch documents citing one image document pin it once: the pins + // are a set, so a second citation neither duplicates the pin nor + // cancels it. + let mut twice = cut_spec("2", 1); + twice.rulings = vec![s(&id("ruling", "R1"))]; + let D::Question(mut raised) = question("Q1", &["A", "B"], "A") else { panic!() }; + raised.raised_in = Some(r(K::Ruling, &id("ruling", "R1"))); + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(twice), D::Question(raised)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited[0])], "cited twice, pinned once"); + } + + #[test] + fn an_operator_quote_requires_operator_authority() { + let mut mind = seeded(); + let mut quoted = ruling("R1"); + quoted.operator_quote = Some("all recommendations, go ahead".into()); + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + quoted.authority = RulingAuthority::Mind; + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + assert!(mind.get(K::Ruling, &id("ruling", "R1")).unwrap().is_none(), "a refused batch leaves the store unchanged"); + quoted.authority = RulingAuthority::Operator; + committed(admit(&mut mind, vec![D::Ruling(quoted)])); + } + + #[test] + fn a_mind_ruling_without_a_quote_is_admitted() { + let mut mind = seeded(); + let mut own = ruling("R1"); + own.authority = RulingAuthority::Mind; + committed(admit(&mut mind, vec![D::Ruling(own)])); + } + + #[test] + fn a_revision_requires_its_predecessors_supersession_in_the_batch() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![target(2, &[INVARIANT])])), + MindRefusal::RevisionWithoutSupersession { kind: K::Target, revision: 2 } + ); + committed(admit(&mut mind, vec![ + target(2, &[INVARIANT]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 2))])), + MindRefusal::RevisionWithoutSupersession { kind: K::CutSpec, revision: 2 } + ); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + } + + #[test] + fn a_cut_report_cites_an_in_force_spec_and_agrees_with_it() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), + MindRefusal::CutReportWithoutSpec { report: id("cut_report", "cut-1.h1") } + ); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + let mut report = cut_report("1", 1); + report.branch = s("another-branch"); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); + // A branch is compared as written. Git's refs are case-sensitive, and + // a report of `Codex/...` is a report of another branch or of none. + let mut report = cut_report("1", 1); + report.branch = s("CODEX/eureka-pipeline-state"); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); + let mut report = cut_report("1", 1); + report.repo = repo(OTHER_REPO); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "repo".into() }); + let mut report = cut_report("1", 1); + report.range.head = eureka_pipeline::Sha("abcdef0".into()); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { head: "abcdef0".into() }); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + } + + fn spec_ref(cut: &str) -> PipelineRef { + r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))) + } + + fn run_key(label: &str) -> String { + format!("{INSTANCE}:run:{label}") + } + + /// A seeded mind holding cut specs 1 and 2. + fn with_specs() -> Mind { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutSpec(cut_spec("2", 1))])); + mind + } + + fn recorded(label: &str) -> PipelineDocument { + resolution(r(K::Run, &run_key(label)), ResolutionOutcome::Recorded { reason: "finished".into() }) + } + + /// run-is-the-grant: the claim is the only consumption fact. A second run + /// on a claim a live run holds is AlreadyClaimed, naming the claim and the + /// holder; another claim is free; once the holder is Recorded the claim is + /// free, and withdrawing that record makes the holder live again. + #[test] + fn a_second_run_on_a_live_claim_is_already_claimed() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) }; + assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), claimed); + assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("2"), spec_ref("1")])])), claimed, "any one held claim refuses the run"); + committed(admit(&mut mind, vec![run("b", &[spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); + + let mut reopened = with_specs(); + committed(admit(&mut reopened, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut reopened, vec![recorded("a")])); + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + committed(admit(&mut reopened, vec![resolution(closure, withdrawn())])); + assert_eq!(refusal(admit(&mut reopened, vec![run("d", &[spec_ref("1")])])), claimed); + } + + /// one-live-self-run at admission: a second run of hers of one turn is + /// AlreadyLive naming the holder, whether the holder is claimless or + /// claiming, and a claim the holder holds is still AlreadyClaimed first. + #[test] + fn a_second_live_run_of_hers_of_one_turn_is_already_live() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + let live_a = MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), live_a, "a claimless holder"); + assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])), live_a); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])); + let live_b = MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("2")])])), live_b, "a claiming holder"); + let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("b")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("1")])])), claimed, "claims answer first"); + // The holder is named by its literal key, not by the constructor that made the expectation above. + let MindRefusal::AlreadyLive { run } = refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])) else { panic!("not live") }; + assert_eq!(run.as_str(), format!("{INSTANCE}:run:b")); + assert_eq!(run.to_ref().id.0, format!("{INSTANCE}:run:b")); + } + + /// The rule counts only in-force runs of hers of the same turn: an + /// operator's run, a run of the other turn and a run after the holder is + /// Recorded are admitted, and one live PersonaTurn and one live SelfRun of + /// hers coexist. + #[test] + fn an_operators_run_another_turn_and_a_run_after_the_holder_closed_are_admitted() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![run("op1", &[])])); + committed(admit(&mut mind, vec![run("op2", &[])])); + committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])])); + let live_p = MindRefusal::AlreadyLive { run: RunId::held(&run_key("p")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("p2", RunTurn::PersonaTurn, &[])])), live_p); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("b")])); + committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); + + let mut operator_only = with_specs(); + committed(admit(&mut operator_only, vec![run("op", &[])])); + committed(admit(&mut operator_only, vec![mind_run("a", RunTurn::SelfRun, &[])])); + } + + /// The slot is keyed on instance and turn alone: a holder that started + /// earlier, on another host and under another budget still blocks. + #[test] + fn a_holder_that_differs_in_start_host_and_budget_still_holds_the_slot() { + let mut mind = with_specs(); + let D::Run(mut holder) = mind_run("a", RunTurn::SelfRun, &[]) else { unreachable!() }; + holder.started_on = eureka_pipeline::Date("2026-01-01".into()); + holder.host = s("elsewhere"); + holder.budget_usd = s("9"); + committed(admit(&mut mind, vec![D::Run(holder)])); + assert_eq!( + refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) } + ); + } + + /// A run opened and closed in one batch is never live, so a live holder + /// of its turn does not refuse it; it holds no slot afterwards either. + #[test] + fn a_run_born_closed_beside_a_live_holder_is_admitted() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[]), recorded("b")])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); + } + + /// Two runs of hers of one turn in one batch hold nothing: the batch is + /// checked against itself, refused whole, and nothing is written. + #[test] + fn two_live_runs_of_hers_in_one_batch_are_already_live_and_nothing_commits() { + let mut mind = with_specs(); + let refused = refusal(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[]), mind_run("b", RunTurn::SelfRun, &[])])); + assert!(matches!(refused, MindRefusal::AlreadyLive { .. }), "{refused:?}"); + assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); + } + + /// Withdrawing the close of a run of hers reinstates it, and a reinstated + /// run is a live one: refused while another run of its turn is live, + /// allowed when none is. + #[test] + fn reinstating_a_run_of_hers_over_a_live_holder_is_already_live() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) } + ); + committed(admit(&mut mind, vec![recorded("b")])); + committed(admit(&mut mind, vec![resolution(closure, withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }, + "the reinstated run holds the slot" + ); + } + + /// A retry of an admitted run answers AlreadyAdmitted even when another + /// run of its turn has since become the live one. + #[test] + fn replaying_an_admitted_run_of_hers_after_another_took_the_turn_is_already_admitted() { + let mut mind = with_specs(); + let (first, _) = committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + assert_eq!( + admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } + ); + } + + /// Two openers on one claim in one batch: the claim is held by neither, so + /// the batch is refused whole and nothing is written. One opener at a time + /// is `a_second_run_on_a_live_claim_is_already_claimed`. + #[test] + fn two_openers_in_one_batch_hold_nothing() { + let mut mind = with_specs(); + let refused = refusal(admit(&mut mind, vec![run("a", &[spec_ref("1")]), run("b", &[spec_ref("1")])])); + assert!(matches!(refused, MindRefusal::AlreadyClaimed { .. }), "{refused:?}"); + assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); + } + + /// Every claim names a document that exists and is in force. + #[test] + fn a_runs_claims_must_exist_and_be_in_force() { + let mut mind = with_specs(); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("9")])])), + MindRefusal::MissingReference { kind: K::CutSpec, id: spec_ref("9").id.0 } + ); + committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } + ); + assert!(mind.envelope(K::Run, &run_key("a")).is_none()); + } + + /// A replay of an admitted run is AlreadyAdmitted even when its claim has + /// since been closed: a retry owes nothing to the work moving on. A + /// different run on that closed claim is still refused. + #[test] + fn replaying_an_admitted_run_after_its_claim_closed_is_already_admitted() { + let mut mind = with_specs(); + let (first, _) = committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); + assert_eq!(admit(&mut mind, vec![run("a", &[spec_ref("1")])]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first }); + assert_eq!( + refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } + ); + } + + /// Withdrawing a run's Recorded resolution puts the run back in force, so + /// it goes through the same exclusivity as an opening: refused while + /// another live run holds one of its claims, allowed when none does. + #[test] + fn reinstating_a_run_over_a_live_holder_is_already_claimed() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("c")) } + ); + + let mut free = with_specs(); + committed(admit(&mut free, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut free, vec![recorded("a")])); + committed(admit(&mut free, vec![resolution(closure, withdrawn())])); + } + + /// Reinstatement finds a contested claim anywhere in the run's list: a + /// reinstated run of two claims where only the second is held is refused, + /// and the first alone being contested is refused too. + #[test] + fn reinstating_a_run_finds_a_contested_second_claim() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + for (held, expected) in [("2", "2"), ("1", "1")] { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref(held)])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyClaimed { item: spec_ref(expected).id.0, run: RunId::held(&run_key("c")) } + ); + } + } + + /// Reinstating a run needs every claim in force, as opening it does: once + /// a claimed item has resolved the withdrawal is refused, and while all + /// stay in force it is allowed. + #[test] + fn reinstating_a_run_needs_its_claims_in_force() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } + ); + + let mut live = with_specs(); + committed(admit(&mut live, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut live, vec![recorded("a")])); + committed(admit(&mut live, vec![resolution(closure, withdrawn())])); + } + + /// A holder is found by any of its claims, not only the first. + #[test] + fn a_holder_is_found_by_its_second_claim() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("2"), spec_ref("1")])])); + assert_eq!( + refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), + MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) } + ); + } + + /// A key already holding a run is taken: the same key with different + /// content is an IdentityCollision, never a replay; only byte-identical + /// content answers AlreadyAdmitted. + #[test] + fn a_run_key_reused_with_different_content_is_an_identity_collision() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; + changed.budget_usd = "6".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(changed)])), + MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), + MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } + ); + } + + /// The replay clause is byte identity, not the key: a changed run under an + /// admitted key whose claim has since closed is not a replay, so the closed + /// claim refuses it. + #[test] + fn a_changed_run_under_an_admitted_key_is_not_a_replay() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); + let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; + changed.budget_usd = "6".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(changed)])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } + ); + } + + /// A5 for runs: a run of another instance is a ForeignInstance. + #[test] + fn a_run_of_another_mind_is_a_foreign_instance() { + let mut mind = with_specs(); + let D::Run(mut foreign) = run("a", &[]) else { panic!() }; + foreign.instance = slug(OTHER_INSTANCE); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(foreign)])), + MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() } + ); + } + + /// A run ends: Recorded or Withdrawn, and no other outcome. + #[test] + fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() { + let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Run, outcome: outcome.into() }; + let subject = || r(K::Run, &run_key("a")); + let open = || { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), run("a", &[spec_ref("1")]), run("z", &[])])); + mind + }; + committed(admit(&mut open(), vec![recorded("a")])); + committed(admit(&mut open(), vec![resolution(subject(), withdrawn())])); + let other_run = r(K::Run, &run_key("z")); + let refused = [ + (superseded(std::slice::from_ref(&other_run)), "Superseded"), + (ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"), + (ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"), + (ResolutionOutcome::Deferred { to: other_run }, "Deferred"), + ]; + for (outcome, name) in refused { + assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}"); + } + } + + #[test] + fn depends_on_names_cuts() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // An unknown label, and a spec id (a revision, not a cut), are refused. + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), + MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } + ); + // The same batch carries the cut it depends on. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); + // The image carries it, and any revision of the cut names it. + committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); + // A cut of another campaign is not this campaign's cut. + let mut other = cut_spec("9", 1); + other.campaign = slug("elsewhere"); + let mut elsewhere = campaign(&[REPO]); + if let D::Campaign(campaign) = &mut elsewhere { + campaign.slug = slug("elsewhere"); + } + committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("4", &["9"])])), + MindRefusal::UnknownDependency { cut: "9".into() } + ); + } + + #[test] + fn depends_on_matches_cut_labels_whole() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + // The image carries cut 10: neither its prefix nor an extension of it names it. + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); + for near in ["1", "100"] { + assert_eq!( + refusal(admit(&mut mind, vec![depending("11", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + // The same holds when the cut arrives in the same batch. + for near in ["2", "200"] { + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("20", 1)), depending("21", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + committed(admit(&mut mind, vec![depending("12", &["10"])])); + } + + #[test] + fn a_cut_may_not_depend_on_itself() { + let depending = |cut: &str, revision: u32, on: &[&str]| { + let mut spec = cut_spec(cut, revision); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // Alone in the batch, and beside another cut it legitimately names. + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("2", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["2", "1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + // An earlier revision of the same cut in the image does not make it a real dependency. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 2, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + } + + #[test] + fn a_report_head_may_spell_its_commit_short_or_full() { + let full = "5f98228d0123456789abcdef0123456789abcdef"; + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + // The commit is spelled short and the head full, and the reverse. + let mut report = cut_report("1", 1); + report.range.head = eureka_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + let mut report = cut_report("1", 2); + report.commits[0].sha = eureka_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + // A head that is no commit's prefix, long or short, is outside. + let mut report = cut_report("1", 3); + report.range.head = eureka_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + let mut report = cut_report("1", 3); + report.commits[0].sha = eureka_pipeline::Sha(full.into()); + report.range.head = eureka_pipeline::Sha("5f98229".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + } + + #[test] + fn a_finding_defers_to_a_cut_spec() { + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); + committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); + // A spec superseded since is no longer a place to defer to. + let mut mind = world(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + assert_eq!( + refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } + ); + } + + #[test] + fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutReport(cut_report("1", 1))])); + let finding_id = id("finding", "cut-1.s1.F1"); + let confirmed = D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[], Some("P1"), &[])])])), + MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: "Falsified claim".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![ + verdict("1", 1, vec![claim(ClaimOutcome::Unproven, &[&finding_id], Some("P1"), &[])]), + confirmed.clone(), + ])), + MindRefusal::UnprovenClaimWithConfirmedFinding { claim: "Unproven claim".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], None, &[])])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![ + claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), + claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), + ])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + // A promise is measured by the claim that names it, not by any claim + // that names a promise: the report promises P1 and nothing else. + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P2"), &[])])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &["M9"])])])), + MindRefusal::UnknownMutationLabel { label: "M9".into() } + ); + committed(admit(&mut mind, vec![ + verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&finding_id], Some("P1"), &["M1"])]), + confirmed, + ])); + } + + #[test] + fn a_finding_names_evidence_locations_and_known_invariants() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 1)), + D::CutReport(cut_report("1", 1)), + verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])]), + ])); + let mut bare = finding("1", 1, "F1", FindingConfidence::Plausible); + bare.evidence = vec![]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(bare)])), MindRefusal::FindingWithoutEvidence); + // Locations are evidence's other half: a finding that names no place + // in the code is refused the same way. + let mut placeless = finding("1", 1, "F1", FindingConfidence::Plausible); + placeless.locations = vec![]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(placeless)])), MindRefusal::FindingWithoutEvidence); + let mut unknown = finding("1", 1, "F1", FindingConfidence::Plausible); + unknown.invariants = vec![l("nope")]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(unknown)])), MindRefusal::UnknownInvariant { label: "nope".into() }); + + + // A raw envelope without `range` cannot be built from the type, and + // it is the leaf's decode that refuses it, before any organ rule. + #[derive(serde::Serialize)] + struct Rangeless { + campaign: Slug, + verdict: Short, + label: eureka_pipeline::Label, + confidence: FindingConfidence, + severity: eureka_pipeline::FindingSeverity, + claim: Line, + invariants: Vec, + locations: Vec, + failure_scenario: eureka_pipeline::Para, + evidence: Vec, + precedents: Vec, + origin: eureka_pipeline::FindingOrigin, + } + let whole = finding("1", 1, "F1", FindingConfidence::Plausible); + let rangeless = Rangeless { + campaign: whole.campaign.clone(), + verdict: whole.verdict.clone(), + label: whole.label.clone(), + confidence: whole.confidence, + severity: whole.severity, + claim: whole.claim.clone(), + invariants: whole.invariants.clone(), + locations: whole.locations.clone(), + failure_scenario: whole.failure_scenario.clone(), + evidence: whole.evidence.clone(), + precedents: vec![], + origin: whole.origin, + }; + let mut envelope = prepare(&D::Finding(whole)); + envelope.payload = rmp_serde::to_vec_named(&(rangeless,)).unwrap(); + let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Soul), vec![envelope], now()); + assert!( + matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::InvalidFormat { field, .. }) if field == "payload") + ); + + // The in-force target owns the invariant vocabulary. Supersede r1 with + // an r2 that drops its label, and the label is unknown again; r2's own + // label is what a finding may cite. The target is looked for in image + // and batch, like every other citation, so a finding may cite the + // vocabulary of the revision landing beside it: `other` is admitted in + // r2's own batch, before r2 is anywhere but here. + let mut fresh = finding("1", 1, "F4", FindingConfidence::Plausible); + fresh.invariants = vec![l("other")]; + assert_eq!( + refusal(admit(&mut mind, vec![D::Finding(fresh.clone())])), + MindRefusal::UnknownInvariant { label: "other".into() } + ); + committed(admit(&mut mind, vec![ + target(2, &["other"]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + D::Finding(fresh), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![D::Finding(finding("1", 1, "F2", FindingConfidence::Plausible))])), + MindRefusal::UnknownInvariant { label: INVARIANT.into() } + ); + let mut current = finding("1", 1, "F3", FindingConfidence::Plausible); + current.invariants = vec![l("other")]; + committed(admit(&mut mind, vec![D::Finding(current)])); + } + + #[test] + fn a_campaign_needs_no_stewardship() { + let mut mind = opened(MemoryStore::new(), INSTANCE); + committed(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])); + let D::Campaign(mut empty) = campaign(&[]) else { panic!() }; + empty.slug = slug("other"); + assert_eq!( + refusal(admit(&mut mind, vec![D::Campaign(empty)])), + MindRefusal::EmptyRepos { campaign: "other:campaign:self".into() } + ); + let mut spec = cut_spec("1", 1); + spec.repo = repo(OTHER_REPO); + assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + } + + /// A target revision naming repos, and the supersession of its predecessor. + fn repo_revision(revision: u32, repos: &[&str]) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!() }; + named.repos = repos.iter().map(|name| repo(name)).collect(); + let previous = r(K::Target, &id("target", &format!("r{}", revision - 1))); + let this = r(K::Target, &id("target", &format!("r{revision}"))); + vec![D::Target(named), resolution(previous, superseded(&[this]))] + } + + fn spec_on(cut: &str, repo_name: &str) -> D { + let mut spec = cut_spec(cut, 1); + spec.repo = repo(repo_name); + D::CutSpec(spec) + } + + #[test] + fn a_target_revision_adds_a_repo_a_spec_may_name() { + let mut mind = seeded(); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + + // The same revision, supersession and spec in one batch. + let mut fresh = seeded(); + let mut batch = repo_revision(2, &[REPO, OTHER_REPO]); + batch.push(spec_on("1", OTHER_REPO)); + committed(admit(&mut fresh, batch)); + } + + #[test] + fn a_target_revision_removes_a_repo() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + committed(admit(&mut mind, repo_revision(2, &[OTHER_REPO]))); + let spec_id = id("cut_spec", "cut-1.r1"); + let spec_view = mind.view(&r(K::CutSpec, &spec_id)).expect("a valid ref").expect("the spec is held"); + assert_eq!(spec_view.status, crate::query::PipelineStatus::InForce, "spec 1 r1 stays in force"); + committed(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &spec_id), withdrawn())])); + + let mut second = cut_spec("1", 2); + second.repo = repo(REPO); + let revised = id("cut_spec", "cut-1.r2"); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(second), resolution(r(K::CutSpec, &spec_id), superseded(&[r(K::CutSpec, &revised)]))])), + MindRefusal::RepoNotInCampaign { repo: REPO.into() } + ); + committed(admit(&mut mind, vec![spec_on("2", OTHER_REPO)])); + } + + #[test] + fn a_target_revision_cannot_silently_drop_its_repos() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + refusal(admit(&mut mind, repo_revision(3, &[]))), + MindRefusal::EmptyRepos { campaign: id("target", "r3") } + ); + // Nothing was written: the restating revision takes the same key. + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + // An empty revision whose predecessor named none keeps the campaign's set. + let mut plain = seeded(); + committed(admit(&mut plain, repo_revision(2, &[]))); + } + + #[test] + fn a_repo_revision_replays_idempotently() { + let mut mind = seeded(); + let (first, _) = committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO])), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } + ); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + } + + /// Another campaign, opened with `repos`, and its own target revision 1 naming `named`. + fn other_campaign(repos: &[&str], named: &[&str]) -> Vec { + let D::Campaign(mut campaign) = campaign(repos) else { panic!("a campaign") }; + campaign.slug = "other-campaign".into(); + let D::Target(mut target) = target(1, &[INVARIANT]) else { panic!("a target") }; + target.campaign = "other-campaign".into(); + target.repos = named.iter().map(|name| repo(name)).collect(); + vec![D::Campaign(campaign), D::Target(target)] + } + + #[test] + fn another_campaigns_target_does_not_widen_membership() { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[REPO, OTHER_REPO]))); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + } + + #[test] + fn a_target_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let foreign = r(K::Target, "other-campaign:target:r1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[foreign]))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Target, outcome: "Superseded".into() } + ); + // Nothing was written: r2 still names the repos, and its own revision still lands. + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + } + + /// A target of the first campaign (`CAMPAIGN`) at `revision`, naming no repos. + fn own_target(revision: u32) -> D { + target(revision, &[INVARIANT]) + } + + const FOREIGN_TARGET: &str = "other-campaign:target:r1"; + + fn supersession_refused(outcome: PipelineAdmissionOutcome, subject_kind: K) -> bool { + refusal(outcome) == MindRefusal::IncompatibleResolution { subject_kind, outcome: "Superseded".into() } + } + + #[test] + fn a_foreign_target_in_the_same_batch_does_not_supersede() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, FOREIGN_TARGET)]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn every_supersessor_of_a_target_is_checked_whichever_is_foreign() { + for foreign_first in [false, true] { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let own = r(K::Target, &id("target", "r2")); + let foreign = r(K::Target, FOREIGN_TARGET); + let by = if foreign_first { [foreign, own] } else { [own, foreign] }; + let batch = vec![own_target(2), resolution(r(K::Target, &id("target", "r1")), superseded(&by))]; + assert!(supersession_refused(admit(&mut mind, batch), K::Target), "foreign first: {foreign_first}"); + } + } + + #[test] + fn a_new_campaigns_target_in_its_own_batch_is_not_closed_by_a_foreign_one() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, FOREIGN_TARGET), superseded(&[r(K::Target, &id("target", "r1"))]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn a_cut_spec_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + let mut batch = other_campaign(&[REPO], &[]); + let D::CutSpec(mut foreign) = spec_on("1", REPO) else { panic!("a spec") }; + foreign.campaign = "other-campaign".into(); + batch.push(D::CutSpec(foreign)); + committed(admit(&mut mind, batch)); + let lone = vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, "other-campaign:cut_spec:cut-1.r1")]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::CutSpec)); + // Nothing was written: this campaign's own revision still lands. + let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; + next.revision = 2; + let this = r(K::CutSpec, &id("cut_spec", "cut-1.r2")); + committed(admit(&mut mind, vec![D::CutSpec(next), resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[this]))])); + } + + #[test] + fn a_ruling_is_superseded_by_a_ruling_of_any_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + let mut batch = other_campaign(&[REPO], &[]); + let mut foreign = ruling("new"); + foreign.campaign = "other-campaign".into(); + batch.push(D::Ruling(foreign)); + batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, "other-campaign:ruling:new")]))); + committed(admit(&mut mind, batch)); + } + + #[test] + fn a_hand_off_derives_this_minds_side_only() { + let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); + let campaign_key = id("campaign", "self"); + let stewardship_key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + + let mut source = seeded(); + assert_eq!( + refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[])])), + MindRefusal::NotStewarded { repo: OTHER_REPO.into() } + ); + assert_eq!( + refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&id("ruling", "R1")])])), + MindRefusal::MissingReference { kind: K::Ruling, id: id("ruling", "R1") } + ); + let (_, writes) = committed(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); + let withdrawal = id_of(&stewardship_key); + assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Resolution, &withdrawal)]); + let Some(D::Resolution(derived)) = source.get(K::Resolution, &withdrawal).unwrap() else { panic!() }; + assert_eq!(derived.outcome, ResolutionOutcome::Withdrawn { reason: Line(key.clone()) }); + assert_eq!(derived.subject, r(K::Stewardship, &stewardship_key)); + + let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); + let (_, writes) = committed(admit(&mut receiving, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); + let assigned = format!("{OTHER_INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Stewardship, &assigned)]); + let Some(D::Stewardship(derived)) = receiving.get(K::Stewardship, &assigned).unwrap() else { panic!() }; + assert_eq!(derived.note, Line(key.clone())); + assert_eq!(derived.instance, slug(OTHER_INSTANCE)); + // The stewardship starts the day the repo was handed over, not on some + // clock of admission's own. + assert_eq!(derived.assigned_on, date()); + assert_eq!(source.envelope(K::HandOff, &key).map(|e| &e.payload), receiving.envelope(K::HandOff, &key).map(|e| &e.payload)); + + // A derived write passes A3-A7 and A8 like any other, and it counts as + // a record of its scope. A batch carrying its own assignment of the + // repo beside the hand-off is two assignments: the derived one takes + // the sequence after the batch's, and the batch's own is then out of + // sequence -- a typed refusal, not a duplicate identity the store + // complains about. + let mut colliding = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut colliding, vec![instance(OTHER_INSTANCE)])); + assert_eq!( + refusal(admit(&mut colliding, vec![ + hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]), + stewardship(OTHER_INSTANCE, REPO), + ])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 1 } + ); + } + + /// The key of the first resolution of a stewardship: the subject's root, + /// then its kind and local -- the subject's own sequence included -- then + /// this resolution's own sequence. + fn id_of(stewardship_key: &str) -> String { + let (root, rest) = stewardship_key.split_once(':').unwrap(); + format!("{root}:resolution:{}.n1", rest.replace(':', ".")) + } + + /// The second construction site of a receipt, deliberately: a receipt + /// planted through the store must be found by replay and must match. + #[test] + fn a_planted_receipt_with_other_content_is_refused_not_replayed() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; + let ordinal = receipt::head(&mind).unwrap() + 1; + let candidate = + receipt::candidate(&slug(INSTANCE), provenance(Faculty::Hands), &[], &envelopes, ordinal, now()).unwrap(); + let mut planted = HuginnCommitReceipt { writes: vec![], ..candidate.clone() }; + planted.writes = candidate.strong_reads.clone(); + planted.receipt_id = candidate.receipt_id.clone(); + store.plant(mind.cache().prepare_entry_named(&planted.receipt_id, &planted).unwrap().0); + let mut mind = opened(store, INSTANCE); + assert!(matches!( + refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), envelopes, now())), + MindRefusal::Unavailable { .. } + )); + } +} diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index 2ff49b5..bb73776 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -7,7 +7,7 @@ use cultcache_rs::CultCacheEnvelope; use eureka_pipeline::{ - OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, + Label, OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RunOperator, Short, Slug, }; use schemars::JsonSchema; @@ -299,22 +299,57 @@ impl Docs { } } -/// Whether `other` is a later record than `base` in the scope `base` is -/// sequenced in: `(instance, repo)` for a stewardship, the campaign for a -/// target and the cut for a cut spec. Kinds that carry no sequence have no -/// scope and no order, and a resolution is never a base here: the cap above -/// refuses the withdrawal that would reinstate one before the question of a -/// later record arises. +/// The line a record belongs to (ruling `supersession-lineage-2`): the one place +/// lineage is decided. A record supersedes, and is reinstated over, only a record +/// of its own line. `None` is a kind the resolution matrix never lets be +/// superseded; the match names every kind, so a new kind fails to compile until +/// its line is stated. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Lineage<'a> { + /// A target, or a follow-up: the campaign. + Campaign(&'a Slug), + /// A cut spec: the campaign and the cut. + Cut(&'a Slug, &'a Label), + /// A stewardship: the instance and the repo. + Assignment(&'a Slug, &'a OrgRepo), + /// A ruling: operator directions cut across campaigns. + Global, +} + +pub(crate) fn line_of(document: &PipelineDocument) -> Option> { + use PipelineDocument as D; + match document { + D::Target(target) => Some(Lineage::Campaign(&target.campaign)), + D::CutSpec(spec) => Some(Lineage::Cut(&spec.campaign, &spec.cut)), + D::FollowUp(follow_up) => Some(Lineage::Campaign(&follow_up.campaign)), + D::Stewardship(stewardship) => Some(Lineage::Assignment(&stewardship.instance, &stewardship.repo)), + D::Ruling(_) => Some(Lineage::Global), + D::Campaign(_) + | D::Question(_) + | D::CutReport(_) + | D::Verdict(_) + | D::Finding(_) + | D::Resolution(_) + | D::Instance(_) + | D::HandOff(_) + | D::Run(_) => None, + } +} + +/// Whether `other` is a later record than `base` on the line `base` is +/// sequenced in. Kinds that carry no sequence have no order, and a resolution +/// is never a base here: the cap above refuses the withdrawal that would +/// reinstate one before the question of a later record arises. fn later_than(base: &PipelineDocument, other: &PipelineDocument) -> bool { use PipelineDocument as D; + let Some(line) = line_of(base) else { return false }; + if line_of(other) != Some(line) { + return false; + } match (base, other) { - (D::Stewardship(base), D::Stewardship(other)) => { - other.instance == base.instance && other.repo == base.repo && other.sequence > base.sequence - } - (D::Target(base), D::Target(other)) => other.campaign == base.campaign && other.revision > base.revision, - (D::CutSpec(base), D::CutSpec(other)) => { - other.campaign == base.campaign && other.cut == base.cut && other.revision > base.revision - } + (D::Stewardship(base), D::Stewardship(other)) => other.sequence > base.sequence, + (D::Target(base), D::Target(other)) => other.revision > base.revision, + (D::CutSpec(base), D::CutSpec(other)) => other.revision > base.revision, _ => false, } } From e4f8434f615099cb97479eeb8dc42e64c051ac1c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 20:12:10 +0200 Subject: [PATCH 08/12] Supersession tests: every kind on its line, foreign lines refused, prefix-sharing campaigns at revisions above 1 Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 134 ++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 6383e00..7cb1b3d 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -2892,6 +2892,140 @@ mod tests { committed(admit(&mut mind, batch)); } + /// The slug that shares its dash prefix with `CAMPAIGN` (`eureka-state`). + const RIVAL: &str = "eureka-substrate"; + + /// Revision `revision` of the rival campaign's target, with its campaign when it is + /// the first and the supersession of its predecessor when it is not. + fn rival_revision(revision: u32) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!("a target") }; + named.campaign = RIVAL.into(); + let mut batch = Vec::new(); + if revision == 1 { + let D::Campaign(mut opened) = campaign(&[REPO]) else { panic!("a campaign") }; + opened.slug = RIVAL.into(); + batch.push(D::Campaign(opened)); + } + batch.push(D::Target(named)); + if revision > 1 { + let previous = r(K::Target, &format!("{RIVAL}:target:r{}", revision - 1)); + batch.push(resolution(previous, superseded(&[r(K::Target, &format!("{RIVAL}:target:r{revision}"))]))); + } + batch + } + + /// Both campaigns at revision 3, so each one's target in force is above revision 1. + fn both_campaigns_at_revision_three() -> Mind { + let mut mind = seeded(); + for revision in 2..=3 { + committed(admit(&mut mind, repo_revision(revision, &[REPO]))); + } + for revision in 1..=3 { + committed(admit(&mut mind, rival_revision(revision))); + } + mind + } + + #[test] + fn a_target_is_not_superseded_by_a_campaign_sharing_its_prefix_or_a_higher_revision() { + for (subject, supersessor) in [ + (id("target", "r3"), format!("{RIVAL}:target:r3")), + (format!("{RIVAL}:target:r3"), id("target", "r3")), + ] { + let mut mind = both_campaigns_at_revision_three(); + let lone = vec![resolution(r(K::Target, &subject), superseded(&[r(K::Target, &supersessor)]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Target), "{subject} by {supersessor}"); + } + // A foreign revision above the subject's is no likelier to pass. + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO]))); + for revision in 1..=4 { + committed(admit(&mut mind, rival_revision(revision))); + } + let lone = vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[r(K::Target, &format!("{RIVAL}:target:r4"))]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Target)); + } + + #[test] + fn a_stewardship_is_superseded_only_on_its_own_instance_and_repo_line() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![stewardship(INSTANCE, OTHER_REPO)])); + let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + let huginn = format!("{INSTANCE}:stewardship:gamecult_-huginn.n1"); + let lone = vec![resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &huginn)]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Stewardship)); + // Nothing was written: the repo's own next assignment still supersedes it. + let next = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + committed(admit(&mut mind, vec![ + stewardship_n(INSTANCE, REPO, 2), + resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &next)])), + ])); + } + + #[test] + fn a_follow_up_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + let source = r(K::Target, &id("target", "r1")); + committed(admit(&mut mind, vec![follow_up("old", source.clone())])); + let mut batch = rival_revision(1); + let D::FollowUp(mut foreign) = follow_up("new", source.clone()) else { panic!("a follow-up") }; + foreign.campaign = RIVAL.into(); + batch.push(D::FollowUp(foreign)); + committed(admit(&mut mind, batch)); + let lone = vec![resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &format!("{RIVAL}:follow_up:new"))]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::FollowUp)); + // Nothing was written: this campaign's own follow-up still supersedes it. + committed(admit(&mut mind, vec![ + follow_up("own", source), + resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "own"))])), + ])); + } + + #[test] + fn supersession_on_its_own_line_commits_for_every_kind() { + let source = r(K::Target, &id("target", "r1")); + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO]))); + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; + next.revision = 2; + committed(admit(&mut mind, vec![ + D::CutSpec(next), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + let mut mind = seeded(); + committed(admit(&mut mind, vec![follow_up("old", source.clone())])); + committed(admit(&mut mind, vec![ + follow_up("new", source), + resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "new"))])), + ])); + let mut mind = seeded(); + let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany"); + committed(admit(&mut mind, vec![ + stewardship_n(INSTANCE, REPO, 2), + resolution(r(K::Stewardship, &format!("{epiphany}.n1")), superseded(&[r(K::Stewardship, &format!("{epiphany}.n2"))])), + ])); + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("new")), + resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &id("ruling", "new"))])), + ])); + } + + #[test] + fn a_ruling_of_one_campaign_is_superseded_by_a_ruling_of_a_prefix_sharing_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + let mut batch = rival_revision(1); + let mut foreign = ruling("new"); + foreign.campaign = RIVAL.into(); + batch.push(D::Ruling(foreign)); + batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &format!("{RIVAL}:ruling:new"))]))); + committed(admit(&mut mind, batch)); + } + #[test] fn a_hand_off_derives_this_minds_side_only() { let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); From d289fcb09ab353a3e5cc5cfd7283ffd1201c85a0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 20:44:51 +0200 Subject: [PATCH 09/12] Restore LF line endings in admission.rs and docs.rs (my edits wrote CRLF) Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 6220 +++++++++++++-------------- 1 file changed, 3110 insertions(+), 3110 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 7cb1b3d..28b9a03 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -1,3110 +1,3110 @@ -//! Admission: the single door into a mind. -//! -//! `admit` prepares typed documents through the leaf and calls -//! `admit_prepared`, the one commit path for Cut 8, 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: -//! -//! - A1 the batch names this mind, and attributes itself to a named agent and -//! session (`refuse_provenance`); A2 one to sixty-four envelopes; -//! - A3 every envelope passes the leaf's bounds, formats and key -//! recomputation; A4 identities are unique in the batch; A5 every document -//! carrying an instance names this mind; -//! - A6 an empty mind's first batch carries exactly one `instance` document, -//! and a batch carrying one derives the epoch record; -//! - A7 every reference names a document present in the image or the batch, -//! looked up by `(kind, id)`; the ones resolved in the image become the -//! receipt's strong reads; -//! - the derived writes (a ruling's `Answered` resolution, a hand-off's -//! stewardship withdrawal or assignment) are appended and pass A3-A7; -//! - A8 the per-kind rules, below; A9 an exact replay answers with the stored -//! receipt, after validation and never before it (ruling 20); A10 no write -//! collides with the image; A11 the commit. -//! -//! Every cross-field and cross-document rule lives here and nowhere else: -//! the leaf never gains one, the daemon and the client never re-derive one. -//! In force, the sequences and the scopes are `docs.rs`'s, shared with the -//! views (Cut 9). - -use std::collections::BTreeSet; - -use chrono::{DateTime, Utc}; -use cultcache_rs::{CultCache, CultCacheEnvelope}; -use eureka_pipeline::{ - ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal, - PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key, - validate_pipeline_write_envelope, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::docs::{CitationRole, Docs, Staged, citations, kind_of_id, line_of, outcome_citations}; -use crate::mind::{HuginnMindEpoch, Mind, unavailable}; -use crate::persona::{self, HuginnPersonaEntry, PersonaPutOutcome}; -use crate::receipt::{self, CommitOutcome, PipelineProvenance}; -use crate::refusal::{MindRefusal, RunId}; -use crate::store::MindStore; - -/// The most envelopes one batch may carry. -pub const BATCH_MAX: usize = 64; - -/// One admission request: the mind it is for, who asks, and the documents. -/// Rides the wire whole (`wire::HuginnMindRequest::Admit`). -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct PipelineAdmissionBatch { - pub instance: Slug, - pub provenance: PipelineProvenance, - pub documents: Vec, -} - -/// How admission ended. `Committed.writes` names every document the batch -/// landed, derived writes included; Cut 11 indexes from it after `admit` -/// returns, and never inside it. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub enum PipelineAdmissionOutcome { - Committed { receipt_id: String, committed_at: String, writes: Vec }, - AlreadyAdmitted { receipt_id: String }, - Refused(MindRefusal), - Conflict { identities: Vec }, -} - -/// Keys a document through the leaf, then encodes it. A key the leaf cannot -/// compose is the document's fault and is refused as a document; only the -/// cache's encode fault is the organ's. -fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result { - pipeline_key(document).map_err(MindRefusal::Document)?; - document.prepare(cache).map_err(unavailable) -} - -impl Mind { - /// Validates and prepares every document through the leaf, then admits - /// the envelopes. - pub fn admit(&mut self, batch: PipelineAdmissionBatch, now: DateTime) -> PipelineAdmissionOutcome { - let mut envelopes = Vec::with_capacity(batch.documents.len()); - for document in &batch.documents { - if let Err(refusal) = document.validate() { - return PipelineAdmissionOutcome::Refused(MindRefusal::Document(refusal)); - } - match prepare(document, self.cache()) { - Ok(envelope) => envelopes.push(envelope), - Err(refusal) => return PipelineAdmissionOutcome::Refused(refusal), - } - } - self.admit_prepared(&batch.instance, batch.provenance, envelopes, now) - } - - /// The single commit path: envelopes prepared by anyone, validated here - /// whoever prepared them. - pub fn admit_prepared( - &mut self, - instance: &Slug, - provenance: PipelineProvenance, - envelopes: Vec, - now: DateTime, - ) -> PipelineAdmissionOutcome { - match self.admit_steps(instance, provenance, envelopes, now) { - Ok(outcome) => outcome, - Err(refusal) => PipelineAdmissionOutcome::Refused(refusal), - } - } - - fn admit_steps( - &mut self, - instance: &Slug, - provenance: PipelineProvenance, - envelopes: Vec, - now: DateTime, - ) -> Result { - // A1 - self.require_instance(instance)?; - refuse_provenance(&provenance)?; - // A2 - if envelopes.is_empty() || envelopes.len() > BATCH_MAX { - return Err(MindRefusal::BatchSize { actual: envelopes.len() as u32 }); - } - let mind = self.instance().clone(); - let mut docs = Docs::from_image(self.envelopes())?; - // A3, A5, then A4 on push. - for envelope in envelopes { - docs.push(stage(envelope, &mind)?)?; - } - // A6 - let carries_instance = docs.batch.iter().filter(|staged| staged.kind == PipelineKind::Instance).count() == 1; - if self.is_empty() && !carries_instance { - return Err(MindRefusal::MissingIdentity); - } - // A7 - let mut strong = BTreeSet::new(); - for staged in &docs.batch { - resolve(&docs, staged, &mind, &mut strong)?; - } - // Derived writes pass A3-A7 themselves. - let originals = docs.batch.len(); - for document in derive(&docs, &mind) { - document.validate()?; - let envelope = prepare(&document, self.cache())?; - docs.push(stage(envelope, &mind)?)?; - } - for staged in &docs.batch[originals..] { - resolve(&docs, staged, &mind, &mut strong)?; - } - // The pins: cited image bytes, and every write. - let strong_reads = strong - .iter() - .filter_map(|(type_id, key)| self.raw_envelope(type_id, key).cloned()) - .collect::>(); - let mut writes = docs.batch.iter().map(|staged| staged.envelope.clone()).collect::>(); - if carries_instance { - writes.push(HuginnMindEpoch::envelope(self.cache())?); - } - let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; - // A8 - for staged in &docs.batch { - check(&docs, staged, &mind)?; - } - // A9 - if let Some(existing) = receipt::replay(self, &candidate)? { - return Ok(PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: existing.receipt_id }); - } - // A10 - refuse_collisions(&docs)?; - // A11 - let landed = docs.batch.iter().map(|staged| PipelineRef { kind: staged.kind, id: Short(staged.key.clone()) }).collect(); - match receipt::commit(self, candidate, strong_reads, writes)? { - CommitOutcome::Committed(receipt) => Ok(PipelineAdmissionOutcome::Committed { - receipt_id: receipt.receipt_id, - committed_at: receipt.committed_at, - writes: landed, - }), - CommitOutcome::Conflict(identities) => Ok(PipelineAdmissionOutcome::Conflict { identities }), - } - } - - /// Replaces the mind's persona document whole, or refuses. One door for - /// the one document a mind holds, outside every pipeline batch: the same - /// instance and provenance checks as `admit`, then the persona's five - /// rules in order (schema-valid, `personaId` is this mind, canonical - /// provenance, `expected_updated_at` is the stored `updatedAt`, the new - /// `updatedAt` is later), then the same commit primitive. The stored - /// document is the commit's strong read, so the swap fails if it changed. - pub fn put_persona( - &mut self, - instance: &Slug, - state: Value, - expected_updated_at: Option, - provenance: PipelineProvenance, - now: DateTime, - ) -> PersonaPutOutcome { - match self.persona_steps(instance, state, expected_updated_at, provenance, now) { - Ok(receipt_id) => PersonaPutOutcome::Committed { receipt_id }, - Err(refusal) => PersonaPutOutcome::Refused(refusal), - } - } - - fn persona_steps( - &mut self, - instance: &Slug, - state: Value, - expected_updated_at: Option, - provenance: PipelineProvenance, - now: DateTime, - ) -> Result { - self.require_instance(instance)?; - refuse_provenance(&provenance)?; - // The persona rides a mind that has its identity (A6's analogue). - if self.is_empty() { - return Err(MindRefusal::MissingIdentity); - } - // P1 - persona::validate_shape(&state)?; - // P2 - let mind = self.instance().clone(); - if state.get("personaId").and_then(Value::as_str) != Some(mind.0.as_str()) { - return Err(MindRefusal::PersonaForeign { instance: mind.0 }); - } - // P3 - if state.pointer("/provenance/authority").and_then(Value::as_str) != Some("canonical") { - return Err(MindRefusal::PersonaNotCanonical); - } - // P4 - let stored = self.stored_updated_at()?; - if stored != expected_updated_at { - return Err(MindRefusal::PersonaStale { stored }); - } - // P5 - if let Some(stored) = &stored { - let instant = |stamp: &str| { - DateTime::parse_from_rfc3339(stamp).map_err(|_| MindRefusal::PersonaInvalid { - path: "/updatedAt".into(), - message: "is not an RFC 3339 date-time".into(), - }) - }; - let offered = state.get("updatedAt").and_then(Value::as_str).map(instant).transpose()?; - if offered <= Some(instant(stored)?) { - return Err(MindRefusal::PersonaInvalid { - path: "/updatedAt".into(), - message: "must be later than the stored updatedAt".into(), - }); - } - } - let (envelope, _) = self - .cache() - .prepare_entry_named(mind.0.clone(), &HuginnPersonaEntry { value: state }) - .map_err(unavailable)?; - let strong_reads = self.persona_envelope().cloned().into_iter().collect::>(); - let writes = vec![envelope]; - let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; - match receipt::commit(self, candidate, strong_reads, writes)? { - CommitOutcome::Committed(receipt) => Ok(receipt.receipt_id), - CommitOutcome::Conflict(_) => Err(MindRefusal::PersonaStale { stored: self.stored_updated_at()? }), - } - } -} - -/// The bound `eureka_pipeline::Short` declares, in UTF-8 bytes. The leaf -/// exposes no validator for it, so `short_bound_is_the_leafs_own` pins this to -/// the `maxLength` its schema publishes. -const SHORT_MAX: usize = 200; - -/// Who admitted a batch is the receipt's only record of it, so the mind checks -/// it here and every writer, whatever its transport, gets the check: the agent, -/// the session and the tool follow the leaf's `Title` rule (one alphanumeric -/// character, no control or line-break character, no bidi control) within -/// `Short`'s bound. The leaf keeps that rule private, so -/// `provenance_text_is_the_leafs_title_rule` pins this to it. Refused as the -/// leaf's own refusals, since the fields are the leaf's own type. -fn refuse_provenance(provenance: &PipelineProvenance) -> Result<(), MindRefusal> { - let bidi = |c: char| { - matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') - }; - let control = |c: char| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'); - for (field, value) in [ - ("provenance.agent", &provenance.agent), - ("provenance.session", &provenance.session), - ("provenance.tool", &provenance.tool), - ] { - let text = value.0.as_str(); - if !text.chars().any(char::is_alphanumeric) || text.contains(control) || text.contains(bidi) { - return Err(PipelineRefusal::InvalidFormat { field: field.into(), value: text.into() }.into()); - } - if text.len() > SHORT_MAX { - return Err(PipelineRefusal::FieldBound { field: field.into(), limit: SHORT_MAX as u32, actual: text.len() as u32 }.into()); - } - } - Ok(()) -} - -/// A3 through the leaf, then A5. -fn stage(envelope: CultCacheEnvelope, mind: &Slug) -> Result { - validate_pipeline_write_envelope(&envelope)?; - let document = PipelineDocument::decode(&envelope)?; - refuse_foreign_instance(&document, mind)?; - Ok(Staged { kind: document.kind(), key: envelope.key.clone(), document, envelope }) -} - -/// A5: every document carrying an instance field names this mind (a run -/// included); a hand-off names it on one side. -fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(), MindRefusal> { - let foreign = |declared: &Slug| MindRefusal::ForeignInstance { declared: declared.0.clone(), mind: mind.0.clone() }; - match document { - PipelineDocument::Instance(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)), - PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => { - Err(foreign(&value.from_instance)) - } - _ => Ok(()), - } -} - -/// What a missing referent is called, by the field that cited it. -enum Missing { - Reference, - SpecOfReport(String), - Supersessor, -} - -struct Reference { - kind: PipelineKind, - id: String, - missing: Missing, -} - -/// A7's field list is `docs::citations`: every `PipelineRef` and every -/// full-id `Short` field, each carrying the refusal its absence earns. A -/// hand-off's documents are cited on the source side only, and each must read -/// as an id of a kind; the receiving side imports them afterwards (Cut 12). -fn references(staged: &Staged, mind: &Slug) -> Result, MindRefusal> { - if let PipelineDocument::HandOff(hand_off) = &staged.document { - if hand_off.from_instance != *mind { - return Ok(Vec::new()); - } - if let Some(document) = hand_off.documents.iter().find(|document| kind_of_id(&document.0).is_none()) { - return Err(PipelineRefusal::InvalidFormat { - field: "hand_off.documents".into(), - value: document.0.clone(), - } - .into()); - } - } - Ok(citations(&staged.document) - .into_iter() - .map(|(role, target)| Reference { - kind: target.kind, - id: target.id.0, - missing: match role { - CitationRole::CutSpec => Missing::SpecOfReport(staged.key.clone()), - CitationRole::SupersededBy => Missing::Supersessor, - _ => Missing::Reference, - }, - }) - .collect()) -} - -/// A7: every referent is in the batch or the image; the image ones are the -/// receipt's strong reads. -fn resolve(docs: &Docs, staged: &Staged, mind: &Slug, strong: &mut BTreeSet<(String, String)>) -> Result<(), MindRefusal> { - for reference in references(staged, mind)? { - if docs.in_batch(reference.kind, &reference.id).is_some() { - continue; - } - if docs.in_image(reference.kind, &reference.id).is_some() { - strong.insert((reference.kind.type_id().to_string(), reference.id)); - continue; - } - return Err(match reference.missing { - Missing::Reference => MindRefusal::MissingReference { kind: reference.kind, id: reference.id }, - Missing::SpecOfReport(report) => MindRefusal::CutReportWithoutSpec { report }, - Missing::Supersessor => MindRefusal::UnknownSupersessor { id: reference.id }, - }); - } - Ok(()) -} - -/// The writes admission derives from the batch: 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. One the batch already carries is not -/// derived again. -fn derive(docs: &Docs, mind: &Slug) -> Vec { - use PipelineDocument as D; - use PipelineKind as K; - let mut derived = Vec::new(); - for staged in &docs.batch { - match &staged.document { - D::Ruling(ruling) => { - if let Some(question) = &ruling.answers { - let subject = PipelineRef { kind: K::Question, id: question.clone() }; - let sequence = docs.derived_resolution_sequence(&subject, |outcome| { - matches!(outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == staged.key) - }); - derived.push(D::Resolution(PipelineResolution { - sequence, - subject, - outcome: ResolutionOutcome::Answered { by: PipelineRef { kind: K::Ruling, id: Short(staged.key.clone()) } }, - rationale: ruling.ruling.clone(), - resolved_on: ruling.ruled_on.clone(), - })); - } - } - D::HandOff(hand_off) => { - if hand_off.from_instance == *mind - && let Some((stewardship_key, _)) = docs.stewardship_of(mind, &hand_off.repo, Some(&staged.key)) - { - let subject = PipelineRef { kind: K::Stewardship, id: Short(stewardship_key.to_string()) }; - let sequence = docs.derived_resolution_sequence(&subject, |outcome| { - matches!(outcome, ResolutionOutcome::Withdrawn { reason } if reason.0 == staged.key) - }); - derived.push(D::Resolution(PipelineResolution { - sequence, - subject, - outcome: ResolutionOutcome::Withdrawn { reason: Line(staged.key.clone()) }, - rationale: hand_off.reason.clone(), - resolved_on: hand_off.handed_on.clone(), - })); - } - if hand_off.to_instance == *mind { - derived.push(D::Stewardship(PipelineStewardship { - instance: mind.clone(), - sequence: docs.derived_stewardship_sequence(mind, &hand_off.repo, &staged.key), - repo: hand_off.repo.clone(), - assigned_on: hand_off.handed_on.clone(), - note: Line(staged.key.clone()), - })); - } - } - _ => {} - } - } - derived.retain(|document| !docs.batch.iter().any(|staged| staged.document == *document)); - derived -} - -/// A8: the per-kind rules. -fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { - use PipelineDocument as D; - use PipelineKind as K; - let key = staged.key.as_str(); - match &staged.document { - D::Campaign(campaign) => { - if campaign.repos.is_empty() { - return Err(MindRefusal::EmptyRepos { campaign: key.into() }); - } - Ok(()) - } - D::Target(target) => { - let predecessor = D::Target(eureka_pipeline::PipelineTarget { revision: target.revision.wrapping_sub(1), ..target.clone() }); - revision_rule(docs, K::Target, key, target.revision, &predecessor)?; - // A revision past the first may not empty the repo set its predecessor - // named: empty means "keep the previous set", and that would silently - // narrow it to the campaign's opening set (ruling campaign-repo-set-home). - if target.repos.is_empty() && target.revision > 1 { - let named = matches!(docs.find(K::Target, &pipeline_key(&predecessor)?), Some(D::Target(previous)) if !previous.repos.is_empty()); - if named { - return Err(MindRefusal::EmptyRepos { campaign: key.into() }); - } - } - unique_labels("target.invariants", target.invariants.iter().map(|invariant| invariant.label.0.as_str())) - } - D::Question(question) => { - if question.options.len() < 2 || !question.options.iter().any(|option| option.label == question.recommended) { - return Err(MindRefusal::InvalidOptions { question: key.into() }); - } - unique_labels("question.options", question.options.iter().map(|option| option.label.0.as_str())) - } - D::Ruling(ruling) => { - if ruling.operator_quote.is_some() && ruling.authority != RulingAuthority::Operator { - return Err(MindRefusal::QuoteWithoutOperator { ruling: key.into() }); - } - let Some(question_id) = &ruling.answers else { return Ok(()) }; - let Some(D::Question(question)) = docs.find(K::Question, &question_id.0) else { - return Err(MindRefusal::MissingReference { kind: K::Question, id: question_id.0.clone() }); - }; - let own = |resolution: &PipelineResolution| { - matches!(&resolution.outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == key) - }; - if !docs.in_force_unless(K::Question, &question_id.0, own) { - return Err(MindRefusal::AlreadyResolved { subject: question_id.0.clone() }); - } - let choice = ruling.choice.as_ref().map_or("", |choice| choice.0.as_str()); - if !question.options.iter().any(|option| option.label.0 == choice) { - return Err(MindRefusal::InvalidChoice { ruling: key.into(), choice: choice.into() }); - } - Ok(()) - } - D::CutSpec(spec) => { - let campaign = docs.of_kind(K::Campaign).find_map(|(_, document)| match document { - D::Campaign(campaign) if campaign.slug == spec.campaign => Some(campaign), - _ => None, - }); - let Some(campaign) = campaign else { - return Err(MindRefusal::MissingReference { kind: K::Campaign, id: spec.campaign.0.clone() }); - }; - if !docs.repos_in_force(campaign).contains(&spec.repo) { - return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() }); - } - for ruling in &spec.rulings { - if !docs.in_force(K::Ruling, &ruling.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); - } - } - for dependency in &spec.depends_on { - // A cut naming itself is no ordering (ruling self-dep). - if dependency.0 == spec.cut.0 { - return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); - } - let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { - matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) - }); - if !names_a_cut { - return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); - } - } - let predecessor = D::CutSpec(eureka_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); - revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) - } - D::CutReport(report) => { - let Some(D::CutSpec(spec)) = docs.find(K::CutSpec, &report.cut_spec.0) else { - return Err(MindRefusal::CutReportWithoutSpec { report: key.into() }); - }; - if !docs.in_force(K::CutSpec, &report.cut_spec.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: report.cut_spec.0.clone() }); - } - if report.repo != spec.repo { - return Err(MindRefusal::SpecMismatch { field: "repo".into() }); - } - if report.branch != spec.branch { - return Err(MindRefusal::SpecMismatch { field: "branch".into() }); - } - if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { - return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); - } - Ok(()) - } - D::Verdict(verdict) => { - let Some(D::CutReport(report)) = docs.find(K::CutReport, &verdict.cut_report.0) else { - return Err(MindRefusal::MissingReference { kind: K::CutReport, id: verdict.cut_report.0.clone() }); - }; - for claim in &verdict.claims { - let confirmed = claim.findings.iter().filter_map(|finding| docs.find(K::Finding, &finding.0)).any(|document| { - matches!(document, D::Finding(finding) if finding.confidence == FindingConfidence::Confirmed) - }); - match claim.outcome { - ClaimOutcome::Falsified if !confirmed => { - return Err(MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: claim.claim.0.clone() }); - } - ClaimOutcome::Unproven if confirmed => { - return Err(MindRefusal::UnprovenClaimWithConfirmedFinding { claim: claim.claim.0.clone() }); - } - _ => {} - } - for label in &claim.mutations { - if !report.mutations.iter().any(|mutation| mutation.label == *label) { - return Err(MindRefusal::UnknownMutationLabel { label: label.0.clone() }); - } - } - } - for promise in &report.promises { - let count = verdict.claims.iter().filter(|claim| claim.promise.as_ref() == Some(&promise.label)).count(); - if count != 1 { - return Err(MindRefusal::PromiseWithoutVerdict { promise: promise.label.0.clone() }); - } - } - Ok(()) - } - D::Finding(finding) => { - if finding.evidence.is_empty() || finding.locations.is_empty() { - return Err(MindRefusal::FindingWithoutEvidence); - } - let target = docs.of_kind(K::Target).find_map(|(target_key, document)| match document { - D::Target(target) if target.campaign == finding.campaign && docs.in_force(K::Target, target_key) => Some(target), - _ => None, - }); - for label in &finding.invariants { - if !target.is_some_and(|target| target.invariants.iter().any(|invariant| invariant.label == *label)) { - return Err(MindRefusal::UnknownInvariant { label: label.0.clone() }); - } - } - Ok(()) - } - D::Resolution(resolution) => resolution_rule(docs, resolution), - D::Stewardship(stewardship) => { - let expected = docs.latest_stewardship(&stewardship.instance, &stewardship.repo, Some(stewardship)) + 1; - if stewardship.sequence != expected { - return Err(MindRefusal::StewardshipOutOfSequence { - repo: stewardship.repo.0.clone(), - expected, - actual: stewardship.sequence, - }); - } - if docs.stewardships_of(mind, &stewardship.repo, None).iter().any(|(other, _)| *other != key) { - return Err(MindRefusal::AlreadyStewarded { repo: stewardship.repo.0.clone() }); - } - Ok(()) - } - D::HandOff(hand_off) => { - if hand_off.from_instance == *mind && docs.stewardship_of(mind, &hand_off.repo, Some(key)).is_none() { - return Err(MindRefusal::NotStewarded { repo: hand_off.repo.0.clone() }); - } - Ok(()) - } - // run-is-the-grant: the claim is the one consumption fact. A claim must - // be in force, and no other run in force may hold it. A run already in - // the image under this key, byte for byte, is a replay and owes its - // claims no fresh freedom, so the replay still answers AlreadyAdmitted - // after the work it claimed has moved on. One live run of hers - // per instance and turn (ruling one-live-self-run) is decided by the same - // rule. - D::Run(run) => { - let replay = docs.in_image(K::Run, key) == Some(&staged.document); - run_rule(docs, key, run, replay) - } - D::FollowUp(_) | D::Instance(_) => Ok(()), - } -} - -/// The one rule for a run opening or reinstated by withdrawing its close: every -/// claim in force (a replay of an admitted run owes none) and held by no other -/// live run, then at most one live run of hers per instance and turn. -fn run_rule(docs: &Docs, run_key: &str, run: &PipelineRun, replay: bool) -> Result<(), MindRefusal> { - for claim in &run.claims { - if !replay && !docs.in_force(claim.kind, &claim.id.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: claim.kind, id: claim.id.0.clone() }); - } - if let Some(holder) = docs.claim_holder(run_key, claim) { - return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: RunId::held(holder) }); - } - } - if !replay && docs.in_force(PipelineKind::Run, run_key) && let Some(holder) = docs.live_holder(run_key, run) { - return Err(MindRefusal::AlreadyLive { run: RunId::held(holder) }); - } - Ok(()) -} - -/// Revision 1 stands alone; revision N needs a resolution in the batch that -/// supersedes revision N-1 by this document. The predecessor's key is the -/// leaf's, from the same document one revision back. -fn revision_rule(docs: &Docs, kind: PipelineKind, key: &str, revision: u32, predecessor: &PipelineDocument) -> Result<(), MindRefusal> { - if revision == 1 { - return Ok(()); - } - let refusal = || MindRefusal::RevisionWithoutSupersession { kind, revision }; - if revision == 0 { - return Err(refusal()); - } - let predecessor_key = pipeline_key(predecessor)?; - let superseded = docs.batch.iter().any(|staged| { - matches!(&staged.document, PipelineDocument::Resolution(resolution) - if resolution.subject.kind == kind - && resolution.subject.id.0 == predecessor_key - && matches!(&resolution.outcome, ResolutionOutcome::Superseded { by } - if by.iter().any(|supersessor| supersessor.kind == kind && supersessor.id.0 == key))) - }); - if superseded { Ok(()) } else { Err(refusal()) } -} - -fn unique_labels<'a>(field: &str, labels: impl Iterator) -> Result<(), MindRefusal> { - let mut seen = BTreeSet::new(); - for label in labels { - if !seen.insert(label) { - return Err(MindRefusal::DuplicateLabel { field: field.into(), label: label.into() }); - } - } - Ok(()) -} - -fn outcome_name(outcome: &ResolutionOutcome) -> &'static str { - match outcome { - ResolutionOutcome::Superseded { .. } => "Superseded", - ResolutionOutcome::Answered { .. } => "Answered", - ResolutionOutcome::Fixed { .. } => "Fixed", - ResolutionOutcome::Deferred { .. } => "Deferred", - ResolutionOutcome::Recorded { .. } => "Recorded", - ResolutionOutcome::Withdrawn { .. } => "Withdrawn", - } -} - -/// The resolution matrix: which outcomes a subject kind admits, and of which -/// kinds its referents must be. Admission's, never the leaf's. -/// -/// A resolution is itself resolvable, by withdrawal alone: the operator's -/// ruling on Q17 keeps a withdrawn resolution attached to its subject rather -/// than erasing it, and a subject whose resolution is withdrawn may be -/// resolved again at the next sequence, which the leaf's key now carries. The -/// chain stops there: Q19 A caps it at depth two, in `resolution_rule`, since -/// a withdrawal that could itself be withdrawn would leave two closures -/// standing over one subject. -fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { - use PipelineKind as K; - use ResolutionOutcome as O; - let all = |by: &[PipelineRef], kind: K| by.iter().all(|supersessor| supersessor.kind == kind); - match (subject, outcome) { - (K::Target, O::Superseded { by }) => all(by, K::Target), - (K::Question, O::Answered { by }) => by.kind == K::Ruling, - (K::Question, O::Withdrawn { .. }) => true, - (K::Ruling, O::Superseded { by }) => all(by, K::Ruling), - (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), - (K::CutSpec, O::Withdrawn { .. }) => true, - (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), - (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, - (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), - (K::FollowUp, O::Withdrawn { .. }) => true, - (K::Stewardship, O::Superseded { by }) => all(by, K::Stewardship), - (K::Stewardship, O::Withdrawn { .. }) => true, - (K::Run, O::Recorded { .. } | O::Withdrawn { .. }) => true, - (K::Resolution, O::Withdrawn { .. }) => true, - _ => false, - } -} - -/// The resolution row: the sequence, the in-force subject, the matrix, the -/// Q19 cap and the reinstatement rule it rests on, a non-empty supersession, -/// every referent in force, and the two -/// coherence rules (an `Answered` ruling answers this question; a cut spec is -/// superseded within its cut). -fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), MindRefusal> { - use PipelineDocument as D; - use PipelineKind as K; - let subject_kind = resolution.subject.kind; - let subject_id = resolution.subject.id.0.as_str(); - if let ResolutionOutcome::Superseded { by } = &resolution.outcome && by.is_empty() { - return Err(MindRefusal::EmptySupersession); - } - // The sequence is the writer's and is checked against the image, as a - // revision is: the previous plus one, counting every record of this - // subject other than this document itself. - let expected = docs.latest_resolution(&resolution.subject, Some(resolution)) + 1; - if resolution.sequence != expected { - return Err(MindRefusal::ResolutionOutOfSequence { - subject: subject_id.into(), - expected, - actual: resolution.sequence, - }); - } - // A8: a subject with a resolution still in force is closed. A withdrawn - // one is not in force, so the subject is open and this is its next record. - if !docs.in_force_unless(subject_kind, subject_id, |other| other == resolution) { - return Err(MindRefusal::AlreadyResolved { subject: subject_id.into() }); - } - let incompatible = || MindRefusal::IncompatibleResolution { subject_kind, outcome: outcome_name(&resolution.outcome).into() }; - if !matrix(subject_kind, &resolution.outcome) { - return Err(incompatible()); - } - // Q19 A: the chain stops at depth two. A withdrawal cannot be withdrawn; - // to reinstate a closure, resolve the subject again at the next sequence. - if subject_kind == K::Resolution - && let Some(D::Resolution(subject)) = docs.find(K::Resolution, subject_id) - && subject.subject.kind == K::Resolution - { - return Err(incompatible()); - } - // Q19 A's rationale one step out from the cap: a withdrawal never - // re-raises an earlier record over a later one. Withdrawing a resolution - // puts its subject back in force, so refuse while a later record of that - // subject's own scope stands -- a repo's next assignment, a document's - // next revision. - if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) - && subject_kind == K::Resolution - && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) - && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) - && let Some(later) = docs.later_in_force(base) - { - return Err(MindRefusal::WouldReinstateOverLater { - subject: reinstating.subject.id.0.clone(), - later: later.into(), - }); - } - // Withdrawing a run's closure puts the run back in force, and a run in - // force holds its claims and its turn's slot: refuse while another run in force - // holds one of the claims or another run of hers of its turn is live. - if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) - && subject_kind == K::Resolution - && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) - && reinstating.subject.kind == K::Run - && let Some(D::Run(run)) = docs.find(K::Run, &reinstating.subject.id.0) - { - run_rule(docs, &reinstating.subject.id.0, run, false)?; - } - for (_, referent) in outcome_citations(&resolution.outcome) { - if !docs.in_force(referent.kind, &referent.id.0) { - return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 }); - } - } - match &resolution.outcome { - ResolutionOutcome::Superseded { by } => { - let Some(subject) = docs.find(subject_kind, subject_id) else { return Ok(()) }; - let line = line_of(subject); - for supersessor in by { - if let Some(next) = docs.find(subject_kind, &supersessor.id.0) - && (line.is_none() || line != line_of(next)) - { - return Err(incompatible()); - } - } - } - ResolutionOutcome::Answered { by } => { - let Some(D::Ruling(ruling)) = docs.find(K::Ruling, &by.id.0) else { return Ok(()) }; - if ruling.answers.as_ref().map(|answers| answers.0.as_str()) != Some(subject_id) { - return Err(incompatible()); - } - } - _ => {} - } - Ok(()) -} - -/// A10: a write whose identity the image already holds. Every kind answers -/// the same way now that a resolution key carries a sequence: the identity is -/// taken. Whether a subject is closed is A8's question, not this one's. -fn refuse_collisions(docs: &Docs) -> Result<(), MindRefusal> { - for staged in &docs.batch { - if docs.in_image(staged.kind, &staged.key).is_some() { - return Err(MindRefusal::IdentityCollision { kind: staged.kind, id: staged.key.clone() }); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::fixtures::*; - use crate::fixtures::prepare; - use crate::mind::schema_cache; - use crate::receipt::{DocumentVersion, Faculty, HuginnCommitReceipt}; - use crate::store::test_stores::{MemoryStore, RefusingStore, SwapCommand}; - use cultcache_rs::DatabaseEntry; - use eureka_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn}; - use sha2::{Digest, Sha256}; - - fn receipt_count(mind: &Mind) -> usize { - mind.receipts().unwrap().len() - } - - #[test] - fn an_empty_store_opens_and_the_first_write_must_carry_the_instance() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - assert!(mind.is_empty()); - assert_eq!(refusal(admit(&mut mind, vec![stewardship(INSTANCE, REPO), campaign(&[REPO])])), MindRefusal::MissingIdentity); - assert!(store.rows().is_empty()); - let (receipt_id, writes) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - assert!(writes.contains(&r(K::Instance, &format!("{INSTANCE}:instance:self")))); - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts.len(), 1); - assert_eq!(receipts[0].receipt_id, receipt_id); - let identities = receipts[0].writes.iter().map(DocumentVersion::identity).collect::>(); - assert!(identities.contains(&(K::Instance.type_id(), format!("{INSTANCE}:instance:self").as_str()))); - assert!(identities.contains(&(HuginnMindEpoch::TYPE, PIPELINE_SCHEMA_EPOCH)), "the epoch record is derived: {identities:?}"); - assert!(receipts[0].strong_reads.is_empty()); - // The store now opens again as this instance, and as nothing else. - assert!(!Mind::open_with(store.clone(), &slug(INSTANCE)).unwrap().is_empty()); - assert!(Mind::open_with(store, &slug(OTHER_INSTANCE)).is_err()); - } - - /// The identity the index labels its collection with: none before the - /// first write, then the first receipt's id and never a later one. - #[test] - fn the_genesis_receipt_id_is_the_first_admissions_and_stays_put() { - let mut mind = opened(MemoryStore::new(), INSTANCE); - assert_eq!(mind.genesis_receipt_id().unwrap(), None); - let (first, _) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first.clone())); - let (second, _) = committed(admit(&mut mind, vec![campaign(&[REPO])])); - assert_ne!(first, second); - assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first)); - } - - fn admit_attributed(agent: &str, session: &str) -> (Mind, PipelineAdmissionOutcome) { - admit_signed(agent, session, "admit") - } - - fn admit_signed(agent: &str, session: &str, tool: &str) -> (Mind, PipelineAdmissionOutcome) { - let mut mind = opened(MemoryStore::new(), INSTANCE); - let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(agent), session: s(session), tool: s(tool) }; - let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance, documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO)] }; - let outcome = mind.admit(batch, now()); - (mind, outcome) - } - - /// Who admitted a batch is validated by the mind, so no writer can commit - /// a blank, multi-line or oversized attribution, and a refused batch leaves - /// nothing behind. - #[test] - fn provenance_names_an_agent_a_session_and_a_tool_on_one_clean_line_within_short() { - let over = "x".repeat(SHORT_MAX + 1); - let exact = "y".repeat(SHORT_MAX); - for bad in [ - "", - " \t ", - "\u{2003}", - "\u{200B}", - "line\nbreak", - "line\rbreak", - "a\u{0B}b", - "a\u{0C}b", - "a\u{85}b", - "a\u{2028}b", - "a\u{2029}b", - "a\0b", - "a\u{1B}[31mb", - "a\u{202E}b", - "a\u{2066}b", - "a\u{200F}b", - ] { - for field in ["provenance.agent", "provenance.session", "provenance.tool"] { - let (mind, outcome) = match field { - "provenance.agent" => admit_signed(bad, "session-1", "admit"), - "provenance.session" => admit_signed("claude", bad, "admit"), - _ => admit_signed("claude", "session-1", bad), - }; - let refused = refusal(outcome); - assert!( - matches!(&refused, MindRefusal::Document(PipelineRefusal::InvalidFormat { field: named, .. }) if named == field), - "{field}={bad:?}: {refused:?}" - ); - assert!(mind.is_empty(), "{field}={bad:?}: a refused batch wrote"); - } - } - for (field, outcome) in [ - ("provenance.agent", admit_signed(&over, "session-1", "admit")), - ("provenance.session", admit_signed("claude", &over, "admit")), - ("provenance.tool", admit_signed("claude", "session-1", &over)), - ] { - let (mind, outcome) = outcome; - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::FieldBound { field: field.into(), limit: 200, actual: 201 }) - ); - assert!(mind.is_empty()); - } - // The bound is in bytes, not characters: 101 two-byte characters are 202 bytes. - let (_, outcome) = admit_attributed(&"é".repeat(101), "session-1"); - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::FieldBound { field: "provenance.agent".into(), limit: 200, actual: 202 }) - ); - let (_, outcome) = admit_signed("claude", "session-1", &"é".repeat(101)); - assert!(matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::FieldBound { actual: 202, .. }))); - let (_, outcome) = admit_attributed(&"é".repeat(100), "session-1"); - committed(outcome); - // The bound is inclusive and a name with inner spaces is a name. - let (mind, outcome) = admit_signed(&exact, "session 7", "admit tool"); - committed(outcome); - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts[0].provenance.agent, s(&exact)); - assert_eq!(receipts[0].provenance.session, s("session 7")); - assert_eq!(receipts[0].provenance.tool, s("admit tool")); - } - - /// The read bounds are the leaf's: admission applies the leaf's validation - /// and adds none. Sixteen anchors of a hundred lines admit and read back; - /// a seventeenth refuses at `cut_spec.reads` and nothing is written. - #[test] - fn reads_admit_at_the_bound_and_refuse_over_it() { - let anchors = |count: usize| { - (0..count) - .map(|n| eureka_pipeline::ReadAnchor { - location: eureka_pipeline::CodeLocation { - path: s("crates/eureka-pipeline/src/lib.rs"), - line: 1, - end_line: Some(100), - symbol: Some(format!("symbol_{n}").as_str().into()), - }, - why: s("read"), - }) - .collect::>() - }; - let mut mind = seeded(); - let mut spec = cut_spec("1", 1); - spec.reads = anchors(16); - committed(admit(&mut mind, vec![D::CutSpec(spec.clone())])); - let held = mind.view(&r(K::CutSpec, &id("cut_spec", "cut-1.r1"))).unwrap().expect("the spec is held"); - assert_eq!(held.document, D::CutSpec(spec)); - - let mut over = cut_spec("2", 1); - over.reads = anchors(17); - let receipts = mind.receipts().unwrap().len(); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(over)])), - MindRefusal::Document(PipelineRefusal::FieldBound { field: "cut_spec.reads".into(), limit: 16, actual: 17 }) - ); - assert_eq!(mind.receipts().unwrap().len(), receipts); - assert_eq!(mind.view(&r(K::CutSpec, &id("cut_spec", "cut-2.r1"))).unwrap(), None, "nothing is written"); - } - - /// The leaf's `Title` rule is private, so this holds the provenance rule to - /// it: the two accept and refuse the same text. - #[test] - fn provenance_text_is_the_leafs_title_rule() { - let leaf_accepts = |text: &str| { - let D::Campaign(mut campaign) = campaign(&[REPO]) else { unreachable!() }; - campaign.title = eureka_pipeline::Title(text.into()); - D::Campaign(campaign).validate().is_ok() - }; - let mine_accepts = |text: &str| { - let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(text), session: s("s"), tool: s("t") }; - refuse_provenance(&provenance).is_ok() - }; - for text in [ - "claude", "session 7", "", " ", "\t", "\u{200B}", "\u{2003}", "!!!", "a\nb", "a\u{0B}b", "a\u{85}b", "a\u{2028}b", - "a\0b", "a\u{7F}b", "a\u{202E}b", "a\u{061C}b", "a\u{2069}b", "a\u{200D}b", "é", "日本", "a\u{FEFF}b", - &"x".repeat(200), &"x".repeat(201), &"é".repeat(100), &"é".repeat(101), - ] { - assert_eq!(mine_accepts(text), leaf_accepts(text), "{text:?}"); - } - } - - #[test] - fn short_bound_is_the_leafs_own() { - let schema = serde_json::to_value(schemars::schema_for!(Short)).unwrap(); - assert_eq!(schema["maxLength"], serde_json::json!(SHORT_MAX)); - } - - #[test] - fn admission_refuses_a_foreign_instance_whatever_the_transport() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let foreign = MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }; - let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; - assert_eq!( - refusal(mind.admit_prepared(&slug(OTHER_INSTANCE), provenance(Faculty::Hands), envelopes, now())), - foreign - ); - assert_eq!(refusal(admit(&mut mind, vec![stewardship(OTHER_INSTANCE, OTHER_REPO)])), foreign); - assert_eq!( - refusal(admit(&mut mind, vec![hand_off("a", "b", REPO, &[])])), - MindRefusal::ForeignInstance { declared: "a".into(), mind: INSTANCE.into() } - ); - assert_eq!(refusal(admit(&mut mind, vec![instance(OTHER_INSTANCE)])), foreign); - assert_eq!(store.rows(), before); - } - - #[test] - fn two_minds_in_one_state_root_stay_separate() { - let root = tempfile::tempdir().unwrap(); - let mut yggdrasil = Mind::open(root.path(), &slug(INSTANCE)).unwrap(); - let mut thought_cage = Mind::open(root.path(), &slug(OTHER_INSTANCE)).unwrap(); - assert_ne!( - Mind::store_path_for(root.path(), &slug(INSTANCE)).unwrap(), - Mind::store_path_for(root.path(), &slug(OTHER_INSTANCE)).unwrap() - ); - committed(admit(&mut yggdrasil, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); - committed(admit(&mut thought_cage, vec![instance(OTHER_INSTANCE)])); - let key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert!(yggdrasil.envelope(K::Stewardship, &key).is_some()); - assert!(thought_cage.envelope(K::Stewardship, &key).is_none()); - assert_eq!(yggdrasil.envelopes().len(), 4); - assert_eq!(thought_cage.envelopes().len(), 3); - } - - #[test] - fn keys_are_recomputed_and_a_forged_key_refuses() { - let mut mind = seeded(); - let mut envelope = prepare(&question("Q1", &["A", "B"], "A")); - let expected = envelope.key.clone(); - envelope.key = format!("{expected}-forged"); - let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), vec![envelope.clone()], now()); - assert_eq!( - refusal(outcome), - MindRefusal::Document(PipelineRefusal::InvalidIdentity { kind: K::Question, key: envelope.key, expected }) - ); - } - - #[test] - fn references_must_exist_in_image_or_batch() { - let mut mind = seeded(); - let absent_question = id("question", "Q9"); - let mut answering = ruling("R1"); - answering.answers = Some(s(&absent_question)); - answering.choice = Some(l("A")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering)])), - MindRefusal::MissingReference { kind: K::Question, id: absent_question } - ); - let absent_finding = id("finding", "cut-1.s1.F9"); - assert_eq!( - refusal(admit(&mut mind, vec![follow_up("FU-1", r(K::Finding, &absent_finding))])), - MindRefusal::MissingReference { kind: K::Finding, id: absent_finding } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])])])), - MindRefusal::MissingReference { kind: K::CutReport, id: id("cut_report", "cut-1.h1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])), - MindRefusal::MissingReference { kind: K::Question, id: id("question", "Q1") } - ); - // A well-formed id of another kind is a missing reference of the - // cited kind, not a wrong-kind refusal. - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("question", "Q1"))]; - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(spec)])), - MindRefusal::MissingReference { kind: K::Ruling, id: id("question", "Q1") } - ); - } - - #[test] - fn batch_is_all_or_nothing() { - let store = RefusingStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let receipts = receipt_count(&mind); - let mut quoted = ruling("R1"); - quoted.operator_quote = Some("go ahead".into()); - let batch = vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "B"), D::Ruling(quoted)]; - assert_eq!(refusal(admit(&mut mind, batch)), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - - store.command(SwapCommand::Lose); - let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); - assert!(matches!(outcome, PipelineAdmissionOutcome::Conflict { .. }), "{outcome:?}"); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - - store.command(SwapCommand::Fail); - assert!(matches!(refusal(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])), MindRefusal::Unavailable { .. })); - assert_eq!(store.rows(), before); - - store.command(SwapCommand::Delegate); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - assert_eq!(receipt_count(&mind), receipts + 1); - } - - #[test] - fn exact_replay_returns_already_admitted_across_provenance() { - let mut mind = seeded(); - let receipts = receipt_count(&mind); - let (first, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let batch = PipelineAdmissionBatch { - instance: slug(INSTANCE), - provenance: provenance(Faculty::Soul), - documents: vec![question("Q1", &["A", "B"], "A")], - }; - let later = now() + chrono::Duration::hours(1); - assert_eq!(mind.admit(batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first.clone() }); - assert_eq!(receipt_count(&mind), receipts + 1); - // The first write, epoch record and all, replays too. - let seed_batch = PipelineAdmissionBatch { - instance: slug(INSTANCE), - provenance: provenance(Faculty::Imagination), - documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO]), target(1, &[INVARIANT])], - }; - assert!(matches!(mind.admit(seed_batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { .. })); - assert_eq!(receipt_count(&mind), receipts + 1); - } - - /// RS-1: the ordinal is `head + 1` at admission, not a rank derived from - /// the clock or the receipt id. Three batches land at one `now`, so a - /// mutant that ranked by `(committed_at, receipt_id)` would fall back to - /// id order among them; the ids are checked not to already sort in - /// admission order, so that fallback provably disagrees with the truth. - #[test] - fn ordinals_are_admission_order_not_clock_or_id_order() { - let mut mind = seeded(); - assert_eq!(receipt::head(&mind).unwrap(), 1, "the seed batch is ordinal 1"); - let same_time = now(); - let (id1, _) = committed(admit_at(&mut mind, vec![question_n(1)], same_time)); - let (id2, _) = committed(admit_at(&mut mind, vec![question_n(2)], same_time)); - let (id3, _) = committed(admit_at(&mut mind, vec![question_n(3)], same_time)); - - let mut by_id = [id1.clone(), id2.clone(), id3.clone()]; - by_id.sort(); - assert_ne!( - by_id, - [id1.clone(), id2.clone(), id3.clone()], - "the fixture needs receipt ids that do not already sort in admission order" - ); - - let receipts = mind.receipts().unwrap(); - let ordinal_of = |id: &str| receipts.iter().find(|receipt| receipt.receipt_id == id).unwrap().ordinal; - assert_eq!(ordinal_of(&id1), 2); - assert_eq!(ordinal_of(&id2), 3); - assert_eq!(ordinal_of(&id3), 4); - assert_eq!(receipt::head(&mind).unwrap(), 4); - } - - /// RS-1, A9: the ordinal is not digested, so an exact replay finds the - /// receipt it first landed with and keeps that receipt's ordinal, even - /// when the head has moved since and a fresh candidate would be assigned - /// a different one. - #[test] - fn an_exact_replay_keeps_its_ordinal() { - let mut mind = seeded(); - let (first_id, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let first_ordinal = - mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == first_id).unwrap().ordinal; - - // Move the head, so a replay's own candidate would carry a different - // ordinal than the one already on the stored receipt. - committed(admit(&mut mind, vec![question("Q2", &["A", "B"], "A")])); - - let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); - assert_eq!(outcome, PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first_id.clone() }); - - let receipts = mind.receipts().unwrap(); - assert_eq!(receipts.iter().filter(|receipt| receipt.receipt_id == first_id).count(), 1, "no second receipt lands"); - let stored = receipts.iter().find(|receipt| receipt.receipt_id == first_id).unwrap(); - assert_eq!(stored.ordinal, first_ordinal, "the replay does not overwrite the ordinal"); - } - - /// A batch that derives a write replays like any other: the second - /// admission re-derives the record the first one landed, sequence and - /// all, so A9 recognises the digest and answers with the first receipt - /// instead of the derived write colliding with itself. - #[test] - fn an_exact_replay_re_derives_the_rulings_resolution() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering.clone())])); - assert!(writes.contains(&r(K::Resolution, &id("resolution", "question.Q1.n1")))); - let receipts = receipt_count(&mind); - assert_eq!( - admit(&mut mind, vec![D::Ruling(answering)]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id } - ); - assert_eq!(receipt_count(&mind), receipts, "a replay writes no second receipt"); - } - - /// The same on both sides of a hand-off: the source re-derives the - /// withdrawal it landed, the receiving mind the assignment. - #[test] - fn an_exact_replay_re_derives_both_sides_of_a_hand_off() { - let away = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]); - let mut source = seeded(); - let (receipt_id, _) = committed(admit(&mut source, vec![away.clone()])); - let receipts = receipt_count(&source); - assert_eq!(admit(&mut source, vec![away.clone()]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); - assert_eq!(receipt_count(&source), receipts); - - let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); - let (receipt_id, _) = committed(admit(&mut receiving, vec![away.clone()])); - let receipts = receipt_count(&receiving); - assert_eq!(admit(&mut receiving, vec![away]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); - assert_eq!(receipt_count(&receiving), receipts); - } - - /// The sequence a ruling's answer takes is the one that ruling's own - /// answer carries, not whatever answer the question already has. A - /// question reopened by withdrawing its first answer is answered again at - /// the next sequence, so the first answer stays readable under it. - #[test] - fn a_ruling_answering_a_reopened_question_takes_the_next_sequence() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut first = ruling("R1"); - first.answers = Some(s(&id("question", "Q1"))); - first.choice = Some(l("A")); - committed(admit(&mut mind, vec![D::Ruling(first)])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); - let mut second = ruling("R2"); - second.answers = Some(s(&id("question", "Q1"))); - second.choice = Some(l("B")); - let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(second)])); - assert_eq!(writes, vec![ - r(K::Ruling, &id("ruling", "R2")), - r(K::Resolution, &id("resolution", "question.Q1.n2")), - ]); - assert!(mind.envelope(K::Resolution, &id("resolution", "question.Q1.n1")).is_some()); - } - - #[test] - fn a_refused_batch_is_not_answered_from_a_stored_receipt() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let envelopes = vec![prepare(&D::CutSpec(spec))]; - committed(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes.clone(), now())); - committed(admit(&mut mind, vec![ - D::Ruling(ruling("R2")), - resolution(r(K::Ruling, &id("ruling", "R1")), superseded(&[r(K::Ruling, &id("ruling", "R2"))])), - ])); - assert_eq!( - refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes, now())), - MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R1") } - ); - } - - #[test] - fn a_receipt_names_the_exact_bytes_it_read_and_wrote() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); - let cited = mind.envelope(K::Ruling, &id("ruling", "R1")).unwrap().clone(); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited)]); - let written = mind.envelope(K::CutSpec, &id("cut_spec", "cut-1.r1")).unwrap(); - assert_eq!(receipt.writes, vec![DocumentVersion::from_envelope(written)]); - for version in receipt.strong_reads.iter().chain(&receipt.writes) { - assert_eq!(version.payload_sha256, format!("{:x}", Sha256::digest(&version.payload_msgpack))); - } - let digest = rmp_serde::to_vec_named(&(&receipt.instance, &receipt.strong_reads, &receipt.writes)).unwrap(); - assert_eq!(receipt_id, format!("mind-commit-{:x}", Sha256::digest(&digest))); - - // The same content admitted by another faculty in another mind has - // the same id: provenance is not digested. - let mut twin = opened(MemoryStore::new(), INSTANCE); - seed(&mut twin); - committed(admit(&mut twin, vec![D::Ruling(ruling("R1"))])); - let mut spec = cut_spec("1", 1); - spec.rulings = vec![s(&id("ruling", "R1"))]; - let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance: provenance(Faculty::Soul), documents: vec![D::CutSpec(spec)] }; - let (twin_id, _) = committed(twin.admit(batch, now() + chrono::Duration::days(1))); - assert_eq!(twin_id, receipt_id); - let twin_receipt = twin.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == twin_id).unwrap(); - assert_ne!(twin_receipt.provenance, receipt.provenance); - } - - #[test] - fn a_document_is_written_once_and_superseded_by_resolution() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R8"))])); - let mut rewritten = ruling("R8"); - rewritten.ruling = "A repo owns its mind.".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(rewritten)])), - MindRefusal::IdentityCollision { kind: K::Ruling, id: id("ruling", "R8") } - ); - committed(admit(&mut mind, vec![ - D::Ruling(ruling("R9")), - resolution(r(K::Ruling, &id("ruling", "R8")), superseded(&[r(K::Ruling, &id("ruling", "R9"))])), - ])); - assert_eq!(mind.get(K::Ruling, &id("ruling", "R8")).unwrap(), Some(D::Ruling(ruling("R8")))); - } - - /// A8: one resolution of a subject is in force at a time, and the next - /// record of that subject is the next sequence. The two rules are - /// separate refusals: a second closure at the right sequence is refused - /// because the first still stands, and one at a taken sequence is refused - /// before the question of standing arises. - #[test] - fn a_subject_with_a_resolution_in_force_refuses_another() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject.clone(), 2, ResolutionOutcome::Withdrawn { reason: "again".into() })])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(subject.clone(), ResolutionOutcome::Withdrawn { reason: "again".into() })])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 2, actual: 1 } - ); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering)])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - } - - /// Q17 B and Q19 A together: withdrawing a resolution reopens its subject - /// without erasing the record. The withdrawn closure is still readable by - /// key, the subject takes its next resolution at the next sequence, and - /// the chain stops at depth two -- a withdrawal cannot be withdrawn. - #[test] - fn a_withdrawn_resolution_reopens_its_subject_and_stays_readable() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - let first = id("resolution", "question.Q1.n1"); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - - // The withdrawal of that closure: one nesting out, under the - // resolution it withdraws. - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); - let withdrawal = id("resolution", "resolution.question.Q1.n1.n1"); - assert!(mind.get(K::Resolution, &withdrawal).unwrap().is_some()); - - // The subject is open again, and its next record is `n2`. The first - // one is still there, outcome and all: this is a log, not an erasure. - committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); - let Some(D::Resolution(kept)) = mind.get(K::Resolution, &first).unwrap() else { panic!() }; - assert_eq!(kept.outcome, withdrawn()); - assert_eq!(kept.sequence, 1); - - // `n2` stands, so a third is refused, and the withdrawal cannot - // itself be withdrawn to re-raise `n1` over it (Q19 A). - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject, 3, withdrawn())])), - MindRefusal::AlreadyResolved { subject: id("question", "Q1") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), - MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } - ); - - // And a ruling may answer a question whose first closure was - // withdrawn: the derived resolution takes the next sequence. - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!(writes, vec![ - r(K::Ruling, &id("ruling", "R1")), - r(K::Resolution, &id("resolution", "question.Q1.n2")), - ]); - } - - /// The sequence is exactly the previous plus one: a gap refuses as loudly - /// as a taken sequence, on both sequenced kinds, and before the question - /// of what still stands arises. - #[test] - fn a_sequence_gap_refuses_on_both_sequenced_kinds() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); - committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution_n(subject, 4, withdrawn())])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 4 } - ); - - let mut steward = seeded(); - committed(admit(&mut steward, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - committed(admit(&mut steward, vec![D::HandOff(back)])); - assert_eq!( - refusal(admit(&mut steward, vec![stewardship_n(INSTANCE, REPO, 4)])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 4 } - ); - } - - /// The sequence counts the batch as well as the image: two records of one - /// subject landing together are the second and the third, never two - /// firsts, and the first of them is refused for the sequence rather than - /// for what the other closes. - #[test] - fn the_latest_resolution_counts_the_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let subject = r(K::Question, &id("question", "Q1")); - assert_eq!( - refusal(admit(&mut mind, vec![ - resolution_n(subject.clone(), 1, withdrawn()), - resolution_n(subject, 2, ResolutionOutcome::Withdrawn { reason: "again".into() }), - ])), - MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 1 } - ); - } - - /// The Q19 cap reads the batch too: a chain three deep landing in one - /// batch is capped exactly as one landing over three admissions. - #[test] - fn the_cap_reads_a_chain_landing_in_one_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![ - resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn()), - resolution(r(K::Resolution, &id("resolution", "resolution.question.Q1.n1.n1")), withdrawn()), - ])), - MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } - ); - } - - /// Q18 A and Q20 A at admission: a repo is stewarded by one record at a - /// time on a mind, and a repo handed away and handed back is a second - /// record rather than a collision or an overwrite. - #[test] - fn a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer() { - let mut mind = seeded(); - let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 2)])), - MindRefusal::AlreadyStewarded { repo: REPO.into() } - ); - let D::Stewardship(mut retaken) = stewardship(INSTANCE, REPO) else { panic!() }; - retaken.note = "assigned again".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Stewardship(retaken)])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 2, actual: 1 } - ); - - // Handing the repo away withdraws the assignment, and the withdrawal - // is a resolution of that record, sequence and all. The stewardship it - // reads to derive that is an image document, so it is pinned. - let (receipt_id, writes) = committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let away = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.{}", date().0); - assert_eq!(writes, vec![ - r(K::HandOff, &away), - r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1")), - ]); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - let stewardship_envelope = mind.envelope(K::Stewardship, &first).unwrap().clone(); - assert!( - receipt.strong_reads.contains(&DocumentVersion::from_envelope(&stewardship_envelope)), - "the derived withdrawal's own citation is pinned: {:?}", - receipt.strong_reads - ); - - // Nothing stewards the repo now, and a campaign over it still admits: - // campaign admission reads no stewardship (ruling stewardship-rule). - let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; - second.slug = slug("second"); - committed(admit(&mut mind, vec![D::Campaign(second)])); - - // Handed back, the mind stewards it again, as `n2`. No field names a - // return: this is the same hand-off shape with the instances swapped. - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(back)])); - let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); - assert_eq!(writes, vec![ - r(K::HandOff, &format!("{OTHER_INSTANCE}:hand_off:{INSTANCE}.gamecult_-epiphany.2026-09-17")), - r(K::Stewardship, &again), - ]); - let Some(D::Stewardship(taken)) = mind.get(K::Stewardship, &again).unwrap() else { panic!() }; - assert_eq!(taken.sequence, 2); - assert_eq!(taken.assigned_on, eureka_pipeline::Date("2026-09-17".into())); - assert!(mind.envelope(K::Stewardship, &first).is_some(), "the first assignment is still readable by key"); - } - - /// Q19 A's rationale, which the cap alone does not carry: a withdrawal - /// never re-raises an earlier record over a later one. The repo went away - /// and came back, so withdrawing the hand-off's withdrawal would put the - /// first assignment back in force beside the second. - #[test] - fn a_withdrawal_does_not_reinstate_a_stewardship_over_a_later_one() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; - back.handed_on = eureka_pipeline::Date("2026-09-17".into()); - committed(admit(&mut mind, vec![D::HandOff(back)])); - let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), - MindRefusal::WouldReinstateOverLater { subject: first, later: again.clone() } - ); - let docs = Docs::from_image(mind.envelopes()).unwrap(); - let in_force = docs.stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| *key).collect::>(); - assert_eq!(in_force, vec![again.as_str()], "one assignment stands, and it is the later one"); - } - - /// The same rule one kind over: withdrawing a supersession while the - /// revision that superseded stands would leave two revisions in force. - #[test] - fn a_withdrawal_does_not_reinstate_a_revision_over_a_later_one() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - target(2, &[INVARIANT]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "target.r1.n1")), withdrawn())])), - MindRefusal::WouldReinstateOverLater { subject: id("target", "r1"), later: id("target", "r2") } - ); - assert!(docs_of(&mind).in_force(K::Target, &id("target", "r2"))); - assert!(!docs_of(&mind).in_force(K::Target, &id("target", "r1"))); - } - - fn docs_of(mind: &Mind) -> Docs { - Docs::from_image(mind.envelopes()).unwrap() - } - - /// The withdrawal a hand-off derives is a record of its subject like any - /// other: the second time the repo leaves, it takes the next sequence. - #[test] - fn a_hand_offs_derived_withdrawal_takes_the_next_sequence() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); - // Nothing later stands over the first assignment, so withdrawing the - // withdrawal puts it back in force and the repo is this mind's again. - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let D::HandOff(mut again) = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]) else { panic!() }; - again.handed_on = eureka_pipeline::Date("2026-09-17".into()); - let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(again)])); - assert_eq!(writes, vec![ - r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-17")), - r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n2")), - ]); - } - - fn hand_off_on(from: &str, to: &str, day: &str) -> D { - let D::HandOff(mut hand_off) = hand_off(from, to, REPO, &[]) else { panic!() }; - hand_off.handed_on = eureka_pipeline::Date(day.into()); - D::HandOff(hand_off) - } - - fn stewardship_key(sequence: u32) -> String { - format!("{INSTANCE}:stewardship:gamecult_-epiphany.n{sequence}") - } - - /// Which assignment a hand-off's derived withdrawal names is a question of - /// content, not of order. While the withdrawal is being derived the - /// hand-off's own is excluded from what stands, so two assignments are in - /// force at once, and after ten transfers the one that stewards the repo - /// now sorts first: `n11` before `n2`. The replay must still withdraw the - /// assignment it withdrew the first time, or it derives a second - /// withdrawal of a standing assignment and collides instead of answering - /// with its receipt. - #[test] - fn a_source_side_replay_withdraws_the_assignment_it_withdrew() { - let mut mind = seeded(); - let mut aways = Vec::new(); - for transfer in 1..=10u32 { - let day = format!("2026-09-{transfer:02}"); - let away = hand_off_on(INSTANCE, OTHER_INSTANCE, &day); - let (receipt_id, _) = committed(admit(&mut mind, vec![away.clone()])); - aways.push((away, receipt_id)); - committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, &day)])); - } - let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); - assert_eq!(in_force, vec![stewardship_key(11)]); - - for index in [1usize, 8] { - let (away, receipt_id) = &aways[index]; - assert_eq!( - admit(&mut mind, vec![away.clone()]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() }, - "the replay of transfer {} re-derives its own withdrawal", - index + 1 - ); - } - - // The standing assignment is handed away and reinstated, so it too - // carries a withdrawal -- of another hand-off. Sorting first, it is - // what a match on the outcome alone would pick. - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-11")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n11.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let (away, receipt_id) = &aways[1]; - assert_eq!( - admit(&mut mind, vec![away.clone()]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() } - ); - } - - /// `later_in_force` counts what stands, not what exists: the repo went - /// away, came back and went away again, so the second assignment is later - /// than the first but withdrawn, and reinstating the first leaves one - /// assignment in force rather than two. - #[test] - fn a_withdrawn_later_assignment_does_not_block_a_reinstatement() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); - committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17")])); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-18")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); - let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); - assert_eq!(in_force, vec![stewardship_key(1)]); - } - - /// And it reads the batch: the assignment that stands later than the one - /// a withdrawal would reinstate may be landing beside that withdrawal, - /// derived by a hand-off in the same batch. - #[test] - fn a_reinstatement_is_blocked_by_an_assignment_in_its_own_batch() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); - let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![ - hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17"), - resolution(r(K::Resolution, &withdrawal), withdrawn()), - ])), - MindRefusal::WouldReinstateOverLater { subject: stewardship_key(1), later: stewardship_key(2) } - ); - } - - /// A cut spec's revisions are sequenced within its cut, so another cut's - /// later revision is not what a withdrawal reads: once cut A's second - /// revision is gone, reinstating its first commits while cut B's second - /// stands. - #[test] - fn a_reinstatement_reads_the_revisions_of_its_own_cut() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("A", 1)), D::CutSpec(cut_spec("B", 1))])); - for cut in ["A", "B"] { - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec(cut, 2)), - resolution( - r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))), - superseded(&[r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r2")))]), - ), - ])); - } - let supersession = id("resolution", "cut_spec.cut-A.r1.n1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])), - MindRefusal::WouldReinstateOverLater { - subject: id("cut_spec", "cut-A.r1"), - later: id("cut_spec", "cut-A.r2"), - } - ); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-A.r2")), withdrawn())])); - committed(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])); - assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-A.r1"))); - assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-B.r2"))); - } - - /// A seeded mind with one document of every resolvable kind, and the - /// non-resolvable ones beside them. - fn world() -> Mind { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - stewardship(INSTANCE, OTHER_REPO), - question("Q1", &["A", "B"], "A"), - D::Ruling(ruling("R1")), - D::CutSpec(cut_spec("1", 1)), - D::CutReport(cut_report("1", 1)), - verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&id("finding", "cut-1.s1.F1")], Some("P1"), &["M1"])]), - D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)), - follow_up("FU-1", r(K::Finding, &id("finding", "cut-1.s1.F1"))), - hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[]), - ])); - mind - } - - #[test] - fn the_resolution_matrix_is_admissions() { - let incompatible = |kind: K, outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: kind, outcome: outcome.into() }; - let fixed = || ResolutionOutcome::Fixed { commit: sha(), by: None }; - let target_ref = r(K::Target, &id("target", "r1")); - let target_r2 = r(K::Target, &id("target", "r2")); - let question_ref = r(K::Question, &id("question", "Q1")); - let ruling_ref = r(K::Ruling, &id("ruling", "R1")); - let spec_ref = r(K::CutSpec, &id("cut_spec", "cut-1.r1")); - let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); - let follow_up_ref = r(K::FollowUp, &id("follow_up", "FU-1")); - let stewardship_ref = r(K::Stewardship, &format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")); - let campaign_ref = r(K::Campaign, &id("campaign", "self")); - let report_ref = r(K::CutReport, &id("cut_report", "cut-1.h1")); - let verdict_ref = r(K::Verdict, &id("verdict", "cut-1.s1")); - let instance_ref = r(K::Instance, &format!("{INSTANCE}:instance:self")); - let hand_off_ref = r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-huginn.2026-09-16")); - - // Accepted, one per resolvable kind. - committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(std::slice::from_ref(&target_r2)))])); - committed(admit(&mut world(), vec![resolution(question_ref.clone(), withdrawn())])); - committed(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(ruling_ref.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])); - committed(admit(&mut world(), vec![resolution(spec_ref.clone(), withdrawn())])); - committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Recorded { reason: "noted".into() })])); - committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to: follow_up_ref.clone() })])); - committed(admit(&mut world(), vec![resolution(follow_up_ref.clone(), fixed())])); - committed(admit(&mut world(), vec![resolution(stewardship_ref.clone(), withdrawn())])); - let mut mind = world(); - committed(admit(&mut mind, vec![resolution(question_ref.clone(), withdrawn())])); - let nested = r(K::Resolution, &id("resolution", "question.Q1.n1")); - committed(admit(&mut mind, vec![resolution(nested.clone(), withdrawn())])); - - // Refused, one per kind, and every non-resolvable kind. - assert_eq!(refusal(admit(&mut world(), vec![resolution(target_ref.clone(), withdrawn())])), incompatible(K::Target, "Withdrawn")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(question_ref.clone(), fixed())])), incompatible(K::Question, "Fixed")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(ruling_ref.clone(), withdrawn())])), incompatible(K::Ruling, "Withdrawn")); - // A kind admits the outcomes of its own row and no others: a question - // is answered or withdrawn, never superseded; a target is superseded, - // never answered. - assert_eq!( - refusal(admit(&mut world(), vec![ - question("Q2", &["A", "B"], "A"), - resolution(question_ref, superseded(&[r(K::Question, &id("question", "Q2"))])), - ])), - incompatible(K::Question, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(target_ref, ResolutionOutcome::Answered { by: ruling_ref })])), - incompatible(K::Target, "Answered") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(spec_ref.clone(), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), - incompatible(K::CutSpec, "Answered") - ); - // A cut spec superseded across cuts is refused too. - let mut other_cut = cut_spec("2", 1); - other_cut.cut = l("2"); - assert_eq!( - refusal(admit(&mut world(), vec![D::CutSpec(other_cut), resolution(spec_ref, superseded(&[r(K::CutSpec, &id("cut_spec", "cut-2.r1"))]))])), - incompatible(K::CutSpec, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(std::slice::from_ref(&finding_ref)))])), - incompatible(K::Finding, "Superseded") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(follow_up_ref, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), - incompatible(K::FollowUp, "Answered") - ); - assert_eq!( - refusal(admit(&mut world(), vec![resolution(stewardship_ref, ResolutionOutcome::Recorded { reason: "no".into() })])), - incompatible(K::Stewardship, "Recorded") - ); - let mut mind = world(); - committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(nested.clone(), superseded(&[nested]))])), - incompatible(K::Resolution, "Superseded") - ); - for (subject, kind) in [(campaign_ref, K::Campaign), (report_ref, K::CutReport), (verdict_ref, K::Verdict), (instance_ref, K::Instance), (hand_off_ref, K::HandOff)] { - assert_eq!(refusal(admit(&mut world(), vec![resolution(subject, withdrawn())])), incompatible(kind, "Withdrawn")); - } - } - - #[test] - fn supersession_names_each_supersessor_and_none_is_empty() { - let subject = r(K::Ruling, &id("ruling", "R1")); - assert_eq!(refusal(admit(&mut world(), vec![resolution(subject.clone(), superseded(&[]))])), MindRefusal::EmptySupersession); - let (_, writes) = committed(admit(&mut world(), vec![ - D::Ruling(ruling("R2")), - D::Ruling(ruling("R3")), - resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R3"))])), - ])); - assert_eq!(writes.len(), 3); - assert_eq!( - refusal(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R4"))]))])), - MindRefusal::UnknownSupersessor { id: id("ruling", "R4") } - ); - let mut mind = world(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R2")), D::Ruling(ruling("R3")), resolution(r(K::Ruling, &id("ruling", "R2")), superseded(&[r(K::Ruling, &id("ruling", "R3"))]))])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(subject, superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])), - MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R2") } - ); - } - - /// refusals-typed: a document the leaf cannot key is the client's fault. - /// The finding's local is 64 bytes of label under `cut-1.s1.`; the - /// resolution's subject is a resolution whose own local leaves no room for - /// a third nesting. Both pass `validate` and fail only at the key. - fn unkeyable() -> Vec { - let wide = D::Finding(finding("1", 1, &"w".repeat(64), FindingConfidence::Confirmed)); - let deep_subject = id("resolution", &format!("{}.{}", "x".repeat(60), "y".repeat(40))); - vec![wide, resolution(r(K::Resolution, &deep_subject), withdrawn())] - } - - #[test] - fn an_unkeyable_document_is_refused_as_a_document() { - let store = RefusingStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let before = store.rows(); - let receipts = receipt_count(&mind); - for (document, field) in unkeyable().into_iter().zip(["finding.key", "resolution.key"]) { - document.validate().unwrap(); - // The leaf owns what a key error says; admission must carry it - // unchanged, field and offending value included. - let leaf = pipeline_key(&document).expect_err("the fixture must not key"); - assert!( - matches!(&leaf, PipelineRefusal::InvalidFormat { field: named, value } if named == field && !value.is_empty()), - "{leaf:?}" - ); - assert_eq!(refusal(admit(&mut mind, vec![document])), MindRefusal::Document(leaf), "{field}"); - assert_eq!(store.rows(), before); - assert_eq!(receipt_count(&mind), receipts); - } - } - - /// The derived path (`admit_steps`) shares the helper. No derivable - /// document can fail to key after the leaf's per-kind bound (a derived - /// resolution is depth one or two over a subject of at most 64 bytes), so - /// the helper is pinned directly. - #[test] - fn prepare_classifies_a_key_refusal_as_a_document_and_encodes_the_rest() { - let cache = schema_cache().unwrap(); - let keyable = question("Q1", &["A", "B"], "A"); - assert_eq!(super::prepare(&keyable, &cache).unwrap(), prepare(&keyable)); - for document in unkeyable() { - let leaf = pipeline_key(&document).expect_err("the fixture must not key"); - assert_eq!(super::prepare(&document, &cache).unwrap_err(), MindRefusal::Document(leaf)); - } - } - - #[test] - fn a_ruling_answering_a_question_derives_the_answered_resolution_atomically() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("C")); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering.clone())])), - MindRefusal::InvalidChoice { ruling: id("ruling", "R1"), choice: "C".into() } - ); - answering.choice = Some(l("A")); - let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!(writes, vec![r(K::Ruling, &id("ruling", "R1")), r(K::Resolution, &id("resolution", "question.Q1.n1"))]); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.writes.len(), 2); - let Some(D::Resolution(derived)) = mind.get(K::Resolution, &id("resolution", "question.Q1.n1")).unwrap() else { panic!() }; - assert_eq!(derived.outcome, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); - let mut again = ruling("R2"); - again.answers = Some(s(&id("question", "Q1"))); - again.choice = Some(l("B")); - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(again)])), MindRefusal::AlreadyResolved { subject: id("question", "Q1") }); - - // An explicit `Answered` must name the ruling that actually answers - // its subject: a ruling answering Q1 does not resolve Q2. - let mut mind = seeded(); - committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); - let mut answering = ruling("R1"); - answering.answers = Some(s(&id("question", "Q1"))); - answering.choice = Some(l("A")); - let wrong = resolution(r(K::Question, &id("question", "Q2")), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); - assert_eq!( - refusal(admit(&mut mind, vec![D::Ruling(answering.clone()), wrong.clone()])), - MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } - ); - // The coherence is checked whether the ruling is in the batch or in - // the image: a later batch citing the stored R1 is refused the same - // way, and the derived resolution of Q1 is not what saves it. - committed(admit(&mut mind, vec![D::Ruling(answering)])); - assert_eq!( - refusal(admit(&mut mind, vec![wrong])), - MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } - ); - } - - #[test] - fn a_batch_is_one_to_sixty_four_documents_each_with_its_own_identity() { - let mut mind = seeded(); - // A2 at both ends, and at the boundary the batch is admitted whole. - assert_eq!(refusal(admit(&mut mind, vec![])), MindRefusal::BatchSize { actual: 0 }); - let over = (0..65).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); - assert_eq!(refusal(admit(&mut mind, over)), MindRefusal::BatchSize { actual: 65 }); - let full = (0..BATCH_MAX).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); - let (_, writes) = committed(admit(&mut mind, full)); - assert_eq!(writes.len(), BATCH_MAX); - // A4: one identity, once, whatever the two documents say. - assert_eq!( - refusal(admit(&mut mind, vec![question("Z", &["A", "B"], "A"), question("Z", &["A", "B"], "B")])), - MindRefusal::IdentityCollision { kind: K::Question, id: id("question", "Z") } - ); - } - - #[test] - fn a_question_offers_at_least_two_options_and_recommends_one_of_them() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q7", &["A"], "A")])), - MindRefusal::InvalidOptions { question: id("question", "Q7") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q8", &["A", "B"], "C")])), - MindRefusal::InvalidOptions { question: id("question", "Q8") } - ); - committed(admit(&mut mind, vec![question("Q9", &["A", "B"], "B")])); - } - - #[test] - fn a_label_names_one_option_and_one_invariant() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![question("Q1", &["A", "A"], "A")])), - MindRefusal::DuplicateLabel { field: "question.options".into(), label: "A".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![ - target(2, &["x", "x"]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])), - MindRefusal::DuplicateLabel { field: "target.invariants".into(), label: "x".into() } - ); - } - - /// Every A7 reference the image resolved is pinned, not the first of - /// them, so a batch citing three documents cannot commit over a change to - /// the other two. - #[test] - fn every_cited_image_document_is_a_strong_read() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), D::Ruling(ruling("R2")), D::Ruling(ruling("R3"))])); - let cited = ["R1", "R2", "R3"].map(|label| mind.envelope(K::Ruling, &id("ruling", label)).unwrap().clone()); - let mut spec = cut_spec("1", 1); - spec.rulings = cited.iter().map(|envelope| s(&envelope.key)).collect(); - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads.len(), 3); - for envelope in &cited { - assert!(receipt.strong_reads.contains(&DocumentVersion::from_envelope(envelope)), "{}", envelope.key); - } - - // Two batch documents citing one image document pin it once: the pins - // are a set, so a second citation neither duplicates the pin nor - // cancels it. - let mut twice = cut_spec("2", 1); - twice.rulings = vec![s(&id("ruling", "R1"))]; - let D::Question(mut raised) = question("Q1", &["A", "B"], "A") else { panic!() }; - raised.raised_in = Some(r(K::Ruling, &id("ruling", "R1"))); - let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(twice), D::Question(raised)])); - let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); - assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited[0])], "cited twice, pinned once"); - } - - #[test] - fn an_operator_quote_requires_operator_authority() { - let mut mind = seeded(); - let mut quoted = ruling("R1"); - quoted.operator_quote = Some("all recommendations, go ahead".into()); - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - quoted.authority = RulingAuthority::Mind; - assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); - assert!(mind.get(K::Ruling, &id("ruling", "R1")).unwrap().is_none(), "a refused batch leaves the store unchanged"); - quoted.authority = RulingAuthority::Operator; - committed(admit(&mut mind, vec![D::Ruling(quoted)])); - } - - #[test] - fn a_mind_ruling_without_a_quote_is_admitted() { - let mut mind = seeded(); - let mut own = ruling("R1"); - own.authority = RulingAuthority::Mind; - committed(admit(&mut mind, vec![D::Ruling(own)])); - } - - #[test] - fn a_revision_requires_its_predecessors_supersession_in_the_batch() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![target(2, &[INVARIANT])])), - MindRefusal::RevisionWithoutSupersession { kind: K::Target, revision: 2 } - ); - committed(admit(&mut mind, vec![ - target(2, &[INVARIANT]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - ])); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 2))])), - MindRefusal::RevisionWithoutSupersession { kind: K::CutSpec, revision: 2 } - ); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 2)), - resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), - ])); - } - - #[test] - fn a_cut_report_cites_an_in_force_spec_and_agrees_with_it() { - let mut mind = seeded(); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), - MindRefusal::CutReportWithoutSpec { report: id("cut_report", "cut-1.h1") } - ); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - let mut report = cut_report("1", 1); - report.branch = s("another-branch"); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); - // A branch is compared as written. Git's refs are case-sensitive, and - // a report of `Codex/...` is a report of another branch or of none. - let mut report = cut_report("1", 1); - report.branch = s("CODEX/eureka-pipeline-state"); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); - let mut report = cut_report("1", 1); - report.repo = repo(OTHER_REPO); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "repo".into() }); - let mut report = cut_report("1", 1); - report.range.head = eureka_pipeline::Sha("abcdef0".into()); - assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { head: "abcdef0".into() }); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } - ); - } - - fn spec_ref(cut: &str) -> PipelineRef { - r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))) - } - - fn run_key(label: &str) -> String { - format!("{INSTANCE}:run:{label}") - } - - /// A seeded mind holding cut specs 1 and 2. - fn with_specs() -> Mind { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutSpec(cut_spec("2", 1))])); - mind - } - - fn recorded(label: &str) -> PipelineDocument { - resolution(r(K::Run, &run_key(label)), ResolutionOutcome::Recorded { reason: "finished".into() }) - } - - /// run-is-the-grant: the claim is the only consumption fact. A second run - /// on a claim a live run holds is AlreadyClaimed, naming the claim and the - /// holder; another claim is free; once the holder is Recorded the claim is - /// free, and withdrawing that record makes the holder live again. - #[test] - fn a_second_run_on_a_live_claim_is_already_claimed() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) }; - assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), claimed); - assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("2"), spec_ref("1")])])), claimed, "any one held claim refuses the run"); - committed(admit(&mut mind, vec![run("b", &[spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); - - let mut reopened = with_specs(); - committed(admit(&mut reopened, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut reopened, vec![recorded("a")])); - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - committed(admit(&mut reopened, vec![resolution(closure, withdrawn())])); - assert_eq!(refusal(admit(&mut reopened, vec![run("d", &[spec_ref("1")])])), claimed); - } - - /// one-live-self-run at admission: a second run of hers of one turn is - /// AlreadyLive naming the holder, whether the holder is claimless or - /// claiming, and a claim the holder holds is still AlreadyClaimed first. - #[test] - fn a_second_live_run_of_hers_of_one_turn_is_already_live() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - let live_a = MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), live_a, "a claimless holder"); - assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])), live_a); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])); - let live_b = MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("2")])])), live_b, "a claiming holder"); - let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("b")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("1")])])), claimed, "claims answer first"); - // The holder is named by its literal key, not by the constructor that made the expectation above. - let MindRefusal::AlreadyLive { run } = refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])) else { panic!("not live") }; - assert_eq!(run.as_str(), format!("{INSTANCE}:run:b")); - assert_eq!(run.to_ref().id.0, format!("{INSTANCE}:run:b")); - } - - /// The rule counts only in-force runs of hers of the same turn: an - /// operator's run, a run of the other turn and a run after the holder is - /// Recorded are admitted, and one live PersonaTurn and one live SelfRun of - /// hers coexist. - #[test] - fn an_operators_run_another_turn_and_a_run_after_the_holder_closed_are_admitted() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![run("op1", &[])])); - committed(admit(&mut mind, vec![run("op2", &[])])); - committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])])); - let live_p = MindRefusal::AlreadyLive { run: RunId::held(&run_key("p")) }; - assert_eq!(refusal(admit(&mut mind, vec![mind_run("p2", RunTurn::PersonaTurn, &[])])), live_p); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("b")])); - committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); - - let mut operator_only = with_specs(); - committed(admit(&mut operator_only, vec![run("op", &[])])); - committed(admit(&mut operator_only, vec![mind_run("a", RunTurn::SelfRun, &[])])); - } - - /// The slot is keyed on instance and turn alone: a holder that started - /// earlier, on another host and under another budget still blocks. - #[test] - fn a_holder_that_differs_in_start_host_and_budget_still_holds_the_slot() { - let mut mind = with_specs(); - let D::Run(mut holder) = mind_run("a", RunTurn::SelfRun, &[]) else { unreachable!() }; - holder.started_on = eureka_pipeline::Date("2026-01-01".into()); - holder.host = s("elsewhere"); - holder.budget_usd = s("9"); - committed(admit(&mut mind, vec![D::Run(holder)])); - assert_eq!( - refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) } - ); - } - - /// A run opened and closed in one batch is never live, so a live holder - /// of its turn does not refuse it; it holds no slot afterwards either. - #[test] - fn a_run_born_closed_beside_a_live_holder_is_admitted() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[]), recorded("b")])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); - } - - /// Two runs of hers of one turn in one batch hold nothing: the batch is - /// checked against itself, refused whole, and nothing is written. - #[test] - fn two_live_runs_of_hers_in_one_batch_are_already_live_and_nothing_commits() { - let mut mind = with_specs(); - let refused = refusal(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[]), mind_run("b", RunTurn::SelfRun, &[])])); - assert!(matches!(refused, MindRefusal::AlreadyLive { .. }), "{refused:?}"); - assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); - } - - /// Withdrawing the close of a run of hers reinstates it, and a reinstated - /// run is a live one: refused while another run of its turn is live, - /// allowed when none is. - #[test] - fn reinstating_a_run_of_hers_over_a_live_holder_is_already_live() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) } - ); - committed(admit(&mut mind, vec![recorded("b")])); - committed(admit(&mut mind, vec![resolution(closure, withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])), - MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }, - "the reinstated run holds the slot" - ); - } - - /// A retry of an admitted run answers AlreadyAdmitted even when another - /// run of its turn has since become the live one. - #[test] - fn replaying_an_admitted_run_of_hers_after_another_took_the_turn_is_already_admitted() { - let mut mind = with_specs(); - let (first, _) = committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); - assert_eq!( - admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])]), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } - ); - } - - /// Two openers on one claim in one batch: the claim is held by neither, so - /// the batch is refused whole and nothing is written. One opener at a time - /// is `a_second_run_on_a_live_claim_is_already_claimed`. - #[test] - fn two_openers_in_one_batch_hold_nothing() { - let mut mind = with_specs(); - let refused = refusal(admit(&mut mind, vec![run("a", &[spec_ref("1")]), run("b", &[spec_ref("1")])])); - assert!(matches!(refused, MindRefusal::AlreadyClaimed { .. }), "{refused:?}"); - assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); - } - - /// Every claim names a document that exists and is in force. - #[test] - fn a_runs_claims_must_exist_and_be_in_force() { - let mut mind = with_specs(); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("9")])])), - MindRefusal::MissingReference { kind: K::CutSpec, id: spec_ref("9").id.0 } - ); - committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } - ); - assert!(mind.envelope(K::Run, &run_key("a")).is_none()); - } - - /// A replay of an admitted run is AlreadyAdmitted even when its claim has - /// since been closed: a retry owes nothing to the work moving on. A - /// different run on that closed claim is still refused. - #[test] - fn replaying_an_admitted_run_after_its_claim_closed_is_already_admitted() { - let mut mind = with_specs(); - let (first, _) = committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); - assert_eq!(admit(&mut mind, vec![run("a", &[spec_ref("1")])]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first }); - assert_eq!( - refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } - ); - } - - /// Withdrawing a run's Recorded resolution puts the run back in force, so - /// it goes through the same exclusivity as an opening: refused while - /// another live run holds one of its claims, allowed when none does. - #[test] - fn reinstating_a_run_over_a_live_holder_is_already_claimed() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("c")) } - ); - - let mut free = with_specs(); - committed(admit(&mut free, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut free, vec![recorded("a")])); - committed(admit(&mut free, vec![resolution(closure, withdrawn())])); - } - - /// Reinstatement finds a contested claim anywhere in the run's list: a - /// reinstated run of two claims where only the second is held is refused, - /// and the first alone being contested is refused too. - #[test] - fn reinstating_a_run_finds_a_contested_second_claim() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - for (held, expected) in [("2", "2"), ("1", "1")] { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![run("c", &[spec_ref(held)])])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::AlreadyClaimed { item: spec_ref(expected).id.0, run: RunId::held(&run_key("c")) } - ); - } - } - - /// Reinstating a run needs every claim in force, as opening it does: once - /// a claimed item has resolved the withdrawal is refused, and while all - /// stay in force it is allowed. - #[test] - fn reinstating_a_run_needs_its_claims_in_force() { - let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut mind, vec![recorded("a")])); - committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } - ); - - let mut live = with_specs(); - committed(admit(&mut live, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); - committed(admit(&mut live, vec![recorded("a")])); - committed(admit(&mut live, vec![resolution(closure, withdrawn())])); - } - - /// A holder is found by any of its claims, not only the first. - #[test] - fn a_holder_is_found_by_its_second_claim() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("2"), spec_ref("1")])])); - assert_eq!( - refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), - MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) } - ); - } - - /// A key already holding a run is taken: the same key with different - /// content is an IdentityCollision, never a replay; only byte-identical - /// content answers AlreadyAdmitted. - #[test] - fn a_run_key_reused_with_different_content_is_an_identity_collision() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; - changed.budget_usd = "6".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(changed)])), - MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } - ); - assert_eq!( - refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), - MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } - ); - } - - /// The replay clause is byte identity, not the key: a changed run under an - /// admitted key whose claim has since closed is not a replay, so the closed - /// claim refuses it. - #[test] - fn a_changed_run_under_an_admitted_key_is_not_a_replay() { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); - committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); - let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; - changed.budget_usd = "6".into(); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(changed)])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } - ); - } - - /// A5 for runs: a run of another instance is a ForeignInstance. - #[test] - fn a_run_of_another_mind_is_a_foreign_instance() { - let mut mind = with_specs(); - let D::Run(mut foreign) = run("a", &[]) else { panic!() }; - foreign.instance = slug(OTHER_INSTANCE); - assert_eq!( - refusal(admit(&mut mind, vec![D::Run(foreign)])), - MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() } - ); - } - - /// A run ends: Recorded or Withdrawn, and no other outcome. - #[test] - fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() { - let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Run, outcome: outcome.into() }; - let subject = || r(K::Run, &run_key("a")); - let open = || { - let mut mind = with_specs(); - committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), run("a", &[spec_ref("1")]), run("z", &[])])); - mind - }; - committed(admit(&mut open(), vec![recorded("a")])); - committed(admit(&mut open(), vec![resolution(subject(), withdrawn())])); - let other_run = r(K::Run, &run_key("z")); - let refused = [ - (superseded(std::slice::from_ref(&other_run)), "Superseded"), - (ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"), - (ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"), - (ResolutionOutcome::Deferred { to: other_run }, "Deferred"), - ]; - for (outcome, name) in refused { - assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}"); - } - } - - #[test] - fn depends_on_names_cuts() { - let depending = |cut: &str, on: &[&str]| { - let mut spec = cut_spec(cut, 1); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - let mut mind = seeded(); - // An unknown label, and a spec id (a revision, not a cut), are refused. - assert_eq!( - refusal(admit(&mut mind, vec![depending("2", &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), - MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } - ); - // The same batch carries the cut it depends on. - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); - // The image carries it, and any revision of the cut names it. - committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); - // A cut of another campaign is not this campaign's cut. - let mut other = cut_spec("9", 1); - other.campaign = slug("elsewhere"); - let mut elsewhere = campaign(&[REPO]); - if let D::Campaign(campaign) = &mut elsewhere { - campaign.slug = slug("elsewhere"); - } - committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("4", &["9"])])), - MindRefusal::UnknownDependency { cut: "9".into() } - ); - } - - #[test] - fn depends_on_matches_cut_labels_whole() { - let depending = |cut: &str, on: &[&str]| { - let mut spec = cut_spec(cut, 1); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - // The image carries cut 10: neither its prefix nor an extension of it names it. - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); - for near in ["1", "100"] { - assert_eq!( - refusal(admit(&mut mind, vec![depending("11", &[near])])), - MindRefusal::UnknownDependency { cut: near.into() } - ); - } - // The same holds when the cut arrives in the same batch. - for near in ["2", "200"] { - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("20", 1)), depending("21", &[near])])), - MindRefusal::UnknownDependency { cut: near.into() } - ); - } - committed(admit(&mut mind, vec![depending("12", &["10"])])); - } - - #[test] - fn a_cut_may_not_depend_on_itself() { - let depending = |cut: &str, revision: u32, on: &[&str]| { - let mut spec = cut_spec(cut, revision); - spec.depends_on = on.iter().map(|label| s(label)).collect(); - D::CutSpec(spec) - }; - let mut mind = seeded(); - // Alone in the batch, and beside another cut it legitimately names. - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 1, &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("2", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 1, &["2", "1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - // An earlier revision of the same cut in the image does not make it a real dependency. - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - assert_eq!( - refusal(admit(&mut mind, vec![depending("1", 2, &["1"])])), - MindRefusal::UnknownDependency { cut: "1".into() } - ); - } - - #[test] - fn a_report_head_may_spell_its_commit_short_or_full() { - let full = "5f98228d0123456789abcdef0123456789abcdef"; - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); - // The commit is spelled short and the head full, and the reverse. - let mut report = cut_report("1", 1); - report.range.head = eureka_pipeline::Sha(full.into()); - committed(admit(&mut mind, vec![D::CutReport(report)])); - let mut report = cut_report("1", 2); - report.commits[0].sha = eureka_pipeline::Sha(full.into()); - committed(admit(&mut mind, vec![D::CutReport(report)])); - // A head that is no commit's prefix, long or short, is outside. - let mut report = cut_report("1", 3); - report.range.head = eureka_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); - assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); - let mut report = cut_report("1", 3); - report.commits[0].sha = eureka_pipeline::Sha(full.into()); - report.range.head = eureka_pipeline::Sha("5f98229".into()); - assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); - } - - #[test] - fn a_finding_defers_to_a_cut_spec() { - let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); - let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); - committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); - // A spec superseded since is no longer a place to defer to. - let mut mind = world(); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 2)), - resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), - MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } - ); - assert_eq!( - refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), - MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } - ); - } - - #[test] - fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutReport(cut_report("1", 1))])); - let finding_id = id("finding", "cut-1.s1.F1"); - let confirmed = D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[], Some("P1"), &[])])])), - MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: "Falsified claim".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![ - verdict("1", 1, vec![claim(ClaimOutcome::Unproven, &[&finding_id], Some("P1"), &[])]), - confirmed.clone(), - ])), - MindRefusal::UnprovenClaimWithConfirmedFinding { claim: "Unproven claim".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], None, &[])])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![ - claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), - claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), - ])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - // A promise is measured by the claim that names it, not by any claim - // that names a promise: the report promises P1 and nothing else. - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P2"), &[])])])), - MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } - ); - assert_eq!( - refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &["M9"])])])), - MindRefusal::UnknownMutationLabel { label: "M9".into() } - ); - committed(admit(&mut mind, vec![ - verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&finding_id], Some("P1"), &["M1"])]), - confirmed, - ])); - } - - #[test] - fn a_finding_names_evidence_locations_and_known_invariants() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![ - D::CutSpec(cut_spec("1", 1)), - D::CutReport(cut_report("1", 1)), - verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])]), - ])); - let mut bare = finding("1", 1, "F1", FindingConfidence::Plausible); - bare.evidence = vec![]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(bare)])), MindRefusal::FindingWithoutEvidence); - // Locations are evidence's other half: a finding that names no place - // in the code is refused the same way. - let mut placeless = finding("1", 1, "F1", FindingConfidence::Plausible); - placeless.locations = vec![]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(placeless)])), MindRefusal::FindingWithoutEvidence); - let mut unknown = finding("1", 1, "F1", FindingConfidence::Plausible); - unknown.invariants = vec![l("nope")]; - assert_eq!(refusal(admit(&mut mind, vec![D::Finding(unknown)])), MindRefusal::UnknownInvariant { label: "nope".into() }); - - - // A raw envelope without `range` cannot be built from the type, and - // it is the leaf's decode that refuses it, before any organ rule. - #[derive(serde::Serialize)] - struct Rangeless { - campaign: Slug, - verdict: Short, - label: eureka_pipeline::Label, - confidence: FindingConfidence, - severity: eureka_pipeline::FindingSeverity, - claim: Line, - invariants: Vec, - locations: Vec, - failure_scenario: eureka_pipeline::Para, - evidence: Vec, - precedents: Vec, - origin: eureka_pipeline::FindingOrigin, - } - let whole = finding("1", 1, "F1", FindingConfidence::Plausible); - let rangeless = Rangeless { - campaign: whole.campaign.clone(), - verdict: whole.verdict.clone(), - label: whole.label.clone(), - confidence: whole.confidence, - severity: whole.severity, - claim: whole.claim.clone(), - invariants: whole.invariants.clone(), - locations: whole.locations.clone(), - failure_scenario: whole.failure_scenario.clone(), - evidence: whole.evidence.clone(), - precedents: vec![], - origin: whole.origin, - }; - let mut envelope = prepare(&D::Finding(whole)); - envelope.payload = rmp_serde::to_vec_named(&(rangeless,)).unwrap(); - let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Soul), vec![envelope], now()); - assert!( - matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::InvalidFormat { field, .. }) if field == "payload") - ); - - // The in-force target owns the invariant vocabulary. Supersede r1 with - // an r2 that drops its label, and the label is unknown again; r2's own - // label is what a finding may cite. The target is looked for in image - // and batch, like every other citation, so a finding may cite the - // vocabulary of the revision landing beside it: `other` is admitted in - // r2's own batch, before r2 is anywhere but here. - let mut fresh = finding("1", 1, "F4", FindingConfidence::Plausible); - fresh.invariants = vec![l("other")]; - assert_eq!( - refusal(admit(&mut mind, vec![D::Finding(fresh.clone())])), - MindRefusal::UnknownInvariant { label: "other".into() } - ); - committed(admit(&mut mind, vec![ - target(2, &["other"]), - resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), - D::Finding(fresh), - ])); - assert_eq!( - refusal(admit(&mut mind, vec![D::Finding(finding("1", 1, "F2", FindingConfidence::Plausible))])), - MindRefusal::UnknownInvariant { label: INVARIANT.into() } - ); - let mut current = finding("1", 1, "F3", FindingConfidence::Plausible); - current.invariants = vec![l("other")]; - committed(admit(&mut mind, vec![D::Finding(current)])); - } - - #[test] - fn a_campaign_needs_no_stewardship() { - let mut mind = opened(MemoryStore::new(), INSTANCE); - committed(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])); - let D::Campaign(mut empty) = campaign(&[]) else { panic!() }; - empty.slug = slug("other"); - assert_eq!( - refusal(admit(&mut mind, vec![D::Campaign(empty)])), - MindRefusal::EmptyRepos { campaign: "other:campaign:self".into() } - ); - let mut spec = cut_spec("1", 1); - spec.repo = repo(OTHER_REPO); - assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - } - - /// A target revision naming repos, and the supersession of its predecessor. - fn repo_revision(revision: u32, repos: &[&str]) -> Vec { - let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!() }; - named.repos = repos.iter().map(|name| repo(name)).collect(); - let previous = r(K::Target, &id("target", &format!("r{}", revision - 1))); - let this = r(K::Target, &id("target", &format!("r{revision}"))); - vec![D::Target(named), resolution(previous, superseded(&[this]))] - } - - fn spec_on(cut: &str, repo_name: &str) -> D { - let mut spec = cut_spec(cut, 1); - spec.repo = repo(repo_name); - D::CutSpec(spec) - } - - #[test] - fn a_target_revision_adds_a_repo_a_spec_may_name() { - let mut mind = seeded(); - assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - - // The same revision, supersession and spec in one batch. - let mut fresh = seeded(); - let mut batch = repo_revision(2, &[REPO, OTHER_REPO]); - batch.push(spec_on("1", OTHER_REPO)); - committed(admit(&mut fresh, batch)); - } - - #[test] - fn a_target_revision_removes_a_repo() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![spec_on("1", REPO)])); - committed(admit(&mut mind, repo_revision(2, &[OTHER_REPO]))); - let spec_id = id("cut_spec", "cut-1.r1"); - let spec_view = mind.view(&r(K::CutSpec, &spec_id)).expect("a valid ref").expect("the spec is held"); - assert_eq!(spec_view.status, crate::query::PipelineStatus::InForce, "spec 1 r1 stays in force"); - committed(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])); - committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &spec_id), withdrawn())])); - - let mut second = cut_spec("1", 2); - second.repo = repo(REPO); - let revised = id("cut_spec", "cut-1.r2"); - assert_eq!( - refusal(admit(&mut mind, vec![D::CutSpec(second), resolution(r(K::CutSpec, &spec_id), superseded(&[r(K::CutSpec, &revised)]))])), - MindRefusal::RepoNotInCampaign { repo: REPO.into() } - ); - committed(admit(&mut mind, vec![spec_on("2", OTHER_REPO)])); - } - - #[test] - fn a_target_revision_cannot_silently_drop_its_repos() { - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - assert_eq!( - refusal(admit(&mut mind, repo_revision(3, &[]))), - MindRefusal::EmptyRepos { campaign: id("target", "r3") } - ); - // Nothing was written: the restating revision takes the same key. - committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); - // An empty revision whose predecessor named none keeps the campaign's set. - let mut plain = seeded(); - committed(admit(&mut plain, repo_revision(2, &[]))); - } - - #[test] - fn a_repo_revision_replays_idempotently() { - let mut mind = seeded(); - let (first, _) = committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - assert_eq!( - admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO])), - PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } - ); - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - } - - /// Another campaign, opened with `repos`, and its own target revision 1 naming `named`. - fn other_campaign(repos: &[&str], named: &[&str]) -> Vec { - let D::Campaign(mut campaign) = campaign(repos) else { panic!("a campaign") }; - campaign.slug = "other-campaign".into(); - let D::Target(mut target) = target(1, &[INVARIANT]) else { panic!("a target") }; - target.campaign = "other-campaign".into(); - target.repos = named.iter().map(|name| repo(name)).collect(); - vec![D::Campaign(campaign), D::Target(target)] - } - - #[test] - fn another_campaigns_target_does_not_widen_membership() { - let mut mind = seeded(); - committed(admit(&mut mind, other_campaign(&[REPO], &[REPO, OTHER_REPO]))); - assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); - } - - #[test] - fn a_target_is_superseded_only_within_its_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); - committed(admit(&mut mind, other_campaign(&[REPO], &[]))); - let foreign = r(K::Target, "other-campaign:target:r1"); - assert_eq!( - refusal(admit(&mut mind, vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[foreign]))])), - MindRefusal::IncompatibleResolution { subject_kind: K::Target, outcome: "Superseded".into() } - ); - // Nothing was written: r2 still names the repos, and its own revision still lands. - committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); - committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); - } - - /// A target of the first campaign (`CAMPAIGN`) at `revision`, naming no repos. - fn own_target(revision: u32) -> D { - target(revision, &[INVARIANT]) - } - - const FOREIGN_TARGET: &str = "other-campaign:target:r1"; - - fn supersession_refused(outcome: PipelineAdmissionOutcome, subject_kind: K) -> bool { - refusal(outcome) == MindRefusal::IncompatibleResolution { subject_kind, outcome: "Superseded".into() } - } - - #[test] - fn a_foreign_target_in_the_same_batch_does_not_supersede() { - let mut mind = seeded(); - let mut batch = other_campaign(&[REPO], &[]); - batch.push(resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, FOREIGN_TARGET)]))); - assert!(supersession_refused(admit(&mut mind, batch), K::Target)); - } - - #[test] - fn every_supersessor_of_a_target_is_checked_whichever_is_foreign() { - for foreign_first in [false, true] { - let mut mind = seeded(); - committed(admit(&mut mind, other_campaign(&[REPO], &[]))); - let own = r(K::Target, &id("target", "r2")); - let foreign = r(K::Target, FOREIGN_TARGET); - let by = if foreign_first { [foreign, own] } else { [own, foreign] }; - let batch = vec![own_target(2), resolution(r(K::Target, &id("target", "r1")), superseded(&by))]; - assert!(supersession_refused(admit(&mut mind, batch), K::Target), "foreign first: {foreign_first}"); - } - } - - #[test] - fn a_new_campaigns_target_in_its_own_batch_is_not_closed_by_a_foreign_one() { - let mut mind = seeded(); - let mut batch = other_campaign(&[REPO], &[]); - batch.push(resolution(r(K::Target, FOREIGN_TARGET), superseded(&[r(K::Target, &id("target", "r1"))]))); - assert!(supersession_refused(admit(&mut mind, batch), K::Target)); - } - - #[test] - fn a_cut_spec_is_superseded_only_within_its_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![spec_on("1", REPO)])); - let mut batch = other_campaign(&[REPO], &[]); - let D::CutSpec(mut foreign) = spec_on("1", REPO) else { panic!("a spec") }; - foreign.campaign = "other-campaign".into(); - batch.push(D::CutSpec(foreign)); - committed(admit(&mut mind, batch)); - let lone = vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, "other-campaign:cut_spec:cut-1.r1")]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::CutSpec)); - // Nothing was written: this campaign's own revision still lands. - let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; - next.revision = 2; - let this = r(K::CutSpec, &id("cut_spec", "cut-1.r2")); - committed(admit(&mut mind, vec![D::CutSpec(next), resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[this]))])); - } - - #[test] - fn a_ruling_is_superseded_by_a_ruling_of_any_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); - let mut batch = other_campaign(&[REPO], &[]); - let mut foreign = ruling("new"); - foreign.campaign = "other-campaign".into(); - batch.push(D::Ruling(foreign)); - batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, "other-campaign:ruling:new")]))); - committed(admit(&mut mind, batch)); - } - - /// The slug that shares its dash prefix with `CAMPAIGN` (`eureka-state`). - const RIVAL: &str = "eureka-substrate"; - - /// Revision `revision` of the rival campaign's target, with its campaign when it is - /// the first and the supersession of its predecessor when it is not. - fn rival_revision(revision: u32) -> Vec { - let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!("a target") }; - named.campaign = RIVAL.into(); - let mut batch = Vec::new(); - if revision == 1 { - let D::Campaign(mut opened) = campaign(&[REPO]) else { panic!("a campaign") }; - opened.slug = RIVAL.into(); - batch.push(D::Campaign(opened)); - } - batch.push(D::Target(named)); - if revision > 1 { - let previous = r(K::Target, &format!("{RIVAL}:target:r{}", revision - 1)); - batch.push(resolution(previous, superseded(&[r(K::Target, &format!("{RIVAL}:target:r{revision}"))]))); - } - batch - } - - /// Both campaigns at revision 3, so each one's target in force is above revision 1. - fn both_campaigns_at_revision_three() -> Mind { - let mut mind = seeded(); - for revision in 2..=3 { - committed(admit(&mut mind, repo_revision(revision, &[REPO]))); - } - for revision in 1..=3 { - committed(admit(&mut mind, rival_revision(revision))); - } - mind - } - - #[test] - fn a_target_is_not_superseded_by_a_campaign_sharing_its_prefix_or_a_higher_revision() { - for (subject, supersessor) in [ - (id("target", "r3"), format!("{RIVAL}:target:r3")), - (format!("{RIVAL}:target:r3"), id("target", "r3")), - ] { - let mut mind = both_campaigns_at_revision_three(); - let lone = vec![resolution(r(K::Target, &subject), superseded(&[r(K::Target, &supersessor)]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::Target), "{subject} by {supersessor}"); - } - // A foreign revision above the subject's is no likelier to pass. - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO]))); - for revision in 1..=4 { - committed(admit(&mut mind, rival_revision(revision))); - } - let lone = vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[r(K::Target, &format!("{RIVAL}:target:r4"))]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::Target)); - } - - #[test] - fn a_stewardship_is_superseded_only_on_its_own_instance_and_repo_line() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![stewardship(INSTANCE, OTHER_REPO)])); - let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - let huginn = format!("{INSTANCE}:stewardship:gamecult_-huginn.n1"); - let lone = vec![resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &huginn)]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::Stewardship)); - // Nothing was written: the repo's own next assignment still supersedes it. - let next = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); - committed(admit(&mut mind, vec![ - stewardship_n(INSTANCE, REPO, 2), - resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &next)])), - ])); - } - - #[test] - fn a_follow_up_is_superseded_only_within_its_campaign() { - let mut mind = seeded(); - let source = r(K::Target, &id("target", "r1")); - committed(admit(&mut mind, vec![follow_up("old", source.clone())])); - let mut batch = rival_revision(1); - let D::FollowUp(mut foreign) = follow_up("new", source.clone()) else { panic!("a follow-up") }; - foreign.campaign = RIVAL.into(); - batch.push(D::FollowUp(foreign)); - committed(admit(&mut mind, batch)); - let lone = vec![resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &format!("{RIVAL}:follow_up:new"))]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::FollowUp)); - // Nothing was written: this campaign's own follow-up still supersedes it. - committed(admit(&mut mind, vec![ - follow_up("own", source), - resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "own"))])), - ])); - } - - #[test] - fn supersession_on_its_own_line_commits_for_every_kind() { - let source = r(K::Target, &id("target", "r1")); - let mut mind = seeded(); - committed(admit(&mut mind, repo_revision(2, &[REPO]))); - let mut mind = seeded(); - committed(admit(&mut mind, vec![spec_on("1", REPO)])); - let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; - next.revision = 2; - committed(admit(&mut mind, vec![ - D::CutSpec(next), - resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), - ])); - let mut mind = seeded(); - committed(admit(&mut mind, vec![follow_up("old", source.clone())])); - committed(admit(&mut mind, vec![ - follow_up("new", source), - resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "new"))])), - ])); - let mut mind = seeded(); - let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany"); - committed(admit(&mut mind, vec![ - stewardship_n(INSTANCE, REPO, 2), - resolution(r(K::Stewardship, &format!("{epiphany}.n1")), superseded(&[r(K::Stewardship, &format!("{epiphany}.n2"))])), - ])); - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); - committed(admit(&mut mind, vec![ - D::Ruling(ruling("new")), - resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &id("ruling", "new"))])), - ])); - } - - #[test] - fn a_ruling_of_one_campaign_is_superseded_by_a_ruling_of_a_prefix_sharing_campaign() { - let mut mind = seeded(); - committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); - let mut batch = rival_revision(1); - let mut foreign = ruling("new"); - foreign.campaign = RIVAL.into(); - batch.push(D::Ruling(foreign)); - batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &format!("{RIVAL}:ruling:new"))]))); - committed(admit(&mut mind, batch)); - } - - #[test] - fn a_hand_off_derives_this_minds_side_only() { - let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); - let campaign_key = id("campaign", "self"); - let stewardship_key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); - - let mut source = seeded(); - assert_eq!( - refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[])])), - MindRefusal::NotStewarded { repo: OTHER_REPO.into() } - ); - assert_eq!( - refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&id("ruling", "R1")])])), - MindRefusal::MissingReference { kind: K::Ruling, id: id("ruling", "R1") } - ); - let (_, writes) = committed(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); - let withdrawal = id_of(&stewardship_key); - assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Resolution, &withdrawal)]); - let Some(D::Resolution(derived)) = source.get(K::Resolution, &withdrawal).unwrap() else { panic!() }; - assert_eq!(derived.outcome, ResolutionOutcome::Withdrawn { reason: Line(key.clone()) }); - assert_eq!(derived.subject, r(K::Stewardship, &stewardship_key)); - - let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); - let (_, writes) = committed(admit(&mut receiving, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); - let assigned = format!("{OTHER_INSTANCE}:stewardship:gamecult_-epiphany.n1"); - assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Stewardship, &assigned)]); - let Some(D::Stewardship(derived)) = receiving.get(K::Stewardship, &assigned).unwrap() else { panic!() }; - assert_eq!(derived.note, Line(key.clone())); - assert_eq!(derived.instance, slug(OTHER_INSTANCE)); - // The stewardship starts the day the repo was handed over, not on some - // clock of admission's own. - assert_eq!(derived.assigned_on, date()); - assert_eq!(source.envelope(K::HandOff, &key).map(|e| &e.payload), receiving.envelope(K::HandOff, &key).map(|e| &e.payload)); - - // A derived write passes A3-A7 and A8 like any other, and it counts as - // a record of its scope. A batch carrying its own assignment of the - // repo beside the hand-off is two assignments: the derived one takes - // the sequence after the batch's, and the batch's own is then out of - // sequence -- a typed refusal, not a duplicate identity the store - // complains about. - let mut colliding = opened(MemoryStore::new(), OTHER_INSTANCE); - committed(admit(&mut colliding, vec![instance(OTHER_INSTANCE)])); - assert_eq!( - refusal(admit(&mut colliding, vec![ - hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]), - stewardship(OTHER_INSTANCE, REPO), - ])), - MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 1 } - ); - } - - /// The key of the first resolution of a stewardship: the subject's root, - /// then its kind and local -- the subject's own sequence included -- then - /// this resolution's own sequence. - fn id_of(stewardship_key: &str) -> String { - let (root, rest) = stewardship_key.split_once(':').unwrap(); - format!("{root}:resolution:{}.n1", rest.replace(':', ".")) - } - - /// The second construction site of a receipt, deliberately: a receipt - /// planted through the store must be found by replay and must match. - #[test] - fn a_planted_receipt_with_other_content_is_refused_not_replayed() { - let store = MemoryStore::new(); - let mut mind = opened(store.clone(), INSTANCE); - seed(&mut mind); - let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; - let ordinal = receipt::head(&mind).unwrap() + 1; - let candidate = - receipt::candidate(&slug(INSTANCE), provenance(Faculty::Hands), &[], &envelopes, ordinal, now()).unwrap(); - let mut planted = HuginnCommitReceipt { writes: vec![], ..candidate.clone() }; - planted.writes = candidate.strong_reads.clone(); - planted.receipt_id = candidate.receipt_id.clone(); - store.plant(mind.cache().prepare_entry_named(&planted.receipt_id, &planted).unwrap().0); - let mut mind = opened(store, INSTANCE); - assert!(matches!( - refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), envelopes, now())), - MindRefusal::Unavailable { .. } - )); - } -} +//! Admission: the single door into a mind. +//! +//! `admit` prepares typed documents through the leaf and calls +//! `admit_prepared`, the one commit path for Cut 8, 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: +//! +//! - A1 the batch names this mind, and attributes itself to a named agent and +//! session (`refuse_provenance`); A2 one to sixty-four envelopes; +//! - A3 every envelope passes the leaf's bounds, formats and key +//! recomputation; A4 identities are unique in the batch; A5 every document +//! carrying an instance names this mind; +//! - A6 an empty mind's first batch carries exactly one `instance` document, +//! and a batch carrying one derives the epoch record; +//! - A7 every reference names a document present in the image or the batch, +//! looked up by `(kind, id)`; the ones resolved in the image become the +//! receipt's strong reads; +//! - the derived writes (a ruling's `Answered` resolution, a hand-off's +//! stewardship withdrawal or assignment) are appended and pass A3-A7; +//! - A8 the per-kind rules, below; A9 an exact replay answers with the stored +//! receipt, after validation and never before it (ruling 20); A10 no write +//! collides with the image; A11 the commit. +//! +//! Every cross-field and cross-document rule lives here and nowhere else: +//! the leaf never gains one, the daemon and the client never re-derive one. +//! In force, the sequences and the scopes are `docs.rs`'s, shared with the +//! views (Cut 9). + +use std::collections::BTreeSet; + +use chrono::{DateTime, Utc}; +use cultcache_rs::{CultCache, CultCacheEnvelope}; +use eureka_pipeline::{ + ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal, + PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key, + validate_pipeline_write_envelope, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::docs::{CitationRole, Docs, Staged, citations, kind_of_id, line_of, outcome_citations}; +use crate::mind::{HuginnMindEpoch, Mind, unavailable}; +use crate::persona::{self, HuginnPersonaEntry, PersonaPutOutcome}; +use crate::receipt::{self, CommitOutcome, PipelineProvenance}; +use crate::refusal::{MindRefusal, RunId}; +use crate::store::MindStore; + +/// The most envelopes one batch may carry. +pub const BATCH_MAX: usize = 64; + +/// One admission request: the mind it is for, who asks, and the documents. +/// Rides the wire whole (`wire::HuginnMindRequest::Admit`). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct PipelineAdmissionBatch { + pub instance: Slug, + pub provenance: PipelineProvenance, + pub documents: Vec, +} + +/// How admission ended. `Committed.writes` names every document the batch +/// landed, derived writes included; Cut 11 indexes from it after `admit` +/// returns, and never inside it. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum PipelineAdmissionOutcome { + Committed { receipt_id: String, committed_at: String, writes: Vec }, + AlreadyAdmitted { receipt_id: String }, + Refused(MindRefusal), + Conflict { identities: Vec }, +} + +/// Keys a document through the leaf, then encodes it. A key the leaf cannot +/// compose is the document's fault and is refused as a document; only the +/// cache's encode fault is the organ's. +fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result { + pipeline_key(document).map_err(MindRefusal::Document)?; + document.prepare(cache).map_err(unavailable) +} + +impl Mind { + /// Validates and prepares every document through the leaf, then admits + /// the envelopes. + pub fn admit(&mut self, batch: PipelineAdmissionBatch, now: DateTime) -> PipelineAdmissionOutcome { + let mut envelopes = Vec::with_capacity(batch.documents.len()); + for document in &batch.documents { + if let Err(refusal) = document.validate() { + return PipelineAdmissionOutcome::Refused(MindRefusal::Document(refusal)); + } + match prepare(document, self.cache()) { + Ok(envelope) => envelopes.push(envelope), + Err(refusal) => return PipelineAdmissionOutcome::Refused(refusal), + } + } + self.admit_prepared(&batch.instance, batch.provenance, envelopes, now) + } + + /// The single commit path: envelopes prepared by anyone, validated here + /// whoever prepared them. + pub fn admit_prepared( + &mut self, + instance: &Slug, + provenance: PipelineProvenance, + envelopes: Vec, + now: DateTime, + ) -> PipelineAdmissionOutcome { + match self.admit_steps(instance, provenance, envelopes, now) { + Ok(outcome) => outcome, + Err(refusal) => PipelineAdmissionOutcome::Refused(refusal), + } + } + + fn admit_steps( + &mut self, + instance: &Slug, + provenance: PipelineProvenance, + envelopes: Vec, + now: DateTime, + ) -> Result { + // A1 + self.require_instance(instance)?; + refuse_provenance(&provenance)?; + // A2 + if envelopes.is_empty() || envelopes.len() > BATCH_MAX { + return Err(MindRefusal::BatchSize { actual: envelopes.len() as u32 }); + } + let mind = self.instance().clone(); + let mut docs = Docs::from_image(self.envelopes())?; + // A3, A5, then A4 on push. + for envelope in envelopes { + docs.push(stage(envelope, &mind)?)?; + } + // A6 + let carries_instance = docs.batch.iter().filter(|staged| staged.kind == PipelineKind::Instance).count() == 1; + if self.is_empty() && !carries_instance { + return Err(MindRefusal::MissingIdentity); + } + // A7 + let mut strong = BTreeSet::new(); + for staged in &docs.batch { + resolve(&docs, staged, &mind, &mut strong)?; + } + // Derived writes pass A3-A7 themselves. + let originals = docs.batch.len(); + for document in derive(&docs, &mind) { + document.validate()?; + let envelope = prepare(&document, self.cache())?; + docs.push(stage(envelope, &mind)?)?; + } + for staged in &docs.batch[originals..] { + resolve(&docs, staged, &mind, &mut strong)?; + } + // The pins: cited image bytes, and every write. + let strong_reads = strong + .iter() + .filter_map(|(type_id, key)| self.raw_envelope(type_id, key).cloned()) + .collect::>(); + let mut writes = docs.batch.iter().map(|staged| staged.envelope.clone()).collect::>(); + if carries_instance { + writes.push(HuginnMindEpoch::envelope(self.cache())?); + } + let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; + // A8 + for staged in &docs.batch { + check(&docs, staged, &mind)?; + } + // A9 + if let Some(existing) = receipt::replay(self, &candidate)? { + return Ok(PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: existing.receipt_id }); + } + // A10 + refuse_collisions(&docs)?; + // A11 + let landed = docs.batch.iter().map(|staged| PipelineRef { kind: staged.kind, id: Short(staged.key.clone()) }).collect(); + match receipt::commit(self, candidate, strong_reads, writes)? { + CommitOutcome::Committed(receipt) => Ok(PipelineAdmissionOutcome::Committed { + receipt_id: receipt.receipt_id, + committed_at: receipt.committed_at, + writes: landed, + }), + CommitOutcome::Conflict(identities) => Ok(PipelineAdmissionOutcome::Conflict { identities }), + } + } + + /// Replaces the mind's persona document whole, or refuses. One door for + /// the one document a mind holds, outside every pipeline batch: the same + /// instance and provenance checks as `admit`, then the persona's five + /// rules in order (schema-valid, `personaId` is this mind, canonical + /// provenance, `expected_updated_at` is the stored `updatedAt`, the new + /// `updatedAt` is later), then the same commit primitive. The stored + /// document is the commit's strong read, so the swap fails if it changed. + pub fn put_persona( + &mut self, + instance: &Slug, + state: Value, + expected_updated_at: Option, + provenance: PipelineProvenance, + now: DateTime, + ) -> PersonaPutOutcome { + match self.persona_steps(instance, state, expected_updated_at, provenance, now) { + Ok(receipt_id) => PersonaPutOutcome::Committed { receipt_id }, + Err(refusal) => PersonaPutOutcome::Refused(refusal), + } + } + + fn persona_steps( + &mut self, + instance: &Slug, + state: Value, + expected_updated_at: Option, + provenance: PipelineProvenance, + now: DateTime, + ) -> Result { + self.require_instance(instance)?; + refuse_provenance(&provenance)?; + // The persona rides a mind that has its identity (A6's analogue). + if self.is_empty() { + return Err(MindRefusal::MissingIdentity); + } + // P1 + persona::validate_shape(&state)?; + // P2 + let mind = self.instance().clone(); + if state.get("personaId").and_then(Value::as_str) != Some(mind.0.as_str()) { + return Err(MindRefusal::PersonaForeign { instance: mind.0 }); + } + // P3 + if state.pointer("/provenance/authority").and_then(Value::as_str) != Some("canonical") { + return Err(MindRefusal::PersonaNotCanonical); + } + // P4 + let stored = self.stored_updated_at()?; + if stored != expected_updated_at { + return Err(MindRefusal::PersonaStale { stored }); + } + // P5 + if let Some(stored) = &stored { + let instant = |stamp: &str| { + DateTime::parse_from_rfc3339(stamp).map_err(|_| MindRefusal::PersonaInvalid { + path: "/updatedAt".into(), + message: "is not an RFC 3339 date-time".into(), + }) + }; + let offered = state.get("updatedAt").and_then(Value::as_str).map(instant).transpose()?; + if offered <= Some(instant(stored)?) { + return Err(MindRefusal::PersonaInvalid { + path: "/updatedAt".into(), + message: "must be later than the stored updatedAt".into(), + }); + } + } + let (envelope, _) = self + .cache() + .prepare_entry_named(mind.0.clone(), &HuginnPersonaEntry { value: state }) + .map_err(unavailable)?; + let strong_reads = self.persona_envelope().cloned().into_iter().collect::>(); + let writes = vec![envelope]; + let candidate = receipt::candidate(&mind, provenance, &strong_reads, &writes, receipt::head(self)? + 1, now)?; + match receipt::commit(self, candidate, strong_reads, writes)? { + CommitOutcome::Committed(receipt) => Ok(receipt.receipt_id), + CommitOutcome::Conflict(_) => Err(MindRefusal::PersonaStale { stored: self.stored_updated_at()? }), + } + } +} + +/// The bound `eureka_pipeline::Short` declares, in UTF-8 bytes. The leaf +/// exposes no validator for it, so `short_bound_is_the_leafs_own` pins this to +/// the `maxLength` its schema publishes. +const SHORT_MAX: usize = 200; + +/// Who admitted a batch is the receipt's only record of it, so the mind checks +/// it here and every writer, whatever its transport, gets the check: the agent, +/// the session and the tool follow the leaf's `Title` rule (one alphanumeric +/// character, no control or line-break character, no bidi control) within +/// `Short`'s bound. The leaf keeps that rule private, so +/// `provenance_text_is_the_leafs_title_rule` pins this to it. Refused as the +/// leaf's own refusals, since the fields are the leaf's own type. +fn refuse_provenance(provenance: &PipelineProvenance) -> Result<(), MindRefusal> { + let bidi = |c: char| { + matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') + }; + let control = |c: char| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'); + for (field, value) in [ + ("provenance.agent", &provenance.agent), + ("provenance.session", &provenance.session), + ("provenance.tool", &provenance.tool), + ] { + let text = value.0.as_str(); + if !text.chars().any(char::is_alphanumeric) || text.contains(control) || text.contains(bidi) { + return Err(PipelineRefusal::InvalidFormat { field: field.into(), value: text.into() }.into()); + } + if text.len() > SHORT_MAX { + return Err(PipelineRefusal::FieldBound { field: field.into(), limit: SHORT_MAX as u32, actual: text.len() as u32 }.into()); + } + } + Ok(()) +} + +/// A3 through the leaf, then A5. +fn stage(envelope: CultCacheEnvelope, mind: &Slug) -> Result { + validate_pipeline_write_envelope(&envelope)?; + let document = PipelineDocument::decode(&envelope)?; + refuse_foreign_instance(&document, mind)?; + Ok(Staged { kind: document.kind(), key: envelope.key.clone(), document, envelope }) +} + +/// A5: every document carrying an instance field names this mind (a run +/// included); a hand-off names it on one side. +fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(), MindRefusal> { + let foreign = |declared: &Slug| MindRefusal::ForeignInstance { declared: declared.0.clone(), mind: mind.0.clone() }; + match document { + PipelineDocument::Instance(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)), + PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => { + Err(foreign(&value.from_instance)) + } + _ => Ok(()), + } +} + +/// What a missing referent is called, by the field that cited it. +enum Missing { + Reference, + SpecOfReport(String), + Supersessor, +} + +struct Reference { + kind: PipelineKind, + id: String, + missing: Missing, +} + +/// A7's field list is `docs::citations`: every `PipelineRef` and every +/// full-id `Short` field, each carrying the refusal its absence earns. A +/// hand-off's documents are cited on the source side only, and each must read +/// as an id of a kind; the receiving side imports them afterwards (Cut 12). +fn references(staged: &Staged, mind: &Slug) -> Result, MindRefusal> { + if let PipelineDocument::HandOff(hand_off) = &staged.document { + if hand_off.from_instance != *mind { + return Ok(Vec::new()); + } + if let Some(document) = hand_off.documents.iter().find(|document| kind_of_id(&document.0).is_none()) { + return Err(PipelineRefusal::InvalidFormat { + field: "hand_off.documents".into(), + value: document.0.clone(), + } + .into()); + } + } + Ok(citations(&staged.document) + .into_iter() + .map(|(role, target)| Reference { + kind: target.kind, + id: target.id.0, + missing: match role { + CitationRole::CutSpec => Missing::SpecOfReport(staged.key.clone()), + CitationRole::SupersededBy => Missing::Supersessor, + _ => Missing::Reference, + }, + }) + .collect()) +} + +/// A7: every referent is in the batch or the image; the image ones are the +/// receipt's strong reads. +fn resolve(docs: &Docs, staged: &Staged, mind: &Slug, strong: &mut BTreeSet<(String, String)>) -> Result<(), MindRefusal> { + for reference in references(staged, mind)? { + if docs.in_batch(reference.kind, &reference.id).is_some() { + continue; + } + if docs.in_image(reference.kind, &reference.id).is_some() { + strong.insert((reference.kind.type_id().to_string(), reference.id)); + continue; + } + return Err(match reference.missing { + Missing::Reference => MindRefusal::MissingReference { kind: reference.kind, id: reference.id }, + Missing::SpecOfReport(report) => MindRefusal::CutReportWithoutSpec { report }, + Missing::Supersessor => MindRefusal::UnknownSupersessor { id: reference.id }, + }); + } + Ok(()) +} + +/// The writes admission derives from the batch: 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. One the batch already carries is not +/// derived again. +fn derive(docs: &Docs, mind: &Slug) -> Vec { + use PipelineDocument as D; + use PipelineKind as K; + let mut derived = Vec::new(); + for staged in &docs.batch { + match &staged.document { + D::Ruling(ruling) => { + if let Some(question) = &ruling.answers { + let subject = PipelineRef { kind: K::Question, id: question.clone() }; + let sequence = docs.derived_resolution_sequence(&subject, |outcome| { + matches!(outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == staged.key) + }); + derived.push(D::Resolution(PipelineResolution { + sequence, + subject, + outcome: ResolutionOutcome::Answered { by: PipelineRef { kind: K::Ruling, id: Short(staged.key.clone()) } }, + rationale: ruling.ruling.clone(), + resolved_on: ruling.ruled_on.clone(), + })); + } + } + D::HandOff(hand_off) => { + if hand_off.from_instance == *mind + && let Some((stewardship_key, _)) = docs.stewardship_of(mind, &hand_off.repo, Some(&staged.key)) + { + let subject = PipelineRef { kind: K::Stewardship, id: Short(stewardship_key.to_string()) }; + let sequence = docs.derived_resolution_sequence(&subject, |outcome| { + matches!(outcome, ResolutionOutcome::Withdrawn { reason } if reason.0 == staged.key) + }); + derived.push(D::Resolution(PipelineResolution { + sequence, + subject, + outcome: ResolutionOutcome::Withdrawn { reason: Line(staged.key.clone()) }, + rationale: hand_off.reason.clone(), + resolved_on: hand_off.handed_on.clone(), + })); + } + if hand_off.to_instance == *mind { + derived.push(D::Stewardship(PipelineStewardship { + instance: mind.clone(), + sequence: docs.derived_stewardship_sequence(mind, &hand_off.repo, &staged.key), + repo: hand_off.repo.clone(), + assigned_on: hand_off.handed_on.clone(), + note: Line(staged.key.clone()), + })); + } + } + _ => {} + } + } + derived.retain(|document| !docs.batch.iter().any(|staged| staged.document == *document)); + derived +} + +/// A8: the per-kind rules. +fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { + use PipelineDocument as D; + use PipelineKind as K; + let key = staged.key.as_str(); + match &staged.document { + D::Campaign(campaign) => { + if campaign.repos.is_empty() { + return Err(MindRefusal::EmptyRepos { campaign: key.into() }); + } + Ok(()) + } + D::Target(target) => { + let predecessor = D::Target(eureka_pipeline::PipelineTarget { revision: target.revision.wrapping_sub(1), ..target.clone() }); + revision_rule(docs, K::Target, key, target.revision, &predecessor)?; + // A revision past the first may not empty the repo set its predecessor + // named: empty means "keep the previous set", and that would silently + // narrow it to the campaign's opening set (ruling campaign-repo-set-home). + if target.repos.is_empty() && target.revision > 1 { + let named = matches!(docs.find(K::Target, &pipeline_key(&predecessor)?), Some(D::Target(previous)) if !previous.repos.is_empty()); + if named { + return Err(MindRefusal::EmptyRepos { campaign: key.into() }); + } + } + unique_labels("target.invariants", target.invariants.iter().map(|invariant| invariant.label.0.as_str())) + } + D::Question(question) => { + if question.options.len() < 2 || !question.options.iter().any(|option| option.label == question.recommended) { + return Err(MindRefusal::InvalidOptions { question: key.into() }); + } + unique_labels("question.options", question.options.iter().map(|option| option.label.0.as_str())) + } + D::Ruling(ruling) => { + if ruling.operator_quote.is_some() && ruling.authority != RulingAuthority::Operator { + return Err(MindRefusal::QuoteWithoutOperator { ruling: key.into() }); + } + let Some(question_id) = &ruling.answers else { return Ok(()) }; + let Some(D::Question(question)) = docs.find(K::Question, &question_id.0) else { + return Err(MindRefusal::MissingReference { kind: K::Question, id: question_id.0.clone() }); + }; + let own = |resolution: &PipelineResolution| { + matches!(&resolution.outcome, ResolutionOutcome::Answered { by } if by.kind == K::Ruling && by.id.0 == key) + }; + if !docs.in_force_unless(K::Question, &question_id.0, own) { + return Err(MindRefusal::AlreadyResolved { subject: question_id.0.clone() }); + } + let choice = ruling.choice.as_ref().map_or("", |choice| choice.0.as_str()); + if !question.options.iter().any(|option| option.label.0 == choice) { + return Err(MindRefusal::InvalidChoice { ruling: key.into(), choice: choice.into() }); + } + Ok(()) + } + D::CutSpec(spec) => { + let campaign = docs.of_kind(K::Campaign).find_map(|(_, document)| match document { + D::Campaign(campaign) if campaign.slug == spec.campaign => Some(campaign), + _ => None, + }); + let Some(campaign) = campaign else { + return Err(MindRefusal::MissingReference { kind: K::Campaign, id: spec.campaign.0.clone() }); + }; + if !docs.repos_in_force(campaign).contains(&spec.repo) { + return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() }); + } + for ruling in &spec.rulings { + if !docs.in_force(K::Ruling, &ruling.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); + } + } + for dependency in &spec.depends_on { + // A cut naming itself is no ordering (ruling self-dep). + if dependency.0 == spec.cut.0 { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { + matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) + }); + if !names_a_cut { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + } + let predecessor = D::CutSpec(eureka_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); + revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) + } + D::CutReport(report) => { + let Some(D::CutSpec(spec)) = docs.find(K::CutSpec, &report.cut_spec.0) else { + return Err(MindRefusal::CutReportWithoutSpec { report: key.into() }); + }; + if !docs.in_force(K::CutSpec, &report.cut_spec.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: report.cut_spec.0.clone() }); + } + if report.repo != spec.repo { + return Err(MindRefusal::SpecMismatch { field: "repo".into() }); + } + if report.branch != spec.branch { + return Err(MindRefusal::SpecMismatch { field: "branch".into() }); + } + if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { + return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); + } + Ok(()) + } + D::Verdict(verdict) => { + let Some(D::CutReport(report)) = docs.find(K::CutReport, &verdict.cut_report.0) else { + return Err(MindRefusal::MissingReference { kind: K::CutReport, id: verdict.cut_report.0.clone() }); + }; + for claim in &verdict.claims { + let confirmed = claim.findings.iter().filter_map(|finding| docs.find(K::Finding, &finding.0)).any(|document| { + matches!(document, D::Finding(finding) if finding.confidence == FindingConfidence::Confirmed) + }); + match claim.outcome { + ClaimOutcome::Falsified if !confirmed => { + return Err(MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: claim.claim.0.clone() }); + } + ClaimOutcome::Unproven if confirmed => { + return Err(MindRefusal::UnprovenClaimWithConfirmedFinding { claim: claim.claim.0.clone() }); + } + _ => {} + } + for label in &claim.mutations { + if !report.mutations.iter().any(|mutation| mutation.label == *label) { + return Err(MindRefusal::UnknownMutationLabel { label: label.0.clone() }); + } + } + } + for promise in &report.promises { + let count = verdict.claims.iter().filter(|claim| claim.promise.as_ref() == Some(&promise.label)).count(); + if count != 1 { + return Err(MindRefusal::PromiseWithoutVerdict { promise: promise.label.0.clone() }); + } + } + Ok(()) + } + D::Finding(finding) => { + if finding.evidence.is_empty() || finding.locations.is_empty() { + return Err(MindRefusal::FindingWithoutEvidence); + } + let target = docs.of_kind(K::Target).find_map(|(target_key, document)| match document { + D::Target(target) if target.campaign == finding.campaign && docs.in_force(K::Target, target_key) => Some(target), + _ => None, + }); + for label in &finding.invariants { + if !target.is_some_and(|target| target.invariants.iter().any(|invariant| invariant.label == *label)) { + return Err(MindRefusal::UnknownInvariant { label: label.0.clone() }); + } + } + Ok(()) + } + D::Resolution(resolution) => resolution_rule(docs, resolution), + D::Stewardship(stewardship) => { + let expected = docs.latest_stewardship(&stewardship.instance, &stewardship.repo, Some(stewardship)) + 1; + if stewardship.sequence != expected { + return Err(MindRefusal::StewardshipOutOfSequence { + repo: stewardship.repo.0.clone(), + expected, + actual: stewardship.sequence, + }); + } + if docs.stewardships_of(mind, &stewardship.repo, None).iter().any(|(other, _)| *other != key) { + return Err(MindRefusal::AlreadyStewarded { repo: stewardship.repo.0.clone() }); + } + Ok(()) + } + D::HandOff(hand_off) => { + if hand_off.from_instance == *mind && docs.stewardship_of(mind, &hand_off.repo, Some(key)).is_none() { + return Err(MindRefusal::NotStewarded { repo: hand_off.repo.0.clone() }); + } + Ok(()) + } + // run-is-the-grant: the claim is the one consumption fact. A claim must + // be in force, and no other run in force may hold it. A run already in + // the image under this key, byte for byte, is a replay and owes its + // claims no fresh freedom, so the replay still answers AlreadyAdmitted + // after the work it claimed has moved on. One live run of hers + // per instance and turn (ruling one-live-self-run) is decided by the same + // rule. + D::Run(run) => { + let replay = docs.in_image(K::Run, key) == Some(&staged.document); + run_rule(docs, key, run, replay) + } + D::FollowUp(_) | D::Instance(_) => Ok(()), + } +} + +/// The one rule for a run opening or reinstated by withdrawing its close: every +/// claim in force (a replay of an admitted run owes none) and held by no other +/// live run, then at most one live run of hers per instance and turn. +fn run_rule(docs: &Docs, run_key: &str, run: &PipelineRun, replay: bool) -> Result<(), MindRefusal> { + for claim in &run.claims { + if !replay && !docs.in_force(claim.kind, &claim.id.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: claim.kind, id: claim.id.0.clone() }); + } + if let Some(holder) = docs.claim_holder(run_key, claim) { + return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: RunId::held(holder) }); + } + } + if !replay && docs.in_force(PipelineKind::Run, run_key) && let Some(holder) = docs.live_holder(run_key, run) { + return Err(MindRefusal::AlreadyLive { run: RunId::held(holder) }); + } + Ok(()) +} + +/// Revision 1 stands alone; revision N needs a resolution in the batch that +/// supersedes revision N-1 by this document. The predecessor's key is the +/// leaf's, from the same document one revision back. +fn revision_rule(docs: &Docs, kind: PipelineKind, key: &str, revision: u32, predecessor: &PipelineDocument) -> Result<(), MindRefusal> { + if revision == 1 { + return Ok(()); + } + let refusal = || MindRefusal::RevisionWithoutSupersession { kind, revision }; + if revision == 0 { + return Err(refusal()); + } + let predecessor_key = pipeline_key(predecessor)?; + let superseded = docs.batch.iter().any(|staged| { + matches!(&staged.document, PipelineDocument::Resolution(resolution) + if resolution.subject.kind == kind + && resolution.subject.id.0 == predecessor_key + && matches!(&resolution.outcome, ResolutionOutcome::Superseded { by } + if by.iter().any(|supersessor| supersessor.kind == kind && supersessor.id.0 == key))) + }); + if superseded { Ok(()) } else { Err(refusal()) } +} + +fn unique_labels<'a>(field: &str, labels: impl Iterator) -> Result<(), MindRefusal> { + let mut seen = BTreeSet::new(); + for label in labels { + if !seen.insert(label) { + return Err(MindRefusal::DuplicateLabel { field: field.into(), label: label.into() }); + } + } + Ok(()) +} + +fn outcome_name(outcome: &ResolutionOutcome) -> &'static str { + match outcome { + ResolutionOutcome::Superseded { .. } => "Superseded", + ResolutionOutcome::Answered { .. } => "Answered", + ResolutionOutcome::Fixed { .. } => "Fixed", + ResolutionOutcome::Deferred { .. } => "Deferred", + ResolutionOutcome::Recorded { .. } => "Recorded", + ResolutionOutcome::Withdrawn { .. } => "Withdrawn", + } +} + +/// The resolution matrix: which outcomes a subject kind admits, and of which +/// kinds its referents must be. Admission's, never the leaf's. +/// +/// A resolution is itself resolvable, by withdrawal alone: the operator's +/// ruling on Q17 keeps a withdrawn resolution attached to its subject rather +/// than erasing it, and a subject whose resolution is withdrawn may be +/// resolved again at the next sequence, which the leaf's key now carries. The +/// chain stops there: Q19 A caps it at depth two, in `resolution_rule`, since +/// a withdrawal that could itself be withdrawn would leave two closures +/// standing over one subject. +fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { + use PipelineKind as K; + use ResolutionOutcome as O; + let all = |by: &[PipelineRef], kind: K| by.iter().all(|supersessor| supersessor.kind == kind); + match (subject, outcome) { + (K::Target, O::Superseded { by }) => all(by, K::Target), + (K::Question, O::Answered { by }) => by.kind == K::Ruling, + (K::Question, O::Withdrawn { .. }) => true, + (K::Ruling, O::Superseded { by }) => all(by, K::Ruling), + (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), + (K::CutSpec, O::Withdrawn { .. }) => true, + (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), + (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), + (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, + (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), + (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), + (K::FollowUp, O::Withdrawn { .. }) => true, + (K::Stewardship, O::Superseded { by }) => all(by, K::Stewardship), + (K::Stewardship, O::Withdrawn { .. }) => true, + (K::Run, O::Recorded { .. } | O::Withdrawn { .. }) => true, + (K::Resolution, O::Withdrawn { .. }) => true, + _ => false, + } +} + +/// The resolution row: the sequence, the in-force subject, the matrix, the +/// Q19 cap and the reinstatement rule it rests on, a non-empty supersession, +/// every referent in force, and the two +/// coherence rules (an `Answered` ruling answers this question; a cut spec is +/// superseded within its cut). +fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), MindRefusal> { + use PipelineDocument as D; + use PipelineKind as K; + let subject_kind = resolution.subject.kind; + let subject_id = resolution.subject.id.0.as_str(); + if let ResolutionOutcome::Superseded { by } = &resolution.outcome && by.is_empty() { + return Err(MindRefusal::EmptySupersession); + } + // The sequence is the writer's and is checked against the image, as a + // revision is: the previous plus one, counting every record of this + // subject other than this document itself. + let expected = docs.latest_resolution(&resolution.subject, Some(resolution)) + 1; + if resolution.sequence != expected { + return Err(MindRefusal::ResolutionOutOfSequence { + subject: subject_id.into(), + expected, + actual: resolution.sequence, + }); + } + // A8: a subject with a resolution still in force is closed. A withdrawn + // one is not in force, so the subject is open and this is its next record. + if !docs.in_force_unless(subject_kind, subject_id, |other| other == resolution) { + return Err(MindRefusal::AlreadyResolved { subject: subject_id.into() }); + } + let incompatible = || MindRefusal::IncompatibleResolution { subject_kind, outcome: outcome_name(&resolution.outcome).into() }; + if !matrix(subject_kind, &resolution.outcome) { + return Err(incompatible()); + } + // Q19 A: the chain stops at depth two. A withdrawal cannot be withdrawn; + // to reinstate a closure, resolve the subject again at the next sequence. + if subject_kind == K::Resolution + && let Some(D::Resolution(subject)) = docs.find(K::Resolution, subject_id) + && subject.subject.kind == K::Resolution + { + return Err(incompatible()); + } + // Q19 A's rationale one step out from the cap: a withdrawal never + // re-raises an earlier record over a later one. Withdrawing a resolution + // puts its subject back in force, so refuse while a later record of that + // subject's own scope stands -- a repo's next assignment, a document's + // next revision. + if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) + && subject_kind == K::Resolution + && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) + && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) + && let Some(later) = docs.later_in_force(base) + { + return Err(MindRefusal::WouldReinstateOverLater { + subject: reinstating.subject.id.0.clone(), + later: later.into(), + }); + } + // Withdrawing a run's closure puts the run back in force, and a run in + // force holds its claims and its turn's slot: refuse while another run in force + // holds one of the claims or another run of hers of its turn is live. + if matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. }) + && subject_kind == K::Resolution + && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) + && reinstating.subject.kind == K::Run + && let Some(D::Run(run)) = docs.find(K::Run, &reinstating.subject.id.0) + { + run_rule(docs, &reinstating.subject.id.0, run, false)?; + } + for (_, referent) in outcome_citations(&resolution.outcome) { + if !docs.in_force(referent.kind, &referent.id.0) { + return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 }); + } + } + match &resolution.outcome { + ResolutionOutcome::Superseded { by } => { + let Some(subject) = docs.find(subject_kind, subject_id) else { return Ok(()) }; + let line = line_of(subject); + for supersessor in by { + if let Some(next) = docs.find(subject_kind, &supersessor.id.0) + && (line.is_none() || line != line_of(next)) + { + return Err(incompatible()); + } + } + } + ResolutionOutcome::Answered { by } => { + let Some(D::Ruling(ruling)) = docs.find(K::Ruling, &by.id.0) else { return Ok(()) }; + if ruling.answers.as_ref().map(|answers| answers.0.as_str()) != Some(subject_id) { + return Err(incompatible()); + } + } + _ => {} + } + Ok(()) +} + +/// A10: a write whose identity the image already holds. Every kind answers +/// the same way now that a resolution key carries a sequence: the identity is +/// taken. Whether a subject is closed is A8's question, not this one's. +fn refuse_collisions(docs: &Docs) -> Result<(), MindRefusal> { + for staged in &docs.batch { + if docs.in_image(staged.kind, &staged.key).is_some() { + return Err(MindRefusal::IdentityCollision { kind: staged.kind, id: staged.key.clone() }); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::*; + use crate::fixtures::prepare; + use crate::mind::schema_cache; + use crate::receipt::{DocumentVersion, Faculty, HuginnCommitReceipt}; + use crate::store::test_stores::{MemoryStore, RefusingStore, SwapCommand}; + use cultcache_rs::DatabaseEntry; + use eureka_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument as D, PipelineKind as K, RunTurn}; + use sha2::{Digest, Sha256}; + + fn receipt_count(mind: &Mind) -> usize { + mind.receipts().unwrap().len() + } + + #[test] + fn an_empty_store_opens_and_the_first_write_must_carry_the_instance() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + assert!(mind.is_empty()); + assert_eq!(refusal(admit(&mut mind, vec![stewardship(INSTANCE, REPO), campaign(&[REPO])])), MindRefusal::MissingIdentity); + assert!(store.rows().is_empty()); + let (receipt_id, writes) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + assert!(writes.contains(&r(K::Instance, &format!("{INSTANCE}:instance:self")))); + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts.len(), 1); + assert_eq!(receipts[0].receipt_id, receipt_id); + let identities = receipts[0].writes.iter().map(DocumentVersion::identity).collect::>(); + assert!(identities.contains(&(K::Instance.type_id(), format!("{INSTANCE}:instance:self").as_str()))); + assert!(identities.contains(&(HuginnMindEpoch::TYPE, PIPELINE_SCHEMA_EPOCH)), "the epoch record is derived: {identities:?}"); + assert!(receipts[0].strong_reads.is_empty()); + // The store now opens again as this instance, and as nothing else. + assert!(!Mind::open_with(store.clone(), &slug(INSTANCE)).unwrap().is_empty()); + assert!(Mind::open_with(store, &slug(OTHER_INSTANCE)).is_err()); + } + + /// The identity the index labels its collection with: none before the + /// first write, then the first receipt's id and never a later one. + #[test] + fn the_genesis_receipt_id_is_the_first_admissions_and_stays_put() { + let mut mind = opened(MemoryStore::new(), INSTANCE); + assert_eq!(mind.genesis_receipt_id().unwrap(), None); + let (first, _) = committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first.clone())); + let (second, _) = committed(admit(&mut mind, vec![campaign(&[REPO])])); + assert_ne!(first, second); + assert_eq!(mind.genesis_receipt_id().unwrap(), Some(first)); + } + + fn admit_attributed(agent: &str, session: &str) -> (Mind, PipelineAdmissionOutcome) { + admit_signed(agent, session, "admit") + } + + fn admit_signed(agent: &str, session: &str, tool: &str) -> (Mind, PipelineAdmissionOutcome) { + let mut mind = opened(MemoryStore::new(), INSTANCE); + let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(agent), session: s(session), tool: s(tool) }; + let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance, documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO)] }; + let outcome = mind.admit(batch, now()); + (mind, outcome) + } + + /// Who admitted a batch is validated by the mind, so no writer can commit + /// a blank, multi-line or oversized attribution, and a refused batch leaves + /// nothing behind. + #[test] + fn provenance_names_an_agent_a_session_and_a_tool_on_one_clean_line_within_short() { + let over = "x".repeat(SHORT_MAX + 1); + let exact = "y".repeat(SHORT_MAX); + for bad in [ + "", + " \t ", + "\u{2003}", + "\u{200B}", + "line\nbreak", + "line\rbreak", + "a\u{0B}b", + "a\u{0C}b", + "a\u{85}b", + "a\u{2028}b", + "a\u{2029}b", + "a\0b", + "a\u{1B}[31mb", + "a\u{202E}b", + "a\u{2066}b", + "a\u{200F}b", + ] { + for field in ["provenance.agent", "provenance.session", "provenance.tool"] { + let (mind, outcome) = match field { + "provenance.agent" => admit_signed(bad, "session-1", "admit"), + "provenance.session" => admit_signed("claude", bad, "admit"), + _ => admit_signed("claude", "session-1", bad), + }; + let refused = refusal(outcome); + assert!( + matches!(&refused, MindRefusal::Document(PipelineRefusal::InvalidFormat { field: named, .. }) if named == field), + "{field}={bad:?}: {refused:?}" + ); + assert!(mind.is_empty(), "{field}={bad:?}: a refused batch wrote"); + } + } + for (field, outcome) in [ + ("provenance.agent", admit_signed(&over, "session-1", "admit")), + ("provenance.session", admit_signed("claude", &over, "admit")), + ("provenance.tool", admit_signed("claude", "session-1", &over)), + ] { + let (mind, outcome) = outcome; + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::FieldBound { field: field.into(), limit: 200, actual: 201 }) + ); + assert!(mind.is_empty()); + } + // The bound is in bytes, not characters: 101 two-byte characters are 202 bytes. + let (_, outcome) = admit_attributed(&"é".repeat(101), "session-1"); + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::FieldBound { field: "provenance.agent".into(), limit: 200, actual: 202 }) + ); + let (_, outcome) = admit_signed("claude", "session-1", &"é".repeat(101)); + assert!(matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::FieldBound { actual: 202, .. }))); + let (_, outcome) = admit_attributed(&"é".repeat(100), "session-1"); + committed(outcome); + // The bound is inclusive and a name with inner spaces is a name. + let (mind, outcome) = admit_signed(&exact, "session 7", "admit tool"); + committed(outcome); + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts[0].provenance.agent, s(&exact)); + assert_eq!(receipts[0].provenance.session, s("session 7")); + assert_eq!(receipts[0].provenance.tool, s("admit tool")); + } + + /// The read bounds are the leaf's: admission applies the leaf's validation + /// and adds none. Sixteen anchors of a hundred lines admit and read back; + /// a seventeenth refuses at `cut_spec.reads` and nothing is written. + #[test] + fn reads_admit_at_the_bound_and_refuse_over_it() { + let anchors = |count: usize| { + (0..count) + .map(|n| eureka_pipeline::ReadAnchor { + location: eureka_pipeline::CodeLocation { + path: s("crates/eureka-pipeline/src/lib.rs"), + line: 1, + end_line: Some(100), + symbol: Some(format!("symbol_{n}").as_str().into()), + }, + why: s("read"), + }) + .collect::>() + }; + let mut mind = seeded(); + let mut spec = cut_spec("1", 1); + spec.reads = anchors(16); + committed(admit(&mut mind, vec![D::CutSpec(spec.clone())])); + let held = mind.view(&r(K::CutSpec, &id("cut_spec", "cut-1.r1"))).unwrap().expect("the spec is held"); + assert_eq!(held.document, D::CutSpec(spec)); + + let mut over = cut_spec("2", 1); + over.reads = anchors(17); + let receipts = mind.receipts().unwrap().len(); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(over)])), + MindRefusal::Document(PipelineRefusal::FieldBound { field: "cut_spec.reads".into(), limit: 16, actual: 17 }) + ); + assert_eq!(mind.receipts().unwrap().len(), receipts); + assert_eq!(mind.view(&r(K::CutSpec, &id("cut_spec", "cut-2.r1"))).unwrap(), None, "nothing is written"); + } + + /// The leaf's `Title` rule is private, so this holds the provenance rule to + /// it: the two accept and refuse the same text. + #[test] + fn provenance_text_is_the_leafs_title_rule() { + let leaf_accepts = |text: &str| { + let D::Campaign(mut campaign) = campaign(&[REPO]) else { unreachable!() }; + campaign.title = eureka_pipeline::Title(text.into()); + D::Campaign(campaign).validate().is_ok() + }; + let mine_accepts = |text: &str| { + let provenance = PipelineProvenance { faculty: Faculty::Hands, agent: s(text), session: s("s"), tool: s("t") }; + refuse_provenance(&provenance).is_ok() + }; + for text in [ + "claude", "session 7", "", " ", "\t", "\u{200B}", "\u{2003}", "!!!", "a\nb", "a\u{0B}b", "a\u{85}b", "a\u{2028}b", + "a\0b", "a\u{7F}b", "a\u{202E}b", "a\u{061C}b", "a\u{2069}b", "a\u{200D}b", "é", "日本", "a\u{FEFF}b", + &"x".repeat(200), &"x".repeat(201), &"é".repeat(100), &"é".repeat(101), + ] { + assert_eq!(mine_accepts(text), leaf_accepts(text), "{text:?}"); + } + } + + #[test] + fn short_bound_is_the_leafs_own() { + let schema = serde_json::to_value(schemars::schema_for!(Short)).unwrap(); + assert_eq!(schema["maxLength"], serde_json::json!(SHORT_MAX)); + } + + #[test] + fn admission_refuses_a_foreign_instance_whatever_the_transport() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let foreign = MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() }; + let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; + assert_eq!( + refusal(mind.admit_prepared(&slug(OTHER_INSTANCE), provenance(Faculty::Hands), envelopes, now())), + foreign + ); + assert_eq!(refusal(admit(&mut mind, vec![stewardship(OTHER_INSTANCE, OTHER_REPO)])), foreign); + assert_eq!( + refusal(admit(&mut mind, vec![hand_off("a", "b", REPO, &[])])), + MindRefusal::ForeignInstance { declared: "a".into(), mind: INSTANCE.into() } + ); + assert_eq!(refusal(admit(&mut mind, vec![instance(OTHER_INSTANCE)])), foreign); + assert_eq!(store.rows(), before); + } + + #[test] + fn two_minds_in_one_state_root_stay_separate() { + let root = tempfile::tempdir().unwrap(); + let mut yggdrasil = Mind::open(root.path(), &slug(INSTANCE)).unwrap(); + let mut thought_cage = Mind::open(root.path(), &slug(OTHER_INSTANCE)).unwrap(); + assert_ne!( + Mind::store_path_for(root.path(), &slug(INSTANCE)).unwrap(), + Mind::store_path_for(root.path(), &slug(OTHER_INSTANCE)).unwrap() + ); + committed(admit(&mut yggdrasil, vec![instance(INSTANCE), stewardship(INSTANCE, REPO)])); + committed(admit(&mut thought_cage, vec![instance(OTHER_INSTANCE)])); + let key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert!(yggdrasil.envelope(K::Stewardship, &key).is_some()); + assert!(thought_cage.envelope(K::Stewardship, &key).is_none()); + assert_eq!(yggdrasil.envelopes().len(), 4); + assert_eq!(thought_cage.envelopes().len(), 3); + } + + #[test] + fn keys_are_recomputed_and_a_forged_key_refuses() { + let mut mind = seeded(); + let mut envelope = prepare(&question("Q1", &["A", "B"], "A")); + let expected = envelope.key.clone(); + envelope.key = format!("{expected}-forged"); + let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), vec![envelope.clone()], now()); + assert_eq!( + refusal(outcome), + MindRefusal::Document(PipelineRefusal::InvalidIdentity { kind: K::Question, key: envelope.key, expected }) + ); + } + + #[test] + fn references_must_exist_in_image_or_batch() { + let mut mind = seeded(); + let absent_question = id("question", "Q9"); + let mut answering = ruling("R1"); + answering.answers = Some(s(&absent_question)); + answering.choice = Some(l("A")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering)])), + MindRefusal::MissingReference { kind: K::Question, id: absent_question } + ); + let absent_finding = id("finding", "cut-1.s1.F9"); + assert_eq!( + refusal(admit(&mut mind, vec![follow_up("FU-1", r(K::Finding, &absent_finding))])), + MindRefusal::MissingReference { kind: K::Finding, id: absent_finding } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])])])), + MindRefusal::MissingReference { kind: K::CutReport, id: id("cut_report", "cut-1.h1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])), + MindRefusal::MissingReference { kind: K::Question, id: id("question", "Q1") } + ); + // A well-formed id of another kind is a missing reference of the + // cited kind, not a wrong-kind refusal. + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("question", "Q1"))]; + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(spec)])), + MindRefusal::MissingReference { kind: K::Ruling, id: id("question", "Q1") } + ); + } + + #[test] + fn batch_is_all_or_nothing() { + let store = RefusingStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let receipts = receipt_count(&mind); + let mut quoted = ruling("R1"); + quoted.operator_quote = Some("go ahead".into()); + let batch = vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "B"), D::Ruling(quoted)]; + assert_eq!(refusal(admit(&mut mind, batch)), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + + store.command(SwapCommand::Lose); + let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); + assert!(matches!(outcome, PipelineAdmissionOutcome::Conflict { .. }), "{outcome:?}"); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + + store.command(SwapCommand::Fail); + assert!(matches!(refusal(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])), MindRefusal::Unavailable { .. })); + assert_eq!(store.rows(), before); + + store.command(SwapCommand::Delegate); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + assert_eq!(receipt_count(&mind), receipts + 1); + } + + #[test] + fn exact_replay_returns_already_admitted_across_provenance() { + let mut mind = seeded(); + let receipts = receipt_count(&mind); + let (first, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let batch = PipelineAdmissionBatch { + instance: slug(INSTANCE), + provenance: provenance(Faculty::Soul), + documents: vec![question("Q1", &["A", "B"], "A")], + }; + let later = now() + chrono::Duration::hours(1); + assert_eq!(mind.admit(batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first.clone() }); + assert_eq!(receipt_count(&mind), receipts + 1); + // The first write, epoch record and all, replays too. + let seed_batch = PipelineAdmissionBatch { + instance: slug(INSTANCE), + provenance: provenance(Faculty::Imagination), + documents: vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO]), target(1, &[INVARIANT])], + }; + assert!(matches!(mind.admit(seed_batch, later), PipelineAdmissionOutcome::AlreadyAdmitted { .. })); + assert_eq!(receipt_count(&mind), receipts + 1); + } + + /// RS-1: the ordinal is `head + 1` at admission, not a rank derived from + /// the clock or the receipt id. Three batches land at one `now`, so a + /// mutant that ranked by `(committed_at, receipt_id)` would fall back to + /// id order among them; the ids are checked not to already sort in + /// admission order, so that fallback provably disagrees with the truth. + #[test] + fn ordinals_are_admission_order_not_clock_or_id_order() { + let mut mind = seeded(); + assert_eq!(receipt::head(&mind).unwrap(), 1, "the seed batch is ordinal 1"); + let same_time = now(); + let (id1, _) = committed(admit_at(&mut mind, vec![question_n(1)], same_time)); + let (id2, _) = committed(admit_at(&mut mind, vec![question_n(2)], same_time)); + let (id3, _) = committed(admit_at(&mut mind, vec![question_n(3)], same_time)); + + let mut by_id = [id1.clone(), id2.clone(), id3.clone()]; + by_id.sort(); + assert_ne!( + by_id, + [id1.clone(), id2.clone(), id3.clone()], + "the fixture needs receipt ids that do not already sort in admission order" + ); + + let receipts = mind.receipts().unwrap(); + let ordinal_of = |id: &str| receipts.iter().find(|receipt| receipt.receipt_id == id).unwrap().ordinal; + assert_eq!(ordinal_of(&id1), 2); + assert_eq!(ordinal_of(&id2), 3); + assert_eq!(ordinal_of(&id3), 4); + assert_eq!(receipt::head(&mind).unwrap(), 4); + } + + /// RS-1, A9: the ordinal is not digested, so an exact replay finds the + /// receipt it first landed with and keeps that receipt's ordinal, even + /// when the head has moved since and a fresh candidate would be assigned + /// a different one. + #[test] + fn an_exact_replay_keeps_its_ordinal() { + let mut mind = seeded(); + let (first_id, _) = committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let first_ordinal = + mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == first_id).unwrap().ordinal; + + // Move the head, so a replay's own candidate would carry a different + // ordinal than the one already on the stored receipt. + committed(admit(&mut mind, vec![question("Q2", &["A", "B"], "A")])); + + let outcome = admit(&mut mind, vec![question("Q1", &["A", "B"], "A")]); + assert_eq!(outcome, PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first_id.clone() }); + + let receipts = mind.receipts().unwrap(); + assert_eq!(receipts.iter().filter(|receipt| receipt.receipt_id == first_id).count(), 1, "no second receipt lands"); + let stored = receipts.iter().find(|receipt| receipt.receipt_id == first_id).unwrap(); + assert_eq!(stored.ordinal, first_ordinal, "the replay does not overwrite the ordinal"); + } + + /// A batch that derives a write replays like any other: the second + /// admission re-derives the record the first one landed, sequence and + /// all, so A9 recognises the digest and answers with the first receipt + /// instead of the derived write colliding with itself. + #[test] + fn an_exact_replay_re_derives_the_rulings_resolution() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering.clone())])); + assert!(writes.contains(&r(K::Resolution, &id("resolution", "question.Q1.n1")))); + let receipts = receipt_count(&mind); + assert_eq!( + admit(&mut mind, vec![D::Ruling(answering)]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id } + ); + assert_eq!(receipt_count(&mind), receipts, "a replay writes no second receipt"); + } + + /// The same on both sides of a hand-off: the source re-derives the + /// withdrawal it landed, the receiving mind the assignment. + #[test] + fn an_exact_replay_re_derives_both_sides_of_a_hand_off() { + let away = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]); + let mut source = seeded(); + let (receipt_id, _) = committed(admit(&mut source, vec![away.clone()])); + let receipts = receipt_count(&source); + assert_eq!(admit(&mut source, vec![away.clone()]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); + assert_eq!(receipt_count(&source), receipts); + + let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); + let (receipt_id, _) = committed(admit(&mut receiving, vec![away.clone()])); + let receipts = receipt_count(&receiving); + assert_eq!(admit(&mut receiving, vec![away]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id }); + assert_eq!(receipt_count(&receiving), receipts); + } + + /// The sequence a ruling's answer takes is the one that ruling's own + /// answer carries, not whatever answer the question already has. A + /// question reopened by withdrawing its first answer is answered again at + /// the next sequence, so the first answer stays readable under it. + #[test] + fn a_ruling_answering_a_reopened_question_takes_the_next_sequence() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut first = ruling("R1"); + first.answers = Some(s(&id("question", "Q1"))); + first.choice = Some(l("A")); + committed(admit(&mut mind, vec![D::Ruling(first)])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); + let mut second = ruling("R2"); + second.answers = Some(s(&id("question", "Q1"))); + second.choice = Some(l("B")); + let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(second)])); + assert_eq!(writes, vec![ + r(K::Ruling, &id("ruling", "R2")), + r(K::Resolution, &id("resolution", "question.Q1.n2")), + ]); + assert!(mind.envelope(K::Resolution, &id("resolution", "question.Q1.n1")).is_some()); + } + + #[test] + fn a_refused_batch_is_not_answered_from_a_stored_receipt() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let envelopes = vec![prepare(&D::CutSpec(spec))]; + committed(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes.clone(), now())); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("R2")), + resolution(r(K::Ruling, &id("ruling", "R1")), superseded(&[r(K::Ruling, &id("ruling", "R2"))])), + ])); + assert_eq!( + refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Imagination), envelopes, now())), + MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R1") } + ); + } + + #[test] + fn a_receipt_names_the_exact_bytes_it_read_and_wrote() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1"))])); + let cited = mind.envelope(K::Ruling, &id("ruling", "R1")).unwrap().clone(); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited)]); + let written = mind.envelope(K::CutSpec, &id("cut_spec", "cut-1.r1")).unwrap(); + assert_eq!(receipt.writes, vec![DocumentVersion::from_envelope(written)]); + for version in receipt.strong_reads.iter().chain(&receipt.writes) { + assert_eq!(version.payload_sha256, format!("{:x}", Sha256::digest(&version.payload_msgpack))); + } + let digest = rmp_serde::to_vec_named(&(&receipt.instance, &receipt.strong_reads, &receipt.writes)).unwrap(); + assert_eq!(receipt_id, format!("mind-commit-{:x}", Sha256::digest(&digest))); + + // The same content admitted by another faculty in another mind has + // the same id: provenance is not digested. + let mut twin = opened(MemoryStore::new(), INSTANCE); + seed(&mut twin); + committed(admit(&mut twin, vec![D::Ruling(ruling("R1"))])); + let mut spec = cut_spec("1", 1); + spec.rulings = vec![s(&id("ruling", "R1"))]; + let batch = PipelineAdmissionBatch { instance: slug(INSTANCE), provenance: provenance(Faculty::Soul), documents: vec![D::CutSpec(spec)] }; + let (twin_id, _) = committed(twin.admit(batch, now() + chrono::Duration::days(1))); + assert_eq!(twin_id, receipt_id); + let twin_receipt = twin.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == twin_id).unwrap(); + assert_ne!(twin_receipt.provenance, receipt.provenance); + } + + #[test] + fn a_document_is_written_once_and_superseded_by_resolution() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R8"))])); + let mut rewritten = ruling("R8"); + rewritten.ruling = "A repo owns its mind.".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(rewritten)])), + MindRefusal::IdentityCollision { kind: K::Ruling, id: id("ruling", "R8") } + ); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("R9")), + resolution(r(K::Ruling, &id("ruling", "R8")), superseded(&[r(K::Ruling, &id("ruling", "R9"))])), + ])); + assert_eq!(mind.get(K::Ruling, &id("ruling", "R8")).unwrap(), Some(D::Ruling(ruling("R8")))); + } + + /// A8: one resolution of a subject is in force at a time, and the next + /// record of that subject is the next sequence. The two rules are + /// separate refusals: a second closure at the right sequence is refused + /// because the first still stands, and one at a taken sequence is refused + /// before the question of standing arises. + #[test] + fn a_subject_with_a_resolution_in_force_refuses_another() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject.clone(), 2, ResolutionOutcome::Withdrawn { reason: "again".into() })])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(subject.clone(), ResolutionOutcome::Withdrawn { reason: "again".into() })])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 2, actual: 1 } + ); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering)])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + } + + /// Q17 B and Q19 A together: withdrawing a resolution reopens its subject + /// without erasing the record. The withdrawn closure is still readable by + /// key, the subject takes its next resolution at the next sequence, and + /// the chain stops at depth two -- a withdrawal cannot be withdrawn. + #[test] + fn a_withdrawn_resolution_reopens_its_subject_and_stays_readable() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + let first = id("resolution", "question.Q1.n1"); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + + // The withdrawal of that closure: one nesting out, under the + // resolution it withdraws. + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); + let withdrawal = id("resolution", "resolution.question.Q1.n1.n1"); + assert!(mind.get(K::Resolution, &withdrawal).unwrap().is_some()); + + // The subject is open again, and its next record is `n2`. The first + // one is still there, outcome and all: this is a log, not an erasure. + committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); + let Some(D::Resolution(kept)) = mind.get(K::Resolution, &first).unwrap() else { panic!() }; + assert_eq!(kept.outcome, withdrawn()); + assert_eq!(kept.sequence, 1); + + // `n2` stands, so a third is refused, and the withdrawal cannot + // itself be withdrawn to re-raise `n1` over it (Q19 A). + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject, 3, withdrawn())])), + MindRefusal::AlreadyResolved { subject: id("question", "Q1") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), + MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } + ); + + // And a ruling may answer a question whose first closure was + // withdrawn: the derived resolution takes the next sequence. + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let (_, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!(writes, vec![ + r(K::Ruling, &id("ruling", "R1")), + r(K::Resolution, &id("resolution", "question.Q1.n2")), + ]); + } + + /// The sequence is exactly the previous plus one: a gap refuses as loudly + /// as a taken sequence, on both sequenced kinds, and before the question + /// of what still stands arises. + #[test] + fn a_sequence_gap_refuses_on_both_sequenced_kinds() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + committed(admit(&mut mind, vec![resolution(subject.clone(), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn())])); + committed(admit(&mut mind, vec![resolution_n(subject.clone(), 2, withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution_n(subject, 4, withdrawn())])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 4 } + ); + + let mut steward = seeded(); + committed(admit(&mut steward, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + committed(admit(&mut steward, vec![D::HandOff(back)])); + assert_eq!( + refusal(admit(&mut steward, vec![stewardship_n(INSTANCE, REPO, 4)])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 4 } + ); + } + + /// The sequence counts the batch as well as the image: two records of one + /// subject landing together are the second and the third, never two + /// firsts, and the first of them is refused for the sequence rather than + /// for what the other closes. + #[test] + fn the_latest_resolution_counts_the_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let subject = r(K::Question, &id("question", "Q1")); + assert_eq!( + refusal(admit(&mut mind, vec![ + resolution_n(subject.clone(), 1, withdrawn()), + resolution_n(subject, 2, ResolutionOutcome::Withdrawn { reason: "again".into() }), + ])), + MindRefusal::ResolutionOutOfSequence { subject: id("question", "Q1"), expected: 3, actual: 1 } + ); + } + + /// The Q19 cap reads the batch too: a chain three deep landing in one + /// batch is capped exactly as one landing over three admissions. + #[test] + fn the_cap_reads_a_chain_landing_in_one_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![ + resolution(r(K::Resolution, &id("resolution", "question.Q1.n1")), withdrawn()), + resolution(r(K::Resolution, &id("resolution", "resolution.question.Q1.n1.n1")), withdrawn()), + ])), + MindRefusal::IncompatibleResolution { subject_kind: K::Resolution, outcome: "Withdrawn".into() } + ); + } + + /// Q18 A and Q20 A at admission: a repo is stewarded by one record at a + /// time on a mind, and a repo handed away and handed back is a second + /// record rather than a collision or an overwrite. + #[test] + fn a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer() { + let mut mind = seeded(); + let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 2)])), + MindRefusal::AlreadyStewarded { repo: REPO.into() } + ); + let D::Stewardship(mut retaken) = stewardship(INSTANCE, REPO) else { panic!() }; + retaken.note = "assigned again".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Stewardship(retaken)])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 2, actual: 1 } + ); + + // Handing the repo away withdraws the assignment, and the withdrawal + // is a resolution of that record, sequence and all. The stewardship it + // reads to derive that is an image document, so it is pinned. + let (receipt_id, writes) = committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let away = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.{}", date().0); + assert_eq!(writes, vec![ + r(K::HandOff, &away), + r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1")), + ]); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + let stewardship_envelope = mind.envelope(K::Stewardship, &first).unwrap().clone(); + assert!( + receipt.strong_reads.contains(&DocumentVersion::from_envelope(&stewardship_envelope)), + "the derived withdrawal's own citation is pinned: {:?}", + receipt.strong_reads + ); + + // Nothing stewards the repo now, and a campaign over it still admits: + // campaign admission reads no stewardship (ruling stewardship-rule). + let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; + second.slug = slug("second"); + committed(admit(&mut mind, vec![D::Campaign(second)])); + + // Handed back, the mind stewards it again, as `n2`. No field names a + // return: this is the same hand-off shape with the instances swapped. + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(back)])); + let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + assert_eq!(writes, vec![ + r(K::HandOff, &format!("{OTHER_INSTANCE}:hand_off:{INSTANCE}.gamecult_-epiphany.2026-09-17")), + r(K::Stewardship, &again), + ]); + let Some(D::Stewardship(taken)) = mind.get(K::Stewardship, &again).unwrap() else { panic!() }; + assert_eq!(taken.sequence, 2); + assert_eq!(taken.assigned_on, eureka_pipeline::Date("2026-09-17".into())); + assert!(mind.envelope(K::Stewardship, &first).is_some(), "the first assignment is still readable by key"); + } + + /// Q19 A's rationale, which the cap alone does not carry: a withdrawal + /// never re-raises an earlier record over a later one. The repo went away + /// and came back, so withdrawing the hand-off's withdrawal would put the + /// first assignment back in force beside the second. + #[test] + fn a_withdrawal_does_not_reinstate_a_stewardship_over_a_later_one() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; + back.handed_on = eureka_pipeline::Date("2026-09-17".into()); + committed(admit(&mut mind, vec![D::HandOff(back)])); + let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), + MindRefusal::WouldReinstateOverLater { subject: first, later: again.clone() } + ); + let docs = Docs::from_image(mind.envelopes()).unwrap(); + let in_force = docs.stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| *key).collect::>(); + assert_eq!(in_force, vec![again.as_str()], "one assignment stands, and it is the later one"); + } + + /// The same rule one kind over: withdrawing a supersession while the + /// revision that superseded stands would leave two revisions in force. + #[test] + fn a_withdrawal_does_not_reinstate_a_revision_over_a_later_one() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + target(2, &[INVARIANT]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &id("resolution", "target.r1.n1")), withdrawn())])), + MindRefusal::WouldReinstateOverLater { subject: id("target", "r1"), later: id("target", "r2") } + ); + assert!(docs_of(&mind).in_force(K::Target, &id("target", "r2"))); + assert!(!docs_of(&mind).in_force(K::Target, &id("target", "r1"))); + } + + fn docs_of(mind: &Mind) -> Docs { + Docs::from_image(mind.envelopes()).unwrap() + } + + /// The withdrawal a hand-off derives is a record of its subject like any + /// other: the second time the repo leaves, it takes the next sequence. + #[test] + fn a_hand_offs_derived_withdrawal_takes_the_next_sequence() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[])])); + // Nothing later stands over the first assignment, so withdrawing the + // withdrawal puts it back in force and the repo is this mind's again. + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let D::HandOff(mut again) = hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]) else { panic!() }; + again.handed_on = eureka_pipeline::Date("2026-09-17".into()); + let (_, writes) = committed(admit(&mut mind, vec![D::HandOff(again)])); + assert_eq!(writes, vec![ + r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-17")), + r(K::Resolution, &format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n2")), + ]); + } + + fn hand_off_on(from: &str, to: &str, day: &str) -> D { + let D::HandOff(mut hand_off) = hand_off(from, to, REPO, &[]) else { panic!() }; + hand_off.handed_on = eureka_pipeline::Date(day.into()); + D::HandOff(hand_off) + } + + fn stewardship_key(sequence: u32) -> String { + format!("{INSTANCE}:stewardship:gamecult_-epiphany.n{sequence}") + } + + /// Which assignment a hand-off's derived withdrawal names is a question of + /// content, not of order. While the withdrawal is being derived the + /// hand-off's own is excluded from what stands, so two assignments are in + /// force at once, and after ten transfers the one that stewards the repo + /// now sorts first: `n11` before `n2`. The replay must still withdraw the + /// assignment it withdrew the first time, or it derives a second + /// withdrawal of a standing assignment and collides instead of answering + /// with its receipt. + #[test] + fn a_source_side_replay_withdraws_the_assignment_it_withdrew() { + let mut mind = seeded(); + let mut aways = Vec::new(); + for transfer in 1..=10u32 { + let day = format!("2026-09-{transfer:02}"); + let away = hand_off_on(INSTANCE, OTHER_INSTANCE, &day); + let (receipt_id, _) = committed(admit(&mut mind, vec![away.clone()])); + aways.push((away, receipt_id)); + committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, &day)])); + } + let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); + assert_eq!(in_force, vec![stewardship_key(11)]); + + for index in [1usize, 8] { + let (away, receipt_id) = &aways[index]; + assert_eq!( + admit(&mut mind, vec![away.clone()]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() }, + "the replay of transfer {} re-derives its own withdrawal", + index + 1 + ); + } + + // The standing assignment is handed away and reinstated, so it too + // carries a withdrawal -- of another hand-off. Sorting first, it is + // what a match on the outcome alone would pick. + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-11")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n11.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let (away, receipt_id) = &aways[1]; + assert_eq!( + admit(&mut mind, vec![away.clone()]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: receipt_id.clone() } + ); + } + + /// `later_in_force` counts what stands, not what exists: the repo went + /// away, came back and went away again, so the second assignment is later + /// than the first but withdrawn, and reinstating the first leaves one + /// assignment in force rather than two. + #[test] + fn a_withdrawn_later_assignment_does_not_block_a_reinstatement() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); + committed(admit(&mut mind, vec![hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17")])); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-18")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])); + let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); + assert_eq!(in_force, vec![stewardship_key(1)]); + } + + /// And it reads the batch: the assignment that stands later than the one + /// a withdrawal would reinstate may be landing beside that withdrawal, + /// derived by a hand-off in the same batch. + #[test] + fn a_reinstatement_is_blocked_by_an_assignment_in_its_own_batch() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![hand_off_on(INSTANCE, OTHER_INSTANCE, "2026-09-16")])); + let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![ + hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17"), + resolution(r(K::Resolution, &withdrawal), withdrawn()), + ])), + MindRefusal::WouldReinstateOverLater { subject: stewardship_key(1), later: stewardship_key(2) } + ); + } + + /// A cut spec's revisions are sequenced within its cut, so another cut's + /// later revision is not what a withdrawal reads: once cut A's second + /// revision is gone, reinstating its first commits while cut B's second + /// stands. + #[test] + fn a_reinstatement_reads_the_revisions_of_its_own_cut() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("A", 1)), D::CutSpec(cut_spec("B", 1))])); + for cut in ["A", "B"] { + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec(cut, 2)), + resolution( + r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))), + superseded(&[r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r2")))]), + ), + ])); + } + let supersession = id("resolution", "cut_spec.cut-A.r1.n1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])), + MindRefusal::WouldReinstateOverLater { + subject: id("cut_spec", "cut-A.r1"), + later: id("cut_spec", "cut-A.r2"), + } + ); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-A.r2")), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &supersession), withdrawn())])); + assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-A.r1"))); + assert!(docs_of(&mind).in_force(K::CutSpec, &id("cut_spec", "cut-B.r2"))); + } + + /// A seeded mind with one document of every resolvable kind, and the + /// non-resolvable ones beside them. + fn world() -> Mind { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + stewardship(INSTANCE, OTHER_REPO), + question("Q1", &["A", "B"], "A"), + D::Ruling(ruling("R1")), + D::CutSpec(cut_spec("1", 1)), + D::CutReport(cut_report("1", 1)), + verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&id("finding", "cut-1.s1.F1")], Some("P1"), &["M1"])]), + D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)), + follow_up("FU-1", r(K::Finding, &id("finding", "cut-1.s1.F1"))), + hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[]), + ])); + mind + } + + #[test] + fn the_resolution_matrix_is_admissions() { + let incompatible = |kind: K, outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: kind, outcome: outcome.into() }; + let fixed = || ResolutionOutcome::Fixed { commit: sha(), by: None }; + let target_ref = r(K::Target, &id("target", "r1")); + let target_r2 = r(K::Target, &id("target", "r2")); + let question_ref = r(K::Question, &id("question", "Q1")); + let ruling_ref = r(K::Ruling, &id("ruling", "R1")); + let spec_ref = r(K::CutSpec, &id("cut_spec", "cut-1.r1")); + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let follow_up_ref = r(K::FollowUp, &id("follow_up", "FU-1")); + let stewardship_ref = r(K::Stewardship, &format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")); + let campaign_ref = r(K::Campaign, &id("campaign", "self")); + let report_ref = r(K::CutReport, &id("cut_report", "cut-1.h1")); + let verdict_ref = r(K::Verdict, &id("verdict", "cut-1.s1")); + let instance_ref = r(K::Instance, &format!("{INSTANCE}:instance:self")); + let hand_off_ref = r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-huginn.2026-09-16")); + + // Accepted, one per resolvable kind. + committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(std::slice::from_ref(&target_r2)))])); + committed(admit(&mut world(), vec![resolution(question_ref.clone(), withdrawn())])); + committed(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(ruling_ref.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])); + committed(admit(&mut world(), vec![resolution(spec_ref.clone(), withdrawn())])); + committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Recorded { reason: "noted".into() })])); + committed(admit(&mut world(), vec![resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to: follow_up_ref.clone() })])); + committed(admit(&mut world(), vec![resolution(follow_up_ref.clone(), fixed())])); + committed(admit(&mut world(), vec![resolution(stewardship_ref.clone(), withdrawn())])); + let mut mind = world(); + committed(admit(&mut mind, vec![resolution(question_ref.clone(), withdrawn())])); + let nested = r(K::Resolution, &id("resolution", "question.Q1.n1")); + committed(admit(&mut mind, vec![resolution(nested.clone(), withdrawn())])); + + // Refused, one per kind, and every non-resolvable kind. + assert_eq!(refusal(admit(&mut world(), vec![resolution(target_ref.clone(), withdrawn())])), incompatible(K::Target, "Withdrawn")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(question_ref.clone(), fixed())])), incompatible(K::Question, "Fixed")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(ruling_ref.clone(), withdrawn())])), incompatible(K::Ruling, "Withdrawn")); + // A kind admits the outcomes of its own row and no others: a question + // is answered or withdrawn, never superseded; a target is superseded, + // never answered. + assert_eq!( + refusal(admit(&mut world(), vec![ + question("Q2", &["A", "B"], "A"), + resolution(question_ref, superseded(&[r(K::Question, &id("question", "Q2"))])), + ])), + incompatible(K::Question, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(target_ref, ResolutionOutcome::Answered { by: ruling_ref })])), + incompatible(K::Target, "Answered") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(spec_ref.clone(), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), + incompatible(K::CutSpec, "Answered") + ); + // A cut spec superseded across cuts is refused too. + let mut other_cut = cut_spec("2", 1); + other_cut.cut = l("2"); + assert_eq!( + refusal(admit(&mut world(), vec![D::CutSpec(other_cut), resolution(spec_ref, superseded(&[r(K::CutSpec, &id("cut_spec", "cut-2.r1"))]))])), + incompatible(K::CutSpec, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(std::slice::from_ref(&finding_ref)))])), + incompatible(K::Finding, "Superseded") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(follow_up_ref, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) })])), + incompatible(K::FollowUp, "Answered") + ); + assert_eq!( + refusal(admit(&mut world(), vec![resolution(stewardship_ref, ResolutionOutcome::Recorded { reason: "no".into() })])), + incompatible(K::Stewardship, "Recorded") + ); + let mut mind = world(); + committed(admit(&mut mind, vec![resolution(r(K::Question, &id("question", "Q1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(nested.clone(), superseded(&[nested]))])), + incompatible(K::Resolution, "Superseded") + ); + for (subject, kind) in [(campaign_ref, K::Campaign), (report_ref, K::CutReport), (verdict_ref, K::Verdict), (instance_ref, K::Instance), (hand_off_ref, K::HandOff)] { + assert_eq!(refusal(admit(&mut world(), vec![resolution(subject, withdrawn())])), incompatible(kind, "Withdrawn")); + } + } + + #[test] + fn supersession_names_each_supersessor_and_none_is_empty() { + let subject = r(K::Ruling, &id("ruling", "R1")); + assert_eq!(refusal(admit(&mut world(), vec![resolution(subject.clone(), superseded(&[]))])), MindRefusal::EmptySupersession); + let (_, writes) = committed(admit(&mut world(), vec![ + D::Ruling(ruling("R2")), + D::Ruling(ruling("R3")), + resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R3"))])), + ])); + assert_eq!(writes.len(), 3); + assert_eq!( + refusal(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(subject.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2")), r(K::Ruling, &id("ruling", "R4"))]))])), + MindRefusal::UnknownSupersessor { id: id("ruling", "R4") } + ); + let mut mind = world(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R2")), D::Ruling(ruling("R3")), resolution(r(K::Ruling, &id("ruling", "R2")), superseded(&[r(K::Ruling, &id("ruling", "R3"))]))])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(subject, superseded(&[r(K::Ruling, &id("ruling", "R2"))]))])), + MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: id("ruling", "R2") } + ); + } + + /// refusals-typed: a document the leaf cannot key is the client's fault. + /// The finding's local is 64 bytes of label under `cut-1.s1.`; the + /// resolution's subject is a resolution whose own local leaves no room for + /// a third nesting. Both pass `validate` and fail only at the key. + fn unkeyable() -> Vec { + let wide = D::Finding(finding("1", 1, &"w".repeat(64), FindingConfidence::Confirmed)); + let deep_subject = id("resolution", &format!("{}.{}", "x".repeat(60), "y".repeat(40))); + vec![wide, resolution(r(K::Resolution, &deep_subject), withdrawn())] + } + + #[test] + fn an_unkeyable_document_is_refused_as_a_document() { + let store = RefusingStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let before = store.rows(); + let receipts = receipt_count(&mind); + for (document, field) in unkeyable().into_iter().zip(["finding.key", "resolution.key"]) { + document.validate().unwrap(); + // The leaf owns what a key error says; admission must carry it + // unchanged, field and offending value included. + let leaf = pipeline_key(&document).expect_err("the fixture must not key"); + assert!( + matches!(&leaf, PipelineRefusal::InvalidFormat { field: named, value } if named == field && !value.is_empty()), + "{leaf:?}" + ); + assert_eq!(refusal(admit(&mut mind, vec![document])), MindRefusal::Document(leaf), "{field}"); + assert_eq!(store.rows(), before); + assert_eq!(receipt_count(&mind), receipts); + } + } + + /// The derived path (`admit_steps`) shares the helper. No derivable + /// document can fail to key after the leaf's per-kind bound (a derived + /// resolution is depth one or two over a subject of at most 64 bytes), so + /// the helper is pinned directly. + #[test] + fn prepare_classifies_a_key_refusal_as_a_document_and_encodes_the_rest() { + let cache = schema_cache().unwrap(); + let keyable = question("Q1", &["A", "B"], "A"); + assert_eq!(super::prepare(&keyable, &cache).unwrap(), prepare(&keyable)); + for document in unkeyable() { + let leaf = pipeline_key(&document).expect_err("the fixture must not key"); + assert_eq!(super::prepare(&document, &cache).unwrap_err(), MindRefusal::Document(leaf)); + } + } + + #[test] + fn a_ruling_answering_a_question_derives_the_answered_resolution_atomically() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("C")); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering.clone())])), + MindRefusal::InvalidChoice { ruling: id("ruling", "R1"), choice: "C".into() } + ); + answering.choice = Some(l("A")); + let (receipt_id, writes) = committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!(writes, vec![r(K::Ruling, &id("ruling", "R1")), r(K::Resolution, &id("resolution", "question.Q1.n1"))]); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.writes.len(), 2); + let Some(D::Resolution(derived)) = mind.get(K::Resolution, &id("resolution", "question.Q1.n1")).unwrap() else { panic!() }; + assert_eq!(derived.outcome, ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); + let mut again = ruling("R2"); + again.answers = Some(s(&id("question", "Q1"))); + again.choice = Some(l("B")); + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(again)])), MindRefusal::AlreadyResolved { subject: id("question", "Q1") }); + + // An explicit `Answered` must name the ruling that actually answers + // its subject: a ruling answering Q1 does not resolve Q2. + let mut mind = seeded(); + committed(admit(&mut mind, vec![question("Q1", &["A", "B"], "A"), question("Q2", &["A", "B"], "A")])); + let mut answering = ruling("R1"); + answering.answers = Some(s(&id("question", "Q1"))); + answering.choice = Some(l("A")); + let wrong = resolution(r(K::Question, &id("question", "Q2")), ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }); + assert_eq!( + refusal(admit(&mut mind, vec![D::Ruling(answering.clone()), wrong.clone()])), + MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } + ); + // The coherence is checked whether the ruling is in the batch or in + // the image: a later batch citing the stored R1 is refused the same + // way, and the derived resolution of Q1 is not what saves it. + committed(admit(&mut mind, vec![D::Ruling(answering)])); + assert_eq!( + refusal(admit(&mut mind, vec![wrong])), + MindRefusal::IncompatibleResolution { subject_kind: K::Question, outcome: "Answered".into() } + ); + } + + #[test] + fn a_batch_is_one_to_sixty_four_documents_each_with_its_own_identity() { + let mut mind = seeded(); + // A2 at both ends, and at the boundary the batch is admitted whole. + assert_eq!(refusal(admit(&mut mind, vec![])), MindRefusal::BatchSize { actual: 0 }); + let over = (0..65).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); + assert_eq!(refusal(admit(&mut mind, over)), MindRefusal::BatchSize { actual: 65 }); + let full = (0..BATCH_MAX).map(|index| question(&format!("Q{index}"), &["A", "B"], "A")).collect::>(); + let (_, writes) = committed(admit(&mut mind, full)); + assert_eq!(writes.len(), BATCH_MAX); + // A4: one identity, once, whatever the two documents say. + assert_eq!( + refusal(admit(&mut mind, vec![question("Z", &["A", "B"], "A"), question("Z", &["A", "B"], "B")])), + MindRefusal::IdentityCollision { kind: K::Question, id: id("question", "Z") } + ); + } + + #[test] + fn a_question_offers_at_least_two_options_and_recommends_one_of_them() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q7", &["A"], "A")])), + MindRefusal::InvalidOptions { question: id("question", "Q7") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q8", &["A", "B"], "C")])), + MindRefusal::InvalidOptions { question: id("question", "Q8") } + ); + committed(admit(&mut mind, vec![question("Q9", &["A", "B"], "B")])); + } + + #[test] + fn a_label_names_one_option_and_one_invariant() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![question("Q1", &["A", "A"], "A")])), + MindRefusal::DuplicateLabel { field: "question.options".into(), label: "A".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![ + target(2, &["x", "x"]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])), + MindRefusal::DuplicateLabel { field: "target.invariants".into(), label: "x".into() } + ); + } + + /// Every A7 reference the image resolved is pinned, not the first of + /// them, so a batch citing three documents cannot commit over a change to + /// the other two. + #[test] + fn every_cited_image_document_is_a_strong_read() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), D::Ruling(ruling("R2")), D::Ruling(ruling("R3"))])); + let cited = ["R1", "R2", "R3"].map(|label| mind.envelope(K::Ruling, &id("ruling", label)).unwrap().clone()); + let mut spec = cut_spec("1", 1); + spec.rulings = cited.iter().map(|envelope| s(&envelope.key)).collect(); + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(spec)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads.len(), 3); + for envelope in &cited { + assert!(receipt.strong_reads.contains(&DocumentVersion::from_envelope(envelope)), "{}", envelope.key); + } + + // Two batch documents citing one image document pin it once: the pins + // are a set, so a second citation neither duplicates the pin nor + // cancels it. + let mut twice = cut_spec("2", 1); + twice.rulings = vec![s(&id("ruling", "R1"))]; + let D::Question(mut raised) = question("Q1", &["A", "B"], "A") else { panic!() }; + raised.raised_in = Some(r(K::Ruling, &id("ruling", "R1"))); + let (receipt_id, _) = committed(admit(&mut mind, vec![D::CutSpec(twice), D::Question(raised)])); + let receipt = mind.receipts().unwrap().into_iter().find(|receipt| receipt.receipt_id == receipt_id).unwrap(); + assert_eq!(receipt.strong_reads, vec![DocumentVersion::from_envelope(&cited[0])], "cited twice, pinned once"); + } + + #[test] + fn an_operator_quote_requires_operator_authority() { + let mut mind = seeded(); + let mut quoted = ruling("R1"); + quoted.operator_quote = Some("all recommendations, go ahead".into()); + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + quoted.authority = RulingAuthority::Mind; + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + assert!(mind.get(K::Ruling, &id("ruling", "R1")).unwrap().is_none(), "a refused batch leaves the store unchanged"); + quoted.authority = RulingAuthority::Operator; + committed(admit(&mut mind, vec![D::Ruling(quoted)])); + } + + #[test] + fn a_mind_ruling_without_a_quote_is_admitted() { + let mut mind = seeded(); + let mut own = ruling("R1"); + own.authority = RulingAuthority::Mind; + committed(admit(&mut mind, vec![D::Ruling(own)])); + } + + #[test] + fn a_revision_requires_its_predecessors_supersession_in_the_batch() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![target(2, &[INVARIANT])])), + MindRefusal::RevisionWithoutSupersession { kind: K::Target, revision: 2 } + ); + committed(admit(&mut mind, vec![ + target(2, &[INVARIANT]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + ])); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 2))])), + MindRefusal::RevisionWithoutSupersession { kind: K::CutSpec, revision: 2 } + ); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + } + + #[test] + fn a_cut_report_cites_an_in_force_spec_and_agrees_with_it() { + let mut mind = seeded(); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), + MindRefusal::CutReportWithoutSpec { report: id("cut_report", "cut-1.h1") } + ); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + let mut report = cut_report("1", 1); + report.branch = s("another-branch"); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); + // A branch is compared as written. Git's refs are case-sensitive, and + // a report of `Codex/...` is a report of another branch or of none. + let mut report = cut_report("1", 1); + report.branch = s("CODEX/eureka-pipeline-state"); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "branch".into() }); + let mut report = cut_report("1", 1); + report.repo = repo(OTHER_REPO); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::SpecMismatch { field: "repo".into() }); + let mut report = cut_report("1", 1); + report.range.head = eureka_pipeline::Sha("abcdef0".into()); + assert_eq!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { head: "abcdef0".into() }); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + } + + fn spec_ref(cut: &str) -> PipelineRef { + r(K::CutSpec, &id("cut_spec", &format!("cut-{cut}.r1"))) + } + + fn run_key(label: &str) -> String { + format!("{INSTANCE}:run:{label}") + } + + /// A seeded mind holding cut specs 1 and 2. + fn with_specs() -> Mind { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutSpec(cut_spec("2", 1))])); + mind + } + + fn recorded(label: &str) -> PipelineDocument { + resolution(r(K::Run, &run_key(label)), ResolutionOutcome::Recorded { reason: "finished".into() }) + } + + /// run-is-the-grant: the claim is the only consumption fact. A second run + /// on a claim a live run holds is AlreadyClaimed, naming the claim and the + /// holder; another claim is free; once the holder is Recorded the claim is + /// free, and withdrawing that record makes the holder live again. + #[test] + fn a_second_run_on_a_live_claim_is_already_claimed() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) }; + assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), claimed); + assert_eq!(refusal(admit(&mut mind, vec![run("b", &[spec_ref("2"), spec_ref("1")])])), claimed, "any one held claim refuses the run"); + committed(admit(&mut mind, vec![run("b", &[spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); + + let mut reopened = with_specs(); + committed(admit(&mut reopened, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut reopened, vec![recorded("a")])); + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + committed(admit(&mut reopened, vec![resolution(closure, withdrawn())])); + assert_eq!(refusal(admit(&mut reopened, vec![run("d", &[spec_ref("1")])])), claimed); + } + + /// one-live-self-run at admission: a second run of hers of one turn is + /// AlreadyLive naming the holder, whether the holder is claimless or + /// claiming, and a claim the holder holds is still AlreadyClaimed first. + #[test] + fn a_second_live_run_of_hers_of_one_turn_is_already_live() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + let live_a = MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), live_a, "a claimless holder"); + assert_eq!(refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])), live_a); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[spec_ref("1")])])); + let live_b = MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("2")])])), live_b, "a claiming holder"); + let claimed = MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("b")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[spec_ref("1")])])), claimed, "claims answer first"); + // The holder is named by its literal key, not by the constructor that made the expectation above. + let MindRefusal::AlreadyLive { run } = refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])) else { panic!("not live") }; + assert_eq!(run.as_str(), format!("{INSTANCE}:run:b")); + assert_eq!(run.to_ref().id.0, format!("{INSTANCE}:run:b")); + } + + /// The rule counts only in-force runs of hers of the same turn: an + /// operator's run, a run of the other turn and a run after the holder is + /// Recorded are admitted, and one live PersonaTurn and one live SelfRun of + /// hers coexist. + #[test] + fn an_operators_run_another_turn_and_a_run_after_the_holder_closed_are_admitted() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![run("op1", &[])])); + committed(admit(&mut mind, vec![run("op2", &[])])); + committed(admit(&mut mind, vec![mind_run("p", RunTurn::PersonaTurn, &[])])); + let live_p = MindRefusal::AlreadyLive { run: RunId::held(&run_key("p")) }; + assert_eq!(refusal(admit(&mut mind, vec![mind_run("p2", RunTurn::PersonaTurn, &[])])), live_p); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("b")])); + committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); + + let mut operator_only = with_specs(); + committed(admit(&mut operator_only, vec![run("op", &[])])); + committed(admit(&mut operator_only, vec![mind_run("a", RunTurn::SelfRun, &[])])); + } + + /// The slot is keyed on instance and turn alone: a holder that started + /// earlier, on another host and under another budget still blocks. + #[test] + fn a_holder_that_differs_in_start_host_and_budget_still_holds_the_slot() { + let mut mind = with_specs(); + let D::Run(mut holder) = mind_run("a", RunTurn::SelfRun, &[]) else { unreachable!() }; + holder.started_on = eureka_pipeline::Date("2026-01-01".into()); + holder.host = s("elsewhere"); + holder.budget_usd = s("9"); + committed(admit(&mut mind, vec![D::Run(holder)])); + assert_eq!( + refusal(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) } + ); + } + + /// A run opened and closed in one batch is never live, so a live holder + /// of its turn does not refuse it; it holds no slot afterwards either. + #[test] + fn a_run_born_closed_beside_a_live_holder_is_admitted() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[]), recorded("b")])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])); + } + + /// Two runs of hers of one turn in one batch hold nothing: the batch is + /// checked against itself, refused whole, and nothing is written. + #[test] + fn two_live_runs_of_hers_in_one_batch_are_already_live_and_nothing_commits() { + let mut mind = with_specs(); + let refused = refusal(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[]), mind_run("b", RunTurn::SelfRun, &[])])); + assert!(matches!(refused, MindRefusal::AlreadyLive { .. }), "{refused:?}"); + assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); + } + + /// Withdrawing the close of a run of hers reinstates it, and a reinstated + /// run is a live one: refused while another run of its turn is live, + /// allowed when none is. + #[test] + fn reinstating_a_run_of_hers_over_a_live_holder_is_already_live() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("b")) } + ); + committed(admit(&mut mind, vec![recorded("b")])); + committed(admit(&mut mind, vec![resolution(closure, withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![mind_run("c", RunTurn::SelfRun, &[])])), + MindRefusal::AlreadyLive { run: RunId::held(&run_key("a")) }, + "the reinstated run holds the slot" + ); + } + + /// A retry of an admitted run answers AlreadyAdmitted even when another + /// run of its turn has since become the live one. + #[test] + fn replaying_an_admitted_run_of_hers_after_another_took_the_turn_is_already_admitted() { + let mut mind = with_specs(); + let (first, _) = committed(admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![mind_run("b", RunTurn::SelfRun, &[])])); + assert_eq!( + admit(&mut mind, vec![mind_run("a", RunTurn::SelfRun, &[])]), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } + ); + } + + /// Two openers on one claim in one batch: the claim is held by neither, so + /// the batch is refused whole and nothing is written. One opener at a time + /// is `a_second_run_on_a_live_claim_is_already_claimed`. + #[test] + fn two_openers_in_one_batch_hold_nothing() { + let mut mind = with_specs(); + let refused = refusal(admit(&mut mind, vec![run("a", &[spec_ref("1")]), run("b", &[spec_ref("1")])])); + assert!(matches!(refused, MindRefusal::AlreadyClaimed { .. }), "{refused:?}"); + assert!(mind.envelope(K::Run, &run_key("a")).is_none() && mind.envelope(K::Run, &run_key("b")).is_none()); + } + + /// Every claim names a document that exists and is in force. + #[test] + fn a_runs_claims_must_exist_and_be_in_force() { + let mut mind = with_specs(); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("9")])])), + MindRefusal::MissingReference { kind: K::CutSpec, id: spec_ref("9").id.0 } + ); + committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } + ); + assert!(mind.envelope(K::Run, &run_key("a")).is_none()); + } + + /// A replay of an admitted run is AlreadyAdmitted even when its claim has + /// since been closed: a retry owes nothing to the work moving on. A + /// different run on that closed claim is still refused. + #[test] + fn replaying_an_admitted_run_after_its_claim_closed_is_already_admitted() { + let mut mind = with_specs(); + let (first, _) = committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); + assert_eq!(admit(&mut mind, vec![run("a", &[spec_ref("1")])]), PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first }); + assert_eq!( + refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } + ); + } + + /// Withdrawing a run's Recorded resolution puts the run back in force, so + /// it goes through the same exclusivity as an opening: refused while + /// another live run holds one of its claims, allowed when none does. + #[test] + fn reinstating_a_run_over_a_live_holder_is_already_claimed() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref("1")])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("c")) } + ); + + let mut free = with_specs(); + committed(admit(&mut free, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut free, vec![recorded("a")])); + committed(admit(&mut free, vec![resolution(closure, withdrawn())])); + } + + /// Reinstatement finds a contested claim anywhere in the run's list: a + /// reinstated run of two claims where only the second is held is refused, + /// and the first alone being contested is refused too. + #[test] + fn reinstating_a_run_finds_a_contested_second_claim() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + for (held, expected) in [("2", "2"), ("1", "1")] { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![run("c", &[spec_ref(held)])])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::AlreadyClaimed { item: spec_ref(expected).id.0, run: RunId::held(&run_key("c")) } + ); + } + } + + /// Reinstating a run needs every claim in force, as opening it does: once + /// a claimed item has resolved the withdrawal is refused, and while all + /// stay in force it is allowed. + #[test] + fn reinstating_a_run_needs_its_claims_in_force() { + let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1")); + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut mind, vec![recorded("a")])); + committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 } + ); + + let mut live = with_specs(); + committed(admit(&mut live, vec![run("a", &[spec_ref("1"), spec_ref("2")])])); + committed(admit(&mut live, vec![recorded("a")])); + committed(admit(&mut live, vec![resolution(closure, withdrawn())])); + } + + /// A holder is found by any of its claims, not only the first. + #[test] + fn a_holder_is_found_by_its_second_claim() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("2"), spec_ref("1")])])); + assert_eq!( + refusal(admit(&mut mind, vec![run("b", &[spec_ref("1")])])), + MindRefusal::AlreadyClaimed { item: spec_ref("1").id.0, run: RunId::held(&run_key("a")) } + ); + } + + /// A key already holding a run is taken: the same key with different + /// content is an IdentityCollision, never a replay; only byte-identical + /// content answers AlreadyAdmitted. + #[test] + fn a_run_key_reused_with_different_content_is_an_identity_collision() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; + changed.budget_usd = "6".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(changed)])), + MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } + ); + assert_eq!( + refusal(admit(&mut mind, vec![run("a", &[spec_ref("2")])])), + MindRefusal::IdentityCollision { kind: K::Run, id: run_key("a") } + ); + } + + /// The replay clause is byte identity, not the key: a changed run under an + /// admitted key whose claim has since closed is not a replay, so the closed + /// claim refuses it. + #[test] + fn a_changed_run_under_an_admitted_key_is_not_a_replay() { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![run("a", &[spec_ref("1")])])); + committed(admit(&mut mind, vec![resolution(spec_ref("1"), withdrawn())])); + let D::Run(mut changed) = run("a", &[spec_ref("1")]) else { panic!() }; + changed.budget_usd = "6".into(); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(changed)])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("1").id.0 } + ); + } + + /// A5 for runs: a run of another instance is a ForeignInstance. + #[test] + fn a_run_of_another_mind_is_a_foreign_instance() { + let mut mind = with_specs(); + let D::Run(mut foreign) = run("a", &[]) else { panic!() }; + foreign.instance = slug(OTHER_INSTANCE); + assert_eq!( + refusal(admit(&mut mind, vec![D::Run(foreign)])), + MindRefusal::ForeignInstance { declared: OTHER_INSTANCE.into(), mind: INSTANCE.into() } + ); + } + + /// A run ends: Recorded or Withdrawn, and no other outcome. + #[test] + fn a_run_resolves_recorded_or_withdrawn_and_nothing_else() { + let incompatible = |outcome: &str| MindRefusal::IncompatibleResolution { subject_kind: K::Run, outcome: outcome.into() }; + let subject = || r(K::Run, &run_key("a")); + let open = || { + let mut mind = with_specs(); + committed(admit(&mut mind, vec![D::Ruling(ruling("R1")), run("a", &[spec_ref("1")]), run("z", &[])])); + mind + }; + committed(admit(&mut open(), vec![recorded("a")])); + committed(admit(&mut open(), vec![resolution(subject(), withdrawn())])); + let other_run = r(K::Run, &run_key("z")); + let refused = [ + (superseded(std::slice::from_ref(&other_run)), "Superseded"), + (ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"), + (ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"), + (ResolutionOutcome::Deferred { to: other_run }, "Deferred"), + ]; + for (outcome, name) in refused { + assert_eq!(refusal(admit(&mut open(), vec![resolution(subject(), outcome)])), incompatible(name), "{name}"); + } + } + + #[test] + fn depends_on_names_cuts() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // An unknown label, and a spec id (a revision, not a cut), are refused. + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), + MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } + ); + // The same batch carries the cut it depends on. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); + // The image carries it, and any revision of the cut names it. + committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); + // A cut of another campaign is not this campaign's cut. + let mut other = cut_spec("9", 1); + other.campaign = slug("elsewhere"); + let mut elsewhere = campaign(&[REPO]); + if let D::Campaign(campaign) = &mut elsewhere { + campaign.slug = slug("elsewhere"); + } + committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("4", &["9"])])), + MindRefusal::UnknownDependency { cut: "9".into() } + ); + } + + #[test] + fn depends_on_matches_cut_labels_whole() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + // The image carries cut 10: neither its prefix nor an extension of it names it. + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); + for near in ["1", "100"] { + assert_eq!( + refusal(admit(&mut mind, vec![depending("11", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + // The same holds when the cut arrives in the same batch. + for near in ["2", "200"] { + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("20", 1)), depending("21", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + committed(admit(&mut mind, vec![depending("12", &["10"])])); + } + + #[test] + fn a_cut_may_not_depend_on_itself() { + let depending = |cut: &str, revision: u32, on: &[&str]| { + let mut spec = cut_spec(cut, revision); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // Alone in the batch, and beside another cut it legitimately names. + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("2", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["2", "1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + // An earlier revision of the same cut in the image does not make it a real dependency. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 2, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + } + + #[test] + fn a_report_head_may_spell_its_commit_short_or_full() { + let full = "5f98228d0123456789abcdef0123456789abcdef"; + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + // The commit is spelled short and the head full, and the reverse. + let mut report = cut_report("1", 1); + report.range.head = eureka_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + let mut report = cut_report("1", 2); + report.commits[0].sha = eureka_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + // A head that is no commit's prefix, long or short, is outside. + let mut report = cut_report("1", 3); + report.range.head = eureka_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + let mut report = cut_report("1", 3); + report.commits[0].sha = eureka_pipeline::Sha(full.into()); + report.range.head = eureka_pipeline::Sha("5f98229".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + } + + #[test] + fn a_finding_defers_to_a_cut_spec() { + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); + committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); + // A spec superseded since is no longer a place to defer to. + let mut mind = world(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + assert_eq!( + refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } + ); + } + + #[test] + fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), D::CutReport(cut_report("1", 1))])); + let finding_id = id("finding", "cut-1.s1.F1"); + let confirmed = D::Finding(finding("1", 1, "F1", FindingConfidence::Confirmed)); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[], Some("P1"), &[])])])), + MindRefusal::FalsifiedClaimWithoutConfirmedFinding { claim: "Falsified claim".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![ + verdict("1", 1, vec![claim(ClaimOutcome::Unproven, &[&finding_id], Some("P1"), &[])]), + confirmed.clone(), + ])), + MindRefusal::UnprovenClaimWithConfirmedFinding { claim: "Unproven claim".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], None, &[])])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![ + claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), + claim(ClaimOutcome::Holds, &[], Some("P1"), &[]), + ])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + // A promise is measured by the claim that names it, not by any claim + // that names a promise: the report promises P1 and nothing else. + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P2"), &[])])])), + MindRefusal::PromiseWithoutVerdict { promise: "P1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &["M9"])])])), + MindRefusal::UnknownMutationLabel { label: "M9".into() } + ); + committed(admit(&mut mind, vec![ + verdict("1", 1, vec![claim(ClaimOutcome::Falsified, &[&finding_id], Some("P1"), &["M1"])]), + confirmed, + ])); + } + + #[test] + fn a_finding_names_evidence_locations_and_known_invariants() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 1)), + D::CutReport(cut_report("1", 1)), + verdict("1", 1, vec![claim(ClaimOutcome::Holds, &[], Some("P1"), &[])]), + ])); + let mut bare = finding("1", 1, "F1", FindingConfidence::Plausible); + bare.evidence = vec![]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(bare)])), MindRefusal::FindingWithoutEvidence); + // Locations are evidence's other half: a finding that names no place + // in the code is refused the same way. + let mut placeless = finding("1", 1, "F1", FindingConfidence::Plausible); + placeless.locations = vec![]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(placeless)])), MindRefusal::FindingWithoutEvidence); + let mut unknown = finding("1", 1, "F1", FindingConfidence::Plausible); + unknown.invariants = vec![l("nope")]; + assert_eq!(refusal(admit(&mut mind, vec![D::Finding(unknown)])), MindRefusal::UnknownInvariant { label: "nope".into() }); + + + // A raw envelope without `range` cannot be built from the type, and + // it is the leaf's decode that refuses it, before any organ rule. + #[derive(serde::Serialize)] + struct Rangeless { + campaign: Slug, + verdict: Short, + label: eureka_pipeline::Label, + confidence: FindingConfidence, + severity: eureka_pipeline::FindingSeverity, + claim: Line, + invariants: Vec, + locations: Vec, + failure_scenario: eureka_pipeline::Para, + evidence: Vec, + precedents: Vec, + origin: eureka_pipeline::FindingOrigin, + } + let whole = finding("1", 1, "F1", FindingConfidence::Plausible); + let rangeless = Rangeless { + campaign: whole.campaign.clone(), + verdict: whole.verdict.clone(), + label: whole.label.clone(), + confidence: whole.confidence, + severity: whole.severity, + claim: whole.claim.clone(), + invariants: whole.invariants.clone(), + locations: whole.locations.clone(), + failure_scenario: whole.failure_scenario.clone(), + evidence: whole.evidence.clone(), + precedents: vec![], + origin: whole.origin, + }; + let mut envelope = prepare(&D::Finding(whole)); + envelope.payload = rmp_serde::to_vec_named(&(rangeless,)).unwrap(); + let outcome = mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Soul), vec![envelope], now()); + assert!( + matches!(refusal(outcome), MindRefusal::Document(PipelineRefusal::InvalidFormat { field, .. }) if field == "payload") + ); + + // The in-force target owns the invariant vocabulary. Supersede r1 with + // an r2 that drops its label, and the label is unknown again; r2's own + // label is what a finding may cite. The target is looked for in image + // and batch, like every other citation, so a finding may cite the + // vocabulary of the revision landing beside it: `other` is admitted in + // r2's own batch, before r2 is anywhere but here. + let mut fresh = finding("1", 1, "F4", FindingConfidence::Plausible); + fresh.invariants = vec![l("other")]; + assert_eq!( + refusal(admit(&mut mind, vec![D::Finding(fresh.clone())])), + MindRefusal::UnknownInvariant { label: "other".into() } + ); + committed(admit(&mut mind, vec![ + target(2, &["other"]), + resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, &id("target", "r2"))])), + D::Finding(fresh), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![D::Finding(finding("1", 1, "F2", FindingConfidence::Plausible))])), + MindRefusal::UnknownInvariant { label: INVARIANT.into() } + ); + let mut current = finding("1", 1, "F3", FindingConfidence::Plausible); + current.invariants = vec![l("other")]; + committed(admit(&mut mind, vec![D::Finding(current)])); + } + + #[test] + fn a_campaign_needs_no_stewardship() { + let mut mind = opened(MemoryStore::new(), INSTANCE); + committed(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])); + let D::Campaign(mut empty) = campaign(&[]) else { panic!() }; + empty.slug = slug("other"); + assert_eq!( + refusal(admit(&mut mind, vec![D::Campaign(empty)])), + MindRefusal::EmptyRepos { campaign: "other:campaign:self".into() } + ); + let mut spec = cut_spec("1", 1); + spec.repo = repo(OTHER_REPO); + assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + } + + /// A target revision naming repos, and the supersession of its predecessor. + fn repo_revision(revision: u32, repos: &[&str]) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!() }; + named.repos = repos.iter().map(|name| repo(name)).collect(); + let previous = r(K::Target, &id("target", &format!("r{}", revision - 1))); + let this = r(K::Target, &id("target", &format!("r{revision}"))); + vec![D::Target(named), resolution(previous, superseded(&[this]))] + } + + fn spec_on(cut: &str, repo_name: &str) -> D { + let mut spec = cut_spec(cut, 1); + spec.repo = repo(repo_name); + D::CutSpec(spec) + } + + #[test] + fn a_target_revision_adds_a_repo_a_spec_may_name() { + let mut mind = seeded(); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + + // The same revision, supersession and spec in one batch. + let mut fresh = seeded(); + let mut batch = repo_revision(2, &[REPO, OTHER_REPO]); + batch.push(spec_on("1", OTHER_REPO)); + committed(admit(&mut fresh, batch)); + } + + #[test] + fn a_target_revision_removes_a_repo() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + committed(admit(&mut mind, repo_revision(2, &[OTHER_REPO]))); + let spec_id = id("cut_spec", "cut-1.r1"); + let spec_view = mind.view(&r(K::CutSpec, &spec_id)).expect("a valid ref").expect("the spec is held"); + assert_eq!(spec_view.status, crate::query::PipelineStatus::InForce, "spec 1 r1 stays in force"); + committed(admit(&mut mind, vec![D::CutReport(cut_report("1", 1))])); + committed(admit(&mut mind, vec![resolution(r(K::CutSpec, &spec_id), withdrawn())])); + + let mut second = cut_spec("1", 2); + second.repo = repo(REPO); + let revised = id("cut_spec", "cut-1.r2"); + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(second), resolution(r(K::CutSpec, &spec_id), superseded(&[r(K::CutSpec, &revised)]))])), + MindRefusal::RepoNotInCampaign { repo: REPO.into() } + ); + committed(admit(&mut mind, vec![spec_on("2", OTHER_REPO)])); + } + + #[test] + fn a_target_revision_cannot_silently_drop_its_repos() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + refusal(admit(&mut mind, repo_revision(3, &[]))), + MindRefusal::EmptyRepos { campaign: id("target", "r3") } + ); + // Nothing was written: the restating revision takes the same key. + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + // An empty revision whose predecessor named none keeps the campaign's set. + let mut plain = seeded(); + committed(admit(&mut plain, repo_revision(2, &[]))); + } + + #[test] + fn a_repo_revision_replays_idempotently() { + let mut mind = seeded(); + let (first, _) = committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + assert_eq!( + admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO])), + PipelineAdmissionOutcome::AlreadyAdmitted { receipt_id: first } + ); + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + } + + /// Another campaign, opened with `repos`, and its own target revision 1 naming `named`. + fn other_campaign(repos: &[&str], named: &[&str]) -> Vec { + let D::Campaign(mut campaign) = campaign(repos) else { panic!("a campaign") }; + campaign.slug = "other-campaign".into(); + let D::Target(mut target) = target(1, &[INVARIANT]) else { panic!("a target") }; + target.campaign = "other-campaign".into(); + target.repos = named.iter().map(|name| repo(name)).collect(); + vec![D::Campaign(campaign), D::Target(target)] + } + + #[test] + fn another_campaigns_target_does_not_widen_membership() { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[REPO, OTHER_REPO]))); + assert_eq!(refusal(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); + } + + #[test] + fn a_target_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO, OTHER_REPO]))); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let foreign = r(K::Target, "other-campaign:target:r1"); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[foreign]))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Target, outcome: "Superseded".into() } + ); + // Nothing was written: r2 still names the repos, and its own revision still lands. + committed(admit(&mut mind, vec![spec_on("1", OTHER_REPO)])); + committed(admit(&mut mind, repo_revision(3, &[REPO, OTHER_REPO]))); + } + + /// A target of the first campaign (`CAMPAIGN`) at `revision`, naming no repos. + fn own_target(revision: u32) -> D { + target(revision, &[INVARIANT]) + } + + const FOREIGN_TARGET: &str = "other-campaign:target:r1"; + + fn supersession_refused(outcome: PipelineAdmissionOutcome, subject_kind: K) -> bool { + refusal(outcome) == MindRefusal::IncompatibleResolution { subject_kind, outcome: "Superseded".into() } + } + + #[test] + fn a_foreign_target_in_the_same_batch_does_not_supersede() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, &id("target", "r1")), superseded(&[r(K::Target, FOREIGN_TARGET)]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn every_supersessor_of_a_target_is_checked_whichever_is_foreign() { + for foreign_first in [false, true] { + let mut mind = seeded(); + committed(admit(&mut mind, other_campaign(&[REPO], &[]))); + let own = r(K::Target, &id("target", "r2")); + let foreign = r(K::Target, FOREIGN_TARGET); + let by = if foreign_first { [foreign, own] } else { [own, foreign] }; + let batch = vec![own_target(2), resolution(r(K::Target, &id("target", "r1")), superseded(&by))]; + assert!(supersession_refused(admit(&mut mind, batch), K::Target), "foreign first: {foreign_first}"); + } + } + + #[test] + fn a_new_campaigns_target_in_its_own_batch_is_not_closed_by_a_foreign_one() { + let mut mind = seeded(); + let mut batch = other_campaign(&[REPO], &[]); + batch.push(resolution(r(K::Target, FOREIGN_TARGET), superseded(&[r(K::Target, &id("target", "r1"))]))); + assert!(supersession_refused(admit(&mut mind, batch), K::Target)); + } + + #[test] + fn a_cut_spec_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + let mut batch = other_campaign(&[REPO], &[]); + let D::CutSpec(mut foreign) = spec_on("1", REPO) else { panic!("a spec") }; + foreign.campaign = "other-campaign".into(); + batch.push(D::CutSpec(foreign)); + committed(admit(&mut mind, batch)); + let lone = vec![resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, "other-campaign:cut_spec:cut-1.r1")]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::CutSpec)); + // Nothing was written: this campaign's own revision still lands. + let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; + next.revision = 2; + let this = r(K::CutSpec, &id("cut_spec", "cut-1.r2")); + committed(admit(&mut mind, vec![D::CutSpec(next), resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[this]))])); + } + + #[test] + fn a_ruling_is_superseded_by_a_ruling_of_any_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + let mut batch = other_campaign(&[REPO], &[]); + let mut foreign = ruling("new"); + foreign.campaign = "other-campaign".into(); + batch.push(D::Ruling(foreign)); + batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, "other-campaign:ruling:new")]))); + committed(admit(&mut mind, batch)); + } + + /// The slug that shares its dash prefix with `CAMPAIGN` (`eureka-state`). + const RIVAL: &str = "eureka-substrate"; + + /// Revision `revision` of the rival campaign's target, with its campaign when it is + /// the first and the supersession of its predecessor when it is not. + fn rival_revision(revision: u32) -> Vec { + let D::Target(mut named) = target(revision, &[INVARIANT]) else { panic!("a target") }; + named.campaign = RIVAL.into(); + let mut batch = Vec::new(); + if revision == 1 { + let D::Campaign(mut opened) = campaign(&[REPO]) else { panic!("a campaign") }; + opened.slug = RIVAL.into(); + batch.push(D::Campaign(opened)); + } + batch.push(D::Target(named)); + if revision > 1 { + let previous = r(K::Target, &format!("{RIVAL}:target:r{}", revision - 1)); + batch.push(resolution(previous, superseded(&[r(K::Target, &format!("{RIVAL}:target:r{revision}"))]))); + } + batch + } + + /// Both campaigns at revision 3, so each one's target in force is above revision 1. + fn both_campaigns_at_revision_three() -> Mind { + let mut mind = seeded(); + for revision in 2..=3 { + committed(admit(&mut mind, repo_revision(revision, &[REPO]))); + } + for revision in 1..=3 { + committed(admit(&mut mind, rival_revision(revision))); + } + mind + } + + #[test] + fn a_target_is_not_superseded_by_a_campaign_sharing_its_prefix_or_a_higher_revision() { + for (subject, supersessor) in [ + (id("target", "r3"), format!("{RIVAL}:target:r3")), + (format!("{RIVAL}:target:r3"), id("target", "r3")), + ] { + let mut mind = both_campaigns_at_revision_three(); + let lone = vec![resolution(r(K::Target, &subject), superseded(&[r(K::Target, &supersessor)]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Target), "{subject} by {supersessor}"); + } + // A foreign revision above the subject's is no likelier to pass. + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO]))); + for revision in 1..=4 { + committed(admit(&mut mind, rival_revision(revision))); + } + let lone = vec![resolution(r(K::Target, &id("target", "r2")), superseded(&[r(K::Target, &format!("{RIVAL}:target:r4"))]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Target)); + } + + #[test] + fn a_stewardship_is_superseded_only_on_its_own_instance_and_repo_line() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![stewardship(INSTANCE, OTHER_REPO)])); + let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + let huginn = format!("{INSTANCE}:stewardship:gamecult_-huginn.n1"); + let lone = vec![resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &huginn)]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::Stewardship)); + // Nothing was written: the repo's own next assignment still supersedes it. + let next = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); + committed(admit(&mut mind, vec![ + stewardship_n(INSTANCE, REPO, 2), + resolution(r(K::Stewardship, &epiphany), superseded(&[r(K::Stewardship, &next)])), + ])); + } + + #[test] + fn a_follow_up_is_superseded_only_within_its_campaign() { + let mut mind = seeded(); + let source = r(K::Target, &id("target", "r1")); + committed(admit(&mut mind, vec![follow_up("old", source.clone())])); + let mut batch = rival_revision(1); + let D::FollowUp(mut foreign) = follow_up("new", source.clone()) else { panic!("a follow-up") }; + foreign.campaign = RIVAL.into(); + batch.push(D::FollowUp(foreign)); + committed(admit(&mut mind, batch)); + let lone = vec![resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &format!("{RIVAL}:follow_up:new"))]))]; + assert!(supersession_refused(admit(&mut mind, lone), K::FollowUp)); + // Nothing was written: this campaign's own follow-up still supersedes it. + committed(admit(&mut mind, vec![ + follow_up("own", source), + resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "own"))])), + ])); + } + + #[test] + fn supersession_on_its_own_line_commits_for_every_kind() { + let source = r(K::Target, &id("target", "r1")); + let mut mind = seeded(); + committed(admit(&mut mind, repo_revision(2, &[REPO]))); + let mut mind = seeded(); + committed(admit(&mut mind, vec![spec_on("1", REPO)])); + let D::CutSpec(mut next) = spec_on("1", REPO) else { panic!("a spec") }; + next.revision = 2; + committed(admit(&mut mind, vec![ + D::CutSpec(next), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + let mut mind = seeded(); + committed(admit(&mut mind, vec![follow_up("old", source.clone())])); + committed(admit(&mut mind, vec![ + follow_up("new", source), + resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "new"))])), + ])); + let mut mind = seeded(); + let epiphany = format!("{INSTANCE}:stewardship:gamecult_-epiphany"); + committed(admit(&mut mind, vec![ + stewardship_n(INSTANCE, REPO, 2), + resolution(r(K::Stewardship, &format!("{epiphany}.n1")), superseded(&[r(K::Stewardship, &format!("{epiphany}.n2"))])), + ])); + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + committed(admit(&mut mind, vec![ + D::Ruling(ruling("new")), + resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &id("ruling", "new"))])), + ])); + } + + #[test] + fn a_ruling_of_one_campaign_is_superseded_by_a_ruling_of_a_prefix_sharing_campaign() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::Ruling(ruling("old"))])); + let mut batch = rival_revision(1); + let mut foreign = ruling("new"); + foreign.campaign = RIVAL.into(); + batch.push(D::Ruling(foreign)); + batch.push(resolution(r(K::Ruling, &id("ruling", "old")), superseded(&[r(K::Ruling, &format!("{RIVAL}:ruling:new"))]))); + committed(admit(&mut mind, batch)); + } + + #[test] + fn a_hand_off_derives_this_minds_side_only() { + let key = format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-epiphany.2026-09-16"); + let campaign_key = id("campaign", "self"); + let stewardship_key = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); + + let mut source = seeded(); + assert_eq!( + refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, OTHER_REPO, &[])])), + MindRefusal::NotStewarded { repo: OTHER_REPO.into() } + ); + assert_eq!( + refusal(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&id("ruling", "R1")])])), + MindRefusal::MissingReference { kind: K::Ruling, id: id("ruling", "R1") } + ); + let (_, writes) = committed(admit(&mut source, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); + let withdrawal = id_of(&stewardship_key); + assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Resolution, &withdrawal)]); + let Some(D::Resolution(derived)) = source.get(K::Resolution, &withdrawal).unwrap() else { panic!() }; + assert_eq!(derived.outcome, ResolutionOutcome::Withdrawn { reason: Line(key.clone()) }); + assert_eq!(derived.subject, r(K::Stewardship, &stewardship_key)); + + let mut receiving = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut receiving, vec![instance(OTHER_INSTANCE)])); + let (_, writes) = committed(admit(&mut receiving, vec![hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[&campaign_key])])); + let assigned = format!("{OTHER_INSTANCE}:stewardship:gamecult_-epiphany.n1"); + assert_eq!(writes, vec![r(K::HandOff, &key), r(K::Stewardship, &assigned)]); + let Some(D::Stewardship(derived)) = receiving.get(K::Stewardship, &assigned).unwrap() else { panic!() }; + assert_eq!(derived.note, Line(key.clone())); + assert_eq!(derived.instance, slug(OTHER_INSTANCE)); + // The stewardship starts the day the repo was handed over, not on some + // clock of admission's own. + assert_eq!(derived.assigned_on, date()); + assert_eq!(source.envelope(K::HandOff, &key).map(|e| &e.payload), receiving.envelope(K::HandOff, &key).map(|e| &e.payload)); + + // A derived write passes A3-A7 and A8 like any other, and it counts as + // a record of its scope. A batch carrying its own assignment of the + // repo beside the hand-off is two assignments: the derived one takes + // the sequence after the batch's, and the batch's own is then out of + // sequence -- a typed refusal, not a duplicate identity the store + // complains about. + let mut colliding = opened(MemoryStore::new(), OTHER_INSTANCE); + committed(admit(&mut colliding, vec![instance(OTHER_INSTANCE)])); + assert_eq!( + refusal(admit(&mut colliding, vec![ + hand_off(INSTANCE, OTHER_INSTANCE, REPO, &[]), + stewardship(OTHER_INSTANCE, REPO), + ])), + MindRefusal::StewardshipOutOfSequence { repo: REPO.into(), expected: 3, actual: 1 } + ); + } + + /// The key of the first resolution of a stewardship: the subject's root, + /// then its kind and local -- the subject's own sequence included -- then + /// this resolution's own sequence. + fn id_of(stewardship_key: &str) -> String { + let (root, rest) = stewardship_key.split_once(':').unwrap(); + format!("{root}:resolution:{}.n1", rest.replace(':', ".")) + } + + /// The second construction site of a receipt, deliberately: a receipt + /// planted through the store must be found by replay and must match. + #[test] + fn a_planted_receipt_with_other_content_is_refused_not_replayed() { + let store = MemoryStore::new(); + let mut mind = opened(store.clone(), INSTANCE); + seed(&mut mind); + let envelopes = vec![prepare(&question("Q1", &["A", "B"], "A"))]; + let ordinal = receipt::head(&mind).unwrap() + 1; + let candidate = + receipt::candidate(&slug(INSTANCE), provenance(Faculty::Hands), &[], &envelopes, ordinal, now()).unwrap(); + let mut planted = HuginnCommitReceipt { writes: vec![], ..candidate.clone() }; + planted.writes = candidate.strong_reads.clone(); + planted.receipt_id = candidate.receipt_id.clone(); + store.plant(mind.cache().prepare_entry_named(&planted.receipt_id, &planted).unwrap().0); + let mut mind = opened(store, INSTANCE); + assert!(matches!( + refusal(mind.admit_prepared(&slug(INSTANCE), provenance(Faculty::Hands), envelopes, now())), + MindRefusal::Unavailable { .. } + )); + } +} From 383600c23426d339ea6045a5786afd564701adea Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 21:56:58 +0200 Subject: [PATCH 10/12] Lineage: a follow-up's line is global, as a ruling's; re-homing work across campaigns supersedes (ruling supersession-lineage-3) Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 23 +++++++++++++---------- crates/huginn-mind/src/docs.rs | 10 +++++----- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 28b9a03..de9a875 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -2962,23 +2962,26 @@ mod tests { ])); } + /// Re-homing work is how Self routes it (ruling `supersession-lineage-3`): + /// a gap follow-up of one campaign is superseded by its successor in the + /// campaign that owns the work now, and the typed edge survives. #[test] - fn a_follow_up_is_superseded_only_within_its_campaign() { + fn a_follow_up_is_superseded_by_one_re_homed_to_another_campaign() { let mut mind = seeded(); let source = r(K::Target, &id("target", "r1")); - committed(admit(&mut mind, vec![follow_up("old", source.clone())])); + committed(admit(&mut mind, vec![follow_up("gap-session-id", source.clone())])); let mut batch = rival_revision(1); - let D::FollowUp(mut foreign) = follow_up("new", source.clone()) else { panic!("a follow-up") }; + let D::FollowUp(mut foreign) = follow_up("gap-session-id", source) else { panic!("a follow-up") }; foreign.campaign = RIVAL.into(); batch.push(D::FollowUp(foreign)); + batch.push(resolution( + r(K::FollowUp, &id("follow_up", "gap-session-id")), + superseded(&[r(K::FollowUp, &format!("{RIVAL}:follow_up:gap-session-id"))]), + )); committed(admit(&mut mind, batch)); - let lone = vec![resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &format!("{RIVAL}:follow_up:new"))]))]; - assert!(supersession_refused(admit(&mut mind, lone), K::FollowUp)); - // Nothing was written: this campaign's own follow-up still supersedes it. - committed(admit(&mut mind, vec![ - follow_up("own", source), - resolution(r(K::FollowUp, &id("follow_up", "old")), superseded(&[r(K::FollowUp, &id("follow_up", "own"))])), - ])); + let docs = docs_of(&mind); + assert!(!docs.in_force(K::FollowUp, &id("follow_up", "gap-session-id"))); + assert!(docs.in_force(K::FollowUp, &format!("{RIVAL}:follow_up:gap-session-id"))); } #[test] diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index bb73776..49bbbbb 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -299,20 +299,21 @@ impl Docs { } } -/// The line a record belongs to (ruling `supersession-lineage-2`): the one place +/// The line a record belongs to (rulings `supersession-lineage-2` and `-3`): the one place /// lineage is decided. A record supersedes, and is reinstated over, only a record /// of its own line. `None` is a kind the resolution matrix never lets be /// superseded; the match names every kind, so a new kind fails to compile until /// its line is stated. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Lineage<'a> { - /// A target, or a follow-up: the campaign. + /// A target: the campaign. Campaign(&'a Slug), /// A cut spec: the campaign and the cut. Cut(&'a Slug, &'a Label), /// A stewardship: the instance and the repo. Assignment(&'a Slug, &'a OrgRepo), - /// A ruling: operator directions cut across campaigns. + /// A ruling or a follow-up: operator directions and re-homed work cut + /// across campaigns. Global, } @@ -321,9 +322,8 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option> { match document { D::Target(target) => Some(Lineage::Campaign(&target.campaign)), D::CutSpec(spec) => Some(Lineage::Cut(&spec.campaign, &spec.cut)), - D::FollowUp(follow_up) => Some(Lineage::Campaign(&follow_up.campaign)), D::Stewardship(stewardship) => Some(Lineage::Assignment(&stewardship.instance, &stewardship.repo)), - D::Ruling(_) => Some(Lineage::Global), + D::Ruling(_) | D::FollowUp(_) => Some(Lineage::Global), D::Campaign(_) | D::Question(_) | D::CutReport(_) From e1332fbd305478adb5720f5b29998d6ac7fecf05 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 21:58:06 +0200 Subject: [PATCH 11/12] Stewardship: rival_in_force is the one owner of one-in-force; admission and reinstatement ask it AlreadyStewarded and the withdrawal-over-later check each decided it with their own comparison, and disagreed when a later record was reinstated beside an earlier one. later_than's stewardship arm is gone. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 41 ++++++++++++++++++++++++++--- crates/huginn-mind/src/docs.rs | 27 +++++++++++-------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index de9a875..f278935 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -598,7 +598,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { actual: stewardship.sequence, }); } - if docs.stewardships_of(mind, &stewardship.repo, None).iter().any(|(other, _)| *other != key) { + if docs.rival_in_force(key, &staged.document).is_some() { return Err(MindRefusal::AlreadyStewarded { repo: stewardship.repo.0.clone() }); } Ok(()) @@ -770,11 +770,19 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M && subject_kind == K::Resolution && let Some(D::Resolution(reinstating)) = docs.find(K::Resolution, subject_id) && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) - && let Some(later) = docs.later_in_force(base) + && let Some(rival) = docs.rival_in_force(&reinstating.subject.id.0, base) { + // A stewardship put back in force beside an earlier one meets the + // admission refusal for the same fact: the repo already has a steward. + if let D::Stewardship(base) = base + && let Some(D::Stewardship(standing)) = docs.find(K::Stewardship, rival) + && standing.sequence < base.sequence + { + return Err(MindRefusal::AlreadyStewarded { repo: base.repo.0.clone() }); + } return Err(MindRefusal::WouldReinstateOverLater { subject: reinstating.subject.id.0.clone(), - later: later.into(), + later: rival.into(), }); } // Withdrawing a run's closure puts the run back in force, and a run in @@ -1682,6 +1690,33 @@ mod tests { assert_eq!(in_force, vec![stewardship_key(1)]); } + /// One assignment stands per repo, whichever record a withdrawal would put + /// back: reinstating the later of two withdrawn assignments while the + /// earlier is back in force is refused as the assignment itself would be, + /// and the next assignment then meets exactly the one that stands. + #[test] + fn a_withdrawal_does_not_reinstate_a_stewardship_beside_an_earlier_one() { + let mut mind = seeded(); + let n1 = stewardship_key(1); + let n2 = stewardship_key(2); + let first = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); + let second = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n2.n1"); + committed(admit(&mut mind, vec![resolution(r(K::Stewardship, &n1), withdrawn())])); + committed(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 2)])); + committed(admit(&mut mind, vec![resolution(r(K::Stewardship, &n2), withdrawn())])); + committed(admit(&mut mind, vec![resolution(r(K::Resolution, &first), withdrawn())])); + assert_eq!( + refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &second), withdrawn())])), + MindRefusal::AlreadyStewarded { repo: REPO.into() } + ); + let in_force = docs_of(&mind).stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| key.to_string()).collect::>(); + assert_eq!(in_force, vec![n1], "the reinstated assignment stands alone"); + assert_eq!( + refusal(admit(&mut mind, vec![stewardship_n(INSTANCE, REPO, 3)])), + MindRefusal::AlreadyStewarded { repo: REPO.into() } + ); + } + /// And it reads the batch: the assignment that stands later than the one /// a withdrawal would reinstate may be landing beside that withdrawal, /// derived by a hand-off in the same batch. diff --git a/crates/huginn-mind/src/docs.rs b/crates/huginn-mind/src/docs.rs index 49bbbbb..f7af836 100644 --- a/crates/huginn-mind/src/docs.rs +++ b/crates/huginn-mind/src/docs.rs @@ -224,12 +224,17 @@ impl Docs { .collect() } - /// The in-force record that stands later than `base` in the scope `base` - /// is sequenced in, if any: what a withdrawal that put `base` back in - /// force would leave standing beside it. - pub(crate) fn later_in_force(&self, base: &PipelineDocument) -> Option<&str> { + /// The in-force record, other than `base_key`, that cannot stand beside + /// `base` on its line: the one question "may this record be in force" + /// asks, whether `base` is being admitted (a stewardship's + /// `AlreadyStewarded`) or put back in force by a withdrawal. A line holds + /// one stewardship at a time, so any other stands in its way; a revision + /// is only displaced by a later one. + pub(crate) fn rival_in_force(&self, base_key: &str, base: &PipelineDocument) -> Option<&str> { let kind = base.kind(); - self.of_kind(kind).find(|(key, other)| later_than(base, other) && self.in_force(kind, key)).map(|(key, _)| key) + self.of_kind(kind) + .find(|(key, other)| *key != base_key && rivals(base, other) && self.in_force(kind, key)) + .map(|(key, _)| key) } /// The in-force run, other than `run_key`, that holds `claim`: the one @@ -336,18 +341,18 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option> { } } -/// Whether `other` is a later record than `base` on the line `base` is -/// sequenced in. Kinds that carry no sequence have no order, and a resolution -/// is never a base here: the cap above refuses the withdrawal that would -/// reinstate one before the question of a later record arises. -fn later_than(base: &PipelineDocument, other: &PipelineDocument) -> bool { +/// Whether `other` cannot stand in force beside `base`, being on `base`'s line: +/// any other stewardship of an instance and repo, a later revision of a target +/// or cut spec. A resolution is never a base here: the cap in `resolution_rule` +/// refuses the withdrawal that would reinstate one first. +fn rivals(base: &PipelineDocument, other: &PipelineDocument) -> bool { use PipelineDocument as D; let Some(line) = line_of(base) else { return false }; if line_of(other) != Some(line) { return false; } match (base, other) { - (D::Stewardship(base), D::Stewardship(other)) => other.sequence > base.sequence, + (D::Stewardship(_), D::Stewardship(_)) => true, (D::Target(base), D::Target(other)) => other.revision > base.revision, (D::CutSpec(base), D::CutSpec(other)) => other.revision > base.revision, _ => false, From 4442e1acd9e272d4e930d8dc473eb1981d4ce910 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 22 Oct 2026 22:27:00 +0200 Subject: [PATCH 12/12] Stewardship reinstatement: the repo already has a steward is one refusal, earlier or later rival Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index f278935..96b39f9 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -772,12 +772,9 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M && let Some(base) = docs.find(reinstating.subject.kind, &reinstating.subject.id.0) && let Some(rival) = docs.rival_in_force(&reinstating.subject.id.0, base) { - // A stewardship put back in force beside an earlier one meets the - // admission refusal for the same fact: the repo already has a steward. - if let D::Stewardship(base) = base - && let Some(D::Stewardship(standing)) = docs.find(K::Stewardship, rival) - && standing.sequence < base.sequence - { + // The repo already has a steward, earlier or later: the admission + // refusal for the same fact, from the same owner. + if let D::Stewardship(base) = base { return Err(MindRefusal::AlreadyStewarded { repo: base.repo.0.clone() }); } return Err(MindRefusal::WouldReinstateOverLater { @@ -1559,7 +1556,8 @@ mod tests { /// Q19 A's rationale, which the cap alone does not carry: a withdrawal /// never re-raises an earlier record over a later one. The repo went away /// and came back, so withdrawing the hand-off's withdrawal would put the - /// first assignment back in force beside the second. + /// first assignment back in force beside the second: the repo already has + /// a steward. #[test] fn a_withdrawal_does_not_reinstate_a_stewardship_over_a_later_one() { let mut mind = seeded(); @@ -1567,12 +1565,11 @@ mod tests { let D::HandOff(mut back) = hand_off(OTHER_INSTANCE, INSTANCE, REPO, &[]) else { panic!() }; back.handed_on = eureka_pipeline::Date("2026-09-17".into()); committed(admit(&mut mind, vec![D::HandOff(back)])); - let first = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"); let again = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"); let withdrawal = format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"); assert_eq!( refusal(admit(&mut mind, vec![resolution(r(K::Resolution, &withdrawal), withdrawn())])), - MindRefusal::WouldReinstateOverLater { subject: first, later: again.clone() } + MindRefusal::AlreadyStewarded { repo: REPO.into() } ); let docs = Docs::from_image(mind.envelopes()).unwrap(); let in_force = docs.stewardships_of(&slug(INSTANCE), &repo(REPO), None).iter().map(|(key, _)| *key).collect::>(); @@ -1692,8 +1689,8 @@ mod tests { /// One assignment stands per repo, whichever record a withdrawal would put /// back: reinstating the later of two withdrawn assignments while the - /// earlier is back in force is refused as the assignment itself would be, - /// and the next assignment then meets exactly the one that stands. + /// earlier is back in force is refused, and the next assignment then meets + /// exactly the one that stands. #[test] fn a_withdrawal_does_not_reinstate_a_stewardship_beside_an_earlier_one() { let mut mind = seeded(); @@ -1730,7 +1727,7 @@ mod tests { hand_off_on(OTHER_INSTANCE, INSTANCE, "2026-09-17"), resolution(r(K::Resolution, &withdrawal), withdrawn()), ])), - MindRefusal::WouldReinstateOverLater { subject: stewardship_key(1), later: stewardship_key(2) } + MindRefusal::AlreadyStewarded { repo: REPO.into() } ); }