Runs: reinstatement shares the opening claim rule, liveness included
run_claims_rule is the one check for a run's claims (in force, held by no other live run), called by opening and by the withdrawal that reinstates. Tests: contested second claim on reinstatement, resolved claim refused and live claims allowed, a repeated non-first claim refused at validate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0f02914b57
commit
e1b0a5c6ff
2 changed files with 63 additions and 17 deletions
|
|
@ -1531,13 +1531,18 @@ mod tests {
|
|||
/// A run claims an item once: a repeated claim is refused at validate.
|
||||
#[test]
|
||||
fn a_run_cannot_claim_one_item_twice() {
|
||||
let claim = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F1") };
|
||||
let claim_one = || PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F1") };
|
||||
let claim = claim_one();
|
||||
let other = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F2") };
|
||||
let mut run = run_sample();
|
||||
run.claims = vec![claim.clone(), other.clone()];
|
||||
assert_eq!(PipelineDocument::Run(run.clone()).validate(), Ok(()));
|
||||
run.claims = vec![claim.clone(), other, claim];
|
||||
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.claims[2]"));
|
||||
let third = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F3") };
|
||||
let mut run = run_sample();
|
||||
run.claims = vec![claim_one(), third.clone(), third];
|
||||
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.claims[2]"));
|
||||
}
|
||||
|
||||
/// The budget has one spelling per amount.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ use chrono::{DateTime, Utc};
|
|||
use cultcache_rs::{CultCache, CultCacheEnvelope};
|
||||
use eureka_pipeline::{
|
||||
ClaimOutcome, FindingConfidence, Line, PipelineDocument, PipelineKind, PipelineRef, PipelineRefusal,
|
||||
PipelineResolution, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key,
|
||||
PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome, RulingAuthority, Short, Slug, pipeline_key,
|
||||
validate_pipeline_write_envelope,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
|
|
@ -525,21 +525,27 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> {
|
|||
// after the work it claimed has moved on.
|
||||
D::Run(run) => {
|
||||
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
|
||||
for claim in &run.claims {
|
||||
if !replay && !docs.in_force(claim.kind, &claim.id.0) {
|
||||
return Err(MindRefusal::CitesResolvedDocument { kind: claim.kind, id: claim.id.0.clone() });
|
||||
}
|
||||
let holder = docs.claim_holder(key, claim);
|
||||
if let Some(holder) = holder {
|
||||
return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: holder.into() });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
run_claims_rule(docs, key, run, replay)
|
||||
}
|
||||
D::FollowUp(_) | D::Instance(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The one rule for a run holding claims, whether it is opened 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.
|
||||
fn run_claims_rule(docs: &Docs, run_key: &str, run: &PipelineRun, replay: bool) -> Result<(), MindRefusal> {
|
||||
for claim in &run.claims {
|
||||
if !replay && !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) {
|
||||
return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: holder.into() });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Revision 1 stands alone; revision N needs a resolution in the batch that
|
||||
/// supersedes revision N-1 by this document. The predecessor's key is the
|
||||
/// leaf's, from the same document one revision back.
|
||||
|
|
@ -684,11 +690,7 @@ fn resolution_rule(docs: &Docs, resolution: &PipelineResolution) -> Result<(), M
|
|||
&& reinstating.subject.kind == K::Run
|
||||
&& let Some(D::Run(run)) = docs.find(K::Run, &reinstating.subject.id.0)
|
||||
{
|
||||
for claim in &run.claims {
|
||||
if let Some(holder) = docs.claim_holder(&reinstating.subject.id.0, claim) {
|
||||
return Err(MindRefusal::AlreadyClaimed { item: claim.id.0.clone(), run: holder.into() });
|
||||
}
|
||||
}
|
||||
run_claims_rule(docs, &reinstating.subject.id.0, run, false)?;
|
||||
}
|
||||
for (_, referent) in outcome_citations(&resolution.outcome) {
|
||||
if !docs.in_force(referent.kind, &referent.id.0) {
|
||||
|
|
@ -2064,6 +2066,45 @@ mod tests {
|
|||
committed(admit(&mut free, vec![resolution(closure, withdrawn())]));
|
||||
}
|
||||
|
||||
/// Reinstatement finds a contested claim anywhere in the run's list: a
|
||||
/// reinstated run of two claims where only the second is held is refused,
|
||||
/// and the first alone being contested is refused too.
|
||||
#[test]
|
||||
fn reinstating_a_run_finds_a_contested_second_claim() {
|
||||
let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1"));
|
||||
for (held, expected) in [("2", "2"), ("1", "1")] {
|
||||
let mut mind = with_specs();
|
||||
committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])]));
|
||||
committed(admit(&mut mind, vec![recorded("a")]));
|
||||
committed(admit(&mut mind, vec![run("c", &[spec_ref(held)])]));
|
||||
assert_eq!(
|
||||
refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])),
|
||||
MindRefusal::AlreadyClaimed { item: spec_ref(expected).id.0, run: run_key("c") }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reinstating a run needs every claim in force, as opening it does: once
|
||||
/// a claimed item has resolved the withdrawal is refused, and while all
|
||||
/// stay in force it is allowed.
|
||||
#[test]
|
||||
fn reinstating_a_run_needs_its_claims_in_force() {
|
||||
let closure = r(K::Resolution, &format!("{INSTANCE}:resolution:run.a.n1"));
|
||||
let mut mind = with_specs();
|
||||
committed(admit(&mut mind, vec![run("a", &[spec_ref("1"), spec_ref("2")])]));
|
||||
committed(admit(&mut mind, vec![recorded("a")]));
|
||||
committed(admit(&mut mind, vec![resolution(spec_ref("2"), withdrawn())]));
|
||||
assert_eq!(
|
||||
refusal(admit(&mut mind, vec![resolution(closure.clone(), withdrawn())])),
|
||||
MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: spec_ref("2").id.0 }
|
||||
);
|
||||
|
||||
let mut live = with_specs();
|
||||
committed(admit(&mut live, vec![run("a", &[spec_ref("1"), spec_ref("2")])]));
|
||||
committed(admit(&mut live, vec![recorded("a")]));
|
||||
committed(admit(&mut live, vec![resolution(closure, withdrawn())]));
|
||||
}
|
||||
|
||||
/// A holder is found by any of its claims, not only the first.
|
||||
#[test]
|
||||
fn a_holder_is_found_by_its_second_claim() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue