eureka-pipeline: the Pulse kind (a Mind's heartbeat vote and declared urgency), instance-rooted; huginn-mind admits it in her own mind only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-31 05:15:15 +01:00
commit a4f4d04665
7 changed files with 65 additions and 15 deletions

View file

@ -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<PipelineRef>[64], campaigns: Vec<Slug>[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 `<instance>:pulse:<label>`, the label
/// time-ordered like a run's.
pub struct PipelinePulse {
instance: Slug, label: Label, tempo_vote_beat_s: Option<u32>, urgency_permille: Option<u16>, note: Line,
} => pulse_is_well_formed
}
/// A reference to another document. The id is parsed as a full pipeline id of
@ -812,7 +845,7 @@ macro_rules! pipeline_kinds {
/// because a value is a named map of its own and an internal tag would
/// have to be merged into it; the tag stays beside the value instead.
/// This is not a published schema: `schemas/cultnet` publishes the
/// fourteen per-kind value documents, and the envelope is the wire's
/// fifteen per-kind value documents, and the envelope is the wire's
/// shape, not a document's.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
@ -931,6 +964,8 @@ pipeline_kinds! {
"epiphany.pipeline.hand_off.v2", "EpiphanyPipelineHandOffDocument";
Run(PipelineRun) => EpiphanyPipelineRunDocument, "run",
"epiphany.pipeline.run.v2", "EpiphanyPipelineRunDocument";
Pulse(PipelinePulse) => EpiphanyPipelinePulseDocument, "pulse",
"epiphany.pipeline.pulse.v2", "EpiphanyPipelinePulseDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -1126,6 +1161,7 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
}
// A run hangs off its instance too: its label is its whole local.
D::Run(value) => ("run.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Pulse(value) => ("pulse.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Target(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&format!("r{}", value.revision)])?),
D::Question(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Ruling(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
@ -1370,6 +1406,10 @@ mod tests {
claims: vec![PipelineRef { kind: PipelineKind::CutSpec, id: id("cut_spec", "cut-run-kind.r2") }],
campaigns: vec![slug(CAMPAIGN)],
}), format!("{INSTANCE}:run:mind-2026-10-09-1")),
(D::Pulse(PipelinePulse {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: Some(600), urgency_permille: Some(250),
note: "The swarm is idle; a shorter beat.".into(),
}), format!("{INSTANCE}:pulse:2026-10-31t041200-t0")),
]
}
@ -3213,11 +3253,11 @@ mod tests {
};
let document_schema = serde_json::to_value(schemars::schema_for!(PipelineDocument))?;
let variants = document_schema["oneOf"].as_array().expect("the document schema is a oneOf");
assert_eq!(PipelineKind::ALL.len(), 14, "fourteen kinds");
assert_eq!(PipelineKind::ALL.len(), 15, "fifteen kinds");
assert_eq!(
variants.iter().map(tag).collect::<Vec<_>>(),
PipelineKind::ALL.iter().map(|kind| kind.name().to_owned()).collect::<Vec<_>>(),
"the document schema lists exactly the fourteen kinds, by name"
"the document schema lists exactly the fifteen kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;

View file

@ -311,6 +311,7 @@ fn refuse_foreign_instance(document: &PipelineDocument, mind: &Slug) -> Result<(
PipelineDocument::Instance(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Stewardship(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Run(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::Pulse(value) if value.instance != *mind => Err(foreign(&value.instance)),
PipelineDocument::HandOff(value) if value.from_instance != *mind && value.to_instance != *mind => {
Err(foreign(&value.from_instance))
}
@ -620,7 +621,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> {
let replay = docs.in_image(K::Run, key) == Some(&staged.document);
run_rule(docs, key, run, replay)
}
D::FollowUp(_) | D::Instance(_) => Ok(()),
D::FollowUp(_) | D::Instance(_) | D::Pulse(_) => Ok(()),
}
}

View file

@ -337,7 +337,8 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option<Lineage<'_>> {
| D::Resolution(_)
| D::Instance(_)
| D::HandOff(_)
| D::Run(_) => None,
| D::Run(_)
| D::Pulse(_) => None,
}
}
@ -537,7 +538,7 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel
hand_off.documents.iter().filter_map(|to| kind_of_id(&to.0).map(|kind| (R::Documents, cited(kind, to)))),
),
D::Run(run) => edges.extend(run.claims.iter().map(|to| (R::Claims, to.clone()))),
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) => {}
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) | D::Pulse(_) => {}
}
edges
}

View file

@ -86,7 +86,7 @@ pub fn index_text(document: &PipelineDocument) -> Option<String> {
}
D::Resolution(resolution) => lines.push(&resolution.rationale.0),
D::HandOff(hand_off) => lines.push(&hand_off.reason.0),
D::Instance(_) | D::Stewardship(_) | D::Run(_) => {}
D::Instance(_) | D::Stewardship(_) | D::Run(_) | D::Pulse(_) => {}
}
lines.retain(|line| !line.is_empty());
if lines.is_empty() {

View file

@ -73,7 +73,7 @@ fn require_grammatical_slug(field: &str, declared: &Slug) -> Result<(), MindRefu
}
/// A cache that knows the seventeen types a mind's store may hold: the leaf's
/// fourteen through its registrar, the epoch record, the commit receipt and
/// fifteen through its registrar, the epoch record, the commit receipt and
/// the persona entry. It is the cache every envelope is prepared against.
pub(crate) fn schema_cache() -> Result<CultCache, MindRefusal> {
let mut cache = CultCache::new();

View file

@ -141,6 +141,7 @@ pub enum PipelineFacts {
Stewardship { instance: Slug, repo: OrgRepo, sequence: u32, assigned_on: Date, note: Line },
HandOff { from_instance: Slug, to_instance: Slug, repo: OrgRepo, handed_on: Date },
Run { label: Label, operated_by: RunOperator, turn: RunTurn, started_on: Date, claims: Vec<PipelineRef> },
Pulse { label: Label, tempo_vote_beat_s: Option<u32>, urgency_permille: Option<u16> },
}
impl PipelineFacts {
@ -239,6 +240,11 @@ impl PipelineFacts {
started_on: value.started_on.clone(),
claims: value.claims.clone(),
},
D::Pulse(value) => Self::Pulse {
label: value.label.clone(),
tempo_vote_beat_s: value.tempo_vote_beat_s,
urgency_permille: value.urgency_permille,
},
}
}
}

View file

@ -193,7 +193,7 @@ impl SelectionRow {
D::FollowUp(follow_up) => vec![follow_up.repo.identity()],
D::Stewardship(stewardship) => vec![stewardship.repo.identity()],
D::HandOff(hand_off) => vec![hand_off.repo.identity()],
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) => {
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) => {
Vec::new()
}
},
@ -255,7 +255,7 @@ impl Row for SelectionRow {
}
}
/// The organ's closed lists: fourteen schemas, eleven aliases, sixteen roles.
/// The organ's closed lists: fifteen schemas, eleven 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;
@ -301,7 +301,7 @@ fn invalid(field: impl Into<String>, value: &str, message: impl Into<String>) ->
/// because `OrgRepo` identity is case-insensitive.
///
/// Refuses, naming the field the client sent: a `schemas` entry that is not
/// one of the fourteen ids; a `keys` entry that is not a full id whose kind
/// one of the fifteen ids; a `keys` entry that is not a full id whose kind
/// segment reads and passes the leaf's ref grammar; an `any_of` value outside
/// its alias's domain; and a `cites.target` whose key fails the ref grammar
/// for its schema's kind (V24's rule, at this door). Whether the target
@ -368,7 +368,7 @@ mod tests {
/// refused at the substrate's door.
#[test]
fn each_kind_declares_exactly_its_aliases_and_none_is_numeric() {
let table: [(K, &[&str]); 14] = [
let table: [(K, &[&str]); 15] = [
(K::Campaign, &["repo"]),
(K::Target, &[]),
(K::Question, &[]),
@ -383,6 +383,7 @@ mod tests {
(K::Stewardship, &["repo"]),
(K::HandOff, &["repo"]),
(K::Run, &["turn", "operated_by"]),
(K::Pulse, &[]),
];
for (kind, extra) in table {
let want = [&["root", "in_force", "faculty"][..], extra].concat();
@ -395,10 +396,10 @@ mod tests {
/// Each role is declared by the one kind whose field carries it, and its
/// targets are the kinds that field is typed to, "any" spelled as all
/// fourteen.
/// fifteen.
#[test]
fn each_kind_declares_the_roles_it_carries_and_their_targets() {
let table: [(K, &[&str]); 14] = [
let table: [(K, &[&str]); 15] = [
(K::Campaign, &[]),
(K::Target, &[]),
(K::Question, &["raised_in"]),
@ -413,6 +414,7 @@ mod tests {
(K::Stewardship, &[]),
(K::HandOff, &["documents"]),
(K::Run, &["claims"]),
(K::Pulse, &[]),
];
for (kind, roles) in table {
assert_eq!(sorted(Vocabulary.declared_roles(kind.type_id())), strings(roles), "{kind:?}");