Huginn/crates/eureka-pipeline/src/lib.rs

4688 lines
244 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Eureka pipeline documents: the typed shape of a Eureka campaign's state.
//!
//! This is a leaf type library, and deliberately nothing else. It owns document
//! shape, field bounds, formats, key derivation and the JSON schemas published
//! under `schemas/cultnet`; it owns no storage, no admission, no process and no
//! network. That is the whole reason it is a package: the organ that will admit
//! these documents depends on these types without depending on the harness
//! that used to hold them.
//!
//! Every kind is a plain `serde` + `JsonSchema` value inside a one-slot
//! `DatabaseEntry` wrapper, always prepared with `prepare_entry_named`, so the
//! stored payload is `[value]` and the value is the named map the published
//! schema describes. Keys are semantic: `pipeline_key` derives them from the
//! value, and the write validator refuses any envelope whose key differs.
//!
//! Every key is `<root>:<kind>:<local>`, three segments for every kind with
//! none excused: the root is a `Slug`, the kind is the literal kind name, and
//! the local is `Label`s joined by `.`, so no local part carries a dot and a
//! reader recovers the parts by splitting. A root's local is the constant
//! `self`; a resolution's local is its subject's kind and local. A
//! resolution's local ends in its per-subject sequence, `n<N>`, and a
//! stewardship's in its per-repo sequence, so a subject's resolutions and a
//! repo's assignments each share a prefix.
//!
//! **Validation follows the type.** `value_types!` is the single field list: it
//! emits the struct and its `Bounded` impl from the same tokens, so a field
//! cannot be declared without being validated. Format rules live in the field's
//! type (`Label`, `Slug`, `OrgRepo`, `Sha`, `Sha256Hex`, `Date`), never in a
//! hand-written impl that a later field can slip past. A `Vec` field must carry
//! a maximum, because `Vec<T>` has no `Bounded` impl of its own. A field added
//! at the epoch carries the `= absent` marker, which defaults it on read and
//! omits it on write when empty.
//!
//! The wrappers are crate-private: outside code registers, prepares and
//! decodes them through `register_pipeline_document_types`,
//! `PipelineDocument::prepare` and `PipelineDocument::decode`, and validates a
//! write through `validate_pipeline_write_envelope`.
// `anyhow` is the cache's own error type, which `prepare` and the registrar
// pass through; every refusal this crate decides is a `PipelineRefusal`.
use anyhow::Result;
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Typed refusals of the pipeline documents: bounds, formats and key identity
/// (D2's document half), and `ForeignStore`, raised by `decode` for an
/// envelope of any other type. This is every refusal the leaf decides; a
/// store's identity and its schema epoch are not decided here, since this
/// crate owns no store, and belong to the organ that will admit these
/// documents.
///
/// Serialised externally tagged, serde's default for an enum: each variant is
/// a one-key map from the variant's name to its named fields, so `field` and
/// `value` are on the wire under their own names and a reader of a refusal
/// carried over a wire reads the same parts a local caller matches on. No
/// attribute buys that, which is why there is none: an internal or adjacent
/// tag would move the fields under a content key without making any of them
/// more visible.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum PipelineRefusal {
FieldBound { field: String, limit: u32, actual: u32 },
InvalidFormat { field: String, value: String },
InvalidIdentity { kind: PipelineKind, key: String, expected: String },
DuplicateRepo { field: String, repo: String },
ForeignStore { r#type: String },
}
impl std::fmt::Display for PipelineRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "pipeline refusal: {self:?}")
}
}
impl std::error::Error for PipelineRefusal {}
pub(crate) trait Bounded {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal>;
}
impl<T: Bounded> Bounded for Option<T> {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
self.as_ref().map_or(Ok(()), |value| value.validate(field))
}
}
/// A value with an empty spelling. The `= absent` marker on a `value_types!`
/// field requires it, and the field is then omitted on write when it is absent.
pub(crate) trait Absent {
fn absent(&self) -> bool;
}
impl<T> Absent for Option<T> {
fn absent(&self) -> bool {
self.is_none()
}
}
impl Absent for bool {
fn absent(&self) -> bool {
!*self
}
}
impl<T> Absent for Vec<T> {
fn absent(&self) -> bool {
self.is_empty()
}
}
macro_rules! unbounded_scalars {
($($ty:ty),* $(,)?) => {$(
impl Bounded for $ty {
fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> {
Ok(())
}
}
)*};
}
unbounded_scalars!(u8, u16, u32, u64, bool);
fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> {
if actual > limit {
return Err(PipelineRefusal::FieldBound {
field: field.into(),
limit: limit as u32,
actual: actual as u32,
});
}
Ok(())
}
/// List maximums only. Minimum counts are admission rules with their own
/// refusals (Cut 3b), so a bounds pass never pre-empts them.
fn list<T: Bounded>(field: &str, items: &[T], max: usize) -> Result<(), PipelineRefusal> {
bound(field, max, items.len())?;
for (index, item) in items.iter().enumerate() {
item.validate(&format!("{field}[{index}]"))?;
}
Ok(())
}
fn format_error(field: &str, value: &str) -> PipelineRefusal {
PipelineRefusal::InvalidFormat {
field: field.into(),
value: value.into(),
}
}
fn hex(field: &str, value: &str, lengths: std::ops::RangeInclusive<usize>) -> Result<(), PipelineRefusal> {
let lower_hex = value.bytes().all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'));
if !lengths.contains(&value.len()) || !lower_hex {
return Err(format_error(field, value));
}
Ok(())
}
/// A key label: `[A-Za-z0-9_-]{1,64}`. Dots are excluded so that a composed key
/// segments unambiguously; see `local`.
fn label_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
let valid = value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'));
if !valid || value.is_empty() || value.len() > 64 {
return Err(format_error(field, value));
}
Ok(())
}
/// A dotted key segment: label parts joined by `.`, at most 64 bytes. Every
/// part must be a label, so `..`, a leading or trailing dot, and an empty part
/// are all refused.
fn dotted_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
dotted_within(field, value, 64)
}
/// `dotted_text`'s grammar with the bound as a parameter: the one place a
/// dotted segment is parsed, whoever owns the bound.
fn dotted_within(field: &str, value: &str, max: usize) -> Result<(), PipelineRefusal> {
if value.is_empty() || value.len() > max {
return Err(format_error(field, value));
}
for part in value.split('.') {
label_text(field, part)?;
}
Ok(())
}
/// GitHub's own grammar for `owner/repo`, exactly one `/`: the owner is
/// `[A-Za-z0-9-]`, 1 to 39 bytes, with no leading or trailing hyphen; the repo
/// is `[A-Za-z0-9._-]`, 1 to 100 bytes, is never `.` or `..`, and does not end
/// in `.git`, ASCII-case-insensitively (`a/b.GIT` is refused exactly as
/// `a/b.git` is), so the suffix rule cannot be dodged by a case variant whose
/// `identity()` is refused. The repo's own byte alphabet excludes `/`, so a
/// second `/` inside it is already refused by the byte check; no separate
/// `contains('/')` check is needed.
fn org_repo_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
let valid = value.split_once('/').is_some_and(|(owner, repo)| {
(1..=39).contains(&owner.len())
&& owner.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
&& !owner.starts_with('-')
&& !owner.ends_with('-')
&& (1..=100).contains(&repo.len())
&& repo.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
&& repo != "."
&& repo != ".."
&& !(repo.len() >= 4 && repo[repo.len() - 4..].eq_ignore_ascii_case(".git"))
});
if valid {
Ok(())
} else {
Err(format_error(field, value))
}
}
macro_rules! bounded_text {
($($(#[$doc:meta])* $name:ident = $limit:literal, $check:expr;)*) => {$(
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(extend("maxLength" = $limit))]
$(#[$doc])*
pub struct $name(pub String);
impl Bounded for $name {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
let check: fn(&str, &str) -> Result<(), PipelineRefusal> = $check;
check(field, &self.0)
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self(value.into())
}
}
)*};
}
fn within(limit: usize) -> impl Fn(&str, &str) -> Result<(), PipelineRefusal> {
move |field: &str, value: &str| bound(field, limit, value.len())
}
/// A title holds at least one `char::is_alphanumeric()` character, no
/// `char::is_control()` character, no U+2028 LINE SEPARATOR or U+2029
/// PARAGRAPH SEPARATOR (both `Zl`/`Zp`, so `is_control()` does not reach
/// them, and a one-line field should carry no hard line break), and no bidi
/// control character: `U+061C`, `U+200E`, `U+200F`, `U+202A`-`U+202E`,
/// `U+2066`-`U+2069`. No denylist of individual zero-width or format
/// characters is maintained here; a character not covered by one of these
/// tests is allowed, so ZWJ and ZWNJ still work.
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
let alphanumeric = value.chars().any(|character| character.is_alphanumeric());
let control = value
.chars()
.any(|character| character.is_control() || matches!(character, '\u{2028}' | '\u{2029}'));
let bidi = value.chars().any(|character| {
matches!(
character,
'\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}'
)
});
if !alphanumeric || control || bidi {
return Err(format_error(field, value));
}
within(200)(field, value)
}
/// A symbol is 1 to 128 bytes with no whitespace and no control character.
fn symbol_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
if value.is_empty() || value.chars().any(|character| character.is_whitespace() || character.is_control()) {
return Err(format_error(field, value));
}
within(128)(field, value)
}
/// A lowercase D-format UUID: 8-4-4-4-12 lowercase hex digits, any version.
fn uuid_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
let shaped = value.len() == 36
&& value.bytes().enumerate().all(|(at, byte)| match at {
8 | 13 | 18 | 23 => byte == b'-',
_ => matches!(byte, b'0'..=b'9' | b'a'..=b'f'),
});
if !shaped {
return Err(format_error(field, value));
}
Ok(())
}
/// A person's words: 1 to 4000 UTF-8 bytes and not blank (`str::trim` leaves something).
fn words_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
if value.trim().is_empty() {
return Err(format_error(field, value));
}
within(4000)(field, value)
}
bounded_text! {
/// Text of at most 200 UTF-8 bytes.
Short = 200, |field, value| within(200)(field, value);
/// A title: 1 to 200 UTF-8 bytes, enforced by admission; JSON Schema
/// length counts characters. Holds at least one alphanumeric character,
/// no control character, and no bidi control character (`U+061C`,
/// `U+200E`, `U+200F`, `U+202A`-`U+202E`, `U+2066`-`U+2069`).
#[schemars(extend("minLength" = 1))]
Title = 200, title_text;
/// Text of at most 1,000 UTF-8 bytes.
Line = 1000, |field, value| within(1000)(field, value);
/// A name the location's lines must contain as written there (an
/// identifier or a YAML `&fileID`); not an identity. 1 to 128 bytes, no
/// whitespace and no control character.
#[schemars(extend("minLength" = 1))]
Symbol = 128, symbol_text;
/// Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.
Para = 4000, |field, value| within(4000)(field, value);
/// A person's words, verbatim: 1 to 4,000 UTF-8 bytes, not blank (CultLib PR #72, `answer.note`).
#[schemars(extend("minLength" = 1))]
Words = 4000, words_text;
/// A client's retry token: a lowercase D-format UUID.
RequestId = 36, uuid_text;
/// A key label: `[A-Za-z0-9_-]{1,64}`.
Label = 64, label_text;
/// A campaign slug or key segment: label parts joined by `.`.
Slug = 64, dotted_text;
/// A git commit id: 7-40 lowercase hex characters.
Sha = 40, |field, value| hex(field, value, 7..=40);
/// A full git commit id: exactly 40 lowercase hex characters.
FullSha = 40, |field, value| hex(field, value, 40..=40);
/// A SHA-256 digest: exactly 64 lowercase hex characters.
Sha256Hex = 64, |field, value| hex(field, value, 64..=64);
}
impl Sha {
/// The type's identity rule: two spellings name one commit when one is a
/// prefix of the other, so a 7-character abbreviation and the full id of
/// the same commit are equal here though not `==`. Both are already 7-40
/// lowercase hex. Two distinct commits that share a 7-character prefix are
/// git's ambiguity, not the mind's.
pub fn names_same_commit(&self, other: &Sha) -> bool {
self.0.starts_with(&other.0) || other.0.starts_with(&self.0)
}
}
impl Slug {
/// The public door onto the slug grammar, for a caller outside this crate
/// that holds a bare `Slug` and no way to reach `Bounded`, which is
/// crate-private. On the pattern of `PipelineRef::validate_ref`: it
/// delegates so the grammar keeps one owner, and it applies exactly the
/// `dotted_text` check that every `Slug` field in this crate is held to,
/// so a caller that validates a declared name this way and a document
/// field of the same type refuse the same inputs. Named `validate_slug`
/// rather than a bare `validate`, because an inherent `validate` would
/// shadow `Bounded`'s for every in-crate caller holding a `Slug`.
pub fn validate_slug(&self) -> Result<(), PipelineRefusal> {
Bounded::validate(self, "slug")
}
}
/// A GitHub repository as `Org/Repo`, at most 140 UTF-8 bytes (39 + 1 +
/// 100). The owner is 1 to 39 `[A-Za-z0-9-]` bytes with no leading or
/// trailing hyphen; the repo is 1 to 100 `[A-Za-z0-9._-]` bytes, is never
/// `.` or `..`, and does not end in `.git`, ASCII-case-insensitively.
/// Identity is case-insensitive: `GameCult/Epiphany` and `gamecult/epiphany`
/// are the same repository.
//
// The raw string is display-only. `OrgRepo` is not generated by
// `bounded_text!`: that macro derives `PartialEq`, `Eq`, `Hash`, `PartialOrd`
// and `Ord` on the raw string, with no per-member override, so a type built
// from it compares and keys on spelling. `OrgRepo` instead carries
// hand-written impls of those five traits, every one of them over
// `identity()` (the ASCII-lowercased value), so `==`, a `BTreeMap` key and a
// `HashMap` key are all case-insensitive by construction: `GameCult/Epiphany`
// and `gamecult/epiphany` compare equal, hash equal, order together and
// collapse to one map entry, and a caller cannot reach the case-sensitive
// behaviour by accident. `Hash` and `Eq` agree because both are defined over
// the same `identity()` string. Every other bounded type keeps the macro's
// derives untouched. This reasoning is a source comment, not a doc comment,
// so it does not enter the published schema description (RS-L closing fix 2):
// a consumer needs the grammar, the bound and the case-insensitivity, not the
// Rust-macro justification for how that got built.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(extend("maxLength" = 140))]
pub struct OrgRepo(pub String);
impl Bounded for OrgRepo {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
org_repo_text(field, &self.0)
}
}
impl From<&str> for OrgRepo {
fn from(value: &str) -> Self {
Self(value.into())
}
}
impl PartialEq for OrgRepo {
fn eq(&self, other: &Self) -> bool {
self.identity() == other.identity()
}
}
impl Eq for OrgRepo {}
impl std::hash::Hash for OrgRepo {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.identity().hash(state);
}
}
impl PartialOrd for OrgRepo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrgRepo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.identity().cmp(&other.identity())
}
}
impl OrgRepo {
/// The public door onto the `org_repo` grammar, for a caller outside this
/// crate that holds a bare `OrgRepo` (the organ's `repo` alias, RS-3) and
/// no way to reach `Bounded`. On the pattern of `Slug::validate_slug`: it
/// delegates to `org_repo_text` through `Bounded::validate`, so the
/// grammar keeps one owner and a declared name checked this way refuses
/// exactly what a document's own `OrgRepo` field refuses.
pub fn validate_org_repo(&self) -> Result<(), PipelineRefusal> {
Bounded::validate(self, "org_repo")
}
/// The one canonical key for an `OrgRepo`: the value, ASCII-lowercased.
/// Every comparison, hash, ordering and lookup keyed by repository
/// identity goes through this — `PartialEq`, `Hash` and `Ord` above are
/// all defined in terms of it — so `GameCult/Epiphany` and
/// `gamecult/epiphany` are the same repository everywhere, not only where
/// a caller remembers to ask.
pub fn identity(&self) -> String {
self.0.to_ascii_lowercase()
}
}
impl Label {
/// The public door onto the label grammar, for a caller outside this
/// crate that holds a bare `Label` (the organ's `cut` alias, RS-3) and no
/// way to reach `Bounded`. On the pattern of `Slug::validate_slug`: it
/// delegates to `label_text` through `Bounded::validate`, so the grammar
/// keeps one owner and a declared name checked this way refuses exactly
/// what a document's own `Label` field refuses.
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
Bounded::validate(self, "label")
}
}
/// A calendar date, `YYYY-MM-DD`.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(extend("maxLength" = 10))]
pub struct Date(pub String);
impl Bounded for Date {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match chrono::NaiveDate::parse_from_str(&self.0, "%Y-%m-%d") {
Ok(_) if self.0.len() == 10 => Ok(()),
_ => Err(format_error(field, &self.0)),
}
}
}
macro_rules! bounded_field {
($value:expr, $field:expr) => {
Bounded::validate(&$value, $field)?
};
($value:expr, $field:expr, $max:literal) => {
list($field, &$value, $max)?
};
}
/// The single field list: one invocation emits the value struct and its
/// `Bounded` impl, so a field can never be left out of validation.
macro_rules! value_types {
($($(#[$attr:meta])* pub struct $name:ident { $($field:ident: $ty:ty $([$max:literal])? $(= $marker:ident)?),* $(,)? } $(=> $extra:path)?)*) => {$(
$(#[$attr])*
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct $name {
$(
$(#[schemars(extend("maxItems" = $max))])?
// `= absent`: defaulted on read, omitted on write when absent.
// The `cfg_attr(any(), ..)` is a no-op that binds `$marker` to the group.
$(#[serde(default, skip_serializing_if = "Absent::absent")] #[cfg_attr(any(), $marker)])?
pub $field: $ty
),*
}
impl Bounded for $name {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
$(
$(let _: fn(&$ty) -> bool = <$ty as Absent>::$marker;)?
bounded_field!(self.$field, &format!("{at}.{}", stringify!($field)) $(, $max)?);
)*
$($extra(self, at)?;)?
Ok(())
}
}
)*};
}
macro_rules! unit_enums {
($($name:ident { $($variant:ident),* $(,)? })*) => {$(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
pub enum $name { $($variant),* }
impl $name {
/// The variants' spellings, in declaration order: the names the wire and
/// `Debug` use.
pub const NAMES: &'static [&'static str] = &[$(stringify!($variant)),*];
}
impl Bounded for $name {
fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> {
Ok(())
}
}
)*};
}
/// RS-L closing fix 4: a repo list refuses two entries naming the same
/// repository by identity, not by raw spelling, so `GameCult/Epiphany` and
/// `gamecult/epiphany` in one list refuse exactly as one entry twice would.
/// This is a cross-element invariant on the lists alone, not a per-type format
/// rule any other `value_types!` struct shares, so it is wired in as the
/// `=> extra` hook of the two structs that carry a repo list
/// (`PipelineCampaign.repos` and `PipelineTarget.repos`) rather than folded
/// into `Bounded for OrgRepo` or the list bound in `bounded_field!`.
fn repos_are_distinct(repos: &[OrgRepo], at: &str) -> Result<(), PipelineRefusal> {
let mut seen = std::collections::HashSet::new();
for repo in repos {
if !seen.insert(repo.identity()) {
return Err(PipelineRefusal::DuplicateRepo {
field: format!("{at}.repos"),
repo: repo.0.clone(),
});
}
}
Ok(())
}
fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result<(), PipelineRefusal> {
repos_are_distinct(&campaign.repos, at)
}
fn target_repos_are_distinct(target: &PipelineTarget, at: &str) -> Result<(), PipelineRefusal> {
repos_are_distinct(&target.repos, at)
}
/// The most lines one read anchor may span.
const READ_WINDOW_MAX_LINES: u32 = 100;
/// A location starts at line 1 or later, ends no earlier than it starts, and a
/// read anchor spans at most `READ_WINDOW_MAX_LINES`. The bounds are on
/// `ReadAnchor` alone: `CodeLocation` elsewhere keeps its stored shape.
fn read_window_is_bounded(anchor: &ReadAnchor, at: &str) -> Result<(), PipelineRefusal> {
let location = &anchor.location;
if location.line < 1 {
return Err(format_error(&format!("{at}.location.line"), &location.line.to_string()));
}
if let Some(end_line) = location.end_line {
if end_line < location.line {
return Err(format_error(&format!("{at}.location.end_line"), &end_line.to_string()));
}
let span = end_line - location.line + 1;
if span > READ_WINDOW_MAX_LINES {
return Err(PipelineRefusal::FieldBound {
field: format!("{at}.location.end_line"),
limit: READ_WINDOW_MAX_LINES,
actual: span,
});
}
}
Ok(())
}
/// A run claims work, never runs, rulings or campaigns: its claims are cut
/// specs, findings and follow-ups, the three kinds a Self takes on. And its
/// budget is a canonical decimal (`0`, `12`, `7.5`: no sign, no leading or
/// trailing zeros, no exponent), so one spend has one spelling.
fn run_is_well_formed(run: &PipelineRun, at: &str) -> Result<(), PipelineRefusal> {
for (index, claim) in run.claims.iter().enumerate() {
if !matches!(claim.kind, PipelineKind::CutSpec | PipelineKind::Finding | PipelineKind::FollowUp) {
return Err(format_error(&format!("{at}.claims[{index}].kind"), claim.kind.name()));
}
if run.claims[..index].contains(claim) {
return Err(format_error(&format!("{at}.claims[{index}]"), &claim.id.0));
}
}
let (whole, fraction) = run.budget_usd.0.split_once('.').map_or((run.budget_usd.0.as_str(), None), |(whole, fraction)| (whole, Some(fraction)));
let digits = |text: &str| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit());
let canonical = digits(whole)
&& (whole == "0" || !whole.starts_with('0'))
&& fraction.is_none_or(|fraction| digits(fraction) && !fraction.ends_with('0'));
if canonical {
Ok(())
} else {
Err(format_error(&format!("{at}.budget_usd"), &run.budget_usd.0))
}
}
/// A chat name holds a letter or a digit and has no space at either end, so it can match a whole word.
fn stewardship_is_well_formed(stewardship: &PipelineStewardship, at: &str) -> Result<(), PipelineRefusal> {
for (index, name) in stewardship.names.iter().enumerate() {
if name.0.trim() != name.0 || !name.0.chars().any(char::is_alphanumeric) {
return Err(format_error(&format!("{at}.names[{index}]"), &name.0));
}
}
Ok(())
}
/// The longest and shortest beat a Mind may vote for, in seconds.
pub const TEMPO_VOTE_MIN_S: u32 = 60;
pub const TEMPO_VOTE_MAX_S: u32 = 86_400;
/// Urgency is in thousandths: 0 declares none, 1000 the most.
pub const URGENCY_MAX_PERMILLE: u16 = 1000;
/// A pulse votes a tempo or declares an urgency, in bounds, or says nothing it
/// could be read for: an empty pulse is refused.
fn pulse_is_well_formed(pulse: &PipelinePulse, at: &str) -> Result<(), PipelineRefusal> {
if let Some(beat) = pulse.tempo_vote_beat_s
&& !(TEMPO_VOTE_MIN_S..=TEMPO_VOTE_MAX_S).contains(&beat)
{
return Err(format_error(&format!("{at}.tempo_vote_beat_s"), &beat.to_string()));
}
if let Some(urgency) = pulse.urgency_permille
&& urgency > URGENCY_MAX_PERMILLE
{
return Err(format_error(&format!("{at}.urgency_permille"), &urgency.to_string()));
}
if pulse.tempo_vote_withdrawn && pulse.tempo_vote_beat_s.is_some() {
return Err(format_error(&format!("{at}.tempo_vote_withdrawn"), "a pulse that votes a beat and withdraws the vote"));
}
if pulse.tempo_vote_beat_s.is_none() && !pulse.tempo_vote_withdrawn && pulse.urgency_permille.is_none() {
return Err(format_error(&format!("{at}.tempo_vote_beat_s"), "a pulse that votes and declares nothing"));
}
Ok(())
}
/// A reply says something and answers only follow-ups, from a run.
fn reply_is_well_formed(reply: &PipelineReply, at: &str) -> Result<(), PipelineRefusal> {
if reply.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "a reply with no words"));
}
if reply.run.kind != PipelineKind::Run {
return Err(format_error(&format!("{at}.run.kind"), reply.run.kind.name()));
}
for (index, answer) in reply.answers.iter().enumerate() {
if answer.kind != PipelineKind::FollowUp {
return Err(format_error(&format!("{at}.answers[{index}].kind"), answer.kind.name()));
}
}
Ok(())
}
/// The most candidates a continuation may offer, and the nearness that means
/// she had no earlier thought to be near.
pub const THOUGHT_CANDIDATES_MAX: usize = 4;
pub const THOUGHT_NEAREST_NONE_PERMILLE: u16 = 1000;
/// A thought says something it was chosen to say: its text is not blank and is
/// the chosen candidate, every candidate has its nearness, and no nearness is
/// over 1000 thousandths.
fn thought_is_well_formed(thought: &PipelineThought, at: &str) -> Result<(), PipelineRefusal> {
if thought.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "a thought with no words"));
}
if thought.prompt_version.0.trim().is_empty() {
return Err(format_error(&format!("{at}.prompt_version"), "a thought with no prompt version"));
}
if thought.candidates.is_empty() || thought.candidates.len() > THOUGHT_CANDIDATES_MAX {
return Err(format_error(&format!("{at}.candidates"), &format!("{} candidates", thought.candidates.len())));
}
if thought.nearest_permille.len() != thought.candidates.len() {
return Err(format_error(&format!("{at}.nearest_permille"), "a nearness list that is not one per candidate"));
}
if let Some(over) = thought.nearest_permille.iter().position(|near| *near > THOUGHT_NEAREST_NONE_PERMILLE) {
return Err(format_error(&format!("{at}.nearest_permille[{over}]"), &thought.nearest_permille[over].to_string()));
}
match thought.candidates.get(usize::from(thought.chosen)) {
None => Err(format_error(&format!("{at}.chosen"), &thought.chosen.to_string())),
Some(chosen) if chosen.0 != thought.text.0 => Err(format_error(&format!("{at}.text"), "a thought that is not its chosen candidate")),
Some(_) => Ok(()),
}
}
/// The most thousandths a memory's salience may carry.
pub const SALIENCE_MAX_PERMILLE: u16 = 1000;
/// A memory says something (its text is not empty), its salience is in
/// thousandths, and it consolidates only memories.
fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), PipelineRefusal> {
if memory.text.0.trim().is_empty() {
return Err(format_error(&format!("{at}.text"), "an empty memory"));
}
if let MemoryOrigin::Direct { with, .. } = &memory.origin
&& with.is_empty()
{
return Err(format_error(&format!("{at}.origin.with"), "a direct memory that names no one she heard it from"));
}
if memory.salience_permille > SALIENCE_MAX_PERMILLE {
return Err(format_error(&format!("{at}.salience_permille"), &memory.salience_permille.to_string()));
}
for (index, entry) in memory.consolidates.iter().enumerate() {
if entry.kind != PipelineKind::Memory {
return Err(format_error(&format!("{at}.consolidates[{index}].kind"), entry.kind.name()));
}
}
Ok(())
}
/// A research note states a claim and stands on at least one source that
/// names something: a Repo source a file and a range that does not end before
/// it starts, a Web source its address and work, an Observed one its
/// locator and result, a Document one a note other than this one. A claim
/// with no source is not research. An inferred note names what it inferred
/// from in those sources. Two notes citing each other cannot be seen from one
/// document; that refusal would need the image, so it is not made here.
fn research_is_well_formed(research: &PipelineResearch, at: &str) -> Result<(), PipelineRefusal> {
if research.claim.0.trim().is_empty() {
return Err(format_error(&format!("{at}.claim"), "an empty claim"));
}
if research.sources.is_empty() {
return Err(format_error(&format!("{at}.sources"), "a research note that names no source"));
}
let own_key = format!("{}:research:{}.{}", research.campaign.0, research.pass.0, research.label.0);
let blank = |text: &str| text.trim().is_empty();
for (index, source) in research.sources.iter().enumerate() {
let (field, empty) = match source {
ResearchSource::Repo { at, .. } if blank(&at.path.0) => ("at.path", "a repo source pinning no file"),
ResearchSource::Repo { at, .. } if at.end_line < at.start_line => ("at.end_line", "a repo source whose range ends before it starts"),
ResearchSource::Web { url, .. } if blank(&url.0) => ("url", "a web source with no address"),
ResearchSource::Web { work, .. } if blank(&work.0) => ("work", "a web source naming no work"),
ResearchSource::Observed { evidence } if blank(&evidence.locator.0) => ("evidence.locator", "an observation naming nothing observed"),
ResearchSource::Observed { evidence } if blank(&evidence.result.0) => ("evidence.result", "an observation with no result"),
ResearchSource::Document { reference } if reference.kind == PipelineKind::Research && reference.id.0 == own_key => ("reference", "a note that is its own source"),
_ => continue,
};
return Err(format_error(&format!("{at}.sources[{index}].{field}"), empty));
}
Ok(())
}
/// An idea says something. It may serve no fork and rest on nothing.
fn idea_is_well_formed(idea: &PipelineIdea, at: &str) -> Result<(), PipelineRefusal> {
if idea.idea.0.trim().is_empty() {
return Err(format_error(&format!("{at}.idea"), "an empty idea"));
}
Ok(())
}
unit_enums! {
EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture }
ClaimOutcome { Holds, Falsified, Unproven }
FindingConfidence { Confirmed, Plausible }
FindingSeverity { Blocker, High, Medium, Low }
FindingOrigin { Introduced, PreExisting }
RunOperator { Operator, Mind }
RunTurn { PersonaTurn, SelfRun, Sleep }
MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport }
// The role the verified account holds, an attribution copied from Bifrost's state (CultLib PR #72,
// fork-card.md). It confers nothing: whether an answerer may decide is the decider list's, never this.
DeskAuthority { Operator, Steward, Member }
// Why a desk answer was refused and stored: binding spec 1.3 steps 4-7 and R1.
DeskRefusal { StaleFork, NotADecider, ForkClosed, ChoiceInvalid, BadRetraction, AlreadyRetracted }
}
/// The slots of a Mind's runs: one live run of hers per instance and slot. A
/// Persona session and her Sleep run share the Persona slot (unit
/// `mind-persona@`); a Self run holds the other.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RunSlot {
Persona,
SelfRun,
}
impl RunTurn {
/// The slot this turn holds while live; admission and launch both read it here.
pub fn slot(self) -> RunSlot {
match self {
Self::PersonaTurn | Self::Sleep => RunSlot::Persona,
Self::SelfRun => RunSlot::SelfRun,
}
}
}
value_types! {
pub struct CodeLocation { path: Short, line: u32, end_line: Option<u32>, symbol: Option<Symbol> = absent }
pub struct CommitRange { base: Sha, head: Sha }
pub struct Evidence { kind: EvidenceKind, locator: Line, result: Line }
/// A citation of a document in another repo's pipeline store (D6).
pub struct ForeignRef { repo: OrgRepo, commit: FullSha, kind: PipelineKind, id: Short, payload_sha256: Sha256Hex }
pub struct DocRef { path: Short, start_line: u32, end_line: u32, commit: Sha }
pub struct TargetInvariant { label: Label, statement: Line }
pub struct QuestionOption { label: Label, text: Line }
pub struct CutDelete { path: Short, lines: u32, note: Line }
pub struct FileChange { location: CodeLocation, change: Line }
/// Code Hands must see but not change, in the spec's repo at the spec's base.
pub struct ReadAnchor { location: CodeLocation, why: Short } => read_window_is_bounded
pub struct AuthorityMap {
owner: Line, inputs: Vec<Line>[16], outputs: Vec<Line>[16], derived_state: Vec<Line>[16],
forbidden_writers: Vec<Line>[16], shared_paths: Vec<Line>[16], deletion_line: Line,
}
pub struct VerificationTest { name: Short, pins: Line }
pub struct NegativeCheck { pattern: Short, scope: Line }
pub struct CutVerification {
builds: Vec<Line>[64], tests: Vec<VerificationTest>[64],
negative: Vec<NegativeCheck>[64], operator: Vec<Line>[64],
}
pub struct ReportCommit { sha: Sha, subject: Line, builds: bool }
/// One mutation a report ran: a key-safe label a verdict claim can name,
/// the exact edit, and the tree it was applied to.
pub struct MutationRecord { label: Label, rule: Line, location: CodeLocation, before: Line, after: Line, commit: Sha, failed_as_expected: bool }
pub struct Deviation { what: Line, why: Line }
// D1 tables no maximum for these lists, so they take the shared list
// default of 64 rather than a number invented for this field alone.
pub struct StructuralDelta {
lines_added: u32, lines_removed: u32,
dependencies_added: Vec<Short>[64], dependencies_removed: Vec<Short>[64],
formats_added: Vec<Short>[64], formats_removed: Vec<Short>[64],
targets_added: Vec<Short>[64], targets_removed: Vec<Short>[64],
}
/// A name the cut landed, and where it lives.
pub struct LandedName { name: Short, path: Short }
/// A promise a report makes about what it landed. Not a document and not
/// resolvable: it is identified by its report's id and its label, as a
/// `TargetInvariant` is by its target's. Soul measures every one (ruling
/// A), and the rule that every one is measured is admission's.
pub struct Promise { label: Label, text: Line }
/// The promise this claim measured, and the report's mutations it ran. An
/// `Unproven` claim with a promise is one Soul could not reach.
pub struct VerdictClaim {
claim: Line, outcome: ClaimOutcome, evidence: Vec<Evidence>[8], findings: Vec<Short>[16],
promise: Option<Label>, mutations: Vec<Label>[8],
}
pub struct PipelineCampaign { slug: Slug, title: Title, repos: Vec<OrgRepo>[8], working_branch: Short, target_doc: DocRef } => campaign_repos_are_distinct
/// `repos`, when non-empty, is the campaign's whole repo set from this
/// revision on; empty keeps the previous set (ruling campaign-repo-set-home).
pub struct PipelineTarget {
campaign: Slug, revision: u32, invariants: Vec<TargetInvariant>[32], not_in_scope: Vec<Line>[32],
canonical_implementations: Vec<Line>[16], repos: Vec<OrgRepo>[16] = absent, doc: DocRef,
} => target_repos_are_distinct
pub struct PipelineQuestion {
campaign: Slug, label: Label, title: Title, question: Para, options: Vec<QuestionOption>[8], recommended: Label,
depends: Vec<Line>[8], raised_in: Option<PipelineRef>, asked_on: Date,
}
pub struct PipelineRuling {
campaign: Slug, label: Label, title: Title, answers: Option<Short>, choice: Option<Label>, ruling: Para,
operator_quote: Option<Para>, ruled_on: Date, precedents: Vec<ForeignRef>[8], authority: RulingAuthority,
}
pub struct PipelineCutSpec {
campaign: Slug, cut: Label, revision: u32, title: Title, repo: OrgRepo, branch: Short, base: Sha,
depends_on: Vec<Short>[8], first: Vec<Line>[16], deletes: Vec<CutDelete>[64], keeps_moves: Vec<Line>[64],
adds: Vec<Line>[64], file_changes: Vec<FileChange>[256], reads: Vec<ReadAnchor>[16] = absent, authority_map: Option<AuthorityMap>,
verification: CutVerification, estimate: StructuralDelta,
rulings: Vec<Short>[32], questions: Vec<Short>[16],
}
pub struct PipelineCutReport {
campaign: Slug, cut_spec: Short, attempt: u32, repo: OrgRepo, branch: Short,
commits: Vec<ReportCommit>[128], range: CommitRange, verification: Vec<Evidence>[64],
mutations: Vec<MutationRecord>[64], deviations: Vec<Deviation>[32], forks: Vec<Short>[8],
structural_delta: StructuralDelta, landed_names: Vec<LandedName>[128], undone: Vec<Line>[32],
promises: Vec<Promise>[64],
}
/// One answer the fork desk's receiver passed to the Mind, refused ones included.
/// `answerer` and `authority` (an attribution, never a decision) are stamped by the
/// Mind from the verified caller and are absent from the request type; `answered_at`
/// is the client's claim kept as written, display only: order is `sequence`, the
/// receiver's order. `received_at` is the Mind's admission time (UTC) and
/// `claim_in_future` is set when `answered_at` is more than five minutes after it.
/// `request_id` is the client's retry token and `body_sha256` the digest of the
/// request it came with: the pair that makes a retry the same answer, never a new
/// one; the key is `(answerer, request_id)`. A record holds no card: a replayed
/// `stale_fork` is shown the current card. `sequence` is per question and is the
/// last part of the key (the answer id). `choice`, `note` and (for a retraction) the
/// target are kept as sent whether the request was counted or refused, so a refused
/// `choice_invalid` keeps a choice that is no option label; which answer counts now is
/// derived by the reader, never stored here.
pub struct PipelineDeskAnswer {
campaign: Slug, fork: Short, sequence: u32, options_hash: Sha256Hex, choice: Option<Short>, note: Option<Words>,
answered_at: Short, received_at: Short, claim_in_future: bool, request_id: RequestId, body_sha256: Sha256Hex,
answerer: Short, authority: DeskAuthority, outcome: DeskOutcome,
}
pub struct PipelineVerdict { campaign: Slug, cut_report: Short, pass: u32, range: CommitRange, claims: Vec<VerdictClaim>[64] }
pub struct PipelineFinding {
campaign: Slug, verdict: Short, label: Label, range: CommitRange, confidence: FindingConfidence,
severity: FindingSeverity, claim: Line, invariants: Vec<Label>[8], locations: Vec<CodeLocation>[16],
failure_scenario: Para, evidence: Vec<Evidence>[16], precedents: Vec<ForeignRef>[8], origin: FindingOrigin,
}
/// A follow-up. `origin` is where the Persona turn that admitted it was
/// asked, as the organ derived it: a brief records the conversation whose
/// message started it. Absent on follow-ups no Persona turn admitted and
/// on briefs admitted before it existed.
pub struct PipelineFollowUp {
campaign: Slug, label: Label, source: PipelineRef, repo: OrgRepo, locations: Vec<CodeLocation>[16],
item: Line, why_it_can_wait: Line, owner: Short, origin: Option<MemoryOrigin> = absent,
}
/// The record of how a subject was closed and by what. `sequence` is per
/// subject and set by the writer, `1` for the first, and it is the last
/// part of the key, so a subject's resolutions share a prefix and a
/// withdrawn one is kept under its subject rather than overwritten.
/// Whether a sequence is the previous plus one, and whether an earlier
/// resolution still stands, are admission's rules: this crate refuses
/// neither `0` nor a gap, exactly as it refuses neither `revision: 0` nor
/// a `revision` with no predecessor.
pub struct PipelineResolution { subject: PipelineRef, sequence: u32, outcome: ResolutionOutcome, rationale: Para, resolved_on: Date }
/// A mind's identity document. A store is canonical to exactly one
/// instance, and this says which; identity lives in the state, not in a
/// path.
pub struct PipelineInstance { instance: Slug, display_name: Short, created_at: Date, host: Short }
/// Stewardship over a repo, as an assignment recorded in a mind. One
/// instance may steward several repos, so the repo is part of the key.
/// `sequence` is per `(instance, repo)`, set by the writer, `1` for the
/// first, so a repo transferred away and back is two records under one
/// prefix rather than one document overwriting the other. `assigned_on`
/// is a field, not a key part. As on a resolution, whether the sequence
/// is the previous plus one, and which assignment is in force, are
/// admission's rules and not this crate's.
/// `names` are the chat names that address her for this repo, matched exactly
/// as written; none means the repo's own name, matched in any case.
pub struct PipelineStewardship { instance: Slug, repo: OrgRepo, sequence: u32, assigned_on: Date, note: Line, names: Vec<Short>[8] = absent } => stewardship_is_well_formed
/// A reassignment of stewardship, recorded in both minds. `documents` names
/// what travels with it.
pub struct PipelineHandOff {
from_instance: Slug, to_instance: Slug, repo: OrgRepo, documents: Vec<Short>[256],
reason: Para, handed_on: Date,
}
/// One launch of a Self or a Persona turn: the one record of what it took
/// on. `claims` are the work it holds, and a claim is live while its run
/// has no resolution in force. `budget_usd` is a canonical decimal string,
/// `0` for an interactive run. Instance-rooted like stewardship: the key
/// is `<instance>:run:<label>`. Whether a claim is free is admission's rule.
pub struct PipelineRun {
instance: Slug, label: Label, operated_by: RunOperator, turn: RunTurn, host: Short, started_on: Date,
budget_usd: Short, claims: Vec<PipelineRef>[64], campaigns: Vec<Slug>[8],
} => run_is_well_formed
/// One Mind's heartbeat vote, in her own mind: the beat she wants (the swarm's
/// tempo is the lower median of every Mind's newest vote), the withdrawal of
/// her standing vote (`tempo_vote_withdrawn`, an explicit null: she counts as
/// silent again), and the urgency she declares for herself until her next
/// Persona turn. A pulse says at least one of the three; a withdrawal and a
/// beat are never both. Instance-rooted: the key is `<instance>:pulse:<label>`, the label
/// time-ordered like a run's.
pub struct PipelinePulse {
instance: Slug, label: Label, tempo_vote_beat_s: Option<u32>, tempo_vote_withdrawn: bool = absent, urgency_permille: Option<u16>, note: Line,
} => pulse_is_well_formed
/// One result of her Self session, as its actuator heard it: the words her
/// Self ended a turn with, and the follow-ups (briefs) the turn answers.
/// Instance-rooted: the key is `<instance>:reply:<label>`. Not semantically
/// indexed. Whether the run is hers is admission's rule.
pub struct PipelineReply { instance: Slug, label: Label, run: PipelineRef, answers: Vec<PipelineRef>[64], text: Para } => reply_is_well_formed
/// One tick of her stream of thought, offline: the words a base model
/// continued from a projection of her own state, with the whole trace that
/// made them (the seed, the projection, every candidate and how near each
/// was to her earlier thoughts). `text` is the chosen candidate. `reads`
/// are the documents the seed was drawn from. A thought is divergence for
/// delivery to weigh, never a memory: its words reach a later seed only
/// through something she wrote. Instance-rooted: the key is
/// `<instance>:thought:<label>`, the label `t<unix seconds>` of the tick.
/// Semantically indexed on `text` only. `nearest_permille` is 1000 where she
/// had no earlier thought. `prompt_version` names the fixed parts of both
/// prompts (the Projector's builder revision, guidance and word budget, and
/// the continuer's frame), so the exact prompts of an old thought can be
/// rebuilt from the trace.
pub struct PipelineThought {
instance: Slug, label: Label, text: Para, reads: Vec<PipelineRef>[64], persona_receipt: Short,
identity: Line, context: Para, stimulus: Para, projector_model: Short, projection: Para, base_model: Short,
temperature_permille: u16, min_p_permille: u16, max_tokens: u16,
candidates: Vec<Line>[4], nearest_permille: Vec<u16>[4], chosen: u8, projector_ms: u32, continuer_ms: u32,
prompt_version: Line,
} => thought_is_well_formed
/// One thing a Mind keeps for herself, in her own mind: what she was asked,
/// decided, committed to, learned or did, an idea, or the rapport between
/// her and another Mind or person. `text` is her own words. `about` names
/// the Matrix users it concerns (Minds included) and nothing about where it
/// came from; `origin` is where and from whom she learned it, and `sources`
/// the event ids, commits or documents it rests on; the organ fills both,
/// not the model. `salience_permille` ranks it and decides nothing.
/// `consolidates` names the memories a consolidating memory replaces.
/// Instance-rooted: the key is `<instance>:memory:<label>`.
pub struct PipelineMemory {
instance: Slug, label: Label, sort: MemorySort, text: Line, about: Vec<Short>[8], origin: MemoryOrigin,
sources: Vec<Short>[8], salience_permille: u16, consolidates: Vec<PipelineRef>[16],
} => memory_is_well_formed
/// One claim a pass found, with the sources it stands on, grouped by `pass`.
/// What is so, never what to do: a recommendation is an idea. `inferred`
/// marks a claim read from its sources rather than seen in them. It stores
/// no status: whether a note is current, superseded or wrong is its
/// resolutions'. `serves` names the question, cut or campaign document it
/// was gathered for. The pass's method and what it did not search is its
/// first note, labelled `scope`. Campaign-rooted:
/// `<campaign>:research:<pass>.<label>`.
pub struct PipelineResearch {
campaign: Slug, pass: Label, label: Label, claim: Line, detail: Para, inferred: bool,
sources: Vec<ResearchSource>[16], serves: Vec<PipelineRef>[8], observed_on: Date,
} => research_is_well_formed
/// One thing that could be, including options rejected and ideas no cut
/// uses yet. `rests_on` is what it stands on (a reader selects the ideas
/// resting on a stale note with `cites`); `toward` is the fork, ruling or
/// follow-up it was made for, and empty is legal. It stores no status:
/// taken up is its Deferred resolution, rejected its Recorded one.
/// Campaign-rooted: `<campaign>:idea:<pass>.<label>`.
pub struct PipelineIdea {
campaign: Slug, pass: Label, label: Label, title: Title, idea: Para, tradeoffs: Para,
rests_on: Vec<PipelineRef>[16], toward: Vec<PipelineRef>[8], proposed_on: Date,
} => idea_is_well_formed
}
/// A reference to another document. The id is parsed as a full pipeline id of
/// the declared `kind`, so the kind and the id's kind segment cannot disagree.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PipelineRef {
pub kind: PipelineKind,
pub id: Short,
}
impl Bounded for PipelineRef {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
self.id.validate(&format!("{at}.id"))?;
pipeline_id(&format!("{at}.id"), &self.id.0, self.kind).map(|_| ())
}
}
impl PipelineRef {
/// The grammar's door for a reader holding a bare reference. A read side
/// outside this crate is handed a `kind` and an `id` that may disagree, and
/// `Bounded` is crate-private, so without this there is no way to ask; a
/// reader with no way to ask answers empty, which reads as "no such
/// document" rather than "that is not a reference". It delegates, so the
/// grammar keeps one owner: this is exactly what a document's own
/// `PipelineRef` field is held to, at the field name `ref`. It is kind-deep,
/// like every other read of an id: whether the named document exists, and
/// whether the local's shape suits the kind, are admission's rules.
///
/// A method rather than a free function because it concerns one public
/// type, as `prepare` and `decode` do; the two free doors act on a cache or
/// an envelope this crate does not own. `validate_ref` rather than a bare
/// `validate`, because an inherent `validate` would shadow `Bounded`'s for
/// every in-crate caller holding a `PipelineRef`.
pub fn validate_ref(&self) -> Result<(), PipelineRefusal> {
self.validate("ref")
}
}
/// Where a Persona turn's batch came from and whom she heard it from: a
/// memory's origin and a brief's. The organ derives it from the batch of the
/// turn; the model never writes it.
/// `about` names whom a memory concerns, this names the room and the voices.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum MemoryOrigin {
/// A turn in rooms: their aliases comma-joined, and the batch's other senders.
Room {
rooms: Short,
#[schemars(extend("maxItems" = 8))]
heard_from: Vec<Short>,
},
/// A direct message: the DM's room id, and the others who spoke in it (whom
/// she heard it from in confidence).
Direct {
room: Short,
#[schemars(extend("maxItems" = 8))]
with: Vec<Short>,
},
/// A wake: no one spoke.
Wake,
/// A Self run of hers, by its label.
SelfRun { run: Short },
/// Her Sleep run, by its label: a memory she formed in her night.
Sleep { run: Short },
}
impl Bounded for MemoryOrigin {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Room { rooms, heard_from } => {
rooms.validate(&format!("{field}.rooms"))?;
list(&format!("{field}.heard_from"), heard_from, 8)
}
Self::Direct { room, with } => {
room.validate(&format!("{field}.room"))?;
list(&format!("{field}.with"), with, 8)
}
Self::Wake => Ok(()),
Self::SelfRun { run } | Self::Sleep { run } => run.validate(&format!("{field}.run")),
}
}
}
/// Where a research claim comes from. A `Repo` pin and a `Web` page are
/// outside the document set; an `Observed` result reuses a finding's evidence;
/// a `Document` is the one source that is a citation edge.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ResearchSource {
/// A pinned file range in a repo.
Repo { repo: OrgRepo, at: DocRef },
/// A page read on the web: its address, the work it belongs to, and when.
Web { url: Line, work: Short, retrieved_on: Date },
/// A command, probe, read or capture the pass ran itself.
Observed { evidence: Evidence },
/// Another document in the mind.
Document { reference: PipelineRef },
}
impl Bounded for ResearchSource {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Repo { repo, at } => {
repo.validate(&format!("{field}.repo"))?;
at.validate(&format!("{field}.at"))
}
Self::Web { url, work, retrieved_on } => {
url.validate(&format!("{field}.url"))?;
work.validate(&format!("{field}.work"))?;
retrieved_on.validate(&format!("{field}.retrieved_on"))
}
Self::Observed { evidence } => evidence.validate(&format!("{field}.evidence")),
Self::Document { reference } => reference.validate(&format!("{field}.reference")),
}
}
}
/// Who decided a ruling, and under what. `Mind` is this mind's own decision, admitted by her Self. `Steward` is
/// a named peer's decision inside the peer's stewardship, as this mind heard it: `steward` is the deciding Mind's
/// Matrix id, `repo` the stewardship she decided under, `words` her own words copied from the line this mind heard,
/// `event` that line's Matrix event id when it was a delivery. The four unit variants stay bare strings on the wire.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
pub enum RulingAuthority {
Operator,
Standing,
Defaulted,
Mind,
Steward { steward: Short, repo: OrgRepo, words: Para, event: Option<Short> },
}
impl RulingAuthority {
/// The variants' spellings, in declaration order: the names the wire and listings use.
pub const NAMES: &'static [&'static str] = &["Operator", "Standing", "Defaulted", "Mind", "Steward"];
/// The variant's spelling, for rows, headers and listings.
pub fn name(&self) -> &'static str {
match self {
Self::Operator => "Operator",
Self::Standing => "Standing",
Self::Defaulted => "Defaulted",
Self::Mind => "Mind",
Self::Steward { .. } => "Steward",
}
}
}
impl Bounded for RulingAuthority {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Steward { steward, repo, words, event } => {
steward.validate(&format!("{field}.steward"))?;
if steward.0.trim().is_empty() {
return Err(format_error(&format!("{field}.steward"), "a steward naming no Mind"));
}
repo.validate(&format!("{field}.repo"))?;
words.validate(&format!("{field}.words"))?;
if words.0.trim().is_empty() {
return Err(format_error(&format!("{field}.words"), "a steward ruling with no words"));
}
event.validate(&format!("{field}.event"))
}
_ => Ok(()),
}
}
}
/// How a subject was resolved. Every referent is a parsed `PipelineRef`, so a
/// resolution names its records by ids of the kinds they declare, validated
/// where every other referent is; a `Fixed` commit is a `Sha` whose referent
/// is outside the document set (ruling B). Whether a named document exists,
/// and how many may supersede one subject, are admission's rules.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ResolutionOutcome {
/// Overturned, in whole or in part, by later records. The list is bounded
/// here because the enum is outside `value_types!`, the only place a list
/// maximum is emitted automatically.
Superseded {
#[schemars(extend("maxItems" = 8))]
by: Vec<PipelineRef>,
},
Answered { by: PipelineRef },
Fixed { commit: Sha, by: Option<PipelineRef> },
Deferred { to: PipelineRef },
Recorded { reason: Line },
Withdrawn { reason: Line },
/// How a Sleep run ended when the ledger took her reply: `line` is her one line, `day_through` the last memory
/// of her day she was shown (none when the day was empty). It is the one place a night's cut line is kept.
Slept { line: Line, day_through: Option<PipelineRef> },
/// A campaign's history left this mind for `to`, the mind that now keeps it. Final: the campaign and every
/// document under its root read resolved by this and take no further writes here.
Moved { to: Slug },
}
impl Bounded for ResolutionOutcome {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Superseded { by } => list(&format!("{field}.by"), by, 8),
Self::Answered { by } => by.validate(&format!("{field}.by")),
Self::Fixed { commit, by } => {
commit.validate(&format!("{field}.commit"))?;
by.validate(&format!("{field}.by"))
}
Self::Deferred { to } => to.validate(&format!("{field}.to")),
Self::Recorded { reason } | Self::Withdrawn { reason } => reason.validate(&format!("{field}.reason")),
Self::Slept { line, day_through } => {
line.validate(&format!("{field}.line"))?;
day_through.validate(&format!("{field}.day_through"))
}
Self::Moved { to } => to.validate(&format!("{field}.to")),
}
}
}
/// The one tagged outcome of a desk answer. A retraction is a variant, so a
/// retraction without a target and a target without a retraction cannot be
/// stored. `of` and `retracts` name desk answers by answer id (the record key).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum DeskOutcome {
/// The answer that counts for its fork when it was admitted.
Counted,
/// A later passing answer, kept visible; `of` is the counted answer it followed.
Revision { of: Short },
Retraction { retracts: Short },
/// A request the desk would not count, kept with what it was sent with. `retracts` is the
/// target a refused retraction named, as sent and unchecked (it may name no answer at all);
/// it is `None` on a refused answer, and it is never a citation.
Refused { code: DeskRefusal, retracts: Option<Short> },
}
impl DeskOutcome {
/// The variant's spelling, for headers and listings.
pub fn name(&self) -> &'static str {
match self {
Self::Counted => "Counted",
Self::Revision { .. } => "Revision",
Self::Retraction { .. } => "Retraction",
Self::Refused { .. } => "Refused",
}
}
}
impl Bounded for DeskOutcome {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Counted => Ok(()),
Self::Refused { retracts, .. } => retracts.as_ref().map_or(Ok(()), |target| target.validate(&format!("{field}.retracts"))),
Self::Revision { of: id } | Self::Retraction { retracts: id } => {
let field = format!("{field}.{}", if matches!(self, Self::Revision { .. }) { "of" } else { "retracts" });
id.validate(&field)?;
pipeline_id(&field, &id.0, PipelineKind::DeskAnswer).map(|_| ())
}
}
}
}
macro_rules! pipeline_kinds {
($($variant:ident($value:ident) => $document:ident, $name:literal, $type_id:tt, $schema:tt;)*) => {
$(
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = $type_id, schema = $schema)]
pub(crate) struct $document {
#[cultcache(key = 0)]
pub(crate) value: $value,
}
)*
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
pub enum PipelineKind { $($variant),* }
impl Bounded for PipelineKind {
fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> {
Ok(())
}
}
/// The envelope enum, adjacently tagged on the kind name: a document
/// on a wire is `{ kind, value }`, and `kind` is exactly what
/// `PipelineKind::name()` returns, so a reader dispatches on the kind
/// segment it already knows from the key and needs no second registry
/// mapping variant spellings to kinds. Adjacent rather than internal
/// because a value is a named map of its own and an internal tag would
/// have to be merged into it; the tag stays beside the value instead.
/// This is not a published schema: `schemas/cultnet` publishes the
/// eighteen per-kind value documents, and the envelope is the wire's
/// shape, not a document's.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant, reason = "one value per request or per batch of at most 64; boxing a variant would buy no memory and change every construction site")]
pub enum PipelineDocument { $($variant($value)),* }
impl PipelineKind {
pub const ALL: &'static [PipelineKind] = &[$(Self::$variant),*];
/// The kind segment of a document key.
pub fn name(self) -> &'static str {
match self { $(Self::$variant => $name),* }
}
pub fn type_id(self) -> &'static str {
match self { $(Self::$variant => <$document as DatabaseEntry>::TYPE),* }
}
#[cfg(test)]
pub(crate) fn schema_name(self) -> &'static str {
match self { $(Self::$variant => <$document as DatabaseEntry>::SCHEMA_NAME),* }
}
#[cfg(test)]
pub(crate) fn derived_schema(self) -> schemars::Schema {
match self { $(Self::$variant => schemars::schema_for!($value)),* }
}
}
impl PipelineDocument {
pub fn kind(&self) -> PipelineKind {
match self { $(Self::$variant(_) => PipelineKind::$variant),* }
}
/// Field bounds and formats (D1), in UTF-8 bytes and list maximums.
pub fn validate(&self) -> Result<(), PipelineRefusal> {
match self { $(Self::$variant(value) => value.validate($name)),* }
}
/// Prepares the envelope to be stored: keyed by `pipeline_key`,
/// payload `[value]` through `prepare_entry_named`.
pub fn prepare(&self, cache: &CultCache) -> Result<CultCacheEnvelope> {
let key = pipeline_key(self)?;
Ok(match self {
$(Self::$variant(value) => {
cache.prepare_entry_named(key, &$document { value: value.clone() })?.0
})*
})
}
/// Decodes a stored envelope, type-matched both ways: an envelope of
/// any other type is `ForeignStore`, never the kind that happens to
/// parse its payload. It validates neither bounds nor the key: a
/// decoded document is a typed read, and
/// `validate_pipeline_write_envelope` is the check before a write.
pub fn decode(envelope: &CultCacheEnvelope) -> Result<Self, PipelineRefusal> {
let invalid = |error: rmp_serde::decode::Error| format_error("payload", &error.to_string());
$(if envelope.r#type == <$document as DatabaseEntry>::TYPE {
let document: $document = rmp_serde::from_slice(&envelope.payload).map_err(invalid)?;
return Ok(Self::$variant(document.value));
})*
Err(PipelineRefusal::ForeignStore { r#type: envelope.r#type.clone() })
}
}
/// Registers every pipeline kind in a cache, and nothing else: the one
/// door to the crate-private wrappers, so a caller registers exactly
/// what this crate publishes.
pub fn register_pipeline_document_types(cache: &mut CultCache) -> Result<()> {
$(cache.register_entry_type::<$document>()?;)*
Ok(())
}
};
}
/// A document that is not a pipeline document, for the decode refusal and the
/// registrar's count to be pinned against something real. It stands in for
/// any other document a store may hold beside pipeline documents, a commit
/// receipt say, which is still not a document this library may decode. The
/// stand-in carries a receipt-shaped type id and is registered only by the
/// tests' own cache, never by the live registrar.
#[cfg(test)]
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.mind_commit_receipt.v1", schema = "ForeignDocument")]
pub(crate) struct ForeignDocument {
#[cultcache(key = 0)]
pub(crate) marker: Short,
}
pipeline_kinds! {
Campaign(PipelineCampaign) => EpiphanyPipelineCampaignDocument, "campaign",
"epiphany.pipeline.campaign.v2", "EpiphanyPipelineCampaignDocument";
Target(PipelineTarget) => EpiphanyPipelineTargetDocument, "target",
"epiphany.pipeline.target.v2", "EpiphanyPipelineTargetDocument";
Question(PipelineQuestion) => EpiphanyPipelineQuestionDocument, "question",
"epiphany.pipeline.question.v2", "EpiphanyPipelineQuestionDocument";
Ruling(PipelineRuling) => EpiphanyPipelineRulingDocument, "ruling",
"epiphany.pipeline.ruling.v2", "EpiphanyPipelineRulingDocument";
CutSpec(PipelineCutSpec) => EpiphanyPipelineCutSpecDocument, "cut_spec",
"epiphany.pipeline.cut_spec.v2", "EpiphanyPipelineCutSpecDocument";
CutReport(PipelineCutReport) => EpiphanyPipelineCutReportDocument, "cut_report",
"epiphany.pipeline.cut_report.v2", "EpiphanyPipelineCutReportDocument";
Verdict(PipelineVerdict) => EpiphanyPipelineVerdictDocument, "verdict",
"epiphany.pipeline.verdict.v2", "EpiphanyPipelineVerdictDocument";
Finding(PipelineFinding) => EpiphanyPipelineFindingDocument, "finding",
"epiphany.pipeline.finding.v2", "EpiphanyPipelineFindingDocument";
FollowUp(PipelineFollowUp) => EpiphanyPipelineFollowUpDocument, "follow_up",
"epiphany.pipeline.follow_up.v2", "EpiphanyPipelineFollowUpDocument";
Resolution(PipelineResolution) => EpiphanyPipelineResolutionDocument, "resolution",
"epiphany.pipeline.resolution.v2", "EpiphanyPipelineResolutionDocument";
Instance(PipelineInstance) => EpiphanyPipelineInstanceDocument, "instance",
"epiphany.pipeline.instance.v2", "EpiphanyPipelineInstanceDocument";
Stewardship(PipelineStewardship) => EpiphanyPipelineStewardshipDocument, "stewardship",
"epiphany.pipeline.stewardship.v2", "EpiphanyPipelineStewardshipDocument";
HandOff(PipelineHandOff) => EpiphanyPipelineHandOffDocument, "hand_off",
"epiphany.pipeline.hand_off.v2", "EpiphanyPipelineHandOffDocument";
Run(PipelineRun) => EpiphanyPipelineRunDocument, "run",
"epiphany.pipeline.run.v2", "EpiphanyPipelineRunDocument";
Pulse(PipelinePulse) => EpiphanyPipelinePulseDocument, "pulse",
"epiphany.pipeline.pulse.v2", "EpiphanyPipelinePulseDocument";
Memory(PipelineMemory) => EpiphanyPipelineMemoryDocument, "memory",
"epiphany.pipeline.memory.v2", "EpiphanyPipelineMemoryDocument";
Research(PipelineResearch) => EpiphanyPipelineResearchDocument, "research",
"epiphany.pipeline.research.v2", "EpiphanyPipelineResearchDocument";
Idea(PipelineIdea) => EpiphanyPipelineIdeaDocument, "idea",
"epiphany.pipeline.idea.v2", "EpiphanyPipelineIdeaDocument";
Reply(PipelineReply) => EpiphanyPipelineReplyDocument, "reply",
"epiphany.pipeline.reply.v2", "EpiphanyPipelineReplyDocument";
Thought(PipelineThought) => EpiphanyPipelineThoughtDocument, "thought",
"epiphany.pipeline.thought.v2", "EpiphanyPipelineThoughtDocument";
DeskAnswer(PipelineDeskAnswer) => EpiphanyPipelineDeskAnswerDocument, "desk_answer",
"epiphany.pipeline.desk_answer.v2", "EpiphanyPipelineDeskAnswerDocument";
}
/// Parses a full document id: the grammar read backwards. A key and an id are
/// the same string, so this accepts exactly what `pipeline_key` derives:
/// `<root>:<kind>:<local>`, three segments for every kind. The kind segment
/// must equal `kind`'s name, the root is a `Slug`, and the local is `Label`s
/// joined by `.` and bounded whole by `local_max(kind)`, so `..`, an empty part, spaces and trailing
/// junk are all refused. Returns the root and the local; nothing is inferred
/// from either, and no kind is read any other way.
fn pipeline_id<'a>(
field: &str,
id: &'a str,
kind: PipelineKind,
) -> Result<(&'a str, &'a str), PipelineRefusal> {
let mut segments = id.split(':');
let (Some(root), Some(name), Some(local), None) = (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) else {
return Err(format_error(field, id));
};
if name != kind.name() {
return Err(format_error(field, id));
}
dotted_text(field, root)?;
dotted_within(field, local, local_max(kind))?;
Ok((root, local))
}
/// The local segment of a parent id in `campaign`, of the expected kind.
fn parent_local<'a>(
field: &str,
id: &'a str,
campaign: &str,
kind: PipelineKind,
) -> Result<&'a str, PipelineRefusal> {
let (owner, local) = pipeline_id(field, id, kind)?;
if owner != campaign {
return Err(format_error(field, id));
}
Ok(local)
}
/// The cut label inside a parent local `cut-<label>.<marker><N>`: exactly two
/// parts, the label a `Label` and `<N>` a non-empty run of digits.
fn parent_cut<'a>(field: &str, local: &'a str, marker: char) -> Result<&'a str, PipelineRefusal> {
let invalid = || format_error(field, local);
let mut parts = local.split('.');
let (Some(head), Some(suffix), None) = (parts.next(), parts.next(), parts.next()) else {
return Err(invalid());
};
let cut = head.strip_prefix("cut-").ok_or_else(invalid)?;
label_text(field, cut)?;
let digits = suffix.strip_prefix(marker).ok_or_else(invalid)?;
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(invalid());
}
Ok(cut)
}
/// A `Slug` or an `OrgRepo` entering a local, escaped to one label. `/` is not
/// a `Label` byte, so it cannot survive into a key, and neither may `.`: no
/// local part carries the separator, or the boundary before it would have two
/// readings. A dotted slug and a dotted repo name are both ordinary, so both
/// bytes are escaped rather than refused. The escape has to be injective or two
/// values claim one key: replacing `/` with `_` alone is not, since
/// `GameCult_Epiphany/thing` and `GameCult/Epiphany_thing` both give
/// `GameCult_Epiphany_thing`. So `_` is escaped as well, and every code is two
/// bytes starting with `_`: `_` becomes `__`, `/` becomes `_-`, and `.` becomes
/// `_d`. Every other byte is passed through and is never `_`, so a reader going
/// left to right takes each `_` together with the byte after it and never has a
/// choice to make; the encoding is therefore reversible, and distinct values
/// give distinct segments. The caller validates the value as its own type first
/// and checks the result against the local's rules, which is where an over-long
/// or otherwise unlabelled value is refused.
fn key_segment(value: &str) -> String {
value.replace('_', "__").replace('/', "_-").replace('.', "_d")
}
/// The local of a root. A campaign's or an instance's identity is entirely its
/// root segment, so its local carries none; a reader never consults it.
const ROOT_LOCAL: &str = "self";
/// The bound on a composed local of every kind but a resolution, whole, in
/// UTF-8 bytes. Every stored subject key is within it.
const SUBJECT_LOCAL_MAX: usize = 64;
/// The longest kind name a resolution can name as its subject (`stewardship`),
/// checked against `PipelineKind::ALL` by a test.
const LONGEST_KIND_NAME: usize = "stewardship".len();
/// A `.n<sequence>` part at `u32::MAX`.
const SEQUENCE_PART_MAX: usize = ".n".len() + "4294967295".len();
/// The bound on a resolution's local: a depth-two chain over a maximal
/// subject, `resolution.<kind>.<subject local>.n<seq>.n<seq>`, at `u32::MAX`
/// sequences. Depth is capped at two by admission (Q19), and this bound admits
/// exactly that for every subject, so no subject is born unresolvable. Derived
/// from `SUBJECT_LOCAL_MAX` and the kind names, never written as a number.
const RESOLUTION_LOCAL_MAX: usize = "resolution.".len() + LONGEST_KIND_NAME + ".".len() + SUBJECT_LOCAL_MAX + 2 * SEQUENCE_PART_MAX;
// `<root>:<kind>:<local>` must fit a `Short` wherever an id is cited.
const _: () = assert!(64 + ":".len() + LONGEST_KIND_NAME + ":".len() + RESOLUTION_LOCAL_MAX <= 200);
/// The bound on a kind's composed local, chosen here and nowhere else.
fn local_max(kind: PipelineKind) -> usize {
match kind {
PipelineKind::Resolution => RESOLUTION_LOCAL_MAX,
// `<question label>.n<sequence>`: every question label is answerable.
PipelineKind::DeskAnswer => SUBJECT_LOCAL_MAX + SEQUENCE_PART_MAX,
_ => SUBJECT_LOCAL_MAX,
}
}
/// The schema epoch every pipeline store is written at, owned here with the
/// schemas it names. Evolution is additive and keeps it: a new named field
/// with the `value_types!` `= absent` marker (a replayed batch re-encodes byte
/// for byte, because an absent field is not written), or a widened `PipelineKind`, since each reader ships
/// with the variants it knows and refuses an unknown kind on the kind, not on
/// the epoch. A breaking change bumps it, so that a store written at the old
/// one can be refused by whoever opens it; this crate owns no store and
/// refuses none.
pub const PIPELINE_SCHEMA_EPOCH: &str = "epiphany.pipeline.epoch.v2";
/// Composes and validates a local: every part is a `Label`, and the join is
/// bounded whole by `local_max(kind)`. Both rules live here because this is the only way a local is
/// built; `pipeline_key` has no other path to a key string. Parts are a slice,
/// not a builder, so an arm's arity is written at its call site.
fn local(field: &str, kind: PipelineKind, parts: &[&str]) -> Result<String, PipelineRefusal> {
for part in parts {
label_text(field, part)?;
}
let joined = parts.join(".");
if joined.len() > local_max(kind) {
return Err(format_error(field, &joined));
}
Ok(joined)
}
/// Derives a document's identity key (D1, "Keys: identity, not convenience").
/// Fourteen arms, one exit: every arm names its root and composes its local
/// through `local`, and the key is formatted here and nowhere else.
pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefusal> {
use PipelineDocument as D;
let document_kind = document.kind();
let kind = document_kind.name();
let key_field = format!("{kind}.key");
let campaign_field = format!("{kind}.campaign");
let (root_field, root, composed) = match document {
D::Campaign(value) => ("campaign.slug", value.slug.0.as_str(), local(&key_field, document_kind, &[ROOT_LOCAL])?),
D::Instance(value) => ("instance.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[ROOT_LOCAL])?),
// A resolution is keyed inside its subject's root, with the subject's
// kind and local as its own local and its per-subject sequence last, so
// the subject's resolutions and only they share the prefix
// `<root>:resolution:<kind>.<local>.n`. A resolution's own key is an
// ordinary id, so it composes as a subject like any other; each nesting
// prepends `resolution.` (11 bytes) and appends `.n<s>` (3 bytes for
// one digit). Depth is capped at two by admission (Q19), and
// `RESOLUTION_LOCAL_MAX` admits exactly a depth-two chain over a
// maximal subject, so every subject resolves and its resolution can be
// withdrawn.
D::Resolution(value) => {
let field = "resolution.subject.id";
let (subject_root, subject_local) = pipeline_id(field, &value.subject.id.0, value.subject.kind)?;
let sequence = format!("n{}", value.sequence);
let parts = std::iter::once(value.subject.kind.name()).chain(subject_local.split('.')).chain(std::iter::once(sequence.as_str())).collect::<Vec<_>>();
(field, subject_root, local(&key_field, document_kind, &parts)?)
}
// Stewardship and hand-off hang off an instance rather than a campaign.
// The root is the whole difference; the key shape is the same.
D::Stewardship(value) => {
org_repo_text("stewardship.repo", &value.repo.0)?;
let sequence = format!("n{}", value.sequence);
(
"stewardship.instance",
value.instance.0.as_str(),
local(&key_field, document_kind, &[&key_segment(&value.repo.identity()), &sequence])?,
)
}
D::HandOff(value) => {
dotted_text("hand_off.to_instance", &value.to_instance.0)?;
org_repo_text("hand_off.repo", &value.repo.0)?;
value.handed_on.validate("hand_off.handed_on")?;
(
"hand_off.from_instance",
value.from_instance.0.as_str(),
local(
&key_field,
document_kind,
&[&key_segment(&value.to_instance.0), &key_segment(&value.repo.identity()), &value.handed_on.0],
)?,
)
}
// A run hangs off its instance too: its label is its whole local.
D::Run(value) => ("run.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Pulse(value) => ("pulse.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Reply(value) => ("reply.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Thought(value) => ("thought.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Memory(value) => ("memory.instance", value.instance.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Research(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &value.label.0])?),
D::Idea(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.pass.0, &value.label.0])?),
D::Target(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&format!("r{}", value.revision)])?),
D::Question(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::Ruling(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::FollowUp(value) => (campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[&value.label.0])?),
D::CutSpec(value) => (
campaign_field.as_str(),
value.campaign.0.as_str(),
local(&key_field, document_kind, &[&format!("cut-{}", value.cut.0), &format!("r{}", value.revision)])?,
),
D::CutReport(value) => {
let spec = parent_local("cut_report.cut_spec", &value.cut_spec.0, &value.campaign.0, PipelineKind::CutSpec)?;
let cut = parent_cut("cut_report.cut_spec", spec, 'r')?;
(
campaign_field.as_str(),
value.campaign.0.as_str(),
local(&key_field, document_kind, &[&format!("cut-{cut}"), &format!("h{}", value.attempt)])?,
)
}
D::DeskAnswer(value) => {
let label = parent_local("desk_answer.fork", &value.fork.0, &value.campaign.0, PipelineKind::Question)?;
let sequence = format!("n{}", value.sequence);
(campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &[label, &sequence])?)
}
D::Verdict(value) => {
let report = parent_local("verdict.cut_report", &value.cut_report.0, &value.campaign.0, PipelineKind::CutReport)?;
let cut = parent_cut("verdict.cut_report", report, 'h')?;
(
campaign_field.as_str(),
value.campaign.0.as_str(),
local(&key_field, document_kind, &[&format!("cut-{cut}"), &format!("s{}", value.pass)])?,
)
}
D::Finding(value) => {
let verdict = parent_local("finding.verdict", &value.verdict.0, &value.campaign.0, PipelineKind::Verdict)?;
parent_cut("finding.verdict", verdict, 's')?;
let parts = verdict.split('.').chain(std::iter::once(value.label.0.as_str())).collect::<Vec<_>>();
(campaign_field.as_str(), value.campaign.0.as_str(), local(&key_field, document_kind, &parts)?)
}
};
dotted_text(root_field, root)?;
Ok(format!("{root}:{kind}:{composed}"))
}
/// Bounds, formats, then key recomputation, on the envelope that will be
/// stored, whoever prepared it. It is the whole of what this crate checks
/// before a write; per-kind and cross-document rules are not decided here and
/// belong to the admission path of the organ that will admit these documents,
/// which is expected to call this first rather than re-derive it.
pub fn validate_pipeline_write_envelope(envelope: &CultCacheEnvelope) -> Result<(), PipelineRefusal> {
let document = PipelineDocument::decode(envelope)?;
document.validate()?;
let expected = pipeline_key(&document)?;
if envelope.key != expected {
return Err(PipelineRefusal::InvalidIdentity {
kind: document.kind(),
key: envelope.key.clone(),
expected,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
const CAMPAIGN: &str = "eureka-state";
const INSTANCE: &str = "yggdrasil";
fn s(value: &str) -> Short {
value.into()
}
fn t(value: &str) -> Title {
value.into()
}
fn l(value: &str) -> Label {
value.into()
}
fn slug(value: &str) -> Slug {
value.into()
}
fn id(kind: &str, local: &str) -> Short {
Short(format!("{CAMPAIGN}:{kind}:{local}"))
}
fn sha() -> Sha {
Sha("5f98228d".into())
}
fn date() -> Date {
Date("2026-09-15".into())
}
fn range() -> CommitRange {
CommitRange { base: sha(), head: sha() }
}
fn location() -> CodeLocation {
CodeLocation { path: s("crates/eureka-pipeline/src/lib.rs"), line: 1, end_line: Some(9), symbol: None }
}
fn evidence() -> Evidence {
Evidence { kind: EvidenceKind::Test, locator: "cargo test".into(), result: "ok".into() }
}
fn doc_ref() -> DocRef {
DocRef { path: s("notes/eureka-pipeline-state-target.md"), start_line: 1, end_line: 9, commit: sha() }
}
/// One valid document of every kind, with its expected key.
fn samples() -> Vec<(PipelineDocument, String)> {
use PipelineDocument as D;
let repo = || OrgRepo("GameCult/Epiphany".into());
let branch = || s("codex/eureka-pipeline-state");
vec![
(D::Campaign(PipelineCampaign {
slug: slug(CAMPAIGN), title: t("Eureka pipeline state"), repos: vec![repo()],
working_branch: branch(), target_doc: doc_ref(),
}), format!("{CAMPAIGN}:campaign:self")),
(D::Target(PipelineTarget {
campaign: slug(CAMPAIGN), revision: 2,
invariants: vec![TargetInvariant { label: l("mind-admits"), statement: "Only admission writes.".into() }],
not_in_scope: vec!["Eve browsing".into()], canonical_implementations: vec!["CultLib".into()], repos: vec![], doc: doc_ref(),
}), format!("{CAMPAIGN}:target:r2")),
(D::Question(PipelineQuestion {
campaign: slug(CAMPAIGN), label: l("Q1"), title: t("Who owns the state?"), question: "Who owns the state?".into(),
options: vec![
QuestionOption { label: l("A"), text: "an instance".into() },
QuestionOption { label: l("B"), text: "a repo".into() },
],
recommended: l("A"), depends: vec!["Cut 3a".into()],
raised_in: Some(PipelineRef { kind: PipelineKind::CutSpec, id: id("cut_spec", "cut-3a.r1") }),
asked_on: date(),
}), format!("{CAMPAIGN}:question:Q1")),
(D::Ruling(PipelineRuling {
campaign: slug(CAMPAIGN), label: l("R8"), title: t("An instance owns its mind"), answers: Some(id("question", "Q1")), choice: Some(l("A")),
ruling: "An instance owns its mind.".into(), operator_quote: Some("all recommendations, go ahead".into()),
ruled_on: date(),
precedents: vec![ForeignRef {
repo: OrgRepo("GameCult/Aetheria".into()), commit: FullSha("a".repeat(40)), kind: PipelineKind::Ruling,
id: s("cultcache:ruling:R1"), payload_sha256: Sha256Hex("b".repeat(64)),
}],
authority: RulingAuthority::Operator,
}), format!("{CAMPAIGN}:ruling:R8")),
(D::CutSpec(PipelineCutSpec {
campaign: slug(CAMPAIGN), cut: l("3a"), revision: 1, title: t("Pipeline documents"), repo: repo(),
branch: branch(), base: sha(), depends_on: vec![s("2")], first: vec!["Read the spec.".into()],
deletes: vec![CutDelete { path: s("old.rs"), lines: 3, note: "dead".into() }],
keeps_moves: vec!["commit owner".into()], adds: vec!["pipeline_documents.rs".into()],
file_changes: vec![FileChange { location: location(), change: "add".into() }],
reads: vec![],
authority_map: Some(AuthorityMap {
owner: "core".into(), inputs: vec!["typed documents".into()], outputs: vec!["envelopes".into()],
derived_state: vec!["derived keys".into()], forbidden_writers: vec!["MCP".into()],
shared_paths: vec!["admission".into()], deletion_line: "n/a".into(),
}),
verification: CutVerification {
builds: vec!["cargo check".into()],
tests: vec![VerificationTest { name: s("keys"), pins: "one derived key".into() }],
negative: vec![NegativeCheck { pattern: s("Vec<u8>"), scope: "documents".into() }],
operator: vec!["none".into()],
},
// Cut 3a's real numbers, and in this order: the tripwire test
// reads them back, so a transposition is a failure, not a typo.
estimate: StructuralDelta {
lines_added: 900, lines_removed: 0,
dependencies_added: vec![s("schemars")], dependencies_removed: vec![],
formats_added: vec![s("epiphany.pipeline.*.v1")], formats_removed: vec![],
targets_added: vec![], targets_removed: vec![],
},
rulings: vec![id("ruling", "R8")], questions: vec![id("question", "Q1")],
}), format!("{CAMPAIGN}:cut_spec:cut-3a.r1")),
(D::CutReport(PipelineCutReport {
campaign: slug(CAMPAIGN), cut_spec: id("cut_spec", "cut-3a.r1"), attempt: 1, repo: repo(), branch: branch(),
commits: vec![ReportCommit { sha: sha(), subject: "Add the pipeline documents".into(), builds: true }],
range: range(), verification: vec![evidence()],
mutations: vec![MutationRecord {
// `.into()` on both, not `l()` and `sha()`: the type-level
// mutations (M19, M20) widen these fields to `Short`, and
// a sample spelled with the narrow constructors would stop
// compiling instead of letting the forgery through.
label: "M1".into(), rule: "key derivation".into(), location: location(),
before: "parent_cut(field, spec, 'r')?".into(), after: "spec".into(), commit: "5f98228d".into(),
failed_as_expected: true,
}],
deviations: vec![Deviation { what: "names".into(), why: "glob exports".into() }],
forks: vec![id("question", "Q1")],
structural_delta: StructuralDelta {
lines_added: 900, lines_removed: 0, dependencies_added: vec![s("schemars")],
dependencies_removed: vec![], formats_added: vec![s("epiphany.pipeline.*.v1")],
formats_removed: vec![], targets_added: vec![], targets_removed: vec![],
},
landed_names: vec![LandedName { name: s("PipelineDocument"), path: s("crates/eureka-pipeline/src/lib.rs") }],
undone: vec!["admission".into()],
promises: vec![Promise { label: l("P1"), text: "One derived key per document.".into() }],
}), format!("{CAMPAIGN}:cut_report:cut-3a.h1")),
(D::Verdict(PipelineVerdict {
campaign: slug(CAMPAIGN), cut_report: id("cut_report", "cut-3a.h1"), pass: 2, range: range(),
claims: vec![VerdictClaim {
claim: "A composed key has one source.".into(), outcome: ClaimOutcome::Falsified,
evidence: vec![evidence()], findings: vec![id("finding", "cut-3a.s2.F4")],
promise: Some(l("P1")), mutations: vec![l("M1")],
}],
}), format!("{CAMPAIGN}:verdict:cut-3a.s2")),
(D::Finding(PipelineFinding {
campaign: slug(CAMPAIGN), verdict: id("verdict", "cut-3a.s2"), label: l("F4"), range: range(),
confidence: FindingConfidence::Confirmed, severity: FindingSeverity::High,
claim: "A dotted label composes two keys.".into(), invariants: vec![l("mind-admits")],
locations: vec![location()], failure_scenario: "Two documents claim one key.".into(),
evidence: vec![evidence()], precedents: vec![], origin: FindingOrigin::Introduced,
}), format!("{CAMPAIGN}:finding:cut-3a.s2.F4")),
(D::FollowUp(PipelineFollowUp {
campaign: slug(CAMPAIGN), label: l("FU-4"),
source: PipelineRef { kind: PipelineKind::Finding, id: id("finding", "cut-3a.s2.F4") },
repo: repo(), locations: vec![location()], item: "Per-kind admission rules.".into(),
why_it_can_wait: "The organ owns admission.".into(), owner: s("Hands"),
origin: None,
}), format!("{CAMPAIGN}:follow_up:FU-4")),
(D::Resolution(PipelineResolution {
subject: PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }, sequence: 1,
outcome: ResolutionOutcome::Answered { by: PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") } },
rationale: "Ruled A.".into(), resolved_on: date(),
}), format!("{CAMPAIGN}:resolution:question.Q1.n1")),
// The mind's own three kinds. They are appended rather than
// inserted because the helpers below index this list by position.
(D::Instance(PipelineInstance {
instance: slug(INSTANCE), display_name: s("Yggdrasil mind"), created_at: date(),
host: s("yggdrasil"),
}), format!("{INSTANCE}:instance:self")),
(D::Stewardship(PipelineStewardship {
instance: slug(INSTANCE), repo: repo(), sequence: 1, assigned_on: date(),
note: "The Eureka campaign repo.".into(), names: vec![],
}), format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")),
(D::HandOff(PipelineHandOff {
from_instance: slug(INSTANCE), to_instance: slug("thought-cage"), repo: repo(),
documents: vec![s(CAMPAIGN), id("ruling", "R8")],
reason: "The workstation mind takes the campaign.".into(), handed_on: date(),
}), format!("{INSTANCE}:hand_off:thought-cage.gamecult_-epiphany.{}", date().0)),
(D::Run(PipelineRun {
instance: slug(INSTANCE), label: l("mind-2026-10-09-1"), operated_by: RunOperator::Mind, turn: RunTurn::SelfRun,
host: s("yggdrasil"), started_on: date(), budget_usd: s("7.5"),
claims: vec![PipelineRef { kind: PipelineKind::CutSpec, id: id("cut_spec", "cut-run-kind.r2") }],
campaigns: vec![slug(CAMPAIGN)],
}), format!("{INSTANCE}:run:mind-2026-10-09-1")),
(D::Pulse(PipelinePulse {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: Some(600), tempo_vote_withdrawn: false, urgency_permille: Some(250),
note: "The swarm is idle; a shorter beat.".into(),
}), format!("{INSTANCE}:pulse:2026-10-31t041200-t0")),
(D::Memory(PipelineMemory {
instance: slug(INSTANCE), label: l("2026-10-31t041200-t0-m0"), sort: MemorySort::Committed,
text: "I take the background shaders; Libby holds the sounds.".into(), about: vec![s("@libby:chat")],
origin: MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![s("@libby:chat")] }, sources: vec![s("$e1")], salience_permille: 700, consolidates: vec![],
}), format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")),
(D::Research(PipelineResearch {
campaign: slug(CAMPAIGN), pass: l("p1"), label: l("scope"),
claim: "The embed limit is 4096 tokens with silent truncation.".into(),
detail: "Read from the daemon's index module and probed live.".into(), inferred: false,
sources: vec![
ResearchSource::Repo { repo: repo(), at: DocRef { path: s("crates/huginn-daemon/src/index/mod.rs"), start_line: 50, end_line: 56, commit: sha() } },
ResearchSource::Web { url: "https://zettelkasten.de/atomicity/".into(), work: s("Zettelkasten atomicity"), retrieved_on: date() },
ResearchSource::Observed { evidence: evidence() },
ResearchSource::Document { reference: PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") } },
],
serves: vec![PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }], observed_on: date(),
}), format!("{CAMPAIGN}:research:p1.scope")),
(D::Idea(PipelineIdea {
campaign: slug(CAMPAIGN), pass: l("p1"), label: l("i1"), title: t("One note per claim"),
idea: "Keep a pass as many small notes.".into(), tradeoffs: "More documents to list.".into(),
rests_on: vec![PipelineRef { kind: PipelineKind::Research, id: id("research", "p1.scope") }],
toward: vec![PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") }], proposed_on: date(),
}), format!("{CAMPAIGN}:idea:p1.i1")),
(D::Reply(PipelineReply {
instance: slug(INSTANCE), label: l("mind-2026-10-09-1-r1"),
run: PipelineRef { kind: PipelineKind::Run, id: Short(format!("{INSTANCE}:run:mind-2026-10-09-1")) },
answers: vec![PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "ask-the-self") }],
text: "The shader pass is done; the sounds are Libby's.".into(),
}), format!("{INSTANCE}:reply:mind-2026-10-09-1-r1")),
(D::Thought(PipelineThought {
instance: slug(INSTANCE), label: l("t1793000000"),
text: "The sounds are Libby's; the shaders were never the hard part.".into(),
reads: vec![PipelineRef { kind: PipelineKind::Memory, id: Short(format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0")) }],
persona_receipt: s("mind-commit-0a1b"), identity: "Eureka, she/her, plain-spoken.".into(),
context: "Values: coherence over velocity.".into(), stimulus: "Libby asked about the sounds.".into(),
projector_model: s("projector:latest"), projection: "I keep turning over who holds the sounds.".into(), base_model: s("base:latest"),
temperature_permille: 1000, min_p_permille: 50, max_tokens: 160,
candidates: vec!["The sounds are Libby's; the shaders were never the hard part.".into(), "A second drift.".into()],
nearest_permille: vec![310, 870], chosen: 0, projector_ms: 4200, continuer_ms: 9100,
prompt_version: "ghostlight=c43eb54; words=180".into(),
}), format!("{INSTANCE}:thought:t1793000000")),
(D::DeskAnswer(PipelineDeskAnswer {
campaign: slug(CAMPAIGN), fork: id("question", "Q1"), sequence: 1, options_hash: Sha256Hex("c".repeat(64)),
choice: Some(s("a")), note: Some("Share it, with the mark.".into()), answered_at: s("2026-11-10T15:00:00Z"),
received_at: s("2026-11-10T15:00:03Z"), claim_in_future: false, request_id: RequestId("6f1c0a52-3b7e-4d1a-9c58-0e2d7a4b9f13".into()), body_sha256: Sha256Hex("d".repeat(64)),
answerer: s("user-7"), authority: DeskAuthority::Member,
outcome: DeskOutcome::Revision { of: id("desk_answer", "Q1.n1") },
}), format!("{CAMPAIGN}:desk_answer:Q1.n1")),
]
}
fn instance_sample() -> PipelineInstance {
let PipelineDocument::Instance(instance) = samples().remove(10).0 else { unreachable!() };
instance
}
fn stewardship_sample() -> PipelineStewardship {
let PipelineDocument::Stewardship(stewardship) = samples().remove(11).0 else { unreachable!() };
stewardship
}
fn hand_off_sample() -> PipelineHandOff {
let PipelineDocument::HandOff(hand_off) = samples().remove(12).0 else { unreachable!() };
hand_off
}
fn campaign_sample() -> PipelineDocument {
samples().remove(0).0
}
fn question_sample() -> PipelineQuestion {
let PipelineDocument::Question(question) = samples().remove(2).0 else { unreachable!() };
question
}
fn ruling_sample() -> PipelineRuling {
let PipelineDocument::Ruling(ruling) = samples().remove(3).0 else { unreachable!() };
ruling
}
/// A ruling the Mind's own Self made: authority Mind, no operator quote,
/// no question answered. Built from the ruling sample, not inserted into
/// `samples()`, whose tests pick kinds by index.
fn mind_ruling() -> PipelineRuling {
let mut ruling = ruling_sample();
(ruling.authority, ruling.operator_quote, ruling.answers, ruling.choice) = (RulingAuthority::Mind, None, None, None);
ruling
}
fn steward_ruling() -> PipelineRuling {
let mut ruling = mind_ruling();
ruling.authority = RulingAuthority::Steward {
steward: "@heimdall:chat".into(),
repo: OrgRepo("GameCult/Bifrost".into()),
words: "refuse leading zeros".into(),
event: Some("$ev1".into()),
};
ruling
}
#[test]
fn a_steward_ruling_round_trips_and_leaves_the_unit_authorities_bare_strings() -> Result<()> {
let cache = schema_cache()?;
let document = PipelineDocument::Ruling(steward_ruling());
document.validate()?;
assert_eq!(PipelineDocument::decode(&document.prepare(&cache)?)?, document);
let json = serde_json::to_value(&document)?;
assert_eq!(json["value"]["authority"]["Steward"]["steward"], "@heimdall:chat");
let mut unit = ruling_sample();
unit.authority = RulingAuthority::Defaulted;
assert_eq!(serde_json::to_value(PipelineDocument::Ruling(unit))?["value"]["authority"], "Defaulted");
Ok(())
}
#[test]
fn a_steward_ruling_refuses_blank_words_blank_steward_and_a_bad_repo() {
let refused = |edit: fn(&mut RulingAuthority)| {
let mut ruling = steward_ruling();
edit(&mut ruling.authority);
PipelineDocument::Ruling(ruling).validate().expect_err("refused").to_string()
};
let fields = [
refused(|a| if let RulingAuthority::Steward { words, .. } = a { words.0 = " ".into() }),
refused(|a| if let RulingAuthority::Steward { steward, .. } = a { steward.0 = String::new() }),
refused(|a| if let RulingAuthority::Steward { repo, .. } = a { repo.0 = "nope".into() }),
refused(|a| if let RulingAuthority::Steward { words, .. } = a { words.0 = "w".repeat(4001) }),
refused(|a| if let RulingAuthority::Steward { event, .. } = a { *event = Some("e".repeat(201).as_str().into()) }),
];
for (fault, field) in fields.iter().zip(["authority.words", "authority.steward", "authority.repo", "authority.words", "authority.event"]) {
assert!(fault.contains(field), "{fault}");
}
}
/// A withdrawal is a pulse that says only `tempo_vote_withdrawn`: it must survive
/// encoding to an envelope, JSON and named MessagePack as a withdrawal, and an
/// unwithdrawn pulse must not write the key.
#[test]
fn a_withdrawal_pulse_encodes_and_reads_back_as_a_withdrawal() -> Result<()> {
let cache = schema_cache()?;
let pulse = |withdrawn: bool, urgency: Option<u16>| {
PipelineDocument::Pulse(PipelinePulse { instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: None, tempo_vote_withdrawn: withdrawn, urgency_permille: urgency, note: "".into() })
};
let withdrawal = pulse(true, None);
withdrawal.validate()?;
let read_back = PipelineDocument::decode(&withdrawal.prepare(&cache)?)?;
assert!(matches!(&read_back, PipelineDocument::Pulse(read) if read.tempo_vote_withdrawn), "{read_back:?}");
let json = serde_json::to_value(&withdrawal)?;
assert_eq!(json["value"]["tempo_vote_withdrawn"], true);
assert!(matches!(serde_json::from_value::<PipelineDocument>(json)?, PipelineDocument::Pulse(read) if read.tempo_vote_withdrawn));
let packed = rmp_serde::to_vec_named(&withdrawal)?;
assert!(matches!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, PipelineDocument::Pulse(read) if read.tempo_vote_withdrawn));
let urging = pulse(false, Some(5));
assert!(serde_json::to_value(&urging)?["value"].get("tempo_vote_withdrawn").is_none(), "an unwithdrawn pulse writes no key");
assert!(matches!(PipelineDocument::decode(&urging.prepare(&cache)?)?, PipelineDocument::Pulse(read) if !read.tempo_vote_withdrawn));
Ok(())
}
#[test]
fn a_ruling_with_authority_mind_keys_encodes_and_round_trips_like_any_ruling() -> Result<()> {
let cache = schema_cache()?;
let document = PipelineDocument::Ruling(mind_ruling());
document.validate()?;
let envelope = document.prepare(&cache)?;
assert!(envelope.key.starts_with("eureka-state:ruling:"), "{}", envelope.key);
assert_eq!(PipelineDocument::decode(&envelope)?, document);
let json = serde_json::to_value(&document)?;
assert_eq!(json["value"]["authority"], "Mind");
assert_eq!(serde_json::from_value::<PipelineDocument>(json)?, document);
let packed = rmp_serde::to_vec_named(&document)?;
assert_eq!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, document);
let standing = PipelineDocument::Ruling(ruling_sample());
assert_ne!(standing.prepare(&cache)?.payload, envelope.payload, "Mind is not a spelling of another authority");
Ok(())
}
#[test]
fn unit_enum_names_are_the_variants_in_declaration_order() {
assert_eq!(RulingAuthority::NAMES, ["Operator", "Standing", "Defaulted", "Mind", "Steward"]);
assert_eq!(FindingOrigin::NAMES, ["Introduced", "PreExisting"]);
}
fn report_sample() -> PipelineCutReport {
let PipelineDocument::CutReport(report) = samples().remove(5).0 else { unreachable!() };
report
}
fn verdict_sample() -> PipelineVerdict {
let PipelineDocument::Verdict(verdict) = samples().remove(6).0 else { unreachable!() };
verdict
}
fn finding_sample() -> PipelineFinding {
let PipelineDocument::Finding(finding) = samples().remove(7).0 else { unreachable!() };
finding
}
fn resolution_sample() -> PipelineResolution {
let PipelineDocument::Resolution(resolution) = samples().remove(9).0 else { unreachable!() };
resolution
}
/// The golden corpus: every sample plus one document per encoding-relevant
/// variant (every enum variant, every Option both ways), each under a
/// unique label that names what it varies.
fn golden_documents() -> Vec<(String, PipelineDocument)> {
use PipelineDocument as D;
let mut out: Vec<(String, PipelineDocument)> =
samples().into_iter().map(|(d, _)| (format!("sample.{}", d.kind().name()), d)).collect();
let by = |kind: PipelineKind, kind_name: &str, local: &str| PipelineRef { kind, id: id(kind_name, local) };
let resolution = |outcome: ResolutionOutcome| {
let mut r = resolution_sample();
r.outcome = outcome;
D::Resolution(r)
};
let ruling_ref = || by(PipelineKind::Ruling, "ruling", "R8");
out.push(("resolution.superseded".into(), resolution(ResolutionOutcome::Superseded { by: vec![ruling_ref(), by(PipelineKind::Question, "question", "Q1")] })));
out.push(("resolution.fixed-by-none".into(), resolution(ResolutionOutcome::Fixed { commit: sha(), by: None })));
out.push(("resolution.fixed-by-some".into(), resolution(ResolutionOutcome::Fixed { commit: sha(), by: Some(by(PipelineKind::CutReport, "cut_report", "cut-3a.h1")) })));
out.push(("resolution.deferred".into(), resolution(ResolutionOutcome::Deferred { to: by(PipelineKind::FollowUp, "follow_up", "FU-4") })));
out.push(("resolution.recorded".into(), resolution(ResolutionOutcome::Recorded { reason: "Noted.".into() })));
out.push(("resolution.withdrawn".into(), resolution(ResolutionOutcome::Withdrawn { reason: "Mistaken.".into() })));
out.push(("resolution.slept".into(), resolution(ResolutionOutcome::Slept { line: "Let the rest go.".into(), day_through: Some(by(PipelineKind::Memory, "memory", "m-1")) })));
for (name, authority) in [("standing", RulingAuthority::Standing), ("defaulted", RulingAuthority::Defaulted)] {
let mut r = ruling_sample();
r.authority = authority;
out.push((format!("ruling.authority-{name}"), D::Ruling(r)));
}
let mut r = ruling_sample();
(r.answers, r.choice, r.operator_quote, r.precedents) = (None, None, None, vec![]);
out.push(("ruling.options-none".into(), D::Ruling(r)));
let mut q = question_sample();
q.raised_in = None;
out.push(("question.raised-in-none".into(), D::Question(q)));
let PipelineDocument::CutSpec(mut spec) = samples().remove(4).0 else { unreachable!() };
spec.authority_map = None;
out.push(("cut_spec.authority-map-none".into(), D::CutSpec(spec)));
for (name, outcome) in [("holds", ClaimOutcome::Holds), ("unproven", ClaimOutcome::Unproven)] {
let mut v = verdict_sample();
v.claims[0].outcome = outcome;
v.claims[0].promise = None;
out.push((format!("verdict.claim-{name}-promise-none"), D::Verdict(v)));
}
let mut f = finding_sample();
(f.confidence, f.severity, f.origin) = (FindingConfidence::Plausible, FindingSeverity::Blocker, FindingOrigin::PreExisting);
f.locations[0].end_line = None;
f.evidence = [EvidenceKind::Command, EvidenceKind::Mutation, EvidenceKind::Probe, EvidenceKind::SourceRead, EvidenceKind::Capture]
.into_iter().map(|kind| Evidence { kind, locator: "x".into(), result: "y".into() }).collect();
out.push(("finding.plausible-blocker-preexisting-kinds".into(), D::Finding(f)));
for (name, severity) in [("medium", FindingSeverity::Medium), ("low", FindingSeverity::Low)] {
let mut f = finding_sample();
f.severity = severity;
out.push((format!("finding.severity-{name}"), D::Finding(f)));
}
out.push(("ruling.authority-mind".into(), D::Ruling(mind_ruling())));
out.push(("ruling.authority-steward".into(), D::Ruling(steward_ruling())));
let PipelineDocument::CutSpec(mut spec) = samples().remove(4).0 else { unreachable!() };
spec.file_changes[0].location.symbol = Some("value_types".into());
spec.reads = vec![
ReadAnchor { location: CodeLocation { path: s("crates/eureka-pipeline/src/lib.rs"), line: 10, end_line: Some(20), symbol: Some("PipelineCutSpec".into()) }, why: "the document shape".into() },
ReadAnchor { location: CodeLocation { path: s("crates/eureka-pipeline/src/lib.rs"), line: 30, end_line: None, symbol: None }, why: "one line".into() },
];
out.push(("cut_spec.reads-and-symbol".into(), D::CutSpec(spec)));
let mut f = finding_sample();
f.locations[0].symbol = Some("&1153090498".into());
out.push(("finding.location-symbol".into(), D::Finding(f)));
let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() };
target.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("GameCult/Huginn".into())];
out.push(("target.repos".into(), D::Target(target)));
let mut stewardship = stewardship_sample();
stewardship.names = vec![s("BDS"), s("Bad Day Simulator")];
out.push(("stewardship.names".into(), D::Stewardship(stewardship)));
out.push(("resolution.moved".into(), resolution(ResolutionOutcome::Moved { to: Slug("heimdall".into()) })));
out
}
/// One line per document: `label key type schema payload-hex`.
fn golden_lines() -> Result<Vec<String>> {
let cache = schema_cache()?;
golden_documents()
.into_iter()
.map(|(label, document)| {
document.validate()?;
let envelope = document.prepare(&cache)?;
let hex: String = envelope.payload.iter().map(|b| format!("{b:02x}")).collect();
Ok(format!("{label} {} {} {} {hex}", envelope.key, envelope.r#type, document.kind().schema_name()))
})
.collect()
}
/// The wire is pinned to bytes. `golden/envelopes.txt` was derived once
/// from this crate at its copy of Epiphany ef956865 (huginn 87b6455), which
/// was proved byte-identical to the live mind store and to Epiphany; see
/// its header. Payload bytes are load-bearing (`ForeignRef.payload_sha256`
/// hashes them), and the cultcache type id and schema name are how a store
/// resolves a persisted entry. A deliberate wire change re-derives the file
/// with `golden_dump` below, in the same commit as a ruling that allows it.
#[test]
fn encoded_envelopes_match_the_committed_golden() -> Result<()> {
let golden = include_str!("../golden/envelopes.txt");
let expected: Vec<&str> = golden.lines().filter(|line| !line.starts_with('#') && !line.is_empty()).collect();
let actual = golden_lines()?;
assert_eq!(actual.len(), expected.len(), "the corpus and the golden file have the same documents");
for (actual, expected) in actual.iter().zip(expected) {
assert_eq!(actual, expected);
}
let kinds: BTreeSet<&str> = actual.iter().filter_map(|line| line.split(' ').nth(2)).collect();
assert_eq!(kinds.len(), PipelineKind::ALL.len(), "every kind's type id is in the golden");
Ok(())
}
/// Prints the golden body to the file named by `GOLDEN_OUT`. Not a check:
/// run only for a deliberate wire change, then paste under the header.
#[test]
#[ignore = "re-derives golden/envelopes.txt; run only for a ruled wire change"]
fn golden_dump() -> Result<()> {
let out = std::env::var("GOLDEN_OUT").expect("GOLDEN_OUT names the output file");
std::fs::write(out, golden_lines()?.join("
") + "
")?;
Ok(())
}
fn run_sample() -> PipelineRun {
let PipelineDocument::Run(run) = samples().remove(13).0 else { unreachable!() };
run
}
fn run_key(run: PipelineRun) -> Result<String, PipelineRefusal> {
pipeline_key(&PipelineDocument::Run(run))
}
/// Identity: a run is its instance and its label, and no other field moves
/// its key, so two launches under one label are one key.
#[test]
fn a_run_keys_off_its_instance_and_label_and_nothing_else() -> Result<()> {
let base = run_sample();
let key = run_key(base.clone())?;
assert_eq!(key, format!("{INSTANCE}:run:mind-2026-10-09-1"));
let mut varied = base.clone();
varied.operated_by = RunOperator::Operator;
varied.turn = RunTurn::PersonaTurn;
varied.host = s("starfire");
varied.started_on = Date("2026-10-10".into());
varied.budget_usd = s("0");
varied.claims = vec![];
varied.campaigns = vec![];
assert_eq!(run_key(varied)?, key, "no other field is part of the key");
let mut relabelled = base.clone();
relabelled.label = l("mind-2026-10-09-2");
assert_ne!(run_key(relabelled)?, key);
let mut moved = base;
moved.instance = slug("thought-cage");
assert_ne!(run_key(moved)?, key);
Ok(())
}
/// Claims name work: a cut spec, a finding or a follow-up, and every other
/// kind is refused at validate, whatever its id.
#[test]
fn a_run_claims_only_a_cut_spec_a_finding_or_a_follow_up() {
for kind in PipelineKind::ALL.iter().copied() {
let mut run = run_sample();
run.claims = vec![PipelineRef { kind, id: id(kind.name(), "x") }];
let outcome = PipelineDocument::Run(run).validate();
if matches!(kind, PipelineKind::CutSpec | PipelineKind::Finding | PipelineKind::FollowUp) {
assert_eq!(outcome, Ok(()), "{kind:?} is claimable");
} else {
assert!(
matches!(&outcome, Err(PipelineRefusal::InvalidFormat { field, value }) if field == "run.claims[0].kind" && value == kind.name()),
"{kind:?} is not claimable, got {outcome:?}"
);
}
}
}
/// A run claims an item once: a repeated claim is refused at validate.
#[test]
fn a_run_cannot_claim_one_item_twice() {
let claim_one = || PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F1") };
let claim = claim_one();
let other = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F2") };
let mut run = run_sample();
run.claims = vec![claim.clone(), other.clone()];
assert_eq!(PipelineDocument::Run(run.clone()).validate(), Ok(()));
run.claims = vec![claim.clone(), other, claim];
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.claims[2]"));
let third = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "F3") };
let mut run = run_sample();
run.claims = vec![claim_one(), third.clone(), third];
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.claims[2]"));
}
/// The budget has one spelling per amount.
#[test]
fn a_runs_budget_is_a_canonical_decimal() {
let validated = |budget: &str| {
let mut run = run_sample();
run.budget_usd = s(budget);
PipelineDocument::Run(run).validate()
};
for good in ["0", "1", "12", "7.5", "0.25", "100.001"] {
assert_eq!(validated(good), Ok(()), "{good}");
}
for bad in ["", "00", "01", "1.", "1.0", "7.50", "0.0", ".5", "-1", "+1", "1e3", "1,5", " 1", "1 ", "1.2.3"] {
assert!(
matches!(validated(bad), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "run.budget_usd"),
"{bad:?} is refused"
);
}
}
/// A name must be able to match a whole word; a stewardship with none writes what it wrote before names existed.
#[test]
fn a_stewardships_names_are_words_and_absent_when_empty() -> Result<()> {
let validated = |names: Vec<&str>| {
let mut stewardship = stewardship_sample();
stewardship.names = names.into_iter().map(s).collect();
PipelineDocument::Stewardship(stewardship).validate()
};
assert_eq!(validated(vec!["BDS", "Bad Day Simulator", "moonlight-ios", "3D"]), Ok(()));
for bad in ["", " ", "BDS ", " BDS", "--", "\tBDS"] {
assert!(matches!(validated(vec!["ok", bad]), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "stewardship.names[1]"), "{bad:?} is refused");
}
let nine: Vec<&str> = vec!["a"; 9];
assert!(validated(nine).is_err(), "a ninth name is refused");
assert_eq!(validated(vec!["a"; 8]), Ok(()));
let json = serde_json::to_value(stewardship_sample())?;
assert!(json.get("names").is_none(), "no names writes no field");
Ok(())
}
/// A reply has words, comes from a run, and answers only follow-ups.
#[test]
fn a_reply_has_words_a_run_and_follow_ups() {
let validated = |change: &dyn Fn(&mut PipelineReply)| {
let PipelineDocument::Reply(mut reply) = samples().remove(18).0 else { unreachable!() };
change(&mut reply);
PipelineDocument::Reply(reply).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
assert_eq!(validated(&|reply| reply.answers.clear()), Ok(()), "a reply may answer nothing");
for (change, field) in [
(&(|reply: &mut PipelineReply| reply.text = "".into()) as &dyn Fn(&mut PipelineReply), "reply.text"),
(&|reply: &mut PipelineReply| reply.text = "
".into(), "reply.text"),
(&|reply: &mut PipelineReply| reply.run = PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", "ask-the-self") }, "reply.run.kind"),
(&|reply: &mut PipelineReply| reply.answers[0] = PipelineRef { kind: PipelineKind::Run, id: Short(format!("{INSTANCE}:run:mind-2026-10-09-1")) }, "reply.answers[0].kind"),
] {
assert!(matches!(validated(change), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{field}");
}
}
/// A thought is its chosen candidate, with one nearness per candidate, in bounds.
#[test]
fn malformed_thought_refused() {
let validated = |change: &dyn Fn(&mut PipelineThought)| {
let PipelineDocument::Thought(mut thought) = samples().remove(19).0 else { unreachable!() };
change(&mut thought);
PipelineDocument::Thought(thought).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
assert_eq!(validated(&|thought| { thought.chosen = 1; thought.text = thought.candidates[1].0.as_str().into(); }), Ok(()));
assert_eq!(validated(&|thought| { thought.candidates.truncate(1); thought.nearest_permille.truncate(1); }), Ok(()));
assert_eq!(validated(&|thought| thought.nearest_permille[0] = 1000), Ok(()), "1000 means no earlier thought");
let four = |thought: &mut PipelineThought| {
thought.candidates = vec![thought.text.0.as_str().into(); 4];
thought.nearest_permille = vec![0; 4];
};
assert_eq!(validated(&four), Ok(()), "four candidates are the most there may be");
for (change, field) in [
(&(|thought: &mut PipelineThought| { thought.text = " \n".into(); thought.candidates[0] = " \n".into(); }) as &dyn Fn(&mut PipelineThought), "thought.text"),
(&|thought: &mut PipelineThought| thought.text = "Another thing entirely.".into(), "thought.text"),
(&|thought: &mut PipelineThought| { thought.nearest_permille.pop(); }, "thought.nearest_permille"),
(&|thought: &mut PipelineThought| thought.nearest_permille[1] = 1001, "thought.nearest_permille[1]"),
(&|thought: &mut PipelineThought| thought.chosen = 2, "thought.chosen"),
(&|thought: &mut PipelineThought| thought.prompt_version = " ".into(), "thought.prompt_version"),
(&|thought: &mut PipelineThought| { thought.candidates.clear(); thought.nearest_permille.clear(); }, "thought.candidates"),
] {
assert!(matches!(validated(change), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{field}");
}
let five = |thought: &mut PipelineThought| {
thought.candidates = vec![thought.text.0.as_str().into(); 5];
thought.nearest_permille = vec![0; 5];
};
assert!(matches!(validated(&five), Err(PipelineRefusal::FieldBound { .. })), "five candidates are over the bound");
}
/// A pulse votes a tempo in bounds, declares an urgency in bounds, or does both; a pulse that does neither is refused.
#[test]
fn a_pulse_is_in_bounds_and_says_something() {
let validated = |beat: Option<u32>, urgency: Option<u16>| {
PipelineDocument::Pulse(PipelinePulse { instance: slug(INSTANCE), label: l("2026-10-31t041200-t0"), tempo_vote_beat_s: beat, tempo_vote_withdrawn: false, urgency_permille: urgency, note: "".into() }).validate()
};
for (beat, urgency) in [(Some(60), None), (Some(86_400), None), (None, Some(0)), (None, Some(1000)), (Some(600), Some(250))] {
assert_eq!(validated(beat, urgency), Ok(()), "{beat:?} {urgency:?}");
}
for (beat, urgency, field) in [
(Some(59), None, "pulse.tempo_vote_beat_s"),
(Some(86_401), None, "pulse.tempo_vote_beat_s"),
(None, Some(1001), "pulse.urgency_permille"),
(None, None, "pulse.tempo_vote_beat_s"),
] {
assert!(matches!(validated(beat, urgency), Err(PipelineRefusal::InvalidFormat { field: f, .. }) if f == field), "{beat:?} {urgency:?}");
}
}
fn memory_sample() -> PipelineMemory {
let PipelineDocument::Memory(memory) = samples().remove(15).0 else { unreachable!() };
memory
}
/// A memory says something, its salience is in thousandths, and it consolidates only memories.
#[test]
fn a_memory_says_something_in_bounds_and_consolidates_only_memories() {
let validated = |change: &dyn Fn(&mut PipelineMemory)| {
let mut memory = memory_sample();
change(&mut memory);
PipelineDocument::Memory(memory).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
assert_eq!(validated(&|memory| memory.salience_permille = 1000), Ok(()));
let bad = |outcome: Result<(), PipelineRefusal>, expected: &str| {
assert!(matches!(outcome, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == expected), "{expected}");
};
bad(validated(&|memory| memory.text = " ".into()), "memory.text");
bad(validated(&|memory| memory.text = "".into()), "memory.text");
bad(validated(&|memory| memory.salience_permille = 1001), "memory.salience_permille");
bad(validated(&|memory| memory.consolidates = vec![PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") }]), "memory.consolidates[0].kind");
let own = PipelineRef { kind: PipelineKind::Memory, id: Short(format!("{INSTANCE}:memory:older")) };
assert_eq!(validated(&|memory| memory.consolidates = vec![own.clone()]), Ok(()));
assert!(matches!(validated(&|memory| memory.about = vec![s("@a:chat"); 9]), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
assert!(matches!(validated(&|memory| memory.sources = vec![s("$e"); 9]), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
assert!(matches!(validated(&|memory| memory.text = Line("x".repeat(1001))), Err(PipelineRefusal::FieldBound { limit: 1000, .. })));
}
fn research_sample() -> PipelineResearch {
let PipelineDocument::Research(research) = samples().remove(16).0 else { unreachable!() };
research
}
fn idea_sample() -> PipelineIdea {
let PipelineDocument::Idea(idea) = samples().remove(17).0 else { unreachable!() };
idea
}
/// A research note has a claim and at least one source; each source is bounded.
#[test]
fn research_needs_a_source_and_a_claim() {
let validated = |change: &dyn Fn(&mut PipelineResearch)| {
let mut research = research_sample();
change(&mut research);
PipelineDocument::Research(research).validate()
};
assert_eq!(validated(&|_| {}), Ok(()));
let bad = |result: Result<(), PipelineRefusal>, field: &str| {
assert!(matches!(&result, Err(PipelineRefusal::InvalidFormat { field: found, .. }) if found == field), "{field}: {result:?}");
};
bad(validated(&|research| research.sources = vec![]), "research.sources");
bad(validated(&|research| research.claim = " ".into()), "research.claim");
bad(validated(&|research| research.claim = "".into()), "research.claim");
let source_bad = |source: ResearchSource, field: &str| {
bad(validated(&|research| research.sources = vec![source.clone()]), &format!("research.sources[0].{field}"));
};
source_bad(ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s(" "), start_line: 1, end_line: 2, commit: sha() } }, "at.path");
source_bad(ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s("a.rs"), start_line: 5, end_line: 4, commit: sha() } }, "at.end_line");
assert_eq!(validated(&|research| research.sources = vec![ResearchSource::Repo { repo: OrgRepo("GameCult/Huginn".into()), at: DocRef { path: s("a.rs"), start_line: 5, end_line: 5, commit: sha() } }]), Ok(()));
source_bad(ResearchSource::Web { url: " ".into(), work: s("w"), retrieved_on: date() }, "url");
source_bad(ResearchSource::Web { url: "https://x.test/".into(), work: s(""), retrieved_on: date() }, "work");
source_bad(ResearchSource::Observed { evidence: Evidence { kind: EvidenceKind::Probe, locator: " ".into(), result: "found".into() } }, "evidence.locator");
source_bad(ResearchSource::Observed { evidence: Evidence { kind: EvidenceKind::Probe, locator: "ran it".into(), result: "".into() } }, "evidence.result");
let own = research_sample();
let cites = |label: &str| PipelineRef { kind: PipelineKind::Research, id: Short(format!("{}:research:{}.{label}", own.campaign.0, own.pass.0)) };
source_bad(ResearchSource::Document { reference: cites(&own.label.0) }, "reference");
assert_eq!(validated(&|research| research.sources = vec![ResearchSource::Document { reference: cites("another") }]), Ok(()));
assert_eq!(validated(&|research| research.inferred = true), Ok(()));
assert!(matches!(validated(&|research| research.sources = vec![research_sample().sources.remove(2); 17]), Err(PipelineRefusal::FieldBound { limit: 16, .. })));
assert!(validated(&|research| research.sources = vec![ResearchSource::Web { url: Line("x".repeat(1001)), work: s("w"), retrieved_on: date() }]).is_err());
}
/// A blank idea is refused; an idea that serves and rests on nothing is admitted.
#[test]
fn idea_needs_text_and_may_serve_nothing() {
let validated = |change: &dyn Fn(&mut PipelineIdea)| {
let mut idea = idea_sample();
change(&mut idea);
PipelineDocument::Idea(idea).validate()
};
assert_eq!(validated(&|idea| (idea.rests_on, idea.toward) = (vec![], vec![])), Ok(()));
assert!(matches!(validated(&|idea| idea.idea = " ".into()), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "idea.idea"));
}
/// Both are keyed under their campaign by pass and label.
#[test]
fn research_and_idea_are_keyed_by_pass_and_label() -> Result<()> {
assert_eq!(pipeline_key(&PipelineDocument::Research(research_sample()))?, format!("{CAMPAIGN}:research:p1.scope"));
assert_eq!(pipeline_key(&PipelineDocument::Idea(idea_sample()))?, format!("{CAMPAIGN}:idea:p1.i1"));
Ok(())
}
/// A memory's origin is bounded field by field, and a private one names whom she heard it from.
#[test]
fn a_memory_origin_is_bounded_and_a_direct_one_names_who_she_heard_it_from() {
let validated = |origin: MemoryOrigin| {
let mut memory = memory_sample();
memory.origin = origin;
PipelineDocument::Memory(memory).validate()
};
let long = Short("r".repeat(201));
let bound_200 = |outcome: Result<(), PipelineRefusal>| assert!(matches!(outcome, Err(PipelineRefusal::FieldBound { limit: 200, .. })), "{outcome:?}");
let bound_8 = |outcome: Result<(), PipelineRefusal>| assert!(matches!(outcome, Err(PipelineRefusal::FieldBound { limit: 8, .. })), "{outcome:?}");
assert_eq!(validated(MemoryOrigin::Wake), Ok(()));
assert_eq!(validated(MemoryOrigin::SelfRun { run: s("run-7") }), Ok(()));
assert_eq!(validated(MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![s("@a:chat"); 8] }), Ok(()));
assert_eq!(validated(MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![s("@a:chat"); 8] }), Ok(()));
bound_200(validated(MemoryOrigin::Room { rooms: long.clone(), heard_from: vec![] }));
bound_8(validated(MemoryOrigin::Room { rooms: s("swarm"), heard_from: vec![s("@a:chat"); 9] }));
bound_200(validated(MemoryOrigin::Direct { room: long.clone(), with: vec![s("@a:chat")] }));
bound_8(validated(MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![s("@a:chat"); 9] }));
bound_200(validated(MemoryOrigin::SelfRun { run: long }));
assert!(
matches!(validated(MemoryOrigin::Direct { room: s("!dm:chat"), with: vec![] }), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "memory.origin.with"),
"a private memory names no one"
);
}
/// A memory is instance-rooted and its label is its whole local.
#[test]
fn a_memory_is_keyed_under_its_instance_by_its_label() -> Result<()> {
assert_eq!(pipeline_key(&PipelineDocument::Memory(memory_sample()))?, format!("{INSTANCE}:memory:2026-10-31t041200-t0-m0"));
Ok(())
}
/// Bounds: a run's campaigns and claims stop at their maximums.
#[test]
fn a_runs_lists_are_bounded() {
let mut run = run_sample();
run.campaigns = (0..9).map(|n| slug(&format!("c{n}"))).collect();
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::FieldBound { limit: 8, .. })));
let mut run = run_sample();
run.claims = (0..65).map(|n| PipelineRef { kind: PipelineKind::FollowUp, id: id("follow_up", &format!("F{n}")) }).collect();
assert!(matches!(PipelineDocument::Run(run).validate(), Err(PipelineRefusal::FieldBound { limit: 64, .. })));
}
/// every-subject-resolvable (ruling key-bound): at the widest instance and
/// label a run's id fits a `Short`, and its resolution, and that
/// resolution's withdrawal, key within the 111-byte resolution local.
#[test]
fn the_widest_run_id_fits_a_short_and_resolves_two_deep() -> Result<()> {
let mut run = run_sample();
run.instance = slug(&"i".repeat(64));
run.label = l(&"a".repeat(64));
let run_id = run_key(run)?;
assert_eq!(run_id.len(), 64 + ":run:".len() + 64);
assert!(run_id.len() <= 200);
PipelineRef { kind: PipelineKind::Run, id: Short(run_id.clone()) }.validate_ref()?;
let one = resolved_at(PipelineKind::Run, &run_id, u32::MAX)?;
let two = resolved_at(PipelineKind::Resolution, &one, u32::MAX)?;
assert!(two.len() <= 200);
Ok(())
}
/// The live registrar plus the foreign stand-in, which only the tests
/// register.
fn schema_cache() -> Result<CultCache> {
let mut cache = CultCache::new();
register_pipeline_document_types(&mut cache)?;
cache.register_entry_type::<ForeignDocument>()?;
Ok(cache)
}
#[test]
fn every_pipeline_kind_round_trips_through_named_slot_zero() -> Result<()> {
let cache = schema_cache()?;
let samples = samples();
let kinds = samples.iter().map(|(document, _)| document.kind()).collect::<BTreeSet<_>>();
assert_eq!(kinds.len(), PipelineKind::ALL.len(), "every kind has a sample");
for (document, _) in samples {
document.validate()?;
let envelope = document.prepare(&cache)?;
assert_eq!(envelope.r#type, document.kind().type_id());
assert_eq!(envelope.payload[0], 0x91, "{:?} payload is a one-element array", document.kind());
assert!(
matches!(envelope.payload[1], 0x80..=0x8f | 0xde | 0xdf),
"{:?} slot 0 is a named map",
document.kind()
);
assert_eq!(PipelineDocument::decode(&envelope)?, document);
Ok::<_, anyhow::Error>(())?;
}
Ok(())
}
/// Soul F2: a decode is type-matched in both directions. The round-trip
/// test above pins the positive match; this pins the refusal, so an
/// envelope belonging to another kind can never be decoded as whichever
/// pipeline kind happens to parse its payload. The commit receipt is the
/// sharp case: a mind's store may legitimately hold one, and it is still
/// not a document; `ForeignDocument` stands in for it here.
#[test]
fn decode_refuses_an_envelope_of_a_foreign_type() -> Result<()> {
let cache = schema_cache()?;
let foreign = <ForeignDocument as DatabaseEntry>::TYPE;
assert!(!foreign.starts_with("epiphany.pipeline."), "{foreign} is a foreign type id");
let mut envelope = campaign_sample().prepare(&cache)?;
envelope.r#type = foreign.into();
assert_eq!(
PipelineDocument::decode(&envelope),
Err(PipelineRefusal::ForeignStore { r#type: foreign.into() })
);
Ok(())
}
#[test]
fn bounds_refuse_in_utf8_bytes() {
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
campaign.title = Title("é".repeat(100));
assert_eq!(PipelineDocument::Campaign(campaign.clone()).validate(), Ok(()));
campaign.title = Title(format!("{}a", "é".repeat(100)));
assert_eq!(
PipelineDocument::Campaign(campaign.clone()).validate(),
Err(PipelineRefusal::FieldBound { field: "campaign.title".into(), limit: 200, actual: 201 })
);
campaign.title = t("ok");
campaign.repos = vec![OrgRepo("GameCult/Epiphany".into()); 9];
assert_eq!(
PipelineDocument::Campaign(campaign).validate(),
Err(PipelineRefusal::FieldBound { field: "campaign.repos".into(), limit: 8, actual: 9 })
);
}
/// RS-L closing fix 4: `campaign.repos` refuses two entries naming one
/// repository by identity, not only by raw spelling, so `GameCult/Epiphany`
/// and `gamecult/epiphany` in one list refuse exactly as the same spelling
/// twice would. Two genuinely distinct repos still validate.
#[test]
fn campaign_repos_refuse_duplicates_by_identity() {
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
campaign.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("gamecult/epiphany".into())];
assert_eq!(
PipelineDocument::Campaign(campaign.clone()).validate(),
Err(PipelineRefusal::DuplicateRepo {
field: "campaign.repos".into(),
repo: "gamecult/epiphany".into(),
})
);
campaign.repos = vec![OrgRepo("GameCult/Epiphany".into()); 2];
assert_eq!(
PipelineDocument::Campaign(campaign.clone()).validate(),
Err(PipelineRefusal::DuplicateRepo {
field: "campaign.repos".into(),
repo: "GameCult/Epiphany".into(),
})
);
campaign.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("GameCult/Aetheria".into())];
assert_eq!(PipelineDocument::Campaign(campaign).validate(), Ok(()));
}
#[test]
fn repo_fields_must_be_org_slash_repo() {
let mut report = report_sample();
report.repo = OrgRepo("Epiphany".into());
assert_eq!(
PipelineDocument::CutReport(report).validate(),
Err(PipelineRefusal::InvalidFormat { field: "cut_report.repo".into(), value: "Epiphany".into() })
);
let PipelineDocument::CutSpec(mut spec) = samples().remove(4).0 else { unreachable!() };
spec.repo = OrgRepo("GameCult/Epiphany/extra".into());
assert!(matches!(
PipelineDocument::CutSpec(spec).validate(),
Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "cut_spec.repo"
));
let PipelineDocument::FollowUp(mut follow_up) = samples().remove(8).0 else { unreachable!() };
follow_up.repo = OrgRepo("/Epiphany".into());
assert!(matches!(
PipelineDocument::FollowUp(follow_up).validate(),
Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "follow_up.repo"
));
}
#[test]
fn keys_are_derived_and_mismatch_refuses() -> Result<()> {
let cache = schema_cache()?;
for (document, key) in samples() {
assert_eq!(pipeline_key(&document), Ok(key.clone()));
let mut envelope = document.prepare(&cache)?;
envelope.key = format!("{key}-forged");
assert_eq!(
validate_pipeline_write_envelope(&envelope),
Err(PipelineRefusal::InvalidIdentity { kind: document.kind(), key: envelope.key, expected: key })
);
}
let mut resolution = resolution_sample();
let answered = pipeline_key(&PipelineDocument::Resolution(resolution.clone()));
resolution.outcome = ResolutionOutcome::Withdrawn { reason: "moot".into() };
assert_eq!(
pipeline_key(&PipelineDocument::Resolution(resolution)),
answered,
"a subject has one resolution key whatever the outcome"
);
Ok(())
}
/// F2, and Soul's second pass: a composed local segments one way only. The
/// rule -- no local part carries the separator -- is pinned on the composer
/// in `no_local_part_carries_the_separator`; the pairs below are the ones
/// Soul measured through the documents, and each is a pair only because the
/// rule holds.
#[test]
fn composed_keys_cannot_collide() {
let key = |document: PipelineDocument| pipeline_key(&document);
// Soul's hand-off pair. A dotted slug is ordinary on the receiver side
// and a dotted repo name (the part after the mandatory `/`, GitHub's
// grammar owns no dot on the owner side) is ordinary on the repo side,
// so both are escaped: left unescaped these two both key to
// `yggdrasil:hand_off:thought-cage.GameCult.Epiphany_-thing.2026-09-15`.
let handed = |to: &str, repo: &str| {
let mut hand_off = hand_off_sample();
hand_off.to_instance = slug(to);
hand_off.repo = OrgRepo(repo.into());
key(PipelineDocument::HandOff(hand_off))
};
let dotted_receiver = handed("thought-cage.GameCult", "Epiphany/thing");
let dotted_repo = handed("thought-cage", "GameCult/Epiphany.thing");
assert_eq!(
dotted_receiver,
Ok(format!("{INSTANCE}:hand_off:thought-cage_dGameCult.epiphany_-thing.2026-09-15"))
);
assert_eq!(
dotted_repo,
Ok(format!("{INSTANCE}:hand_off:thought-cage.gamecult_-epiphany_dthing.2026-09-15"))
);
assert_ne!(dotted_receiver, dotted_repo, "a dotted repo and a dotted receiver cannot claim one key");
let mut wide_label = finding_sample();
wide_label.label = l("F1.G");
assert!(
matches!(key(PipelineDocument::Finding(wide_label)), Err(PipelineRefusal::InvalidFormat { .. })),
"a finding label carrying a dot is refused, not silently composed"
);
let mut deep_verdict = finding_sample();
deep_verdict.verdict = id("verdict", "cut-3a.s1.F1");
deep_verdict.label = l("G");
assert!(
matches!(key(PipelineDocument::Finding(deep_verdict)), Err(PipelineRefusal::InvalidFormat { .. })),
"a verdict local that is not cut-<label>.s<N> is refused"
);
let mut plain = finding_sample();
plain.verdict = id("verdict", "cut-3a.s1");
plain.label = l("F1");
assert_eq!(key(PipelineDocument::Finding(plain)), Ok(format!("{CAMPAIGN}:finding:cut-3a.s1.F1")));
let mut deep_spec = report_sample();
deep_spec.cut_spec = id("cut_spec", "cut-3a.h1.r1");
deep_spec.attempt = 2;
assert!(
matches!(key(PipelineDocument::CutReport(deep_spec)), Err(PipelineRefusal::InvalidFormat { .. })),
"a cut label carrying a dot is refused, so cut-3a.h1.h2 has one source"
);
let mut attempt_two = report_sample();
attempt_two.attempt = 2;
assert_eq!(key(PipelineDocument::CutReport(attempt_two)), Ok(format!("{CAMPAIGN}:cut_report:cut-3a.h2")));
}
/// F3: every parent id is parsed strictly. These are the bad parents Soul
/// found accepted, plus the wrong-kind and marker rules that mutations S1
/// and S3 remove.
#[test]
fn parent_ids_are_parsed_strictly() {
let refused = |parent: &str| {
let mut report = report_sample();
report.cut_spec = Short(parent.into());
let key = pipeline_key(&PipelineDocument::CutReport(report));
assert!(
matches!(&key, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "cut_report.cut_spec"),
"parent {parent:?} must be refused, got {key:?}"
);
};
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.r1:junk"));
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.rX"));
refused(&format!("{CAMPAIGN}:cut_spec:cut-..r1"));
refused(&format!("{CAMPAIGN}:cut_spec:cut-.r1"));
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.r1 "));
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.r"));
refused(&format!("{CAMPAIGN}:cut_report:cut-3a.h1"));
// Right kind and a well-formed number, but the marker belongs to
// another kind: the marker rule is pinned on its own.
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.h1"));
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.s1"));
// A well-formed number with no marker at all. Only the marker rule
// refuses this one: the digits rule is satisfied either way.
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.1"));
refused(&format!("{CAMPAIGN}:CUT_SPEC:cut-3a.r1"));
refused("EUREKA-STATE:cut_spec:cut-3a.r1");
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.r1:"));
refused(&format!("{CAMPAIGN}::cut-3a.r1"));
refused("cut-3a.r1");
let mut other_campaign = report_sample();
other_campaign.campaign = slug("other-campaign");
assert!(
matches!(
pipeline_key(&PipelineDocument::CutReport(other_campaign)),
Err(PipelineRefusal::InvalidFormat { .. })
),
"a parent in another campaign is refused"
);
let mut verdict = verdict_sample();
verdict.cut_report = id("cut_report", "cut-3a.r1");
assert!(
matches!(pipeline_key(&PipelineDocument::Verdict(verdict)), Err(PipelineRefusal::InvalidFormat { .. })),
"a verdict's parent must carry the report marker, not the spec marker"
);
}
/// F4: a resolution's subject id is a full id of the declared kind.
#[test]
fn resolution_subject_is_a_full_id_of_its_kind() {
let refused = |kind: PipelineKind, subject: &str| {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind, id: Short(subject.into()) };
let document = PipelineDocument::Resolution(resolution);
assert!(
matches!(document.validate(), Err(PipelineRefusal::InvalidFormat { .. })),
"subject {subject:?} must fail validation"
);
assert!(
matches!(pipeline_key(&document), Err(PipelineRefusal::InvalidFormat { .. })),
"subject {subject:?} must not compose a key"
);
};
refused(PipelineKind::Question, "");
refused(PipelineKind::Question, "not an id: spaces and : colons");
refused(PipelineKind::Question, "eureka-state:question:11");
refused(PipelineKind::Question, &format!("{CAMPAIGN}:ruling:R8"));
refused(PipelineKind::Ruling, &format!("{CAMPAIGN}:question:Q1"));
refused(PipelineKind::Question, &format!("{CAMPAIGN}:question:.."));
let mut valid = resolution_sample();
valid.subject = PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") };
assert_eq!(
pipeline_key(&PipelineDocument::Resolution(valid)),
Ok(format!("{CAMPAIGN}:resolution:ruling.R8.n1"))
);
}
/// Soul G2: a resolution is named by the key it has, and a `PipelineRef`
/// of kind `Resolution` accepts exactly that id. `PipelineRef` takes any
/// kind, so this is reachable; a follow-up sourced from a resolution is the
/// live path. The grammar admits `<campaign>:resolution:R8` too: it is
/// well-formed, and no resolution derives it, but whether a document with an
/// id exists is admission's rule, not the grammar's, so the reader does not
/// refuse it and this test does not ask it to.
#[test]
fn a_resolution_is_named_by_the_key_it_has() {
let sourced = |id: &str| {
let PipelineDocument::FollowUp(mut follow_up) = samples().remove(8).0 else { unreachable!() };
follow_up.source = PipelineRef { kind: PipelineKind::Resolution, id: Short(id.into()) };
PipelineDocument::FollowUp(follow_up).validate()
};
let key = pipeline_key(&PipelineDocument::Resolution(resolution_sample())).expect("the sample keys");
assert_eq!(key, format!("{CAMPAIGN}:resolution:question.Q1.n1"));
assert_eq!(sourced(&key), Ok(()), "a resolution is named by the key it has");
assert_eq!(sourced(&format!("{CAMPAIGN}:resolution:R8")), Ok(()), "well-formed grammar is not the reader's to refuse");
for refused in [
// The prefix shape the writer used to emit. No resolution carries it.
format!("resolution:{CAMPAIGN}:question:Q1"),
format!("{CAMPAIGN}:resolution:{CAMPAIGN}:question:Q1"),
// No local, a subject local with an empty part, trailing junk, and a
// kind segment that is not `resolution`.
format!("{CAMPAIGN}:resolution:"),
format!("{CAMPAIGN}:resolution:question..Q1"),
format!("{CAMPAIGN}:resolution:question.Q1:junk"),
format!("{CAMPAIGN}:question:Q1"),
] {
assert!(
matches!(sourced(&refused), Err(PipelineRefusal::InvalidFormat { .. })),
"{refused:?} is not an id any resolution has"
);
}
// A root subject reads back too: a resolution of a campaign carries the
// campaign's kind and its `self` local.
let mut of_campaign = resolution_sample();
of_campaign.subject = PipelineRef { kind: PipelineKind::Campaign, id: Short(format!("{CAMPAIGN}:campaign:self")) };
let root_key = pipeline_key(&PipelineDocument::Resolution(of_campaign)).expect("a root subject keys");
assert_eq!(root_key, format!("{CAMPAIGN}:resolution:campaign.self.n1"));
assert_eq!(sourced(&root_key), Ok(()));
}
/// The three kinds a mind is keyed by. The samples above already round-trip
/// every kind; this pins the shapes D2 gives these three specifically: an
/// instance is a root keyed by its own slug, and the other two hang off an
/// instance rather than a campaign, so neither borrows the campaign tail.
#[test]
fn instance_stewardship_and_hand_off_round_trip() -> Result<()> {
let cache = schema_cache()?;
let instance = PipelineDocument::Instance(instance_sample());
assert_eq!(pipeline_key(&instance), Ok(format!("{INSTANCE}:instance:self")));
for document in [
instance,
PipelineDocument::Stewardship(stewardship_sample()),
PipelineDocument::HandOff(hand_off_sample()),
] {
document.validate()?;
let envelope = document.prepare(&cache)?;
assert_eq!(PipelineDocument::decode(&envelope)?, document);
let key = pipeline_key(&document)?;
assert!(
!key.starts_with(&format!("{CAMPAIGN}:")),
"{:?} is keyed by its instance, not by a campaign: {key}",
document.kind()
);
validate_pipeline_write_envelope(&envelope)?;
}
// A bounded list is still bounded: `documents` carries a maximum, as
// every `Vec` field must.
let mut wide = hand_off_sample();
wide.documents = vec![s("x"); 257];
assert_eq!(
PipelineDocument::HandOff(wide).validate(),
Err(PipelineRefusal::FieldBound { field: "hand_off.documents".into(), limit: 256, actual: 257 })
);
Ok(())
}
/// Soul F3: a key and an id are the same string, so every kind's key reads
/// back as an id of that kind. The kinds a mind is keyed by were added to
/// the key writer and left out of the id reader, and nothing noticed,
/// because the key tests assert strings and never read one back: an
/// instance keys to its slug and then fails to parse as an instance id, so
/// no `PipelineRef` and no resolution could ever name one. Every kind now
/// reads back with no kind excused, a resolution included, and a root kind
/// read as the other root kind is refused: the reader checks the kind
/// segment for the roots exactly as for every other kind.
#[test]
fn keys_read_back_as_ids_of_their_kind() -> Result<()> {
for (document, expected) in samples() {
let kind = document.kind();
let key = pipeline_key(&document)?;
assert_eq!(key, expected, "{kind:?} keys to its sample's key");
assert_eq!(
pipeline_id("read_back", &key, kind).map(|_| ()),
Ok(()),
"{kind:?} key {key} does not read back as an id of its kind"
);
}
let campaign_key = pipeline_key(&campaign_sample())?;
let instance_key = pipeline_key(&PipelineDocument::Instance(instance_sample()))?;
assert_eq!(
pipeline_id("read_back", &instance_key, PipelineKind::Instance),
Ok((INSTANCE, ROOT_LOCAL)),
"an instance key reads back as an instance"
);
assert_eq!(
pipeline_id("read_back", &instance_key, PipelineKind::Campaign),
Err(format_error("read_back", &instance_key)),
"an instance key is not a campaign id"
);
assert_eq!(
pipeline_id("read_back", &campaign_key, PipelineKind::Instance),
Err(format_error("read_back", &campaign_key)),
"a campaign key is not an instance id"
);
// Soul X1 and X11: the reader's root and local are each `dotted_text`
// whole, so a trailing dot on either is refused by the reader itself,
// not only by the writer that never composes one. A leading dot and
// an empty part are the same rule on the local.
for malformed in ["c.:target:x", "c:target:x.", "c:target:.x", "c:target:a..b"] {
let read = pipeline_id("read_back", malformed, PipelineKind::Target);
assert!(
matches!(&read, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "read_back"),
"{malformed:?} is not an id of any kind, got {read:?}"
);
}
// The instance root's own key segment is validated, not merely bounded.
// Nothing else stands between a `Slug` and a key: a space would compose
// a store key carrying a byte no `Label` may hold.
let mut spaced = instance_sample();
spaced.instance = Slug("thought cage".into());
assert_eq!(
pipeline_key(&PipelineDocument::Instance(spaced)),
Err(PipelineRefusal::InvalidFormat {
field: "instance.instance".into(),
value: "thought cage".into(),
}),
"an instance slug is validated where the key is composed"
);
Ok(())
}
/// The repo is one key segment, so its slash is escaped. Left unescaped it
/// would both add a segment the reader cannot tell from a real one and put a
/// byte in the key that no `Label` may carry; escaped to a bare `_` it would
/// let two repos claim one key, which the colliding pair below pins.
#[test]
fn stewardship_key_escapes_the_repo_slash() {
let stewardship = stewardship_sample();
assert_eq!(stewardship.repo, OrgRepo("GameCult/Epiphany".into()));
let key = pipeline_key(&PipelineDocument::Stewardship(stewardship.clone()));
assert_eq!(key, Ok(format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1")));
assert!(!key.unwrap().contains('/'), "no key segment carries a slash");
// The escape is not a cosmetic substitution, and the pair that proves
// it is a pair: GitHub's grammar keeps `.` and `_` out of the owner,
// so both live in the repo half here, and under a naive scheme that
// collapsed `/`, `.` and `_` all to a bare `_` both of these key to
// `GameCult_Epiphany_thing`, and one of the two documents is lost.
let keyed = |repo: &str| {
let mut value = stewardship.clone();
value.repo = OrgRepo(repo.into());
pipeline_key(&PipelineDocument::Stewardship(value))
};
let dotted_repo = keyed("GameCult/Epiphany.thing");
let underscored_repo = keyed("GameCult/Epiphany_thing");
assert_eq!(
dotted_repo,
Ok(format!("{INSTANCE}:stewardship:gamecult_-epiphany_dthing.n1"))
);
assert_eq!(
underscored_repo,
Ok(format!("{INSTANCE}:stewardship:gamecult_-epiphany__thing.n1"))
);
assert_ne!(dotted_repo, underscored_repo, "two repos cannot claim one key");
let mut no_org = stewardship.clone();
no_org.repo = OrgRepo("Epiphany".into());
assert_eq!(
pipeline_key(&PipelineDocument::Stewardship(no_org)),
Err(PipelineRefusal::InvalidFormat { field: "stewardship.repo".into(), value: "Epiphany".into() })
);
// S-1: key derivation goes through `identity()`, so a case variant of
// the same repository is one store key, not two stewardships of what
// is really one repo.
let mut upper = stewardship.clone();
upper.repo = OrgRepo("gamecult/epiphany".into());
assert_eq!(
pipeline_key(&PipelineDocument::Stewardship(upper)),
pipeline_key(&PipelineDocument::Stewardship(stewardship.clone())),
"GameCult/Epiphany and gamecult/epiphany must derive one store key"
);
// An escaped repo is still bound by the key's segment rules.
let mut long = stewardship;
long.repo = OrgRepo(format!("GameCult/{}", "a".repeat(60)));
assert!(
matches!(
pipeline_key(&PipelineDocument::Stewardship(long)),
Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "stewardship.key"
),
"a repo segment longer than a key segment is refused"
);
}
/// A hand-off is recorded in both minds, so both instances are in the key:
/// the sender owns the first segment and the receiver the local. Changing
/// either one moves the document.
#[test]
fn hand_off_names_both_instances() {
let key = |hand_off: PipelineHandOff| pipeline_key(&PipelineDocument::HandOff(hand_off));
let base = key(hand_off_sample()).expect("the sample keys");
assert_eq!(base, format!("{INSTANCE}:hand_off:thought-cage.gamecult_-epiphany.2026-09-15"));
let mut other_sender = hand_off_sample();
other_sender.from_instance = slug("thought-cage");
assert_ne!(key(other_sender), Ok(base.clone()), "the sender is in the key");
let mut other_receiver = hand_off_sample();
other_receiver.to_instance = slug("mimir");
assert_ne!(key(other_receiver), Ok(base.clone()), "the receiver is in the key");
// Two hand-offs of the same repo between the same pair on different
// days are different documents.
let mut later = hand_off_sample();
later.handed_on = Date("2026-09-16".into());
assert_ne!(key(later), Ok(base), "the date is in the key");
// A dotted receiver is a `Slug` entering a local, so it is escaped to
// one label rather than widening the local by a part.
let mut dotted_receiver = hand_off_sample();
dotted_receiver.to_instance = slug("thought-cage.GameCult");
assert_eq!(
key(dotted_receiver),
Ok(format!("{INSTANCE}:hand_off:thought-cage_dGameCult.gamecult_-epiphany.2026-09-15"))
);
// The escape's `_` half is not only for repos: a receiver `a_db` and a
// receiver `a.b` would both key to `a_db` if the slug's own `_` passed
// through raw, so the slug is escaped as `a__db` and the two differ.
let mut underscored_receiver = hand_off_sample();
underscored_receiver.to_instance = slug("a_db");
let mut dotted_twin = hand_off_sample();
dotted_twin.to_instance = slug("a.b");
let underscored_receiver = key(underscored_receiver).expect("an underscored receiver keys");
assert!(underscored_receiver.contains("a__db"), "{underscored_receiver}: the slug's underscore is escaped");
assert_ne!(key(dotted_twin), Ok(underscored_receiver), "`a_db` and `a.b` are two receivers");
// Both instance slugs are validated as key segments, not merely bounded.
let mut bad_receiver = hand_off_sample();
bad_receiver.to_instance = Slug("thought cage".into());
assert_eq!(
key(bad_receiver),
Err(PipelineRefusal::InvalidFormat {
field: "hand_off.to_instance".into(),
value: "thought cage".into(),
})
);
let mut bad_date = hand_off_sample();
bad_date.handed_on = Date("2026-9-15".into());
assert!(
matches!(key(bad_date), Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "hand_off.handed_on"),
"a malformed date does not compose a key"
);
}
/// A resolution whose subject is the given document, keyed.
fn resolution_of(kind: PipelineKind, id: &str) -> Result<String, PipelineRefusal> {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind, id: Short(id.into()) };
pipeline_key(&PipelineDocument::Resolution(resolution))
}
/// R1, the grammar's own test: every key is `<root>:<kind>:<local>`, three
/// segments with no kind excused, the root a `Slug`, the kind the literal
/// name, and every local part a `Label`. A root that would add a segment is
/// refused where the key is composed: `pipeline_key` is `pub` and does not
/// validate the document first, so this is reachable without one.
#[test]
fn every_key_has_exactly_three_segments() {
let nested = resolution_of(PipelineKind::Resolution, &format!("{CAMPAIGN}:resolution:question.Q1.n1"))
.expect("a resolution of a resolution keys");
let keys = samples()
.into_iter()
.map(|(document, _)| (document.kind(), pipeline_key(&document).expect("every sample keys")))
.chain([(PipelineKind::Resolution, nested)]);
for (kind, key) in keys {
let segments = key.split(':').collect::<Vec<_>>();
assert_eq!(segments.len(), 3, "{kind:?} key {key} has three segments");
assert_eq!(dotted_text("root", segments[0]), Ok(()), "{key}: the root is a slug");
assert_eq!(segments[1], kind.name(), "{key}: the kind segment is the literal kind");
assert!(PipelineKind::ALL.iter().any(|known| known.name() == segments[1]));
for part in segments[2].split('.') {
assert_eq!(label_text("local", part), Ok(()), "{key}: local part {part:?} is a label");
}
}
// A root that would add a segment, one that would empty a part, and one
// wider than a segment are all refused before a key is formatted. The
// refusal names the part that failed, so an empty part reports `""`.
let trailing_dot = format!("{CAMPAIGN}.");
let leading_dot = format!(".{CAMPAIGN}");
let over = "a".repeat(65);
for root in ["a:b", trailing_dot.as_str(), leading_dot.as_str(), "a..b", over.as_str()] {
let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() };
target.campaign = Slug(root.into());
let key = pipeline_key(&PipelineDocument::Target(target));
assert!(
matches!(&key, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "target.campaign"),
"root {root:?} does not compose a key, got {key:?}"
);
}
}
/// Defect 1: the two roots are in distinct namespaces because the kind
/// segment distinguishes them, the same mechanism that separates every
/// other pair of kinds. Both key; only the pair sees a shared namespace.
#[test]
fn roots_of_different_kinds_do_not_share_a_key() {
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
campaign.slug = slug(INSTANCE);
let campaign = pipeline_key(&PipelineDocument::Campaign(campaign));
let instance = pipeline_key(&PipelineDocument::Instance(instance_sample()));
assert_eq!(campaign, Ok(format!("{INSTANCE}:campaign:self")));
assert_eq!(instance, Ok(format!("{INSTANCE}:instance:self")));
assert_ne!(campaign, instance, "a campaign and an instance of one slug are two documents");
}
/// The root is a `Slug`, so a dotted root keys and reads back whole on
/// both sides: the writer's root check and the reader's root check are
/// each `dotted_text`, not `label_text`. A campaign `game.cult` and an
/// instance `ygg.drasil` key, a document under the dotted campaign keys,
/// and each reads back through `pipeline_id` recovering the root exactly.
#[test]
fn dotted_roots_key_and_read_back() {
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
campaign.slug = slug("game.cult");
let campaign_key = pipeline_key(&PipelineDocument::Campaign(campaign)).expect("a dotted campaign keys");
assert_eq!(campaign_key, "game.cult:campaign:self");
assert_eq!(
pipeline_id("read_back", &campaign_key, PipelineKind::Campaign),
Ok(("game.cult", ROOT_LOCAL)),
"the dotted campaign root reads back whole"
);
let mut instance = instance_sample();
instance.instance = slug("ygg.drasil");
let instance_key = pipeline_key(&PipelineDocument::Instance(instance)).expect("a dotted instance keys");
assert_eq!(instance_key, "ygg.drasil:instance:self");
assert_eq!(
pipeline_id("read_back", &instance_key, PipelineKind::Instance),
Ok(("ygg.drasil", ROOT_LOCAL)),
"the dotted instance root reads back whole"
);
let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() };
target.campaign = slug("game.cult");
let target_key = pipeline_key(&PipelineDocument::Target(target)).expect("a target under a dotted campaign keys");
assert_eq!(target_key, "game.cult:target:r2");
assert_eq!(
pipeline_id("read_back", &target_key, PipelineKind::Target),
Ok(("game.cult", "r2")),
"the dotted root and the local both read back"
);
let resolution = resolution_of(PipelineKind::Campaign, &campaign_key).expect("a resolution of a dotted campaign keys");
assert_eq!(resolution, "game.cult:resolution:campaign.self.n1");
assert_eq!(
pipeline_id("read_back", &resolution, PipelineKind::Resolution),
Ok(("game.cult", "campaign.self.n1")),
"a resolution under a dotted root reads back"
);
}
/// Cut 6c: a resolution's referent is a full id of the kind it declares,
/// validated through `PipelineRef` like every other referent, so a
/// well-formed id of the wrong kind is refused and the refusal names the
/// entry. The positive case is Ghostlight's own: a ruling partly
/// overturned by two later records, one of them a resolution named by the
/// key it has, which only the 6b grammar made nameable. An id no
/// resolution derives, `eureka-state:resolution:R8`, still validates:
/// whether a document with an id exists is admission's rule.
#[test]
fn resolution_outcome_referents_are_parsed_ids_of_their_kind() {
let resolved = |subject: PipelineRef, outcome: ResolutionOutcome| {
let mut resolution = resolution_sample();
resolution.subject = subject;
resolution.outcome = outcome;
PipelineDocument::Resolution(resolution).validate()
};
let refused = |field: &str, result: Result<(), PipelineRefusal>| {
assert!(
matches!(&result, Err(PipelineRefusal::InvalidFormat { field: at, .. }) if at == field),
"expected InvalidFormat at {field}, got {result:?}"
);
};
let ruling = |label: &str| PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", label) };
let question = |label: &str| PipelineRef { kind: PipelineKind::Question, id: id("question", label) };
let inner = pipeline_key(&PipelineDocument::Resolution(resolution_sample())).expect("the sample keys");
let resolution = PipelineRef { kind: PipelineKind::Resolution, id: Short(inner.clone()) };
let superseded = |second: PipelineRef| ResolutionOutcome::Superseded { by: vec![ruling("R9"), second] };
assert_eq!(resolved(ruling("R8"), superseded(resolution)), Ok(()));
let forged = PipelineRef { kind: PipelineKind::Ruling, id: Short(inner) };
refused("resolution.outcome.by[1].id", resolved(ruling("R8"), superseded(forged)));
let underived = PipelineRef { kind: PipelineKind::Resolution, id: Short(format!("{CAMPAIGN}:resolution:R8")) };
assert_eq!(resolved(ruling("R8"), superseded(underived)), Ok(()), "well-formed grammar is not the shape's to refuse");
assert_eq!(resolved(question("Q1"), ResolutionOutcome::Answered { by: ruling("R8") }), Ok(()));
let forged = PipelineRef { kind: PipelineKind::Question, id: id("ruling", "R8") };
refused("resolution.outcome.by.id", resolved(question("Q1"), ResolutionOutcome::Answered { by: forged }));
assert_eq!(resolved(question("Q1"), ResolutionOutcome::Deferred { to: question("Q2") }), Ok(()));
let forged = PipelineRef { kind: PipelineKind::Ruling, id: id("question", "Q2") };
refused("resolution.outcome.to.id", resolved(question("Q1"), ResolutionOutcome::Deferred { to: forged }));
let finding = PipelineRef { kind: PipelineKind::Finding, id: id("finding", "cut-3a.s2.F4") };
let fixed = |by: Option<PipelineRef>| ResolutionOutcome::Fixed { commit: sha(), by };
assert_eq!(resolved(finding.clone(), fixed(None)), Ok(()));
assert_eq!(resolved(finding.clone(), fixed(Some(ruling("R8")))), Ok(()));
let forged = PipelineRef { kind: PipelineKind::Finding, id: id("ruling", "R8") };
refused("resolution.outcome.by.id", resolved(finding, fixed(Some(forged))));
// Soul F2: the validator's bound on a supersession is its own, not
// only the schema's. Eight referents are the most a subject names;
// nine are refused as a count before any item is read.
let rulings = |count: usize| (1..=count).map(|n| ruling(&format!("R{n}"))).collect::<Vec<_>>();
assert_eq!(resolved(ruling("R8"), ResolutionOutcome::Superseded { by: rulings(8) }), Ok(()));
assert_eq!(
resolved(ruling("R8"), ResolutionOutcome::Superseded { by: rulings(9) }),
Err(PipelineRefusal::FieldBound { field: "resolution.outcome.by".into(), limit: 8, actual: 9 })
);
// Soul F3: the two reason-carrying outcomes are bounded like every
// `Line`; the shared arm is pinned on both so neither can drop out.
let wide = Line("a".repeat(1001));
for outcome in [
ResolutionOutcome::Recorded { reason: wide.clone() },
ResolutionOutcome::Withdrawn { reason: wide },
] {
let finding = PipelineRef { kind: PipelineKind::Finding, id: id("finding", "cut-3a.s2.F4") };
assert_eq!(
resolved(finding, outcome),
Err(PipelineRefusal::FieldBound { field: "resolution.outcome.reason".into(), limit: 1000, actual: 1001 })
);
}
// A night's closing is bounded on both of its fields: her line like any `Line`, and the memory it names
// like any referent.
let run = PipelineRef { kind: PipelineKind::Run, id: id("run", "night") };
let memory = PipelineRef { kind: PipelineKind::Memory, id: id("memory", "m-1") };
let slept = |line: Line, day_through: Option<PipelineRef>| resolved(run.clone(), ResolutionOutcome::Slept { line, day_through });
assert_eq!(slept("ok".into(), Some(memory.clone())), Ok(()));
assert_eq!(slept("ok".into(), None), Ok(()));
assert_eq!(slept(Line("a".repeat(1001)), None), Err(PipelineRefusal::FieldBound { field: "resolution.outcome.line".into(), limit: 1000, actual: 1001 }));
refused("resolution.outcome.day_through.id", slept("ok".into(), Some(PipelineRef { kind: PipelineKind::Memory, id: id("ruling", "R8") })));
// A move names the receiving mind as any other slug is named, and refuses one that is not a slug.
let campaign = PipelineRef { kind: PipelineKind::Campaign, id: id("campaign", "self") };
let moved = |to: &str| resolved(campaign.clone(), ResolutionOutcome::Moved { to: Slug(to.into()) });
assert_eq!(moved("heimdall"), Ok(()));
refused("resolution.outcome.to", moved("Not A Slug!"));
refused("resolution.outcome.to", moved(""));
}
/// Ruling B: a fix names the tree where the finding stopped being true,
/// as a `Sha`. Uppercase hex of a legal length is the forgery a length
/// check passes and `hex` refuses.
#[test]
fn fixed_resolution_requires_a_commit_sha() {
let fixed = |commit: &str, by: Option<PipelineRef>| {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind: PipelineKind::Finding, id: id("finding", "cut-3a.s2.F4") };
resolution.outcome = ResolutionOutcome::Fixed { commit: Sha(commit.into()), by };
PipelineDocument::Resolution(resolution).validate()
};
assert_eq!(fixed("5f98228d9c", None), Ok(()));
for forged in ["5F98228D9C", "5f9822", "dirty-worktree", ""] {
assert_eq!(
fixed(forged, None),
Err(PipelineRefusal::InvalidFormat { field: "resolution.outcome.commit".into(), value: forged.into() }),
"{forged:?} is not a commit"
);
}
// Soul F1: naming the record that fixed it does not excuse the commit.
let ruling = PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "R8") };
assert_eq!(
fixed("dirty-worktree", Some(ruling)),
Err(PipelineRefusal::InvalidFormat { field: "resolution.outcome.commit".into(), value: "dirty-worktree".into() }),
"a fix with a named record still names a commit"
);
}
/// A mutation record has a key-safe identity a verdict claim can name,
/// and is pinned to a tree: its label is a `Label` and its commit a
/// `Sha`, so a dotted label and free text where a sha belongs are refused.
#[test]
fn mutation_records_carry_a_dot_free_label_and_a_commit() {
let recorded = |label: &str, commit: &str| {
let mut report = report_sample();
// Through `From<&str>`, which every text type has, so the
// widened types under M19 and M20 still compile and the
// forgeries reach validation.
report.mutations[0].label = label.into();
report.mutations[0].commit = commit.into();
PipelineDocument::CutReport(report).validate()
};
assert_eq!(recorded("M1", "5f98228d"), Ok(()));
assert_eq!(
recorded("M1.a", "5f98228d"),
Err(PipelineRefusal::InvalidFormat { field: "cut_report.mutations[0].label".into(), value: "M1.a".into() })
);
assert_eq!(
recorded("M1", "dirty-worktree"),
Err(PipelineRefusal::InvalidFormat {
field: "cut_report.mutations[0].commit".into(),
value: "dirty-worktree".into(),
})
);
}
/// Ruling A's shape half: a claim names the promise it measured and the
/// mutations it ran, both as `Label`s, and names at most eight mutations.
#[test]
fn a_verdict_claim_names_the_promise_and_the_mutation_it_measured() {
let sample = verdict_sample();
assert_eq!(sample.claims[0].promise, Some(l("P1")));
assert_eq!(sample.claims[0].mutations, vec![l("M1")]);
assert_eq!(PipelineDocument::Verdict(sample).validate(), Ok(()));
let claimed = |promise: Option<Label>, mutations: Vec<Label>| {
let mut verdict = verdict_sample();
verdict.claims[0].promise = promise;
verdict.claims[0].mutations = mutations;
PipelineDocument::Verdict(verdict).validate()
};
assert_eq!(claimed(None, vec![]), Ok(()), "a claim need not measure a promise");
assert_eq!(
claimed(Some(l("P1.a")), vec![]),
Err(PipelineRefusal::InvalidFormat { field: "verdict.claims[0].promise".into(), value: "P1.a".into() })
);
assert_eq!(
claimed(None, vec![l("M1.a")]),
Err(PipelineRefusal::InvalidFormat { field: "verdict.claims[0].mutations[0]".into(), value: "M1.a".into() })
);
let labels = |count: usize| (1..=count).map(|n| l(&format!("M{n}"))).collect::<Vec<_>>();
assert_eq!(claimed(None, labels(8)), Ok(()));
assert_eq!(
claimed(None, labels(9)),
Err(PipelineRefusal::FieldBound { field: "verdict.claims[0].mutations".into(), limit: 8, actual: 9 })
);
}
/// A tripwire on the one hazard of retyping the estimate: the delta's two
/// `u32` fields are not interchangeable. The sample spec is Cut 3a's, a
/// net addition of 900 lines, and the order is asserted.
#[test]
fn the_sample_cut_spec_estimates_a_net_addition() {
let PipelineDocument::CutSpec(spec) = samples().remove(4).0 else { unreachable!() };
assert_eq!(spec.estimate.lines_added, 900);
assert_eq!(spec.estimate.lines_removed, 0);
assert_eq!(spec.estimate, report_sample().structural_delta, "the estimate and the actual are one shape");
}
/// Defect 2: a resolution's key carries its subject's kind as a literal
/// part of the local, so nothing infers it and two subjects of one local
/// and different kinds resolve to two keys. Each reads back through the
/// reader with the subject kind it was built from.
#[test]
fn a_resolution_names_its_subjects_kind() {
let pairs = [
(PipelineKind::Question, id("question", "Q1").0, PipelineKind::Ruling, id("ruling", "Q1").0),
(
PipelineKind::Campaign,
format!("{CAMPAIGN}:campaign:self"),
PipelineKind::Instance,
format!("{CAMPAIGN}:instance:self"),
),
];
for (left_kind, left_id, right_kind, right_id) in pairs {
let left = resolution_of(left_kind, &left_id).expect("the left subject keys");
let right = resolution_of(right_kind, &right_id).expect("the right subject keys");
assert_ne!(left, right, "resolutions of {left_id} and {right_id} are two documents");
for (kind, key) in [(left_kind, &left), (right_kind, &right)] {
let (root, local) = pipeline_id("read_back", key, PipelineKind::Resolution).expect("reads back");
assert_eq!(root, CAMPAIGN);
assert_eq!(local.split('.').next(), Some(kind.name()), "{key} names its subject's kind");
}
}
}
/// Defect 3: a resolution's own key is an ordinary id, so it composes as a
/// subject like any other, and the key reads back to the inner key -- the
/// whole inner key, sequence included, since the subject is recovered by
/// stripping this resolution's own sequence from the end and the kind from
/// the front. The depth bound is tested per kind by
/// `every_resolvable_kind_resolves_two_deep`.
#[test]
fn a_resolution_of_a_resolution_reads_back() {
let inner = resolution_of(PipelineKind::Question, &id("question", "Q1").0).expect("the inner keys");
let mut outer = resolution_sample();
outer.subject = PipelineRef { kind: PipelineKind::Resolution, id: Short(inner.clone()) };
let outer = PipelineDocument::Resolution(outer);
assert_eq!(outer.validate(), Ok(()));
let key = pipeline_key(&outer).expect("a resolution of a resolution keys");
assert_eq!(key, format!("{CAMPAIGN}:resolution:resolution.question.Q1.n1.n1"));
let (root, local) = pipeline_id("read_back", &key, PipelineKind::Resolution).expect("reads back");
let (subject_kind, rest) = local.split_once('.').expect("the local names a subject");
let (subject_local, _sequence) = rest.rsplit_once('.').expect("the local ends in this resolution's sequence");
assert_eq!(format!("{root}:{subject_kind}:{subject_local}"), inner, "the recovered subject is the inner key");
}
/// Each level of a nesting carries its own sequence, and only its own: the
/// part appended is this resolution's `sequence` field, never a copy of the
/// sequence the subject already ends in. The fixtures elsewhere all sit at
/// `1` at every level, where the two are indistinguishable, so the chain
/// here differs at every level and reads back level by level.
///
/// On spelling, so silence is not read as coverage: the leaf emits `n<N>`
/// with `N` as `u32::to_string` writes it, so `n0` and `n12` are emitted
/// and `n01` is not. A hand-built id ending `n01`, `n1x`, or no `n<N>` at
/// all is still a well-formed reference -- those are legal labels, and a
/// grammar that reads keys may not refuse them on spelling. Nothing in the
/// leaf canonicalises them either: such an id simply names a document the
/// leaf would never key, which admission's A7 sees as a missing referent.
#[test]
fn every_level_of_a_nesting_keeps_its_own_sequence() {
let resolved = |kind: PipelineKind, subject: &str, sequence: u32| {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind, id: Short(subject.into()) };
resolution.sequence = sequence;
pipeline_key(&PipelineDocument::Resolution(resolution)).expect("a sequenced resolution keys")
};
let first = resolved(PipelineKind::Question, &id("question", "Q1").0, 9);
let second = resolved(PipelineKind::Resolution, &first, 10);
let third = resolved(PipelineKind::Resolution, &second, 0);
let fourth = resolved(PipelineKind::Resolution, &third, 12);
assert_eq!(first, format!("{CAMPAIGN}:resolution:question.Q1.n9"));
assert_eq!(second, format!("{CAMPAIGN}:resolution:resolution.question.Q1.n9.n10"));
assert_eq!(third, format!("{CAMPAIGN}:resolution:resolution.resolution.question.Q1.n9.n10.n0"));
assert_eq!(
fourth,
format!("{CAMPAIGN}:resolution:resolution.resolution.resolution.question.Q1.n9.n10.n0.n12")
);
for (outer, inner, sequence) in [(&fourth, &third, 12), (&third, &second, 0), (&second, &first, 10)] {
let (root, local) = pipeline_id("read_back", outer, PipelineKind::Resolution).expect("reads back");
let (subject_kind, rest) = local.split_once('.').expect("the local names a subject");
let (subject_local, last) = rest.rsplit_once('.').expect("the local ends in this resolution's sequence");
assert_eq!(subject_kind, "resolution", "{outer}: the subject of a nesting is a resolution");
assert_eq!(format!("{root}:{subject_kind}:{subject_local}"), *inner, "{outer}: the subject is the inner key");
assert_eq!(last, format!("n{sequence}"), "{outer}: this level's own sequence, not the subject's");
}
}
/// A stewardship is the other sequenced kind, and it composes as a subject
/// like any other: the subject's whole local, escaped repo and sequence
/// both, is the resolution's local, with the resolution's own sequence
/// last. So the two assignments of one repo have two resolutions, and a
/// resolution of either is itself resolvable.
#[test]
fn a_resolution_of_a_stewardship_reads_back() {
let stewarded = |sequence: u32| {
let mut stewardship = stewardship_sample();
stewardship.sequence = sequence;
pipeline_key(&PipelineDocument::Stewardship(stewardship)).expect("a sequenced stewardship keys")
};
let first = stewarded(1);
let second = stewarded(2);
assert_eq!(first, format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"));
assert_eq!(second, format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"));
let resolved = resolution_of(PipelineKind::Stewardship, &first).expect("a resolution of a stewardship keys");
let resolved_second = resolution_of(PipelineKind::Stewardship, &second).expect("the second assignment resolves");
assert_eq!(resolved, format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n1.n1"));
assert_eq!(resolved_second, format!("{INSTANCE}:resolution:stewardship.gamecult_-epiphany.n2.n1"));
assert_ne!(resolved, resolved_second, "each assignment keeps its own resolution");
for (key, subject) in [(&resolved, &first), (&resolved_second, &second)] {
let (root, local) = pipeline_id("read_back", key, PipelineKind::Resolution).expect("reads back");
assert_eq!(root, INSTANCE, "{key}: a stewardship's resolution lives in the mind's root");
let (subject_kind, rest) = local.split_once('.').expect("the local names a subject");
let (subject_local, last) = rest.rsplit_once('.').expect("the local ends in this resolution's sequence");
assert_eq!(subject_kind, PipelineKind::Stewardship.name(), "{key} names its subject's kind");
assert_eq!(
format!("{root}:{subject_kind}:{subject_local}"),
*subject,
"{key}: the subject's own sequence is part of the recovered key"
);
assert_eq!(last, "n1", "{key}: this resolution's own sequence");
// A withdrawal of that resolution nests one out, as it does under
// any other subject, and reads back to the resolution it withdraws.
let withdrawal = resolution_of(PipelineKind::Resolution, key).expect("a withdrawal keys");
let subject_local = key.split(':').nth(2).expect("three segments");
assert_eq!(withdrawal, format!("{INSTANCE}:resolution:resolution.{subject_local}.n1"));
let (root, local) = pipeline_id("read_back", &withdrawal, PipelineKind::Resolution).expect("reads back");
let (subject_kind, rest) = local.split_once('.').expect("the local names a subject");
let (subject_local, _) = rest.rsplit_once('.').expect("the local ends in its own sequence");
assert_eq!(format!("{root}:{subject_kind}:{subject_local}"), **key, "{withdrawal}: the withdrawn resolution");
}
}
/// A legal dotted local of exactly `length` bytes (`a.a.a`, each part a
/// label), for bounds wider than one label.
fn dotted_local(length: usize) -> String {
match length % 2 {
1 => format!("{}a", "a.".repeat(length / 2)),
_ => format!("{}aa", "a.".repeat(length / 2 - 1)),
}
}
/// A resolution of `subject`, at `sequence`, keyed.
fn resolved_at(kind: PipelineKind, subject: &str, sequence: u32) -> Result<String, PipelineRefusal> {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind, id: Short(subject.into()) };
resolution.sequence = sequence;
pipeline_key(&PipelineDocument::Resolution(resolution))
}
/// every-subject-resolvable: for each kind that can be a subject, at the
/// widest subject local (64) and `u32::MAX` sequences, depth one and depth
/// two key and read back through `pipeline_id`; depth three is refused on
/// the local bound.
#[test]
fn every_resolvable_kind_resolves_two_deep() {
let wide = "a".repeat(SUBJECT_LOCAL_MAX);
let mut widest = 0;
for kind in PipelineKind::ALL.iter().copied().filter(|kind| *kind != PipelineKind::Resolution) {
let subject = format!("{CAMPAIGN}:{}:{wide}", kind.name());
assert_eq!(pipeline_id("subject", &subject, kind), Ok((CAMPAIGN, wide.as_str())), "{kind:?}: the subject parses");
let one = resolved_at(kind, &subject, u32::MAX).unwrap_or_else(|error| panic!("{kind:?} depth one: {error}"));
let two = resolved_at(PipelineKind::Resolution, &one, u32::MAX)
.unwrap_or_else(|error| panic!("{kind:?} depth two: {error}"));
for key in [&one, &two] {
assert!(key.len() <= 200, "{key}: an id fits a Short");
let (root, local) = pipeline_id("read_back", key, PipelineKind::Resolution)
.unwrap_or_else(|error| panic!("{kind:?}: {key} does not read back: {error}"));
assert_eq!(root, CAMPAIGN);
assert!(local.len() <= RESOLUTION_LOCAL_MAX);
}
widest = widest.max(two.len() - format!("{CAMPAIGN}:resolution:").len());
assert!(
matches!(
resolved_at(PipelineKind::Resolution, &two, u32::MAX),
Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "resolution.key"
),
"{kind:?}: depth three is refused on the local bound"
);
}
assert_eq!(widest, RESOLUTION_LOCAL_MAX, "the bound is exactly the widest depth-two chain");
assert_eq!(RESOLUTION_LOCAL_MAX, 111);
}
#[test]
fn the_longest_kind_name_is_the_longest() {
let longest = PipelineKind::ALL.iter().map(|kind| kind.name().len()).max();
assert_eq!(longest, Some(LONGEST_KIND_NAME));
}
/// The parser bounds an id's local by kind, exactly where the composer does.
#[test]
fn an_id_local_is_bounded_by_its_kind() {
let at = |kind: PipelineKind, length: usize| {
pipeline_id("probe", &format!("{CAMPAIGN}:{}:{}", kind.name(), dotted_local(length)), kind).map(|_| ())
};
assert_eq!(at(PipelineKind::Question, SUBJECT_LOCAL_MAX), Ok(()));
assert!(at(PipelineKind::Question, SUBJECT_LOCAL_MAX + 1).is_err());
assert_eq!(at(PipelineKind::Resolution, RESOLUTION_LOCAL_MAX), Ok(()));
assert!(at(PipelineKind::Resolution, RESOLUTION_LOCAL_MAX + 1).is_err());
}
#[test]
fn sha_names_same_commit() {
let full = Sha("5f98228d1c2b3a4f5e6d7c8b9a0f1e2d3c4b5a69".into());
assert!(full.names_same_commit(&Sha("5f98228".into())));
assert!(Sha("5f98228".into()).names_same_commit(&full));
assert!(full.names_same_commit(&full));
assert!(!full.names_same_commit(&Sha("5f98229".into())));
assert!(!Sha("5f98228".into()).names_same_commit(&Sha("5f98229".into())));
assert!(!Sha("5f98228a".into()).names_same_commit(&Sha("5f98228b".into())), "equal length, differing");
}
/// The total bound, in the one place it lives: parts that are each a legal
/// label compose a local wider than 64 bytes and are refused as a whole.
#[test]
fn a_composed_local_is_bounded_whole() {
let mut wide = hand_off_sample();
wide.to_instance = Slug("a".repeat(60));
assert_eq!(wide.to_instance.validate("hand_off.to_instance"), Ok(()), "the receiver alone is legal");
assert_eq!(
label_text("part", &key_segment(&wide.repo.0)),
Ok(()),
"the repo alone is legal"
);
let key = pipeline_key(&PipelineDocument::HandOff(wide));
assert!(
matches!(&key, Err(PipelineRefusal::InvalidFormat { field, value }) if field == "hand_off.key" && value.len() > 64),
"a local of legal parts is still bounded whole, got {key:?}"
);
// The bound is 64 exactly, on both sides, whether the part that fills
// it is a plain label or an escaped repo. A hand-off local is
// `<receiver>.<repo>.<date>`, the repo `gamecult_-epiphany` and the
// date ten bytes, so the receiver fills the rest; a stewardship local is
// `<repo escaped>.n<N>`, so `GameCult_-` and `.n1` bound the name.
let hand_off_rest = "gamecult_-epiphany".len() + ".".len() + date().0.len() + ".".len();
let stewardship_rest = "GameCult_-".len() + ".n1".len();
let received = |receiver: usize| {
let mut hand_off = hand_off_sample();
hand_off.to_instance = Slug("a".repeat(receiver));
pipeline_key(&PipelineDocument::HandOff(hand_off))
};
let stewarded = |name: usize| {
let mut stewardship = stewardship_sample();
stewardship.repo = OrgRepo(format!("GameCult/{}", "a".repeat(name)));
pipeline_key(&PipelineDocument::Stewardship(stewardship))
};
for (label, at_64, at_65) in [
("plain label", received(64 - hand_off_rest), received(65 - hand_off_rest)),
("escaped repo", stewarded(64 - stewardship_rest), stewarded(65 - stewardship_rest)),
] {
let keyed = at_64.unwrap_or_else(|error| panic!("{label}: a 64-byte local keys: {error}"));
assert_eq!(keyed.split(':').nth(2).map(str::len), Some(64), "{label}: {keyed} fills the local exactly");
assert!(
matches!(&at_65, Err(PipelineRefusal::InvalidFormat { value, .. }) if value.len() == 65),
"{label}: a 65-byte local is refused, got {at_65:?}"
);
}
}
/// The resolution side of the bound: 111 keys and 112 is refused on
/// `resolution.key`, filled by a resolution subject (`resolution.<L>.n1`).
#[test]
fn a_composed_resolution_local_is_bounded_whole() {
let overhead = "resolution.".len() + ".n1".len();
let subject = |length: usize| format!("{CAMPAIGN}:resolution:{}", dotted_local(length));
let at_max = resolved_at(PipelineKind::Resolution, &subject(RESOLUTION_LOCAL_MAX - overhead), 1)
.expect("a 111-byte resolution local keys");
assert_eq!(at_max.split(':').nth(2).map(str::len), Some(RESOLUTION_LOCAL_MAX));
assert!(matches!(
resolved_at(PipelineKind::Resolution, &subject(RESOLUTION_LOCAL_MAX - overhead + 1), 1),
Err(PipelineRefusal::InvalidFormat { field, value }) if field == "resolution.key" && value.len() == RESOLUTION_LOCAL_MAX + 1
));
}
/// R2, on the composer every kind goes through: no local part carries the
/// separator, the head included. The live case is a hand-off with a dotted
/// receiver, which keys to an escaped label rather than a wider local.
#[test]
fn no_local_part_carries_the_separator() {
assert_eq!(
local("probe", PipelineKind::Question, &["a.b"]),
Err(PipelineRefusal::InvalidFormat { field: "probe".into(), value: "a.b".into() }),
"the head may not carry the separator"
);
assert_eq!(
local("probe", PipelineKind::Question, &["a", "b.c"]),
Err(PipelineRefusal::InvalidFormat { field: "probe".into(), value: "b.c".into() }),
"a tail part may not carry the separator"
);
assert_eq!(local("probe", PipelineKind::Question, &["a", "b", "c"]), Ok("a.b.c".into()));
let mut dotted = hand_off_sample();
dotted.to_instance = slug("thought-cage.GameCult");
let key = pipeline_key(&PipelineDocument::HandOff(dotted)).expect("a dotted receiver keys");
let local = key.split(':').nth(2).expect("three segments");
assert_eq!(local.split('.').count(), 3, "{key}: the receiver is one part, not two");
assert_eq!(local.split('.').next(), Some("thought-cage_dGameCult"));
}
/// Q17 B: a subject's resolutions are distinct records, ordered by a
/// per-subject sequence the key carries as its last part, so a withdrawn
/// resolution stays under its subject and the next closure is nameable.
/// The sequence is a field the writer sets and this crate only composes:
/// it bounds nothing beyond `u32` and the local's own width, so `0` and a
/// gap both key. Which sequence a document may claim, and whether an
/// earlier resolution still stands, are admission's rules, exactly as
/// `revision: 0` is.
#[test]
fn a_subject_keeps_every_resolution_it_had() {
let resolved = |sequence: u32| {
let mut resolution = resolution_sample();
resolution.sequence = sequence;
pipeline_key(&PipelineDocument::Resolution(resolution)).expect("a sequenced resolution keys")
};
let first = resolved(1);
let second = resolved(2);
assert_eq!(first, format!("{CAMPAIGN}:resolution:question.Q1.n1"));
assert_eq!(second, format!("{CAMPAIGN}:resolution:question.Q1.n2"));
assert_ne!(first, second, "two resolutions of one subject are two documents");
for (sequence, key) in [(1, &first), (2, &second)] {
let (root, local) = pipeline_id("read_back", key, PipelineKind::Resolution).expect("reads back");
assert_eq!(root, CAMPAIGN, "{key}: the subject's root");
let last = format!("n{sequence}");
assert_eq!(local.rsplit('.').next(), Some(last.as_str()), "{key} ends in its sequence");
assert_eq!(local.split('.').count(), 3, "{key}: subject kind, subject local, sequence");
}
// Outcome-invariance now holds per sequence: one record under two
// outcomes is still one key, which is exactly why a second closure of
// a subject needs a second sequence and not a second outcome.
let mut withdrawn = resolution_sample();
withdrawn.outcome = ResolutionOutcome::Withdrawn { reason: "moot".into() };
assert_eq!(
pipeline_key(&PipelineDocument::Resolution(withdrawn)),
Ok(first.clone()),
"one sequence is one key whatever the outcome"
);
// Stated limits. Both are admission's to refuse and neither is the
// leaf's, so both compose here.
assert_eq!(resolved(0), format!("{CAMPAIGN}:resolution:question.Q1.n0"), "the leaf refuses no sequence value");
let mut widest = resolution_sample();
widest.subject = PipelineRef { kind: PipelineKind::Ruling, id: id("ruling", "A") };
widest.sequence = u32::MAX;
assert_eq!(
pipeline_key(&PipelineDocument::Resolution(widest)),
Ok(format!("{CAMPAIGN}:resolution:ruling.A.n4294967295")),
"the widest sequence is an eleven-byte label, not a refusal"
);
}
/// D2: the sequence is the *last* part, so a subject's resolutions and
/// only they share the prefix `<root>:resolution:<kind>.<local>.n`. R2 is
/// what makes the prefix exact: no local part carries a dot, so
/// `question.Q1.n` cannot match `question.Q10.n1`, and the withdrawal of
/// `Q1`'s first resolution is a record of that resolution, one nesting
/// out and outside `Q1`'s prefix. Pinned as a string property, which is
/// what a prefix query is.
#[test]
fn a_subjects_resolutions_share_a_prefix_no_other_key_has() {
let resolved = |kind: PipelineKind, subject: &str, sequence: u32| {
let mut resolution = resolution_sample();
resolution.subject = PipelineRef { kind, id: Short(subject.into()) };
resolution.sequence = sequence;
pipeline_key(&PipelineDocument::Resolution(resolution)).expect("the subject keys")
};
let history = [1, 2, 3].map(|sequence| resolved(PipelineKind::Question, &id("question", "Q1").0, sequence));
let others = [
resolved(PipelineKind::Question, &id("question", "Q10").0, 1),
resolved(PipelineKind::Ruling, &id("ruling", "Q1").0, 1),
resolved(PipelineKind::Campaign, &format!("{CAMPAIGN}:campaign:self"), 1),
resolved(PipelineKind::Resolution, &history[0], 1),
];
assert_eq!(
others[3],
format!("{CAMPAIGN}:resolution:resolution.question.Q1.n1.n1"),
"a withdrawal keys under the resolution it withdraws, not under the subject"
);
let prefix = format!("{CAMPAIGN}:resolution:question.Q1.n");
let selected = history
.iter()
.chain(others.iter())
.filter(|key| key.starts_with(&prefix))
.collect::<Vec<_>>();
assert_eq!(
selected,
history.iter().collect::<Vec<_>>(),
"the prefix selects the subject's resolutions and nothing else"
);
}
/// Q18 A and Q20 A: a repo's stewardships on a mind are distinct records,
/// ordered by a per-`(instance, repo)` sequence the key carries last, so a
/// repo transferred away and later transferred back is two records under
/// one prefix rather than one document overwriting the other. The date is
/// not in the key: `assigned_on` is validated where every field is, by the
/// derived `Bounded` impl, and two assignments differing only in it are
/// one document.
#[test]
fn a_repo_keeps_every_stewardship_it_had() {
let stewarded = |repo: &str, sequence: u32, assigned_on: &str| {
let mut stewardship = stewardship_sample();
stewardship.repo = OrgRepo(repo.into());
stewardship.sequence = sequence;
stewardship.assigned_on = Date(assigned_on.into());
pipeline_key(&PipelineDocument::Stewardship(stewardship)).expect("a sequenced stewardship keys")
};
let first = stewarded("GameCult/Epiphany", 1, "2026-09-15");
let second = stewarded("GameCult/Epiphany", 2, "2026-09-16");
assert_eq!(first, format!("{INSTANCE}:stewardship:gamecult_-epiphany.n1"));
assert_eq!(second, format!("{INSTANCE}:stewardship:gamecult_-epiphany.n2"));
assert_ne!(first, second, "two assignments of one repo are two documents");
for (sequence, key) in [(1, &first), (2, &second)] {
let (root, local) = pipeline_id("read_back", key, PipelineKind::Stewardship).expect("reads back");
assert_eq!(root, INSTANCE, "{key}: the mind's root");
let last = format!("n{sequence}");
assert_eq!(local.rsplit('.').next(), Some(last.as_str()), "{key} ends in its sequence");
assert_eq!(local.split('.').count(), 2, "{key}: the escaped repo and the sequence, nothing else");
}
assert_eq!(
stewarded("GameCult/Epiphany", 1, "2026-09-16"),
first,
"assigned_on is a field, not a key part: one sequence is one key whatever the date"
);
let others = [
stewarded("GameCult/Epiphany_thing", 1, "2026-09-15"),
stewarded("GameCult/Huginn", 1, "2026-09-15"),
];
let prefix = format!("{INSTANCE}:stewardship:gamecult_-epiphany.n");
let history = [first, second];
let selected = history
.iter()
.chain(others.iter())
.filter(|key| key.starts_with(&prefix))
.collect::<Vec<_>>();
assert_eq!(
selected,
history.iter().collect::<Vec<_>>(),
"the prefix selects the repo's assignments on this mind and nothing else"
);
assert_eq!(
stewarded("GameCult/Epiphany", 0, "2026-09-15"),
format!("{INSTANCE}:stewardship:gamecult_-epiphany.n0"),
"the leaf refuses no sequence value"
);
}
fn read_anchor(line: u32, end_line: Option<u32>, symbol: Option<&str>) -> ReadAnchor {
ReadAnchor {
location: CodeLocation { path: s("crates/eureka-pipeline/src/lib.rs"), line, end_line, symbol: symbol.map(Symbol::from) },
why: "read".into(),
}
}
fn cut_spec_sample() -> PipelineCutSpec {
let PipelineDocument::CutSpec(spec) = samples().remove(4).0 else { unreachable!() };
spec
}
/// Every committed golden line decodes, from its committed payload bytes,
/// to the corpus document it was derived from. The pre-field lines carry no
/// `reads` or `symbol` key, so they decode to empty and `None` only
/// because both fields default on read.
#[test]
fn golden_payloads_decode_to_their_documents() -> Result<()> {
let cache = schema_cache()?;
let golden = include_str!("../golden/envelopes.txt");
let lines: BTreeMap<&str, &str> = golden
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.filter_map(|line| Some((line.split(' ').next()?, line.rsplit(' ').next()?)))
.collect();
let corpus = golden_documents();
assert_eq!(lines.len(), corpus.len());
for (label, document) in corpus {
let hex = lines[label.as_str()];
let mut envelope = document.prepare(&cache)?;
envelope.payload = (0..hex.len()).step_by(2).map(|at| u8::from_str_radix(&hex[at..at + 2], 16).unwrap()).collect();
assert_eq!(PipelineDocument::decode(&envelope)?, document, "{label} decodes to its document");
}
Ok(())
}
/// The MCP door: JSON with no `reads` key and locations with no `symbol`
/// key is the pre-field shape, and it decodes.
#[test]
fn pre_field_json_decodes() -> Result<()> {
let json = serde_json::to_value(PipelineDocument::CutSpec(cut_spec_sample()))?;
assert!(json["value"].get("reads").is_none(), "an empty reads is not written");
assert!(json["value"]["file_changes"][0]["location"].get("symbol").is_none(), "an absent symbol is not written");
let PipelineDocument::CutSpec(spec) = serde_json::from_value::<PipelineDocument>(json)? else { unreachable!() };
assert!(spec.reads.is_empty());
assert_eq!(spec.file_changes[0].location.symbol, None);
Ok(())
}
#[test]
fn symbol_and_reads_round_trip() -> Result<()> {
let mut spec = cut_spec_sample();
spec.reads = (0..16).map(|index| read_anchor(index + 1, Some(index + 100), Some("value_types"))).collect();
let mut finding = finding_sample();
finding.locations[0].symbol = Some("&1153090498".into());
for document in [PipelineDocument::CutSpec(spec), PipelineDocument::Finding(finding)] {
document.validate()?;
let json = serde_json::to_value(&document)?;
assert_eq!(serde_json::from_value::<PipelineDocument>(json.clone())?, document);
let packed = rmp_serde::to_vec_named(&document)?;
assert_eq!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, document);
assert!(packed.windows(6).any(|window| window == b"symbol"), "the symbol key is written");
assert_eq!(packed.windows(5).any(|window| window == b"reads"), matches!(document, PipelineDocument::CutSpec(_)));
assert!(json.to_string().contains("\"symbol\""));
}
Ok(())
}
#[test]
fn read_and_symbol_bounds() {
let with_reads = |reads: Vec<ReadAnchor>| {
let mut spec = cut_spec_sample();
spec.reads = reads;
PipelineDocument::CutSpec(spec).validate()
};
let with_symbol = |symbol: &str| {
let mut finding = finding_sample();
finding.locations[0].symbol = Some(symbol.into());
PipelineDocument::Finding(finding).validate()
};
assert_eq!(with_reads((0..16).map(|_| read_anchor(1, Some(100), None)).collect()), Ok(()));
assert_eq!(
with_reads((0..17).map(|_| read_anchor(1, Some(100), None)).collect()),
Err(PipelineRefusal::FieldBound { field: "cut_spec.reads".into(), limit: 16, actual: 17 })
);
assert_eq!(
with_reads(vec![read_anchor(5, Some(4), None)]),
Err(PipelineRefusal::InvalidFormat { field: "cut_spec.reads[0].location.end_line".into(), value: "4".into() })
);
assert_eq!(
with_reads(vec![read_anchor(0, None, None)]),
Err(PipelineRefusal::InvalidFormat { field: "cut_spec.reads[0].location.line".into(), value: "0".into() })
);
assert_eq!(with_reads(vec![read_anchor(7, Some(7), None)]), Ok(()), "a one-line span");
assert_eq!(with_reads(vec![read_anchor(10, Some(109), None)]), Ok(()), "a span of 100");
assert_eq!(
with_reads(vec![read_anchor(10, Some(110), None)]),
Err(PipelineRefusal::FieldBound { field: "cut_spec.reads[0].location.end_line".into(), limit: 100, actual: 101 })
);
assert_eq!(with_symbol(&"s".repeat(128)), Ok(()));
assert_eq!(
with_symbol(&"s".repeat(129)),
Err(PipelineRefusal::FieldBound { field: "finding.locations[0].symbol".into(), limit: 128, actual: 129 })
);
for bad in ["a b", "a\tb", "", "a\u{0}b", "a\u{3000}b"] {
assert_eq!(
with_symbol(bad),
Err(PipelineRefusal::InvalidFormat { field: "finding.locations[0].symbol".into(), value: bad.into() }),
"{bad:?}"
);
}
// A location outside a read anchor keeps its stored shape.
let mut finding = finding_sample();
finding.locations[0].line = 0;
finding.locations[0].end_line = Some(0);
assert_eq!(PipelineDocument::Finding(finding).validate(), Ok(()));
}
#[test]
fn target_repos_absent_decode_and_round_trip() -> Result<()> {
// The MCP door: a JSON target with no `repos` key decodes to an empty list.
let PipelineDocument::Target(sample) = samples().remove(1).0 else { unreachable!() };
let mut json = serde_json::to_value(PipelineDocument::Target(sample.clone()))?;
assert!(json["value"].get("repos").is_none(), "an empty repos is not written");
let PipelineDocument::Target(decoded) = serde_json::from_value::<PipelineDocument>(json.clone())? else { unreachable!() };
assert!(decoded.repos.is_empty());
assert!(!rmp_serde::to_vec_named(&PipelineDocument::Target(sample.clone()))?.windows(5).any(|window| window == b"repos"));
let mut named = sample;
named.repos = vec![OrgRepo("GameCult/Epiphany".into()), OrgRepo("GameCult/Huginn".into())];
let document = PipelineDocument::Target(named);
document.validate()?;
json = serde_json::to_value(&document)?;
assert_eq!(serde_json::from_value::<PipelineDocument>(json)?, document);
let packed = rmp_serde::to_vec_named(&document)?;
assert_eq!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, document);
assert!(packed.windows(5).any(|window| window == b"repos"), "the repos key is written");
Ok(())
}
#[test]
fn follow_up_origin_absent_is_not_written_and_a_direct_origin_round_trips() -> Result<()> {
let sample = samples()
.into_iter()
.find_map(|(document, _)| if let PipelineDocument::FollowUp(follow_up) = document { Some(follow_up) } else { None })
.expect("a follow_up sample");
let json = serde_json::to_value(PipelineDocument::FollowUp(sample.clone()))?;
assert!(json["value"].get("origin").is_none(), "an absent origin is not written");
assert!(!rmp_serde::to_vec_named(&PipelineDocument::FollowUp(sample.clone()))?.windows(6).any(|window| window == b"origin"));
let PipelineDocument::FollowUp(decoded) = serde_json::from_value::<PipelineDocument>(json)? else { unreachable!() };
assert_eq!(decoded.origin, None);
let mut direct = sample;
direct.origin = Some(MemoryOrigin::Direct { room: "!dm:chat".into(), with: vec!["@sample:chat".into()] });
let document = PipelineDocument::FollowUp(direct);
document.validate()?;
let json = serde_json::to_value(&document)?;
assert_eq!(serde_json::from_value::<PipelineDocument>(json)?, document);
let packed = rmp_serde::to_vec_named(&document)?;
assert_eq!(rmp_serde::from_slice::<PipelineDocument>(&packed)?, document);
assert!(packed.windows(6).any(|window| window == b"origin"), "the origin key is written");
Ok(())
}
#[test]
fn target_repos_bounded_and_distinct() {
let with_repos = |repos: Vec<OrgRepo>| {
let PipelineDocument::Target(mut target) = samples().remove(1).0 else { unreachable!() };
target.repos = repos;
PipelineDocument::Target(target).validate()
};
let distinct = |count: usize| (0..count).map(|index| OrgRepo(format!("GameCult/Repo{index}"))).collect::<Vec<_>>();
assert_eq!(with_repos(distinct(16)), Ok(()));
assert_eq!(
with_repos(distinct(17)),
Err(PipelineRefusal::FieldBound { field: "target.repos".into(), limit: 16, actual: 17 })
);
assert_eq!(
with_repos(vec![OrgRepo("GameCult/Huginn".into()), OrgRepo("gamecult/huginn".into())]),
Err(PipelineRefusal::DuplicateRepo { field: "target.repos".into(), repo: "gamecult/huginn".into() })
);
}
/// The marker needs an `Absent` spelling; the two impls say what absent is.
#[test]
fn absent_is_none_and_empty() {
assert!(Absent::absent(&None::<u32>));
assert!(!Absent::absent(&Some(0u32)));
assert!(Absent::absent(&Vec::<u32>::new()));
assert!(!Absent::absent(&vec![0u32]));
}
/// Cut 8: the leaf owns the serialisation of the shapes it owns, so a wire
/// carrying a document or a refusal reads back the same value rather than
/// a mirror definition kept in step by hand in the reader's repo. Both
/// encodings are pinned: JSON is what a schema describes, MessagePack is
/// what a CultCache payload is written in, and an adjacent tag has to
/// survive both. The tag is asserted against `name()` rather than against
/// a literal, because the point of the tag is that a reader dispatching on
/// a key's kind segment finds the same string here.
#[test]
fn every_document_and_refusal_serialises_and_reads_back() -> Result<()> {
for (document, _) in samples() {
let json = serde_json::to_value(&document)?;
assert_eq!(json["kind"].as_str(), Some(document.kind().name()), "{json} is tagged with its kind name");
assert_eq!(serde_json::from_value::<PipelineDocument>(json.clone())?, document, "{json} reads back");
let packed = rmp_serde::to_vec_named(&document)?;
assert_eq!(
rmp_serde::from_slice::<PipelineDocument>(&packed)?,
document,
"{:?} reads back through MessagePack",
document.kind()
);
}
// Every variant, and every one of them carrying its named parts: a
// refusal read off a wire names the field it refused, as a local one
// does.
let refusals = [
PipelineRefusal::FieldBound { field: "campaign.title".into(), limit: 200, actual: 201 },
PipelineRefusal::InvalidFormat { field: "campaign.slug".into(), value: "a b".into() },
PipelineRefusal::InvalidIdentity {
kind: PipelineKind::Campaign,
key: format!("{CAMPAIGN}:campaign:forged"),
expected: format!("{CAMPAIGN}:campaign:self"),
},
PipelineRefusal::DuplicateRepo { field: "campaign.repos".into(), repo: "gamecult/epiphany".into() },
PipelineRefusal::ForeignStore { r#type: <ForeignDocument as DatabaseEntry>::TYPE.into() },
];
for refusal in &refusals {
let json = serde_json::to_value(refusal)?;
assert_eq!(&serde_json::from_value::<PipelineRefusal>(json.clone())?, refusal, "{json} reads back");
let packed = rmp_serde::to_vec_named(refusal)?;
assert_eq!(
&rmp_serde::from_slice::<PipelineRefusal>(&packed)?,
refusal,
"{refusal:?} reads back through MessagePack"
);
}
// The derived schemas list the same two sets. A `const` in the tag
// position is what a schema reader dispatches on, so it is read as the
// schema writes it and compared to `name()`, not to a literal list.
let tag = |variant: &serde_json::Value| {
variant["properties"]["kind"]["const"]
.as_str()
.unwrap_or_else(|| panic!("a document variant schema carries a kind const: {variant}"))
.to_owned()
};
let document_schema = serde_json::to_value(schemars::schema_for!(PipelineDocument))?;
let variants = document_schema["oneOf"].as_array().expect("the document schema is a oneOf");
assert_eq!(PipelineKind::ALL.len(), 21, "twenty-one kinds");
assert_eq!(
variants.iter().map(tag).collect::<Vec<_>>(),
PipelineKind::ALL.iter().map(|kind| kind.name().to_owned()).collect::<Vec<_>>(),
"the document schema lists exactly the twenty-one kinds, by name"
);
let refusal_schema = serde_json::to_value(schemars::schema_for!(PipelineRefusal))?;
let refusal_variants = refusal_schema["oneOf"].as_array().expect("the refusal schema is a oneOf");
let named = refusal_variants
.iter()
.map(|variant| {
let properties = variant["properties"].as_object().expect("a refusal variant is an object");
assert_eq!(properties.len(), 1, "{variant} is externally tagged");
properties.keys().next().expect("one key").to_owned()
})
.collect::<Vec<_>>();
assert_eq!(named, vec!["FieldBound", "InvalidFormat", "InvalidIdentity", "DuplicateRepo", "ForeignStore"]);
assert_eq!(named.len(), refusals.len(), "every refusal variant is in the schema and in the round trip");
Ok(())
}
#[test]
fn pipeline_published_schemas_match_derivation() -> Result<()> {
let published = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../schemas/cultnet");
let index: serde_json::Value = serde_json::from_slice(&std::fs::read(published.join("index.json"))?)?;
// A directory this run alone writes, so everything in it is what this
// run derived. A previous run's leftovers -- a mutation run's
// especially, since those deliberately derive schemas no committed type
// produces -- are not this derivation, and copying one of those into
// `schemas/cultnet` publishes a schema no Rust type produces. A shared
// directory cleared first would have to defend that clear against a
// held handle; an unshared one has nothing to defend. The cost is that
// nothing ever removes one. The directory is only created when
// something is stale, so a passing run leaves none -- but a failing run
// leaves its copies behind for good, one directory per failing run
// where the shared design left one however often it failed. That is the
// trade: the copies are the evidence the failure message names, and
// they outlive the run that wrote them. Whoever reads the failure is
// the one who deletes them.
let derived_dir = std::env::temp_dir().join(format!(
"eureka-pipeline-schemas-{}-{}",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_nanos()
));
let mut stale = Vec::new();
for kind in PipelineKind::ALL {
let file = format!("{}.schema.json", kind.type_id());
let mut schema = serde_json::to_value(kind.derived_schema())?;
schema["$id"] = format!("https://gamecult.dev/epiphany/cultnet/{file}").into();
let derived = format!("{}\n", serde_json::to_string_pretty(&schema)?);
if std::fs::read(published.join(&file)).ok().as_deref() != Some(derived.as_bytes()) {
std::fs::create_dir_all(&derived_dir)?;
std::fs::write(derived_dir.join(&file), &derived)?;
stale.push(derived_dir.join(&file));
}
let entry = serde_json::json!({
"schemaId": format!("https://gamecult.dev/epiphany/cultnet/{file}"),
"kind": "document_payload",
"wireContracts": ["cultnet.schema.v0"],
"schemaVersion": kind.type_id(),
"documentType": kind.type_id(),
"title": format!("Epiphany Pipeline {kind:?} v2"),
"path": file,
});
assert!(
index["schemas"].as_array().is_some_and(|schemas| schemas.contains(&entry)),
"index.json lacks {entry}"
);
}
assert!(stale.is_empty(), "published pipeline schemas differ from the Rust derivation; derived copies: {stale:?}");
Ok(())
}
/// A refusal read as text names its variant and its parts, so an operator
/// who sees only the message (a log line, an `anyhow` chain) can tell which
/// field was refused and why.
#[test]
fn a_refusal_read_as_text_names_its_variant_and_parts() {
let text = PipelineRefusal::FieldBound { field: "title".into(), limit: 200, actual: 201 }.to_string();
assert!(text.starts_with("pipeline refusal: "), "{text}");
for part in ["FieldBound", "title", "200", "201"] {
assert!(text.contains(part), "{part} is missing from {text}");
}
}
/// The catalogue is exactly the files beside it: every entry's `path` is a
/// schema file in the directory, and every schema file in the directory
/// has an entry, so a schema removed from the catalogue (or a file left
/// behind by a move) fails here instead of publishing a dangling or
/// unlisted contract. The one rule: every `*.schema.json` in the directory,
/// Huginn's own wire schemas included, is listed.
#[test]
fn the_catalogue_lists_exactly_the_schema_files_it_publishes() -> Result<()> {
let published = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../schemas/cultnet");
let index: serde_json::Value = serde_json::from_slice(&std::fs::read(published.join("index.json"))?)?;
let listed = index["schemas"]
.as_array()
.expect("index.json carries a schemas array")
.iter()
.map(|entry| entry["path"].as_str().expect("every entry names its file").to_owned())
.collect::<std::collections::BTreeSet<_>>();
for path in &listed {
assert!(published.join(path).is_file(), "index.json lists {path}, which is not in the directory");
}
let mut files = std::collections::BTreeSet::new();
for entry in std::fs::read_dir(&published)? {
let name = entry?.file_name().to_string_lossy().into_owned();
if name.ends_with(".schema.json") {
files.insert(name);
}
}
assert_eq!(listed, files, "the catalogue and the schema files beside it disagree");
Ok(())
}
/// The epoch is the version every kind is published at: its version
/// segment is read from the constant, not restated, and every type id
/// carries the same one, so a bumped epoch fails here until the kinds
/// move with it. The live registrar registers the kinds and nothing else,
/// so the foreign stand-in is not among them.
#[test]
fn every_kind_is_at_the_epochs_version() -> Result<()> {
let (name, version) = PIPELINE_SCHEMA_EPOCH.rsplit_once('.').expect("the epoch carries a version");
assert_eq!(name, "epiphany.pipeline.epoch");
assert!(
version.len() > 1 && version.starts_with('v') && version[1..].bytes().all(|byte| byte.is_ascii_digit()),
"{version:?} is not a version segment"
);
for kind in PipelineKind::ALL {
let type_id = kind.type_id();
assert!(type_id.ends_with(&format!(".{version}")), "{type_id} is not at the epoch's version {version}");
}
let mut cache = CultCache::new();
register_pipeline_document_types(&mut cache)?;
let registered = cache.registered_entry_types();
assert_eq!(registered.len(), PipelineKind::ALL.len(), "the live registrar registers exactly the kinds: {registered:?}");
for kind in PipelineKind::ALL {
assert!(registered.iter().any(|name| name == kind.type_id()), "{kind:?} is registered");
}
Ok(())
}
/// The public door onto the reference grammar, for the read side that
/// holds a `kind` and an `id` and no way to reach `Bounded`. Every sample
/// document's own key is a reference of its own kind; a kind that is not
/// the id's, and a local no writer composes, are `InvalidFormat` at `ref.id`.
#[test]
fn a_ref_validates_as_an_id_of_the_kind_it_declares() -> Result<()> {
for (document, expected) in samples() {
let kind = document.kind();
let key = pipeline_key(&document)?;
assert_eq!(key, expected);
assert_eq!(
PipelineRef { kind, id: s(&key) }.validate_ref(),
Ok(()),
"{kind:?}'s own key is not a reference of its kind"
);
}
let refused = |reference: PipelineRef, why: &str| {
let read = reference.validate_ref();
assert!(
matches!(&read, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "ref.id"),
"{why}: {reference:?} was not refused, got {read:?}"
);
};
// The kind and the id's kind segment cannot disagree: the whole reason
// the door exists, since a reader that cannot ask would look this up
// and report the nothing it found.
refused(
PipelineRef { kind: PipelineKind::Campaign, id: id("question", "Q1") },
"a question id is not a campaign reference",
);
refused(
PipelineRef { kind: PipelineKind::Ruling, id: id("question", "Q1") },
"a question id is not a ruling reference",
);
// The local is `dotted_text` whole, so a trailing dot is refused by the
// reader and not only by the writer that never composes one.
refused(
PipelineRef { kind: PipelineKind::Question, id: s(&format!("{CAMPAIGN}:question:Q1.")) },
"a trailing dot is no local",
);
// The boundary, pinned rather than assumed: a resolution id missing the
// per-subject sequence its writer always composes is still a
// well-formed id of its kind here. The leaf's grammar is kind-deep, and
// the local's per-kind shape belongs to admission; refusing this would
// be a new rule in `pipeline_id`, not a door onto the one that exists.
assert_eq!(
PipelineRef { kind: PipelineKind::Resolution, id: s(&format!("{CAMPAIGN}:resolution:question.Q1")) }
.validate_ref(),
Ok(()),
"the leaf reads a resolution id kind-deep"
);
Ok(())
}
/// The public door onto the slug grammar, for a caller outside this crate
/// holding a bare declared name (Huginn's `require_instance` and
/// `Mind::open`, before either reaches admission or the filesystem). It
/// applies exactly `dotted_text`: one valid dotted name, and refusals for
/// fullwidth characters, `..`, both path separators, the empty string,
/// and an empty label.
#[test]
fn slug_validate_applies_the_dotted_grammar() {
assert_eq!(slug("huginn-yggdrasil").validate_slug(), Ok(()));
assert_eq!(slug("outer.inner").validate_slug(), Ok(()));
let refused = |value: &str, why: &str| {
let result = slug(value).validate_slug();
assert!(
matches!(&result, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "slug"),
"{why}: {value:?} was not refused, got {result:?}"
);
};
refused("\u{FF41}\u{FF41}", "fullwidth characters are not ascii alphanumeric");
refused("..", "a bare .. is an empty label either side of the dot");
refused("../escaped", "a forward-slash path separator is not a label byte");
refused("..\\escaped", "a backslash path separator is not a label byte");
refused("outer\\inner", "a lone backslash inside one label is not a label byte, with no empty label to hide behind");
refused("", "the empty string has no label");
refused("outer..inner", "an empty label between two dots is refused");
refused(".", "a single dot is one empty label");
// S5: the 64-byte whole-name bound, pinned with a two-label name
// whose parts sit far under 64 bytes each (31 and 32, then 32 and
// 32), so only `dotted_text`'s own `value.len() > 64` check -- not
// `label_text`'s per-part one -- can be the check that refuses 65.
let filler = |n: usize| "a".repeat(n);
let at_64 = format!("{}.{}", filler(31), filler(32));
let at_65 = format!("{}.{}", filler(32), filler(32));
assert_eq!(at_64.len(), 64, "a 64-byte whole name, two labels well under 64 each");
assert_eq!(at_65.len(), 65, "one byte past the whole-name bound, same two-label shape");
assert_eq!(slug(&at_64).validate_slug(), Ok(()));
refused(&at_65, "65 bytes overall is refused, even though neither label alone reaches 64");
// S5: the 64-byte per-label bound, pinned directly against `Label`
// (the type `label_text` guards on its own, with no separate
// whole-name wrapper), and with a lone label carrying no dot, so only
// `label_text`'s own length check can be the one refusing 65.
let label_at_64 = Label::from(filler(64).as_str());
let label_at_65 = Label::from(filler(65).as_str());
assert_eq!(label_at_64.validate("label"), Ok(()));
assert!(
matches!(
label_at_65.validate("label"),
Err(PipelineRefusal::InvalidFormat { ref field, .. }) if field == "label"
),
"a 65-byte label was not refused, got {:?}",
label_at_65.validate("label")
);
// S5: NUL is refused as a label byte, not merely as one more
// non-alphanumeric character that happens to be caught by coincidence.
refused("a\u{0}b", "NUL is not a label byte");
}
/// The two doors added beside `Slug::validate_slug` (RS-L), on its
/// pattern: each delegates to the grammar its own document field already
/// carries, so a declared name checked through the door and a field of
/// the same type refuse the same inputs. `OrgRepo` is the `repo` alias's
/// domain and `Label` is the `cut` alias's (RS-3's vocabulary table).
#[test]
fn the_org_repo_and_label_doors_are_the_grammar() {
assert_eq!(OrgRepo::from("GameCult/Epiphany").validate_org_repo(), Ok(()));
assert_eq!(OrgRepo::from("a/b").validate_org_repo(), Ok(()));
assert_eq!(OrgRepo::from("a-b/c.d_e").validate_org_repo(), Ok(()));
// Consecutive hyphens stay allowed on both sides; only a leading or
// trailing owner hyphen is refused.
assert_eq!(OrgRepo::from("a--b/c--d").validate_org_repo(), Ok(()));
// S4: a 39-byte owner is accepted, a 100-byte repo is accepted.
let owner_at_39 = format!("{}/b", "a".repeat(39));
assert_eq!(OrgRepo::from(owner_at_39.as_str()).validate_org_repo(), Ok(()));
let repo_at_100 = format!("a/{}", "a".repeat(100));
assert_eq!(OrgRepo::from(repo_at_100.as_str()).validate_org_repo(), Ok(()));
let repo_refused = |value: &str, why: &str| {
let result = OrgRepo::from(value).validate_org_repo();
assert!(
matches!(&result, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "org_repo"),
"{why}: {value:?} was not refused, got {result:?}"
);
};
repo_refused("GameCult", "no slash at all");
repo_refused("/Repo", "an empty org before the slash");
repo_refused("GameCult/", "an empty repo after the slash");
repo_refused("a/b/c", "a second slash makes the repo half ambiguous");
repo_refused("../..", "an owner of dots is not GitHub's grammar");
repo_refused("a/..", "a repo of .. is refused outright");
repo_refused("a/.", "a repo of . is refused outright");
repo_refused("-a/b", "a leading hyphen is not a valid owner");
repo_refused("a-/b", "a trailing hyphen is not a valid owner");
repo_refused("a b/c", "a space is not an owner byte");
repo_refused("a/b c", "a space is not a repo byte");
repo_refused("\u{e9}/\u{fc}", "non-ascii bytes are refused on either side");
repo_refused("\0/\0", "NUL is refused on either side");
repo_refused("a\n/b", "a newline is not an owner byte");
let owner_at_40 = format!("{}/b", "a".repeat(40));
repo_refused(&owner_at_40, "40 bytes is past the 39-byte owner bound");
let repo_at_101 = format!("a/{}", "a".repeat(101));
repo_refused(&repo_at_101, "101 bytes is past the 100-byte repo bound");
repo_refused("a//b", "an empty middle segment is still a second slash");
repo_refused("/b", "an empty owner is refused");
repo_refused("a/", "an empty repo is refused");
repo_refused("a.b/c", "a dot is not an owner byte");
repo_refused("a_b/c", "an underscore is not an owner byte");
// F7 fix batch: a repo name ending in `.git` is refused.
repo_refused("a/b.git", "a repo ending in .git is refused");
repo_refused("a/.git", "a repo of exactly .git is still a .git suffix");
// S-2: the `.git` refusal is ASCII-case-insensitive, so a case variant
// cannot dodge it and reach an `identity()` the validator refuses.
repo_refused("a/b.GIT", "a repo ending in .GIT is refused like .git");
repo_refused("a/.GIT", "a repo of exactly .GIT is still a .git suffix");
repo_refused("A/B.GIT", "a mixed-case owner and a .GIT repo both refuse");
repo_refused("a/b.GiT", "a mixed-case .git suffix is refused");
// identity(): the one canonical key, ASCII-lowercased. Every
// comparison, hash, ordering and key derivation goes through it.
assert_eq!(
OrgRepo::from("GameCult/Epiphany").identity(),
OrgRepo::from("gamecult/epiphany").identity()
);
assert_eq!(OrgRepo::from("GameCult/Epiphany").identity(), "gamecult/epiphany");
// S-1: it is structural, not advisory. `==` on `OrgRepo` compares
// identity, not the raw string, so the two spellings are one
// repository wherever `OrgRepo` is compared, hashed or ordered.
assert_eq!(OrgRepo::from("GameCult/Epiphany"), OrgRepo::from("gamecult/epiphany"));
assert_eq!(OrgRepo::from("GameCult/Epiphany").cmp(&OrgRepo::from("gamecult/epiphany")), std::cmp::Ordering::Equal);
assert_eq!(
OrgRepo::from("GameCult/Epiphany").partial_cmp(&OrgRepo::from("gamecult/epiphany")),
Some(OrgRepo::from("GameCult/Epiphany").cmp(&OrgRepo::from("gamecult/epiphany"))),
"partial_cmp must agree with cmp for an equal pair"
);
// RS-L closing fix 1: every assertion above compares values that
// should be equal, so `eq -> true`, `hash -> ()` and `cmp -> Equal`
// all survive as mutants. Pin the unequal direction as well: two
// different repos must be unequal, hash differently (with
// overwhelming probability, not by construction, so this is belt and
// suspenders on top of the `!=` and `cmp` checks), and not compare
// `Equal`, and `partial_cmp` must agree with `cmp` for an unequal
// pair too, per std's own contract that `partial_cmp(a, b) ==
// Some(cmp(a, b))`.
let a = OrgRepo::from("GameCult/Epiphany");
let b = OrgRepo::from("GameCult/Aetheria");
assert_ne!(a, b, "two different repos must not compare equal");
assert_ne!(
a.cmp(&b),
std::cmp::Ordering::Equal,
"two different repos must not order as Equal"
);
assert_eq!(
a.partial_cmp(&b),
Some(a.cmp(&b)),
"partial_cmp must agree with cmp for an unequal pair"
);
let hash_of = |repo: &OrgRepo| {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
repo.hash(&mut hasher);
hasher.finish()
};
assert_ne!(
hash_of(&a),
hash_of(&b),
"two different repos must hash differently"
);
let mut hash_map = std::collections::HashMap::new();
hash_map.insert(OrgRepo::from("GameCult/Epiphany"), 1);
hash_map.insert(OrgRepo::from("gamecult/epiphany"), 2);
assert_eq!(hash_map.len(), 1, "a HashMap keyed by both spellings holds one entry");
assert_eq!(hash_map.get(&OrgRepo::from("GAMECULT/EPIPHANY")), Some(&2));
let mut btree_map = std::collections::BTreeMap::new();
btree_map.insert(OrgRepo::from("GameCult/Epiphany"), 1);
btree_map.insert(OrgRepo::from("gamecult/epiphany"), 2);
assert_eq!(btree_map.len(), 1, "a BTreeMap keyed by both spellings holds one entry");
assert_eq!(btree_map.get(&OrgRepo::from("GAMECULT/EPIPHANY")), Some(&2));
// `identity()` is always itself a valid `OrgRepo`: lowercasing never
// turns a valid repository into one the grammar refuses, and never
// turns a refused one into one it accepts.
for value in ["GameCult/Epiphany", "A/B", "a-B/c.D_e", "GameCult/Epiphany.GIT"] {
let repo = OrgRepo::from(value);
assert_eq!(
repo.validate_org_repo().is_ok(),
OrgRepo::from(repo.identity().as_str()).validate_org_repo().is_ok(),
"identity() of {value:?} disagreed with the value's own validity"
);
}
assert_eq!(Label::from("cut-10").validate_label(), Ok(()));
let label_refused = |value: &str, why: &str| {
let result = Label::from(value).validate_label();
assert!(
matches!(&result, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "label"),
"{why}: {value:?} was not refused, got {result:?}"
);
};
label_refused("", "the empty string has no label");
label_refused("a.b", "a dot is not a label byte");
label_refused(&"a".repeat(65), "65 bytes is past the 64-byte label bound");
label_refused("\u{e9}", "a non-ascii byte is not a label byte");
}
/// Q-RS1, ruled B: `question` and `ruling` gain a required `title`, the
/// new `Title` type shared with the two titles that move off `Short`
/// (campaign, cut spec). Each kind round-trips through `prepare`/`decode`
/// with its title intact, an empty title is refused for both, and the
/// bound is pinned at its edge: 200 bytes accepted, 201 refused (L3 dies
/// on the removal of the non-empty check, not on the length bound, since
/// `Title`'s length bound is shared with every other `bounded_text!`
/// member and already covered by `bounds_refuse_in_utf8_bytes`).
#[test]
fn question_and_ruling_carry_a_title() -> Result<()> {
let cache = schema_cache()?;
let mut question = question_sample();
question.title = t("Who owns the state?");
let envelope = PipelineDocument::Question(question.clone()).prepare(&cache)?;
assert_eq!(PipelineDocument::decode(&envelope)?, PipelineDocument::Question(question.clone()));
let mut ruling = ruling_sample();
ruling.title = t("An instance owns its mind");
let envelope = PipelineDocument::Ruling(ruling.clone()).prepare(&cache)?;
assert_eq!(PipelineDocument::decode(&envelope)?, PipelineDocument::Ruling(ruling.clone()));
question.title = Title(String::new());
assert_eq!(
PipelineDocument::Question(question.clone()).validate(),
Err(PipelineRefusal::InvalidFormat { field: "question.title".into(), value: String::new() })
);
ruling.title = Title(String::new());
assert_eq!(
PipelineDocument::Ruling(ruling.clone()).validate(),
Err(PipelineRefusal::InvalidFormat { field: "ruling.title".into(), value: String::new() })
);
// F2 second fix batch: a title holds at least one
// `char::is_alphanumeric()` character, no `char::is_control()`
// character, and no bidi control character. No zero-width denylist:
// a zero-width joiner/non-joiner inside real text is allowed, and a
// string of nothing but invisible characters is refused because it
// has no alphanumeric character, not because those characters are
// individually denylisted.
let refused = |value: &str| {
let mut refused_question = question.clone();
refused_question.title = Title(value.into());
assert_eq!(
PipelineDocument::Question(refused_question).validate(),
Err(PipelineRefusal::InvalidFormat { field: "question.title".into(), value: value.into() }),
"{value:?} should be refused"
);
};
refused(" ");
refused("\u{7f}");
refused("a\u{7f}");
refused("\u{0}");
refused("\u{9f}");
refused("\u{2028}");
refused("\u{202e}"); // RLO alone
refused("\u{202e}text"); // RLO plus text
refused("\u{200e}"); // LRM alone
refused("\u{0301}"); // a lone combining mark
refused("\u{fe0f}"); // a variation selector alone
refused("\u{a0}"); // NBSP alone
refused("\u{1f680}"); // emoji only
// S-3: every one of the twelve bidi control code points is pinned
// with an embedded fixture (real alphanumeric text on both sides), so
// the alphanumeric rule cannot mask a hole in the bidi match arm the
// way a lone-character fixture would.
refused("a\u{061C}b"); // ALM
refused("a\u{200E}b"); // LRM
refused("a\u{200F}b"); // RLM
refused("a\u{202A}b"); // LRE
refused("a\u{202B}b"); // RLE
refused("a\u{202C}b"); // PDF
refused("a\u{202D}b"); // LRO
refused("a\u{202E}b"); // RLO
refused("a\u{2066}b"); // LRI
refused("a\u{2067}b"); // RLI
refused("a\u{2068}b"); // FSI
refused("a\u{2069}b"); // PDI
// S-4: U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are
// refused inside otherwise-alphanumeric text, not only alone, since
// `char::is_control()` does not reach `Zl`/`Zp`.
refused("a\u{2028}b");
refused("a\u{2029}b");
let accepted = |value: &str| {
let mut accepted_question = question.clone();
accepted_question.title = Title(value.into());
assert_eq!(
PipelineDocument::Question(accepted_question).validate(),
Ok(()),
"{value:?} should validate"
);
};
accepted("a");
accepted("\u{e9}");
accepted("\u{200c}\u{647}\u{6cc}"); // Persian text containing ZWNJ
accepted("a\u{200d}b");
accepted("a \u{1f680}");
accepted(" a ");
// Discrepancy from the fix spec: HANGUL FILLER (U+3164) is Unicode
// General_Category Lo, so `char::is_alphanumeric()` is true for it in
// this toolchain; it is not a denylist candidate, since the fix
// batch deletes the hand-written exclusion list on purpose. A title
// of U+3164 alone is therefore accepted, not refused.
accepted("\u{3164}");
question.title = Title("a".repeat(200));
assert_eq!(PipelineDocument::Question(question.clone()).validate(), Ok(()));
question.title = Title("a".repeat(201));
assert_eq!(
PipelineDocument::Question(question).validate(),
Err(PipelineRefusal::FieldBound { field: "question.title".into(), limit: 200, actual: 201 })
);
Ok(())
}
fn desk_answer_sample() -> PipelineDeskAnswer {
let PipelineDocument::DeskAnswer(answer) = samples().pop().unwrap().0 else { unreachable!() };
answer
}
#[test]
fn a_desk_answer_is_keyed_under_its_question_and_sequence() {
let mut answer = desk_answer_sample();
let key = |answer: &PipelineDeskAnswer| pipeline_key(&PipelineDocument::DeskAnswer(answer.clone()));
assert_eq!(key(&answer), Ok(format!("{CAMPAIGN}:desk_answer:Q1.n1")));
// Every 64-byte question label is answerable at the largest sequence.
let label = "q".repeat(64);
answer.fork = id("question", &label);
answer.sequence = u32::MAX;
answer.outcome = DeskOutcome::Counted;
assert_eq!(key(&answer), Ok(format!("{CAMPAIGN}:desk_answer:{label}.n4294967295")));
// The fork must be a question of the same campaign.
answer.fork = Short(format!("other:question:{label}"));
assert!(matches!(key(&answer), Err(PipelineRefusal::InvalidFormat { .. })));
answer.fork = id("ruling", "Q1");
assert!(matches!(key(&answer), Err(PipelineRefusal::InvalidFormat { .. })));
}
#[test]
fn a_desk_outcome_names_desk_answers_by_id() {
let mut answer = desk_answer_sample();
for bad in [id("question", "Q1"), Short("n1".into())] {
answer.outcome = DeskOutcome::Retraction { retracts: bad.clone() };
assert!(matches!(PipelineDocument::DeskAnswer(answer.clone()).validate(), Err(PipelineRefusal::InvalidFormat { .. })), "{bad:?}");
answer.outcome = DeskOutcome::Revision { of: bad.clone() };
assert!(matches!(PipelineDocument::DeskAnswer(answer.clone()).validate(), Err(PipelineRefusal::InvalidFormat { .. })), "{bad:?}");
}
answer.outcome = DeskOutcome::Retraction { retracts: id("desk_answer", "Q1.n2") };
assert_eq!(PipelineDocument::DeskAnswer(answer).validate(), Ok(()));
}
#[test]
fn a_refused_target_is_bounded() {
let mut answer = desk_answer_sample();
answer.outcome = DeskOutcome::Refused { code: DeskRefusal::BadRetraction, retracts: Some(Short("x".repeat(200))) };
assert_eq!(PipelineDocument::DeskAnswer(answer.clone()).validate(), Ok(()));
answer.outcome = DeskOutcome::Refused { code: DeskRefusal::BadRetraction, retracts: Some(Short("x".repeat(201))) };
assert_eq!(
PipelineDocument::DeskAnswer(answer).validate(),
Err(PipelineRefusal::FieldBound { field: "desk_answer.outcome.retracts".into(), limit: 200, actual: 201 })
);
}
}