diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 056c9ae..6164288 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -439,6 +439,11 @@ macro_rules! unit_enums { ($($name:ident { $($variant:ident),* $(,)? })*) => {$( #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)] pub enum $name { $($variant),* } + impl $name { + /// The variants' spellings, in declaration order: the names the wire and + /// `Debug` use. + pub const NAMES: &'static [&'static str] = &[$(stringify!($variant)),*]; + } impl Bounded for $name { fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> { Ok(()) @@ -472,7 +477,7 @@ unit_enums! { ClaimOutcome { Holds, Falsified, Unproven } FindingConfidence { Confirmed, Plausible } FindingSeverity { Blocker, High, Medium, Low } - RulingAuthority { Operator, Standing, Defaulted } + RulingAuthority { Operator, Standing, Defaulted, Mind } FindingOrigin { Introduced, PreExisting } } @@ -1269,6 +1274,39 @@ mod tests { ruling } + /// A ruling the Mind's own Self made: authority Mind, no operator quote, + /// no question answered. Built from the ruling sample, not inserted into + /// `samples()`, whose tests pick kinds by index. + fn mind_ruling() -> PipelineRuling { + let mut ruling = ruling_sample(); + (ruling.authority, ruling.operator_quote, ruling.answers, ruling.choice) = (RulingAuthority::Mind, None, None, None); + ruling + } + + #[test] + fn a_ruling_with_authority_mind_keys_encodes_and_round_trips_like_any_ruling() -> Result<()> { + let cache = schema_cache()?; + let document = PipelineDocument::Ruling(mind_ruling()); + document.validate()?; + let envelope = document.prepare(&cache)?; + assert!(envelope.key.starts_with("eureka-state:ruling:"), "{}", envelope.key); + assert_eq!(PipelineDocument::decode(&envelope)?, document); + let json = serde_json::to_value(&document)?; + assert_eq!(json["value"]["authority"], "Mind"); + assert_eq!(serde_json::from_value::(json)?, document); + let packed = rmp_serde::to_vec_named(&document)?; + assert_eq!(rmp_serde::from_slice::(&packed)?, document); + let standing = PipelineDocument::Ruling(ruling_sample()); + assert_ne!(standing.prepare(&cache)?.payload, envelope.payload, "Mind is not a spelling of another authority"); + Ok(()) + } + + #[test] + fn unit_enum_names_are_the_variants_in_declaration_order() { + assert_eq!(RulingAuthority::NAMES, ["Operator", "Standing", "Defaulted", "Mind"]); + assert_eq!(FindingOrigin::NAMES, ["Introduced", "PreExisting"]); + } + fn report_sample() -> PipelineCutReport { let PipelineDocument::CutReport(report) = samples().remove(5).0 else { unreachable!() }; report @@ -1340,6 +1378,7 @@ mod tests { f.severity = severity; out.push((format!("finding.severity-{name}"), D::Finding(f))); } + out.push(("ruling.authority-mind".into(), D::Ruling(mind_ruling()))); out } diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index fddb88d..faaab29 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -1859,10 +1859,21 @@ mod tests { let mut quoted = ruling("R1"); quoted.operator_quote = Some("all recommendations, go ahead".into()); assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + quoted.authority = RulingAuthority::Mind; + assert_eq!(refusal(admit(&mut mind, vec![D::Ruling(quoted.clone())])), MindRefusal::QuoteWithoutOperator { ruling: id("ruling", "R1") }); + assert!(mind.get(K::Ruling, &id("ruling", "R1")).unwrap().is_none(), "a refused batch leaves the store unchanged"); quoted.authority = RulingAuthority::Operator; committed(admit(&mut mind, vec![D::Ruling(quoted)])); } + #[test] + fn a_mind_ruling_without_a_quote_is_admitted() { + let mut mind = seeded(); + let mut own = ruling("R1"); + own.authority = RulingAuthority::Mind; + committed(admit(&mut mind, vec![D::Ruling(own)])); + } + #[test] fn a_revision_requires_its_predecessors_supersession_in_the_batch() { let mut mind = seeded(); diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index c71afd8..d96c0d9 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1219,6 +1219,21 @@ mod tests { } } + /// The alias domain and the enum agree: a Mind-authority ruling is + /// selectable by its own word, and only by it. + #[test] + fn a_query_for_mind_authority_is_answered() { + let mut mind = seeded(); + let mut own = ruling("R1"); + own.authority = RulingAuthority::Mind; + committed(admit(&mut mind, vec![D::Ruling(own), D::Ruling(ruling("R2"))])); + let by = |word: &str| mind.query(&with(of_kinds(&[K::Ruling]), any_of("authority", &[word]))).unwrap(); + assert_eq!(by("Mind").matched, 1); + assert_eq!(by("Standing").matched, 1); + let misspelt = with(of_kinds(&[K::Ruling]), any_of("authority", &["Minds"])); + assert!(matches!(refused(&mind, &misspelt), MindRefusal::SelectionInvalid { value: Some(v), .. } if v == "Minds")); + } + #[test] fn an_unknown_schema_or_malformed_key_is_refused() { let mind = worked(); diff --git a/crates/huginn-mind/src/rows.rs b/crates/huginn-mind/src/rows.rs index e34623f..6e3a19d 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -12,7 +12,7 @@ use std::collections::BTreeMap; use cultnet_rs::{Row, RowSet, Selection}; -use eureka_pipeline::{OrgRepo, Label, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome, Slug}; +use eureka_pipeline::{OrgRepo, Label, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome, RulingAuthority, Slug}; use crate::docs::{CitationRole, Held, kind_of_id, kind_of_type}; use crate::query::PipelineDocumentView; @@ -43,7 +43,6 @@ const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Li const SEVERITIES: [&str; 4] = ["Blocker", "High", "Medium", "Low"]; const CONFIDENCES: [&str; 2] = ["Confirmed", "Plausible"]; const ORIGINS: [&str; 2] = ["Introduced", "PreExisting"]; -const AUTHORITIES: [&str; 3] = ["Operator", "Standing", "Defaulted"]; const CLAIM_OUTCOMES: [&str; 3] = ["Holds", "Falsified", "Unproven"]; const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn"]; const BOOLEANS: [&str; 2] = ["true", "false"]; @@ -106,7 +105,7 @@ impl Alias { Self::Severity => Some(&SEVERITIES), Self::Confidence => Some(&CONFIDENCES), Self::Origin => Some(&ORIGINS), - Self::Authority => Some(&AUTHORITIES), + Self::Authority => Some(RulingAuthority::NAMES), Self::ClaimOutcome => Some(&CLAIM_OUTCOMES), Self::Outcome => Some(&OUTCOMES), Self::Root | Self::Repo | Self::Cut => None, diff --git a/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json b/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json index ce5720d..2439653 100644 --- a/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json +++ b/schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json @@ -75,7 +75,8 @@ "enum": [ "Operator", "Standing", - "Defaulted" + "Defaulted", + "Mind" ], "type": "string" }, diff --git a/schemas/cultnet/huginn.mind_request.v1.schema.json b/schemas/cultnet/huginn.mind_request.v1.schema.json index 045cde5..322186b 100644 --- a/schemas/cultnet/huginn.mind_request.v1.schema.json +++ b/schemas/cultnet/huginn.mind_request.v1.schema.json @@ -1674,7 +1674,8 @@ "enum": [ "Operator", "Standing", - "Defaulted" + "Defaulted", + "Mind" ] }, "SemanticQuery": { diff --git a/schemas/cultnet/huginn.mind_response.v1.schema.json b/schemas/cultnet/huginn.mind_response.v1.schema.json index 91eeaa3..9896044 100644 --- a/schemas/cultnet/huginn.mind_response.v1.schema.json +++ b/schemas/cultnet/huginn.mind_response.v1.schema.json @@ -3702,7 +3702,8 @@ "enum": [ "Operator", "Standing", - "Defaulted" + "Defaulted", + "Mind" ] }, "Sha": {