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:
parent
511afa2f44
commit
eb57bdba40
13 changed files with 2360 additions and 522 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -468,6 +468,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"chrono",
|
||||
"cultcache-rs",
|
||||
"cultnet-rs",
|
||||
"epiphany-pipeline",
|
||||
"rmp-serde",
|
||||
"schemars",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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, ®istry, 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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 { .. }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
336
crates/huginn-mind/src/rows.rs
Normal file
336
crates/huginn-mind/src/rows.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue