From 7218c4a9d7c603900902ce081187db93ef4e9a88 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 8 Nov 2026 19:32:01 +0000 Subject: [PATCH] admission: an import replays a cite of another root's closed follow-up A resolution's cited referent had to be in force in the receiver. When the receiver had already imported the other root (cultlib-gaps, aetheria-ew) and that referent closed later in the giver, the replay of the earlier Deferred cite was refused with CitesResolvedDocument and the import stopped. The cite was in force when the giver made it; an import now asks only that the foreign referent is held (A7), not that it is still open. Same-root cites replay in the giver's order and keep the rule; native writes keep it. Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 13 +++++++--- crates/huginn-mind/src/move_tests.rs | 36 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index a316b34..a1a2b29 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -717,7 +717,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo Ok(()) } D::Resolution(resolution) => { - resolution_rule(docs, resolution)?; + resolution_rule(docs, resolution, import.is_some())?; match &resolution.outcome { ResolutionOutcome::Moved { to } if to == mind => Err(MindRefusal::MoveToSelf), // The campaign's root and the mind's own instance root are one namespace: the freeze would @@ -907,7 +907,13 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { /// 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> { +/// +/// An import replays the giver's judgement of the cite as the giver made it, and the receiver may +/// hold a referent of another root whose closing it imported earlier (`replay`): that referent was +/// in force when the giver cited it, and the replay of this root cannot reorder another root's +/// history. The referent must still be held (A7); only its force is not asked. A referent of the +/// subject's own root replays in the giver's order and is judged as ever. +fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution, replay: bool) -> Result<(), MindRefusal> { use PipelineDocument as D; use PipelineKind as K; let subject_kind = resolution.subject.kind; @@ -977,7 +983,8 @@ fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution) -> Result<( } for (role, referent) in outcome_citations(&resolution.outcome) { // The last memory a night was shown may have been consumed by that night's own batches. - if role != CitationRole::DayThrough && !docs.in_force(referent.kind, &referent.id.0) { + let foreign_replay = replay && root_and_local(&referent.id.0).0 != root_and_local(subject_id).0; + if role != CitationRole::DayThrough && !foreign_replay && !docs.in_force(referent.kind, &referent.id.0) { return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 }); } } diff --git a/crates/huginn-mind/src/move_tests.rs b/crates/huginn-mind/src/move_tests.rs index 3bd200a..b2c7128 100644 --- a/crates/huginn-mind/src/move_tests.rs +++ b/crates/huginn-mind/src/move_tests.rs @@ -638,3 +638,39 @@ fn a_campaign_cannot_be_moved_out_of_its_minds_own_instance_root() { MindRefusal::ImportOutsideCampaign { root: RECEIVER.into() } ); } + +/// The giver cited another root's follow-up while it stood open and that follow-up closed later; +/// the receiver imported the other root first, so it holds the follow-up already closed. The +/// replay of this root's citation is not refused for that. A follow-up of the root itself is +/// replayed in the giver's order and is still judged in force, and a native write meets the rule. +#[test] +fn import_replays_a_cite_of_another_roots_follow_up_that_closed_since() { + let mut mind = receiver(); + committed(admit(&mut mind, vec![other_question("Q5"), other_follow_up("FU-9", "Q5")])); + let foreign = r(K::FollowUp, &format!("{OTHER_ROOT}:follow_up:FU-9")); + committed(admit(&mut mind, vec![resolution(foreign.clone(), ResolutionOutcome::Recorded { reason: "done".into() })])); + assert!(matches!(view_of(&mind, K::FollowUp, &foreign.id.0).status, PipelineStatus::Resolved { .. })); + + let note = idea("p", "note", "an idea"); + let note_ref = r(K::Idea, &pipeline_key(¬e).unwrap()); + let own = follow_up("FU-1", r(K::Question, &id("question", "Q1"))); + let own_ref = r(K::FollowUp, &pipeline_key(&own).unwrap()); + committed(import(&mut mind, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A"), own.clone(), note.clone()], vec![])); + committed(import(&mut mind, vec![resolution(own_ref.clone(), ResolutionOutcome::Recorded { reason: "done".into() })], vec![])); + + let defer = |to: &PipelineRef| resolution(note_ref.clone(), ResolutionOutcome::Deferred { to: to.clone() }); + // The same-root referent is closed in this receiver as it was in the giver: still refused. + assert_eq!( + refusal(import(&mut mind, vec![defer(&own_ref)], vec![])), + MindRefusal::CitesResolvedDocument { kind: K::FollowUp, id: own_ref.id.0.clone() } + ); + // A native write is not a replay, and meets the rule. + assert_eq!( + refusal(admit(&mut mind, vec![defer(&foreign)])), + MindRefusal::CitesResolvedDocument { kind: K::FollowUp, id: foreign.id.0.clone() } + ); + // The replay of the foreign cite lands, and the follow-up it cites is untouched. + let (_, writes) = committed(import(&mut mind, vec![defer(&foreign)], vec![])); + assert_eq!(writes.len(), 1); + assert!(matches!(view_of(&mind, K::Idea, ¬e_ref.id.0).status, PipelineStatus::Resolved { .. })); +}