Desk answers, Huginn half 1: desk_answer kind and the Mind counting door #29
33 changed files with 2925 additions and 51 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -961,6 +961,7 @@ dependencies = [
|
|||
"serde_path_to_error",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1988,6 +1989,12 @@ dependencies = [
|
|||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
|
|
@ -2073,6 +2080,15 @@ version = "1.0.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
|
|
|
|||
|
|
@ -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 918ea863616d706169676eac657572656b612d7374617465a4666f726bb8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501ac6f7074696f6e735f68617368d94063636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363a663686f696365a161a46e6f7465b853686172652069742c207769746820746865206d61726b2eab616e7377657265645f6174b4323032362d31312d31305431353a30303a30305aab72656365697665645f6174b4323032362d31312d31305431353a30303a30335aaf636c61696d5f696e5f667574757265c2aa726571756573745f6964d92436663163306135322d336237652d346431612d396335382d306532643761346239663133ab626f64795f736861323536d94064646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464646464a8616e737765726572a6757365722d37a9617574686f72697479a64d656d626572a76f7574636f6d6581a85265766973696f6e81a26f66be657572656b612d73746174653a6465736b5f616e737765723a51312e6e31
|
||||
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
|
||||
|
|
|
|||
|
|
@ -272,6 +272,27 @@ fn symbol_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
|||
within(128)(field, value)
|
||||
}
|
||||
|
||||
/// A lowercase D-format UUID: 8-4-4-4-12 lowercase hex digits, any version.
|
||||
fn uuid_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
let shaped = value.len() == 36
|
||||
&& value.bytes().enumerate().all(|(at, byte)| match at {
|
||||
8 | 13 | 18 | 23 => byte == b'-',
|
||||
_ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'),
|
||||
});
|
||||
if !shaped {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A person's words: 1 to 4000 UTF-8 bytes and not blank (`str::trim` leaves something).
|
||||
fn words_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.trim().is_empty() {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
within(4000)(field, value)
|
||||
}
|
||||
|
||||
bounded_text! {
|
||||
/// Text of at most 200 UTF-8 bytes.
|
||||
Short = 200, |field, value| within(200)(field, value);
|
||||
|
|
@ -290,6 +311,11 @@ bounded_text! {
|
|||
Symbol = 128, symbol_text;
|
||||
/// Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.
|
||||
Para = 4000, |field, value| within(4000)(field, value);
|
||||
/// A person's words, verbatim: 1 to 4,000 UTF-8 bytes, not blank (CultLib PR #72, `answer.note`).
|
||||
#[schemars(extend("minLength" = 1))]
|
||||
Words = 4000, words_text;
|
||||
/// A client's retry token: a lowercase D-format UUID.
|
||||
RequestId = 36, uuid_text;
|
||||
/// A key label: `[A-Za-z0-9_-]{1,64}`.
|
||||
Label = 64, label_text;
|
||||
/// A campaign slug or key segment: label parts joined by `.`.
|
||||
|
|
@ -738,6 +764,11 @@ unit_enums! {
|
|||
RunOperator { Operator, Mind }
|
||||
RunTurn { PersonaTurn, SelfRun, Sleep }
|
||||
MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport }
|
||||
// The role the verified account holds, an attribution copied from Bifrost's state (CultLib PR #72,
|
||||
// fork-card.md). It confers nothing: whether an answerer may decide is the decider list's, never this.
|
||||
DeskAuthority { Operator, Steward, Member }
|
||||
// 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 +869,25 @@ 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` (an attribution, never a decision) 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. `received_at` is the Mind's admission time (UTC) and
|
||||
/// `claim_in_future` is set when `answered_at` is more than five minutes after it.
|
||||
/// `request_id` is the client's retry token and `body_sha256` the digest of the
|
||||
/// request it came with: the pair that makes a retry the same answer, never a new
|
||||
/// one; the key is `(answerer, request_id)`. A record holds no card: a replayed
|
||||
/// `stale_fork` is shown the current card. `sequence` is per question and is the
|
||||
/// last part of the key (the answer id). `choice`, `note` and (for a retraction) the
|
||||
/// target are kept as sent whether the request was counted or refused, so a refused
|
||||
/// `choice_invalid` keeps a choice that is no option label; which answer counts now is
|
||||
/// derived by the reader, never stored here.
|
||||
pub struct PipelineDeskAnswer {
|
||||
campaign: Slug, fork: Short, sequence: u32, options_hash: Sha256Hex, choice: Option<Short>, note: Option<Words>,
|
||||
answered_at: Short, received_at: Short, claim_in_future: bool, request_id: RequestId, body_sha256: Sha256Hex,
|
||||
answerer: Short, authority: 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 +1174,48 @@ 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 },
|
||||
/// A request the desk would not count, kept with what it was sent with. `retracts` is the
|
||||
/// target a refused retraction named, as sent and unchecked (it may name no answer at all);
|
||||
/// it is `None` on a refused answer, and it is never a citation.
|
||||
Refused { code: DeskRefusal, retracts: Option<Short> },
|
||||
}
|
||||
|
||||
impl DeskOutcome {
|
||||
/// The variant's spelling, for headers and listings.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Counted => "Counted",
|
||||
Self::Revision { .. } => "Revision",
|
||||
Self::Retraction { .. } => "Retraction",
|
||||
Self::Refused { .. } => "Refused",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bounded for DeskOutcome {
|
||||
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
|
||||
match self {
|
||||
Self::Counted => Ok(()),
|
||||
Self::Refused { retracts, .. } => retracts.as_ref().map_or(Ok(()), |target| target.validate(&format!("{field}.retracts"))),
|
||||
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 +1375,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 +1487,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 +1598,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 +1875,13 @@ 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(s("a")), note: Some("Share it, with the mark.".into()), answered_at: s("2026-11-10T15:00:00Z"),
|
||||
received_at: s("2026-11-10T15:00:03Z"), claim_in_future: false, request_id: RequestId("6f1c0a52-3b7e-4d1a-9c58-0e2d7a4b9f13".into()), body_sha256: Sha256Hex("d".repeat(64)),
|
||||
answerer: s("user-7"), authority: DeskAuthority::Member,
|
||||
outcome: DeskOutcome::Revision { of: id("desk_answer", "Q1.n1") },
|
||||
}), format!("{CAMPAIGN}:desk_answer:Q1.n1")),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -3908,11 +4016,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 +4544,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(()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ pub fn check(
|
|||
}
|
||||
text.push_str(&format!("For {id}:\n"));
|
||||
for (_, view) in &found {
|
||||
text.push_str(&render::header(&PipelineDocumentSummary::of(view)));
|
||||
text.push_str(&render::header(&PipelineDocumentSummary::of(view, &|_| None)));
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ macro_rules! kinds {
|
|||
|
||||
kinds!(
|
||||
campaign, target, question, ruling, cut_spec, cut_report, verdict, finding, follow_up, resolution, instance, stewardship,
|
||||
hand_off, run, pulse, memory, research, idea, reply, thought,
|
||||
hand_off, run, pulse, memory, research, idea, reply, thought, desk_answer,
|
||||
);
|
||||
|
||||
/// The schema named, or the names when none is: the exit code is 2 for a name
|
||||
|
|
|
|||
|
|
@ -1144,12 +1144,12 @@ fn schema_prints_the_published_files() {
|
|||
let names = run(&[], &["schema"], None);
|
||||
assert_eq!(names.code, 0);
|
||||
let names: Vec<&str> = names.out.lines().collect();
|
||||
assert_eq!(names.len(), 21, "{names:?}");
|
||||
assert_eq!(names.len(), 22, "{names:?}");
|
||||
assert_eq!(names[0], "campaign");
|
||||
assert_eq!(names[14], "pulse");
|
||||
assert_eq!(names[15], "memory");
|
||||
assert_eq!((names[16], names[17]), ("research", "idea"));
|
||||
assert_eq!((names[18], names[19], names[20]), ("reply", "thought", "selection"));
|
||||
assert_eq!((names[18], names[19], names[20], names[21]), ("reply", "thought", "desk_answer", "selection"));
|
||||
for kind in PipelineKind::ALL {
|
||||
assert!(names.contains(&kind.name()), "{}", kind.name());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
serde_path_to_error = "0.1"
|
||||
sha2 = "0.10"
|
||||
unicode-normalization = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,26 @@ use crate::receipt::{self, CommitOutcome, IMPORT_EXTERNAL_MAX, ImportSource, Pip
|
|||
use crate::refusal::{MindRefusal, RunId};
|
||||
use crate::store::MindStore;
|
||||
|
||||
/// The door a batch enters by. A desk answer is decided by `Mind::answer_desk`
|
||||
/// before its document exists, from a stamp the caller cannot forge, so only the
|
||||
/// desk's own call passes `Desk`; `admit`, `admit_prepared`, imports and
|
||||
/// derivations pass `Admit` and may not write one (ruling
|
||||
/// desk-second-peer-checked-door).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Door {
|
||||
Admit,
|
||||
Desk,
|
||||
}
|
||||
|
||||
/// Every staged `desk_answer`, whether the caller sent it or a derivation
|
||||
/// produced it, is refused unless it came by the desk door.
|
||||
fn refuse_desk_answers_outside_desk(door: Door, docs: &Docs<'_>) -> Result<(), MindRefusal> {
|
||||
match docs.batch.iter().find(|staged| staged.kind == PipelineKind::DeskAnswer) {
|
||||
Some(staged) if door != Door::Desk => Err(MindRefusal::DeskAnswerOutsideDesk { id: staged.key.clone() }),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The most envelopes one batch may carry.
|
||||
pub const BATCH_MAX: usize = 64;
|
||||
|
||||
|
|
@ -77,7 +97,7 @@ pub enum PipelineAdmissionOutcome {
|
|||
/// Keys a document through the leaf, then encodes it. A key the leaf cannot
|
||||
/// compose is the document's fault and is refused as a document; only the
|
||||
/// cache's encode fault is the organ's.
|
||||
fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result<CultCacheEnvelope, MindRefusal> {
|
||||
pub(crate) fn prepare(document: &PipelineDocument, cache: &CultCache) -> Result<CultCacheEnvelope, MindRefusal> {
|
||||
pipeline_key(document).map_err(MindRefusal::Document)?;
|
||||
document.prepare(cache).map_err(unavailable)
|
||||
}
|
||||
|
|
@ -108,14 +128,15 @@ impl<S: MindStore> Mind<S> {
|
|||
envelopes: Vec<CultCacheEnvelope>,
|
||||
now: DateTime<Utc>,
|
||||
) -> PipelineAdmissionOutcome {
|
||||
match self.admit_steps(instance, provenance, envelopes, now) {
|
||||
match self.admit_steps(Door::Admit, instance, provenance, envelopes, now) {
|
||||
Ok(outcome) => outcome,
|
||||
Err(refusal) => PipelineAdmissionOutcome::Refused(refusal),
|
||||
}
|
||||
}
|
||||
|
||||
fn admit_steps(
|
||||
pub(crate) fn admit_steps(
|
||||
&mut self,
|
||||
door: Door,
|
||||
instance: &Slug,
|
||||
provenance: PipelineProvenance,
|
||||
envelopes: Vec<CultCacheEnvelope>,
|
||||
|
|
@ -139,6 +160,7 @@ impl<S: MindStore> Mind<S> {
|
|||
for envelope in envelopes {
|
||||
docs.push(stage(envelope, &mind)?)?;
|
||||
}
|
||||
refuse_desk_answers_outside_desk(door, &docs)?;
|
||||
// A6
|
||||
let carries_instance = docs.batch.iter().filter(|staged| staged.kind == PipelineKind::Instance).count() == 1;
|
||||
if self.is_empty() && !carries_instance {
|
||||
|
|
@ -162,6 +184,7 @@ impl<S: MindStore> Mind<S> {
|
|||
docs.push(stage(envelope, &mind)?)?;
|
||||
}
|
||||
}
|
||||
refuse_desk_answers_outside_desk(door, &docs)?;
|
||||
for staged in &docs.batch[originals..] {
|
||||
resolve(&docs, staged, &mind, external, &mut strong)?;
|
||||
}
|
||||
|
|
@ -602,6 +625,8 @@ fn check(docs: &Docs<'_>, staged: &Staged, mind: &Slug, import: Option<&ImportSo
|
|||
}
|
||||
unique_labels("question.options", question.options.iter().map(|option| option.label.0.as_str()))
|
||||
}
|
||||
// Decided in `answer_desk` before the document exists; no other door can stage one.
|
||||
D::DeskAnswer(_) => Ok(()),
|
||||
D::Ruling(ruling) => {
|
||||
if ruling.operator_quote.is_some() && ruling.authority != RulingAuthority::Operator {
|
||||
return Err(MindRefusal::QuoteWithoutOperator { ruling: key.into() });
|
||||
|
|
|
|||
1506
crates/huginn-mind/src/desk.rs
Normal file
1506
crates/huginn-mind/src/desk.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
use cultcache_rs::CultCacheEnvelope;
|
||||
use eureka_pipeline::{
|
||||
Label, OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome,
|
||||
DeskOutcome, Label, OrgRepo, PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, PipelineRun, PipelineStewardship, ResolutionOutcome,
|
||||
ResearchSource, RunOperator, Short, Slug,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
|
|
@ -382,7 +382,8 @@ pub(crate) fn line_of(document: &PipelineDocument) -> Option<Lineage<'_>> {
|
|||
| D::Run(_)
|
||||
| D::Pulse(_)
|
||||
| D::Reply(_)
|
||||
| D::Thought(_) => None,
|
||||
| D::Thought(_)
|
||||
| D::DeskAnswer(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -434,10 +435,12 @@ pub enum CitationRole {
|
|||
Run,
|
||||
Reads,
|
||||
DayThrough,
|
||||
Fork,
|
||||
Answer,
|
||||
}
|
||||
|
||||
impl CitationRole {
|
||||
pub const ALL: [CitationRole; 24] = [
|
||||
pub const ALL: [CitationRole; 26] = [
|
||||
Self::RaisedIn,
|
||||
Self::Answers,
|
||||
Self::Rulings,
|
||||
|
|
@ -462,6 +465,8 @@ impl CitationRole {
|
|||
Self::Run,
|
||||
Self::Reads,
|
||||
Self::DayThrough,
|
||||
Self::Fork,
|
||||
Self::Answer,
|
||||
];
|
||||
|
||||
/// The wire spelling, equal to the referring field's name.
|
||||
|
|
@ -491,6 +496,8 @@ impl CitationRole {
|
|||
Self::Run => "run",
|
||||
Self::Reads => "reads",
|
||||
Self::DayThrough => "day_through",
|
||||
Self::Fork => "fork",
|
||||
Self::Answer => "answer",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -517,6 +524,7 @@ impl CitationRole {
|
|||
Self::RestsOn | Self::Toward => K::Idea,
|
||||
Self::Run => K::Reply,
|
||||
Self::Reads => K::Thought,
|
||||
Self::Fork | Self::Answer => K::DeskAnswer,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -526,7 +534,7 @@ impl CitationRole {
|
|||
pub fn target_kinds(self) -> Vec<PipelineKind> {
|
||||
use PipelineKind as K;
|
||||
match self {
|
||||
Self::Answers | Self::Questions | Self::Forks => vec![K::Question],
|
||||
Self::Answers | Self::Questions | Self::Forks | Self::Fork => vec![K::Question],
|
||||
Self::Rulings => vec![K::Ruling],
|
||||
Self::CutSpec => vec![K::CutSpec],
|
||||
Self::CutReport => vec![K::CutReport],
|
||||
|
|
@ -535,6 +543,7 @@ impl CitationRole {
|
|||
Self::Claims => vec![K::CutSpec, K::Finding, K::FollowUp],
|
||||
Self::Consolidates | Self::DayThrough => vec![K::Memory],
|
||||
Self::Run => vec![K::Run],
|
||||
Self::Answer => vec![K::DeskAnswer],
|
||||
Self::RaisedIn
|
||||
| Self::Source
|
||||
| Self::Subject
|
||||
|
|
@ -636,6 +645,13 @@ pub(crate) fn citations(document: &PipelineDocument) -> Vec<(CitationRole, Pipel
|
|||
edges.extend(idea.rests_on.iter().map(|to| (R::RestsOn, to.clone())));
|
||||
edges.extend(idea.toward.iter().map(|to| (R::Toward, to.clone())));
|
||||
}
|
||||
D::DeskAnswer(answer) => {
|
||||
edges.push((R::Fork, cited(K::Question, &answer.fork)));
|
||||
match &answer.outcome {
|
||||
DeskOutcome::Revision { of: id } | DeskOutcome::Retraction { retracts: id } => edges.push((R::Answer, cited(K::DeskAnswer, id))),
|
||||
DeskOutcome::Counted | DeskOutcome::Refused { .. } => {}
|
||||
}
|
||||
}
|
||||
D::Campaign(_) | D::Target(_) | D::Instance(_) | D::Stewardship(_) | D::Pulse(_) => {}
|
||||
}
|
||||
edges
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ pub fn index_text(document: &PipelineDocument) -> Option<String> {
|
|||
lines.push(&idea.idea.0);
|
||||
lines.push(&idea.tradeoffs.0);
|
||||
}
|
||||
D::Instance(_) | D::Stewardship(_) | D::Run(_) | D::Pulse(_) | D::Reply(_) => {}
|
||||
// A desk answer is a person's words, not indexed.
|
||||
D::Instance(_) | D::Stewardship(_) | D::Run(_) | D::Pulse(_) | D::Reply(_) | D::DeskAnswer(_) => {}
|
||||
}
|
||||
lines.retain(|line| !line.is_empty());
|
||||
if lines.is_empty() {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
//! `now` is passed in.
|
||||
|
||||
pub mod admission;
|
||||
mod desk;
|
||||
mod docs;
|
||||
mod image;
|
||||
pub mod envelope;
|
||||
|
|
@ -52,6 +53,7 @@ mod read_tests;
|
|||
#[cfg(test)]
|
||||
mod move_tests;
|
||||
|
||||
pub use desk::{DeskAnswerOutcome, DeskAnswerRequest, DeskBody, options_hash};
|
||||
pub use docs::CitationRole;
|
||||
pub use index::{INDEX_TEXT_MAX_BYTES, INDEX_TEXT_VERSION, IndexEntry, index_text};
|
||||
pub use admission::{BATCH_MAX, PipelineAdmissionBatch, PipelineAdmissionOutcome};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use eureka_pipeline::{
|
|||
Line, Para, Short, Slug, StructuralDelta, pipeline_key,
|
||||
};
|
||||
|
||||
use crate::docs::{citations, kind_of_type};
|
||||
use crate::docs::{Docs, citations, kind_of_type};
|
||||
use crate::mind::{Mind, schema_cache};
|
||||
use crate::persona::HuginnPersonaEntry;
|
||||
use crate::query::{AdmissionFacts, PipelineDocumentView, PipelineSelectionPage, PipelineStatus};
|
||||
|
|
@ -168,7 +168,7 @@ pub fn oracle_page<S: MindStore>(mind: &Mind<S>, selection: &Selection, as_of: u
|
|||
let rows: Vec<SelectionRow> = visible.iter().map(|doc| row_of(&visible, doc)).collect();
|
||||
let rows: Vec<&SelectionRow> = rows.iter().collect();
|
||||
let evaluation = select(&Vocabulary, &rows, &selection, as_of, mind.cursor_key())?;
|
||||
Mind::<S>::page(&selection, evaluation, as_of, None)
|
||||
Mind::<S>::page(&selection, evaluation, &Docs::at(mind.image()?, as_of), as_of, None)
|
||||
}
|
||||
|
||||
/// The view of one document at `as_of`, derived from the store alone.
|
||||
|
|
|
|||
|
|
@ -31,13 +31,14 @@ use cultnet_rs::{
|
|||
Cursor, EdgeAnchor, EdgeMatch, Evaluation, LIMIT_MAX, LIMIT_MIN, PROJECTION_DOCUMENT, Selection, select, validate,
|
||||
};
|
||||
use eureka_pipeline::{
|
||||
ClaimOutcome, CommitRange, Date, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo,
|
||||
ClaimOutcome, CommitRange, Date, DeskAuthority, FindingConfidence, FindingOrigin, FindingSeverity, Label, Line, MemoryOrigin, MemorySort, OrgRepo,
|
||||
PipelineCampaign, PipelineDocument, PipelineKind, PipelineRef, PipelineResolution, ResolutionOutcome, RulingAuthority, RunOperator, RunTurn, Sha,
|
||||
Short, Slug, Title, pipeline_key,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::desk::counted_answer;
|
||||
use crate::docs::{CitationRole, Docs, kind_of_type};
|
||||
use crate::image::{Image, status_of};
|
||||
use crate::mind::{Mind, schema_cache};
|
||||
|
|
@ -161,10 +162,15 @@ pub enum PipelineFacts {
|
|||
Memory { label: Label, sort: MemorySort, about: Vec<Short>, origin: MemoryOrigin, salience_permille: u16 },
|
||||
Research { pass: Label, label: Label, claim: Line, inferred: bool, observed_on: Date },
|
||||
Idea { pass: Label, label: Label, title: Title, proposed_on: Date },
|
||||
/// `outcome` is the stored, write-time outcome's variant name. `counts` is whether this answer is
|
||||
/// the one that counts for its fork at the read's snapshot, derived by `counted_answer` and never
|
||||
/// stored: a retracted `Counted` answer reads `counts: false`, the promoted next answer `true`.
|
||||
DeskAnswer { fork: Short, sequence: u32, outcome: Short, counts: bool, authority: DeskAuthority },
|
||||
}
|
||||
|
||||
impl PipelineFacts {
|
||||
fn of(document: &PipelineDocument) -> Self {
|
||||
/// `counted_of` answers, for a fork, the key of the answer that counts at the read's snapshot.
|
||||
fn of(id: &PipelineRef, document: &PipelineDocument, counted_of: &dyn Fn(&str) -> Option<String>) -> Self {
|
||||
use PipelineDocument as D;
|
||||
match document {
|
||||
D::Campaign(value) => Self::Campaign { title: value.title.clone(), repos: value.repos.clone() },
|
||||
|
|
@ -293,13 +299,22 @@ impl PipelineFacts {
|
|||
title: value.title.clone(),
|
||||
proposed_on: value.proposed_on.clone(),
|
||||
},
|
||||
D::DeskAnswer(value) => Self::DeskAnswer {
|
||||
fork: value.fork.clone(),
|
||||
sequence: value.sequence,
|
||||
outcome: Short(value.outcome.name().into()),
|
||||
counts: counted_of(&value.fork.0).as_deref() == Some(id.id.0.as_str()),
|
||||
authority: value.authority,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PipelineDocumentSummary {
|
||||
/// The header of a view: what a `headers` projection answers for it.
|
||||
pub fn of(view: &PipelineDocumentView) -> Self {
|
||||
/// `counted_of` names, for a desk fork, the answer that counts at the snapshot the view was read
|
||||
/// at; a caller that summarises no desk answers passes `&|_| None`.
|
||||
pub fn of(view: &PipelineDocumentView, counted_of: &dyn Fn(&str) -> Option<String>) -> Self {
|
||||
Self {
|
||||
id: view.id.clone(),
|
||||
admission: view.admission.clone(),
|
||||
|
|
@ -309,7 +324,7 @@ impl PipelineDocumentSummary {
|
|||
PipelineStatusSummary::Resolved { resolution: resolution.clone(), outcome: record.outcome.clone() }
|
||||
}
|
||||
},
|
||||
facts: PipelineFacts::of(&view.document),
|
||||
facts: PipelineFacts::of(&view.id, &view.document, counted_of),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -639,7 +654,7 @@ impl<S: MindStore> Mind<S> {
|
|||
let ids = reader.universe(selection);
|
||||
let rows = reader.rows(&ids);
|
||||
let evaluation = select(&Vocabulary, &rows, selection, as_of, self.cursor_key())?;
|
||||
Self::page(selection, evaluation, as_of, None)
|
||||
Self::page(selection, evaluation, &reader.docs, as_of, None)
|
||||
}
|
||||
|
||||
/// Every root this mind holds a document under, with its newest admission and, for a root that is an in-force
|
||||
|
|
@ -807,7 +822,7 @@ impl<S: MindStore> Mind<S> {
|
|||
kept.sort_by_key(|(at, _)| *at);
|
||||
evaluation.edges = kept.into_iter().map(|(_, edge)| edge).collect();
|
||||
evaluation.matched = u32::try_from(members.ids.len()).unwrap_or(u32::MAX);
|
||||
Ok(Ranked { page: Self::page(&selection, evaluation, as_of, Some(row_scores))?, dropped })
|
||||
Ok(Ranked { page: Self::page(&selection, evaluation, &reader.docs, as_of, Some(row_scores))?, dropped })
|
||||
}
|
||||
|
||||
/// The projection of an evaluation: a header or a whole view per row, and
|
||||
|
|
@ -815,13 +830,14 @@ impl<S: MindStore> Mind<S> {
|
|||
pub(crate) fn page(
|
||||
selection: &Selection,
|
||||
evaluation: Evaluation<&SelectionRow>,
|
||||
docs: &Docs<'_>,
|
||||
as_of: u64,
|
||||
scores: Option<Vec<f32>>,
|
||||
) -> Result<PipelineSelectionPage, MindRefusal> {
|
||||
let items = if selection.projection == PROJECTION_DOCUMENT {
|
||||
PipelinePageItems::Documents(evaluation.rows.iter().map(|row| row.view.clone()).collect())
|
||||
} else {
|
||||
PipelinePageItems::Headers(evaluation.rows.iter().map(|row| PipelineDocumentSummary::of(&row.view)).collect())
|
||||
PipelinePageItems::Headers(evaluation.rows.iter().map(|row| PipelineDocumentSummary::of(&row.view, &|fork| counted_answer(docs, fork))).collect())
|
||||
};
|
||||
let edges = if selection.has_hop() {
|
||||
let mut typed = Vec::with_capacity(evaluation.edges.len());
|
||||
|
|
@ -1726,7 +1742,7 @@ mod tests {
|
|||
let page = mind.query(&one).unwrap();
|
||||
let expected = view(&mind, K::Question, &q1);
|
||||
assert_eq!(page.edges, None, "no hop, no edges");
|
||||
assert_eq!(headers(&page), [PipelineDocumentSummary::of(&expected)]);
|
||||
assert_eq!(headers(&page), [PipelineDocumentSummary::of(&expected, &|_| None)]);
|
||||
assert_eq!(headers(&page)[0].facts, PipelineFacts::Question {
|
||||
label: l("Q1"),
|
||||
title: "Who owns the state?".into(),
|
||||
|
|
@ -1748,7 +1764,7 @@ mod tests {
|
|||
thought.nearest_permille = vec![300, 450];
|
||||
thought.chosen = 1;
|
||||
thought.text = "B.".into();
|
||||
assert_eq!(PipelineFacts::of(&D::Thought(thought)), PipelineFacts::Thought { label: "t1".into(), chosen: 1, nearest_permille: 450 });
|
||||
assert_eq!(PipelineFacts::of(&PipelineRef { kind: K::Thought, id: id("thought", "t1").as_str().into() }, &D::Thought(thought), &|_| None), PipelineFacts::Thought { label: "t1".into(), chosen: 1, nearest_permille: 450 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1812,6 +1828,21 @@ mod tests {
|
|||
way_document.toward = vec![r(K::Question, &q1)];
|
||||
committed(admit(&mut mind, vec![D::Research(note_document), D::Idea(way_document)]));
|
||||
|
||||
// Two desk answers on the open fork Q2: the second is a Revision of the first.
|
||||
let D::Question(shown) = question("Q2", &["A", "B"], "A") else { unreachable!() };
|
||||
let stamp = crate::desk::DeskStamp { answerer: s("x"), authority: eureka_pipeline::DeskAuthority::Member, deciders: vec![s("x")] };
|
||||
let pick = |n: u32| crate::desk::DeskAnswerRequest {
|
||||
fork: s(&q2),
|
||||
options_hash: crate::desk::options_hash(&shown),
|
||||
body: crate::desk::DeskBody::Answer { choice: Some(s("A")), note: None },
|
||||
answered_at: s("2026-11-10T12:00:00Z"),
|
||||
request_id: eureka_pipeline::RequestId(format!("00000000-0000-4000-8000-{n:012x}")),
|
||||
};
|
||||
let (answer_1, answer_2) = (id("desk_answer", "Q2.n1"), id("desk_answer", "Q2.n2"));
|
||||
for n in 1..=2 {
|
||||
assert!(matches!(mind.answer_desk(stamp.clone(), pick(n), now()), crate::desk::DeskAnswerOutcome::Stored { .. }));
|
||||
}
|
||||
|
||||
let (n1, f1_closed, f2_closed) = (
|
||||
id("resolution", "question.Q1.n1"),
|
||||
id("resolution", &format!("finding.{}.n1", f1.rsplit(':').next().unwrap())),
|
||||
|
|
@ -1857,8 +1888,11 @@ mod tests {
|
|||
(CitationRole::Sources, ¬e, &f1),
|
||||
(CitationRole::RestsOn, &way, ¬e),
|
||||
(CitationRole::Toward, &way, &q1),
|
||||
(CitationRole::Fork, &answer_1, &q2),
|
||||
(CitationRole::Fork, &answer_2, &q2),
|
||||
(CitationRole::Answer, &answer_2, &answer_1),
|
||||
];
|
||||
assert_eq!(CitationRole::ALL.len(), 24);
|
||||
assert_eq!(CitationRole::ALL.len(), 26);
|
||||
for role in CitationRole::ALL {
|
||||
let selection =
|
||||
Selection { cited: Some(Incoming { role: role.name().into(), exists: true }), ..Selection::default() };
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ pub const RECEIPT_SCHEMA_VERSION: &str = "huginn.mind_commit_receipt.v2";
|
|||
|
||||
/// Which faculty a batch was admitted as. Attribution, not authority
|
||||
/// (ruling 18): no admission rule trusts it, and Cut 9's views read it back
|
||||
/// from the receipt.
|
||||
/// from the receipt. `Member` and `Steward` name a desk answerer who is neither the
|
||||
/// operator nor a faculty; they confer no admission right anywhere (a test admits a
|
||||
/// batch under each and checks the outcomes match).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum Faculty {
|
||||
SelfFaculty,
|
||||
|
|
@ -40,6 +42,8 @@ pub enum Faculty {
|
|||
Life,
|
||||
Eyes,
|
||||
Operator,
|
||||
Member,
|
||||
Steward,
|
||||
}
|
||||
|
||||
/// Who admitted a batch: the faculty, the agent, its session and the tool.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ pub enum MindRefusal {
|
|||
InvalidOptions { question: String },
|
||||
InvalidChoice { ruling: String, choice: String },
|
||||
QuoteWithoutOperator { ruling: String },
|
||||
/// A `desk_answer` reached a door other than the desk's (an admit, an import or a derivation): `id` is its key.
|
||||
DeskAnswerOutsideDesk { id: String },
|
||||
/// A desk request the Mind cannot read as an answer: `field` names the part (the fork, `answered_at`, an answer carrying neither choice nor note, a malformed answer id).
|
||||
DeskRequestInvalid { field: String },
|
||||
/// The desk's `request_id_conflict` (CultLib PR #72): this `(answerer, request_id)` already bound another body. Nothing is stored.
|
||||
DeskRequestIdConflict { request_id: String },
|
||||
/// `campaign` names the refused document: a campaign, or a target revision that empties its predecessor's repos.
|
||||
EmptyRepos { campaign: String },
|
||||
RepoNotInCampaign { repo: String },
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ pub(crate) enum Alias {
|
|||
Pass,
|
||||
}
|
||||
|
||||
const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"];
|
||||
const FACULTIES: [&str; 9] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator", "Member", "Steward"];
|
||||
const OUTCOMES: [&str; 8] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn", "Slept", "Moved"];
|
||||
const BOOLEANS: [&str; 2] = ["true", "false"];
|
||||
const MEMORY_ORIGINS: [&str; 5] = ["Room", "Direct", "Wake", "SelfRun", "Sleep"];
|
||||
|
|
@ -246,7 +246,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::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Memory(_) | D::Research(_) | D::Idea(_) => {
|
||||
D::Target(_) | D::Question(_) | D::Ruling(_) | D::Verdict(_) | D::Finding(_) | D::Instance(_) | D::Resolution(_) | D::Run(_) | D::Pulse(_) | D::Reply(_) | D::Thought(_) | D::Memory(_) | D::Research(_) | D::Idea(_) | D::DeskAnswer(_) => {
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
|
|
@ -435,6 +435,20 @@ mod tests {
|
|||
names
|
||||
}
|
||||
|
||||
/// The `faculty` alias values are exactly Faculty's serde names; the exhaustive match makes
|
||||
/// a new variant a compile error here until FACULTIES names it.
|
||||
#[test]
|
||||
fn the_faculty_alias_values_are_the_faculty_serde_names() {
|
||||
use crate::receipt::Faculty;
|
||||
let named = |faculty: Faculty| match faculty {
|
||||
Faculty::SelfFaculty | Faculty::Imagination | Faculty::Hands | Faculty::Soul | Faculty::Life | Faculty::Eyes | Faculty::Operator | Faculty::Member | Faculty::Steward => {
|
||||
serde_json::to_value(faculty).unwrap().as_str().unwrap().to_string()
|
||||
}
|
||||
};
|
||||
let all = [Faculty::SelfFaculty, Faculty::Imagination, Faculty::Hands, Faculty::Soul, Faculty::Life, Faculty::Eyes, Faculty::Operator, Faculty::Member, Faculty::Steward];
|
||||
assert_eq!(sorted(all.into_iter().map(named).collect()), strings(&FACULTIES));
|
||||
}
|
||||
|
||||
fn strings(names: &[&str]) -> Vec<String> {
|
||||
sorted(names.iter().map(|name| name.to_string()).collect())
|
||||
}
|
||||
|
|
@ -445,7 +459,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]); 20] = [
|
||||
let table: [(K, &[&str]); 21] = [
|
||||
(K::Campaign, &["repo"]),
|
||||
(K::Target, &[]),
|
||||
(K::Question, &[]),
|
||||
|
|
@ -466,6 +480,7 @@ mod tests {
|
|||
(K::Idea, &["pass"]),
|
||||
(K::Reply, &[]),
|
||||
(K::Thought, &[]),
|
||||
(K::DeskAnswer, &[]),
|
||||
];
|
||||
for (kind, extra) in table {
|
||||
let want = [&["root", "in_force", "faculty"][..], extra].concat();
|
||||
|
|
@ -478,10 +493,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
|
||||
/// twenty.
|
||||
/// twenty-one.
|
||||
#[test]
|
||||
fn each_kind_declares_the_roles_it_carries_and_their_targets() {
|
||||
let table: [(K, &[&str]); 20] = [
|
||||
let table: [(K, &[&str]); 21] = [
|
||||
(K::Campaign, &[]),
|
||||
(K::Target, &[]),
|
||||
(K::Question, &["raised_in"]),
|
||||
|
|
@ -502,13 +517,14 @@ mod tests {
|
|||
(K::Idea, &["rests_on", "toward"]),
|
||||
(K::Reply, &["run"]),
|
||||
(K::Thought, &["reads"]),
|
||||
(K::DeskAnswer, &["fork", "answer"]),
|
||||
];
|
||||
for (kind, roles) in table {
|
||||
assert_eq!(sorted(Vocabulary.declared_roles(kind.type_id())), strings(roles), "{kind:?}");
|
||||
}
|
||||
assert_eq!(Vocabulary.target_leaves("answers"), vec![K::Question.type_id().to_string()]);
|
||||
assert_eq!(Vocabulary.target_leaves("rulings"), vec![K::Ruling.type_id().to_string()]);
|
||||
assert_eq!(Vocabulary.target_leaves("subject").len(), 20);
|
||||
assert_eq!(Vocabulary.target_leaves("subject").len(), 21);
|
||||
assert_eq!(
|
||||
sorted(Vocabulary.target_leaves("claims")),
|
||||
sorted([K::CutSpec, K::Finding, K::FollowUp].iter().map(|kind| kind.type_id().to_string()).collect())
|
||||
|
|
|
|||
343
crates/huginn-mind/vectors/fork-card-options-hash.json
Normal file
343
crates/huginn-mind/vectors/fork-card-options-hash.json
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
[
|
||||
{
|
||||
"name": "heimdall_A_five_members",
|
||||
"heimdall": "A",
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_B_recommendation_changed",
|
||||
"heimdall": "B",
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "yes",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"yes\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A22796573222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_C_option_order_changed",
|
||||
"heimdall": "C",
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
},
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"no\",\"text\":\"Keep it local.\"},{\"id\":\"yes\",\"text\":\"Share it.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D2C7B226964223A22796573222C2274657874223A2253686172652069742E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "ac67b41539f869f6203ac0908819df046e4328b27795743a919bd4ab390596b5"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_D_nfc_escapes_astral",
|
||||
"heimdall": "D",
|
||||
"title": "Café “desk”",
|
||||
"context": "Line one\nLine two — 😀",
|
||||
"options": [
|
||||
{
|
||||
"id": "a",
|
||||
"text": "x"
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"text": "y"
|
||||
}
|
||||
],
|
||||
"recommendation": "a",
|
||||
"depends": [],
|
||||
"canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D",
|
||||
"sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_E_decomposed_equals_D",
|
||||
"heimdall": "E",
|
||||
"title": "Café “desk”",
|
||||
"context": "Line one\nLine two — 😀",
|
||||
"options": [
|
||||
{
|
||||
"id": "a",
|
||||
"text": "x"
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"text": "y"
|
||||
}
|
||||
],
|
||||
"recommendation": "a",
|
||||
"depends": [],
|
||||
"canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D",
|
||||
"sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_F_option_id_changed",
|
||||
"heimdall": "F",
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "share",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"share\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A227368617265222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "fb53aa3a9f7e74029f755d774064eedd240ab90d8fc64723f97c31b3d2846d40"
|
||||
},
|
||||
{
|
||||
"name": "heimdall_G_depends_emptied",
|
||||
"heimdall": "G",
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "f856edce121db4121b5f1af400b1c471ebbfa6579a042aca9a34ae31c15f5c86"
|
||||
},
|
||||
{
|
||||
"name": "option_text_changed",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it!"
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it!\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A22536861726520697421227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "9ce0d2770497fdd2d5338e66f9f1ae1741d87301db4c793407c25762929392b6"
|
||||
},
|
||||
{
|
||||
"name": "title_changed",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent?",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent?\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E743F227D",
|
||||
"sha256": "30fe0c5bc88bd21d64592a495316c30e80fcee6cacf5c63fd9a818ca5cf87d0f"
|
||||
},
|
||||
{
|
||||
"name": "context_changed",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should slot marks be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should slot marks be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C6420736C6F74206D61726B73206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "716de65096e34fd89cc92848b667d90f5c27eeb66929da3d340cabb80155e173"
|
||||
},
|
||||
{
|
||||
"name": "depends_order_changed",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience",
|
||||
"x:question:y"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\",\"x:question:y\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365222C22783A7175657374696F6E3A79225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "fc3f651c6f498385256fc1232d5db31ece9dcc542e391f97245d27bc9e385b3d"
|
||||
},
|
||||
{
|
||||
"name": "depends_order_changed_reversed",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"x:question:y",
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"x:question:y\",\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B22783A7175657374696F6E3A79222C2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "b22642e30f8d68839e3a0df5bc23b83ff50a839fcecf0366a65fa6e3cf873796"
|
||||
},
|
||||
{
|
||||
"name": "hidden_members_do_not_move_it",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48"
|
||||
},
|
||||
{
|
||||
"name": "control_characters_and_markup_are_escaped_minimally",
|
||||
"heimdall": null,
|
||||
"title": "q \" b \\ t \t n \n bell \u0007 del ",
|
||||
"context": "</script>
&",
|
||||
"options": [
|
||||
{
|
||||
"id": "yes",
|
||||
"text": "Share it."
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"</script>
&\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"q \\\" b \\\\ t \\t n \\n bell \\u0007 del \"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A223C2F7363726970743E20E280A82026222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A2271205C222062205C5C2074205C74206E205C6E2062656C6C205C75303030372064656C207F227D",
|
||||
"sha256": "f2d5a09513ce728129f6ca13f7123aa75b2bb77541b1de8946f39239634b40fe"
|
||||
},
|
||||
{
|
||||
"name": "astral_option_id_and_text",
|
||||
"heimdall": null,
|
||||
"title": "Slot mark consent",
|
||||
"context": "Should a slot mark be shared?",
|
||||
"options": [
|
||||
{
|
||||
"id": "😀",
|
||||
"text": "𝄞"
|
||||
},
|
||||
{
|
||||
"id": "no",
|
||||
"text": "Keep it local."
|
||||
}
|
||||
],
|
||||
"recommendation": "no",
|
||||
"depends": [
|
||||
"gamecult-chat:question:file-zone-audience"
|
||||
],
|
||||
"canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"😀\",\"text\":\"𝄞\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}",
|
||||
"canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22F09F9880222C2274657874223A22F09D849E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D",
|
||||
"sha256": "836fbfce16127132bae100aa773d328a85ad08d241ae9bf47ea7f84766762cac"
|
||||
}
|
||||
]
|
||||
210
schemas/cultnet/epiphany.pipeline.desk_answer.v2.schema.json
Normal file
210
schemas/cultnet/epiphany.pipeline.desk_answer.v2.schema.json
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
{
|
||||
"$defs": {
|
||||
"DeskAuthority": {
|
||||
"enum": [
|
||||
"Operator",
|
||||
"Steward",
|
||||
"Member"
|
||||
],
|
||||
"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,
|
||||
"description": "A request the desk would not count, kept with what it was sent with. `retracts` is the\ntarget a refused retraction named, as sent and unchecked (it may name no answer at all);\nit is `None` on a refused answer, and it is never a citation.",
|
||||
"properties": {
|
||||
"Refused": {
|
||||
"properties": {
|
||||
"code": {
|
||||
"$ref": "#/$defs/DeskRefusal"
|
||||
},
|
||||
"retracts": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Refused"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeskRefusal": {
|
||||
"enum": [
|
||||
"StaleFork",
|
||||
"NotADecider",
|
||||
"ForkClosed",
|
||||
"ChoiceInvalid",
|
||||
"BadRetraction",
|
||||
"AlreadyRetracted"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"RequestId": {
|
||||
"description": "A client's retry token: a lowercase D-format UUID.",
|
||||
"maxLength": 36,
|
||||
"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"
|
||||
},
|
||||
"Words": {
|
||||
"description": "A person's words, verbatim: 1 to 4,000 UTF-8 bytes, not blank (CultLib PR #72, `answer.note`).",
|
||||
"maxLength": 4000,
|
||||
"minLength": 1,
|
||||
"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` (an attribution, never a decision) are stamped by the\nMind from the verified caller and are absent from the request type; `answered_at`\nis the client's claim kept as written, display only: order is `sequence`, the\nreceiver's order. `received_at` is the Mind's admission time (UTC) and\n`claim_in_future` is set when `answered_at` is more than five minutes after it.\n`request_id` is the client's retry token and `body_sha256` the digest of the\nrequest it came with: the pair that makes a retry the same answer, never a new\none; the key is `(answerer, request_id)`. A record holds no card: a replayed\n`stale_fork` is shown the current card. `sequence` is per question and is the\nlast part of the key (the answer id). `choice`, `note` and (for a retraction) the\ntarget are kept as sent whether the request was counted or refused, so a refused\n`choice_invalid` keeps a choice that is no option label; which answer counts now is\nderived by the reader, never stored here.",
|
||||
"properties": {
|
||||
"answered_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"answerer": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"authority": {
|
||||
"$ref": "#/$defs/DeskAuthority"
|
||||
},
|
||||
"body_sha256": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"campaign": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
},
|
||||
"choice": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim_in_future": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fork": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"note": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Words"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options_hash": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"outcome": {
|
||||
"$ref": "#/$defs/DeskOutcome"
|
||||
},
|
||||
"received_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"request_id": {
|
||||
"$ref": "#/$defs/RequestId"
|
||||
},
|
||||
"sequence": {
|
||||
"format": "uint32",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"campaign",
|
||||
"fork",
|
||||
"sequence",
|
||||
"options_hash",
|
||||
"answered_at",
|
||||
"received_at",
|
||||
"claim_in_future",
|
||||
"request_id",
|
||||
"body_sha256",
|
||||
"answerer",
|
||||
"authority",
|
||||
"outcome"
|
||||
],
|
||||
"title": "PipelineDeskAnswer",
|
||||
"type": "object"
|
||||
}
|
||||
|
|
@ -177,7 +177,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -181,7 +181,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -395,6 +395,107 @@
|
|||
"type": "string",
|
||||
"maxLength": 10
|
||||
},
|
||||
"DeskAuthority": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Operator",
|
||||
"Steward",
|
||||
"Member"
|
||||
]
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"description": "The answer that counts for its fork when it was admitted.",
|
||||
"type": "string",
|
||||
"const": "Counted"
|
||||
},
|
||||
{
|
||||
"description": "A later passing answer, kept visible; `of` is the counted answer it followed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Revision": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"of": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"of"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Revision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Retraction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retracts": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"retracts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Retraction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A request the desk would not count, kept with what it was sent with. `retracts` is the\ntarget a refused retraction named, as sent and unchecked (it may name no answer at all);\nit is `None` on a refused answer, and it is never a citation.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Refused": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"$ref": "#/$defs/DeskRefusal"
|
||||
},
|
||||
"retracts": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Refused"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeskRefusal": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"StaleFork",
|
||||
"NotADecider",
|
||||
"ForkClosed",
|
||||
"ChoiceInvalid",
|
||||
"BadRetraction",
|
||||
"AlreadyRetracted"
|
||||
]
|
||||
},
|
||||
"Deviation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -468,7 +569,7 @@
|
|||
]
|
||||
},
|
||||
"Faculty": {
|
||||
"description": "Which faculty a batch was admitted as. Attribution, not authority\n(ruling 18): no admission rule trusts it, and Cut 9's views read it back\nfrom the receipt.",
|
||||
"description": "Which faculty a batch was admitted as. Attribution, not authority\n(ruling 18): no admission rule trusts it, and Cut 9's views read it back\nfrom the receipt. `Member` and `Steward` name a desk answerer who is neither the\noperator nor a faculty; they confer no admission right anywhere (a test admits a\nbatch under each and checks the outcomes match).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"SelfFaculty",
|
||||
|
|
@ -477,7 +578,9 @@
|
|||
"Soul",
|
||||
"Life",
|
||||
"Eyes",
|
||||
"Operator"
|
||||
"Operator",
|
||||
"Member",
|
||||
"Steward"
|
||||
]
|
||||
},
|
||||
"FileChange": {
|
||||
|
|
@ -1091,6 +1194,84 @@
|
|||
"questions"
|
||||
]
|
||||
},
|
||||
"PipelineDeskAnswer": {
|
||||
"description": "One answer the fork desk's receiver passed to the Mind, refused ones included.\n`answerer` and `authority` (an attribution, never a decision) are stamped by the\nMind from the verified caller and are absent from the request type; `answered_at`\nis the client's claim kept as written, display only: order is `sequence`, the\nreceiver's order. `received_at` is the Mind's admission time (UTC) and\n`claim_in_future` is set when `answered_at` is more than five minutes after it.\n`request_id` is the client's retry token and `body_sha256` the digest of the\nrequest it came with: the pair that makes a retry the same answer, never a new\none; the key is `(answerer, request_id)`. A record holds no card: a replayed\n`stale_fork` is shown the current card. `sequence` is per question and is the\nlast part of the key (the answer id). `choice`, `note` and (for a retraction) the\ntarget are kept as sent whether the request was counted or refused, so a refused\n`choice_invalid` keeps a choice that is no option label; which answer counts now is\nderived by the reader, never stored here.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answered_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"answerer": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"authority": {
|
||||
"$ref": "#/$defs/DeskAuthority"
|
||||
},
|
||||
"body_sha256": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"campaign": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
},
|
||||
"choice": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim_in_future": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fork": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"note": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Words"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options_hash": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"outcome": {
|
||||
"$ref": "#/$defs/DeskOutcome"
|
||||
},
|
||||
"received_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"request_id": {
|
||||
"$ref": "#/$defs/RequestId"
|
||||
},
|
||||
"sequence": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"campaign",
|
||||
"fork",
|
||||
"sequence",
|
||||
"options_hash",
|
||||
"answered_at",
|
||||
"received_at",
|
||||
"claim_in_future",
|
||||
"request_id",
|
||||
"body_sha256",
|
||||
"answerer",
|
||||
"authority",
|
||||
"outcome"
|
||||
]
|
||||
},
|
||||
"PipelineDocument": {
|
||||
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\neighteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
|
||||
"oneOf": [
|
||||
|
|
@ -1413,6 +1594,22 @@
|
|||
"kind",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"const": "desk_answer"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/$defs/PipelineDeskAnswer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1679,7 +1876,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
]
|
||||
},
|
||||
"PipelineMemory": {
|
||||
|
|
@ -2432,6 +2630,11 @@
|
|||
"builds"
|
||||
]
|
||||
},
|
||||
"RequestId": {
|
||||
"description": "A client's retry token: a lowercase D-format UUID.",
|
||||
"type": "string",
|
||||
"maxLength": 36
|
||||
},
|
||||
"ResearchSource": {
|
||||
"description": "Where a research claim comes from. A `Repo` pin and a `Web` page are\noutside the document set; an `Observed` result reuses a finding's evidence;\na `Document` is the one source that is a citation edge.",
|
||||
"oneOf": [
|
||||
|
|
@ -2977,6 +3180,12 @@
|
|||
"name",
|
||||
"pins"
|
||||
]
|
||||
},
|
||||
"Words": {
|
||||
"description": "A person's words, verbatim: 1 to 4,000 UTF-8 bytes, not blank (CultLib PR #72, `answer.note`).",
|
||||
"type": "string",
|
||||
"maxLength": 4000,
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +263,9 @@
|
|||
"toward",
|
||||
"run",
|
||||
"reads",
|
||||
"day_through"
|
||||
"day_through",
|
||||
"fork",
|
||||
"answer"
|
||||
]
|
||||
},
|
||||
"ClaimOutcome": {
|
||||
|
|
@ -402,6 +404,107 @@
|
|||
"manifest"
|
||||
]
|
||||
},
|
||||
"DeskAuthority": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Operator",
|
||||
"Steward",
|
||||
"Member"
|
||||
]
|
||||
},
|
||||
"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": [
|
||||
{
|
||||
"description": "The answer that counts for its fork when it was admitted.",
|
||||
"type": "string",
|
||||
"const": "Counted"
|
||||
},
|
||||
{
|
||||
"description": "A later passing answer, kept visible; `of` is the counted answer it followed.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Revision": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"of": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"of"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Revision"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Retraction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retracts": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"retracts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Retraction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A request the desk would not count, kept with what it was sent with. `retracts` is the\ntarget a refused retraction named, as sent and unchecked (it may name no answer at all);\nit is `None` on a refused answer, and it is never a citation.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Refused": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"$ref": "#/$defs/DeskRefusal"
|
||||
},
|
||||
"retracts": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Refused"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"DeskRefusal": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"StaleFork",
|
||||
"NotADecider",
|
||||
"ForkClosed",
|
||||
"ChoiceInvalid",
|
||||
"BadRetraction",
|
||||
"AlreadyRetracted"
|
||||
]
|
||||
},
|
||||
"Deviation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -475,7 +578,7 @@
|
|||
]
|
||||
},
|
||||
"Faculty": {
|
||||
"description": "Which faculty a batch was admitted as. Attribution, not authority\n(ruling 18): no admission rule trusts it, and Cut 9's views read it back\nfrom the receipt.",
|
||||
"description": "Which faculty a batch was admitted as. Attribution, not authority\n(ruling 18): no admission rule trusts it, and Cut 9's views read it back\nfrom the receipt. `Member` and `Steward` name a desk answerer who is neither the\noperator nor a faculty; they confer no admission right anywhere (a test admits a\nbatch under each and checks the outcomes match).",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"SelfFaculty",
|
||||
|
|
@ -484,7 +587,9 @@
|
|||
"Soul",
|
||||
"Life",
|
||||
"Eyes",
|
||||
"Operator"
|
||||
"Operator",
|
||||
"Member",
|
||||
"Steward"
|
||||
]
|
||||
},
|
||||
"FileChange": {
|
||||
|
|
@ -1400,6 +1505,69 @@
|
|||
"QuoteWithoutOperator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A `desk_answer` reached a door other than the desk's (an admit, an import or a derivation): `id` is its key.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"DeskAnswerOutsideDesk": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"DeskAnswerOutsideDesk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A desk request the Mind cannot read as an answer: `field` names the part (the fork, `answered_at`, an answer carrying neither choice nor note, a malformed answer id).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"DeskRequestInvalid": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"field"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"DeskRequestInvalid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "The desk's `request_id_conflict` (CultLib PR #72): this `(answerer, request_id)` already bound another body. Nothing is stored.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"DeskRequestIdConflict": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"request_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"DeskRequestIdConflict"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "`campaign` names the refused document: a campaign, or a target revision that empties its predecessor's repos.",
|
||||
"type": "object",
|
||||
|
|
@ -2529,6 +2697,84 @@
|
|||
"questions"
|
||||
]
|
||||
},
|
||||
"PipelineDeskAnswer": {
|
||||
"description": "One answer the fork desk's receiver passed to the Mind, refused ones included.\n`answerer` and `authority` (an attribution, never a decision) are stamped by the\nMind from the verified caller and are absent from the request type; `answered_at`\nis the client's claim kept as written, display only: order is `sequence`, the\nreceiver's order. `received_at` is the Mind's admission time (UTC) and\n`claim_in_future` is set when `answered_at` is more than five minutes after it.\n`request_id` is the client's retry token and `body_sha256` the digest of the\nrequest it came with: the pair that makes a retry the same answer, never a new\none; the key is `(answerer, request_id)`. A record holds no card: a replayed\n`stale_fork` is shown the current card. `sequence` is per question and is the\nlast part of the key (the answer id). `choice`, `note` and (for a retraction) the\ntarget are kept as sent whether the request was counted or refused, so a refused\n`choice_invalid` keeps a choice that is no option label; which answer counts now is\nderived by the reader, never stored here.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answered_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"answerer": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"authority": {
|
||||
"$ref": "#/$defs/DeskAuthority"
|
||||
},
|
||||
"body_sha256": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"campaign": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
},
|
||||
"choice": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim_in_future": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fork": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"note": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/Words"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options_hash": {
|
||||
"$ref": "#/$defs/Sha256Hex"
|
||||
},
|
||||
"outcome": {
|
||||
"$ref": "#/$defs/DeskOutcome"
|
||||
},
|
||||
"received_at": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"request_id": {
|
||||
"$ref": "#/$defs/RequestId"
|
||||
},
|
||||
"sequence": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"campaign",
|
||||
"fork",
|
||||
"sequence",
|
||||
"options_hash",
|
||||
"answered_at",
|
||||
"received_at",
|
||||
"claim_in_future",
|
||||
"request_id",
|
||||
"body_sha256",
|
||||
"answerer",
|
||||
"authority",
|
||||
"outcome"
|
||||
]
|
||||
},
|
||||
"PipelineDocument": {
|
||||
"description": "The envelope enum, adjacently tagged on the kind name: a document\non a wire is `{ kind, value }`, and `kind` is exactly what\n`PipelineKind::name()` returns, so a reader dispatches on the kind\nsegment it already knows from the key and needs no second registry\nmapping variant spellings to kinds. Adjacent rather than internal\nbecause a value is a named map of its own and an internal tag would\nhave to be merged into it; the tag stays beside the value instead.\nThis is not a published schema: `schemas/cultnet` publishes the\neighteen per-kind value documents, and the envelope is the wire's\nshape, not a document's.",
|
||||
"oneOf": [
|
||||
|
|
@ -2851,6 +3097,22 @@
|
|||
"kind",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"const": "desk_answer"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/$defs/PipelineDeskAnswer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -3781,6 +4043,49 @@
|
|||
"kind",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "`outcome` is the stored, write-time outcome's variant name. `counts` is whether this answer is\nthe one that counts for its fork at the read's snapshot, derived by `counted_answer` and never\nstored: a retracted `Counted` answer reads `counts: false`, the promoted next answer `true`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"const": "desk_answer"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authority": {
|
||||
"$ref": "#/$defs/DeskAuthority"
|
||||
},
|
||||
"counts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fork": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"outcome": {
|
||||
"$ref": "#/$defs/Short"
|
||||
},
|
||||
"sequence": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fork",
|
||||
"sequence",
|
||||
"outcome",
|
||||
"counts",
|
||||
"authority"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -4047,7 +4352,8 @@
|
|||
"Research",
|
||||
"Idea",
|
||||
"Reply",
|
||||
"Thought"
|
||||
"Thought",
|
||||
"DeskAnswer"
|
||||
]
|
||||
},
|
||||
"PipelineMemory": {
|
||||
|
|
@ -5257,6 +5563,11 @@
|
|||
"builds"
|
||||
]
|
||||
},
|
||||
"RequestId": {
|
||||
"description": "A client's retry token: a lowercase D-format UUID.",
|
||||
"type": "string",
|
||||
"maxLength": 36
|
||||
},
|
||||
"ResearchSource": {
|
||||
"description": "Where a research claim comes from. A `Repo` pin and a `Web` page are\noutside the document set; an `Observed` result reuses a finding's evidence;\na `Document` is the one source that is a citation edge.",
|
||||
"oneOf": [
|
||||
|
|
@ -5861,6 +6172,12 @@
|
|||
"name",
|
||||
"pins"
|
||||
]
|
||||
},
|
||||
"Words": {
|
||||
"description": "A person's words, verbatim: 1 to 4,000 UTF-8 bytes, not blank (CultLib PR #72, `answer.note`).",
|
||||
"type": "string",
|
||||
"maxLength": 4000,
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue