diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 24fe994..3241b84 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1451,6 +1451,39 @@ mod tests { assert!(matches!(mind.query(&other_kind), Err(MindRefusal::SelectionInvalid { .. }))); } + /// `ran` leaves a withdrawn run out and keeps open and recorded ones; `voted` + /// and `urged` select the pulses that carry each field. + #[test] + fn the_ran_voted_and_urged_aliases_are_presence_indexes() { + let mut mind = seeded(); + committed(admit(&mut mind, vec![run("kept", &[]), run("open", &[]), run("unrun", &[])])); + let id = |label: &str| r(K::Run, &format!("{INSTANCE}:run:{label}")); + committed(admit(&mut mind, vec![resolution(id("kept"), ResolutionOutcome::Recorded { reason: "done".into() })])); + committed(admit(&mut mind, vec![resolution(id("unrun"), withdrawn())])); + let ran = ids(&mind.query(&with(of_kinds(&[K::Run]), any_of("ran", &["true"]))).unwrap()); + let run_id = |label: &str| format!("{INSTANCE}:run:{label}"); + assert_eq!(ran, vec![run_id("kept"), run_id("open")]); + let pulse = |label: &str, beat: Option, urgency: Option| { + D::Pulse(eureka_pipeline::PipelinePulse { + instance: Slug(INSTANCE.into()), + label: Label(label.into()), + tempo_vote_beat_s: beat, + urgency_permille: urgency, + note: "n".into(), + }) + }; + committed(admit(&mut mind, vec![pulse("both", Some(60), Some(1)), pulse("vote", Some(60), None), pulse("urge", None, Some(1)), pulse("none", None, None)])); + let asking = |alias: &str| { + let selection = Selection { descending: true, limit: Some(1), ..with(of_kinds(&[K::Pulse]), any_of(alias, &["true"])) }; + ids(&mind.query(&selection).unwrap()) + }; + let pulse_id = |label: &str| format!("{INSTANCE}:pulse:{label}"); + assert_eq!(asking("voted"), vec![pulse_id("vote")]); + assert_eq!(asking("urged"), vec![pulse_id("urge")]); + let outside = with(of_kinds(&[K::Pulse]), any_of("voted", &["false"])); + assert!(invalid("fields[0].values", "false")(&refused(&mind, &outside))); + } + /// The substrate's own refusals cross as themselves: an undeclared alias /// is a `SelectionInvalid` from the substrate's door, before the organ's. #[test] diff --git a/crates/huginn-mind/src/rows.rs b/crates/huginn-mind/src/rows.rs index 367f006..3f49993 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -44,14 +44,24 @@ pub(crate) enum Alias { Turn, /// A run's `operated_by`, read from its fields. OperatedBy, + /// `true` on a run that was not withdrawn: it opened and was not unmade + /// unrun, so a reader of her last real turn skips the withdrawn ones in the + /// index instead of walking past them. + Ran, + /// `true` on a pulse that casts a tempo vote. + Voted, + /// `true` on a pulse that declares an urgency. + Urged, } const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"]; const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn"]; const BOOLEANS: [&str; 2] = ["true", "false"]; +/// The one value a presence alias answers with; a row without it answers nothing. +const PRESENT: [&str; 1] = ["true"]; impl Alias { - const ALL: [Alias; 13] = [ + const ALL: [Alias; 16] = [ Self::Root, Self::InForce, Self::Faculty, @@ -65,6 +75,9 @@ impl Alias { Self::Outcome, Self::Turn, Self::OperatedBy, + Self::Ran, + Self::Voted, + Self::Urged, ]; fn name(self) -> &'static str { @@ -82,6 +95,9 @@ impl Alias { Self::Outcome => "outcome", Self::Turn => "turn", Self::OperatedBy => "operated_by", + Self::Ran => "ran", + Self::Voted => "voted", + Self::Urged => "urged", } } @@ -101,7 +117,8 @@ impl Alias { Self::Authority => kind == K::Ruling, Self::ClaimOutcome => kind == K::Verdict, Self::Outcome => kind == K::Resolution, - Self::Turn | Self::OperatedBy => kind == K::Run, + Self::Turn | Self::OperatedBy | Self::Ran => kind == K::Run, + Self::Voted | Self::Urged => kind == K::Pulse, } } @@ -118,6 +135,7 @@ impl Alias { Self::Outcome => Some(&OUTCOMES), Self::Turn => Some(RunTurn::NAMES), Self::OperatedBy => Some(RunOperator::NAMES), + Self::Ran | Self::Voted | Self::Urged => Some(&PRESENT), Self::Root | Self::Repo | Self::Cut => None, } } @@ -222,6 +240,12 @@ impl SelectionRow { D::Run(run) => { put(Alias::Turn, vec![format!("{:?}", run.turn)]); put(Alias::OperatedBy, vec![format!("{:?}", run.operated_by)]); + let withdrawn = matches!(&view.status, crate::query::PipelineStatus::Resolved { record, .. } if matches!(record.outcome, ResolutionOutcome::Withdrawn { .. })); + put(Alias::Ran, if withdrawn { Vec::new() } else { vec!["true".into()] }); + } + D::Pulse(pulse) => { + put(Alias::Voted, if pulse.tempo_vote_beat_s.is_some() { vec!["true".into()] } else { Vec::new() }); + put(Alias::Urged, if pulse.urgency_permille.is_some() { vec!["true".into()] } else { Vec::new() }); } _ => {} } @@ -255,7 +279,7 @@ impl Row for SelectionRow { } } -/// The organ's closed lists: fifteen schemas, eleven aliases, sixteen roles. +/// The organ's closed lists: fifteen schemas, sixteen aliases, sixteen roles. /// A schema's name is its id, so the substrate's alias matching resolves /// nothing beyond exact ids. pub(crate) struct Vocabulary; @@ -382,8 +406,8 @@ mod tests { (K::Instance, &[]), (K::Stewardship, &["repo"]), (K::HandOff, &["repo"]), - (K::Run, &["turn", "operated_by"]), - (K::Pulse, &[]), + (K::Run, &["turn", "operated_by", "ran"]), + (K::Pulse, &["voted", "urged"]), ]; for (kind, extra) in table { let want = [&["root", "in_force", "faculty"][..], extra].concat();