From 73908920caf64a799895e0f5fbe47002fd0a41b8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 9 Oct 2026 14:07:58 +0200 Subject: [PATCH 01/11] Copy the pipeline leaf and its schema catalogue from Epiphany ef956865 Byte-identical blobs of epiphany-pipeline/{Cargo.toml,src/lib.rs}, schemas/README.md and schemas/cultnet/* at Epiphany ef9568655e75, so Soul can diff the copy against the source. Left out as the spec orders: epiphany.work_organ_state.v0.schema.json and its index.json entry (no reader outside Epiphany). Epiphany's schemas/cultnet/README.md is not copied over Huginn's own README there; the next commit folds its pipeline section in. The crate is not yet a workspace member and keeps its Epiphany names, so this commit does not build as a package; nothing else changed. Co-Authored-By: Claude Opus 5.5 --- crates/eureka-pipeline/Cargo.toml | 27 + crates/eureka-pipeline/src/lib.rs | 3187 +++++++++++++++++ schemas/README.md | 47 + .../epiphany.pipeline.campaign.v2.schema.json | 89 + ...piphany.pipeline.cut_report.v2.schema.json | 375 ++ .../epiphany.pipeline.cut_spec.v2.schema.json | 412 +++ .../epiphany.pipeline.finding.v2.schema.json | 264 ++ ...epiphany.pipeline.follow_up.v2.schema.json | 132 + .../epiphany.pipeline.hand_off.v2.schema.json | 66 + .../epiphany.pipeline.instance.v2.schema.json | 44 + .../epiphany.pipeline.question.v2.schema.json | 147 + ...piphany.pipeline.resolution.v2.schema.json | 234 ++ .../epiphany.pipeline.ruling.v2.schema.json | 174 + ...iphany.pipeline.stewardship.v2.schema.json | 55 + .../epiphany.pipeline.target.v2.schema.json | 117 + .../epiphany.pipeline.verdict.v2.schema.json | 166 + .../gamecult.persona_state.v0.schema.json | 1024 ++++++ schemas/cultnet/index.json | 159 + 18 files changed, 6719 insertions(+) create mode 100644 crates/eureka-pipeline/Cargo.toml create mode 100644 crates/eureka-pipeline/src/lib.rs create mode 100644 schemas/README.md create mode 100644 schemas/cultnet/epiphany.pipeline.campaign.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.cut_report.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.cut_spec.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.finding.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.follow_up.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.hand_off.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.instance.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.question.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.resolution.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.stewardship.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.target.v2.schema.json create mode 100644 schemas/cultnet/epiphany.pipeline.verdict.v2.schema.json create mode 100644 schemas/cultnet/gamecult.persona_state.v0.schema.json create mode 100644 schemas/cultnet/index.json diff --git a/crates/eureka-pipeline/Cargo.toml b/crates/eureka-pipeline/Cargo.toml new file mode 100644 index 0000000..48d2fdd --- /dev/null +++ b/crates/eureka-pipeline/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "epiphany-pipeline" +version = "0.1.0" +edition = "2024" +publish = false +autobins = false +license-file = "../LICENSE" + +[lib] +name = "epiphany_pipeline" +path = "src/lib.rs" + +# A leaf type library. Every dependency here is one the document types or the +# write path need in a non-test build: the organ prepares, decodes and +# validates envelopes through this crate, so `anyhow` (the cache's error type) +# and `rmp-serde` (the payload decode) are normal dependencies. Only the +# schema-publication check needs JSON. +[dependencies] +anyhow = "1" +chrono = "0.4.44" +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "8fc74c709444b884a2bd339e0c306bf9484d99d2", package = "cultcache-rs" } +rmp-serde = "1" +schemars = "1" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs new file mode 100644 index 0000000..c29146e --- /dev/null +++ b/crates/eureka-pipeline/src/lib.rs @@ -0,0 +1,3187 @@ +//! 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 `::`, 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`, 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` has no `Bounded` impl of its own. +//! +//! 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 Bounded for Option { + fn validate(&self, field: &str) -> Result<(), PipelineRefusal> { + self.as_ref().map_or(Ok(()), |value| value.validate(field)) + } +} + +macro_rules! unbounded_scalars { + ($($ty:ty),* $(,)?) => {$( + impl Bounded for $ty { + fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> { + Ok(()) + } + } + )*}; +} + +unbounded_scalars!(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(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) -> 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) +} + +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); + /// Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`. + Para = 4000, |field, value| within(4000)(field, value); + /// 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(&self, state: &mut H) { + self.identity().hash(state); + } +} + +impl PartialOrd for OrgRepo { + fn partial_cmp(&self, other: &Self) -> Option { + 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])?),* $(,)? } $(=> $extra:path)?)*) => {$( + $(#[$attr])* + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] + pub struct $name { + $( + $(#[schemars(extend("maxItems" = $max))])? + pub $field: $ty + ),* + } + + impl Bounded for $name { + fn validate(&self, at: &str) -> Result<(), PipelineRefusal> { + $(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 Bounded for $name { + fn validate(&self, _field: &str) -> Result<(), PipelineRefusal> { + Ok(()) + } + } + )*}; +} + +/// RS-L closing fix 4: `campaign.repos` 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 `PipelineCampaign` alone, not a +/// per-type format rule any other `value_types!` struct shares, so it is +/// wired in as that struct's one `=> extra` hook rather than folded into +/// `Bounded for OrgRepo` or the list bound in `bounded_field!`. +fn campaign_repos_are_distinct(campaign: &PipelineCampaign, at: &str) -> Result<(), PipelineRefusal> { + let mut seen = std::collections::HashSet::new(); + for repo in &campaign.repos { + if !seen.insert(repo.identity()) { + return Err(PipelineRefusal::DuplicateRepo { + field: format!("{at}.repos"), + repo: repo.0.clone(), + }); + } + } + Ok(()) +} + +unit_enums! { + EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture } + ClaimOutcome { Holds, Falsified, Unproven } + FindingConfidence { Confirmed, Plausible } + FindingSeverity { Blocker, High, Medium, Low } + RulingAuthority { Operator, Standing, Defaulted } + FindingOrigin { Introduced, PreExisting } +} + +value_types! { + pub struct CodeLocation { path: Short, line: u32, end_line: Option } + 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 } + pub struct AuthorityMap { + owner: Line, inputs: Vec[16], outputs: Vec[16], derived_state: Vec[16], + forbidden_writers: Vec[16], shared_paths: Vec[16], deletion_line: Line, + } + pub struct VerificationTest { name: Short, pins: Line } + pub struct NegativeCheck { pattern: Short, scope: Line } + pub struct CutVerification { + builds: Vec[64], tests: Vec[64], + negative: Vec[64], operator: Vec[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[64], dependencies_removed: Vec[64], + formats_added: Vec[64], formats_removed: Vec[64], + targets_added: Vec[64], targets_removed: Vec[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[8], findings: Vec[16], + promise: Option