From 0c4e700b9eb7a5c120679ce395e14da589cb2e5c Mon Sep 17 00:00:00 2001 From: Eureka Date: Mon, 9 Nov 2026 11:48:19 +0000 Subject: [PATCH] Import: the resolution of a follow-up or idea the receiver wrote is inert in the window inert_in_window covered FollowUp and Idea only, so a receiver's own resolution of such a record, admitted after the import began, refused the rerun in both the tool (resume/window_writes) and the daemon (root_holds_native_write). A Resolution whose subject is a follow-up or idea the plan does not replay touches nothing planned and now stays beside the replay. One predicate, both callers; the resolution of a replayed follow-up, or of any other kind, still refuses. Co-Authored-By: Claude Sonnet 5.5 --- crates/eureka-state/src/bin/huginn/import.rs | 71 +++++++++++++++++--- crates/huginn-mind/src/image.rs | 10 ++- crates/huginn-mind/src/import.rs | 13 ++-- crates/huginn-mind/src/move_tests.rs | 25 +++++++ 4 files changed, 102 insertions(+), 17 deletions(-) diff --git a/crates/eureka-state/src/bin/huginn/import.rs b/crates/eureka-state/src/bin/huginn/import.rs index ae334c8..be47df3 100644 --- a/crates/eureka-state/src/bin/huginn/import.rs +++ b/crates/eureka-state/src/bin/huginn/import.rs @@ -15,7 +15,7 @@ use eureka_state::hold; use huginn_mind::eureka_pipeline::{PipelineDocument, PipelineKind, PipelineRef, Short, Slug}; use huginn_mind::{ AdmissionFacts, Faculty, HuginnMindRequest, HuginnMindResponse, ImportBatch, ImportFault, ImportPlan, PipelineAdmissionBatch, PipelineAdmissionOutcome, - PipelineDocumentSummary, PipelineDocumentView, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan, + PipelineDocumentSummary, PipelineFacts, PipelineDocumentView, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan, }; use crate::config::{Settings, TOOL}; @@ -73,7 +73,7 @@ fn import(settings: &Settings, flags: Flags) -> Step<()> { }; let root = headers(&ask_receiver, &campaign)?; let held = resume(&plan, &root, giver.instance())?; - let window = window_writes(root.iter().map(|header| (header.id.kind, header.id.id.0.as_str(), &header.admission)), &planned_keys(&plan), giver.instance()); + let window = window_writes(root.iter().map(|header| (header.id.kind, header.id.id.0.as_str(), &header.admission, resolved_by_facts(&header.facts))), &planned_keys(&plan), giver.instance()); if !window.is_empty() { say(format_args!("left in place, admitted by the receiver after the import began: {}", window.iter().copied().collect::>().join(", "))); } @@ -208,13 +208,29 @@ fn headers(ask: &dyn Fn(Selection) -> Result, campa /// `import-window-writes-ruled`: the import stops at a collision, and only a collision). They are /// left where they are; they cannot be a planned document's key, and the admission of each later /// batch still refuses any replay that meets one. -fn window_writes<'a>(root: impl Iterator + Clone, planned: &BTreeSet<&str>, source: &Slug) -> BTreeSet<&'a str> { - let began = root.clone().filter(|(_, _, admission)| admission.provenance.imported_from.as_ref().is_some_and(|from| from.instance == *source)).map(|(_, _, admission)| admission.ordinal).min(); - root.filter(|(kind, key, admission)| inert_in_window(*kind) && admission.provenance.imported_from.is_none() && !planned.contains(key) && began.is_some_and(|began| admission.ordinal > began)) - .map(|(_, key, _)| key) +fn window_writes<'a>(root: impl Iterator)> + Clone, planned: &BTreeSet<&str>, source: &Slug) -> BTreeSet<&'a str> { + let began = root.clone().filter(|(_, _, admission, _)| admission.provenance.imported_from.as_ref().is_some_and(|from| from.instance == *source)).map(|(_, _, admission, _)| admission.ordinal).min(); + root.filter(|(kind, key, admission, subject)| inert_in_window(*kind, subject.map(|subject| (subject.kind, subject.id.0.as_str())), |id| planned.contains(id)) && admission.provenance.imported_from.is_none() && !planned.contains(key) && began.is_some_and(|began| admission.ordinal > began)) + .map(|(_, key, _, _)| key) .collect() } +/// What a resolution resolves, from a header. +fn resolved_by_facts(facts: &PipelineFacts) -> Option<&PipelineRef> { + match facts { + PipelineFacts::Resolution { subject, .. } => Some(subject), + _ => None, + } +} + +/// What a resolution resolves, from a whole document. +fn resolved_by_document(document: &PipelineDocument) -> Option<&PipelineRef> { + match document { + PipelineDocument::Resolution(resolution) => Some(&resolution.subject), + _ => None, + } +} + /// The plan's document keys. fn planned_keys(plan: &ImportPlan) -> BTreeSet<&str> { plan.batches.iter().flat_map(|batch| batch.documents.iter().map(|(id, _)| id.id.0.as_str())).collect() @@ -233,7 +249,7 @@ fn resume(plan: &ImportPlan, root: &[PipelineDocumentSummary], source: &Slug) -> .collect(); let mut held: Vec> = vec![BTreeSet::new(); plan.batches.len()]; let mut own = Vec::new(); - let window = window_writes(root.iter().map(|header| (header.id.kind, header.id.id.0.as_str(), &header.admission)), &planned_keys(plan), source); + let window = window_writes(root.iter().map(|header| (header.id.kind, header.id.id.0.as_str(), &header.admission, resolved_by_facts(&header.facts))), &planned_keys(plan), source); for header in root { let key = header.id.id.0.as_str(); if window.contains(key) { @@ -289,7 +305,7 @@ fn verify(root: &[PipelineDocumentView], plan: &ImportPlan, source: &Slug) -> St } } let planned = planned_keys(plan); - let window = window_writes(root.iter().map(|view| (view.id.kind, view.id.id.0.as_str(), &view.admission)), &planned, source); + let window = window_writes(root.iter().map(|view| (view.id.kind, view.id.id.0.as_str(), &view.admission, resolved_by_document(&view.document))), &planned, source); differing.extend(held.keys().filter(|key| !planned.contains(*key) && !window.contains(*key)).map(|key| format!("{key}: not in the plan"))); if differing.is_empty() { Ok(()) @@ -300,8 +316,8 @@ fn verify(root: &[PipelineDocumentView], plan: &ImportPlan, source: &Slug) -> St #[cfg(test)] mod tests { - use huginn_mind::eureka_pipeline::{Date, PipelineKind, PipelineRuling, RulingAuthority}; - use huginn_mind::{AdmissionFacts, ImportSource, PipelineFacts, PipelineStatus, PipelineStatusSummary}; + use huginn_mind::eureka_pipeline::{Date, PipelineKind, PipelineRuling, ResolutionOutcome, RulingAuthority}; + use huginn_mind::{AdmissionFacts, ImportSource, PipelineStatus, PipelineStatusSummary}; use super::*; @@ -422,7 +438,7 @@ mod tests { let source = slug("eureka"); let root = [at(header("a", Some(("eureka", 3))), 10), at(header("b", Some(("eureka", 3))), 10), at(note(header("zz", None), PipelineKind::FollowUp), 20)]; assert_eq!(resume(&two(), &root, &source).ok().unwrap(), vec![true, false]); - let window = window_writes(root.iter().map(|held| (held.id.kind, held.id.id.0.as_str(), &held.admission)), &planned_keys(&two()), &source); + let window = window_writes(root.iter().map(|held| (held.id.kind, held.id.id.0.as_str(), &held.admission, resolved_by_facts(&held.facts))), &planned_keys(&two()), &source); assert_eq!(window.into_iter().collect::>(), vec!["c:ruling:zz"]); } @@ -450,6 +466,39 @@ mod tests { assert_eq!(resume(&two(), &between, &source).ok().unwrap(), vec![true, false]); } + /// Void's case: the import stopped after its first receipt, her Persona then admitted + /// follow-ups under the root and resolved them. A resolution of a window follow-up the plan + /// lacks touches nothing planned, so it stays in place like the follow-up. + #[test] + fn a_resolution_of_a_window_follow_up_does_not_block_the_resume() { + let source = slug("eureka"); + let began = [at(header("a", Some(("eureka", 3))), 10), at(header("b", Some(("eureka", 3))), 10)]; + let resolving = |subject_kind: PipelineKind, subject: &str, ordinal| { + let mut held = note(header("zz.n1", None), PipelineKind::Resolution); + held.facts = PipelineFacts::Resolution { + subject: PipelineRef { kind: subject_kind, id: Short(subject.into()) }, + sequence: 1, + outcome: ResolutionOutcome::Recorded { reason: "done".into() }, + resolved_on: Date("2026-11-09".into()), + }; + at(held, ordinal) + }; + for (kind, subject) in [(PipelineKind::FollowUp, "c:follow_up:zz"), (PipelineKind::Idea, "c:idea:zz")] { + let root = [began.as_slice(), &[resolving(kind, subject, 20)]].concat(); + assert_eq!(resume(&two(), &root, &source).ok().unwrap(), vec![true, false], "{kind:?}"); + } + for refused in [ + // a resolution of a planned document, or of a kind that derives status + resolving(PipelineKind::FollowUp, "c:ruling:a", 20), + resolving(PipelineKind::Ruling, "c:ruling:zz", 20), + // admitted before the import began + resolving(PipelineKind::FollowUp, "c:follow_up:zz", 9), + ] { + let said = stopped(resume(&two(), &[began.as_slice(), &[refused]].concat(), &source)); + assert!(said.starts_with("no: Refused the receiver already holds records under the root"), "{said}"); + } + } + #[test] fn the_verification_walk_leaves_a_window_write_out_and_still_names_an_earlier_stranger() { let source = slug("eureka"); diff --git a/crates/huginn-mind/src/image.rs b/crates/huginn-mind/src/image.rs index 2f0ba7a..5e15898 100644 --- a/crates/huginn-mind/src/image.rs +++ b/crates/huginn-mind/src/image.rs @@ -151,9 +151,17 @@ impl Image { .flat_map(|kind| self.posting(*kind, Alias::Root.name(), root).into_iter().flatten().map(move |id| (*kind, self.row(*id)))) .collect(); let began = rows.iter().filter(|(_, row)| row.view.admission.provenance.imported_from.is_some()).map(|(_, row)| row.view.admission.ordinal).min(); + // What the import replays is what this mind holds from it already or will be handed; a + // subject this mind wrote itself is never in the plan. + let replayed = |kind: PipelineKind, id: &str| self.id_of(kind, id).is_some_and(|row| self.row(row).view.admission.provenance.imported_from.is_some()); rows.iter().any(|(kind, row)| { + let resolves = match &row.view.document { + PipelineDocument::Resolution(resolution) => Some((resolution.subject.kind, resolution.subject.id.0.as_str())), + _ => None, + }; row.view.admission.provenance.imported_from.is_none() - && !(crate::import::inert_in_window(*kind) && began.is_some_and(|began| row.view.admission.ordinal > began)) + && !(crate::import::inert_in_window(*kind, resolves, |id| resolves.is_some_and(|(subject, _)| replayed(subject, id))) + && began.is_some_and(|began| row.view.admission.ordinal > began)) }) } diff --git a/crates/huginn-mind/src/import.rs b/crates/huginn-mind/src/import.rs index 19f9559..e689154 100644 --- a/crates/huginn-mind/src/import.rs +++ b/crates/huginn-mind/src/import.rs @@ -53,11 +53,14 @@ pub enum ImportFault { /// Whether a native write of this kind, admitted after an import under its root began, cannot touch /// the status or derivation of any document the import will still replay: a follow-up or an idea -/// has no referent a replay resolves and derives nothing. Ruling `import-window-writes-ruled` -/// stops an import at a collision; this is the writes that cannot collide. Every other kind may -/// answer, supersede or resolve a planned document, and still refuses the import. -pub fn inert_in_window(kind: PipelineKind) -> bool { - matches!(kind, PipelineKind::FollowUp | PipelineKind::Idea) +/// has no referent a replay resolves and derives nothing, and neither has the resolution of one +/// (`resolves`: the subject's kind and id) that the import does not replay (`planned`). Ruling +/// `import-window-writes-ruled` stops an import at a collision; this is the writes that cannot +/// collide. Every other kind may answer, supersede or resolve a planned document, and still refuses +/// the import. +pub fn inert_in_window(kind: PipelineKind, resolves: Option<(PipelineKind, &str)>, planned: impl Fn(&str) -> bool) -> bool { + let harmless = |kind| matches!(kind, PipelineKind::FollowUp | PipelineKind::Idea); + harmless(kind) || (kind == PipelineKind::Resolution && resolves.is_some_and(|(subject, id)| harmless(subject) && !planned(id))) } /// The plan for `receiver` to import `campaign` from `source`, given every document the source diff --git a/crates/huginn-mind/src/move_tests.rs b/crates/huginn-mind/src/move_tests.rs index f4e1c7c..24f60e2 100644 --- a/crates/huginn-mind/src/move_tests.rs +++ b/crates/huginn-mind/src/move_tests.rs @@ -613,6 +613,31 @@ fn an_import_resumes_past_a_follow_up_or_idea_the_receiver_wrote_after_it_began( } } +#[test] +fn an_import_resumes_past_the_resolution_of_a_follow_up_the_receiver_wrote_after_it_began() { + let q1 = id("question", "Q1"); + let begun = || { + let mut recv = receiver(); + committed(import_from_kiko(&mut recv, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A")])); + recv + }; + // Her own follow-up, closed natively: nothing the replay reaches. + let mut recv = begun(); + let native = follow_up("FU-N", r(K::Question, &q1)); + let native_ref = r(K::FollowUp, &pipeline_key(&native).unwrap()); + committed(admit(&mut recv, vec![native])); + committed(admit(&mut recv, vec![resolution(native_ref, ResolutionOutcome::Recorded { reason: "done".into() })])); + committed(import_from_kiko(&mut recv, vec![question("Q2", &["A", "B"], "A")])); + + // A native closing of a replayed follow-up is the receiver reaching into the plan: still refused. + let mut recv = begun(); + let imported = follow_up("FU-I", r(K::Question, &q1)); + let imported_ref = r(K::FollowUp, &pipeline_key(&imported).unwrap()); + committed(import_from_kiko(&mut recv, vec![imported])); + committed(admit(&mut recv, vec![resolution(imported_ref, ResolutionOutcome::Recorded { reason: "done".into() })])); + assert_eq!(refusal(import_from_kiko(&mut recv, vec![question("Q2", &["A", "B"], "A")])), MindRefusal::ImportOutsideCampaign { root: CAMPAIGN.into() }); +} + #[test] fn an_import_never_cites_or_resolves_a_follow_up_the_receiver_wrote() { let q1 = id("question", "Q1");