RS-3 2: the read side is CultNet's selection over the organ's rows

The four hand-written filters, the order and the cap are deleted:
PipelineQuery, PipelineQueryPage, QUERY_LIMIT_MAX, Reader::matches, ordered,
repo_matches. Mind::query(selection, semantic) validates through
cultnet_rs::validate, closes the values door (rows.rs refuse_values, S6),
takes the snapshot from the cursor's own asOf (an asOf beyond the head is
CursorInvalid), builds Reader::at(as_of) so every derivation runs over the
documents admitted at or before it, and evaluates through cultnet_rs::select.
The page carries matched, asOf, the next cursor, headers or whole views by
projection, and typed edges when the selection hops.

rows.rs is the organ's vocabulary: SelectionRow (a Row), Vocabulary (a
RowSet: thirteen schemas, eleven aliases, fifteen roles) and the value door.
huginn-mind links cultnet-rs at the rev the daemon pins. Selection refusals
map totally onto SelectionInvalid, CursorInvalid or Unavailable. The wire's
Query carries CultLib's selection by $ref, and both published schemas are
regenerated. The relations RS-2 unpinned are re-pinned as selections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 05:56:37 +02:00
commit eb57bdba40
13 changed files with 2360 additions and 522 deletions

1
Cargo.lock generated
View file

@ -468,6 +468,7 @@ dependencies = [
"anyhow",
"chrono",
"cultcache-rs",
"cultnet-rs",
"epiphany-pipeline",
"rmp-serde",
"schemars",

View file

@ -85,7 +85,7 @@ impl<S: MindStore, I: IndexSink<S>> Daemon<S, I> {
Ok(view) => HuginnMindResponse::View(view),
Err(refusal) => HuginnMindResponse::Refused(refusal),
},
HuginnMindRequest::Query { query, .. } => match self.mind.query(&query) {
HuginnMindRequest::Query { selection, semantic, .. } => match self.mind.query(&selection, semantic.as_ref()) {
Ok(page) => HuginnMindResponse::Query(page),
Err(refusal) => HuginnMindResponse::Refused(refusal),
},
@ -104,7 +104,8 @@ pub(crate) mod tests {
StructuralDelta, Title, VerificationTest,
};
use huginn_mind::wire::MindStatus;
use huginn_mind::{Faculty, PipelineAdmissionBatch, PipelineProvenance, PipelineQuery, PipelineStatus, SemanticQuery};
use cultnet_rs::Selection;
use huginn_mind::{Faculty, PipelineAdmissionBatch, PipelineProvenance, PipelinePageItems, PipelineStatus, SemanticQuery};
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
@ -375,19 +376,29 @@ pub(crate) mod tests {
);
assert_eq!(daemon.runtime_id(), "huginn-yggdrasil");
let query = PipelineQuery { kinds: vec![PipelineKind::Instance], ..PipelineQuery::default() };
let HuginnMindResponse::Query(page) =
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), query }, now())
else {
let instances = Selection { schemas: Some(vec![PipelineKind::Instance.type_id().into()]), ..Selection::default() };
let HuginnMindResponse::Query(page) = daemon.handle(
HuginnMindRequest::Query { instance: slug(INSTANCE), selection: instances.clone(), semantic: None },
now(),
) else {
panic!("expected a page");
};
assert_eq!(page.matched, 1);
assert_eq!(page.items[0].admission.provenance, provenance());
assert_eq!(page.items[0].status, PipelineStatus::InForce);
let PipelinePageItems::Headers(headers) = &page.items else { panic!("the default projection is a header") };
assert_eq!(headers[0].admission.provenance, provenance());
assert_eq!(headers[0].status, huginn_mind::PipelineStatusSummary::InForce);
let id = writes[0].clone();
let whole = Selection { projection: "document".into(), ..instances };
let HuginnMindResponse::Query(page) =
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), selection: whole, semantic: None }, now())
else {
panic!("expected a page");
};
let PipelinePageItems::Documents(views) = &page.items else { panic!("the document projection is the view") };
assert_eq!(views[0].status, PipelineStatus::InForce);
let view = daemon.handle(HuginnMindRequest::View { instance: slug(INSTANCE), id: id.clone() }, now());
assert_eq!(view, HuginnMindResponse::View(Some(page.items[0].clone())));
assert_eq!(view, HuginnMindResponse::View(Some(views[0].clone())));
}
/// Ruling 14 across the transport, on both sides: the mind refuses a read
@ -402,7 +413,7 @@ pub(crate) mod tests {
};
let foreign = MindRefusal::ForeignInstance { declared: OTHER.into(), mind: INSTANCE.into() };
let query = HuginnMindRequest::Query { instance: slug(OTHER), query: PipelineQuery::default() };
let query = HuginnMindRequest::Query { instance: slug(OTHER), selection: Selection::default(), semantic: None };
assert_eq!(daemon.handle(query, now()), HuginnMindResponse::Refused(foreign.clone()));
let admit = HuginnMindRequest::Admit(batch(OTHER, vec![identity(OTHER)]));
@ -437,7 +448,7 @@ pub(crate) mod tests {
assert_eq!(CASED.to_ascii_lowercase(), INSTANCE);
for declared in [NEAR, PREFIXED, CASED] {
let refusal = MindRefusal::ForeignInstance { declared: declared.into(), mind: INSTANCE.into() };
let query = HuginnMindRequest::Query { instance: slug(declared), query: PipelineQuery::default() };
let query = HuginnMindRequest::Query { instance: slug(declared), selection: Selection::default(), semantic: None };
assert_eq!(daemon.handle(query, now()), HuginnMindResponse::Refused(refusal.clone()), "{declared}");
let view = HuginnMindRequest::View { instance: slug(declared), id: writes[0].clone() };
assert_eq!(daemon.handle(view, now()), HuginnMindResponse::Refused(refusal.clone()), "{declared}");
@ -472,7 +483,7 @@ pub(crate) mod tests {
value: fullwidth.into(),
});
let query = HuginnMindRequest::Query { instance: slug(fullwidth), query: PipelineQuery::default() };
let query = HuginnMindRequest::Query { instance: slug(fullwidth), selection: Selection::default(), semantic: None };
assert_eq!(daemon.handle(query, now()), HuginnMindResponse::Refused(expected.clone()));
let view = HuginnMindRequest::View {
@ -508,7 +519,7 @@ pub(crate) mod tests {
HuginnMindResponse::Admit(PipelineAdmissionOutcome::Refused(expected.clone()))
);
let query = HuginnMindRequest::Query { instance: slug(fullwidth), query: PipelineQuery::default() };
let query = HuginnMindRequest::Query { instance: slug(fullwidth), selection: Selection::default(), semantic: None };
assert_eq!(daemon.handle(query, now()), HuginnMindResponse::Refused(expected));
}
@ -535,14 +546,11 @@ pub(crate) mod tests {
// A semantic query is unavailable until Cut 11 wires an index, and the
// detail is the mind's own sentence.
let query = PipelineQuery {
semantic: Some(SemanticQuery { text: "who owns the state".into(), top_k: 4 }),
..PipelineQuery::default()
};
let semantic = Some(SemanticQuery { text: "who owns the state".into(), top_k: 4 });
let unwired =
MindRefusal::Unavailable { detail: "semantic query: the index is not wired (Cut 11)".into() };
assert_eq!(
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), query }, now()),
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), selection: Selection::default(), semantic }, now()),
HuginnMindResponse::Refused(unwired)
);
@ -553,11 +561,47 @@ pub(crate) mod tests {
PipelineRef { kind: PipelineKind::Question, id: Short(format!("{CAMPAIGN}:question:Q1")) };
let present = HuginnMindRequest::View { instance: slug(INSTANCE), id: absent };
assert_eq!(daemon.handle(present, now()), HuginnMindResponse::View(None));
let plain = HuginnMindRequest::Query { instance: slug(INSTANCE), query: PipelineQuery::default() };
let plain = HuginnMindRequest::Query { instance: slug(INSTANCE), selection: Selection::default(), semantic: None };
let HuginnMindResponse::Query(page) = daemon.handle(plain, now()) else { panic!("expected a page") };
assert_eq!(page.matched, 1);
}
/// A refusal of the selection crosses the dispatch as itself: typed, on
/// the field the client sent, and never swallowed into an empty page or
/// rewrapped as an unavailable store. A stale cursor is the organ's own
/// defect, so a valid walk never meets it.
#[test]
fn a_selection_refusal_crosses_the_dispatch_as_itself() {
let (_root, mut daemon) = seeded();
let ask = |daemon: &mut Daemon<OwnedRedbMessagePackBackingStore, NoIndex>, selection: Selection| {
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), selection, semantic: None }, now())
};
let predicate = |index: &str, value: &str| Selection {
fields: Some(vec![cultnet_rs::FieldPredicate {
index: index.into(),
op: "any_of".into(),
values: Some(vec![value.into()]),
number: None,
}]),
..Selection::default()
};
let HuginnMindResponse::Refused(MindRefusal::SelectionInvalid { field, .. }) =
ask(&mut daemon, predicate("nonsense", "x"))
else {
panic!("an undeclared alias is the substrate's refusal");
};
assert_eq!(field, "fields[0].index");
let HuginnMindResponse::Refused(MindRefusal::SelectionInvalid { field, value, .. }) =
ask(&mut daemon, predicate("repo", "a/b/c"))
else {
panic!("a value outside its domain is the organ's refusal");
};
assert_eq!((field.as_str(), value.as_deref()), ("fields[0].values", Some("a/b/c")));
let garbled = Selection { cursor: Some("not a cursor".into()), ..Selection::default() };
assert!(matches!(ask(&mut daemon, garbled), HuginnMindResponse::Refused(MindRefusal::CursorInvalid { .. })));
assert_eq!(ask(&mut daemon, predicate("repo", "a/b/c")).status(), "rejected");
}
/// Cut 11's seam: the index is handed every landed write, derived ones
/// included, and can read each; an index that fails changes nothing.
#[test]

View file

@ -318,7 +318,8 @@ mod tests {
use base64::engine::general_purpose::STANDARD;
use cultnet_rs::{CultMesh, CultMeshRudpSocketOptions};
use huginn_mind::wire::{HuginnMindRequest, HuginnMindResponse, MindStatus};
use huginn_mind::{MindRefusal, PipelineAdmissionOutcome, PipelineQuery};
use cultnet_rs::{FieldPredicate, Selection};
use huginn_mind::{MindRefusal, PipelineAdmissionOutcome, PipelinePageItems};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@ -408,7 +409,7 @@ mod tests {
// A mind's refusal is an answer on the response schema, not a failure
// of the envelope: the client decodes it as the typed refusal it is.
let read = HuginnMindRequest::Query { instance: slug(OTHER), query: PipelineQuery::default() };
let read = HuginnMindRequest::Query { instance: slug(OTHER), selection: Selection::default(), semantic: None };
let reply = answer(&mut daemon, &registry, encode_request("m-r", &read, None).unwrap(), now());
let CultNetMessage::OperationResponse { status, payload_schema, .. } = &reply else {
panic!("expected an operation response, got {reply:?}");
@ -483,7 +484,7 @@ mod tests {
let requests = [
("m-a", HuginnMindRequest::Admit(batch(INSTANCE, vec![identity(INSTANCE)]))),
("m-b", HuginnMindRequest::Query { instance: slug(INSTANCE), query: PipelineQuery::default() }),
("m-b", HuginnMindRequest::Query { instance: slug(INSTANCE), selection: Selection::default(), semantic: None }),
];
for (client, (message_id, request)) in clients.iter_mut().zip(requests.iter()) {
client.send_schema_message(&encode_request(message_id, request, None).unwrap()).unwrap();
@ -587,7 +588,17 @@ mod tests {
assert!(client.connected());
let reads = [
("m-q", HuginnMindRequest::Query { instance: slug(INSTANCE), query: PipelineQuery::default() }),
// The whole documents: a header page is small by construction, so
// the transport's bound is measured on the projection that can
// exceed it.
(
"m-q",
HuginnMindRequest::Query {
instance: slug(INSTANCE),
selection: Selection { projection: "document".into(), ..Selection::default() },
semantic: None,
},
),
("m-v", HuginnMindRequest::View { instance: slug(INSTANCE), id: wide }),
];
let stopping = Arc::new(AtomicBool::new(false));
@ -906,18 +917,24 @@ mod tests {
/// One cut spec's own reference, by its cut label.
fn spec_ref(daemon: &mut Daemon<OwnedRedbMessagePackBackingStore, NoIndex>, cut: &str) -> PipelineRef {
let query = PipelineQuery {
kinds: vec![PipelineKind::CutSpec],
cut: Some(cut.into()),
..PipelineQuery::default()
let selection = Selection {
schemas: Some(vec![PipelineKind::CutSpec.type_id().into()]),
fields: Some(vec![FieldPredicate {
index: "cut".into(),
op: "any_of".into(),
values: Some(vec![cut.into()]),
number: None,
}]),
..Selection::default()
};
let HuginnMindResponse::Query(page) =
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), query }, now())
daemon.handle(HuginnMindRequest::Query { instance: slug(INSTANCE), selection, semantic: None }, now())
else {
panic!("expected a page");
};
assert_eq!(page.matched, 1, "one cut spec is labelled {cut}");
page.items[0].id.clone()
let PipelinePageItems::Headers(headers) = &page.items else { panic!("expected headers") };
headers[0].id.clone()
}
/// Ruling 15 as an order in the one place both happen. Both gates are shut

View file

@ -8,12 +8,16 @@ publish.workspace = true
# The organ's rule engine: storage, identity and admission of one instance's
# mind. Document shape, bounds, formats and keys come from `epiphany-pipeline`;
# the store comes from CultLib's Rust CultCache at the same rev Epiphany pins,
# so the leaf's wrappers and this crate's cache are one `cultcache-rs`. No
# network, no socket, no index: those are the daemon's (Cut 10, Cut 11).
# so the leaf's wrappers and this crate's cache are one `cultcache-rs`. The
# read side selects through CultNet's typed selection (`cultnet-rs`, the same
# rev the daemon pins): the selection's vocabulary and evaluator are the
# substrate's, this crate supplies the rows. No socket, no index: those are the
# daemon's (Cut 10, Cut 11).
[dependencies]
anyhow = "1"
chrono = "0.4.44"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "6251cbcd37c12ef3f0c1cc50063a03369225b7ac", package = "cultcache-rs" }
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "6251cbcd37c12ef3f0c1cc50063a03369225b7ac", package = "cultnet-rs" }
epiphany-pipeline = { git = "https://github.com/GameCult/Epiphany.git", rev = "4891f2e59008efeb262197564eddaa75bc3c75ea", package = "epiphany-pipeline" }
rmp-serde = "1"
schemars = "1"

View file

@ -36,7 +36,7 @@ pub(crate) struct Docs {
pub(crate) batch: Vec<Staged>,
}
fn kind_of_type(type_id: &str) -> Option<PipelineKind> {
pub(crate) fn kind_of_type(type_id: &str) -> Option<PipelineKind> {
PipelineKind::ALL.iter().copied().find(|kind| kind.type_id() == type_id)
}

View file

@ -10,8 +10,9 @@
//! cross-document rule, every derived write, and the replay check, in one
//! ordered path that `admit`, the daemon's sink and the hand-off all share.
//!
//! The read side derives status, joins the receipts and answers typed queries
//! through the same `docs` the rules use; nothing is stored for it.
//! The read side derives status, joins the receipts and answers one typed
//! selection (CultNet's, over rows this crate supplies) through the same
//! `docs` the rules use; nothing is stored for it.
//!
//! The wire vocabulary is here too, so the daemon and the client share one set
//! of types.
@ -26,6 +27,7 @@
pub mod admission;
mod docs;
pub mod mind;
mod rows;
pub mod query;
pub mod receipt;
pub mod refusal;
@ -39,8 +41,8 @@ pub use docs::CitationRole;
pub use admission::{BATCH_MAX, PipelineAdmissionBatch, PipelineAdmissionOutcome};
pub use mind::{HuginnMindEpoch, Mind};
pub use query::{
AdmissionFacts, PipelineDocumentView, PipelineQuery, PipelineQueryPage, PipelineStatus, QUERY_LIMIT_MAX,
SemanticQuery,
AdmissionFacts, PipelineDocumentSummary, PipelineDocumentView, PipelineEdge, PipelineFacts, PipelinePageItems,
PipelineSelectionPage, PipelineStatus, PipelineStatusSummary, SUMMARY_MAX_BYTES, SemanticQuery,
};
pub use receipt::{DocumentVersion, Faculty, HuginnCommitReceipt, PipelineProvenance, RECEIPT_SCHEMA_VERSION};
pub use refusal::MindRefusal;

View file

@ -12,6 +12,7 @@
use std::path::{Path, PathBuf};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry, OwnedRedbMessagePackBackingStore};
use cultnet_rs::CursorKey;
use epiphany_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineDocument, PipelineKind, Slug, register_pipeline_document_types};
use crate::receipt::HuginnCommitReceipt;
@ -96,6 +97,9 @@ pub struct Mind<S: MindStore> {
store: S,
cache: CultCache,
image: Vec<CultCacheEnvelope>,
/// Mints and verifies this process's selection cursors. Never stored: a
/// cursor does not survive a reopen, which the substrate accepts.
cursor_key: CursorKey,
}
impl Mind<OwnedRedbMessagePackBackingStore> {
@ -180,7 +184,7 @@ impl<S: MindStore> Mind<S> {
refuse_foreign_types(&raw)?;
refuse_foreign_identity(&raw, instance)?;
let (cache, image) = attach(store.clone())?;
Ok(Self { instance: instance.clone(), store, cache, image })
Ok(Self { instance: instance.clone(), store, cache, image, cursor_key: CursorKey::random() })
}
pub fn instance(&self) -> &Slug {
@ -208,6 +212,10 @@ impl<S: MindStore> Mind<S> {
&self.image
}
pub(crate) fn cursor_key(&self) -> &CursorKey {
&self.cursor_key
}
pub fn is_empty(&self) -> bool {
self.image.is_empty()
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
//! act on, never a transport error: the outcome carries it, the wire (Cut 10)
//! serialises it, the tools (Cut 13) show it.
use cultnet_rs::SelectionRefusal;
use epiphany_pipeline::{PipelineKind, PipelineRefusal};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -63,6 +64,13 @@ pub enum MindRefusal {
/// truncated or paginated on its behalf. The daemon owns it and the
/// transport's own bound decides it; no rule of the mind is involved.
ResponseTooLarge { bytes: u64, limit: u64 },
/// A selection the substrate or the organ's value door refused: `field`
/// is the selection field the client sent, `value` the offending part.
SelectionInvalid { field: String, value: Option<String>, message: String },
/// A cursor that does not decode, does not belong to this selection or
/// was not minted by this process, or names a snapshot this mind has not
/// reached.
CursorInvalid { message: String },
Unavailable { detail: String },
}
@ -79,3 +87,60 @@ impl From<PipelineRefusal> for MindRefusal {
Self::Document(refusal)
}
}
/// The one total mapping from the substrate's refusals, with no wildcard arm.
/// `CursorStale` is unreachable: the organ answers at the cursor's own `asOf`,
/// so a stale refusal is an organ defect and says so. `ReferenceOutsideTarget`
/// is integrity: A7 admits only referents of the declared kind, so a stored
/// edge outside its target means the store is not what admission wrote.
impl From<SelectionRefusal> for MindRefusal {
fn from(refusal: SelectionRefusal) -> Self {
match refusal {
SelectionRefusal::Invalid(invalid) => {
Self::SelectionInvalid { field: invalid.field, value: invalid.value, message: invalid.message }
}
SelectionRefusal::CursorInvalid { message } => Self::CursorInvalid { message },
SelectionRefusal::CursorStale { as_of, current } => Self::Unavailable {
detail: format!("cursor_stale: the organ answers at the cursor's asOf ({as_of}), yet was asked at {current}"),
},
reference @ SelectionRefusal::ReferenceOutsideTarget { .. } => {
Self::Unavailable { detail: format!("stored reference outside its declared target: {reference}") }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cultnet_rs::SelectionInvalid;
/// The mapping is the organ's word on each substrate refusal: a bad
/// selection and a bad cursor cross as themselves, and the two the organ
/// is built never to raise cross as integrity faults, not as anything a
/// client could act on.
#[test]
fn each_substrate_refusal_maps_to_one_organ_refusal() {
let invalid = SelectionInvalid { field: "keys".into(), value: Some("x".into()), message: "no".into() };
assert_eq!(
MindRefusal::from(SelectionRefusal::Invalid(invalid)),
MindRefusal::SelectionInvalid { field: "keys".into(), value: Some("x".into()), message: "no".into() }
);
assert_eq!(
MindRefusal::from(SelectionRefusal::CursorInvalid { message: "bad".into() }),
MindRefusal::CursorInvalid { message: "bad".into() }
);
assert!(matches!(
MindRefusal::from(SelectionRefusal::CursorStale { as_of: 3, current: 4 }),
MindRefusal::Unavailable { detail } if detail.contains("cursor_stale")
));
let outside = SelectionRefusal::ReferenceOutsideTarget {
from_schema_id: "a".into(),
from_key: "k".into(),
role: "answers".into(),
to_schema_id: "b".into(),
to_key: "j".into(),
};
assert!(matches!(MindRefusal::from(outside), MindRefusal::Unavailable { .. }));
}
}

View file

@ -0,0 +1,336 @@
//! What the read side gives CultNet's selection evaluator: the rows, the
//! organ's closed vocabulary, and the door that checks the values a selection
//! carries. Types, rules and boundaries only.
//!
//! The substrate owns the grammar of a selection's names, the order, the
//! cursor, the hop and paging. This module owns what a row answers for each
//! declared alias, which aliases and roles exist, and the value domain of
//! every alias: `cultnet_rs::validate` refuses a name it does not know and
//! never looks at a value, so a malformed filter would read as an empty
//! answer. `refuse_values` is the door that closes that (S6).
use std::collections::BTreeMap;
use cultnet_rs::{Row, RowSet, Selection};
use epiphany_pipeline::{OrgRepo, Label, PipelineDocument, PipelineKind, PipelineRef, ResolutionOutcome, Slug};
use crate::docs::{CitationRole, Held, kind_of_id, kind_of_type};
use crate::query::PipelineDocumentView;
use crate::refusal::MindRefusal;
/// The declared index aliases: what a row answers `values(alias)` with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Alias {
/// The key's root segment: a campaign slug, or an instance for
/// stewardships, hand-offs and the identity document. A resolution's root
/// is its subject's.
Root,
InForce,
Faculty,
/// `OrgRepo` identity spelling (lowercase), read through `base`.
Repo,
/// The label in `cut-<label>.`, read through `base`.
Cut,
Severity,
Confidence,
Origin,
Authority,
ClaimOutcome,
Outcome,
}
const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "MindSteward", "Eyes", "Operator"];
const SEVERITIES: [&str; 4] = ["Blocker", "High", "Medium", "Low"];
const CONFIDENCES: [&str; 2] = ["Confirmed", "Plausible"];
const ORIGINS: [&str; 2] = ["Introduced", "PreExisting"];
const AUTHORITIES: [&str; 3] = ["Operator", "Standing", "Defaulted"];
const CLAIM_OUTCOMES: [&str; 3] = ["Holds", "Falsified", "Unproven"];
const OUTCOMES: [&str; 6] = ["Superseded", "Answered", "Fixed", "Deferred", "Recorded", "Withdrawn"];
const BOOLEANS: [&str; 2] = ["true", "false"];
impl Alias {
const ALL: [Alias; 11] = [
Self::Root,
Self::InForce,
Self::Faculty,
Self::Repo,
Self::Cut,
Self::Severity,
Self::Confidence,
Self::Origin,
Self::Authority,
Self::ClaimOutcome,
Self::Outcome,
];
fn name(self) -> &'static str {
match self {
Self::Root => "root",
Self::InForce => "in_force",
Self::Faculty => "faculty",
Self::Repo => "repo",
Self::Cut => "cut",
Self::Severity => "severity",
Self::Confidence => "confidence",
Self::Origin => "origin",
Self::Authority => "authority",
Self::ClaimOutcome => "claim_outcome",
Self::Outcome => "outcome",
}
}
fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|alias| alias.name() == name)
}
fn declared_on(self, kind: PipelineKind) -> bool {
use PipelineKind as K;
match self {
Self::Root | Self::InForce | Self::Faculty => true,
Self::Repo => {
matches!(kind, K::Campaign | K::CutSpec | K::CutReport | K::FollowUp | K::Stewardship | K::HandOff | K::Resolution)
}
Self::Cut => matches!(kind, K::CutSpec | K::CutReport | K::Verdict | K::Finding | K::Resolution),
Self::Severity | Self::Confidence | Self::Origin => kind == K::Finding,
Self::Authority => kind == K::Ruling,
Self::ClaimOutcome => kind == K::Verdict,
Self::Outcome => kind == K::Resolution,
}
}
/// The closed set an enum-valued alias draws from.
fn closed(self) -> Option<&'static [&'static str]> {
match self {
Self::InForce => Some(&BOOLEANS),
Self::Faculty => Some(&FACULTIES),
Self::Severity => Some(&SEVERITIES),
Self::Confidence => Some(&CONFIDENCES),
Self::Origin => Some(&ORIGINS),
Self::Authority => Some(&AUTHORITIES),
Self::ClaimOutcome => Some(&CLAIM_OUTCOMES),
Self::Outcome => Some(&OUTCOMES),
Self::Root | Self::Repo | Self::Cut => None,
}
}
/// Whether `value` is in this alias's domain: the leaf's own grammar for
/// the three grammar-valued aliases, exact spelling for the closed sets.
fn admits(self, value: &str) -> bool {
match self {
Self::Root => Slug(value.into()).validate_slug().is_ok(),
Self::Repo => OrgRepo(value.into()).validate_org_repo().is_ok(),
Self::Cut => Label(value.into()).validate_label().is_ok(),
closed => closed.closed().is_some_and(|set| set.contains(&value)),
}
}
}
/// The root and the local of a key, by position in `<root>:<kind>:<local>`:
/// the leaf derives every key that way and its own test pins the three
/// segments. The only place the read side reads a key apart.
pub(crate) fn root_and_local(key: &str) -> (&str, &str) {
let mut segments = key.split(':');
let root = segments.next().unwrap_or_default();
let local = segments.nth(1).unwrap_or_default();
(root, local)
}
fn outcome_name(outcome: &ResolutionOutcome) -> &'static str {
match outcome {
ResolutionOutcome::Superseded { .. } => "Superseded",
ResolutionOutcome::Answered { .. } => "Answered",
ResolutionOutcome::Fixed { .. } => "Fixed",
ResolutionOutcome::Deferred { .. } => "Deferred",
ResolutionOutcome::Recorded { .. } => "Recorded",
ResolutionOutcome::Withdrawn { .. } => "Withdrawn",
}
}
/// One document as the evaluator sees it: its identity, its ordinal, what it
/// answers for each alias, its citation edges, and the view a page projects.
#[derive(Clone, Debug)]
pub(crate) struct SelectionRow {
pub(crate) kind: PipelineKind,
pub(crate) key: String,
ordinal: u64,
values: BTreeMap<&'static str, Vec<String>>,
refs: Vec<(CitationRole, PipelineRef)>,
pub(crate) view: PipelineDocumentView,
}
impl SelectionRow {
/// `base` is the first non-resolution the document reaches through its
/// subjects: the row a resolution's `repo` and `cut` are read from.
pub(crate) fn new(
held: &Held,
base: &Held,
view: PipelineDocumentView,
refs: Vec<(CitationRole, PipelineRef)>,
) -> Self {
use PipelineDocument as D;
let mut values: BTreeMap<&'static str, Vec<String>> = BTreeMap::new();
let mut put = |alias: Alias, found: Vec<String>| {
values.insert(alias.name(), found);
};
put(Alias::Root, vec![root_and_local(&held.key).0.to_string()]);
put(Alias::InForce, vec![matches!(view.status, crate::query::PipelineStatus::InForce).to_string()]);
put(Alias::Faculty, vec![format!("{:?}", view.admission.provenance.faculty)]);
put(
Alias::Repo,
match &base.document {
D::Campaign(campaign) => campaign.repos.iter().map(OrgRepo::identity).collect(),
D::CutSpec(spec) => vec![spec.repo.identity()],
D::CutReport(report) => vec![report.repo.identity()],
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(_) => {
Vec::new()
}
},
);
put(
Alias::Cut,
root_and_local(&base.key)
.1
.strip_prefix("cut-")
.and_then(|rest| rest.split_once('.'))
.map(|(label, _)| label.to_string())
.into_iter()
.collect(),
);
match &held.document {
D::Finding(finding) => {
put(Alias::Severity, vec![format!("{:?}", finding.severity)]);
put(Alias::Confidence, vec![format!("{:?}", finding.confidence)]);
put(Alias::Origin, vec![format!("{:?}", finding.origin)]);
}
D::Ruling(ruling) => put(Alias::Authority, vec![format!("{:?}", ruling.authority)]),
D::Verdict(verdict) => {
put(Alias::ClaimOutcome, verdict.claims.iter().map(|claim| format!("{:?}", claim.outcome)).collect())
}
D::Resolution(resolution) => put(Alias::Outcome, vec![outcome_name(&resolution.outcome).to_string()]),
_ => {}
}
Self { kind: held.kind, key: held.key.clone(), ordinal: view.admission.ordinal, values, refs, view }
}
}
impl Row for SelectionRow {
fn schema_id(&self) -> &str {
self.kind.type_id()
}
fn record_key(&self) -> &str {
&self.key
}
fn ordinal(&self) -> i64 {
self.ordinal as i64
}
fn values(&self, index: &str) -> Vec<String> {
self.values.get(index).cloned().unwrap_or_default()
}
fn number(&self, _index: &str) -> Option<String> {
None
}
fn references(&self) -> Vec<(String, String, Option<Vec<u8>>)> {
self.refs.iter().map(|(role, target)| (role.name().to_string(), target.id.0.clone(), None)).collect()
}
}
/// The organ's closed lists: thirteen schemas, eleven aliases, fifteen roles.
/// A schema's name is its id, so the substrate's alias matching resolves
/// nothing beyond exact ids.
pub(crate) struct Vocabulary;
impl RowSet for Vocabulary {
fn all_schema_ids(&self) -> Vec<String> {
PipelineKind::ALL.iter().map(|kind| kind.type_id().to_string()).collect()
}
fn schema_name(&self, schema_id: &str) -> Option<String> {
kind_of_type(schema_id).map(|kind| kind.type_id().to_string())
}
fn declared_indexes(&self, schema_id: &str) -> Vec<String> {
let Some(kind) = kind_of_type(schema_id) else { return Vec::new() };
Alias::ALL.into_iter().filter(|alias| alias.declared_on(kind)).map(|alias| alias.name().to_string()).collect()
}
fn declared_roles(&self, schema_id: &str) -> Vec<String> {
let Some(kind) = kind_of_type(schema_id) else { return Vec::new() };
CitationRole::ALL.into_iter().filter(|role| role.carrier() == kind).map(|role| role.name().to_string()).collect()
}
fn is_numeric(&self, _schema_id: &str, _index: &str) -> bool {
false
}
fn target_leaves(&self, role: &str) -> Vec<String> {
CitationRole::from_name(role)
.map(|role| role.target_kinds().into_iter().map(|kind| kind.type_id().to_string()).collect())
.unwrap_or_default()
}
}
fn invalid(field: impl Into<String>, value: &str, message: impl Into<String>) -> MindRefusal {
MindRefusal::SelectionInvalid { field: field.into(), value: Some(value.to_string()), message: message.into() }
}
/// The organ's door (S6), run after `cultnet_rs::validate` has accepted the
/// selection's names: it refuses a value the vocabulary's domains do not hold,
/// so a malformed filter never reads as an empty answer. It also returns the
/// selection with every `repo` value in the identity spelling the rows carry,
/// because `OrgRepo` identity is case-insensitive.
///
/// Refuses, naming the field the client sent: a `schemas` entry that is not
/// one of the thirteen ids; a `keys` entry that is not a full id whose kind
/// segment reads and passes the leaf's ref grammar; an `any_of` value outside
/// its alias's domain; and a `cites.target` whose key fails the ref grammar
/// for its schema's kind (V24's rule, at this door). Whether the target
/// schema id is known is the substrate's refusal and is not repeated here.
pub(crate) fn refuse_values(selection: &Selection) -> Result<Selection, MindRefusal> {
for schema in selection.schemas.iter().flatten() {
if kind_of_type(schema).is_none() {
return Err(invalid("schemas", schema, "no such schema in this mind"));
}
}
for key in selection.keys.iter().flatten() {
let Some(kind) = kind_of_id(key) else {
return Err(invalid("keys", key, "not a pipeline id: the kind segment does not read"));
};
if let Err(refusal) = (PipelineRef { kind, id: key.as_str().into() }).validate_ref() {
return Err(invalid("keys", key, format!("not a pipeline id: {refusal:?}")));
}
}
let mut canonical = selection.clone();
for (position, predicate) in canonical.fields.iter_mut().flatten().enumerate() {
let Some(alias) = Alias::from_name(&predicate.index) else { continue };
for value in predicate.values.iter_mut().flatten() {
if !alias.admits(value) {
return Err(invalid(
format!("fields[{position}].values"),
value,
match alias.closed() {
Some(set) => format!("{} takes one of {}", alias.name(), set.join(", ")),
None => format!("not a valid {}", alias.name()),
},
));
}
if alias == Alias::Repo {
*value = OrgRepo(value.clone()).identity();
}
}
}
if let Some(cites) = &selection.cites
&& let Some(kind) = kind_of_type(&cites.target.schema_id)
&& let Err(refusal) = (PipelineRef { kind, id: cites.target.record_key.as_str().into() }).validate_ref()
{
return Err(invalid("cites.target", &cites.target.record_key, format!("not a {} id: {refusal:?}", kind.name())));
}
Ok(canonical)
}

View file

@ -5,13 +5,14 @@
//! names a transport.
use cultcache_rs::DatabaseEntry;
use cultnet_rs::Selection;
use epiphany_pipeline::{PIPELINE_SCHEMA_EPOCH, PipelineKind, PipelineRef, Slug};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::admission::{PipelineAdmissionBatch, PipelineAdmissionOutcome};
use crate::mind::Mind;
use crate::query::{PipelineDocumentView, PipelineQuery, PipelineQueryPage};
use crate::query::{PipelineDocumentView, PipelineSelectionPage, SemanticQuery};
use crate::receipt::HuginnCommitReceipt;
use crate::refusal::MindRefusal;
use crate::store::MindStore;
@ -31,6 +32,15 @@ pub const MIND_REQUEST_SCHEMA_JSON: &str =
pub const MIND_RESPONSE_SCHEMA_JSON: &str =
include_str!("../../../schemas/cultnet/huginn.mind_response.v1.schema.json");
/// The request's `selection` is CultNet's, and its published schema is
/// CultLib's: a `$ref` to the `$id` it publishes, so one owner holds the
/// selection's shape and nothing here copies it.
fn selection_ref(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"$ref": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json"
})
}
/// Every operation a mind answers. `Admit` carries the batch whole because the
/// batch already names the instance and the asker; a second `instance` beside
/// it would be two declarations. Every read names the instance because ruling
@ -42,7 +52,12 @@ pub enum HuginnMindRequest {
Whoami,
Admit(PipelineAdmissionBatch),
View { instance: Slug, id: PipelineRef },
Query { instance: Slug, query: PipelineQuery },
Query {
instance: Slug,
#[schemars(schema_with = "selection_ref")]
selection: Selection,
semantic: Option<SemanticQuery>,
},
}
impl HuginnMindRequest {
@ -74,7 +89,7 @@ pub enum HuginnMindResponse {
Whoami(MindStatus),
Admit(PipelineAdmissionOutcome),
View(Option<PipelineDocumentView>),
Query(PipelineQueryPage),
Query(PipelineSelectionPage),
Refused(MindRefusal),
}
@ -127,6 +142,7 @@ impl<S: MindStore> Mind<S> {
#[cfg(test)]
mod tests {
use super::*;
use crate::query::PipelinePageItems;
use crate::fixtures::{CAMPAIGN, INSTANCE, OTHER_INSTANCE, instance, provenance, r, seeded, slug};
use crate::receipt::Faculty;
@ -140,7 +156,7 @@ mod tests {
HuginnMindRequest::Whoami,
HuginnMindRequest::Admit(batch),
HuginnMindRequest::View { instance: slug(INSTANCE), id: r(PipelineKind::Campaign, CAMPAIGN) },
HuginnMindRequest::Query { instance: slug(OTHER_INSTANCE), query: PipelineQuery::default() },
HuginnMindRequest::Query { instance: slug(OTHER_INSTANCE), selection: Selection::default(), semantic: None },
]
}
@ -152,7 +168,13 @@ mod tests {
HuginnMindResponse::Admit(PipelineAdmissionOutcome::Conflict { identities: vec![] }),
HuginnMindResponse::Admit(PipelineAdmissionOutcome::Refused(refusal.clone())),
HuginnMindResponse::View(None),
HuginnMindResponse::Query(PipelineQueryPage { items: vec![], matched: 0 }),
HuginnMindResponse::Query(PipelineSelectionPage {
matched: 0,
as_of: 1,
next: None,
items: PipelinePageItems::Headers(vec![]),
edges: None,
}),
HuginnMindResponse::Refused(refusal),
]
}
@ -195,5 +217,10 @@ mod tests {
let response = serde_json::to_string_pretty(&schemars::schema_for!(HuginnMindResponse)).unwrap();
assert_eq!(MIND_REQUEST_SCHEMA_JSON.replace("\r\n", "\n"), request);
assert_eq!(MIND_RESPONSE_SCHEMA_JSON.replace("\r\n", "\n"), response);
// The selection is CultLib's, referenced by the `$id` it publishes and
// not copied into this schema.
let selection = "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json";
assert!(request.contains(&format!("\"$ref\": \"{selection}\"")));
assert!(!request.contains("\"cursor\""), "no second copy of the selection's shape");
}
}

View file

@ -54,13 +54,23 @@
"instance": {
"$ref": "#/$defs/Slug"
},
"query": {
"$ref": "#/$defs/PipelineQuery"
"selection": {
"$ref": "https://github.com/GameCult/CultLib/contracts/cultnet/cultnet.selection.schema.json"
},
"semantic": {
"anyOf": [
{
"$ref": "#/$defs/SemanticQuery"
},
{
"type": "null"
}
]
}
},
"required": [
"instance",
"query"
"selection"
]
}
},
@ -1189,89 +1199,6 @@
"tool"
]
},
"PipelineQuery": {
"description": "The filters over one mind. Each is one rule, and an unset filter matches\neverything. The mind is the instance, so there is no `instance` field: the\ndaemon picks the mind before it calls here.",
"type": "object",
"properties": {
"campaign": {
"description": "The key's root segment: a campaign slug, or an instance for\nstewardships, hand-offs and the identity document.",
"anyOf": [
{
"$ref": "#/$defs/Slug"
},
{
"type": "null"
}
]
},
"cut": {
"anyOf": [
{
"$ref": "#/$defs/Label"
},
{
"type": "null"
}
]
},
"faculty": {
"description": "Attribution, ruling 18: it filters, it grants nothing.",
"anyOf": [
{
"$ref": "#/$defs/Faculty"
},
{
"type": "null"
}
]
},
"in_force": {
"type": [
"boolean",
"null"
]
},
"kinds": {
"description": "Empty matches every kind.",
"type": "array",
"items": {
"$ref": "#/$defs/PipelineKind"
}
},
"limit": {
"description": "`None` is `QUERY_LIMIT_MAX`; anything else is clamped into\n`1..=QUERY_LIMIT_MAX`.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"repo": {
"anyOf": [
{
"$ref": "#/$defs/OrgRepo"
},
{
"type": "null"
}
]
},
"semantic": {
"anyOf": [
{
"$ref": "#/$defs/SemanticQuery"
},
{
"type": "null"
}
]
}
},
"required": [
"kinds"
]
},
"PipelineQuestion": {
"type": "object",
"properties": {

View file

@ -50,7 +50,7 @@
"type": "object",
"properties": {
"Query": {
"$ref": "#/$defs/PipelineQueryPage"
"$ref": "#/$defs/PipelineSelectionPage"
}
},
"additionalProperties": false,
@ -73,7 +73,7 @@
],
"$defs": {
"AdmissionFacts": {
"description": "What admission recorded about a document when it landed: the receipt that\nwrote it, the `now` that receipt was taken at, who asked, and the\nreceipt's ordinal. `ordinal` is display-only here until RS-3 makes it the\nread side's order.",
"description": "What admission recorded about a document when it landed: the receipt that\nwrote it, the `now` that receipt was taken at, who asked, and the\nreceipt's ordinal, which is the read side's order and its snapshot\nposition.",
"type": "object",
"properties": {
"admitted_at": {
@ -153,6 +153,27 @@
"deletion_line"
]
},
"CitationRole": {
"description": "The referring field of a citation edge, one per leaf field that names\nanother document. The wire spelling is the field's own name, so a client\nreads an edge's role as the field it came from.",
"type": "string",
"enum": [
"raised_in",
"answers",
"rulings",
"questions",
"cut_spec",
"forks",
"cut_report",
"findings",
"verdict",
"source",
"subject",
"superseded_by",
"resolved_by",
"deferred_to",
"documents"
]
},
"ClaimOutcome": {
"type": "string",
"enum": [
@ -1209,6 +1230,58 @@
"ResponseTooLarge"
]
},
{
"description": "A selection the substrate or the organ's value door refused: `field`\nis the selection field the client sent, `value` the offending part.",
"type": "object",
"properties": {
"SelectionInvalid": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"message": {
"type": "string"
},
"value": {
"type": [
"string",
"null"
]
}
},
"required": [
"field",
"message"
]
}
},
"additionalProperties": false,
"required": [
"SelectionInvalid"
]
},
{
"description": "A cursor that does not decode, does not belong to this selection or\nwas not minted by this process, or names a snapshot this mind has not\nreached.",
"type": "object",
"properties": {
"CursorInvalid": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
}
},
"additionalProperties": false,
"required": [
"CursorInvalid"
]
},
{
"type": "object",
"properties": {
@ -1876,6 +1949,30 @@
}
]
},
"PipelineDocumentSummary": {
"description": "The organ's meaning of `projection: header`: a document's id, the facts of\nits admission, its status and the few fields of its kind a caller triages\nby. Derived per read and bounded by `SUMMARY_MAX_BYTES`.",
"type": "object",
"properties": {
"admission": {
"$ref": "#/$defs/AdmissionFacts"
},
"facts": {
"$ref": "#/$defs/PipelineFacts"
},
"id": {
"$ref": "#/$defs/PipelineRef"
},
"status": {
"$ref": "#/$defs/PipelineStatusSummary"
}
},
"required": [
"id",
"admission",
"status",
"facts"
]
},
"PipelineDocumentView": {
"description": "One document as a reader gets it: its id, the document, the facts of its\nadmission and its status. The id travels with the document because a\n`PipelineDocument` does not carry its key and a client must never derive\none.",
"type": "object",
@ -1900,6 +1997,587 @@
"status"
]
},
"PipelineEdge": {
"description": "One citation edge a hop traversed: the organ's typing of the substrate's\nedge, whose ends are record keys and whose role is a string.",
"type": "object",
"properties": {
"from": {
"$ref": "#/$defs/PipelineRef"
},
"role": {
"$ref": "#/$defs/CitationRole"
},
"to": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"from",
"role",
"to"
]
},
"PipelineFacts": {
"description": "The triage fields of each kind, in the leaf's own envelope spelling. Prose\n(`Para`) and unbounded lists are not carried; `finding.claim` and\n`follow_up.item` are, because the leaf typed them as one-line statements.",
"oneOf": [
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "campaign"
},
"value": {
"type": "object",
"properties": {
"repos": {
"type": "array",
"items": {
"$ref": "#/$defs/OrgRepo"
}
},
"title": {
"$ref": "#/$defs/Title"
}
},
"required": [
"title",
"repos"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "target"
},
"value": {
"type": "object",
"properties": {
"invariants": {
"type": "array",
"items": {
"$ref": "#/$defs/Label"
}
},
"revision": {
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"revision",
"invariants"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "question"
},
"value": {
"type": "object",
"properties": {
"asked_on": {
"$ref": "#/$defs/Date"
},
"label": {
"$ref": "#/$defs/Label"
},
"options": {
"type": "array",
"items": {
"$ref": "#/$defs/Label"
}
},
"raised_in": {
"anyOf": [
{
"$ref": "#/$defs/PipelineRef"
},
{
"type": "null"
}
]
},
"recommended": {
"$ref": "#/$defs/Label"
},
"title": {
"$ref": "#/$defs/Title"
}
},
"required": [
"label",
"title",
"options",
"recommended",
"asked_on"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "ruling"
},
"value": {
"type": "object",
"properties": {
"answers": {
"anyOf": [
{
"$ref": "#/$defs/Short"
},
{
"type": "null"
}
]
},
"authority": {
"$ref": "#/$defs/RulingAuthority"
},
"choice": {
"anyOf": [
{
"$ref": "#/$defs/Label"
},
{
"type": "null"
}
]
},
"label": {
"$ref": "#/$defs/Label"
},
"ruled_on": {
"$ref": "#/$defs/Date"
},
"title": {
"$ref": "#/$defs/Title"
}
},
"required": [
"label",
"title",
"authority",
"ruled_on"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "cut_spec"
},
"value": {
"type": "object",
"properties": {
"base": {
"$ref": "#/$defs/Sha"
},
"branch": {
"$ref": "#/$defs/Short"
},
"cut": {
"$ref": "#/$defs/Label"
},
"depends_on": {
"type": "array",
"items": {
"$ref": "#/$defs/Short"
}
},
"repo": {
"$ref": "#/$defs/OrgRepo"
},
"revision": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"title": {
"$ref": "#/$defs/Title"
}
},
"required": [
"cut",
"revision",
"title",
"repo",
"branch",
"base",
"depends_on"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "cut_report"
},
"value": {
"type": "object",
"properties": {
"attempt": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"branch": {
"$ref": "#/$defs/Short"
},
"cut_spec": {
"$ref": "#/$defs/Short"
},
"range": {
"$ref": "#/$defs/CommitRange"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
"cut_spec",
"attempt",
"repo",
"branch",
"range"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "verdict"
},
"value": {
"type": "object",
"properties": {
"cut_report": {
"$ref": "#/$defs/Short"
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/$defs/ClaimOutcome"
}
},
"pass": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"range": {
"$ref": "#/$defs/CommitRange"
}
},
"required": [
"cut_report",
"pass",
"range",
"outcomes"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "finding"
},
"value": {
"type": "object",
"properties": {
"claim": {
"$ref": "#/$defs/Line"
},
"confidence": {
"$ref": "#/$defs/FindingConfidence"
},
"invariants": {
"type": "array",
"items": {
"$ref": "#/$defs/Label"
}
},
"label": {
"$ref": "#/$defs/Label"
},
"origin": {
"$ref": "#/$defs/FindingOrigin"
},
"range": {
"$ref": "#/$defs/CommitRange"
},
"severity": {
"$ref": "#/$defs/FindingSeverity"
},
"verdict": {
"$ref": "#/$defs/Short"
}
},
"required": [
"verdict",
"label",
"confidence",
"severity",
"origin",
"claim",
"invariants",
"range"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "follow_up"
},
"value": {
"type": "object",
"properties": {
"item": {
"$ref": "#/$defs/Line"
},
"label": {
"$ref": "#/$defs/Label"
},
"owner": {
"$ref": "#/$defs/Short"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
},
"source": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"label",
"source",
"repo",
"owner",
"item"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "resolution"
},
"value": {
"type": "object",
"properties": {
"outcome": {
"$ref": "#/$defs/ResolutionOutcome"
},
"resolved_on": {
"$ref": "#/$defs/Date"
},
"sequence": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"subject": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"subject",
"sequence",
"outcome",
"resolved_on"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "instance"
},
"value": {
"type": "object",
"properties": {
"created_at": {
"$ref": "#/$defs/Date"
},
"display_name": {
"$ref": "#/$defs/Short"
},
"host": {
"$ref": "#/$defs/Short"
},
"instance": {
"$ref": "#/$defs/Slug"
}
},
"required": [
"instance",
"display_name",
"host",
"created_at"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "stewardship"
},
"value": {
"type": "object",
"properties": {
"assigned_on": {
"$ref": "#/$defs/Date"
},
"instance": {
"$ref": "#/$defs/Slug"
},
"note": {
"$ref": "#/$defs/Line"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
},
"sequence": {
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"instance",
"repo",
"sequence",
"assigned_on",
"note"
]
}
},
"required": [
"kind",
"value"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "hand_off"
},
"value": {
"type": "object",
"properties": {
"from_instance": {
"$ref": "#/$defs/Slug"
},
"handed_on": {
"$ref": "#/$defs/Date"
},
"repo": {
"$ref": "#/$defs/OrgRepo"
},
"to_instance": {
"$ref": "#/$defs/Slug"
}
},
"required": [
"from_instance",
"to_instance",
"repo",
"handed_on"
]
}
},
"required": [
"kind",
"value"
]
}
]
},
"PipelineFinding": {
"type": "object",
"properties": {
@ -2096,6 +2774,41 @@
"HandOff"
]
},
"PipelinePageItems": {
"description": "The items of a page, chosen once by the selection's `projection`.",
"oneOf": [
{
"type": "object",
"properties": {
"Headers": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineDocumentSummary"
}
}
},
"additionalProperties": false,
"required": [
"Headers"
]
},
{
"type": "object",
"properties": {
"Documents": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineDocumentView"
}
}
},
"additionalProperties": false,
"required": [
"Documents"
]
}
]
},
"PipelineProvenance": {
"description": "Who admitted a batch: the faculty, the agent, its session and the tool.\nA field on the receipt and nothing else; the digest excludes it.",
"type": "object",
@ -2120,27 +2833,6 @@
"tool"
]
},
"PipelineQueryPage": {
"description": "One page of a query, with the number of documents that matched before the\ncap, so a short page is never mistaken for the whole answer.",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/$defs/PipelineDocumentView"
}
},
"matched": {
"type": "integer",
"format": "uint32",
"minimum": 0
}
},
"required": [
"items",
"matched"
]
},
"PipelineQuestion": {
"type": "object",
"properties": {
@ -2446,6 +3138,45 @@
"authority"
]
},
"PipelineSelectionPage": {
"description": "One page of a selection. `matched` is the count before paging, `as_of` the\nsnapshot the page and every cursor of its walk are exact as of, and `edges`\nis present exactly when the selection follows a hop.",
"type": "object",
"properties": {
"as_of": {
"type": "integer",
"format": "uint64",
"minimum": 0
},
"edges": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/PipelineEdge"
}
},
"items": {
"$ref": "#/$defs/PipelinePageItems"
},
"matched": {
"type": "integer",
"format": "uint32",
"minimum": 0
},
"next": {
"type": [
"string",
"null"
]
}
},
"required": [
"matched",
"as_of",
"items"
]
},
"PipelineStatus": {
"description": "Whether a document still stands, and if not, what closed it. Derived at\nread time; no document carries a status field and no admission writes one.\nThe whole closing record travels, not only its outcome, because a reader\nrehydrating wants why.",
"oneOf": [
@ -2481,6 +3212,41 @@
}
]
},
"PipelineStatusSummary": {
"description": "Whether a summarised document stands: the closing record's outcome, not its\nrationale. A caller who wants why opens the resolution.",
"oneOf": [
{
"type": "string",
"enum": [
"InForce"
]
},
{
"type": "object",
"properties": {
"Resolved": {
"type": "object",
"properties": {
"outcome": {
"$ref": "#/$defs/ResolutionOutcome"
},
"resolution": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"resolution",
"outcome"
]
}
},
"additionalProperties": false,
"required": [
"Resolved"
]
}
]
},
"PipelineStewardship": {
"description": "Stewardship over a repo, as an assignment recorded in a mind. One\ninstance may steward several repos, so the repo is part of the key.\n`sequence` is per `(instance, repo)`, set by the writer, `1` for the\nfirst, so a repo transferred away and back is two records under one\nprefix rather than one document overwriting the other. `assigned_on`\nis a field, not a key part. As on a resolution, whether the sequence\nis the previous plus one, and which assignment is in force, are\nadmission's rules and not this crate's.",
"type": "object",