Add pipeline documents, store opener, writer lease and schemas

Cut 3a of the Eureka pipeline state campaign (cut map D1-D3).

- pipeline_documents.rs: the ten kinds as serde + JsonSchema values in
  one-slot DatabaseEntry wrappers (epiphany.pipeline.<kind>.v1), the
  identity, provenance and writer-holder documents, UTF-8 byte bounds,
  semantic key derivation, and the pipeline write validator (bounds, then
  key recomputation).
- pipeline_store.rs: PipelineStore::open (read-only, no lease),
  PipelineWriter::attach (fs2 lock in the git common dir, display-only
  holder file, binary/ignore checks), require_branch, PipelineRefusal,
  and PIPELINE_COMMIT_STORE on the one commit owner. The opener refuses
  foreign types, missing identity and foreign epochs before attaching.
- schemars 1 is an unconditional core dependency (Q5-A); the ten published
  schemas are derived and checked byte-for-byte, with index.json entries.

The profile and the prepare path carry expect(dead_code) naming Cut 3b as
their consumer, so the attribute fails the build once admission uses them.

Builds: yes. cargo test -p epiphany-core --lib: 171/171, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-15 21:55:04 +02:00
commit ad18c385dc
19 changed files with 3231 additions and 3 deletions

63
Cargo.lock generated
View file

@ -502,6 +502,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ed25519"
version = "2.2.3"
@ -544,6 +550,7 @@ dependencies = [
"ignore",
"libc",
"rmp-serde",
"schemars",
"semver",
"serde",
"serde_json",
@ -1557,6 +1564,26 @@ dependencies = [
"libc",
]
[[package]]
name = "ref-cast"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
@ -1810,6 +1837,31 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 3.0.3",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@ -1883,6 +1935,17 @@ dependencies = [
"syn 3.0.3",
]
[[package]]
name = "serde_derive_internals"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.151"

View file

@ -24,7 +24,8 @@ ghostlight-persona-projection = { git = "https://github.com/GameCult/Ghostlight.
ed25519-dalek = "2"
ignore = "0.4.23"
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
schemars = "1"
serde ={ version = "1", features = ["derive"] }
serde_json = "1"
semver = { version = "1", features = ["serde"] }
sha2 = "0.10"

View file

@ -17,6 +17,8 @@ mod persona_discord_permit;
mod persona_feedback_admission;
mod persona_social_state;
mod persona_turn;
mod pipeline_documents;
mod pipeline_store;
mod process_observation;
mod public_source_identity;
mod reasoning_context;
@ -126,6 +128,8 @@ pub use packaged_release::{
pub use persona_conversation::*;
pub use persona_discord_crossing::*;
pub use persona_discord_permit::*;
pub use pipeline_documents::*;
pub use pipeline_store::{PIPELINE_STORE_PATH, PipelineRefusal, PipelineStore, PipelineWriter};
pub use persona_feedback_admission::{
BIFROST_PERSONA_FEEDBACK_ADMISSION_SCHEMA_VERSION, BIFROST_PERSONA_FEEDBACK_DELIVERY_TYPE,
BifrostPersonaFeedbackAdmission, LOCAL_PERSONA_FEEDBACK_SCHEMA_VERSION,

View file

@ -0,0 +1,547 @@
//! Eureka pipeline documents (cut map D1).
//!
//! 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.
use crate::pipeline_store::PipelineRefusal;
use anyhow::Result;
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const PIPELINE_SCHEMA_EPOCH: &str = "epiphany.pipeline.epoch.v1";
pub(crate) trait Bounded {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal>;
}
impl<T: Bounded> Bounded for Option<T> {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
self.as_ref().map_or(Ok(()), |value| value.validate(field))
}
}
fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> {
if actual > limit {
return Err(PipelineRefusal::FieldBound {
field: field.into(),
limit: limit as u32,
actual: actual as u32,
});
}
Ok(())
}
/// List maximums only. Minimum counts are admission rules with their own
/// refusals (Cut 3b), so a bounds pass never pre-empts them.
fn list<T: Bounded>(field: &str, items: &[T], max: usize) -> Result<(), PipelineRefusal> {
bound(field, max, items.len())?;
for (index, item) in items.iter().enumerate() {
item.validate(&format!("{field}[{index}]"))?;
}
Ok(())
}
fn format_error(field: &str, value: &str) -> PipelineRefusal {
PipelineRefusal::InvalidFormat {
field: field.into(),
value: value.into(),
}
}
fn hex(field: &str, value: &str, lengths: std::ops::RangeInclusive<usize>) -> Result<(), PipelineRefusal> {
let lower_hex = value.bytes().all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'));
if !lengths.contains(&value.len()) || !lower_hex {
return Err(format_error(field, value));
}
Ok(())
}
/// `<slug>` and `<local>` key segments: `[A-Za-z0-9._-]{1,64}`.
fn label<'a>(field: &str, value: &'a str) -> Result<&'a str, PipelineRefusal> {
let valid = value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
if !valid || value.is_empty() || value.len() > 64 {
return Err(format_error(field, value));
}
Ok(value)
}
fn org_repo(field: &str, value: &str) -> Result<(), PipelineRefusal> {
match value.split_once('/') {
Some((org, repo)) if !org.is_empty() && !repo.is_empty() && !repo.contains('/') => Ok(()),
_ => Err(format_error(field, value)),
}
}
macro_rules! bounded_text {
($($(#[$doc:meta])* $name:ident = $limit:literal;)*) => {$(
$(#[$doc])*
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct $name(pub String);
impl Bounded for $name {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
bound(field, $limit, self.0.len())
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self(value.into())
}
}
)*};
}
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;
}
/// 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)
}
}
/// A calendar date, `YYYY-MM-DD`.
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
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_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)?
};
($value:expr, $field:expr, $max:literal) => {
list($field, &$value, $max)?
};
}
macro_rules! value_types {
($($(#[$attr:meta])* pub struct $name:ident { $($field:ident: $ty:ty),* $(,)? })*) => {$(
$(#[$attr])*
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct $name { $(pub $field: $ty),* }
)*};
}
macro_rules! unit_enums {
($($name:ident { $($variant:ident),* $(,)? })*) => {$(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
pub enum $name { $($variant),* }
)*};
}
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 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 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,
}
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>,
}
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 }
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>,
}
pub struct LandedName { name: Short, kind: LandedNameKind, location: CodeLocation }
pub struct VerdictClaim { claim: Line, outcome: ClaimOutcome, evidence: Vec<Evidence>, findings: Vec<Short> }
pub struct PipelineCampaign { slug: Short, title: Short, repos: Vec<Short>, 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,
}
pub struct PipelineQuestion {
campaign: Short, label: Short, question: Para, options: Vec<QuestionOption>, recommended: Short,
depends: Vec<Line>, 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>,
}
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>,
verification: CutVerification, subtraction_estimate: SubtractionEstimate,
rulings: Vec<Short>, questions: Vec<Short>,
}
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>,
}
pub struct PipelineVerdict { campaign: Short, cut_report: Short, pass: Short, range: CommitRange, claims: Vec<VerdictClaim> }
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>,
}
pub struct PipelineFollowUp {
campaign: Short, label: Short, source: PipelineRef, repo: Short, locations: Vec<CodeLocation>,
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 }
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum ResolutionOutcome {
Superseded { by: Short },
Answered { by: Short },
Fixed { by: Short },
Deferred { to: Short },
Recorded { reason: Line },
Withdrawn { reason: Line },
}
impl Bounded for ResolutionOutcome {
fn validate(&self, field: &str) -> Result<(), PipelineRefusal> {
match self {
Self::Superseded { by } | Self::Answered { by } | Self::Fixed { by } => by.validate(field),
Self::Deferred { to } => to.validate(field),
Self::Recorded { reason } | Self::Withdrawn { reason } => reason.validate(field),
}
}
}
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 {
#[cultcache(key = 0)]
pub value: $value,
}
)*
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
pub enum PipelineKind { $($variant),* }
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PipelineDocument { $($variant($value)),* }
impl PipelineKind {
pub const ALL: &'static [PipelineKind] = &[$(Self::$variant),*];
/// The kind segment of a document key.
pub fn name(self) -> &'static str {
match self { $(Self::$variant => $name),* }
}
pub fn type_id(self) -> &'static str {
match self { $(Self::$variant => <$document as DatabaseEntry>::TYPE),* }
}
#[cfg(test)]
pub(crate) fn derived_schema(self) -> schemars::Schema {
match self { $(Self::$variant => schemars::schema_for!($value)),* }
}
}
impl PipelineDocument {
pub fn kind(&self) -> PipelineKind {
match self { $(Self::$variant(_) => PipelineKind::$variant),* }
}
/// Field bounds (D1), in UTF-8 bytes and list maximums.
pub fn validate(&self) -> Result<(), PipelineRefusal> {
match self { $(Self::$variant(value) => value.validate($name)),* }
}
#[cfg_attr(not(test), expect(dead_code, reason = "Cut 3b admission prepares writes through this"))]
pub(crate) fn prepare(&self, cache: &CultCache) -> Result<CultCacheEnvelope> {
let key = pipeline_key(self)?;
Ok(match self {
$(Self::$variant(value) => {
cache.prepare_entry_named(key, &$document { value: value.clone() })?.0
})*
})
}
pub(crate) fn decode(envelope: &CultCacheEnvelope) -> Result<Self, PipelineRefusal> {
let invalid = |error: rmp_serde::decode::Error| format_error("payload", &error.to_string());
$(if envelope.r#type == <$document as DatabaseEntry>::TYPE {
let document: $document = rmp_serde::from_slice(&envelope.payload).map_err(invalid)?;
return Ok(Self::$variant(document.value));
})*
Err(PipelineRefusal::ForeignStore { r#type: envelope.r#type.clone() })
}
}
/// Registers every type a pipeline store may hold: the ten kinds, the
/// store identity, admission provenance, and the commit receipt.
pub(crate) fn register_pipeline_document_types(cache: &mut CultCache) -> Result<()> {
$(cache.register_entry_type::<$document>()?;)*
cache.register_entry_type::<EpiphanyPipelineIdentity>()?;
cache.register_entry_type::<EpiphanyPipelineProvenance>()?;
cache.register_entry_type::<crate::EpiphanyMindCommitReceipt>()?;
Ok(())
}
};
}
pipeline_kinds! {
Campaign(PipelineCampaign) => EpiphanyPipelineCampaignDocument, "campaign",
"epiphany.pipeline.campaign.v1", "EpiphanyPipelineCampaignDocument";
Target(PipelineTarget) => EpiphanyPipelineTargetDocument, "target",
"epiphany.pipeline.target.v1", "EpiphanyPipelineTargetDocument";
Question(PipelineQuestion) => EpiphanyPipelineQuestionDocument, "question",
"epiphany.pipeline.question.v1", "EpiphanyPipelineQuestionDocument";
Ruling(PipelineRuling) => EpiphanyPipelineRulingDocument, "ruling",
"epiphany.pipeline.ruling.v1", "EpiphanyPipelineRulingDocument";
CutSpec(PipelineCutSpec) => EpiphanyPipelineCutSpecDocument, "cut_spec",
"epiphany.pipeline.cut_spec.v1", "EpiphanyPipelineCutSpecDocument";
CutReport(PipelineCutReport) => EpiphanyPipelineCutReportDocument, "cut_report",
"epiphany.pipeline.cut_report.v1", "EpiphanyPipelineCutReportDocument";
Verdict(PipelineVerdict) => EpiphanyPipelineVerdictDocument, "verdict",
"epiphany.pipeline.verdict.v1", "EpiphanyPipelineVerdictDocument";
Finding(PipelineFinding) => EpiphanyPipelineFindingDocument, "finding",
"epiphany.pipeline.finding.v1", "EpiphanyPipelineFindingDocument";
FollowUp(PipelineFollowUp) => EpiphanyPipelineFollowUpDocument, "follow_up",
"epiphany.pipeline.follow_up.v1", "EpiphanyPipelineFollowUpDocument";
Resolution(PipelineResolution) => EpiphanyPipelineResolutionDocument, "resolution",
"epiphany.pipeline.resolution.v1", "EpiphanyPipelineResolutionDocument";
}
/// 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 {
#[cultcache(key = 0)]
pub schema_epoch: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.pipeline.provenance.v1", schema = "EpiphanyPipelineProvenance")]
pub struct EpiphanyPipelineProvenance {
#[cultcache(key = 0)]
pub value: PipelineProvenance,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.pipeline.writer_holder.v1", schema = "EpiphanyPipelineWriterHolder")]
pub struct EpiphanyPipelineWriterHolder {
#[cultcache(key = 0)]
pub value: PipelineWriterHolder,
}
/// The local segment of a parent id `<campaign>:<kind>:<local>` in `campaign`.
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)),
}
}
/// The cut label inside a parent local `cut-<label>.<marker><N>`.
fn parent_cut<'a>(field: &str, local: &'a str, marker: char) -> Result<&'a str, PipelineRefusal> {
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))
}
/// 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::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()),
D::FollowUp(value) => (&value.campaign, value.label.0.clone()),
D::CutSpec(value) => (&value.campaign, format!("cut-{}.r{}", value.cut.0, value.revision)),
D::CutReport(value) => {
let spec = parent_local("cut_report.cut_spec", &value.cut_spec.0, &value.campaign.0, PipelineKind::CutSpec)?;
let cut = parent_cut("cut_report.cut_spec", spec, 'r')?;
(&value.campaign, format!("cut-{cut}.h{}", value.attempt.0))
}
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))
}
D::Finding(value) => {
let verdict = parent_local("finding.verdict", &value.verdict.0, &value.campaign.0, PipelineKind::Verdict)?;
(&value.campaign, format!("{verdict}.{}", value.label.0))
}
};
let kind = document.kind().name();
label(&format!("{kind}.campaign"), &campaign.0)?;
label(&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<()> {
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 {
return Err(PipelineRefusal::ForeignEpoch {
found: identity.schema_epoch,
expected: PIPELINE_SCHEMA_EPOCH.into(),
}
.into());
}
return Ok(());
}
let document = PipelineDocument::decode(envelope)?;
document.validate()?;
let expected = pipeline_key(&document)?;
if envelope.key != expected {
return Err(PipelineRefusal::InvalidIdentity {
kind: document.kind(),
key: envelope.key.clone(),
expected,
}
.into());
}
Ok(())
}

View file

@ -0,0 +1,771 @@
//! Eureka pipeline store opener and writer lease (cut map D2, D3).
//!
//! 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.
use crate::pipeline_documents::{
Bounded, EpiphanyPipelineIdentity, EpiphanyPipelineWriterHolder, PIPELINE_SCHEMA_EPOCH,
PipelineKind, PipelineWriterHolder, register_pipeline_document_types,
validate_pipeline_write_envelope,
};
use crate::reasoning_context::TypedCommitStore;
use crate::repository_body_observer::repository_git_command;
use crate::runtime_store_backend::{RuntimeSpineBackingStore, runtime_spine_backing_store};
use anyhow::Result;
use cultcache_rs::{
CacheBackingStore, CultCache, CultCacheEnvelope, DatabaseEntry,
SingleFileMessagePackBackingStore,
};
use fs2::FileExt;
use std::fs::{File, OpenOptions};
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";
const WRITER_HOLDER: &str = "epiphany-pipeline-writer.cc";
const HOLDER_KEY: &str = "holder";
/// Typed refusals of the pipeline documents, opener and lease. Cut 3b and 3c
/// add the admission and merge refusals alongside the rules that raise them.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PipelineRefusal {
FieldBound { field: String, limit: u32, actual: u32 },
InvalidFormat { field: String, value: String },
InvalidIdentity { kind: PipelineKind, key: String, expected: String },
NotRepoRoot { repo_root: String },
StoreNotMarkedBinary { line: String },
LockNotIgnored { line: String },
WrongBranch { expected: String, actual: String },
MissingIdentity,
ForeignEpoch { found: String, expected: String },
ForeignStore { r#type: String },
WriterLeaseHeld { holder: Option<PipelineWriterHolder> },
Unavailable { detail: 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 {}
fn refusal(error: anyhow::Error) -> PipelineRefusal {
error
.downcast::<PipelineRefusal>()
.unwrap_or_else(|error| PipelineRefusal::Unavailable { detail: format!("{error:#}") })
}
fn unavailable(error: impl std::fmt::Display) -> PipelineRefusal {
PipelineRefusal::Unavailable { detail: error.to_string() }
}
#[cfg_attr(not(test), expect(dead_code, reason = "Cut 3b admission commits through this profile"))]
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,
};
/// Opens a pipeline store image. Refuses, before attaching the store, any
/// record of an unregistered type (`ForeignStore`), records without the
/// identity (`MissingIdentity`), and an identity at another epoch
/// (`ForeignEpoch`). It never reads the runtime or Mind epoch.
fn pipeline_cache(backing_store: RuntimeSpineBackingStore) -> Result<CultCache> {
let envelopes = backing_store.pull_all()?;
let mut registry = CultCache::new();
register_pipeline_document_types(&mut registry)?;
for envelope in &envelopes {
if registry.put_raw_envelope(envelope.clone()).is_err() {
return Err(PipelineRefusal::ForeignStore { r#type: envelope.r#type.clone() }.into());
}
}
let identities = envelopes
.iter()
.filter(|envelope| envelope.r#type == EpiphanyPipelineIdentity::TYPE)
.collect::<Vec<_>>();
if !envelopes.is_empty() && identities.is_empty() {
return Err(PipelineRefusal::MissingIdentity.into());
}
for envelope in identities {
let found = rmp_serde::from_slice::<EpiphanyPipelineIdentity>(&envelope.payload)
.map(|identity| identity.schema_epoch)
.unwrap_or_else(|_| envelope.key.clone());
if found != PIPELINE_SCHEMA_EPOCH || envelope.key != PIPELINE_SCHEMA_EPOCH {
return Err(PipelineRefusal::ForeignEpoch {
found,
expected: PIPELINE_SCHEMA_EPOCH.into(),
}
.into());
}
}
let mut cache = CultCache::new();
register_pipeline_document_types(&mut cache)?;
cache.add_generic_backing_store(backing_store)?;
cache.pull_all_backing_stores()?;
Ok(cache)
}
fn git(repo_root: &Path, args: &[&str]) -> Result<Output, PipelineRefusal> {
repository_git_command(repo_root)
.map_err(refusal)?
.arg("-C")
.arg(repo_root)
.args(args)
.output()
.map_err(|error| unavailable(format!("git {}: {error}", args.join(" "))))
}
fn git_line(repo_root: &Path, args: &[&str]) -> Result<String, PipelineRefusal> {
let output = git(repo_root, args)?;
if !output.status.success() {
return Err(unavailable(format!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
/// `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() };
let canonical = std::fs::canonicalize(repo_root).map_err(|_| not_root())?;
let toplevel = git_line(repo_root, &["rev-parse", "--show-toplevel"]).map_err(|_| not_root())?;
if std::fs::canonicalize(toplevel).ok() != Some(canonical) {
return Err(not_root());
}
Ok(repo_root.to_path_buf())
}
/// A read-only image of one repo's pipeline store. Takes no lease.
pub struct PipelineStore {
repo_root: PathBuf,
cache: CultCache,
}
impl PipelineStore {
pub fn open(repo_root: impl AsRef<Path>) -> Result<Self, PipelineRefusal> {
let repo_root = 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 })
}
pub fn repo_root(&self) -> &Path {
&self.repo_root
}
pub fn envelopes(&self) -> Vec<CultCacheEnvelope> {
self.cache.snapshot_envelopes()
}
}
/// The one writer lease for a clone. Dropping the handle releases it.
pub struct PipelineWriter {
repo_root: PathBuf,
_lease: File,
}
impl PipelineWriter {
pub fn attach(
repo_root: impl AsRef<Path>,
holder: PipelineWriterHolder,
) -> Result<Self, PipelineRefusal> {
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") {
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:?}"))),
}
let common_dir = repo_root.join(git_line(&repo_root, &["rev-parse", "--git-common-dir"])?);
let lease = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(common_dir.join(WRITER_LEASE))
.map_err(unavailable)?;
if let Err(error) = lease.try_lock_exclusive() {
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) });
}
write_writer_holder(&common_dir, holder).map_err(refusal)?;
Ok(Self { repo_root, _lease: lease })
}
pub fn repo_root(&self) -> &Path {
&self.repo_root
}
pub fn store_path(&self) -> PathBuf {
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.
pub fn require_branch(&self, expected: &str) -> Result<(), PipelineRefusal> {
let actual = git_line(&self.repo_root, &["rev-parse", "--abbrev-ref", "HEAD"])?;
if actual != expected {
return Err(PipelineRefusal::WrongBranch { expected: expected.into(), actual });
}
Ok(())
}
}
fn holder_cache(common_dir: &Path) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<EpiphanyPipelineWriterHolder>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(
common_dir.join(WRITER_HOLDER),
))?;
cache.pull_all_backing_stores()?;
Ok(cache)
}
fn read_writer_holder(common_dir: &Path) -> Option<PipelineWriterHolder> {
let cache = holder_cache(common_dir).ok()?;
Some(cache.get::<EpiphanyPipelineWriterHolder>(HOLDER_KEY).ok()??.value)
}
fn write_writer_holder(common_dir: &Path, value: PipelineWriterHolder) -> Result<()> {
let mut cache = holder_cache(common_dir)?;
let envelope = cache
.prepare_entry_named(HOLDER_KEY, &EpiphanyPipelineWriterHolder { value })?
.0;
cache.put_envelope::<EpiphanyPipelineWriterHolder>(envelope)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EpiphanyMindDocumentVersion;
use crate::pipeline_documents::*;
use crate::reasoning_context::{
EpiphanyMindCommitAuthority, EpiphanyMindCommitOutcome, commit_authorized_mind_mutation,
};
use std::collections::BTreeSet;
use tempfile::{TempDir, tempdir};
const CAMPAIGN: &str = "eureka-state";
fn s(value: &str) -> Short {
value.into()
}
fn id(kind: &str, local: &str) -> Short {
Short(format!("{CAMPAIGN}:{kind}:{local}"))
}
fn sha() -> Sha {
Sha("5f98228d".into())
}
fn date() -> Date {
Date("2026-09-15".into())
}
fn range() -> CommitRange {
CommitRange { base: sha(), head: sha() }
}
fn location() -> CodeLocation {
CodeLocation { path: s("epiphany-core/src/pipeline_store.rs"), line: 1, end_line: Some(9) }
}
fn evidence() -> Evidence {
Evidence { kind: EvidenceKind::Test, locator: "cargo test".into(), result: "ok".into() }
}
fn doc_ref() -> DocRef {
DocRef { path: s("notes/eureka-pipeline-state-target.md"), start_line: 1, end_line: 9, commit: sha() }
}
/// One valid document of every kind, with its expected key.
fn samples() -> Vec<(PipelineDocument, String)> {
use PipelineDocument as D;
let repo = || s("GameCult/Epiphany");
let branch = || s("codex/eureka-pipeline-state");
vec![
(D::Campaign(PipelineCampaign {
slug: s(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() }],
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(),
options: vec![
QuestionOption { label: s("A"), text: "single file".into() },
QuestionOption { label: s("B"), text: "directory store".into() },
],
recommended: s("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")),
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),
}],
}), format!("{CAMPAIGN}:ruling:R8")),
(D::CutSpec(PipelineCutSpec {
campaign: s(CAMPAIGN), cut: s("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()],
file_changes: vec![FileChange { location: location(), change: "add".into() }],
authority_map: Some(AuthorityMap {
owner: "core".into(), inputs: vec!["repo root".into()], outputs: vec!["handles".into()],
derived_state: vec!["holder".into()], forbidden_writers: vec!["MCP".into()],
shared_paths: vec!["attach".into()], deletion_line: "n/a".into(),
}),
verification: CutVerification {
builds: vec!["cargo check".into()],
tests: vec![VerificationTest { name: s("lease"), pins: "one writer".into() }],
negative: vec![NegativeCheck { pattern: s("Vec<u8>"), scope: "documents".into() }],
operator: vec!["none".into()],
},
subtraction_estimate: SubtractionEstimate {
lines_removed: 0, lines_added: 900, removed: vec![], added: vec![s("schemars")],
},
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(),
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 }],
deviations: vec![Deviation { what: "names".into(), why: "glob exports".into() }],
forks: vec![id("question", "Q1")],
structural_delta: StructuralDelta {
lines_added: 900, lines_removed: 0, dependencies_added: vec![s("schemars")],
dependencies_removed: vec![], formats_added: vec![s("epiphany.pipeline.*.v1")],
formats_removed: vec![], targets_added: vec![], targets_removed: vec![],
},
landed_names: vec![LandedName { name: s("PipelineStore"), kind: LandedNameKind::Type, location: location() }],
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(),
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(),
confidence: FindingConfidence::Confirmed, severity: FindingSeverity::High,
claim: "A worktree takes its own lease.".into(), invariants: vec![s("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"),
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"),
}), format!("{CAMPAIGN}:follow_up:FU-4")),
(D::Resolution(PipelineResolution {
subject: PipelineRef { kind: PipelineKind::Question, id: id("question", "Q1") },
outcome: ResolutionOutcome::Answered { by: id("ruling", "R8") },
rationale: "Ruled A.".into(), resolved_on: date(),
}), format!("resolution:{CAMPAIGN}:question:Q1")),
]
}
fn campaign_sample() -> PipelineDocument {
samples().remove(0).0
}
fn schema_cache() -> Result<CultCache> {
let mut cache = CultCache::new();
register_pipeline_document_types(&mut cache)?;
Ok(cache)
}
fn identity(cache: &CultCache, epoch: &str) -> Result<CultCacheEnvelope> {
Ok(cache
.prepare_entry_named(epoch, &EpiphanyPipelineIdentity { schema_epoch: epoch.into() })?
.0)
}
fn write_store(path: &Path, envelopes: &[CultCacheEnvelope]) -> Result<()> {
std::fs::create_dir_all(path.parent().expect("a store path has a parent"))?;
let mut store = SingleFileMessagePackBackingStore::new(path);
for envelope in envelopes {
store.push(envelope)?;
}
Ok(())
}
fn commit(store: &Path, writes: Vec<CultCacheEnvelope>) -> Result<EpiphanyMindCommitOutcome> {
let provenance = schema_cache()?
.prepare_entry_named(
"test-provenance",
&EpiphanyPipelineProvenance {
value: PipelineProvenance {
faculty: Faculty::Hands, agent: s("hands"), session: s("test"), tool: s("test"),
},
},
)?
.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(),
writes,
vec![provenance],
"2026-09-15T00:00:00Z",
)
}
/// Test git setup. Children are `git` only; none re-launches this binary.
fn git_ok(dir: &Path, args: &[&str]) -> Result<()> {
let output = repository_git_command(dir)?
.arg("-C")
.arg(dir)
.args(["-c", "user.name=pipeline-test", "-c", "user.email=pipeline-test@example.invalid"])
.args(args)
.output()?;
anyhow::ensure!(output.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&output.stderr));
Ok(())
}
/// A committed repo at `<temp>/repo` on `main`, with the D2 lines present
/// or absent.
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"))?;
git_ok(&root, &["add", ".gitattributes", ".gitignore"])?;
git_ok(&root, &["commit", "-q", "-m", "seed"])?;
Ok((temp, root))
}
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(),
}
}
#[test]
fn every_pipeline_kind_round_trips_through_named_slot_zero() -> Result<()> {
let cache = schema_cache()?;
let samples = samples();
let kinds = samples.iter().map(|(document, _)| document.kind()).collect::<BTreeSet<_>>();
assert_eq!(kinds.len(), PipelineKind::ALL.len(), "every kind has a sample");
for (document, _) in samples {
document.validate()?;
let envelope = document.prepare(&cache)?;
assert_eq!(envelope.r#type, document.kind().type_id());
assert_eq!(envelope.payload[0], 0x91, "{:?} payload is a one-element array", document.kind());
assert!(
matches!(envelope.payload[1], 0x80..=0x8f | 0xde | 0xdf),
"{:?} slot 0 is a named map",
document.kind()
);
assert_eq!(PipelineDocument::decode(&envelope)?, document);
validate_pipeline_write_envelope(&envelope)?;
}
Ok(())
}
#[test]
fn bounds_refuse_in_utf8_bytes() {
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
campaign.title = Short("é".repeat(100));
assert_eq!(PipelineDocument::Campaign(campaign.clone()).validate(), Ok(()));
campaign.title = Short(format!("{}a", "é".repeat(100)));
assert_eq!(
PipelineDocument::Campaign(campaign.clone()).validate(),
Err(PipelineRefusal::FieldBound { field: "campaign.title".into(), limit: 200, actual: 201 })
);
campaign.title = s("ok");
campaign.repos = vec![s("GameCult/Epiphany"); 9];
assert_eq!(
PipelineDocument::Campaign(campaign).validate(),
Err(PipelineRefusal::FieldBound { field: "campaign.repos".into(), limit: 8, actual: 9 })
);
}
#[test]
fn keys_are_derived_and_mismatch_refuses() -> Result<()> {
let cache = schema_cache()?;
for (document, key) in samples() {
assert_eq!(pipeline_key(&document), Ok(key.clone()));
let mut envelope = document.prepare(&cache)?;
envelope.key = format!("{key}-forged");
let error = validate_pipeline_write_envelope(&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 answered = pipeline_key(&PipelineDocument::Resolution(resolution.clone()));
resolution.outcome = ResolutionOutcome::Withdrawn { reason: "moot".into() };
assert_eq!(
pipeline_key(&PipelineDocument::Resolution(resolution)),
answered,
"a subject has one resolution key whatever the outcome"
);
Ok(())
}
#[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 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();
assert_eq!(committed.downcast_ref::<PipelineRefusal>(), Some(&opened), "the commit profile refuses too");
assert_eq!(std::fs::read(&store)?, before);
Ok(())
};
write_store(&store, &[identity(&cache, "epiphany.pipeline.epoch.v0")?, campaign.clone()])?;
refused(&|refusal| {
refusal
== &PipelineRefusal::ForeignEpoch {
found: "epiphany.pipeline.epoch.v0".into(),
expected: "epiphany.pipeline.epoch.v1".into(),
}
})?;
std::fs::remove_file(&store)?;
write_store(&store, &[campaign.clone()])?;
refused(&|refusal| refusal == &PipelineRefusal::MissingIdentity)?;
std::fs::remove_file(&store)?;
crate::runtime_spine::initialize_runtime_spine(
&store,
crate::runtime_spine::RuntimeSpineInitOptions {
runtime_id: "pipeline-foreign".into(),
display_name: "Pipeline foreign".into(),
created_at: "2026-09-15T00:00:00Z".into(),
},
)?;
refused(&|refusal| {
matches!(refusal, PipelineRefusal::ForeignStore { r#type } if !r#type.starts_with("epiphany.pipeline."))
})
}
#[test]
fn runtime_spine_cache_refuses_a_pipeline_store() -> Result<()> {
let temp = tempdir()?;
let store = temp.path().join("pipeline.cc");
let cache = schema_cache()?;
write_store(&store, &[identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign_sample().prepare(&cache)?])?;
let error = crate::runtime_spine_cache(&store)
.and_then(|mut runtime| runtime.pull_all_backing_stores())
.unwrap_err();
assert!(error.to_string().contains("epiphany.pipeline."), "{error:#}");
Ok(())
}
#[test]
fn pipeline_commit_profile_registers_the_receipt_and_commits_the_first_write() -> 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 EpiphanyMindCommitOutcome::Committed(receipt) =
commit(&store, vec![identity(&cache, PIPELINE_SCHEMA_EPOCH)?, campaign.clone()])?
else {
panic!("the first pipeline write commits");
};
assert!(receipt.writes.iter().all(|version| version.store_id == "epiphany-pipeline"));
let envelopes = PipelineStore::open(&root)?.envelopes();
assert!(envelopes.iter().any(|entry| entry.key == campaign.key && entry.payload == campaign.payload));
assert!(envelopes.iter().any(|entry| entry.key == receipt.receipt_id));
let before = std::fs::read(&store)?;
let mut forged = campaign;
forged.key = "forged".into();
let error = commit(&store, vec![forged]).unwrap_err();
assert!(matches!(error.downcast_ref(), Some(PipelineRefusal::InvalidIdentity { .. })), "{error:#}");
assert_eq!(std::fs::read(&store)?, before);
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 contender = root.clone();
let second = std::thread::spawn(move || PipelineWriter::attach(&contender, holder("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");
drop(first);
let second = PipelineWriter::attach(&root, holder("second"))?;
assert_eq!(second.store_path(), root.join(PIPELINE_STORE_PATH));
Ok(())
}
#[test]
fn writer_lease_is_shared_across_worktrees_of_one_clone() -> 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"))?;
assert_eq!(
PipelineWriter::attach(&worktree, holder("worktree")).err(),
Some(PipelineRefusal::WriterLeaseHeld { holder: Some(holder("main")) })
);
Ok(())
}
#[test]
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() })
);
let (_unignored_temp, unignored) = campaign_repo(true, false)?;
assert_eq!(
PipelineWriter::attach(&unignored, holder("t")).err(),
Some(PipelineRefusal::LockNotIgnored { line: "/.epiphany/pipeline/*.lock".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(),
Some(PipelineRefusal::NotRepoRoot { repo_root: nested.display().to_string() })
);
let writer = PipelineWriter::attach(&root, holder("t"))?;
assert_eq!(
writer.require_branch("codex/eureka-pipeline-state"),
Err(PipelineRefusal::WrongBranch {
expected: "codex/eureka-pipeline-state".into(),
actual: "main".into(),
})
);
assert_eq!(writer.require_branch("main"), Ok(()));
Ok(())
}
#[test]
fn pipeline_published_schemas_match_derivation() -> Result<()> {
let published = Path::new(env!("CARGO_MANIFEST_DIR")).join("../schemas/cultnet");
let index: serde_json::Value = serde_json::from_slice(&std::fs::read(published.join("index.json"))?)?;
let derived_dir = std::env::temp_dir().join("epiphany-pipeline-schemas");
let mut stale = Vec::new();
for kind in PipelineKind::ALL {
let file = format!("{}.schema.json", kind.type_id());
let mut schema = serde_json::to_value(kind.derived_schema())?;
schema["$id"] = format!("https://gamecult.dev/epiphany/cultnet/{file}").into();
let derived = format!("{}\n", serde_json::to_string_pretty(&schema)?);
if std::fs::read(published.join(&file)).ok().as_deref() != Some(derived.as_bytes()) {
std::fs::create_dir_all(&derived_dir)?;
std::fs::write(derived_dir.join(&file), &derived)?;
stale.push(derived_dir.join(&file));
}
let entry = serde_json::json!({
"schemaId": format!("https://gamecult.dev/epiphany/cultnet/{file}"),
"kind": "document_payload",
"wireContracts": ["cultnet.schema.v0"],
"schemaVersion": kind.type_id(),
"documentType": kind.type_id(),
"title": format!("Epiphany Pipeline {kind:?} v1"),
"path": file,
});
assert!(
index["schemas"].as_array().is_some_and(|schemas| schemas.contains(&entry)),
"index.json lacks {entry}"
);
}
assert!(stale.is_empty(), "published pipeline schemas differ from the Rust derivation; derived copies: {stale:?}");
Ok(())
}
/// The child is `powershell.exe` running a fixed script that byte-range
/// locks the lease file and sleeps. It cannot launch this test binary.
#[cfg(windows)]
#[test]
fn writer_lease_releases_when_the_holder_process_dies() -> Result<()> {
struct KillOnDrop(std::process::Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn eventually(condition: impl Fn() -> bool) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while !condition() {
assert!(std::time::Instant::now() < deadline, "condition not reached in 30 s");
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
let (_temp, root) = campaign_repo(true, true)?;
let lease = root.join(".git").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}}}}; \
Start-Sleep -Seconds 120",
lease.display()
);
let mut child = KillOnDrop(
std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?,
);
eventually(|| {
matches!(
PipelineWriter::attach(&root, holder("probe")),
Err(PipelineRefusal::WriterLeaseHeld { .. })
)
});
child.0.kill()?;
child.0.wait()?;
eventually(|| PipelineWriter::attach(&root, holder("after")).is_ok());
Ok(())
}
}

View file

@ -1601,7 +1601,7 @@ pub(crate) const MIND_COMMIT_STORE: TypedCommitStore = TypedCommitStore {
validate_write: crate::mind_documents::validate_mind_write_envelope,
};
fn commit_authorized_mind_mutation(
pub(crate) fn commit_authorized_mind_mutation(
store: &TypedCommitStore,
store_path: &Path,
authority: EpiphanyMindCommitAuthority,

View file

@ -979,7 +979,7 @@ fn sanitized_git_command() -> Command {
sanitize_git_command(&mut command);
command
}
fn repository_git_command(repo: &Path) -> Result<Command> {
pub(crate) fn repository_git_command(repo: &Path) -> Result<Command> {
let canonical = std::fs::canonicalize(repo)
.with_context(|| format!("failed to canonicalize Git repository {}", repo.display()))?;
let mut command = sanitized_git_command();

View file

@ -25,6 +25,18 @@ hand-maintained JSON registry.
- `gamecult.persona_state.v0`: portable public Persona state.
- `epiphany.work_organ_state.v0`: portable state for lean work organs.
- `epiphany.pipeline.*.v1`: Eureka pipeline state (campaign, target, question,
ruling, cut spec, cut report, verdict, finding, follow-up, resolution), stored
per repo at `.epiphany/pipeline/pipeline.cc`.
Pipeline wire note: each document payload is `[value]`, a one-element
MessagePack array whose element is the named map the schema describes. These
contracts cross to Eureka MCP clients and the voidbot projection. The files are
derived from the Rust value types and checked byte-for-byte by
`pipeline_published_schemas_match_derivation`; text bounds are UTF-8 bytes and
enforced by admission, not by the schema. Evolution is additive: a new named
field with a serde default keeps `epiphany.pipeline.epoch.v1`; a breaking change
bumps the epoch, and the new binary refuses the old store.
Provider request contracts are owned by their provider boundary. The shared
Codex contract lives in `GameCult/CodexConnector`; Epiphany's closed durable

View file

@ -0,0 +1,70 @@
{
"$defs": {
"DocRef": {
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
},
"end_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"$ref": "#/$defs/Short"
},
"start_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"path",
"start_line",
"end_line",
"commit"
],
"type": "object"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"repos": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"slug": {
"$ref": "#/$defs/Short"
},
"target_doc": {
"$ref": "#/$defs/DocRef"
},
"title": {
"$ref": "#/$defs/Short"
},
"working_branch": {
"$ref": "#/$defs/Short"
}
},
"required": [
"slug",
"title",
"repos",
"working_branch",
"target_doc"
],
"title": "PipelineCampaign",
"type": "object"
}

View file

@ -0,0 +1,317 @@
{
"$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": {
"$ref": "#/$defs/Sha"
},
"head": {
"$ref": "#/$defs/Sha"
}
},
"required": [
"base",
"head"
],
"type": "object"
},
"Deviation": {
"properties": {
"what": {
"$ref": "#/$defs/Line"
},
"why": {
"$ref": "#/$defs/Line"
}
},
"required": [
"what",
"why"
],
"type": "object"
},
"Evidence": {
"properties": {
"kind": {
"$ref": "#/$defs/EvidenceKind"
},
"locator": {
"$ref": "#/$defs/Line"
},
"result": {
"$ref": "#/$defs/Line"
}
},
"required": [
"kind",
"locator",
"result"
],
"type": "object"
},
"EvidenceKind": {
"enum": [
"Command",
"Test",
"Mutation",
"Probe",
"SourceRead",
"Capture"
],
"type": "string"
},
"LandedName": {
"properties": {
"kind": {
"$ref": "#/$defs/LandedNameKind"
},
"location": {
"$ref": "#/$defs/CodeLocation"
},
"name": {
"$ref": "#/$defs/Short"
}
},
"required": [
"name",
"kind",
"location"
],
"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.",
"type": "string"
},
"MutationRecord": {
"properties": {
"failed_as_expected": {
"type": "boolean"
},
"mutation": {
"$ref": "#/$defs/Line"
},
"rule": {
"$ref": "#/$defs/Line"
}
},
"required": [
"rule",
"mutation",
"failed_as_expected"
],
"type": "object"
},
"ReportCommit": {
"properties": {
"builds": {
"type": "boolean"
},
"sha": {
"$ref": "#/$defs/Sha"
},
"subject": {
"$ref": "#/$defs/Line"
}
},
"required": [
"sha",
"subject",
"builds"
],
"type": "object"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
},
"StructuralDelta": {
"properties": {
"dependencies_added": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"dependencies_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"formats_added": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"formats_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"lines_added": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"lines_removed": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"targets_added": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"targets_removed": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
}
},
"required": [
"lines_added",
"lines_removed",
"dependencies_added",
"dependencies_removed",
"formats_added",
"formats_removed",
"targets_added",
"targets_removed"
],
"type": "object"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"attempt": {
"$ref": "#/$defs/Short"
},
"branch": {
"$ref": "#/$defs/Short"
},
"campaign": {
"$ref": "#/$defs/Short"
},
"commits": {
"items": {
"$ref": "#/$defs/ReportCommit"
},
"type": "array"
},
"cut_spec": {
"$ref": "#/$defs/Short"
},
"deviations": {
"items": {
"$ref": "#/$defs/Deviation"
},
"type": "array"
},
"forks": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"landed_names": {
"items": {
"$ref": "#/$defs/LandedName"
},
"type": "array"
},
"mutations": {
"items": {
"$ref": "#/$defs/MutationRecord"
},
"type": "array"
},
"range": {
"$ref": "#/$defs/CommitRange"
},
"repo": {
"$ref": "#/$defs/Short"
},
"structural_delta": {
"$ref": "#/$defs/StructuralDelta"
},
"undone": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"verification": {
"items": {
"$ref": "#/$defs/Evidence"
},
"type": "array"
}
},
"required": [
"campaign",
"cut_spec",
"attempt",
"repo",
"branch",
"commits",
"range",
"verification",
"mutations",
"deviations",
"forks",
"structural_delta",
"landed_names",
"undone"
],
"title": "PipelineCutReport",
"type": "object"
}

View file

@ -0,0 +1,337 @@
{
"$defs": {
"AuthorityMap": {
"properties": {
"deletion_line": {
"$ref": "#/$defs/Line"
},
"derived_state": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"forbidden_writers": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"inputs": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"outputs": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"owner": {
"$ref": "#/$defs/Line"
},
"shared_paths": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
}
},
"required": [
"owner",
"inputs",
"outputs",
"derived_state",
"forbidden_writers",
"shared_paths",
"deletion_line"
],
"type": "object"
},
"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"
},
"CutDelete": {
"properties": {
"lines": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"note": {
"$ref": "#/$defs/Line"
},
"path": {
"$ref": "#/$defs/Short"
}
},
"required": [
"path",
"lines",
"note"
],
"type": "object"
},
"CutVerification": {
"properties": {
"builds": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"negative": {
"items": {
"$ref": "#/$defs/NegativeCheck"
},
"type": "array"
},
"operator": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"tests": {
"items": {
"$ref": "#/$defs/VerificationTest"
},
"type": "array"
}
},
"required": [
"builds",
"tests",
"negative",
"operator"
],
"type": "object"
},
"FileChange": {
"properties": {
"change": {
"$ref": "#/$defs/Line"
},
"location": {
"$ref": "#/$defs/CodeLocation"
}
},
"required": [
"location",
"change"
],
"type": "object"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"NegativeCheck": {
"properties": {
"pattern": {
"$ref": "#/$defs/Short"
},
"scope": {
"$ref": "#/$defs/Line"
}
},
"required": [
"pattern",
"scope"
],
"type": "object"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
},
"SubtractionEstimate": {
"properties": {
"added": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"lines_added": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"lines_removed": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"removed": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
}
},
"required": [
"lines_removed",
"lines_added",
"removed",
"added"
],
"type": "object"
},
"VerificationTest": {
"properties": {
"name": {
"$ref": "#/$defs/Short"
},
"pins": {
"$ref": "#/$defs/Line"
}
},
"required": [
"name",
"pins"
],
"type": "object"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"adds": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"authority_map": {
"anyOf": [
{
"$ref": "#/$defs/AuthorityMap"
},
{
"type": "null"
}
]
},
"base": {
"$ref": "#/$defs/Sha"
},
"branch": {
"$ref": "#/$defs/Short"
},
"campaign": {
"$ref": "#/$defs/Short"
},
"cut": {
"$ref": "#/$defs/Short"
},
"deletes": {
"items": {
"$ref": "#/$defs/CutDelete"
},
"type": "array"
},
"depends_on": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"file_changes": {
"items": {
"$ref": "#/$defs/FileChange"
},
"type": "array"
},
"first": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"keeps_moves": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"questions": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"repo": {
"$ref": "#/$defs/Short"
},
"revision": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"rulings": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"subtraction_estimate": {
"$ref": "#/$defs/SubtractionEstimate"
},
"title": {
"$ref": "#/$defs/Short"
},
"verification": {
"$ref": "#/$defs/CutVerification"
}
},
"required": [
"campaign",
"cut",
"revision",
"title",
"repo",
"branch",
"base",
"depends_on",
"first",
"deletes",
"keeps_moves",
"adds",
"file_changes",
"verification",
"subtraction_estimate",
"rulings",
"questions"
],
"title": "PipelineCutSpec",
"type": "object"
}

View file

@ -0,0 +1,217 @@
{
"$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": {
"$ref": "#/$defs/Sha"
},
"head": {
"$ref": "#/$defs/Sha"
}
},
"required": [
"base",
"head"
],
"type": "object"
},
"Evidence": {
"properties": {
"kind": {
"$ref": "#/$defs/EvidenceKind"
},
"locator": {
"$ref": "#/$defs/Line"
},
"result": {
"$ref": "#/$defs/Line"
}
},
"required": [
"kind",
"locator",
"result"
],
"type": "object"
},
"EvidenceKind": {
"enum": [
"Command",
"Test",
"Mutation",
"Probe",
"SourceRead",
"Capture"
],
"type": "string"
},
"FindingConfidence": {
"enum": [
"Confirmed",
"Plausible"
],
"type": "string"
},
"FindingSeverity": {
"enum": [
"Blocker",
"High",
"Medium",
"Low"
],
"type": "string"
},
"ForeignRef": {
"description": "A citation of a document in another repo's pipeline store (D6).",
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
},
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
},
"payload_sha256": {
"type": "string"
},
"repo": {
"$ref": "#/$defs/Short"
}
},
"required": [
"repo",
"commit",
"kind",
"id",
"payload_sha256"
],
"type": "object"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution"
],
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
},
"claim": {
"$ref": "#/$defs/Line"
},
"confidence": {
"$ref": "#/$defs/FindingConfidence"
},
"evidence": {
"items": {
"$ref": "#/$defs/Evidence"
},
"type": "array"
},
"failure_scenario": {
"$ref": "#/$defs/Para"
},
"invariants": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"label": {
"$ref": "#/$defs/Short"
},
"locations": {
"items": {
"$ref": "#/$defs/CodeLocation"
},
"type": "array"
},
"precedents": {
"items": {
"$ref": "#/$defs/ForeignRef"
},
"type": "array"
},
"range": {
"$ref": "#/$defs/CommitRange"
},
"severity": {
"$ref": "#/$defs/FindingSeverity"
},
"verdict": {
"$ref": "#/$defs/Short"
}
},
"required": [
"campaign",
"verdict",
"label",
"range",
"confidence",
"severity",
"claim",
"invariants",
"locations",
"failure_scenario",
"evidence",
"precedents"
],
"title": "PipelineFinding",
"type": "object"
}

View file

@ -0,0 +1,110 @@
{
"$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"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution"
],
"type": "string"
},
"PipelineRef": {
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
},
"item": {
"$ref": "#/$defs/Line"
},
"label": {
"$ref": "#/$defs/Short"
},
"locations": {
"items": {
"$ref": "#/$defs/CodeLocation"
},
"type": "array"
},
"owner": {
"$ref": "#/$defs/Short"
},
"repo": {
"$ref": "#/$defs/Short"
},
"source": {
"$ref": "#/$defs/PipelineRef"
},
"why_it_can_wait": {
"$ref": "#/$defs/Line"
}
},
"required": [
"campaign",
"label",
"source",
"repo",
"locations",
"item",
"why_it_can_wait",
"owner"
],
"title": "PipelineFollowUp",
"type": "object"
}

View file

@ -0,0 +1,117 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution"
],
"type": "string"
},
"PipelineRef": {
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"QuestionOption": {
"properties": {
"label": {
"$ref": "#/$defs/Short"
},
"text": {
"$ref": "#/$defs/Line"
}
},
"required": [
"label",
"text"
],
"type": "object"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"asked_on": {
"$ref": "#/$defs/Date"
},
"campaign": {
"$ref": "#/$defs/Short"
},
"depends": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"label": {
"$ref": "#/$defs/Short"
},
"options": {
"items": {
"$ref": "#/$defs/QuestionOption"
},
"type": "array"
},
"question": {
"$ref": "#/$defs/Para"
},
"raised_in": {
"anyOf": [
{
"$ref": "#/$defs/PipelineRef"
},
{
"type": "null"
}
]
},
"recommended": {
"$ref": "#/$defs/Short"
}
},
"required": [
"campaign",
"label",
"question",
"options",
"recommended",
"depends",
"asked_on"
],
"title": "PipelineQuestion",
"type": "object"
}

View file

@ -0,0 +1,198 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution"
],
"type": "string"
},
"PipelineRef": {
"properties": {
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
}
},
"required": [
"kind",
"id"
],
"type": "object"
},
"ResolutionOutcome": {
"oneOf": [
{
"additionalProperties": false,
"properties": {
"Superseded": {
"properties": {
"by": {
"$ref": "#/$defs/Short"
}
},
"required": [
"by"
],
"type": "object"
}
},
"required": [
"Superseded"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Answered": {
"properties": {
"by": {
"$ref": "#/$defs/Short"
}
},
"required": [
"by"
],
"type": "object"
}
},
"required": [
"Answered"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Fixed": {
"properties": {
"by": {
"$ref": "#/$defs/Short"
}
},
"required": [
"by"
],
"type": "object"
}
},
"required": [
"Fixed"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Deferred": {
"properties": {
"to": {
"$ref": "#/$defs/Short"
}
},
"required": [
"to"
],
"type": "object"
}
},
"required": [
"Deferred"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Recorded": {
"properties": {
"reason": {
"$ref": "#/$defs/Line"
}
},
"required": [
"reason"
],
"type": "object"
}
},
"required": [
"Recorded"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"Withdrawn": {
"properties": {
"reason": {
"$ref": "#/$defs/Line"
}
},
"required": [
"reason"
],
"type": "object"
}
},
"required": [
"Withdrawn"
],
"type": "object"
}
]
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"outcome": {
"$ref": "#/$defs/ResolutionOutcome"
},
"rationale": {
"$ref": "#/$defs/Para"
},
"resolved_on": {
"$ref": "#/$defs/Date"
},
"subject": {
"$ref": "#/$defs/PipelineRef"
}
},
"required": [
"subject",
"outcome",
"rationale",
"resolved_on"
],
"title": "PipelineResolution",
"type": "object"
}

View file

@ -0,0 +1,124 @@
{
"$defs": {
"Date": {
"description": "A calendar date, `YYYY-MM-DD`.",
"type": "string"
},
"ForeignRef": {
"description": "A citation of a document in another repo's pipeline store (D6).",
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
},
"id": {
"$ref": "#/$defs/Short"
},
"kind": {
"$ref": "#/$defs/PipelineKind"
},
"payload_sha256": {
"type": "string"
},
"repo": {
"$ref": "#/$defs/Short"
}
},
"required": [
"repo",
"commit",
"kind",
"id",
"payload_sha256"
],
"type": "object"
},
"Para": {
"description": "Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.",
"type": "string"
},
"PipelineKind": {
"enum": [
"Campaign",
"Target",
"Question",
"Ruling",
"CutSpec",
"CutReport",
"Verdict",
"Finding",
"FollowUp",
"Resolution"
],
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"answers": {
"anyOf": [
{
"$ref": "#/$defs/Short"
},
{
"type": "null"
}
]
},
"campaign": {
"$ref": "#/$defs/Short"
},
"choice": {
"anyOf": [
{
"$ref": "#/$defs/Short"
},
{
"type": "null"
}
]
},
"label": {
"$ref": "#/$defs/Short"
},
"operator_quote": {
"anyOf": [
{
"$ref": "#/$defs/Para"
},
{
"type": "null"
}
]
},
"precedents": {
"items": {
"$ref": "#/$defs/ForeignRef"
},
"type": "array"
},
"ruled_on": {
"$ref": "#/$defs/Date"
},
"ruling": {
"$ref": "#/$defs/Para"
}
},
"required": [
"campaign",
"label",
"ruling",
"ruled_on",
"precedents"
],
"title": "PipelineRuling",
"type": "object"
}

View file

@ -0,0 +1,101 @@
{
"$defs": {
"DocRef": {
"properties": {
"commit": {
"$ref": "#/$defs/Sha"
},
"end_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
},
"path": {
"$ref": "#/$defs/Short"
},
"start_line": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"path",
"start_line",
"end_line",
"commit"
],
"type": "object"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
},
"TargetInvariant": {
"properties": {
"label": {
"$ref": "#/$defs/Short"
},
"statement": {
"$ref": "#/$defs/Line"
}
},
"required": [
"label",
"statement"
],
"type": "object"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
},
"canonical_implementations": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"doc": {
"$ref": "#/$defs/DocRef"
},
"invariants": {
"items": {
"$ref": "#/$defs/TargetInvariant"
},
"type": "array"
},
"not_in_scope": {
"items": {
"$ref": "#/$defs/Line"
},
"type": "array"
},
"revision": {
"format": "uint32",
"minimum": 0,
"type": "integer"
}
},
"required": [
"campaign",
"revision",
"invariants",
"not_in_scope",
"canonical_implementations",
"doc"
],
"title": "PipelineTarget",
"type": "object"
}

View file

@ -0,0 +1,129 @@
{
"$defs": {
"ClaimOutcome": {
"enum": [
"Holds",
"Falsified",
"Unproven"
],
"type": "string"
},
"CommitRange": {
"properties": {
"base": {
"$ref": "#/$defs/Sha"
},
"head": {
"$ref": "#/$defs/Sha"
}
},
"required": [
"base",
"head"
],
"type": "object"
},
"Evidence": {
"properties": {
"kind": {
"$ref": "#/$defs/EvidenceKind"
},
"locator": {
"$ref": "#/$defs/Line"
},
"result": {
"$ref": "#/$defs/Line"
}
},
"required": [
"kind",
"locator",
"result"
],
"type": "object"
},
"EvidenceKind": {
"enum": [
"Command",
"Test",
"Mutation",
"Probe",
"SourceRead",
"Capture"
],
"type": "string"
},
"Line": {
"description": "Text of at most 1,000 UTF-8 bytes.",
"type": "string"
},
"Sha": {
"description": "A git commit id: 7-40 lowercase hex characters.",
"type": "string"
},
"Short": {
"description": "Text of at most 200 UTF-8 bytes.",
"type": "string"
},
"VerdictClaim": {
"properties": {
"claim": {
"$ref": "#/$defs/Line"
},
"evidence": {
"items": {
"$ref": "#/$defs/Evidence"
},
"type": "array"
},
"findings": {
"items": {
"$ref": "#/$defs/Short"
},
"type": "array"
},
"outcome": {
"$ref": "#/$defs/ClaimOutcome"
}
},
"required": [
"claim",
"outcome",
"evidence",
"findings"
],
"type": "object"
}
},
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v1.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"campaign": {
"$ref": "#/$defs/Short"
},
"claims": {
"items": {
"$ref": "#/$defs/VerdictClaim"
},
"type": "array"
},
"cut_report": {
"$ref": "#/$defs/Short"
},
"pass": {
"$ref": "#/$defs/Short"
},
"range": {
"$ref": "#/$defs/CommitRange"
}
},
"required": [
"campaign",
"cut_report",
"pass",
"range",
"claims"
],
"title": "PipelineVerdict",
"type": "object"
}

View file

@ -22,6 +22,116 @@
"documentType": "epiphany.work_organ_state.v0",
"title": "Epiphany Work Organ State v0",
"path": "epiphany.work_organ_state.v0.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.campaign.v1",
"documentType": "epiphany.pipeline.campaign.v1",
"title": "Epiphany Pipeline Campaign v1",
"path": "epiphany.pipeline.campaign.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.target.v1",
"documentType": "epiphany.pipeline.target.v1",
"title": "Epiphany Pipeline Target v1",
"path": "epiphany.pipeline.target.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.question.v1",
"documentType": "epiphany.pipeline.question.v1",
"title": "Epiphany Pipeline Question v1",
"path": "epiphany.pipeline.question.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.ruling.v1",
"documentType": "epiphany.pipeline.ruling.v1",
"title": "Epiphany Pipeline Ruling v1",
"path": "epiphany.pipeline.ruling.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.cut_spec.v1",
"documentType": "epiphany.pipeline.cut_spec.v1",
"title": "Epiphany Pipeline CutSpec v1",
"path": "epiphany.pipeline.cut_spec.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.cut_report.v1",
"documentType": "epiphany.pipeline.cut_report.v1",
"title": "Epiphany Pipeline CutReport v1",
"path": "epiphany.pipeline.cut_report.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.verdict.v1",
"documentType": "epiphany.pipeline.verdict.v1",
"title": "Epiphany Pipeline Verdict v1",
"path": "epiphany.pipeline.verdict.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.finding.v1",
"documentType": "epiphany.pipeline.finding.v1",
"title": "Epiphany Pipeline Finding v1",
"path": "epiphany.pipeline.finding.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.follow_up.v1",
"documentType": "epiphany.pipeline.follow_up.v1",
"title": "Epiphany Pipeline FollowUp v1",
"path": "epiphany.pipeline.follow_up.v1.schema.json"
},
{
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v1.schema.json",
"kind": "document_payload",
"wireContracts": [
"cultnet.schema.v0"
],
"schemaVersion": "epiphany.pipeline.resolution.v1",
"documentType": "epiphany.pipeline.resolution.v1",
"title": "Epiphany Pipeline Resolution v1",
"path": "epiphany.pipeline.resolution.v1.schema.json"
}
]
}