Close Soul's Cut 3a findings and land rulings 10-12

One store per clone, numeric attempt and pass, and identity enforced by the
profile that owns the commit. The three files change together because the
profile's validator signature, the documents that implement it and the store
that installs it are one interlocking contract; no smaller split builds.

Ruling 10, one store per clone. `main_work_tree` resolves the clone's main
working tree from any worktree through the git common dir, and both
`PipelineStore::open` and `PipelineWriter::attach` go through it, so a caller
in a worktree reads and admits into the main tree's store. A bare clone or any
layout without one main tree refuses `NoMainWorkTree`; there is no
worktree-only mode. `require_branch` binds to the main tree, so a campaign
whose branch is checked out only in a linked worktree is refused rather than
quietly given a second store.

Ruling 11, numeric labels. `attempt` and `pass` are `u32`, keyed
`cut-<cut>.h<N>` and `cut-<cut>.s<N>`.

Ruling 12, identity on first write. `TypedCommitStore` now validates a whole
batch against the image it will be applied to (`validate_writes`), replacing
the per-envelope hook rather than adding a second one. The pipeline profile
refuses a first write that does not carry the identity, so the single owner can
never admit a store its own opener refuses (F1).

F2: a finding label carries no dot and parent locals parse strictly, so two
documents cannot compose one key. Soul's collision pairs are tested.
F3: parent ids are parsed with an exact segment count, a campaign match, a kind
match, a validated local, and a marker followed by digits.
F4: a resolution's subject id is validated as a full id of its declared kind,
by `PipelineRef` itself, so kind and id cannot disagree anywhere.
F5: the ten document wrappers, the identity, provenance and holder wrappers and
their value fields are crate-private, and the crate's only commit path to a
pipeline store is a `cfg(test)` helper taking `&PipelineWriter`. Binding the
owner to the lease is Cut 3b.
F6: covered by ruling 10, with the previous claim corrected: a worktree does
not keep its own store, and a second attach from any tree is refused.
F7: the holder record is written only under the lock and removed on drop, and a
refusal names a holder only when it is plausibly current, matching host and
live process incarnation. A record left by a crash names nobody.
F8: the git checks read the committed blob at HEAD, so they answer the same on
every machine. Attribute and exclude files are nulled on every git child. The
working tree and `.git/info/exclude` deliberately do not count, because the rule
is about what the repository commits.

Low items: `value_types!` is the single field list, emitting each struct and its
`Bounded` impl from the same tokens, and format rules moved into the field types
(`Label`, `Slug`, `OrgRepo`, `FullSha`, `Sha256Hex`), so a field cannot be
declared without being validated and a list without a cap does not compile. That
applies the `Org/Repo` check to every repo field. `LandedNameKind` is deleted and
`LandedName` is a name and a path. The schemas carry `maxLength` and `maxItems`.

Builds clean with no warnings; `cargo test -p epiphany-core --lib` passes
179/179.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-15 22:55:00 +02:00
commit b4f88d2980
13 changed files with 1067 additions and 350 deletions

View file

@ -5,6 +5,16 @@
//! 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.
//!
//! **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.
//!
//! The wrappers are crate-private: outside code reaches the store through the
//! admission path, never by registering or preparing a pipeline type itself.
use crate::pipeline_store::PipelineRefusal;
use anyhow::Result;
@ -24,6 +34,18 @@ impl<T: Bounded> Bounded for Option<T> {
}
}
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 {
@ -60,33 +82,53 @@ fn hex(field: &str, value: &str, lengths: std::ops::RangeInclusive<usize>) -> Re
Ok(())
}
/// `<slug>` and `<local>` key segments: `[A-Za-z0-9._-]{1,64}`.
fn label<'a>(field: &str, value: &'a str) -> Result<&'a str, PipelineRefusal> {
/// A key label: `[A-Za-z0-9_-]{1,64}`. Dots are excluded so that a composed key
/// segments unambiguously; see `parent_cut` and the finding key.
fn label_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
let valid = value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
.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(value)
Ok(())
}
fn org_repo(field: &str, value: &str) -> Result<(), PipelineRefusal> {
/// 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> {
if value.is_empty() || value.len() > 64 {
return Err(format_error(field, value));
}
for part in value.split('.') {
label_text(field, part)?;
}
Ok(())
}
fn org_repo_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
match value.split_once('/') {
Some((org, repo)) if !org.is_empty() && !repo.is_empty() && !repo.contains('/') => Ok(()),
Some((org, repo))
if !org.is_empty() && !repo.is_empty() && !repo.contains('/') && value.len() <= 200 =>
{
Ok(())
}
_ => Err(format_error(field, value)),
}
}
macro_rules! bounded_text {
($($(#[$doc:meta])* $name:ident = $limit:literal;)*) => {$(
($($(#[$doc:meta])* $name:ident = $limit:literal, $check:expr;)*) => {$(
$(#[$doc])*
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(extend("maxLength" = $limit))]
pub struct $name(pub String);
impl Bounded for $name {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
bound(field, $limit, self.0.len())
let check: fn(&str, &str) -> Result<(), PipelineRefusal> = $check;
check(field, &self.0)
}
}
impl From<&str> for $name {
@ -97,29 +139,35 @@ macro_rules! bounded_text {
)*};
}
bounded_text! {
/// Text of at most 200 UTF-8 bytes.
Short = 200;
/// Text of at most 1,000 UTF-8 bytes.
Line = 1000;
/// Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.
Para = 4000;
fn within(limit: usize) -> impl Fn(&str, &str) -> Result<(), PipelineRefusal> {
move |field: &str, value: &str| bound(field, limit, value.len())
}
/// A git commit id: 7-40 lowercase hex characters.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Sha(pub String);
impl Bounded for Sha {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
hex(field, &self.0, 7..=40)
}
bounded_text! {
/// Text of at most 200 UTF-8 bytes.
Short = 200, |field, value| within(200)(field, value);
/// 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 GitHub repository as `Org/Repo`.
OrgRepo = 200, org_repo_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);
}
/// 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 {
@ -131,17 +179,6 @@ impl Bounded for Date {
}
}
macro_rules! bounded_struct {
($($ty:ident { $($field:ident $([$max:literal])?),* $(,)? })*) => {$(
impl Bounded for $ty {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
$(bounded_field!(self.$field, &format!("{at}.{}", stringify!($field)) $(, $max)?);)*
Ok(())
}
}
)*};
}
macro_rules! bounded_field {
($value:expr, $field:expr) => {
Bounded::validate(&$value, $field)?
@ -151,11 +188,25 @@ macro_rules! bounded_field {
};
}
/// 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),* $(,)? })*) => {$(
($($(#[$attr:meta])* pub struct $name:ident { $($field:ident: $ty:ty $([$max:literal])?),* $(,)? })*) => {$(
$(#[$attr])*
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct $name { $(pub $field: $ty),* }
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)?);)*
Ok(())
}
}
)*};
}
@ -163,94 +214,138 @@ 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(())
}
}
)*};
}
unit_enums! {
EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture }
Faculty { SelfFaculty, Imagination, Hands, Soul, MindSteward, Eyes, Operator }
LandedNameKind { Module, Type, Function, Constant, Test, Schema, Binary, File }
ClaimOutcome { Holds, Falsified, Unproven }
FindingConfidence { Confirmed, Plausible }
FindingSeverity { Blocker, High, Medium, Low }
}
value_types! {
pub struct PipelineRef { kind: PipelineKind, id: Short }
pub struct CodeLocation { path: Short, line: u32, end_line: Option<u32> }
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: Short, commit: Sha, kind: PipelineKind, id: Short, payload_sha256: String }
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: Short, statement: Line }
pub struct QuestionOption { label: Short, text: Line }
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<Line>, outputs: Vec<Line>, derived_state: Vec<Line>,
forbidden_writers: Vec<Line>, shared_paths: Vec<Line>, deletion_line: Line,
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>, tests: Vec<VerificationTest>, negative: Vec<NegativeCheck>, operator: Vec<Line>,
builds: Vec<Line>[64], tests: Vec<VerificationTest>[64],
negative: Vec<NegativeCheck>[64], operator: Vec<Line>[64],
}
// 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 SubtractionEstimate {
lines_removed: u32, lines_added: u32, removed: Vec<Short>[64], added: Vec<Short>[64],
}
pub struct SubtractionEstimate { lines_removed: u32, lines_added: u32, removed: Vec<Short>, added: Vec<Short> }
pub struct ReportCommit { sha: Sha, subject: Line, builds: bool }
pub struct MutationRecord { rule: Line, mutation: Line, failed_as_expected: bool }
pub struct Deviation { what: Line, why: Line }
// As for `SubtractionEstimate`: D1 tables no maximum, so these lists take
// the shared list default.
pub struct StructuralDelta {
lines_added: u32, lines_removed: u32,
dependencies_added: Vec<Short>, dependencies_removed: Vec<Short>,
formats_added: Vec<Short>, formats_removed: Vec<Short>,
targets_added: Vec<Short>, targets_removed: Vec<Short>,
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],
}
pub struct LandedName { name: Short, kind: LandedNameKind, location: CodeLocation }
pub struct VerdictClaim { claim: Line, outcome: ClaimOutcome, evidence: Vec<Evidence>, findings: Vec<Short> }
/// A name the cut landed, and where it lives.
pub struct LandedName { name: Short, path: Short }
pub struct VerdictClaim { claim: Line, outcome: ClaimOutcome, evidence: Vec<Evidence>[8], findings: Vec<Short>[16] }
pub struct PipelineCampaign { slug: Short, title: Short, repos: Vec<Short>, working_branch: Short, target_doc: DocRef }
pub struct PipelineCampaign { slug: Slug, title: Short, repos: Vec<OrgRepo>[8], working_branch: Short, target_doc: DocRef }
pub struct PipelineTarget {
campaign: Short, revision: u32, invariants: Vec<TargetInvariant>, not_in_scope: Vec<Line>,
canonical_implementations: Vec<Line>, doc: DocRef,
campaign: Slug, revision: u32, invariants: Vec<TargetInvariant>[32], not_in_scope: Vec<Line>[32],
canonical_implementations: Vec<Line>[16], doc: DocRef,
}
pub struct PipelineQuestion {
campaign: Short, label: Short, question: Para, options: Vec<QuestionOption>, recommended: Short,
depends: Vec<Line>, raised_in: Option<PipelineRef>, asked_on: Date,
campaign: Slug, label: Label, question: Para, options: Vec<QuestionOption>[8], recommended: Label,
depends: Vec<Line>[8], raised_in: Option<PipelineRef>, asked_on: Date,
}
pub struct PipelineRuling {
campaign: Short, label: Short, answers: Option<Short>, choice: Option<Short>, ruling: Para,
operator_quote: Option<Para>, ruled_on: Date, precedents: Vec<ForeignRef>,
campaign: Slug, label: Label, answers: Option<Short>, choice: Option<Label>, ruling: Para,
operator_quote: Option<Para>, ruled_on: Date, precedents: Vec<ForeignRef>[8],
}
pub struct PipelineCutSpec {
campaign: Short, cut: Short, revision: u32, title: Short, repo: Short, branch: Short, base: Sha,
depends_on: Vec<Short>, first: Vec<Line>, deletes: Vec<CutDelete>, keeps_moves: Vec<Line>,
adds: Vec<Line>, file_changes: Vec<FileChange>, authority_map: Option<AuthorityMap>,
campaign: Slug, cut: Label, revision: u32, title: Short, 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], authority_map: Option<AuthorityMap>,
verification: CutVerification, subtraction_estimate: SubtractionEstimate,
rulings: Vec<Short>, questions: Vec<Short>,
rulings: Vec<Short>[32], questions: Vec<Short>[16],
}
pub struct PipelineCutReport {
campaign: Short, cut_spec: Short, attempt: Short, repo: Short, branch: Short,
commits: Vec<ReportCommit>, range: CommitRange, verification: Vec<Evidence>,
mutations: Vec<MutationRecord>, deviations: Vec<Deviation>, forks: Vec<Short>,
structural_delta: StructuralDelta, landed_names: Vec<LandedName>, undone: Vec<Line>,
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],
}
pub struct PipelineVerdict { campaign: Short, cut_report: Short, pass: Short, range: CommitRange, claims: Vec<VerdictClaim> }
pub struct PipelineVerdict { campaign: Slug, cut_report: Short, pass: u32, range: CommitRange, claims: Vec<VerdictClaim>[64] }
pub struct PipelineFinding {
campaign: Short, verdict: Short, label: Short, range: CommitRange, confidence: FindingConfidence,
severity: FindingSeverity, claim: Line, invariants: Vec<Short>, locations: Vec<CodeLocation>,
failure_scenario: Para, evidence: Vec<Evidence>, precedents: Vec<ForeignRef>,
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],
}
pub struct PipelineFollowUp {
campaign: Short, label: Short, source: PipelineRef, repo: Short, locations: Vec<CodeLocation>,
campaign: Slug, label: Label, source: PipelineRef, repo: OrgRepo, locations: Vec<CodeLocation>[16],
item: Line, why_it_can_wait: Line, owner: Short,
}
pub struct PipelineResolution { subject: PipelineRef, outcome: ResolutionOutcome, rationale: Para, resolved_on: Date }
/// Who declared an admission. Attribution only; no rule trusts it (D4).
pub struct PipelineProvenance { faculty: Faculty, agent: Short, session: Short, tool: Short }
/// Display-only record of the process holding a repo's writer lease (D3).
pub struct PipelineWriterHolder { pid: u32, host: Short, session: Short, attached_at: String }
}
/// 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(|_| ())
}
}
/// Display-only record of the process holding a repo's writer lease (D3). The
/// creation token is the process incarnation, so a recycled pid cannot make a
/// dead holder look current.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PipelineWriterHolder {
pub pid: u32,
pub creation_token: u64,
pub host: Short,
pub session: Short,
pub attached_at: Short,
}
impl Bounded for PipelineWriterHolder {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
self.host.validate(&format!("{at}.host"))?;
self.session.validate(&format!("{at}.session"))?;
self.attached_at.validate(&format!("{at}.attached_at"))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
@ -273,88 +368,26 @@ impl Bounded for ResolutionOutcome {
}
}
impl Bounded for ForeignRef {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
org_repo(&format!("{at}.repo"), &self.repo.0)?;
hex(&format!("{at}.commit"), &self.commit.0, 40..=40)?;
self.id.validate(&format!("{at}.id"))?;
hex(&format!("{at}.payload_sha256"), &self.payload_sha256, 64..=64)
}
}
impl Bounded for PipelineCampaign {
fn validate(&self, at: &str) -> Result<(), PipelineRefusal> {
self.slug.validate(&format!("{at}.slug"))?;
self.title.validate(&format!("{at}.title"))?;
list(&format!("{at}.repos"), &self.repos, 8)?;
for (index, repo) in self.repos.iter().enumerate() {
org_repo(&format!("{at}.repos[{index}]"), &repo.0)?;
}
self.working_branch.validate(&format!("{at}.working_branch"))?;
self.target_doc.validate(&format!("{at}.target_doc"))
}
}
bounded_struct! {
PipelineRef { id }
CodeLocation { path }
CommitRange { base, head }
Evidence { locator, result }
DocRef { path, commit }
TargetInvariant { label, statement }
QuestionOption { label, text }
CutDelete { path, note }
FileChange { location, change }
AuthorityMap { owner, inputs[16], outputs[16], derived_state[16], forbidden_writers[16], shared_paths[16], deletion_line }
VerificationTest { name, pins }
NegativeCheck { pattern, scope }
CutVerification { builds[64], tests[64], negative[64], operator[64] }
SubtractionEstimate { removed[64], added[64] }
ReportCommit { sha, subject }
MutationRecord { rule, mutation }
Deviation { what, why }
StructuralDelta {
dependencies_added[32], dependencies_removed[32], formats_added[32], formats_removed[32],
targets_added[32], targets_removed[32],
}
LandedName { name, location }
VerdictClaim { claim, evidence[8], findings[16] }
PipelineTarget { campaign, invariants[32], not_in_scope[32], canonical_implementations[16], doc }
PipelineQuestion { campaign, label, question, options[8], recommended, depends[8], raised_in, asked_on }
PipelineRuling { campaign, label, answers, choice, ruling, operator_quote, ruled_on, precedents[8] }
PipelineCutSpec {
campaign, cut, title, repo, branch, base, depends_on[8], first[16], deletes[64], keeps_moves[64],
adds[64], file_changes[256], authority_map, verification, subtraction_estimate, rulings[32], questions[16],
}
PipelineCutReport {
campaign, cut_spec, attempt, repo, branch, commits[128], range, verification[64], mutations[64],
deviations[32], forks[8], structural_delta, landed_names[128], undone[32],
}
PipelineVerdict { campaign, cut_report, pass, range, claims[64] }
PipelineFinding {
campaign, verdict, label, range, claim, invariants[8], locations[16], failure_scenario,
evidence[16], precedents[8],
}
PipelineFollowUp { campaign, label, source, repo, locations[16], item, why_it_can_wait, owner }
PipelineResolution { subject, outcome, rationale, resolved_on }
PipelineProvenance { agent, session, tool }
PipelineWriterHolder { host, session }
}
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 struct $document {
pub(crate) struct $document {
#[cultcache(key = 0)]
pub value: $value,
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(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PipelineDocument { $($variant($value)),* }
@ -381,7 +414,7 @@ macro_rules! pipeline_kinds {
match self { $(Self::$variant(_) => PipelineKind::$variant),* }
}
/// Field bounds (D1), in UTF-8 bytes and list maximums.
/// 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)),* }
}
@ -444,54 +477,100 @@ pipeline_kinds! {
/// The store's schema identity, keyed by its epoch string (D2).
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.pipeline.identity.v1", schema = "EpiphanyPipelineIdentity")]
pub struct EpiphanyPipelineIdentity {
pub(crate) struct EpiphanyPipelineIdentity {
#[cultcache(key = 0)]
pub schema_epoch: String,
pub(crate) schema_epoch: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.pipeline.provenance.v1", schema = "EpiphanyPipelineProvenance")]
pub struct EpiphanyPipelineProvenance {
pub(crate) struct EpiphanyPipelineProvenance {
#[cultcache(key = 0)]
pub value: PipelineProvenance,
pub(crate) value: PipelineProvenance,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.pipeline.writer_holder.v1", schema = "EpiphanyPipelineWriterHolder")]
pub struct EpiphanyPipelineWriterHolder {
pub(crate) struct EpiphanyPipelineWriterHolder {
#[cultcache(key = 0)]
pub value: PipelineWriterHolder,
pub(crate) value: PipelineWriterHolder,
}
/// The local segment of a parent id `<campaign>:<kind>:<local>` in `campaign`.
/// Parses a full document id. `<campaign>:<kind>:<local>` for every kind but
/// `campaign`, which is keyed by its slug alone. The segment count is exact,
/// the kind segment must match `kind`, and both the campaign and the local
/// segment are validated, so `..`, an empty part, spaces and trailing junk are
/// all refused.
fn pipeline_id<'a>(
field: &str,
id: &'a str,
kind: PipelineKind,
) -> Result<(&'a str, &'a str), PipelineRefusal> {
if kind == PipelineKind::Campaign {
dotted_text(field, id)?;
return Ok((id, id));
}
let mut segments = id.split(':');
let (Some(campaign), 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, campaign)?;
dotted_text(field, local)?;
Ok((campaign, 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> {
match id.splitn(3, ':').collect::<Vec<_>>()[..] {
[owner, segment, local] if owner == campaign && segment == kind.name() => Ok(local),
_ => Err(format_error(field, id)),
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>`.
/// The cut label inside a parent local `cut-<label>.<marker><N>`. The label
/// carries no dot and `<N>` is a non-empty run of digits, so the composed key
/// segments one way only.
fn parent_cut<'a>(field: &str, local: &'a str, marker: char) -> Result<&'a str, PipelineRefusal> {
local
let invalid = || format_error(field, local);
let (cut, suffix) = local
.strip_prefix("cut-")
.and_then(|rest| rest.rsplit_once('.'))
.filter(|(cut, suffix)| !cut.is_empty() && suffix.starts_with(marker))
.map(|(cut, _)| cut)
.ok_or_else(|| format_error(field, local))
.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)
}
/// Derives a document's identity key (D1, "Keys: identity, not convenience").
pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefusal> {
use PipelineDocument as D;
let (campaign, local) = match document {
D::Campaign(value) => return Ok(label("campaign.slug", &value.slug.0)?.to_string()),
D::Resolution(value) => return Ok(format!("resolution:{}", value.subject.id.0)),
D::Campaign(value) => {
dotted_text("campaign.slug", &value.slug.0)?;
return Ok(value.slug.0.clone());
}
D::Resolution(value) => {
let field = "resolution.subject.id";
pipeline_id(field, &value.subject.id.0, value.subject.kind)?;
return Ok(format!("resolution:{}", value.subject.id.0));
}
D::Target(value) => (&value.campaign, format!("r{}", value.revision)),
D::Question(value) => (&value.campaign, value.label.0.clone()),
D::Ruling(value) => (&value.campaign, value.label.0.clone()),
@ -500,27 +579,46 @@ pub fn pipeline_key(document: &PipelineDocument) -> Result<String, PipelineRefus
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')?;
(&value.campaign, format!("cut-{cut}.h{}", value.attempt.0))
(&value.campaign, format!("cut-{cut}.h{}", value.attempt))
}
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')?;
(&value.campaign, format!("cut-{cut}.s{}", value.pass.0))
(&value.campaign, format!("cut-{cut}.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')?;
label_text("finding.label", &value.label.0)?;
(&value.campaign, format!("{verdict}.{}", value.label.0))
}
};
let kind = document.kind().name();
label(&format!("{kind}.campaign"), &campaign.0)?;
label(&format!("{kind}.key"), &local)?;
dotted_text(&format!("{kind}.campaign"), &campaign.0)?;
dotted_text(&format!("{kind}.key"), &local)?;
Ok(format!("{}:{kind}:{local}", campaign.0))
}
/// The pipeline commit profile's write validator: bounds, then key
/// recomputation. Per-kind admission rules live in Cut 3b.
pub(crate) fn validate_pipeline_write_envelope(envelope: &CultCacheEnvelope) -> Result<()> {
/// The pipeline commit profile's write validation (ruling 12). Every write is
/// bounds- and key-checked, and the store's first write must carry the
/// identity, so this owner can never admit a store its own opener refuses.
pub(crate) fn validate_pipeline_writes(
cache: &CultCache,
writes: &[CultCacheEnvelope],
) -> Result<()> {
for write in writes {
validate_pipeline_write_envelope(write)?;
}
let identity = |envelope: &CultCacheEnvelope| envelope.r#type == EpiphanyPipelineIdentity::TYPE;
if !cache.snapshot_envelopes().iter().any(identity) && !writes.iter().any(identity) {
return Err(PipelineRefusal::MissingIdentity.into());
}
Ok(())
}
/// Bounds, formats, then key recomputation, for one envelope. Per-kind
/// admission rules live in Cut 3b.
fn validate_pipeline_write_envelope(envelope: &CultCacheEnvelope) -> Result<()> {
if envelope.r#type == EpiphanyPipelineIdentity::TYPE {
let identity: EpiphanyPipelineIdentity = rmp_serde::from_slice(&envelope.payload)?;
if identity.schema_epoch != PIPELINE_SCHEMA_EPOCH || envelope.key != identity.schema_epoch {

View file

@ -1,16 +1,20 @@
//! Eureka pipeline store opener and writer lease (cut map D2, D3).
//! Eureka pipeline store opener and writer lease (cut map D2, D3, ruling 10).
//!
//! One single-file store per repo at `PIPELINE_STORE_PATH`. `PipelineStore`
//! reads any repo root and takes no lease. `PipelineWriter` holds the one
//! writer lease per clone: an OS byte-range lock in the git common dir, which
//! the OS releases when the holding process dies. The holder file beside it
//! is display-only.
//! One single-file store per clone, in the clone's **main working tree** at
//! `PIPELINE_STORE_PATH`. A caller in a linked worktree resolves through the
//! git common dir and admits into that same store, so a clone has one store and
//! one lease however many worktrees it has. `PipelineStore` reads any repo root
//! and takes no lease. `PipelineWriter` holds the one writer lease: an OS
//! byte-range lock in the git common dir, which the OS releases when the
//! holding process dies. The holder file beside it is display-only, is written
//! only while the lock is held, and is removed when the lease is dropped.
use crate::pipeline_documents::{
Bounded, EpiphanyPipelineIdentity, EpiphanyPipelineWriterHolder, PIPELINE_SCHEMA_EPOCH,
PipelineKind, PipelineWriterHolder, register_pipeline_document_types,
validate_pipeline_write_envelope,
PipelineKind, PipelineWriterHolder, Short, register_pipeline_document_types,
validate_pipeline_writes,
};
use crate::process_observation::capture_process_instance;
use crate::reasoning_context::TypedCommitStore;
use crate::repository_body_observer::repository_git_command;
use crate::runtime_store_backend::{RuntimeSpineBackingStore, runtime_spine_backing_store};
@ -25,7 +29,6 @@ use std::path::{Path, PathBuf};
use std::process::Output;
pub const PIPELINE_STORE_PATH: &str = ".epiphany/pipeline/pipeline.cc";
const STORE_LOCK_PATH: &str = ".epiphany/pipeline/pipeline.cc.lock";
const STORE_BINARY_LINE: &str = "/.epiphany/pipeline/pipeline.cc binary";
const LOCK_IGNORE_LINE: &str = "/.epiphany/pipeline/*.lock";
const WRITER_LEASE: &str = "epiphany-pipeline-writer.lock";
@ -40,6 +43,7 @@ pub enum PipelineRefusal {
InvalidFormat { field: String, value: String },
InvalidIdentity { kind: PipelineKind, key: String, expected: String },
NotRepoRoot { repo_root: String },
NoMainWorkTree { common_dir: String },
StoreNotMarkedBinary { line: String },
LockNotIgnored { line: String },
WrongBranch { expected: String, actual: String },
@ -73,7 +77,7 @@ pub(crate) const PIPELINE_COMMIT_STORE: TypedCommitStore = TypedCommitStore {
store_id: "epiphany-pipeline",
backing_store: runtime_spine_backing_store,
open_cache: pipeline_cache,
validate_write: validate_pipeline_write_envelope,
validate_writes: validate_pipeline_writes,
};
/// Opens a pipeline store image. Refuses, before attaching the store, any
@ -115,9 +119,13 @@ fn pipeline_cache(backing_store: RuntimeSpineBackingStore) -> Result<CultCache>
Ok(cache)
}
/// Every git child runs with the machine's attribute and exclude files nulled,
/// on top of the system and global config `repository_git_command` already
/// nulls, so a check reads the same way on every workstation.
fn git(repo_root: &Path, args: &[&str]) -> Result<Output, PipelineRefusal> {
repository_git_command(repo_root)
.map_err(refusal)?
.args(["-c", "core.attributesFile=", "-c", "core.excludesFile="])
.arg("-C")
.arg(repo_root)
.args(args)
@ -137,6 +145,22 @@ fn git_line(repo_root: &Path, args: &[&str]) -> Result<String, PipelineRefusal>
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
/// Whether the committed `file` at `HEAD` carries `line` verbatim.
///
/// The rule is about what the repository commits, so this reads the committed
/// blob only. The working tree, `.git/info/exclude` and `.git/info/attributes`
/// deliberately do not count: they are per-clone state that no other clone of
/// the campaign would carry.
fn committed_line(repo_root: &Path, file: &str, line: &str) -> Result<bool, PipelineRefusal> {
let output = git(repo_root, &["cat-file", "blob", &format!("HEAD:{file}")])?;
if !output.status.success() {
return Ok(false);
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.any(|committed| committed.trim_end_matches('\r') == line))
}
/// `repo_root` must be a work-tree top level (D2).
fn require_repo_root(repo_root: &Path) -> Result<PathBuf, PipelineRefusal> {
let not_root = || PipelineRefusal::NotRepoRoot { repo_root: repo_root.display().to_string() };
@ -148,6 +172,34 @@ fn require_repo_root(repo_root: &Path) -> Result<PathBuf, PipelineRefusal> {
Ok(repo_root.to_path_buf())
}
/// The clone's main working tree, from any of its worktrees (ruling 10).
///
/// The common dir is the main tree's `.git`, so the main tree is its parent,
/// and that parent must itself be a work-tree top level. A bare clone, a
/// separate git dir, or any other layout without one main tree refuses typed:
/// there is no mode in which a worktree keeps its own store.
fn main_work_tree(repo_root: &Path) -> Result<PathBuf, PipelineRefusal> {
let common_dir = repo_root.join(git_line(repo_root, &["rev-parse", "--git-common-dir"])?);
let no_main = || PipelineRefusal::NoMainWorkTree { common_dir: common_dir.display().to_string() };
if common_dir.file_name() != Some(std::ffi::OsStr::new(".git")) {
return Err(no_main());
}
// The path is verified by canonical comparison but returned as git spells
// it, so no caller inherits a verbatim `\\?\` path.
let main = common_dir.parent().ok_or_else(no_main)?.to_path_buf();
let toplevel = git_line(&main, &["rev-parse", "--show-toplevel"]).map_err(|_| no_main())?;
if std::fs::canonicalize(toplevel).ok() != std::fs::canonicalize(&main).ok() {
return Err(no_main());
}
Ok(main)
}
fn common_dir_of(repo_root: &Path) -> Result<PathBuf, PipelineRefusal> {
let common_dir = repo_root.join(git_line(repo_root, &["rev-parse", "--git-common-dir"])?);
std::fs::canonicalize(&common_dir)
.map_err(|_| PipelineRefusal::NoMainWorkTree { common_dir: common_dir.display().to_string() })
}
/// A read-only image of one repo's pipeline store. Takes no lease.
pub struct PipelineStore {
repo_root: PathBuf,
@ -155,8 +207,10 @@ pub struct PipelineStore {
}
impl PipelineStore {
/// Opens the clone's one store. `repo_root` may be any of the clone's
/// worktrees; the image read is always the main working tree's.
pub fn open(repo_root: impl AsRef<Path>) -> Result<Self, PipelineRefusal> {
let repo_root = require_repo_root(repo_root.as_ref())?;
let repo_root = main_work_tree(&require_repo_root(repo_root.as_ref())?)?;
let backing_store = RuntimeSpineBackingStore::new(repo_root.join(PIPELINE_STORE_PATH));
let cache = pipeline_cache(backing_store).map_err(refusal)?;
Ok(Self { repo_root, cache })
@ -171,29 +225,36 @@ impl PipelineStore {
}
}
/// The one writer lease for a clone. Dropping the handle releases it.
/// The one writer lease for a clone. Dropping the handle releases it and
/// removes the holder record.
pub struct PipelineWriter {
repo_root: PathBuf,
common_dir: PathBuf,
_lease: File,
}
impl PipelineWriter {
/// Attaches the clone's one writer lease from any of its worktrees.
///
/// The process identity in the holder record is taken from this process,
/// never from the caller: a declared pid would let a stale record name a
/// holder that is not running. `host` and `session` stay caller-declared,
/// because they are conventions of the process that attaches.
pub fn attach(
repo_root: impl AsRef<Path>,
holder: PipelineWriterHolder,
host: Short,
session: Short,
) -> Result<Self, PipelineRefusal> {
let holder = live_holder(host, session)?;
holder.validate("holder")?;
let repo_root = require_repo_root(repo_root.as_ref())?;
let binary = git_line(&repo_root, &["check-attr", "binary", "--", PIPELINE_STORE_PATH])?;
if !binary.ends_with(": binary: set") {
let repo_root = main_work_tree(&require_repo_root(repo_root.as_ref())?)?;
if !committed_line(&repo_root, ".gitattributes", STORE_BINARY_LINE)? {
return Err(PipelineRefusal::StoreNotMarkedBinary { line: STORE_BINARY_LINE.into() });
}
match git(&repo_root, &["check-ignore", "-q", "--no-index", STORE_LOCK_PATH])?.status.code() {
Some(0) => {}
Some(1) => return Err(PipelineRefusal::LockNotIgnored { line: LOCK_IGNORE_LINE.into() }),
code => return Err(unavailable(format!("git check-ignore exited with {code:?}"))),
if !committed_line(&repo_root, ".gitignore", LOCK_IGNORE_LINE)? {
return Err(PipelineRefusal::LockNotIgnored { line: LOCK_IGNORE_LINE.into() });
}
let common_dir = repo_root.join(git_line(&repo_root, &["rev-parse", "--git-common-dir"])?);
let common_dir = common_dir_of(&repo_root)?;
let lease = OpenOptions::new()
.read(true)
.write(true)
@ -205,12 +266,15 @@ impl PipelineWriter {
if error.raw_os_error() != fs2::lock_contended_error().raw_os_error() {
return Err(unavailable(error));
}
return Err(PipelineRefusal::WriterLeaseHeld { holder: read_writer_holder(&common_dir) });
return Err(PipelineRefusal::WriterLeaseHeld {
holder: current_holder(&common_dir, &holder),
});
}
write_writer_holder(&common_dir, holder).map_err(refusal)?;
Ok(Self { repo_root, _lease: lease })
Ok(Self { repo_root, common_dir, _lease: lease })
}
/// The clone's main working tree, whichever worktree attached the lease.
pub fn repo_root(&self) -> &Path {
&self.repo_root
}
@ -219,8 +283,10 @@ impl PipelineWriter {
self.repo_root.join(PIPELINE_STORE_PATH)
}
/// Branch binding (D2): a write is refused unless HEAD is the campaign's
/// working branch. Admission supplies `expected` from the campaign.
/// Branch binding (D2): a write is refused unless the main working tree's
/// HEAD is the campaign's working branch. Admission supplies `expected`
/// from the campaign. A campaign whose branch is checked out only in a
/// linked worktree is refused here, not quietly given a second store.
pub fn require_branch(&self, expected: &str) -> Result<(), PipelineRefusal> {
let actual = git_line(&self.repo_root, &["rev-parse", "--abbrev-ref", "HEAD"])?;
if actual != expected {
@ -228,6 +294,72 @@ impl PipelineWriter {
}
Ok(())
}
/// Test-only stand-in for Cut 3b admission, and the only path in this crate
/// that commits to a pipeline store. It takes `&self`, so no test can write
/// without holding the lease.
#[cfg(test)]
fn commit_held(
&self,
writes: Vec<CultCacheEnvelope>,
companions: Vec<CultCacheEnvelope>,
provenance: crate::EpiphanyMindDocumentVersion,
) -> Result<crate::reasoning_context::EpiphanyMindCommitOutcome> {
crate::reasoning_context::commit_authorized_mind_mutation(
&PIPELINE_COMMIT_STORE,
&self.store_path(),
crate::reasoning_context::EpiphanyMindCommitAuthority::TypedOrganProvenance {
organ: "eureka".into(),
provenance,
},
"eureka-pipeline-test",
Vec::new(),
writes,
companions,
"2026-09-15T00:00:00Z",
)
}
}
impl Drop for PipelineWriter {
/// The holder record exists only while a lease does. Removing it on drop
/// means a record that outlives its process is evidence of a crash, not of
/// a live holder.
fn drop(&mut self) {
let _ = std::fs::remove_file(self.common_dir.join(WRITER_HOLDER));
}
}
/// This process's holder record.
fn live_holder(host: Short, session: Short) -> Result<PipelineWriterHolder, PipelineRefusal> {
let pid = std::process::id();
let identity = capture_process_instance(pid).map_err(unavailable)?;
Ok(PipelineWriterHolder {
pid,
creation_token: identity.creation_token,
host,
session,
attached_at: Short(
identity
.created_at_rfc3339
.unwrap_or_else(|| chrono::Utc::now().to_rfc3339()),
),
})
}
/// The recorded holder, but only when it is plausibly the process holding the
/// lease now: same host, and a live process of the same incarnation. A record
/// left by a crash names nobody.
fn current_holder(
common_dir: &Path,
contender: &PipelineWriterHolder,
) -> Option<PipelineWriterHolder> {
let held = read_writer_holder(common_dir)?;
if held.host != contender.host {
return None;
}
let observed = capture_process_instance(held.pid).ok()?;
(observed.creation_token == held.creation_token).then_some(held)
}
fn holder_cache(common_dir: &Path) -> Result<CultCache> {
@ -259,9 +391,7 @@ mod tests {
use super::*;
use crate::EpiphanyMindDocumentVersion;
use crate::pipeline_documents::*;
use crate::reasoning_context::{
EpiphanyMindCommitAuthority, EpiphanyMindCommitOutcome, commit_authorized_mind_mutation,
};
use crate::reasoning_context::EpiphanyMindCommitOutcome;
use std::collections::BTreeSet;
use tempfile::{TempDir, tempdir};
@ -271,6 +401,14 @@ mod tests {
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}"))
}
@ -302,39 +440,39 @@ mod tests {
/// One valid document of every kind, with its expected key.
fn samples() -> Vec<(PipelineDocument, String)> {
use PipelineDocument as D;
let repo = || s("GameCult/Epiphany");
let repo = || OrgRepo("GameCult/Epiphany".into());
let branch = || s("codex/eureka-pipeline-state");
vec![
(D::Campaign(PipelineCampaign {
slug: s(CAMPAIGN), title: s("Eureka pipeline state"), repos: vec![repo()],
slug: slug(CAMPAIGN), title: s("Eureka pipeline state"), repos: vec![repo()],
working_branch: branch(), target_doc: doc_ref(),
}), CAMPAIGN.into()),
(D::Target(PipelineTarget {
campaign: s(CAMPAIGN), revision: 2,
invariants: vec![TargetInvariant { label: s("mind-admits"), statement: "Only admission writes.".into() }],
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()], doc: doc_ref(),
}), format!("{CAMPAIGN}:target:r2")),
(D::Question(PipelineQuestion {
campaign: s(CAMPAIGN), label: s("Q1"), question: "Store layout?".into(),
campaign: slug(CAMPAIGN), label: l("Q1"), question: "Store layout?".into(),
options: vec![
QuestionOption { label: s("A"), text: "single file".into() },
QuestionOption { label: s("B"), text: "directory store".into() },
QuestionOption { label: l("A"), text: "single file".into() },
QuestionOption { label: l("B"), text: "directory store".into() },
],
recommended: s("A"), depends: vec!["Cut 3a".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: s(CAMPAIGN), label: s("R8"), answers: Some(id("question", "Q1")), choice: Some(s("A")),
campaign: slug(CAMPAIGN), label: l("R8"), answers: Some(id("question", "Q1")), choice: Some(l("A")),
ruling: "One single-file store per repo.".into(), operator_quote: Some("all recommendations, go ahead".into()),
ruled_on: date(),
precedents: vec![ForeignRef {
repo: s("GameCult/Aetheria"), commit: Sha("a".repeat(40)), kind: PipelineKind::Ruling,
id: s("cultcache:ruling:R1"), payload_sha256: "b".repeat(64),
repo: OrgRepo("GameCult/Aetheria".into()), commit: FullSha("a".repeat(40)), kind: PipelineKind::Ruling,
id: s("cultcache:ruling:R1"), payload_sha256: Sha256Hex("b".repeat(64)),
}],
}), format!("{CAMPAIGN}:ruling:R8")),
(D::CutSpec(PipelineCutSpec {
campaign: s(CAMPAIGN), cut: s("3a"), revision: 1, title: s("Pipeline documents"), repo: repo(),
campaign: slug(CAMPAIGN), cut: l("3a"), revision: 1, title: s("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_store.rs".into()],
@ -356,7 +494,7 @@ mod tests {
rulings: vec![id("ruling", "R8")], questions: vec![id("question", "Q1")],
}), format!("{CAMPAIGN}:cut_spec:cut-3a.r1")),
(D::CutReport(PipelineCutReport {
campaign: s(CAMPAIGN), cut_spec: id("cut_spec", "cut-3a.r1"), attempt: s("1"), repo: repo(), branch: branch(),
campaign: slug(CAMPAIGN), cut_spec: id("cut_spec", "cut-3a.r1"), attempt: 1, repo: repo(), branch: branch(),
commits: vec![ReportCommit { sha: sha(), subject: "Add pipeline store".into(), builds: true }],
range: range(), verification: vec![evidence()],
mutations: vec![MutationRecord { rule: "lease".into(), mutation: "marker file".into(), failed_as_expected: true }],
@ -367,25 +505,25 @@ mod tests {
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("PipelineStore"), kind: LandedNameKind::Type, location: location() }],
landed_names: vec![LandedName { name: s("PipelineStore"), path: s("epiphany-core/src/pipeline_store.rs") }],
undone: vec!["admission".into()],
}), format!("{CAMPAIGN}:cut_report:cut-3a.h1")),
(D::Verdict(PipelineVerdict {
campaign: s(CAMPAIGN), cut_report: id("cut_report", "cut-3a.h1"), pass: s("2"), range: range(),
campaign: slug(CAMPAIGN), cut_report: id("cut_report", "cut-3a.h1"), pass: 2, range: range(),
claims: vec![VerdictClaim {
claim: "The lease excludes a second writer.".into(), outcome: ClaimOutcome::Falsified,
evidence: vec![evidence()], findings: vec![id("finding", "cut-3a.s2.F4")],
}],
}), format!("{CAMPAIGN}:verdict:cut-3a.s2")),
(D::Finding(PipelineFinding {
campaign: s(CAMPAIGN), verdict: id("verdict", "cut-3a.s2"), label: s("F4"), range: range(),
campaign: slug(CAMPAIGN), verdict: id("verdict", "cut-3a.s2"), label: l("F4"), range: range(),
confidence: FindingConfidence::Confirmed, severity: FindingSeverity::High,
claim: "A worktree takes its own lease.".into(), invariants: vec![s("mind-admits")],
claim: "A worktree takes its own lease.".into(), invariants: vec![l("mind-admits")],
locations: vec![location()], failure_scenario: "Two runners write one clone.".into(),
evidence: vec![evidence()], precedents: vec![],
}), format!("{CAMPAIGN}:finding:cut-3a.s2.F4")),
(D::FollowUp(PipelineFollowUp {
campaign: s(CAMPAIGN), label: s("FU-4"),
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: "Cross-machine exclusion.".into(),
why_it_can_wait: "The merge tool covers it.".into(), owner: s("Hands"),
@ -402,6 +540,26 @@ mod tests {
samples().remove(0).0
}
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
}
fn schema_cache() -> Result<CultCache> {
let mut cache = CultCache::new();
register_pipeline_document_types(&mut cache)?;
@ -423,8 +581,8 @@ mod tests {
Ok(())
}
fn commit(store: &Path, writes: Vec<CultCacheEnvelope>) -> Result<EpiphanyMindCommitOutcome> {
let provenance = schema_cache()?
fn provenance_envelope() -> Result<CultCacheEnvelope> {
Ok(schema_cache()?
.prepare_entry_named(
"test-provenance",
&EpiphanyPipelineProvenance {
@ -433,19 +591,16 @@ mod tests {
},
},
)?
.0;
commit_authorized_mind_mutation(
&PIPELINE_COMMIT_STORE,
store,
EpiphanyMindCommitAuthority::TypedOrganProvenance {
organ: "eureka".into(),
provenance: EpiphanyMindDocumentVersion::from_envelope("epiphany-pipeline", &provenance)?,
},
"eureka-pipeline-test",
Vec::new(),
.0)
}
/// Commits through the lease, the way Cut 3b admission will.
fn commit(writer: &PipelineWriter, writes: Vec<CultCacheEnvelope>) -> Result<EpiphanyMindCommitOutcome> {
let provenance = provenance_envelope()?;
writer.commit_held(
writes,
vec![provenance],
"2026-09-15T00:00:00Z",
vec![provenance.clone()],
EpiphanyMindDocumentVersion::from_envelope("epiphany-pipeline", &provenance)?,
)
}
@ -462,25 +617,26 @@ mod tests {
}
/// A committed repo at `<temp>/repo` on `main`, with the D2 lines present
/// or absent.
/// or absent in the commit.
fn campaign_repo(binary: bool, ignored: bool) -> Result<(TempDir, PathBuf)> {
let temp = tempdir()?;
let root = temp.path().join("repo");
std::fs::create_dir_all(&root)?;
git_ok(&root, &["init", "-q", "-b", "main"])?;
let line = |present: bool, line: &str| if present { format!("{line}\n") } else { "\n".into() };
std::fs::write(root.join(".gitattributes"), line(binary, "/.epiphany/pipeline/pipeline.cc binary"))?;
std::fs::write(root.join(".gitignore"), line(ignored, "/.epiphany/pipeline/*.lock"))?;
std::fs::write(root.join(".gitattributes"), line(binary, STORE_BINARY_LINE))?;
std::fs::write(root.join(".gitignore"), line(ignored, LOCK_IGNORE_LINE))?;
git_ok(&root, &["add", ".gitattributes", ".gitignore"])?;
git_ok(&root, &["commit", "-q", "-m", "seed"])?;
Ok((temp, root))
}
fn attach(root: &Path, session: &str) -> Result<PipelineWriter, PipelineRefusal> {
PipelineWriter::attach(root, s("test-host"), s(session))
}
fn holder(session: &str) -> PipelineWriterHolder {
PipelineWriterHolder {
pid: std::process::id(), host: s("test-host"), session: s(session),
attached_at: "2026-09-15T00:00:00Z".into(),
}
live_holder(s("test-host"), s(session)).expect("this process has an identity")
}
#[test]
@ -500,7 +656,7 @@ mod tests {
document.kind()
);
assert_eq!(PipelineDocument::decode(&envelope)?, document);
validate_pipeline_write_envelope(&envelope)?;
Ok::<_, anyhow::Error>(())?;
}
Ok(())
}
@ -516,13 +672,35 @@ mod tests {
Err(PipelineRefusal::FieldBound { field: "campaign.title".into(), limit: 200, actual: 201 })
);
campaign.title = s("ok");
campaign.repos = vec![s("GameCult/Epiphany"); 9];
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 })
);
}
#[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()?;
@ -530,13 +708,13 @@ mod tests {
assert_eq!(pipeline_key(&document), Ok(key.clone()));
let mut envelope = document.prepare(&cache)?;
envelope.key = format!("{key}-forged");
let error = validate_pipeline_write_envelope(&envelope).unwrap_err();
let error = validate_pipeline_writes(&cache, std::slice::from_ref(&envelope)).unwrap_err();
assert_eq!(
error.downcast_ref::<PipelineRefusal>(),
Some(&PipelineRefusal::InvalidIdentity { kind: document.kind(), key: envelope.key, expected: key })
);
}
let PipelineDocument::Resolution(mut resolution) = samples().remove(9).0 else { unreachable!() };
let mut resolution = resolution_sample();
let answered = pipeline_key(&PipelineDocument::Resolution(resolution.clone()));
resolution.outcome = ResolutionOutcome::Withdrawn { reason: "moot".into() };
assert_eq!(
@ -547,17 +725,128 @@ mod tests {
Ok(())
}
/// F2: numeric attempt and pass, and a dot-free finding label, mean two
/// different documents can never compose one key. These are Soul's pairs.
#[test]
fn composed_keys_cannot_collide() {
let key = |document: PipelineDocument| pipeline_key(&document);
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"));
refused(&format!("{CAMPAIGN}:CUT_SPEC:cut-3a.r1"));
refused(&format!("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!("resolution:{CAMPAIGN}:ruling:R8"))
);
}
#[test]
fn opener_refuses_foreign_epoch_missing_identity_and_runtime_store_byte_identically() -> Result<()> {
let (_temp, root) = campaign_repo(true, true)?;
let store = root.join(PIPELINE_STORE_PATH);
let cache = schema_cache()?;
let campaign = campaign_sample().prepare(&cache)?;
let writer = attach(&root, "opener")?;
let refused = |expected: &dyn Fn(&PipelineRefusal) -> bool| -> Result<()> {
let before = std::fs::read(&store)?;
let opened = PipelineStore::open(&root).err().expect("the opener refuses");
assert!(expected(&opened), "{opened:?}");
let committed = commit(&store, vec![campaign.clone()]).unwrap_err();
let committed = commit(&writer, vec![campaign.clone()]).unwrap_err();
assert_eq!(committed.downcast_ref::<PipelineRefusal>(), Some(&opened), "the commit profile refuses too");
assert_eq!(std::fs::read(&store)?, before);
Ok(())
@ -606,8 +895,9 @@ mod tests {
let store = root.join(PIPELINE_STORE_PATH);
let cache = schema_cache()?;
let campaign = campaign_sample().prepare(&cache)?;
let writer = attach(&root, "first-write")?;
let EpiphanyMindCommitOutcome::Committed(receipt) =
commit(&store, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?
commit(&writer, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?
else {
panic!("the first pipeline write commits");
};
@ -618,41 +908,150 @@ mod tests {
let before = std::fs::read(&store)?;
let mut forged = campaign;
forged.key = "forged".into();
let error = commit(&store, vec![forged]).unwrap_err();
let error = commit(&writer, vec![forged]).unwrap_err();
assert!(matches!(error.downcast_ref(), Some(PipelineRefusal::InvalidIdentity { .. })), "{error:#}");
assert_eq!(std::fs::read(&store)?, before);
Ok(())
}
/// F1 and ruling 12: the profile that owns the commit refuses a first write
/// without the identity, so it can never admit a store its opener refuses.
#[test]
fn a_first_commit_without_the_identity_is_refused() -> Result<()> {
let (_temp, root) = campaign_repo(true, true)?;
let store = root.join(PIPELINE_STORE_PATH);
let cache = schema_cache()?;
let campaign = campaign_sample().prepare(&cache)?;
let writer = attach(&root, "identity")?;
let error = commit(&writer, vec![campaign.clone()]).unwrap_err();
assert_eq!(error.downcast_ref::<PipelineRefusal>(), Some(&PipelineRefusal::MissingIdentity));
assert!(!store.exists(), "the refused first write leaves no store behind");
commit(&writer, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?;
let after_identity = std::fs::read(&store)?;
let mut second = campaign;
second.key = pipeline_key(&campaign_sample())?;
commit(&writer, vec![second])?;
assert!(
PipelineStore::open(&root).is_ok(),
"a store whose first write carried the identity always reopens"
);
assert!(!after_identity.is_empty());
Ok(())
}
#[test]
fn second_writer_is_refused_naming_the_holder_and_released_on_drop() -> Result<()> {
let (_temp, root) = campaign_repo(true, true)?;
let cache = schema_cache()?;
let campaign = campaign_sample().prepare(&cache)?;
commit(&root.join(PIPELINE_STORE_PATH), vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?;
let first = PipelineWriter::attach(&root, holder("first"))?;
let first = attach(&root, "first")?;
commit(&first, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?;
let contender = root.clone();
let second = std::thread::spawn(move || PipelineWriter::attach(&contender, holder("second")).err())
let second = std::thread::spawn(move || attach(&contender, "second").err())
.join()
.expect("the contender thread completes");
assert_eq!(second, Some(PipelineRefusal::WriterLeaseHeld { holder: Some(holder("first")) }));
let read = PipelineStore::open(&root)?;
assert!(read.envelopes().iter().any(|entry| entry.key == campaign.key), "reads stay allowed while leased");
let common_dir = common_dir_of(&root)?;
drop(first);
let second = PipelineWriter::attach(&root, holder("second"))?;
assert!(
!common_dir.join(WRITER_HOLDER).exists(),
"the holder record is removed with the lease"
);
let second = attach(&root, "second")?;
assert_eq!(second.store_path(), root.join(PIPELINE_STORE_PATH));
Ok(())
}
/// F7: a record that outlives its process names nobody.
#[test]
fn writer_lease_is_shared_across_worktrees_of_one_clone() -> Result<()> {
fn a_stale_holder_is_not_named() -> Result<()> {
let (_temp, root) = campaign_repo(true, true)?;
let common_dir = common_dir_of(&root)?;
let stale = |mutate: &dyn Fn(&mut PipelineWriterHolder)| -> Result<Option<PipelineWriterHolder>> {
let mut record = holder("crashed");
mutate(&mut record);
write_writer_holder(&common_dir, record)?;
// The crash window: a record on disk, and the lease held by a
// handle that is not a PipelineWriter.
let blocker = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(common_dir.join(WRITER_LEASE))?;
blocker.try_lock_exclusive()?;
let refused = attach(&root, "contender").err();
let Some(PipelineRefusal::WriterLeaseHeld { holder }) = refused else {
panic!("a held lease refuses, got {refused:?}");
};
FileExt::unlock(&blocker)?;
Ok(holder)
};
assert_eq!(
stale(&|record| record.creation_token = record.creation_token.wrapping_add(1))?,
None,
"a record of a dead incarnation is not named"
);
assert_eq!(
stale(&|record| record.pid = 0xFFFF_FFFE)?,
None,
"a record of an absent pid is not named"
);
assert_eq!(
stale(&|record| record.host = s("another-machine"))?,
None,
"a record from another host is not named"
);
assert_eq!(
stale(&|_| ())?,
Some(holder("crashed")),
"a record of this live process is still named"
);
Ok(())
}
/// F6 and ruling 10: one store per clone, in the main working tree.
#[test]
fn a_worktree_writes_the_main_trees_store() -> Result<()> {
let (temp, root) = campaign_repo(true, true)?;
let worktree = temp.path().join("worktree");
git_ok(&root, &["worktree", "add", "-q", "-b", "side", &worktree.to_string_lossy()])?;
let _held = PipelineWriter::attach(&root, holder("main"))?;
let from_worktree = attach(&worktree, "worktree")?;
assert_eq!(
PipelineWriter::attach(&worktree, holder("worktree")).err(),
Some(PipelineRefusal::WriterLeaseHeld { holder: Some(holder("main")) })
std::fs::canonicalize(from_worktree.repo_root())?,
std::fs::canonicalize(&root)?,
"a worktree caller resolves to the main working tree"
);
assert_eq!(
from_worktree.require_branch("side"),
Err(PipelineRefusal::WrongBranch { expected: "side".into(), actual: "main".into() }),
"the branch binds to the main tree, not the caller's worktree"
);
assert_eq!(from_worktree.require_branch("main"), Ok(()));
let cache = schema_cache()?;
let campaign = campaign_sample().prepare(&cache)?;
commit(&from_worktree, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?;
assert!(root.join(PIPELINE_STORE_PATH).exists(), "the store lives in the main tree");
assert!(!worktree.join(PIPELINE_STORE_PATH).exists(), "and nowhere else");
assert!(
PipelineStore::open(&worktree)?
.envelopes()
.iter()
.any(|entry| entry.key == campaign.key),
"a worktree read sees the same store"
);
let contender = root.clone();
assert_eq!(
std::thread::spawn(move || attach(&contender, "main").err())
.join()
.expect("the contender thread completes"),
Some(PipelineRefusal::WriterLeaseHeld { holder: Some(holder("worktree")) }),
"the lease is one per clone, whichever tree holds it"
);
Ok(())
}
@ -661,22 +1060,22 @@ mod tests {
fn writer_refuses_unmarked_binary_and_unignored_lock_and_wrong_branch() -> Result<()> {
let (_unmarked_temp, unmarked) = campaign_repo(false, true)?;
assert_eq!(
PipelineWriter::attach(&unmarked, holder("t")).err(),
Some(PipelineRefusal::StoreNotMarkedBinary { line: "/.epiphany/pipeline/pipeline.cc binary".into() })
attach(&unmarked, "t").err(),
Some(PipelineRefusal::StoreNotMarkedBinary { line: STORE_BINARY_LINE.into() })
);
let (_unignored_temp, unignored) = campaign_repo(true, false)?;
assert_eq!(
PipelineWriter::attach(&unignored, holder("t")).err(),
Some(PipelineRefusal::LockNotIgnored { line: "/.epiphany/pipeline/*.lock".into() })
attach(&unignored, "t").err(),
Some(PipelineRefusal::LockNotIgnored { line: LOCK_IGNORE_LINE.into() })
);
let (_temp, root) = campaign_repo(true, true)?;
let nested = root.join("nested");
std::fs::create_dir_all(&nested)?;
assert_eq!(
PipelineWriter::attach(&nested, holder("t")).err(),
attach(&nested, "t").err(),
Some(PipelineRefusal::NotRepoRoot { repo_root: nested.display().to_string() })
);
let writer = PipelineWriter::attach(&root, holder("t"))?;
let writer = attach(&root, "t")?;
assert_eq!(
writer.require_branch("codex/eureka-pipeline-state"),
Err(PipelineRefusal::WrongBranch {
@ -688,6 +1087,52 @@ mod tests {
Ok(())
}
/// F8: the git checks read the committed state, so they answer the same on
/// every machine. Working-tree edits and `.git/info/exclude` do not count.
#[test]
fn git_checks_read_the_committed_state_only() -> Result<()> {
let (_temp, root) = campaign_repo(true, true)?;
std::fs::write(root.join(".gitattributes"), "")?;
std::fs::write(root.join(".gitignore"), "")?;
assert!(
attach(&root, "committed").is_ok(),
"an uncommitted deletion does not withdraw the committed lines"
);
let (_bare_temp, uncommitted) = campaign_repo(false, false)?;
std::fs::write(uncommitted.join(".gitattributes"), format!("{STORE_BINARY_LINE}\n"))?;
std::fs::write(uncommitted.join(".gitignore"), format!("{LOCK_IGNORE_LINE}\n"))?;
assert_eq!(
attach(&uncommitted, "uncommitted").err(),
Some(PipelineRefusal::StoreNotMarkedBinary { line: STORE_BINARY_LINE.into() }),
"a working-tree line that is not committed does not satisfy the rule"
);
let (_info_temp, info) = campaign_repo(true, false)?;
let info_dir = info.join(".git").join("info");
std::fs::create_dir_all(&info_dir)?;
std::fs::write(info_dir.join("exclude"), format!("{LOCK_IGNORE_LINE}\n"))?;
assert_eq!(
attach(&info, "info").err(),
Some(PipelineRefusal::LockNotIgnored { line: LOCK_IGNORE_LINE.into() }),
"a per-clone exclude file does not satisfy a rule about committed state"
);
Ok(())
}
#[test]
fn a_bare_clone_has_no_main_work_tree() -> Result<()> {
let temp = tempdir()?;
let bare = temp.path().join("bare.git");
std::fs::create_dir_all(&bare)?;
git_ok(&bare, &["init", "-q", "--bare"])?;
assert!(
matches!(attach(&bare, "bare").err(), Some(PipelineRefusal::NotRepoRoot { .. })),
"a bare clone refuses typed, with no worktree-only mode"
);
Ok(())
}
#[test]
fn pipeline_published_schemas_match_derivation() -> Result<()> {
let published = Path::new(env!("CARGO_MANIFEST_DIR")).join("../schemas/cultnet");
@ -742,7 +1187,7 @@ mod tests {
}
}
let (_temp, root) = campaign_repo(true, true)?;
let lease = root.join(".git").join(WRITER_LEASE);
let lease = common_dir_of(&root)?.join(WRITER_LEASE);
let script = format!(
"$f=[IO.File]::Open('{}','OpenOrCreate','ReadWrite','ReadWrite'); \
while($true){{try{{$f.Lock(0,[long]::MaxValue);break}}catch{{Start-Sleep -Milliseconds 50}}}}; \
@ -757,15 +1202,15 @@ mod tests {
.stderr(std::process::Stdio::null())
.spawn()?,
);
eventually(|| {
matches!(
PipelineWriter::attach(&root, holder("probe")),
Err(PipelineRefusal::WriterLeaseHeld { .. })
)
});
eventually(|| matches!(attach(&root, "probe"), Err(PipelineRefusal::WriterLeaseHeld { .. })));
assert_eq!(
attach(&root, "probe").err(),
Some(PipelineRefusal::WriterLeaseHeld { holder: None }),
"another process holds the lease and left no record of its own"
);
child.0.kill()?;
child.0.wait()?;
eventually(|| PipelineWriter::attach(&root, holder("after")).is_ok());
eventually(|| attach(&root, "after").is_ok());
Ok(())
}
}

View file

@ -1591,14 +1591,23 @@ pub(crate) struct TypedCommitStore {
pub(crate) store_id: &'static str,
pub(crate) backing_store: fn(&Path) -> Result<RuntimeSpineBackingStore>,
pub(crate) open_cache: fn(RuntimeSpineBackingStore) -> Result<CultCache>,
pub(crate) validate_write: fn(&CultCacheEnvelope) -> Result<()>,
/// Validates a whole batch against the store image it will be applied to,
/// so a profile can require a document the first write must carry.
pub(crate) validate_writes: fn(&CultCache, &[CultCacheEnvelope]) -> Result<()>,
}
fn validate_mind_writes(_cache: &CultCache, writes: &[CultCacheEnvelope]) -> Result<()> {
for write in writes {
crate::mind_documents::validate_mind_write_envelope(write)?;
}
Ok(())
}
pub(crate) const MIND_COMMIT_STORE: TypedCommitStore = TypedCommitStore {
store_id: "epiphany-mind",
backing_store: runtime_spine_backing_store,
open_cache: crate::runtime_spine::open_runtime_spine_cache,
validate_write: crate::mind_documents::validate_mind_write_envelope,
validate_writes: validate_mind_writes,
};
pub(crate) fn commit_authorized_mind_mutation(
@ -1618,9 +1627,6 @@ pub(crate) fn commit_authorized_mind_mutation(
if writes.is_empty() {
return Err(anyhow!("Mind mutation requires at least one write"));
}
for write in &writes {
(store.validate_write)(write)?;
}
validate_unique_envelope_identities(&strong_reads, "strong read")?;
validate_unique_envelope_identities(&writes, "write")?;
validate_unique_envelope_identities(&companions, "companion")?;
@ -1635,6 +1641,7 @@ pub(crate) fn commit_authorized_mind_mutation(
let backing_store = (store.backing_store)(store_path)?;
let mut cache = (store.open_cache)(backing_store.clone())?;
cache.pull_all_backing_stores()?;
(store.validate_writes)(&cache, &writes)?;
let mut companion_expected = Vec::new();
let mut companion_replacements = Vec::new();
for companion in companions {
@ -2521,9 +2528,11 @@ mod tests {
cache.add_generic_backing_store(backing_store)?;
Ok(cache)
},
validate_write: |write| match write.key.as_str() {
"refused" => Err(anyhow!("test profile refuses this write")),
_ => Ok(()),
validate_writes: |_, writes| {
match writes.iter().any(|write| write.key == "refused") {
true => Err(anyhow!("test profile refuses this write")),
false => Ok(()),
}
},
};
@ -2588,7 +2597,7 @@ mod tests {
#[test]
fn typed_commit_store_validates_before_answering_a_replay() -> Result<()> {
const NOW_REFUSING: TypedCommitStore = TypedCommitStore {
validate_write: |_| Err(anyhow!("test profile now refuses every write")),
validate_writes: |_, _| Err(anyhow!("test profile now refuses every write")),
..TEST_COMMIT_STORE
};
let temp = tempdir()?;

View file

@ -27,12 +27,24 @@
],
"type": "object"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
@ -41,12 +53,13 @@
"properties": {
"repos": {
"items": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
},
"maxItems": 8,
"type": "array"
},
"slug": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"target_doc": {
"$ref": "#/$defs/DocRef"

View file

@ -1,30 +1,5 @@
{
"$defs": {
"CodeLocation": {
"properties": {
"end_line": {
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"$ref": "#/$defs/Short"
}
},
"required": [
"path",
"line"
],
"type": "object"
},
"CommitRange": {
"properties": {
"base": {
@ -86,39 +61,24 @@
"type": "string"
},
"LandedName": {
"description": "A name the cut landed, and where it lives.",
"properties": {
"kind": {
"$ref": "#/$defs/LandedNameKind"
},
"location": {
"$ref": "#/$defs/CodeLocation"
},
"name": {
"$ref": "#/$defs/Short"
},
"path": {
"$ref": "#/$defs/Short"
}
},
"required": [
"name",
"kind",
"location"
"path"
],
"type": "object"
},
"LandedNameKind": {
"enum": [
"Module",
"Type",
"Function",
"Constant",
"Test",
"Schema",
"Binary",
"File"
],
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"MutationRecord": {
@ -140,6 +100,11 @@
],
"type": "object"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"ReportCommit": {
"properties": {
"builds": {
@ -161,10 +126,17 @@
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
},
"StructuralDelta": {
@ -173,24 +145,28 @@
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"dependencies_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"formats_added": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"formats_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"lines_added": {
@ -207,12 +183,14 @@
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"targets_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
}
},
@ -233,18 +211,21 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"attempt": {
"$ref": "#/$defs/Short"
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"branch": {
"$ref": "#/$defs/Short"
},
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"commits": {
"items": {
"$ref": "#/$defs/ReportCommit"
},
"maxItems": 128,
"type": "array"
},
"cut_spec": {
@ -254,31 +235,35 @@
"items": {
"$ref": "#/$defs/Deviation"
},
"maxItems": 32,
"type": "array"
},
"forks": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8,
"type": "array"
},
"landed_names": {
"items": {
"$ref": "#/$defs/LandedName"
},
"maxItems": 128,
"type": "array"
},
"mutations": {
"items": {
"$ref": "#/$defs/MutationRecord"
},
"maxItems": 64,
"type": "array"
},
"range": {
"$ref": "#/$defs/CommitRange"
},
"repo": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
},
"structural_delta": {
"$ref": "#/$defs/StructuralDelta"
@ -287,12 +272,14 @@
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 32,
"type": "array"
},
"verification": {
"items": {
"$ref": "#/$defs/Evidence"
},
"maxItems": 64,
"type": "array"
}
},

View file

@ -9,24 +9,28 @@
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"forbidden_writers": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"inputs": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"outputs": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"owner": {
@ -36,6 +40,7 @@
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
}
},
@ -102,24 +107,28 @@
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 64,
"type": "array"
},
"negative": {
"items": {
"$ref": "#/$defs/NegativeCheck"
},
"maxItems": 64,
"type": "array"
},
"operator": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 64,
"type": "array"
},
"tests": {
"items": {
"$ref": "#/$defs/VerificationTest"
},
"maxItems": 64,
"type": "array"
}
},
@ -146,8 +155,14 @@
],
"type": "object"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"NegativeCheck": {
@ -165,12 +180,24 @@
],
"type": "object"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
},
"SubtractionEstimate": {
@ -179,6 +206,7 @@
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
},
"lines_added": {
@ -195,6 +223,7 @@
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 64,
"type": "array"
}
},
@ -229,6 +258,7 @@
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 64,
"type": "array"
},
"authority_map": {
@ -248,49 +278,55 @@
"$ref": "#/$defs/Short"
},
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"cut": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"deletes": {
"items": {
"$ref": "#/$defs/CutDelete"
},
"maxItems": 64,
"type": "array"
},
"depends_on": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 8,
"type": "array"
},
"file_changes": {
"items": {
"$ref": "#/$defs/FileChange"
},
"maxItems": 256,
"type": "array"
},
"first": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"keeps_moves": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 64,
"type": "array"
},
"questions": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 16,
"type": "array"
},
"repo": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
},
"revision": {
"format": "uint32",
@ -301,6 +337,7 @@
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 32,
"type": "array"
},
"subtraction_estimate": {

View file

@ -90,7 +90,7 @@
"description": "A citation of a document in another repo's pipeline store (D6).",
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
"$ref": "#/$defs/FullSha"
},
"id": {
"$ref": "#/$defs/Short"
@ -99,10 +99,10 @@
"$ref": "#/$defs/PipelineKind"
},
"payload_sha256": {
"type": "string"
"$ref": "#/$defs/Sha256Hex"
},
"repo": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
@ -114,12 +114,29 @@
],
"type": "object"
},
"FullSha": {
"description": "A full git commit id: exactly 40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
@ -139,10 +156,22 @@
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Sha256Hex": {
"description": "A SHA-256 digest: exactly 64 lowercase hex characters.",
"maxLength": 64,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
@ -150,7 +179,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"claim": {
"$ref": "#/$defs/Line"
@ -162,6 +191,7 @@
"items": {
"$ref": "#/$defs/Evidence"
},
"maxItems": 16,
"type": "array"
},
"failure_scenario": {
@ -169,23 +199,26 @@
},
"invariants": {
"items": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"maxItems": 8,
"type": "array"
},
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"locations": {
"items": {
"$ref": "#/$defs/CodeLocation"
},
"maxItems": 16,
"type": "array"
},
"precedents": {
"items": {
"$ref": "#/$defs/ForeignRef"
},
"maxItems": 8,
"type": "array"
},
"range": {

View file

@ -25,8 +25,19 @@
],
"type": "object"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"PipelineKind": {
@ -45,6 +56,7 @@
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
@ -61,6 +73,12 @@
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
@ -68,25 +86,26 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"item": {
"$ref": "#/$defs/Line"
},
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"locations": {
"items": {
"$ref": "#/$defs/CodeLocation"
},
"maxItems": 16,
"type": "array"
},
"owner": {
"$ref": "#/$defs/Short"
},
"repo": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
},
"source": {
"$ref": "#/$defs/PipelineRef"

View file

@ -2,14 +2,22 @@
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
@ -28,6 +36,7 @@
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
@ -45,7 +54,7 @@
"QuestionOption": {
"properties": {
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"text": {
"$ref": "#/$defs/Line"
@ -59,6 +68,12 @@
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
@ -69,21 +84,23 @@
"$ref": "#/$defs/Date"
},
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"depends": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 8,
"type": "array"
},
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"options": {
"items": {
"$ref": "#/$defs/QuestionOption"
},
"maxItems": 8,
"type": "array"
},
"question": {
@ -100,7 +117,7 @@
]
},
"recommended": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
}
},
"required": [

View file

@ -2,14 +2,17 @@
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
@ -28,6 +31,7 @@
"type": "string"
},
"PipelineRef": {
"description": "A reference to another document. The id is parsed as a full pipeline id of\nthe declared `kind`, so the kind and the id's kind segment cannot disagree.",
"properties": {
"id": {
"$ref": "#/$defs/Short"
@ -168,6 +172,7 @@
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
}
},

View file

@ -2,13 +2,14 @@
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"maxLength": 10,
"type": "string"
},
"ForeignRef": {
"description": "A citation of a document in another repo's pipeline store (D6).",
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
"$ref": "#/$defs/FullSha"
},
"id": {
"$ref": "#/$defs/Short"
@ -17,10 +18,10 @@
"$ref": "#/$defs/PipelineKind"
},
"payload_sha256": {
"type": "string"
"$ref": "#/$defs/Sha256Hex"
},
"repo": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/OrgRepo"
}
},
"required": [
@ -32,8 +33,24 @@
],
"type": "object"
},
"FullSha": {
"description": "A full git commit id: exactly 40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"OrgRepo": {
"description": "A GitHub repository as `Org/Repo`.",
"maxLength": 200,
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"maxLength": 4000,
"type": "string"
},
"PipelineKind": {
@ -51,12 +68,19 @@
],
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"Sha256Hex": {
"description": "A SHA-256 digest: exactly 64 lowercase hex characters.",
"maxLength": 64,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
}
},
@ -74,12 +98,12 @@
]
},
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"choice": {
"anyOf": [
{
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
{
"type": "null"
@ -87,7 +111,7 @@
]
},
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"operator_quote": {
"anyOf": [
@ -103,6 +127,7 @@
"items": {
"$ref": "#/$defs/ForeignRef"
},
"maxItems": 8,
"type": "array"
},
"ruled_on": {

View file

@ -27,22 +27,35 @@
],
"type": "object"
},
"Label": {
"description": "A key label: `[A-Za-z0-9_-]{1,64}`.",
"maxLength": 64,
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
},
"TargetInvariant": {
"properties": {
"label": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Label"
},
"statement": {
"$ref": "#/$defs/Line"
@ -59,12 +72,13 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"canonical_implementations": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 16,
"type": "array"
},
"doc": {
@ -74,12 +88,14 @@
"items": {
"$ref": "#/$defs/TargetInvariant"
},
"maxItems": 32,
"type": "array"
},
"not_in_scope": {
"items": {
"$ref": "#/$defs/Line"
},
"maxItems": 32,
"type": "array"
},
"revision": {

View file

@ -55,14 +55,22 @@
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"maxLength": 1000,
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"maxLength": 40,
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"maxLength": 200,
"type": "string"
},
"Slug": {
"description": "A campaign slug or key segment: label parts joined by `.`.",
"maxLength": 64,
"type": "string"
},
"VerdictClaim": {
@ -74,12 +82,14 @@
"items": {
"$ref": "#/$defs/Evidence"
},
"maxItems": 8,
"type": "array"
},
"findings": {
"items": {
"$ref": "#/$defs/Short"
},
"maxItems": 16,
"type": "array"
},
"outcome": {
@ -99,19 +109,22 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
"$ref": "#/$defs/Slug"
},
"claims": {
"items": {
"$ref": "#/$defs/VerdictClaim"
},
"maxItems": 64,
"type": "array"
},
"cut_report": {
"$ref": "#/$defs/Short"
},
"pass": {
"$ref": "#/$defs/Short"
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"range": {
"$ref": "#/$defs/CommitRange"