admission: an import is not refused for citing a document resolved after its source ordinal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-08 21:15:16 +01:00
commit 3b64a1aaa3
2 changed files with 111 additions and 9 deletions

View file

@ -538,6 +538,9 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
use PipelineKind as K;
let key = staged.key.as_str();
let external: &[PipelineRef] = import.map_or(&[], |source| source.external.as_slice());
// An import replays a batch its source admitted at its own ordinal: a referent's status here may
// have moved on since (another campaign's import carried the resolution), so no rule judges it.
let historic = import.is_some();
if let D::Resolution(resolution) = &staged.document
&& matches!(resolution.outcome, ResolutionOutcome::Withdrawn { .. })
&& resolution.subject.kind == K::Resolution
@ -627,7 +630,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
return Err(MindRefusal::RepoNotInCampaign { repo: spec.repo.0.clone() });
}
for ruling in &spec.rulings {
if !docs.in_force(K::Ruling, &ruling.0) {
if !historic && !docs.in_force(K::Ruling, &ruling.0) {
return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() });
}
}
@ -653,7 +656,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
}
return Err(MindRefusal::CutReportWithoutSpec { report: key.into() });
};
if !docs.in_force(K::CutSpec, &report.cut_spec.0) {
if !historic && !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 {
@ -717,7 +720,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
Ok(())
}
D::Resolution(resolution) => {
resolution_rule(docs, resolution)?;
resolution_rule(docs, resolution, historic)?;
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
@ -762,7 +765,7 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
// rule.
D::Run(run) => {
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
run_rule(docs, key, run, replay)
run_rule(docs, key, run, replay, historic)
}
D::Memory(memory) => consolidation_rule(docs, key, memory),
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Research(_) | D::Idea(_) => Ok(()),
@ -772,9 +775,9 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
/// 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> {
fn run_rule(docs: &Docs<'_>, run_key: &str, run: &PipelineRun, replay: bool, historic: bool) -> Result<(), MindRefusal> {
for claim in &run.claims {
if !replay && !docs.in_force(claim.kind, &claim.id.0) {
if !replay && !historic && !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) {
@ -907,7 +910,7 @@ 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> {
fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution, historic: bool) -> Result<(), MindRefusal> {
use PipelineDocument as D;
use PipelineKind as K;
let subject_kind = resolution.subject.kind;
@ -973,11 +976,11 @@ fn resolution_rule(docs: &Docs<'_>, resolution: &PipelineResolution) -> Result<(
&& 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)?;
run_rule(docs, &reinstating.subject.id.0, run, false, historic)?;
}
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) {
if role != CitationRole::DayThrough && !historic && !docs.in_force(referent.kind, &referent.id.0) {
return Err(MindRefusal::CitesResolvedDocument { kind: referent.kind, id: referent.id.0 });
}
}

View file

@ -638,3 +638,102 @@ fn a_campaign_cannot_be_moved_out_of_its_minds_own_instance_root() {
MindRefusal::ImportOutsideCampaign { root: RECEIVER.into() }
);
}
fn fu(root: &str, label: &str) -> PipelineRef {
r(K::FollowUp, &format!("{root}:follow_up:{label}"))
}
/// The receiver after campaign A (`CAMPAIGN`) came in whole: its follow-up FU-1 is open there, and
/// FU-A is recorded.
fn receiver_holding_a_resolved_follow_up() -> Mind<MemoryStore> {
let mut mind = receiver();
let q1 = r(K::Question, &id("question", "Q1"));
committed(import(&mut mind, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A"), follow_up("FU-A", q1)], vec![]));
committed(import(&mut mind, vec![resolution(fu(CAMPAIGN, "FU-A"), ResolutionOutcome::Recorded { reason: "noted".into() })], vec![]));
mind
}
/// Campaign B's receipt, admitted by its giver while FU-A was still open: FU-B superseded by FU-A.
fn b_supersedes_a() -> Vec<D> {
vec![resolution(fu(OTHER_ROOT, "FU-B"), ResolutionOutcome::Superseded { by: vec![fu(CAMPAIGN, "FU-A")] })]
}
fn b_campaign() -> Vec<D> {
vec![other_campaign(), other_question("Q9"), other_follow_up("FU-B", "Q9")]
}
#[test]
fn an_import_may_cite_a_document_the_receiver_resolved_after_the_source_ordinal() {
let mut mind = receiver_holding_a_resolved_follow_up();
committed(import(&mut mind, b_campaign(), vec![]));
let (_, writes) = committed(import(&mut mind, b_supersedes_a(), vec![]));
assert_eq!(writes.len(), 1);
let view = view_of(&mind, K::FollowUp, &fu(OTHER_ROOT, "FU-B").id.0);
assert!(matches!(view.status, PipelineStatus::Resolved { .. }), "{:?}", view.status);
}
#[test]
fn a_native_admission_citing_a_resolved_document_is_still_refused() {
let mut mind = receiver();
let q1 = r(K::Question, &id("question", "Q1"));
committed(admit(&mut mind, vec![campaign(&[REPO]), question("Q1", &["A", "B"], "A"), follow_up("FU-A", q1.clone()), follow_up("FU-B", q1)]));
committed(admit(&mut mind, vec![resolution(fu(CAMPAIGN, "FU-A"), ResolutionOutcome::Recorded { reason: "noted".into() })]));
let superseded = resolution(fu(CAMPAIGN, "FU-B"), ResolutionOutcome::Superseded { by: vec![fu(CAMPAIGN, "FU-A")] });
assert_eq!(refusal(admit(&mut mind, vec![superseded])), MindRefusal::CitesResolvedDocument { kind: K::FollowUp, id: fu(CAMPAIGN, "FU-A").id.0 });
}
#[test]
fn a_campaign_imported_after_the_one_it_cites_resumes_and_matches_its_giver() {
let (mut giver, _) = giver();
committed(admit(&mut giver, b_campaign()));
// B's receipt cites FU-1 while it is open; A resolves it afterwards.
committed(admit(&mut giver, vec![resolution(fu(OTHER_ROOT, "FU-B"), ResolutionOutcome::Superseded { by: vec![fu(CAMPAIGN, "FU-1")] })]));
committed(admit(&mut giver, vec![resolution(fu(CAMPAIGN, "FU-1"), ResolutionOutcome::Recorded { reason: "noted".into() })]));
committed(admit(&mut giver, vec![moved_to(RECEIVER)]));
committed(admit(&mut giver, vec![resolution(r(K::Campaign, &format!("{OTHER_ROOT}:campaign:self")), ResolutionOutcome::Moved { to: slug(RECEIVER) })]));
let mut mind = receiver();
for outcome in replay_root(&giver, &mut mind, CAMPAIGN) {
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
}
let outcomes = replay_root(&giver, &mut mind, OTHER_ROOT);
assert!(outcomes.len() >= 3);
for outcome in &outcomes {
assert!(matches!(outcome, PipelineAdmissionOutcome::Committed { .. }), "{outcome:?}");
}
let key = fu(OTHER_ROOT, "FU-B").id.0;
assert_eq!(view_of(&mind, K::FollowUp, &key).status, Reader::at(&giver, head(&giver) - 2).unwrap().view(&r(K::FollowUp, &key)).unwrap().unwrap().status);
// A run stopped partway runs again from the top: what landed answers AlreadyAdmitted.
for outcome in replay_root(&giver, &mut mind, OTHER_ROOT) {
assert!(matches!(outcome, PipelineAdmissionOutcome::AlreadyAdmitted { .. }), "{outcome:?}");
}
}
/// `replay` for any root, declaring external only what the receiver does not hold, as the verb does.
fn replay_root(giver: &Mind<MemoryStore>, receiver: &mut Mind<MemoryStore>, root: &str) -> Vec<PipelineAdmissionOutcome> {
let mut selection = Selection {
fields: Some(vec![FieldPredicate { index: "root".into(), op: "any_of".into(), values: Some(vec![root.into()]), number: None }]),
projection: "document".into(),
limit: Some(cultnet_rs::LIMIT_MAX),
..Selection::default()
};
let mut views = Vec::new();
loop {
let page = giver.query(&selection).unwrap();
let PipelinePageItems::Documents(found) = page.items else { panic!("the document projection answers documents") };
views.extend(found);
match page.next {
Some(next) => selection.cursor = Some(next),
None => break,
}
}
let plan = crate::import::plan(&slug(root), giver.instance(), receiver.instance(), views).unwrap();
plan.batches
.into_iter()
.map(|mut batch| {
batch.source.external.retain(|referent| receiver.view(referent).unwrap().is_none());
import_as(receiver, batch.source, batch.documents.into_iter().map(|(_, document)| document).collect())
})
.collect()
}