Leaf: a run kind, PipelineRun with RunOperator and RunTurn, instance-rooted

A run is the one record of what a Self or a Persona turn took on: key
<instance>:run:<label>, claims of CutSpec, Finding or FollowUp only (refused at
validate), a canonical-decimal budget, turn PersonaTurn or SelfRun per ruling
every-wake-a-run. The run sample is appended at the end of samples(); the golden
gains one line after sample.hand_off and no existing line changes. The
schema and its catalogue entry are derived from the type. No existing kind or
field is touched; the epoch stays v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-09 15:57:59 +02:00
commit 6c705f5f2a
5 changed files with 287 additions and 5 deletions

View file

@ -22,6 +22,7 @@ sample.resolution eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resol
sample.instance yggdrasil:instance:self epiphany.pipeline.instance.v2 EpiphanyPipelineInstanceDocument 9184a8696e7374616e6365a979676764726173696cac646973706c61795f6e616d65ae59676764726173696c206d696e64aa637265617465645f6174aa323032362d30392d3135a4686f7374a979676764726173696c
sample.stewardship yggdrasil:stewardship:gamecult_-epiphany.n1 epiphany.pipeline.stewardship.v2 EpiphanyPipelineStewardshipDocument 9185a8696e7374616e6365a979676764726173696ca47265706fb147616d6543756c742f4570697068616e79a873657175656e636501ab61737369676e65645f6f6eaa323032362d30392d3135a46e6f7465b954686520457572656b612063616d706169676e207265706f2e
sample.hand_off yggdrasil:hand_off:thought-cage.gamecult_-epiphany.2026-09-15 epiphany.pipeline.hand_off.v2 EpiphanyPipelineHandOffDocument 9186ad66726f6d5f696e7374616e6365a979676764726173696cab746f5f696e7374616e6365ac74686f756768742d63616765a47265706fb147616d6543756c742f4570697068616e79a9646f63756d656e747392ac657572656b612d7374617465b6657572656b612d73746174653a72756c696e673a5238a6726561736f6ed92854686520776f726b73746174696f6e206d696e642074616b6573207468652063616d706169676e2ea968616e6465645f6f6eaa323032362d30392d3135
sample.run yggdrasil:run:mind-2026-10-09-1 epiphany.pipeline.run.v2 EpiphanyPipelineRunDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb16d696e642d323032362d31302d30392d31ab6f706572617465645f6279a44d696e64a47475726ea753656c6652756ea4686f7374a979676764726173696caa737461727465645f6f6eaa323032362d30392d3135aa6275646765745f757364a3372e35a6636c61696d739182a46b696e64a743757453706563a26964d925657572656b612d73746174653a6375745f737065633a6375742d72756e2d6b696e642e7232a963616d706169676e7391ac657572656b612d7374617465
resolution.superseded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581aa5375706572736564656481a262799282a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a523882a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.fixed-by-none eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a26279c0a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135
resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a2627982a46b696e64a94375745265706f7274a26964d921657572656b612d73746174653a6375745f7265706f72743a6375742d33612e6831a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135

View file

@ -472,6 +472,28 @@ fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result<
Ok(())
}
/// A run claims work, never runs, rulings or campaigns: its claims are cut
/// specs, findings and follow-ups, the three kinds a Self takes on. And its
/// budget is a canonical decimal (`0`, `12`, `7.5`: no sign, no leading or
/// trailing zeros, no exponent), so one spend has one spelling.
fn run_is_well_formed(run: &PipelineRun, at: &str) -> Result<(), PipelineRefusal> {
for (index, claim) in run.claims.iter().enumerate() {
if !matches!(claim.kind, PipelineKind::CutSpec | PipelineKind::Finding | PipelineKind::FollowUp) {
return Err(format_error(&format!("{at}.claims[{index}].kind"), claim.kind.name()));
}
}
let (whole, fraction) = run.budget_usd.0.split_once('.').map_or((run.budget_usd.0.as_str(), None), |(whole, fraction)| (whole, Some(fraction)));
let digits = |text: &str| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
let canonical = digits(whole)
&& (whole == "0" || !whole.starts_with('0'))
&& fraction.is_none_or(|fraction| digits(fraction) && !fraction.ends_with('0'));
if canonical {
Ok(())
} else {
Err(format_error(&format!("{at}.budget_usd"), &run.budget_usd.0))
}
}
unit_enums! {
EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture }
ClaimOutcome { Holds, Falsified, Unproven }
@ -479,6 +501,8 @@ unit_enums! {
FindingSeverity { Blocker, High, Medium, Low }
RulingAuthority { Operator, Standing, Defaulted, Mind }
FindingOrigin { Introduced, PreExisting }
RunOperator { Operator, Mind }
RunTurn { PersonaTurn, SelfRun }
}
value_types! {
@ -595,6 +619,15 @@ value_types! {
from_instance: Slug, to_instance: Slug, repo: OrgRepo, documents: Vec<Short>[256],
reason: Para, handed_on: Date,
}
/// One launch of a Self or a Persona turn: the one record of what it took
/// on. `claims` are the work it holds, and a claim is live while its run
/// has no resolution in force. `budget_usd` is a canonical decimal string,
/// `0` for an interactive run. Instance-rooted like stewardship: the key
/// is `<instance>:run:<label>`. Whether a claim is free is admission's rule.
pub struct PipelineRun {
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
}
/// A reference to another document. The id is parsed as a full pipeline id of
@ -697,7 +730,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
/// thirteen per-kind value documents, and the envelope is the wire's
/// fourteen 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")]
@ -813,6 +846,8 @@ pipeline_kinds! {
"epiphany.pipeline.stewardship.v2", "EpiphanyPipelineStewardshipDocument";
HandOff(PipelineHandOff) => EpiphanyPipelineHandOffDocument, "hand_off",
"epiphany.pipeline.hand_off.v2", "EpiphanyPipelineHandOffDocument";
Run(PipelineRun) => EpiphanyPipelineRunDocument, "run",
"epiphany.pipeline.run.v2", "EpiphanyPipelineRunDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -952,7 +987,7 @@ fn local(field: &str, kind: PipelineKind, parts: &[&str]) -> Result<String, Pipe
}
/// Derives a document's identity key (D1, "Keys: identity, not convenience").
/// Thirteen arms, one exit: every arm names its root and composes its local
/// Fourteen arms, one exit: every arm names its root and composes its local
/// through `local`, and the key is formatted here and nowhere else.
pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefusal> {
use PipelineDocument as D;
@ -1005,6 +1040,8 @@ 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::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])?),
@ -1242,6 +1279,12 @@ mod tests {
documents: vec![s(CAMPAIGN), id("ruling", "R8")],
reason: "The workstation mind takes the campaign.".into(), handed_on: date(),
}), format!("{INSTANCE}:hand_off:thought-cage.gamecult_-epiphany.{}", date().0)),
(D::Run(PipelineRun {
instance: slug(INSTANCE), label: l("mind-2026-10-09-1"), operated_by: RunOperator::Mind, turn: RunTurn::SelfRun,
host: s("yggdrasil"), started_on: date(), budget_usd: s("7.5"),
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")),
]
}
@ -1429,6 +1472,107 @@ mod tests {
Ok(())
}
fn run_sample() -> PipelineRun {
let PipelineDocument::Run(run) = samples().remove(13).0 else { unreachable!() };
run
}
fn run_key(run: PipelineRun) -> Result<String, PipelineRefusal> {
pipeline_key(&PipelineDocument::Run(run))
}
/// Identity: a run is its instance and its label, and no other field moves
/// its key, so two launches under one label are one key.
#[test]
fn a_run_keys_off_its_instance_and_label_and_nothing_else() -> Result<()> {
let base = run_sample();
let key = run_key(base.clone())?;
assert_eq!(key, format!("{INSTANCE}:run:mind-2026-10-09-1"));
let mut varied = base.clone();
varied.operated_by = RunOperator::Operator;
varied.turn = RunTurn::PersonaTurn;
varied.host = s("starfire");
varied.started_on = Date("2026-10-10".into());
varied.budget_usd = s("0");
varied.claims = vec![];
varied.campaigns = vec![];
assert_eq!(run_key(varied)?, key, "no other field is part of the key");
let mut relabelled = base.clone();
relabelled.label = l("mind-2026-10-09-2");
assert_ne!(run_key(relabelled)?, key);
let mut moved = base;
moved.instance = slug("thought-cage");
assert_ne!(run_key(moved)?, key);
Ok(())
}
/// Claims name work: a cut spec, a finding or a follow-up, and every other
/// kind is refused at validate, whatever its id.
#[test]
fn a_run_claims_only_a_cut_spec_a_finding_or_a_follow_up() {
for kind in PipelineKind::ALL.iter().copied() {
let mut run = run_sample();
run.claims = vec![PipelineRef { kind, id: id(kind.name(), "x") }];
let outcome = PipelineDocument::Run(run).validate();
if matches!(kind, PipelineKind::CutSpec | PipelineKind::Finding | PipelineKind::FollowUp) {
assert_eq!(outcome, Ok(()), "{kind:?} is claimable");
} else {
assert!(
matches!(&outcome, Err(PipelineRefusal::InvalidFormat { field, value }) if field == "run.claims[0].kind" && value == kind.name()),
"{kind:?} is not claimable, got {outcome:?}"
);
}
}
}
/// The budget has one spelling per amount.
#[test]
fn a_runs_budget_is_a_canonical_decimal() {
let validated = |budget: &str| {
let mut run = run_sample();
run.budget_usd = s(budget);
PipelineDocument::Run(run).validate()
};
for good in ["0", "1", "12", "7.5", "0.25", "100.001"] {
assert_eq!(validated(good), Ok(()), "{good}");
}
for bad in ["", "00", "01", "1.", "1.0", "7.50", "0.0", ".5", "-1", "+1", "1e3", "1,5", " 1", "1 ", "1.2.3"] {
assert!(
matches!(validated(bad), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.budget_usd"),
"{bad:?} is refused"
);
}
}
/// Bounds: a run's campaigns and claims stop at their maximums.
#[test]
fn a_runs_lists_are_bounded() {
let mut run = run_sample();
run.campaigns = (0..9).map(|n| slug(&format!("c{n}"))).collect();
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
let mut run = run_sample();
run.claims = (0..65).map(|n| PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", &format!("F{n}")) }).collect();
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::FieldBound { limit: 64, .. })));
}
/// every-subject-resolvable (ruling key-bound): at the widest instance and
/// label a run's id fits a `Short`, and its resolution, and that
/// resolution's withdrawal, key within the 111-byte resolution local.
#[test]
fn the_widest_run_id_fits_a_short_and_resolves_two_deep() -> Result<()> {
let mut run = run_sample();
run.instance = slug(&"i".repeat(64));
run.label = l(&"a".repeat(64));
let run_id = run_key(run)?;
assert_eq!(run_id.len(), 64 + ":run:".len() + 64);
assert!(run_id.len() <= 200);
PipelineRef { kind: PipelineKind::Run, id: Short(run_id.clone()) }.validate_ref()?;
let one = resolved_at(PipelineKind::Run, &run_id, u32::MAX)?;
let two = resolved_at(PipelineKind::Resolution, &one, u32::MAX)?;
assert!(two.len() <= 200);
Ok(())
}
/// The live registrar plus the foreign stand-in, which only the tests
/// register.
fn schema_cache() -> Result<CultCache> {
@ -2859,11 +3003,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(), 13, "thirteen kinds");
assert_eq!(PipelineKind::ALL.len(), 14, "fourteen 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 thirteen kinds, by name"
"the document schema lists exactly the fourteen kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;

View file

@ -12,7 +12,7 @@ compares file to derivation byte for byte.
## The pipeline catalogue
`index.json` is the publication manifest for every schema here: the thirteen
`index.json` is the publication manifest for every schema here: the fourteen
`epiphany.pipeline.*.v2` schemas, `gamecult.persona_state.v0` and the two
`huginn.mind_*` wire schemas. The pipeline
schemas are derived from the value types in `crates/eureka-pipeline` and checked

View file

@ -0,0 +1,126 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution",
"Instance",
"Stewardship",
"HandOff",
"Run"
],
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"RunOperator": {
"enum": [
"Operator",
"Mind"
],
"type": "string"
},
"RunTurn": {
"enum": [
"PersonaTurn",
"SelfRun"
],
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.run.v2.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One launch of a Self or a Persona turn: the one record of what it took\non. `claims` are the work it holds, and a claim is live while its run\nhas no resolution in force. `budget_usd` is a canonical decimal string,\n`0` for an interactive run. Instance-rooted like stewardship: the key\nis `<instance>:run:<label>`. Whether a claim is free is admission's rule.",
"properties": {
"budget_usd": {
"$ref": "#/$defs/Short"
},
"campaigns": {
"items": {
"$ref": "#/$defs/Slug"
},
"maxItems": 8,
"type": "array"
},
"claims": {
"items": {
"$ref": "#/$defs/PipelineRef"
},
"maxItems": 64,
"type": "array"
},
"host": {
"$ref": "#/$defs/Short"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"label": {
"$ref": "#/$defs/Label"
},
"operated_by": {
"$ref": "#/$defs/RunOperator"
},
"started_on": {
"$ref": "#/$defs/Date"
},
"turn": {
"$ref": "#/$defs/RunTurn"
}
},
"required": [
"instance",
"label",
"operated_by",
"turn",
"host",
"started_on",
"budget_usd",
"claims",
"campaigns"
],
"title": "PipelineRun",
"type": "object"
}

View file

@ -155,6 +155,17 @@
"title": "Epiphany Pipeline HandOff v2",
"path": "epiphany.pipeline.hand_off.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.run.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.run.v2",
"documentType": "epiphany.pipeline.run.v2",
"title": "Epiphany Pipeline Run v2",
"path": "epiphany.pipeline.run.v2.schema.json"
},
{
"schemaId": "huginn.mind_request.v1",
"kind": "wire_message",