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:
parent
a1473c452b
commit
ad18c385dc
19 changed files with 3231 additions and 3 deletions
63
Cargo.lock
generated
63
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
547
epiphany-core/src/pipeline_documents.rs
Normal file
547
epiphany-core/src/pipeline_documents.rs
Normal 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(())
|
||||
}
|
||||
771
epiphany-core/src/pipeline_store.rs
Normal file
771
epiphany-core/src/pipeline_store.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
70
schemas/cultnet/epiphany.pipeline.campaign.v1.schema.json
Normal file
70
schemas/cultnet/epiphany.pipeline.campaign.v1.schema.json
Normal 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"
|
||||
}
|
||||
317
schemas/cultnet/epiphany.pipeline.cut_report.v1.schema.json
Normal file
317
schemas/cultnet/epiphany.pipeline.cut_report.v1.schema.json
Normal 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"
|
||||
}
|
||||
337
schemas/cultnet/epiphany.pipeline.cut_spec.v1.schema.json
Normal file
337
schemas/cultnet/epiphany.pipeline.cut_spec.v1.schema.json
Normal 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"
|
||||
}
|
||||
217
schemas/cultnet/epiphany.pipeline.finding.v1.schema.json
Normal file
217
schemas/cultnet/epiphany.pipeline.finding.v1.schema.json
Normal 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"
|
||||
}
|
||||
110
schemas/cultnet/epiphany.pipeline.follow_up.v1.schema.json
Normal file
110
schemas/cultnet/epiphany.pipeline.follow_up.v1.schema.json
Normal 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"
|
||||
}
|
||||
117
schemas/cultnet/epiphany.pipeline.question.v1.schema.json
Normal file
117
schemas/cultnet/epiphany.pipeline.question.v1.schema.json
Normal 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"
|
||||
}
|
||||
198
schemas/cultnet/epiphany.pipeline.resolution.v1.schema.json
Normal file
198
schemas/cultnet/epiphany.pipeline.resolution.v1.schema.json
Normal 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"
|
||||
}
|
||||
124
schemas/cultnet/epiphany.pipeline.ruling.v1.schema.json
Normal file
124
schemas/cultnet/epiphany.pipeline.ruling.v1.schema.json
Normal 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"
|
||||
}
|
||||
101
schemas/cultnet/epiphany.pipeline.target.v1.schema.json
Normal file
101
schemas/cultnet/epiphany.pipeline.target.v1.schema.json
Normal 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"
|
||||
}
|
||||
129
schemas/cultnet/epiphany.pipeline.verdict.v1.schema.json
Normal file
129
schemas/cultnet/epiphany.pipeline.verdict.v1.schema.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue