diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index f2baeea..4b952cf 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -438,7 +438,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { if report.branch != spec.branch { return Err(MindRefusal::SpecMismatch { field: "branch".into() }); } - if !report.commits.iter().any(|commit| commit.sha == report.range.head) { + if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); } Ok(()) @@ -579,7 +579,7 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), (K::CutSpec, O::Withdrawn { .. }) => true, (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::Finding, O::Deferred { to }) => to.kind == K::FollowUp, + (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), @@ -1910,6 +1910,49 @@ mod tests { ); } + #[test] + fn a_report_head_may_spell_its_commit_short_or_full() { + let full = "5f98228d0123456789abcdef0123456789abcdef"; + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + // The commit is spelled short and the head full, and the reverse. + let mut report = cut_report("1", 1); + report.range.head = epiphany_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + let mut report = cut_report("1", 2); + report.commits[0].sha = epiphany_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + // A head that is no commit's prefix, long or short, is outside. + let mut report = cut_report("1", 3); + report.range.head = epiphany_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + let mut report = cut_report("1", 3); + report.commits[0].sha = epiphany_pipeline::Sha(full.into()); + report.range.head = epiphany_pipeline::Sha("5f98229".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + } + + #[test] + fn a_finding_defers_to_a_cut_spec() { + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); + committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); + // A spec superseded since is no longer a place to defer to. + let mut mind = world(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + assert_eq!( + refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } + ); + } + #[test] fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { let mut mind = seeded(); diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 274d902..16a1d95 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1046,6 +1046,9 @@ mod tests { // `faculty`: attribution selects, and grants nothing (ruling 18). assert_eq!(by("faculty", &["Soul"]), vec![id("ruling", "R2")]); assert!(!by("faculty", &["Hands"]).contains(&id("ruling", "R2"))); + // The renamed faculty is selectable by its new name only. + assert!(by("faculty", &["Life"]).is_empty()); + assert!(invalid("fields[0].values", "MindSteward")(&refused(&mind, &with(Selection::default(), any_of("faculty", &["MindSteward"]))))); // A repo is named by its identity, which is case-insensitive. assert_eq!(by("repo", &["GAMECULT/EPIPHANY"]), epiphany); diff --git a/crates/huginn-mind/src/receipt.rs b/crates/huginn-mind/src/receipt.rs index f08188d..3abbf16 100644 --- a/crates/huginn-mind/src/receipt.rs +++ b/crates/huginn-mind/src/receipt.rs @@ -36,7 +36,8 @@ pub enum Faculty { Imagination, Hands, Soul, - MindSteward, + #[serde(alias = "MindSteward")] + Life, Eyes, Operator, } @@ -295,6 +296,17 @@ mod tests { use std::sync::{Arc, Mutex}; use super::*; + + /// A receipt written before the rename spells the faculty `MindSteward`; + /// it decodes as `Life`, and `Life` is what a new one writes. + #[test] + fn life_is_the_faculty_and_the_old_spelling_decodes() { + let old = rmp_serde::to_vec_named("MindSteward").unwrap(); + assert_eq!(rmp_serde::from_slice::(&old).unwrap(), Faculty::Life); + let new = rmp_serde::to_vec_named(&Faculty::Life).unwrap(); + assert_eq!(rmp_serde::from_slice::(&new).unwrap(), "Life"); + } + use crate::fixtures::{INSTANCE, admit, committed, id, instance, now, opened, prepare, provenance, question, refusal, seed, slug}; use crate::mind::schema_cache; use crate::store::test_stores::MemoryStore; diff --git a/crates/huginn-mind/src/rows.rs b/crates/huginn-mind/src/rows.rs index d25f53c..108cc9c 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -39,7 +39,7 @@ pub(crate) enum Alias { Outcome, } -const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "MindSteward", "Eyes", "Operator"]; +const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"]; const SEVERITIES: [&str; 4] = ["Blocker", "High", "Medium", "Low"]; const CONFIDENCES: [&str; 2] = ["Confirmed", "Plausible"]; const ORIGINS: [&str; 2] = ["Introduced", "PreExisting"];