diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 9288d4c..93c4cbf 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -112,7 +112,7 @@ macro_rules! unbounded_scalars { )*}; } -unbounded_scalars!(u32, u64, bool); +unbounded_scalars!(u16, u32, u64, bool); fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> { if actual > limit { @@ -572,6 +572,31 @@ fn run_is_well_formed(run: &PipelineRun, at: &str) -> Result<(), PipelineRefusal } } +/// The longest and shortest beat a Mind may vote for, in seconds. +pub const TEMPO_VOTE_MIN_S: u32 = 60; +pub const TEMPO_VOTE_MAX_S: u32 = 86_400; +/// Urgency is in thousandths: 0 declares none, 1000 the most. +pub const URGENCY_MAX_PERMILLE: u16 = 1000; + +/// A pulse votes a tempo or declares an urgency, in bounds, or says nothing it +/// could be read for: an empty pulse is refused. +fn pulse_is_well_formed(pulse: &PipelinePulse, at: &str) -> Result<(), PipelineRefusal> { + if let Some(beat) = pulse.tempo_vote_beat_s + && !(TEMPO_VOTE_MIN_S..=TEMPO_VOTE_MAX_S).contains(&beat) + { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), &beat.to_string())); + } + if let Some(urgency) = pulse.urgency_permille + && urgency > URGENCY_MAX_PERMILLE + { + return Err(format_error(&format!("{at}.urgency_permille"), &urgency.to_string())); + } + if pulse.tempo_vote_beat_s.is_none() && pulse.urgency_permille.is_none() { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), "a pulse that votes and declares nothing")); + } + Ok(()) +} + unit_enums! { EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture } ClaimOutcome { Holds, Falsified, Unproven } @@ -710,6 +735,14 @@ value_types! { instance: Slug, label: Label, operated_by: RunOperator, turn: RunTurn, host: Short, started_on: Date, budget_usd: Short, claims: Vec[64], campaigns: Vec[8], } => run_is_well_formed + /// One Mind's heartbeat vote, in her own mind: the beat she wants (the swarm's + /// tempo is the lower median of every Mind's newest vote) and the urgency she + /// declares for herself until her next Persona turn. Either may be absent, not + /// both. Instance-rooted: the key is `:pulse: