eureka-pipeline: the desk_answer kind, tagged DeskOutcome and its key

One kind, key <campaign>:desk_answer:<question label>.n<sequence>, one tagged
outcome (Counted, Revision, Retraction, Refused). Published schema, index row,
golden line and the PipelineKind enum in sibling schemas regenerated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-10 15:15:48 +00:00
commit be35c72a16
15 changed files with 314 additions and 13 deletions

View file

@ -11,6 +11,7 @@
# a failing test pass: a diff here is a wire change and needs its own ruling.
# cut_spec.reads-and-symbol and finding.location-symbol were appended after 44c010c added CodeLocation.symbol and PipelineCutSpec.reads (ruling context-pack-read-anchors); no earlier line changed.
# target.repos was appended after 9298371 added PipelineTarget.repos (ruling campaign-repo-set-home); no earlier line changed.
# sample.desk_answer was inserted after the sample corpus when the desk_answer kind was added (ruling eureka-substrate:ruling:desk-answer-kind-tagged-outcome); no earlier line changed.
sample.campaign eureka-state:campaign:self epiphany.pipeline.campaign.v2 EpiphanyPipelineCampaignDocument 9185a4736c7567ac657572656b612d7374617465a57469746c65b5457572656b6120706970656c696e65207374617465a57265706f7391b147616d6543756c742f4570697068616e79ae776f726b696e675f6272616e6368bb636f6465782f657572656b612d706970656c696e652d7374617465aa7461726765745f646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864
sample.target eureka-state:target:r2 epiphany.pipeline.target.v2 EpiphanyPipelineTargetDocument 9186a863616d706169676eac657572656b612d7374617465a87265766973696f6e02aa696e76617269616e74739182a56c6162656cab6d696e642d61646d697473a973746174656d656e74b64f6e6c792061646d697373696f6e207772697465732eac6e6f745f696e5f73636f706591ac4576652062726f7773696e67b963616e6f6e6963616c5f696d706c656d656e746174696f6e7391a743756c744c6962a3646f6384a470617468d9256e6f7465732f657572656b612d706970656c696e652d73746174652d7461726765742e6d64aa73746172745f6c696e6501a8656e645f6c696e6509a6636f6d6d6974a83566393832323864
sample.question eureka-state:question:Q1 epiphany.pipeline.question.v2 EpiphanyPipelineQuestionDocument 9189a863616d706169676eac657572656b612d7374617465a56c6162656ca25131a57469746c65b357686f206f776e73207468652073746174653fa87175657374696f6eb357686f206f776e73207468652073746174653fa76f7074696f6e739282a56c6162656ca141a474657874ab616e20696e7374616e636582a56c6162656ca142a474657874a661207265706fab7265636f6d6d656e646564a141a7646570656e647391a6437574203361a97261697365645f696e82a46b696e64a743757453706563a26964bf657572656b612d73746174653a6375745f737065633a6375742d33612e7231a861736b65645f6f6eaa323032362d30392d3135
@ -31,6 +32,7 @@ sample.research eureka-state:research:p1.scope epiphany.pipeline.research.v2 Epi
sample.idea eureka-state:idea:p1.i1 epiphany.pipeline.idea.v2 EpiphanyPipelineIdeaDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca26931a57469746c65b24f6e65206e6f74652070657220636c61696da469646561d9204b65657020612070617373206173206d616e7920736d616c6c206e6f7465732ea974726164656f666673b74d6f726520646f63756d656e747320746f206c6973742ea872657374735f6f6e9182a46b696e64a85265736561726368a26964be657572656b612d73746174653a72657365617263683a70312e73636f7065a6746f776172649182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab70726f706f7365645f6f6eaa323032362d30392d3135
sample.reply yggdrasil:reply:mind-2026-10-09-1-r1 epiphany.pipeline.reply.v2 EpiphanyPipelineReplyDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb46d696e642d323032362d31302d30392d312d7231a372756e82a46b696e64a352756ea26964bf79676764726173696c3a72756e3a6d696e642d323032362d31302d30392d31a7616e73776572739182a46b696e64a8466f6c6c6f775570a26964d923657572656b612d73746174653a666f6c6c6f775f75703a61736b2d7468652d73656c66a474657874d93054686520736861646572207061737320697320646f6e653b2074686520736f756e647320617265204c6962627927732e
sample.thought yggdrasil:thought:t1793000000 epiphany.pipeline.thought.v2 EpiphanyPipelineThoughtDocument 91de0014a8696e7374616e6365a979676764726173696ca56c6162656cab7431373933303030303030a474657874d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742ea572656164739182a46b696e64a64d656d6f7279a26964d92879676764726173696c3a6d656d6f72793a323032362d31302d3331743034313230302d74302d6d30af706572736f6e615f72656365697074b06d696e642d636f6d6d69742d30613162a86964656e74697479be457572656b612c207368652f6865722c20706c61696e2d73706f6b656e2ea7636f6e74657874d92056616c7565733a20636f686572656e6365206f7665722076656c6f636974792ea87374696d756c7573bd4c696262792061736b65642061626f75742074686520736f756e64732eaf70726f6a6563746f725f6d6f64656cb070726f6a6563746f723a6c6174657374aa70726f6a656374696f6ed92949206b656570207475726e696e67206f7665722077686f20686f6c64732074686520736f756e64732eaa626173655f6d6f64656cab626173653a6c6174657374b474656d70657261747572655f7065726d696c6c65cd03e8ae6d696e5f705f7065726d696c6c6532aa6d61785f746f6b656e73cca0aa63616e6469646174657392d93d54686520736f756e647320617265204c6962627927733b2074686520736861646572732077657265206e6576657220746865206861726420706172742eaf41207365636f6e642064726966742eb06e6561726573745f7065726d696c6c6592cd0136cd0366a663686f73656e00ac70726f6a6563746f725f6d73cd1068ac636f6e74696e7565725f6d73cd238cae70726f6d70745f76657273696f6ebd67686f73746c696768743d633433656235343b20776f7264733d313830
sample.desk_answer eureka-state:desk_answer:Q1.n1 epiphany.pipeline.desk_answer.v2 EpiphanyPipelineDeskAnswerDocument 918aa863616d706169676eac657572656b612d7374617465a4666f726bb8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501ac6f7074696f6e735f68617368d94063636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a663686f696365a161a46e6f7465b853686172652069742c207769746820746865206d61726b2eab616e7377657265645f6174b4323032362d31312d31305431353a30303a30305aa8616e737765726572a6757365722d37a9617574686f72697479a744656369646572a76f7574636f6d6581a85265766973696f6e81a26f66be657572656b612d73746174653a6465736b5f616e737765723a51312e6e31
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

@ -738,6 +738,10 @@ unit_enums! {
RunOperator { Operator, Mind }
RunTurn { PersonaTurn, SelfRun, Sleep }
MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport }
// What a desk answerer is, as the receiver stamped it (never a client's claim).
DeskAuthority { Operator, Decider }
// Why a desk answer was refused and stored: binding spec 1.3 steps 4-7 and R1.
DeskRefusal { StaleFork, NotADecider, ForkClosed, ChoiceInvalid, BadRetraction, AlreadyRetracted }
}
/// The slots of a Mind's runs: one live run of hers per instance and slot. A
@ -838,6 +842,15 @@ value_types! {
structural_delta: StructuralDelta, landed_names: Vec<LandedName>[128], undone: Vec<Line>[32],
promises: Vec<Promise>[64],
}
/// One answer the fork desk's receiver passed to the Mind, refused ones included.
/// `answerer` and `authority` are stamped by the Mind from the verified caller and
/// are absent from the request type; `answered_at` is the client's claim kept as
/// written, display only: order is `sequence`, the receiver's order. `sequence` is
/// per question and is the last part of the key (the answer id).
pub struct PipelineDeskAnswer {
campaign: Slug, fork: Short, sequence: u32, options_hash: Sha256Hex, choice: Option<Label>, note: Option<Para>,
answered_at: Short, answerer: Short, authority: Option<DeskAuthority>, outcome: DeskOutcome,
}
pub struct PipelineVerdict { campaign: Slug, cut_report: Short, pass: u32, range: CommitRange, claims: Vec<VerdictClaim>[64] }
pub struct PipelineFinding {
campaign: Slug, verdict: Short, label: Label, range: CommitRange, confidence: FindingConfidence,
@ -1124,6 +1137,32 @@ impl Bounded for ResolutionOutcome {
}
}
/// The one tagged outcome of a desk answer. A retraction is a variant, so a
/// retraction without a target and a target without a retraction cannot be
/// stored. `of` and `retracts` name desk answers by answer id (the record key).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum DeskOutcome {
/// The answer that counts for its fork when it was admitted.
Counted,
/// A later passing answer, kept visible; `of` is the counted answer it followed.
Revision { of: Short },
Retraction { retracts: Short },
Refused { code: DeskRefusal },
}
impl Bounded for DeskOutcome {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Counted | Self::Refused { .. } => Ok(()),
Self::Revision { of: id } | Self::Retraction { retracts: id } => {
let field = format!("{field}.{}", if matches!(self, Self::Revision { .. }) { "of" } else { "retracts" });
id.validate(&field)?;
pipeline_id(&field, &id.0, PipelineKind::DeskAnswer).map(|_| ())
}
}
}
}
macro_rules! pipeline_kinds {
($($variant:ident($value:ident) => $document:ident, $name:literal, $type_id:tt, $schema:tt;)*) => {
$(
@ -1283,6 +1322,8 @@ pipeline_kinds! {
"epiphany.pipeline.reply.v2", "EpiphanyPipelineReplyDocument";
Thought(PipelineThought) => EpiphanyPipelineThoughtDocument, "thought",
"epiphany.pipeline.thought.v2", "EpiphanyPipelineThoughtDocument";
DeskAnswer(PipelineDeskAnswer) => EpiphanyPipelineDeskAnswerDocument, "desk_answer",
"epiphany.pipeline.desk_answer.v2", "EpiphanyPipelineDeskAnswerDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
@ -1393,6 +1434,8 @@ const _: () = assert!(64 + ":".len() + LONGEST_KIND_NAME + ":".len() + RESOLUTIO
fn local_max(kind: PipelineKind) -> usize {
match kind {
PipelineKind::Resolution => RESOLUTION_LOCAL_MAX,
// `<question label>.n<sequence>`: every question label is answerable.
PipelineKind::DeskAnswer => SUBJECT_LOCAL_MAX + SEQUENCE_PART_MAX,
_ => SUBJECT_LOCAL_MAX,
}
}
@ -1502,6 +1545,11 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
local(&key_field, document_kind, &[&format!("cut-{cut}"), &format!("h{}", value.attempt)])?,
)
}
D::DeskAnswer(value) => {
let label = parent_local("desk_answer.fork", &value.fork.0, &value.campaign.0, PipelineKind::Question)?;
let sequence = format!("n{}", value.sequence);
(campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[label, &sequence])?)
}
D::Verdict(value) => {
let report = parent_local("verdict.cut_report", &value.cut_report.0, &value.campaign.0, PipelineKind::CutReport)?;
let cut = parent_cut("verdict.cut_report", report, 'h')?;
@ -1774,6 +1822,12 @@ mod tests {
nearest_permille: vec![310, 870], chosen: 0, projector_ms: 4200, continuer_ms: 9100,
prompt_version: "ghostlight=c43eb54; words=180".into(),
}), format!("{INSTANCE}:thought:t1793000000")),
(D::DeskAnswer(PipelineDeskAnswer {
campaign: slug(CAMPAIGN), fork: id("question", "Q1"), sequence: 1, options_hash: Sha256Hex("c".repeat(64)),
choice: Some(l("a")), note: Some("Share it, with the mark.".into()), answered_at: s("2026-11-10T15:00:00Z"),
answerer: s("user-7"), authority: Some(DeskAuthority::Decider),
outcome: DeskOutcome::Revision { of: id("desk_answer", "Q1.n1") },
}), format!("{CAMPAIGN}:desk_answer:Q1.n1")),
]
}
@ -3908,11 +3962,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(), 20, "twenty kinds");
assert_eq!(PipelineKind::ALL.len(), 21, "twenty-one 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 twenty kinds, by name"
"the document schema lists exactly the twenty-one kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;
@ -4436,4 +4490,40 @@ mod tests {
);
Ok(())
}
fn desk_answer_sample() -> PipelineDeskAnswer {
let PipelineDocument::DeskAnswer(answer) = samples().pop().unwrap().0 else { unreachable!() };
answer
}
#[test]
fn a_desk_answer_is_keyed_under_its_question_and_sequence() {
let mut answer = desk_answer_sample();
let key = |answer: &PipelineDeskAnswer| pipeline_key(&PipelineDocument::DeskAnswer(answer.clone()));
assert_eq!(key(&answer), Ok(format!("{CAMPAIGN}:desk_answer:Q1.n1")));
// Every 64-byte question label is answerable at the largest sequence.
let label = "q".repeat(64);
answer.fork = id("question", &label);
answer.sequence = u32::MAX;
answer.outcome = DeskOutcome::Counted;
assert_eq!(key(&answer), Ok(format!("{CAMPAIGN}:desk_answer:{label}.n4294967295")));
// The fork must be a question of the same campaign.
answer.fork = Short(format!("other:question:{label}"));
assert!(matches!(key(&answer), Err(PipelineRefusal::InvalidFormat { .. })));
answer.fork = id("ruling", "Q1");
assert!(matches!(key(&answer), Err(PipelineRefusal::InvalidFormat { .. })));
}
#[test]
fn a_desk_outcome_names_desk_answers_by_id() {
let mut answer = desk_answer_sample();
for bad in [id("question", "Q1"), Short("n1".into())] {
answer.outcome = DeskOutcome::Retraction { retracts: bad.clone() };
assert!(matches!(PipelineDocument::DeskAnswer(answer.clone()).validate(), Err(PipelineRefusal::InvalidFormat { .. })), "{bad:?}");
answer.outcome = DeskOutcome::Revision { of: bad.clone() };
assert!(matches!(PipelineDocument::DeskAnswer(answer.clone()).validate(), Err(PipelineRefusal::InvalidFormat { .. })), "{bad:?}");
}
answer.outcome = DeskOutcome::Retraction { retracts: id("desk_answer", "Q1.n2") };
assert_eq!(PipelineDocument::DeskAnswer(answer).validate(), Ok(()));
}
}

View file

@ -0,0 +1,187 @@
{
"$defs": {
"DeskAuthority": {
"enum": [
"Operator",
"Decider"
],
"type": "string"
},
"DeskOutcome": {
"description": "The one tagged outcome of a desk answer. A retraction is a variant, so a\nretraction without a target and a target without a retraction cannot be\nstored. `of` and `retracts` name desk answers by answer id (the record key).",
"oneOf": [
{
"const": "Counted",
"description": "The answer that counts for its fork when it was admitted.",
"type": "string"
},
{
"additionalProperties": false,
"description": "A later passing answer, kept visible; `of` is the counted answer it followed.",
"properties": {
"Revision": {
"properties": {
"of": {
"$ref": "#/$defs/Short"
}
},
"required": [
"of"
],
"type": "object"
}
},
"required": [
"Revision"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Retraction": {
"properties": {
"retracts": {
"$ref": "#/$defs/Short"
}
},
"required": [
"retracts"
],
"type": "object"
}
},
"required": [
"Retraction"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Refused": {
"properties": {
"code": {
"$ref": "#/$defs/DeskRefusal"
}
},
"required": [
"code"
],
"type": "object"
}
},
"required": [
"Refused"
],
"type": "object"
}
]
},
"DeskRefusal": {
"enum": [
"StaleFork",
"NotADecider",
"ForkClosed",
"ChoiceInvalid",
"BadRetraction",
"AlreadyRetracted"
],
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"Sha256Hex": {
"description": "A SHA-256 digest: exactly 64 lowercase hex characters.",
"maxLength": 64,
"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.desk_answer.v2.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "One answer the fork desk's receiver passed to the Mind, refused ones included.\n`answerer` and `authority` are stamped by the Mind from the verified caller and\nare absent from the request type; `answered_at` is the client's claim kept as\nwritten, display only: order is `sequence`, the receiver's order. `sequence` is\nper question and is the last part of the key (the answer id).",
"properties": {
"answered_at": {
"$ref": "#/$defs/Short"
},
"answerer": {
"$ref": "#/$defs/Short"
},
"authority": {
"anyOf": [
{
"$ref": "#/$defs/DeskAuthority"
},
{
"type": "null"
}
]
},
"campaign": {
"$ref": "#/$defs/Slug"
},
"choice": {
"anyOf": [
{
"$ref": "#/$defs/Label"
},
{
"type": "null"
}
]
},
"fork": {
"$ref": "#/$defs/Short"
},
"note": {
"anyOf": [
{
"$ref": "#/$defs/Para"
},
{
"type": "null"
}
]
},
"options_hash": {
"$ref": "#/$defs/Sha256Hex"
},
"outcome": {
"$ref": "#/$defs/DeskOutcome"
},
"sequence": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"campaign",
"fork",
"sequence",
"options_hash",
"answered_at",
"answerer",
"outcome"
],
"title": "PipelineDeskAnswer",
"type": "object"
}

View file

@ -177,7 +177,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -181,7 +181,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -36,7 +36,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -153,7 +153,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -41,7 +41,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -31,7 +31,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -103,7 +103,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -36,7 +36,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -74,7 +74,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -31,7 +31,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -36,7 +36,8 @@
"Research",
"Idea",
"Reply",
"Thought"
"Thought",
"DeskAnswer"
],
"type": "string"
},

View file

@ -232,6 +232,17 @@
"title": "Epiphany Pipeline Thought v2",
"path": "epiphany.pipeline.thought.v2.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.desk_answer.v2.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.desk_answer.v2",
"documentType": "epiphany.pipeline.desk_answer.v2",
"title": "Epiphany Pipeline DeskAnswer v2",
"path": "epiphany.pipeline.desk_answer.v2.schema.json"
},
{
"schemaId": "huginn.mind_request.v1",
"kind": "wire_message",