Desk reads: Mind::desk_forks and desk_fork project card, record and entry at one snapshot
Newest first by admission ordinal, cursor strictly below, counts over the showable set, limit 0 is counts and stamp. Whole record to the peer-checked socket; the trust boundary is pinned in the desk_door module doc and the DeskRequest/DeskResponse docs. Published schemas regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a37bd4f85f
commit
707642bd54
6 changed files with 469 additions and 11 deletions
|
|
@ -9,6 +9,17 @@
|
|||
//! read and write spends what is left of it, so a peer that trickles bytes is cut off at the
|
||||
//! deadline like one that is silent), a reply past `DESK_MAX_RESPONSE_BYTES` refused by name.
|
||||
//!
|
||||
//! The trust boundary of the reads (ruling `desk-read-visibility-whole-record`). The peer-uid check
|
||||
//! IS the whole authentication: Huginn must be reachable only by Bifrost's peer-checked socket, so
|
||||
//! the configured uid is Bifrost's and no other process's. `Forks` and `Fork` return the WHOLE record
|
||||
//! (cards, outcomes, every decider's note) to whoever passes that check; nothing in Huginn strips a
|
||||
//! note or gates a read on `subject` or `deciders` (`deciders` only fills the card and the
|
||||
//! showability gate). Bifrost applies visibility at its own door (a stripped read serves `note: null`,
|
||||
//! CultLib `fork-card.md`). A second reader of this socket is therefore a note leak, and a deployment
|
||||
//! that gives the socket to any uid but Bifrost's has no reader check left. The reads carry no
|
||||
//! `authority` (a payload that names one is refused whole), and the door has no other way to ask who
|
||||
//! is reading.
|
||||
//!
|
||||
//! It owns no rule. Which Mind method answers is `Daemon::handle_desk`'s; when the door is
|
||||
//! served is `serve::run`'s, once per pass in the thread that owns the Mind. A peer that is
|
||||
//! slow costs the loop at most `DESK_ACCEPTS_PER_PASS` connections of `DESK_CONNECTION_DEADLINE`
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ pub fn options_hash(question: &PipelineQuestion) -> Sha256Hex {
|
|||
}
|
||||
|
||||
/// Every desk answer of one fork, with its key.
|
||||
fn answers_of<'a>(docs: &'a Docs<'_>, fork: &'a str) -> impl Iterator<Item = (&'a str, &'a PipelineDeskAnswer)> + 'a {
|
||||
pub(crate) fn answers_of<'a>(docs: &'a Docs<'_>, fork: &'a str) -> impl Iterator<Item = (&'a str, &'a PipelineDeskAnswer)> + 'a {
|
||||
docs.of_kind(PipelineKind::DeskAnswer).filter_map(move |(key, document)| match document {
|
||||
PipelineDocument::DeskAnswer(answer) if answer.fork.0 == fork => Some((key, answer)),
|
||||
_ => None,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
//! `desk-read-visibility-whole-record`). Nothing is stored.
|
||||
|
||||
use super::*;
|
||||
use crate::desk::answers_of;
|
||||
use crate::desk::options_hash;
|
||||
|
||||
/// The card document's schema id.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
@ -223,22 +225,135 @@ impl JsonSchema for ForkLimit {
|
|||
}
|
||||
}
|
||||
|
||||
/// The card of a question held at the snapshot `docs` reads, shown to the call's `deciders`.
|
||||
fn card_of(docs: &Docs<'_>, key: &str, question: &PipelineQuestion, deciders: &[Short]) -> Result<ForkCard, MindRefusal> {
|
||||
let open = docs.in_force(PipelineKind::Question, key);
|
||||
// The ruling that closed the fork: the in-force one that answers it (the key order picks if a second ever does).
|
||||
let ruling = (!open)
|
||||
.then(|| {
|
||||
docs.of_kind(PipelineKind::Ruling)
|
||||
.filter(|(ruling_key, document)| {
|
||||
matches!(document, PipelineDocument::Ruling(ruling) if ruling.answers.as_ref().is_some_and(|answers| answers.0 == key))
|
||||
&& docs.in_force(PipelineKind::Ruling, ruling_key)
|
||||
})
|
||||
.map(|(ruling_key, _)| ruling_key.to_string())
|
||||
.min()
|
||||
})
|
||||
.flatten();
|
||||
Ok(ForkCard {
|
||||
schema_version: CardSchema::V0,
|
||||
fork_id: key.into(),
|
||||
title: question.title.0.clone(),
|
||||
context: question.question.0.clone(),
|
||||
options: question.options.iter().map(|option| ForkOption { id: option.label.0.clone(), text: option.text.0.clone() }).collect(),
|
||||
recommendation: question.recommended.0.clone(),
|
||||
depends: question.depends.iter().map(|line| line.0.clone()).collect(),
|
||||
leans: None,
|
||||
flag: None,
|
||||
raised_in: question.raised_in.as_ref().map(|raised| ForkRef { kind: format!("{:?}", raised.kind), id: raised.id.0.clone() }),
|
||||
deciders: deciders.iter().map(|decider| decider.0.clone()).collect(),
|
||||
status: if open { CardStatus::Open } else { CardStatus::Closed },
|
||||
ruling,
|
||||
ruled_from: Vec::new(),
|
||||
order: docs.ordinal_of(PipelineKind::Question, key).ok_or_else(unreadable_key)?,
|
||||
options_hash: options_hash(question).0,
|
||||
})
|
||||
}
|
||||
|
||||
/// The record of a fork: its answers in admission order and the answer that counts at this snapshot.
|
||||
fn record_of(docs: &Docs<'_>, key: &str, options_hash: &str) -> Result<ForkRecord, MindRefusal> {
|
||||
let mut stored: Vec<_> = answers_of(docs, key).collect();
|
||||
stored.sort_by_key(|(_, answer)| answer.sequence);
|
||||
let answers = stored.into_iter().map(|(answer_key, answer)| project(answer_key, answer)).collect::<Result<Vec<_>, _>>()?;
|
||||
let counted_answer_id = counted_answer(docs, key).map(|counted| answer_seq(key, &counted).map(n_text).ok_or_else(unreadable_key)).transpose()?;
|
||||
Ok(ForkRecord { schema_version: RecordSchema::V0, fork_id: key.into(), options_hash: options_hash.into(), counted_answer_id, answers })
|
||||
}
|
||||
|
||||
fn entry_of(docs: &Docs<'_>, key: &str, question: &PipelineQuestion, deciders: &[Short]) -> Result<ForkEntry, MindRefusal> {
|
||||
let card = card_of(docs, key, question, deciders)?;
|
||||
let record = record_of(docs, key, &card.options_hash)?;
|
||||
Ok(ForkEntry { schema_version: EntrySchema::V0, card, record })
|
||||
}
|
||||
|
||||
impl<S: MindStore> Mind<S> {
|
||||
/// STUB
|
||||
pub fn desk_forks(&self, _deciders: &[Short], _status: Option<ListStatus>, _cursor: Option<ForkCursor>, _limit: ForkLimit) -> DeskResponse {
|
||||
DeskResponse::NotStored(fork_unknown())
|
||||
/// One page of the desk's forks, read at one snapshot (`as_of`, also the page's stamp `s<as_of>`).
|
||||
///
|
||||
/// The showable forks only: a question `showable` refuses (fewer than two options, a blank option
|
||||
/// or depends line, no decider) is in no count and no page. `open_count` and `recorded_count` cover
|
||||
/// that whole set whatever `status`, `cursor` and `limit` ask. The page is the forks newest first by
|
||||
/// `order` (the question's admission ordinal), those with the asked `status` (`None` is both) and
|
||||
/// an order strictly below the cursor's; `limit` 0 returns no entries. `next_cursor` names the last
|
||||
/// order returned and is `None` when no further fork matches. Several questions admitted in one
|
||||
/// commit share an order, and a page that ends between them skips the rest (the contract's cursor
|
||||
/// has no tie-break).
|
||||
///
|
||||
/// The call is handed the call's `deciders` and no subject: they fill each card's `deciders` and
|
||||
/// feed the showability gate, and gate nothing else. The record is whole, notes included
|
||||
/// (ruling `desk-read-visibility-whole-record`: Bifrost applies visibility, Huginn strips nothing).
|
||||
pub fn desk_forks(&self, deciders: &[Short], status: Option<ListStatus>, cursor: Option<ForkCursor>, limit: ForkLimit) -> DeskResponse {
|
||||
self.forks_page(deciders, status, cursor, limit).unwrap_or_else(door_refusal)
|
||||
}
|
||||
|
||||
/// STUB
|
||||
pub fn desk_fork(&self, _deciders: &[Short], _fork_id: &str) -> DeskResponse {
|
||||
DeskResponse::NotStored(fork_unknown())
|
||||
/// One fork, as the entry a page carries, read at one snapshot. A fork that is not held, is not a
|
||||
/// question or is not showable is `fork_unknown`, the answer path's refusal. Whole record, as `desk_forks`.
|
||||
pub fn desk_fork(&self, deciders: &[Short], fork_id: &str) -> DeskResponse {
|
||||
self.fork_entry(deciders, fork_id).unwrap_or_else(door_refusal)
|
||||
}
|
||||
|
||||
fn forks_page(&self, deciders: &[Short], status: Option<ListStatus>, cursor: Option<ForkCursor>, limit: ForkLimit) -> Result<DeskResponse, MindRefusal> {
|
||||
let image = self.image()?;
|
||||
let docs = Docs::at(image, image.head);
|
||||
let mut shown = Vec::new();
|
||||
for (key, document) in docs.of_kind(PipelineKind::Question) {
|
||||
let PipelineDocument::Question(question) = document else { continue };
|
||||
if showable(question, deciders) {
|
||||
let order = docs.ordinal_of(PipelineKind::Question, key).ok_or_else(unreadable_key)?;
|
||||
shown.push((order, key, question, docs.in_force(PipelineKind::Question, key)));
|
||||
}
|
||||
}
|
||||
// Newest first; the key breaks a tie so that a page is the same page on every read.
|
||||
shown.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| right.1.cmp(left.1)));
|
||||
let open_count = shown.iter().filter(|fork| fork.3).count() as u64;
|
||||
let recorded_count = shown.len() as u64 - open_count;
|
||||
let mut matching = shown.iter().filter(|(order, _, _, open)| {
|
||||
cursor.is_none_or(|cursor| *order < cursor.order())
|
||||
&& match status {
|
||||
None => true,
|
||||
Some(ListStatus::Open) => *open,
|
||||
Some(ListStatus::Recorded) => !*open,
|
||||
}
|
||||
});
|
||||
let page: Vec<_> = matching.by_ref().take(limit.get() as usize).collect();
|
||||
let next_cursor = if matching.next().is_some() { page.last().and_then(|(order, ..)| ForkCursor::new(*order)) } else { None };
|
||||
let forks = page.into_iter().map(|(_, key, question, _)| entry_of(&docs, key, question, deciders)).collect::<Result<Vec<_>, _>>()?;
|
||||
let list = ForkList {
|
||||
schema_version: ListSchema::V0,
|
||||
mind: self.instance().0.clone(),
|
||||
stamp: format!("s{}", image.head),
|
||||
open_count,
|
||||
recorded_count,
|
||||
forks,
|
||||
next_cursor,
|
||||
};
|
||||
Ok(DeskResponse::Forks { list, as_of: image.head })
|
||||
}
|
||||
|
||||
fn fork_entry(&self, deciders: &[Short], fork_id: &str) -> Result<DeskResponse, MindRefusal> {
|
||||
let image = self.image()?;
|
||||
let docs = Docs::at(image, image.head);
|
||||
let Some(PipelineDocument::Question(question)) = docs.in_image(PipelineKind::Question, fork_id) else {
|
||||
return Ok(DeskResponse::NotStored(fork_unknown()));
|
||||
};
|
||||
if !showable(question, deciders) {
|
||||
return Ok(DeskResponse::NotStored(fork_unknown()));
|
||||
}
|
||||
Ok(DeskResponse::Fork { entry: entry_of(&docs, fork_id, question, deciders)?, as_of: image.head })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::desk::options_hash;
|
||||
use crate::envelope::{decode_desk_request, encode_desk_request, encode_desk_response};
|
||||
use crate::fixtures::*;
|
||||
use crate::store::test_stores::MemoryStore;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@ impl<'i> Docs<'i> {
|
|||
self.image.id_of(kind, id).filter(|row| self.visible(*row)).map(|row| &self.image.row(row).view.document)
|
||||
}
|
||||
|
||||
/// The ordinal of the receipt that wrote an image document: its admission order.
|
||||
pub(crate) fn ordinal_of(&self, kind: PipelineKind, id: &str) -> Option<u64> {
|
||||
self.image.id_of(kind, id).filter(|row| self.visible(*row)).map(|row| self.image.row(row).ordinal)
|
||||
}
|
||||
|
||||
pub(crate) fn find(&self, kind: PipelineKind, id: &str) -> Option<&PipelineDocument> {
|
||||
self.in_batch(kind, id).or_else(|| self.in_image(kind, id))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "DeskRequest",
|
||||
"description": "Every request the desk door answers. The fields are the wire names: the `mind` addressed\n(the persona and pipeline requests say `instance`; the sets are separate), the verified\n`subject` and `authority` Bifrost vouches for (door context, never body fields), and the\n`deciders` of this call (1 to 64, distinct; the Mind never stores them). Closed: a key outside the set refuses\nthe whole payload. The reads answer in follow-up desk-wire-reads.",
|
||||
"description": "Every request the desk door answers. The fields are the wire names: the `mind` addressed\n(the persona and pipeline requests say `instance`; the sets are separate), the verified\n`subject` and `authority` Bifrost vouches for (door context, never body fields), and the\n`deciders` of this call (1 to 64, distinct; the Mind never stores them). Closed: a key outside\nthe set refuses the whole payload.\n\nThe reads, `Forks` and `Fork`, carry NO `authority` (CultLib `list_request` and\n`read_request` are closed and have none; one that names it is refused whole). Their `subject`\nand `deciders` gate nothing: `deciders` only fills the card's `deciders` and the showability\ngate, `subject` is read by no read. `Forks` takes the contract's `list_request` fields flat\nbeside them (`status`, `cursor`, `limit`), each key written, `null` where absent. Huginn\nanswers every read with the whole record and Bifrost applies visibility; see `DeskResponse`\nfor the trust boundary. These docs and the doc of `Mind::desk_forks`/`Mind::desk_fork` are the\nreads spec.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
|
|
@ -52,6 +52,10 @@
|
|||
"Forks": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cursor": {
|
||||
"type": "string",
|
||||
"pattern": "^c[1-9][0-9]*$"
|
||||
},
|
||||
"deciders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -61,9 +65,20 @@
|
|||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"limit": {
|
||||
"$ref": "#/$defs/ForkLimit"
|
||||
},
|
||||
"mind": {
|
||||
"$ref": "#/$defs/Slug"
|
||||
},
|
||||
"status": {
|
||||
"description": "Which forks a list asks for; absent is both. `recorded` is a closed card.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"open",
|
||||
"recorded"
|
||||
]
|
||||
},
|
||||
"subject": {
|
||||
"$ref": "#/$defs/Short"
|
||||
}
|
||||
|
|
@ -72,7 +87,10 @@
|
|||
"required": [
|
||||
"mind",
|
||||
"subject",
|
||||
"deciders"
|
||||
"deciders",
|
||||
"status",
|
||||
"cursor",
|
||||
"limit"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -185,6 +203,11 @@
|
|||
"request_id"
|
||||
]
|
||||
},
|
||||
"ForkLimit": {
|
||||
"type": "integer",
|
||||
"maximum": 50,
|
||||
"minimum": 0
|
||||
},
|
||||
"RequestSchema": {
|
||||
"description": "The request document's schema id; any other value fails to decode.",
|
||||
"type": "string",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "DeskResponse",
|
||||
"description": "What the door answers one desk request with. `Answered` is a stored answer, refused ones\nincluded (a stale fork, a non-decider and the rest are stored), fresh or replayed alike.\n`NotStored` is a request that stored nothing. `Refused` is any other Mind refusal; an\n`Unavailable` is Bifrost's to turn into its 503, and Huginn writes no document for it.",
|
||||
"description": "What the door answers one desk request with. `Answered` is a stored answer, refused ones\nincluded (a stale fork, a non-decider and the rest are stored), fresh or replayed alike.\n`Forks` and `Fork` are the reads: one page of entries, or one entry, with the head they were\nall read at. `NotStored` is a request that stored nothing (an unknown fork included).\n`Refused` is any other Mind refusal; an `Unavailable` is Bifrost's to turn into its 503, and\nHuginn writes no document for it.\n\nThe trust boundary of the reads (ruling `desk-read-visibility-whole-record`): `Forks` and `Fork`\ncarry the WHOLE record, every answer's note included, to whatever the peer-checked socket\ncarries. Huginn strips nothing and gates nothing; Bifrost applies visibility at its door (a\nstripped read serves `note: null`, CultLib `fork-card.md`). Huginn must therefore be reachable\nonly by Bifrost's peer-checked socket: the peer-uid check IS the whole authentication, and a\nsecond reader of that socket is a note leak.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
|
|
@ -35,6 +35,58 @@
|
|||
"Answered"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Forks": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"as_of": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"list": {
|
||||
"$ref": "#/$defs/ForkList"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"list",
|
||||
"as_of"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Forks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Fork": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"as_of": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"entry": {
|
||||
"$ref": "#/$defs/ForkEntry"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"entry",
|
||||
"as_of"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"Fork"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -68,6 +120,21 @@
|
|||
"gamecult.desk_answer.v0"
|
||||
]
|
||||
},
|
||||
"CardSchema": {
|
||||
"description": "The card document's schema id.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"gamecult.fork.card.v0"
|
||||
]
|
||||
},
|
||||
"CardStatus": {
|
||||
"description": "Whether a ruling closed the fork.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"open",
|
||||
"closed"
|
||||
]
|
||||
},
|
||||
"DeskAuthority": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
|
@ -76,6 +143,114 @@
|
|||
"Member"
|
||||
]
|
||||
},
|
||||
"EntrySchema": {
|
||||
"description": "The entry document's schema id.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"gamecult.fork.entry.v0"
|
||||
]
|
||||
},
|
||||
"ForkCard": {
|
||||
"description": "`gamecult.fork.card.v0`: closed, her field order.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "string"
|
||||
},
|
||||
"deciders": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"depends": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"flag": {
|
||||
"type": "string"
|
||||
},
|
||||
"fork_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"leans": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ForkOption"
|
||||
}
|
||||
},
|
||||
"options_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"raised_in": {
|
||||
"description": "A typed reference, as the question holds it (`raised_in`).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"recommendation": {
|
||||
"type": "string"
|
||||
},
|
||||
"ruled_from": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ruling": {
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"$ref": "#/$defs/CardSchema"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/$defs/CardStatus"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"fork_id",
|
||||
"title",
|
||||
"context",
|
||||
"options",
|
||||
"recommendation",
|
||||
"depends",
|
||||
"leans",
|
||||
"flag",
|
||||
"raised_in",
|
||||
"deciders",
|
||||
"status",
|
||||
"ruling",
|
||||
"ruled_from",
|
||||
"order",
|
||||
"options_hash"
|
||||
]
|
||||
},
|
||||
"ForkDeskAnswer": {
|
||||
"description": "`gamecult.desk_answer.v0`: one stored answer as the desk shows it, her field order,\n`request_id` last. Every value is a stored fact; `answer_id`, `of` and `retracts` are the\nrecord keys in her `n<seq>` spelling.",
|
||||
"type": "object",
|
||||
|
|
@ -158,6 +333,121 @@
|
|||
"request_id"
|
||||
]
|
||||
},
|
||||
"ForkEntry": {
|
||||
"description": "`gamecult.fork.entry.v0`: a card and its record.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {
|
||||
"$ref": "#/$defs/ForkCard"
|
||||
},
|
||||
"record": {
|
||||
"$ref": "#/$defs/ForkRecord"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"$ref": "#/$defs/EntrySchema"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"card",
|
||||
"record"
|
||||
]
|
||||
},
|
||||
"ForkList": {
|
||||
"description": "`gamecult.fork.list.v0`: a page of entries, the counts over the showable set and the stamp.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"forks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ForkEntry"
|
||||
}
|
||||
},
|
||||
"mind": {
|
||||
"type": "string"
|
||||
},
|
||||
"next_cursor": {
|
||||
"type": "string",
|
||||
"pattern": "^c[1-9][0-9]*$"
|
||||
},
|
||||
"open_count": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"recorded_count": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"schemaVersion": {
|
||||
"$ref": "#/$defs/ListSchema"
|
||||
},
|
||||
"stamp": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"mind",
|
||||
"stamp",
|
||||
"open_count",
|
||||
"recorded_count",
|
||||
"forks",
|
||||
"next_cursor"
|
||||
]
|
||||
},
|
||||
"ForkOption": {
|
||||
"description": "One shown option.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"ForkRecord": {
|
||||
"description": "`gamecult.fork.record.v0`: the answers in admission order and the derived counted answer.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ForkDeskAnswer"
|
||||
}
|
||||
},
|
||||
"counted_answer_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"fork_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"options_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"schemaVersion": {
|
||||
"$ref": "#/$defs/RecordSchema"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"fork_id",
|
||||
"options_hash",
|
||||
"counted_answer_id",
|
||||
"answers"
|
||||
]
|
||||
},
|
||||
"ForkRefusal": {
|
||||
"description": "`gamecult.fork.refusal.v0`. `error` is a fixed sentence, never an echo of what was sent;\nfor `body_invalid` it names the field.",
|
||||
"type": "object",
|
||||
|
|
@ -188,6 +478,13 @@
|
|||
"request_id_conflict"
|
||||
]
|
||||
},
|
||||
"ListSchema": {
|
||||
"description": "The list document's schema id.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"gamecult.fork.list.v0"
|
||||
]
|
||||
},
|
||||
"MindRefusal": {
|
||||
"description": "Every way a mind refuses. `Document` wraps the leaf's four (bounds,\nformats, key identity, foreign type); the rest are the organ's: store\nidentity, epoch, ownership, batch shape, references, and every cross-field\nand cross-document rule of admission.\n\nThe two sequenced kinds refuse in the same pair of shapes: an\n`OutOfSequence` when the writer's `sequence` is not the previous plus one,\nand `AlreadyResolved`/`AlreadyStewarded` when a record of that scope is\nstill in force.",
|
||||
"oneOf": [
|
||||
|
|
@ -1497,6 +1794,13 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"RecordSchema": {
|
||||
"description": "The record document's schema id.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"gamecult.fork.record.v0"
|
||||
]
|
||||
},
|
||||
"RefusalSchema": {
|
||||
"description": "The refusal document's schema id.",
|
||||
"type": "string",
|
||||
|
|
|
|||
Loading…
Reference in a new issue