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 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-08 19:32:01 +00:00
commit 7218c4a9d7
2 changed files with 46 additions and 3 deletions

View file

@ -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 });
}
}

View file

@ -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(&note).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, &note_ref.id.0).status, PipelineStatus::Resolved { .. }));
}