Import: order the use list, pin the later-batch collision, state it in the window_writes doc

A follow-up the receiver wrote natively, closed, and a later batch replays is inert to the
daemon now and refused as an identity collision when the batch arrives.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-09 12:05:24 +00:00
commit 3b35fb8bae
2 changed files with 21 additions and 2 deletions

View file

@ -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, PipelineFacts, PipelineDocumentView, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan,
PipelineDocumentSummary, PipelineDocumentView, PipelineFacts, PipelinePageItems, PipelineProvenance, PipelineSelectionPage, inert_in_window, plan,
};
use crate::config::{Settings, TOOL};
@ -207,7 +207,9 @@ fn headers(ask: &dyn Fn(Selection) -> Result<HuginnMindResponse, Trouble>, campa
/// from this source and are no planned document: writes in the import window (ruling
/// `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.
/// batch still refuses any replay that meets one. A record whose key a later batch carries is no window write: the
/// tool keeps it planned and refuses the resume, and the daemon, which cannot see the later batch,
/// refuses that batch as an identity collision when it arrives.
fn window_writes<'a>(root: impl Iterator<Item = (PipelineKind, &'a str, &'a AdmissionFacts, Option<&'a PipelineRef>)> + 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))

View file

@ -638,6 +638,23 @@ fn an_import_resumes_past_the_resolution_of_a_follow_up_the_receiver_wrote_after
assert_eq!(refusal(import_from_kiko(&mut recv, vec![question("Q2", &["A", "B"], "A")])), MindRefusal::ImportOutsideCampaign { root: CAMPAIGN.into() });
}
/// The daemon cannot see a later batch: a follow-up the receiver wrote natively and closed, whose
/// key a later batch replays, is inert to it now. The batch then meets the key and is refused as a
/// collision, so the window never reaches the receiver's own record.
#[test]
fn a_native_follow_up_a_later_batch_replays_is_refused_when_that_batch_arrives() {
let q1 = id("question", "Q1");
let mut recv = receiver();
committed(import_from_kiko(&mut recv, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A")]));
let native = follow_up("FU-L", r(K::Question, &q1));
let native_ref = r(K::FollowUp, &pipeline_key(&native).unwrap());
committed(admit(&mut recv, vec![native.clone()]));
committed(admit(&mut recv, vec![resolution(native_ref, ResolutionOutcome::Recorded { reason: "done".into() })]));
let before = head(&recv);
assert!(matches!(refusal(import_from_kiko(&mut recv, vec![native])), MindRefusal::IdentityCollision { .. }));
assert_eq!(head(&recv), before);
}
#[test]
fn an_import_never_cites_or_resolves_a_follow_up_the_receiver_wrote() {
let q1 = id("question", "Q1");