From ea9c324663adade878a52f60c2dd2f8d3cf6a859 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 20:51:07 +0200 Subject: [PATCH 001/398] Launch primitive: queue, breaker, open_and_launch and the run aliases Adds the turn and operated_by aliases on the run kind, and in mind-body the Launcher port with SystemdLauncher, the work queue, the repetition breaker and open_and_launch. Not yet built: this commit may not compile. Co-Authored-By: Claude Opus 5.5 --- crates/huginn-mind/src/query.rs | 35 +++ crates/huginn-mind/src/rows.rs | 21 +- crates/mind-body/Cargo.toml | 7 +- crates/mind-body/src/launch.rs | 511 ++++++++++++++++++++++++++++++++ crates/mind-body/src/lib.rs | 9 + crates/mind-body/src/queue.rs | 344 +++++++++++++++++++++ crates/mind-body/src/testkit.rs | 368 +++++++++++++++++++++++ 7 files changed, 1291 insertions(+), 4 deletions(-) create mode 100644 crates/mind-body/src/launch.rs create mode 100644 crates/mind-body/src/queue.rs create mode 100644 crates/mind-body/src/testkit.rs diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 04cffe5..c74bedb 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1298,6 +1298,41 @@ mod tests { )), "{facts:?}"); } + /// The run aliases read the run's own fields, so a launcher can ask for the + /// live Self runs of hers; their domains are the leaf's closed sets. + #[test] + fn the_turn_and_operated_by_aliases_select_runs_by_their_fields() { + let mut mind = seeded(); + let variant = |label: &str, turn: RunTurn, operated_by: RunOperator| { + let D::Run(mut value) = run(label, &[]) else { panic!() }; + (value.turn, value.operated_by) = (turn, operated_by); + D::Run(value) + }; + committed(admit( + &mut mind, + vec![ + variant("self-mind", RunTurn::SelfRun, RunOperator::Mind), + variant("self-op", RunTurn::SelfRun, RunOperator::Operator), + variant("persona-mind", RunTurn::PersonaTurn, RunOperator::Mind), + variant("persona-op", RunTurn::PersonaTurn, RunOperator::Operator), + ], + )); + let asking = |turn: &[&str], operated_by: &[&str]| { + let selection = with(with(of_kinds(&[K::Run]), any_of("turn", turn)), any_of("operated_by", operated_by)); + ids(&mind.query(&selection).unwrap()) + }; + let run_id = |label: &str| format!("{INSTANCE}:run:{label}"); + assert_eq!(asking(&["PersonaTurn"], &["Mind"]), vec![run_id("persona-mind")]); + assert_eq!(asking(&["SelfRun"], &["Operator"]), vec![run_id("self-op")]); + assert_eq!(asking(&["SelfRun", "PersonaTurn"], &["Mind"]).len(), 2); + let outside = with(of_kinds(&[K::Run]), any_of("turn", &["Waker"])); + assert!(invalid("fields[0].values", "Waker")(&refused(&mind, &outside))); + let outside = with(of_kinds(&[K::Run]), any_of("operated_by", &["Root"])); + assert!(invalid("fields[0].values", "Root")(&refused(&mind, &outside))); + let other_kind = with(of_kinds(&[K::Ruling]), any_of("turn", &["SelfRun"])); + assert!(matches!(mind.query(&other_kind), Err(MindRefusal::SelectionInvalid { .. }))); + } + /// 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 5fe4b52..98d764f 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use cultnet_rs::{Row, RowSet, Selection}; use eureka_pipeline::{ FindingConfidence, FindingOrigin, FindingSeverity, Label, OrgRepo, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome, - RulingAuthority, Slug, + RulingAuthority, RunOperator, RunTurn, Slug, }; use crate::docs::{CitationRole, Held, kind_of_id, kind_of_type}; @@ -40,6 +40,10 @@ pub(crate) enum Alias { Authority, ClaimOutcome, Outcome, + /// A run's `turn`, read from its fields. + Turn, + /// A run's `operated_by`, read from its fields. + OperatedBy, } const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"]; @@ -47,7 +51,7 @@ const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Rec const BOOLEANS: [&str; 2] = ["true", "false"]; impl Alias { - const ALL: [Alias; 11] = [ + const ALL: [Alias; 13] = [ Self::Root, Self::InForce, Self::Faculty, @@ -59,6 +63,8 @@ impl Alias { Self::Authority, Self::ClaimOutcome, Self::Outcome, + Self::Turn, + Self::OperatedBy, ]; fn name(self) -> &'static str { @@ -74,6 +80,8 @@ impl Alias { Self::Authority => "authority", Self::ClaimOutcome => "claim_outcome", Self::Outcome => "outcome", + Self::Turn => "turn", + Self::OperatedBy => "operated_by", } } @@ -93,6 +101,7 @@ impl Alias { Self::Authority => kind == K::Ruling, Self::ClaimOutcome => kind == K::Verdict, Self::Outcome => kind == K::Resolution, + Self::Turn | Self::OperatedBy => kind == K::Run, } } @@ -107,6 +116,8 @@ impl Alias { Self::Authority => Some(RulingAuthority::NAMES), Self::ClaimOutcome => Some(eureka_pipeline::ClaimOutcome::NAMES), Self::Outcome => Some(&OUTCOMES), + Self::Turn => Some(RunTurn::NAMES), + Self::OperatedBy => Some(RunOperator::NAMES), Self::Root | Self::Repo | Self::Cut => None, } } @@ -208,6 +219,10 @@ impl SelectionRow { put(Alias::ClaimOutcome, verdict.claims.iter().map(|claim| format!("{:?}", claim.outcome)).collect()) } D::Resolution(resolution) => put(Alias::Outcome, vec![outcome_name(&resolution.outcome).to_string()]), + D::Run(run) => { + put(Alias::Turn, vec![format!("{:?}", run.turn)]); + put(Alias::OperatedBy, vec![format!("{:?}", run.operated_by)]); + } _ => {} } Self { kind: held.kind, key: held.key.clone(), ordinal: view.admission.ordinal, values, refs, view } @@ -367,7 +382,7 @@ mod tests { (K::Instance, &[]), (K::Stewardship, &["repo"]), (K::HandOff, &["repo"]), - (K::Run, &[]), + (K::Run, &["turn", "operated_by"]), ]; for (kind, extra) in table { let want = [&["root", "in_force", "faculty"][..], extra].concat(); diff --git a/crates/mind-body/Cargo.toml b/crates/mind-body/Cargo.toml index eab1e63..76ed8ca 100644 --- a/crates/mind-body/Cargo.toml +++ b/crates/mind-body/Cargo.toml @@ -9,7 +9,9 @@ publish.workspace = true # operator's brake and burn-rate dial today: a root-owned per-instance CultCache # store (`control`), the operator's CLI over it (`cli`, the `mind-control` # binary) and the one fail-closed reader every acting organ calls -# (`control::read_effective`). The waker joins this crate in cut mind-wake. +# (`control::read_effective`). It also holds the launch primitive (`launch`, +# `queue`): the one way her organs open a run and start its unit. The waker +# joins this crate in cut mind-wake. # # `rust_decimal` is the dial's number type: heat, cadence and the run cap are # money-adjacent and the tests pin exact products, which a float would blur. It @@ -18,7 +20,10 @@ publish.workspace = true anyhow = "1" chrono = { version = "0.4.44", features = ["serde"] } cultcache-rs.workspace = true +cultnet-rs.workspace = true eureka-pipeline = { path = "../eureka-pipeline" } +eureka-state = { path = "../eureka-state" } +huginn-mind = { path = "../huginn-mind" } rust_decimal = { version = "1", default-features = false, features = ["std", "serde"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/mind-body/src/launch.rs b/crates/mind-body/src/launch.rs new file mode 100644 index 0000000..55a9dbb --- /dev/null +++ b/crates/mind-body/src/launch.rs @@ -0,0 +1,511 @@ +//! The one way her organs open a run and start its unit. +//! +//! `open_and_launch` is the whole path: Busy, queue membership and the +//! repetition breaker are checked in that order, the run is admitted into the +//! mind, and only after it commits is the unit started. The run is the grant +//! (target invariant run-is-the-grant), so there is no pressure file, lease or +//! breaker state beside it, and a unit never starts without a committed run. +//! The primitive owns no file and stops no unit: stopping her units is the +//! operator's brake hold, which is another cut's. + +use anyhow::{Result, anyhow}; +use chrono::{DateTime, Utc}; +use eureka_pipeline::{ + Date, Label, PipelineDocument, PipelineKind, PipelineQuestion, PipelineRef, PipelineResolution, PipelineRun, QuestionOption, + ResolutionOutcome, RunOperator, RunTurn, Slug, +}; +use huginn_mind::{MindRefusal, PipelineAdmissionOutcome}; +use rust_decimal::Decimal; + +use crate::queue::{Breaker, MindPort, any_of, breaker, headers, in_force, of_kind, queue}; + +/// Starts the unit that carries a run, and says whether it is still running. +/// Nothing here stops a unit. +pub trait Launcher { + fn start(&self, turn: RunTurn, label: &Label) -> Result<()>; + fn alive(&self, turn: RunTurn, label: &Label) -> bool; +} + +/// The time the primitive reads: run labels, dates and receipts. +pub trait Clock { + fn now(&self) -> DateTime; +} + +pub struct SystemClock; + +impl Clock for SystemClock { + fn now(&self) -> DateTime { + Utc::now() + } +} + +/// What `systemctl` answered: whether it succeeded, and what it printed. +pub struct SystemctlReply { + pub success: bool, + pub stdout: String, +} + +/// The one door to `systemctl`, so tests never reach a real unit. +pub trait Systemctl { + fn run(&self, args: &[&str]) -> Result; +} + +struct RealSystemctl; + +impl Systemctl for RealSystemctl { + fn run(&self, args: &[&str]) -> Result { + let output = std::process::Command::new("systemctl").args(args).output().map_err(|_| anyhow!("systemctl could not be run"))?; + Ok(SystemctlReply { success: output.status.success(), stdout: String::from_utf8_lossy(&output.stdout).into_owned() }) + } +} + +/// Her units under systemd, templated by instance: +/// `mind-persona@: