Cut RS-L: the leaf gains Title, moves to epoch v2, and two grammar doors
Q-RS1 ruled B: a new bounded text type, Title (1-200 bytes, never empty), replaces Short on all four leaf titles -- campaign, cut spec, and the two new fields on question and ruling. The epoch moves anyway in this cut, so tightening the two existing titles now avoids a second epoch bump later just to make a title required. PIPELINE_SCHEMA_EPOCH and all thirteen type ids move to .v2. The thirteen schema files are renamed and regenerated, and index.json/README.md follow. Two public doors join Slug::validate_slug on the same pattern: OrgRepo::validate_org_repo and Label::validate_label, each delegating to the grammar its own document field is already held to. RS-3 will read the organ's repo and cut aliases through these. Tests: the_org_repo_and_label_doors_are_the_grammar and question_and_ruling_carry_a_title (37 total, up from 35). Mutation suite tools/eureka-readside-leaf-mutations.psd1 (L1/L1L, L2/L2L, L3/L3L) verified killed against M0. No other consumer in this workspace depends on epiphany-pipeline as a Cargo dependency; epiphany-core's runtime_spine stand-in hardcodes a pipeline-shaped type id deliberately (RS-L's negative grep excludes it) and is unaffected since its test only checks the "epiphany.pipeline." prefix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c68db64270
commit
d74e36aa06
17 changed files with 404 additions and 108 deletions
|
|
@ -190,9 +190,22 @@ fn within(limit: usize) -> impl Fn(&str, &str) -> Result<(), PipelineRefusal> {
|
|||
move |field: &str, value: &str| bound(field, limit, value.len())
|
||||
}
|
||||
|
||||
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.is_empty() {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
within(200)(field, value)
|
||||
}
|
||||
|
||||
bounded_text! {
|
||||
/// Text of at most 200 UTF-8 bytes.
|
||||
Short = 200, |field, value| within(200)(field, value);
|
||||
/// A title: 1 to 200 UTF-8 bytes, never empty. All four leaf titles
|
||||
/// (campaign, cut spec, question and ruling) carry this type, ruled B
|
||||
/// under Q-RS1: the epoch moves in this cut regardless, so tightening the
|
||||
/// two titles that were `Short` costs nothing extra and avoids a second
|
||||
/// epoch bump later just to make a title required.
|
||||
Title = 200, title_text;
|
||||
/// Text of at most 1,000 UTF-8 bytes.
|
||||
Line = 1000, |field, value| within(1000)(field, value);
|
||||
/// Text of at most 4,000 UTF-8 bytes; longer narrative is cited by `DocRef`.
|
||||
|
|
@ -226,6 +239,30 @@ impl Slug {
|
|||
}
|
||||
}
|
||||
|
||||
impl OrgRepo {
|
||||
/// The public door onto the `org_repo` grammar, for a caller outside this
|
||||
/// crate that holds a bare `OrgRepo` (the organ's `repo` alias, RS-3) and
|
||||
/// no way to reach `Bounded`. On the pattern of `Slug::validate_slug`: it
|
||||
/// delegates to `org_repo_text` through `Bounded::validate`, so the
|
||||
/// grammar keeps one owner and a declared name checked this way refuses
|
||||
/// exactly what a document's own `OrgRepo` field refuses.
|
||||
pub fn validate_org_repo(&self) -> Result<(), PipelineRefusal> {
|
||||
Bounded::validate(self, "org_repo")
|
||||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
/// The public door onto the label grammar, for a caller outside this
|
||||
/// crate that holds a bare `Label` (the organ's `cut` alias, RS-3) and no
|
||||
/// way to reach `Bounded`. On the pattern of `Slug::validate_slug`: it
|
||||
/// delegates to `label_text` through `Bounded::validate`, so the grammar
|
||||
/// keeps one owner and a declared name checked this way refuses exactly
|
||||
/// what a document's own `Label` field refuses.
|
||||
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
|
||||
Bounded::validate(self, "label")
|
||||
}
|
||||
}
|
||||
|
||||
/// A calendar date, `YYYY-MM-DD`.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
|
|
@ -341,21 +378,21 @@ value_types! {
|
|||
promise: Option<Label>, mutations: Vec<Label>[8],
|
||||
}
|
||||
|
||||
pub struct PipelineCampaign { slug: Slug, title: Short, repos: Vec<OrgRepo>[8], working_branch: Short, target_doc: DocRef }
|
||||
pub struct PipelineCampaign { slug: Slug, title: Title, repos: Vec<OrgRepo>[8], working_branch: Short, target_doc: DocRef }
|
||||
pub struct PipelineTarget {
|
||||
campaign: Slug, revision: u32, invariants: Vec<TargetInvariant>[32], not_in_scope: Vec<Line>[32],
|
||||
canonical_implementations: Vec<Line>[16], doc: DocRef,
|
||||
}
|
||||
pub struct PipelineQuestion {
|
||||
campaign: Slug, label: Label, question: Para, options: Vec<QuestionOption>[8], recommended: Label,
|
||||
campaign: Slug, label: Label, title: Title, question: Para, options: Vec<QuestionOption>[8], recommended: Label,
|
||||
depends: Vec<Line>[8], raised_in: Option<PipelineRef>, asked_on: Date,
|
||||
}
|
||||
pub struct PipelineRuling {
|
||||
campaign: Slug, label: Label, answers: Option<Short>, choice: Option<Label>, ruling: Para,
|
||||
campaign: Slug, label: Label, title: Title, answers: Option<Short>, choice: Option<Label>, ruling: Para,
|
||||
operator_quote: Option<Para>, ruled_on: Date, precedents: Vec<ForeignRef>[8], authority: RulingAuthority,
|
||||
}
|
||||
pub struct PipelineCutSpec {
|
||||
campaign: Slug, cut: Label, revision: u32, title: Short, repo: OrgRepo, branch: Short, base: Sha,
|
||||
campaign: Slug, cut: Label, revision: u32, title: Title, repo: OrgRepo, branch: Short, base: Sha,
|
||||
depends_on: Vec<Short>[8], first: Vec<Line>[16], deletes: Vec<CutDelete>[64], keeps_moves: Vec<Line>[64],
|
||||
adds: Vec<Line>[64], file_changes: Vec<FileChange>[256], authority_map: Option<AuthorityMap>,
|
||||
verification: CutVerification, estimate: StructuralDelta,
|
||||
|
|
@ -595,31 +632,31 @@ pub(crate) struct ForeignDocument {
|
|||
|
||||
pipeline_kinds! {
|
||||
Campaign(PipelineCampaign) => EpiphanyPipelineCampaignDocument, "campaign",
|
||||
"epiphany.pipeline.campaign.v1", "EpiphanyPipelineCampaignDocument";
|
||||
"epiphany.pipeline.campaign.v2", "EpiphanyPipelineCampaignDocument";
|
||||
Target(PipelineTarget) => EpiphanyPipelineTargetDocument, "target",
|
||||
"epiphany.pipeline.target.v1", "EpiphanyPipelineTargetDocument";
|
||||
"epiphany.pipeline.target.v2", "EpiphanyPipelineTargetDocument";
|
||||
Question(PipelineQuestion) => EpiphanyPipelineQuestionDocument, "question",
|
||||
"epiphany.pipeline.question.v1", "EpiphanyPipelineQuestionDocument";
|
||||
"epiphany.pipeline.question.v2", "EpiphanyPipelineQuestionDocument";
|
||||
Ruling(PipelineRuling) => EpiphanyPipelineRulingDocument, "ruling",
|
||||
"epiphany.pipeline.ruling.v1", "EpiphanyPipelineRulingDocument";
|
||||
"epiphany.pipeline.ruling.v2", "EpiphanyPipelineRulingDocument";
|
||||
CutSpec(PipelineCutSpec) => EpiphanyPipelineCutSpecDocument, "cut_spec",
|
||||
"epiphany.pipeline.cut_spec.v1", "EpiphanyPipelineCutSpecDocument";
|
||||
"epiphany.pipeline.cut_spec.v2", "EpiphanyPipelineCutSpecDocument";
|
||||
CutReport(PipelineCutReport) => EpiphanyPipelineCutReportDocument, "cut_report",
|
||||
"epiphany.pipeline.cut_report.v1", "EpiphanyPipelineCutReportDocument";
|
||||
"epiphany.pipeline.cut_report.v2", "EpiphanyPipelineCutReportDocument";
|
||||
Verdict(PipelineVerdict) => EpiphanyPipelineVerdictDocument, "verdict",
|
||||
"epiphany.pipeline.verdict.v1", "EpiphanyPipelineVerdictDocument";
|
||||
"epiphany.pipeline.verdict.v2", "EpiphanyPipelineVerdictDocument";
|
||||
Finding(PipelineFinding) => EpiphanyPipelineFindingDocument, "finding",
|
||||
"epiphany.pipeline.finding.v1", "EpiphanyPipelineFindingDocument";
|
||||
"epiphany.pipeline.finding.v2", "EpiphanyPipelineFindingDocument";
|
||||
FollowUp(PipelineFollowUp) => EpiphanyPipelineFollowUpDocument, "follow_up",
|
||||
"epiphany.pipeline.follow_up.v1", "EpiphanyPipelineFollowUpDocument";
|
||||
"epiphany.pipeline.follow_up.v2", "EpiphanyPipelineFollowUpDocument";
|
||||
Resolution(PipelineResolution) => EpiphanyPipelineResolutionDocument, "resolution",
|
||||
"epiphany.pipeline.resolution.v1", "EpiphanyPipelineResolutionDocument";
|
||||
"epiphany.pipeline.resolution.v2", "EpiphanyPipelineResolutionDocument";
|
||||
Instance(PipelineInstance) => EpiphanyPipelineInstanceDocument, "instance",
|
||||
"epiphany.pipeline.instance.v1", "EpiphanyPipelineInstanceDocument";
|
||||
"epiphany.pipeline.instance.v2", "EpiphanyPipelineInstanceDocument";
|
||||
Stewardship(PipelineStewardship) => EpiphanyPipelineStewardshipDocument, "stewardship",
|
||||
"epiphany.pipeline.stewardship.v1", "EpiphanyPipelineStewardshipDocument";
|
||||
"epiphany.pipeline.stewardship.v2", "EpiphanyPipelineStewardshipDocument";
|
||||
HandOff(PipelineHandOff) => EpiphanyPipelineHandOffDocument, "hand_off",
|
||||
"epiphany.pipeline.hand_off.v1", "EpiphanyPipelineHandOffDocument";
|
||||
"epiphany.pipeline.hand_off.v2", "EpiphanyPipelineHandOffDocument";
|
||||
}
|
||||
|
||||
/// Parses a full document id: the grammar read backwards. A key and an id are
|
||||
|
|
@ -717,7 +754,7 @@ const LOCAL_MAX: usize = 64;
|
|||
/// the epoch. A breaking change bumps it, so that a store written at the old
|
||||
/// one can be refused by whoever opens it; this crate owns no store and
|
||||
/// refuses none.
|
||||
pub const PIPELINE_SCHEMA_EPOCH: &str = "epiphany.pipeline.epoch.v1";
|
||||
pub const PIPELINE_SCHEMA_EPOCH: &str = "epiphany.pipeline.epoch.v2";
|
||||
|
||||
/// Composes and validates a local: every part is a `Label`, and the join is
|
||||
/// bounded whole. Both rules live here because this is the only way a local is
|
||||
|
|
@ -848,6 +885,10 @@ mod tests {
|
|||
value.into()
|
||||
}
|
||||
|
||||
fn t(value: &str) -> Title {
|
||||
value.into()
|
||||
}
|
||||
|
||||
fn l(value: &str) -> Label {
|
||||
value.into()
|
||||
}
|
||||
|
|
@ -891,7 +932,7 @@ mod tests {
|
|||
let branch = || s("codex/eureka-pipeline-state");
|
||||
vec![
|
||||
(D::Campaign(PipelineCampaign {
|
||||
slug: slug(CAMPAIGN), title: s("Eureka pipeline state"), repos: vec![repo()],
|
||||
slug: slug(CAMPAIGN), title: t("Eureka pipeline state"), repos: vec![repo()],
|
||||
working_branch: branch(), target_doc: doc_ref(),
|
||||
}), format!("{CAMPAIGN}:campaign:self")),
|
||||
(D::Target(PipelineTarget {
|
||||
|
|
@ -900,7 +941,7 @@ mod tests {
|
|||
not_in_scope: vec!["Eve browsing".into()], canonical_implementations: vec!["CultLib".into()], doc: doc_ref(),
|
||||
}), format!("{CAMPAIGN}:target:r2")),
|
||||
(D::Question(PipelineQuestion {
|
||||
campaign: slug(CAMPAIGN), label: l("Q1"), question: "Who owns the state?".into(),
|
||||
campaign: slug(CAMPAIGN), label: l("Q1"), title: t("Who owns the state?"), question: "Who owns the state?".into(),
|
||||
options: vec![
|
||||
QuestionOption { label: l("A"), text: "an instance".into() },
|
||||
QuestionOption { label: l("B"), text: "a repo".into() },
|
||||
|
|
@ -910,7 +951,7 @@ mod tests {
|
|||
asked_on: date(),
|
||||
}), format!("{CAMPAIGN}:question:Q1")),
|
||||
(D::Ruling(PipelineRuling {
|
||||
campaign: slug(CAMPAIGN), label: l("R8"), answers: Some(id("question", "Q1")), choice: Some(l("A")),
|
||||
campaign: slug(CAMPAIGN), label: l("R8"), title: t("An instance owns its mind"), answers: Some(id("question", "Q1")), choice: Some(l("A")),
|
||||
ruling: "An instance owns its mind.".into(), operator_quote: Some("all recommendations, go ahead".into()),
|
||||
ruled_on: date(),
|
||||
precedents: vec![ForeignRef {
|
||||
|
|
@ -920,7 +961,7 @@ mod tests {
|
|||
authority: RulingAuthority::Operator,
|
||||
}), format!("{CAMPAIGN}:ruling:R8")),
|
||||
(D::CutSpec(PipelineCutSpec {
|
||||
campaign: slug(CAMPAIGN), cut: l("3a"), revision: 1, title: s("Pipeline documents"), repo: repo(),
|
||||
campaign: slug(CAMPAIGN), cut: l("3a"), revision: 1, title: t("Pipeline documents"), repo: repo(),
|
||||
branch: branch(), base: sha(), depends_on: vec![s("2")], first: vec!["Read the spec.".into()],
|
||||
deletes: vec![CutDelete { path: s("old.rs"), lines: 3, note: "dead".into() }],
|
||||
keeps_moves: vec!["commit owner".into()], adds: vec!["pipeline_documents.rs".into()],
|
||||
|
|
@ -1033,6 +1074,16 @@ mod tests {
|
|||
samples().remove(0).0
|
||||
}
|
||||
|
||||
fn question_sample() -> PipelineQuestion {
|
||||
let PipelineDocument::Question(question) = samples().remove(2).0 else { unreachable!() };
|
||||
question
|
||||
}
|
||||
|
||||
fn ruling_sample() -> PipelineRuling {
|
||||
let PipelineDocument::Ruling(ruling) = samples().remove(3).0 else { unreachable!() };
|
||||
ruling
|
||||
}
|
||||
|
||||
fn report_sample() -> PipelineCutReport {
|
||||
let PipelineDocument::CutReport(report) = samples().remove(5).0 else { unreachable!() };
|
||||
report
|
||||
|
|
@ -1107,14 +1158,14 @@ mod tests {
|
|||
#[test]
|
||||
fn bounds_refuse_in_utf8_bytes() {
|
||||
let PipelineDocument::Campaign(mut campaign) = campaign_sample() else { unreachable!() };
|
||||
campaign.title = Short("é".repeat(100));
|
||||
campaign.title = Title("é".repeat(100));
|
||||
assert_eq!(PipelineDocument::Campaign(campaign.clone()).validate(), Ok(()));
|
||||
campaign.title = Short(format!("{}a", "é".repeat(100)));
|
||||
campaign.title = Title(format!("{}a", "é".repeat(100)));
|
||||
assert_eq!(
|
||||
PipelineDocument::Campaign(campaign.clone()).validate(),
|
||||
Err(PipelineRefusal::FieldBound { field: "campaign.title".into(), limit: 200, actual: 201 })
|
||||
);
|
||||
campaign.title = s("ok");
|
||||
campaign.title = t("ok");
|
||||
campaign.repos = vec![OrgRepo("GameCult/Epiphany".into()); 9];
|
||||
assert_eq!(
|
||||
PipelineDocument::Campaign(campaign).validate(),
|
||||
|
|
@ -2360,7 +2411,7 @@ mod tests {
|
|||
"wireContracts": ["cultnet.schema.v0"],
|
||||
"schemaVersion": kind.type_id(),
|
||||
"documentType": kind.type_id(),
|
||||
"title": format!("Epiphany Pipeline {kind:?} v1"),
|
||||
"title": format!("Epiphany Pipeline {kind:?} v2"),
|
||||
"path": file,
|
||||
});
|
||||
assert!(
|
||||
|
|
@ -2517,4 +2568,84 @@ mod tests {
|
|||
// non-alphanumeric character that happens to be caught by coincidence.
|
||||
refused("a\u{0}b", "NUL is not a label byte");
|
||||
}
|
||||
|
||||
/// The two doors added beside `Slug::validate_slug` (RS-L), on its
|
||||
/// pattern: each delegates to the grammar its own document field already
|
||||
/// carries, so a declared name checked through the door and a field of
|
||||
/// the same type refuse the same inputs. `OrgRepo` is the `repo` alias's
|
||||
/// domain and `Label` is the `cut` alias's (RS-3's vocabulary table).
|
||||
#[test]
|
||||
fn the_org_repo_and_label_doors_are_the_grammar() {
|
||||
assert_eq!(OrgRepo::from("GameCult/Epiphany").validate_org_repo(), Ok(()));
|
||||
let repo_refused = |value: &str, why: &str| {
|
||||
let result = OrgRepo::from(value).validate_org_repo();
|
||||
assert!(
|
||||
matches!(&result, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "org_repo"),
|
||||
"{why}: {value:?} was not refused, got {result:?}"
|
||||
);
|
||||
};
|
||||
repo_refused("GameCult", "no slash at all");
|
||||
repo_refused("/Repo", "an empty org before the slash");
|
||||
repo_refused("GameCult/", "an empty repo after the slash");
|
||||
repo_refused("a/b/c", "a second slash makes the repo half ambiguous");
|
||||
let org_at_201 = format!("{}/{}", "a".repeat(99), "a".repeat(101));
|
||||
assert_eq!(org_at_201.len(), 201, "one byte past the 200-byte org_repo bound");
|
||||
repo_refused(&org_at_201, "201 bytes overall is refused");
|
||||
|
||||
assert_eq!(Label::from("cut-10").validate_label(), Ok(()));
|
||||
let label_refused = |value: &str, why: &str| {
|
||||
let result = Label::from(value).validate_label();
|
||||
assert!(
|
||||
matches!(&result, Err(PipelineRefusal::InvalidFormat { field, .. }) if field == "label"),
|
||||
"{why}: {value:?} was not refused, got {result:?}"
|
||||
);
|
||||
};
|
||||
label_refused("", "the empty string has no label");
|
||||
label_refused("a.b", "a dot is not a label byte");
|
||||
label_refused(&"a".repeat(65), "65 bytes is past the 64-byte label bound");
|
||||
label_refused("\u{e9}", "a non-ascii byte is not a label byte");
|
||||
}
|
||||
|
||||
/// Q-RS1, ruled B: `question` and `ruling` gain a required `title`, the
|
||||
/// new `Title` type shared with the two titles that move off `Short`
|
||||
/// (campaign, cut spec). Each kind round-trips through `prepare`/`decode`
|
||||
/// with its title intact, an empty title is refused for both, and the
|
||||
/// bound is pinned at its edge: 200 bytes accepted, 201 refused (L3 dies
|
||||
/// on the removal of the non-empty check, not on the length bound, since
|
||||
/// `Title`'s length bound is shared with every other `bounded_text!`
|
||||
/// member and already covered by `bounds_refuse_in_utf8_bytes`).
|
||||
#[test]
|
||||
fn question_and_ruling_carry_a_title() -> Result<()> {
|
||||
let cache = schema_cache()?;
|
||||
|
||||
let mut question = question_sample();
|
||||
question.title = t("Who owns the state?");
|
||||
let envelope = PipelineDocument::Question(question.clone()).prepare(&cache)?;
|
||||
assert_eq!(PipelineDocument::decode(&envelope)?, PipelineDocument::Question(question.clone()));
|
||||
|
||||
let mut ruling = ruling_sample();
|
||||
ruling.title = t("An instance owns its mind");
|
||||
let envelope = PipelineDocument::Ruling(ruling.clone()).prepare(&cache)?;
|
||||
assert_eq!(PipelineDocument::decode(&envelope)?, PipelineDocument::Ruling(ruling.clone()));
|
||||
|
||||
question.title = Title(String::new());
|
||||
assert_eq!(
|
||||
PipelineDocument::Question(question.clone()).validate(),
|
||||
Err(PipelineRefusal::InvalidFormat { field: "question.title".into(), value: String::new() })
|
||||
);
|
||||
ruling.title = Title(String::new());
|
||||
assert_eq!(
|
||||
PipelineDocument::Ruling(ruling.clone()).validate(),
|
||||
Err(PipelineRefusal::InvalidFormat { field: "ruling.title".into(), value: String::new() })
|
||||
);
|
||||
|
||||
question.title = Title("a".repeat(200));
|
||||
assert_eq!(PipelineDocument::Question(question.clone()).validate(), Ok(()));
|
||||
question.title = Title("a".repeat(201));
|
||||
assert_eq!(
|
||||
PipelineDocument::Question(question).validate(),
|
||||
Err(PipelineRefusal::FieldBound { field: "question.title".into(), limit: 200, actual: 201 })
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ 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,
|
||||
- `epiphany.pipeline.*.v2`: Eureka pipeline state (campaign, target, question,
|
||||
ruling, cut spec, cut report, verdict, finding, follow-up, resolution, plus
|
||||
instance, stewardship and hand-off), stored in an instance's mind and owned by
|
||||
the Huginn memory organ. A store is canonical to exactly one instance, so the
|
||||
|
|
@ -38,7 +38,7 @@ contracts cross to the Huginn organ and its `eureka-state` client. The files are
|
|||
derived from the Rust value types and checked byte-for-byte by
|
||||
`epiphany-pipeline`'s `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`, and so does
|
||||
field with a serde default keeps `epiphany.pipeline.epoch.v2`, and so does
|
||||
widening an enum such as `PipelineKind`, because these readers are ours and each
|
||||
ships with the variants it knows -- one that meets a kind it has never heard of
|
||||
refuses it on the kind, not on the epoch. A breaking change bumps the epoch, and
|
||||
|
|
|
|||
|
|
@ -46,9 +46,14 @@
|
|||
"description": "A campaign slug or key segment: label parts joined by `.`.",
|
||||
"maxLength": 64,
|
||||
"type": "string"
|
||||
},
|
||||
"Title": {
|
||||
"description": "A title: 1 to 200 UTF-8 bytes, never empty. All four leaf titles\n(campaign, cut spec, question and ruling) carry this type, ruled B\nunder Q-RS1: the epoch moves in this cut regardless, so tightening the\ntwo titles that were `Short` costs nothing extra and avoids a second\nepoch bump later just to make a title required.",
|
||||
"maxLength": 200,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"repos": {
|
||||
|
|
@ -65,7 +70,7 @@
|
|||
"$ref": "#/$defs/DocRef"
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/Short"
|
||||
"$ref": "#/$defs/Title"
|
||||
},
|
||||
"working_branch": {
|
||||
"$ref": "#/$defs/Short"
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"attempt": {
|
||||
|
|
@ -267,6 +267,11 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Title": {
|
||||
"description": "A title: 1 to 200 UTF-8 bytes, never empty. All four leaf titles\n(campaign, cut spec, question and ruling) carry this type, ruled B\nunder Q-RS1: the epoch moves in this cut regardless, so tightening the\ntwo titles that were `Short` costs nothing extra and avoids a second\nepoch bump later just to make a title required.",
|
||||
"maxLength": 200,
|
||||
"type": "string"
|
||||
},
|
||||
"VerificationTest": {
|
||||
"properties": {
|
||||
"name": {
|
||||
|
|
@ -283,7 +288,7 @@
|
|||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"adds": {
|
||||
|
|
@ -376,7 +381,7 @@
|
|||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/Short"
|
||||
"$ref": "#/$defs/Title"
|
||||
},
|
||||
"verification": {
|
||||
"$ref": "#/$defs/CutVerification"
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.hand_off.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.hand_off.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "A reassignment of stewardship, recorded in both minds. `documents` names\nwhat travels with it.",
|
||||
"properties": {
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.instance.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.instance.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "A mind's identity document. A store is canonical to exactly one\ninstance, and this says which; identity lives in the state, not in a\npath.",
|
||||
"properties": {
|
||||
|
|
@ -78,9 +78,14 @@
|
|||
"description": "A campaign slug or key segment: label parts joined by `.`.",
|
||||
"maxLength": 64,
|
||||
"type": "string"
|
||||
},
|
||||
"Title": {
|
||||
"description": "A title: 1 to 200 UTF-8 bytes, never empty. All four leaf titles\n(campaign, cut spec, question and ruling) carry this type, ruled B\nunder Q-RS1: the epoch moves in this cut regardless, so tightening the\ntwo titles that were `Short` costs nothing extra and avoids a second\nepoch bump later just to make a title required.",
|
||||
"maxLength": 200,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"asked_on": {
|
||||
|
|
@ -121,11 +126,15 @@
|
|||
},
|
||||
"recommended": {
|
||||
"$ref": "#/$defs/Label"
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/Title"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"campaign",
|
||||
"label",
|
||||
"title",
|
||||
"question",
|
||||
"options",
|
||||
"recommended",
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "The record of how a subject was closed and by what. `sequence` is per\nsubject and set by the writer, `1` for the first, and it is the last\npart of the key, so a subject's resolutions share a prefix and a\nwithdrawn one is kept under its subject rather than overwritten.\nWhether a sequence is the previous plus one, and whether an earlier\nresolution still stands, are admission's rules: this crate refuses\nneither `0` nor a gap, exactly as it refuses neither `revision: 0` nor\na `revision` with no predecessor.",
|
||||
"properties": {
|
||||
|
|
@ -93,9 +93,14 @@
|
|||
"description": "A campaign slug or key segment: label parts joined by `.`.",
|
||||
"maxLength": 64,
|
||||
"type": "string"
|
||||
},
|
||||
"Title": {
|
||||
"description": "A title: 1 to 200 UTF-8 bytes, never empty. All four leaf titles\n(campaign, cut spec, question and ruling) carry this type, ruled B\nunder Q-RS1: the epoch moves in this cut regardless, so tightening the\ntwo titles that were `Short` costs nothing extra and avoids a second\nepoch bump later just to make a title required.",
|
||||
"maxLength": 200,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"answers": {
|
||||
|
|
@ -149,11 +154,15 @@
|
|||
},
|
||||
"ruling": {
|
||||
"$ref": "#/$defs/Para"
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/Title"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"campaign",
|
||||
"label",
|
||||
"title",
|
||||
"ruling",
|
||||
"ruled_on",
|
||||
"precedents",
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.stewardship.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.stewardship.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Stewardship over a repo, as an assignment recorded in a mind. One\ninstance may steward several repos, so the repo is part of the key.\n`sequence` is per `(instance, repo)`, set by the writer, `1` for the\nfirst, so a repo transferred away and back is two records under one\nprefix rather than one document overwriting the other. `assigned_on`\nis a field, not a key part. As on a resolution, whether the sequence\nis the previous plus one, and which assignment is in force, are\nadmission's rules and not this crate's.",
|
||||
"properties": {
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v1.schema.json",
|
||||
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v2.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"campaign": {
|
||||
|
|
@ -24,147 +24,147 @@
|
|||
"path": "epiphany.work_organ_state.v0.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.campaign.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.campaign.v2",
|
||||
"documentType": "epiphany.pipeline.campaign.v2",
|
||||
"title": "Epiphany Pipeline Campaign v2",
|
||||
"path": "epiphany.pipeline.campaign.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.target.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.target.v2",
|
||||
"documentType": "epiphany.pipeline.target.v2",
|
||||
"title": "Epiphany Pipeline Target v2",
|
||||
"path": "epiphany.pipeline.target.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.question.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.question.v2",
|
||||
"documentType": "epiphany.pipeline.question.v2",
|
||||
"title": "Epiphany Pipeline Question v2",
|
||||
"path": "epiphany.pipeline.question.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.ruling.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.ruling.v2",
|
||||
"documentType": "epiphany.pipeline.ruling.v2",
|
||||
"title": "Epiphany Pipeline Ruling v2",
|
||||
"path": "epiphany.pipeline.ruling.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_spec.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.cut_spec.v2",
|
||||
"documentType": "epiphany.pipeline.cut_spec.v2",
|
||||
"title": "Epiphany Pipeline CutSpec v2",
|
||||
"path": "epiphany.pipeline.cut_spec.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.cut_report.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.cut_report.v2",
|
||||
"documentType": "epiphany.pipeline.cut_report.v2",
|
||||
"title": "Epiphany Pipeline CutReport v2",
|
||||
"path": "epiphany.pipeline.cut_report.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.verdict.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.verdict.v2",
|
||||
"documentType": "epiphany.pipeline.verdict.v2",
|
||||
"title": "Epiphany Pipeline Verdict v2",
|
||||
"path": "epiphany.pipeline.verdict.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.finding.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.finding.v2",
|
||||
"documentType": "epiphany.pipeline.finding.v2",
|
||||
"title": "Epiphany Pipeline Finding v2",
|
||||
"path": "epiphany.pipeline.finding.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.follow_up.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.follow_up.v2",
|
||||
"documentType": "epiphany.pipeline.follow_up.v2",
|
||||
"title": "Epiphany Pipeline FollowUp v2",
|
||||
"path": "epiphany.pipeline.follow_up.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.resolution.v2.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"
|
||||
"schemaVersion": "epiphany.pipeline.resolution.v2",
|
||||
"documentType": "epiphany.pipeline.resolution.v2",
|
||||
"title": "Epiphany Pipeline Resolution v2",
|
||||
"path": "epiphany.pipeline.resolution.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.instance.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.instance.v2.schema.json",
|
||||
"kind": "document_payload",
|
||||
"wireContracts": [
|
||||
"cultnet.schema.v0"
|
||||
],
|
||||
"schemaVersion": "epiphany.pipeline.instance.v1",
|
||||
"documentType": "epiphany.pipeline.instance.v1",
|
||||
"title": "Epiphany Pipeline Instance v1",
|
||||
"path": "epiphany.pipeline.instance.v1.schema.json"
|
||||
"schemaVersion": "epiphany.pipeline.instance.v2",
|
||||
"documentType": "epiphany.pipeline.instance.v2",
|
||||
"title": "Epiphany Pipeline Instance v2",
|
||||
"path": "epiphany.pipeline.instance.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.stewardship.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.stewardship.v2.schema.json",
|
||||
"kind": "document_payload",
|
||||
"wireContracts": [
|
||||
"cultnet.schema.v0"
|
||||
],
|
||||
"schemaVersion": "epiphany.pipeline.stewardship.v1",
|
||||
"documentType": "epiphany.pipeline.stewardship.v1",
|
||||
"title": "Epiphany Pipeline Stewardship v1",
|
||||
"path": "epiphany.pipeline.stewardship.v1.schema.json"
|
||||
"schemaVersion": "epiphany.pipeline.stewardship.v2",
|
||||
"documentType": "epiphany.pipeline.stewardship.v2",
|
||||
"title": "Epiphany Pipeline Stewardship v2",
|
||||
"path": "epiphany.pipeline.stewardship.v2.schema.json"
|
||||
},
|
||||
{
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.hand_off.v1.schema.json",
|
||||
"schemaId": "https://gamecult.dev/epiphany/cultnet/epiphany.pipeline.hand_off.v2.schema.json",
|
||||
"kind": "document_payload",
|
||||
"wireContracts": [
|
||||
"cultnet.schema.v0"
|
||||
],
|
||||
"schemaVersion": "epiphany.pipeline.hand_off.v1",
|
||||
"documentType": "epiphany.pipeline.hand_off.v1",
|
||||
"title": "Epiphany Pipeline HandOff v1",
|
||||
"path": "epiphany.pipeline.hand_off.v1.schema.json"
|
||||
"schemaVersion": "epiphany.pipeline.hand_off.v2",
|
||||
"documentType": "epiphany.pipeline.hand_off.v2",
|
||||
"title": "Epiphany Pipeline HandOff v2",
|
||||
"path": "epiphany.pipeline.hand_off.v2.schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
137
tools/eureka-readside-leaf-mutations.psd1
Normal file
137
tools/eureka-readside-leaf-mutations.psd1
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Eureka read-side cut, Cut RS-L: the leaf's title, the epoch, and the two new
|
||||
# grammar doors. Entries only; the harness is tools/eureka-mutations.ps1.
|
||||
#
|
||||
# powershell -File tools/eureka-mutations.ps1 `
|
||||
# -Entries tools/eureka-readside-leaf-mutations.psd1 `
|
||||
# -Target epiphany-pipeline/src/lib.rs `
|
||||
# -Test 'cargo test -p epiphany-pipeline --lib'
|
||||
#
|
||||
# M0 is the harness's own built-in control and is not an entry here.
|
||||
#
|
||||
# L1 and L1L are `OrgRepo::validate_org_repo`, on `Slug::validate_slug`'s
|
||||
# pattern, and both are killed by `the_org_repo_and_label_doors_are_the_grammar`:
|
||||
# a door that answers Ok to everything dies on "GameCult" alone, and the
|
||||
# loosened grammar that accepts any string carrying a slash dies on "a/b/c"
|
||||
# and on the 201-byte fixture, both of which carry exactly one slash.
|
||||
#
|
||||
# L2 and L2L are `Label::validate_label`, on the same pattern, and both are
|
||||
# killed by the same test: a door that answers Ok to everything dies on the
|
||||
# empty string, and a door delegating to the slug grammar instead of the
|
||||
# label grammar dies on "a.b", which `dotted_text` accepts as two labels
|
||||
# joined by a dot and `label_text` refuses outright because a label carries
|
||||
# no dot.
|
||||
#
|
||||
# L3 and L3L are the `Title` type's non-empty and 200-byte bounds, both
|
||||
# killed by `question_and_ruling_carry_a_title`: removing the non-empty check
|
||||
# dies on the empty-title case, and widening the bound to 201 bytes dies on
|
||||
# the pinned 201-byte refusal.
|
||||
@{
|
||||
Mutations = @(
|
||||
@{
|
||||
Id = 'L1'
|
||||
Rule = 'The public door onto the org_repo grammar is the grammar, not a formality: a door that answers Ok to every repo string leaves a caller holding a bare OrgRepo (the read side vocabulary''s repo alias, RS-3) unable to tell a malformed one from a missing document.'
|
||||
Test = 'tests::the_org_repo_and_label_doors_are_the_grammar'
|
||||
Old = @'
|
||||
pub fn validate_org_repo(&self) -> Result<(), PipelineRefusal> {
|
||||
Bounded::validate(self, "org_repo")
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
pub fn validate_org_repo(&self) -> Result<(), PipelineRefusal> {
|
||||
Ok(())
|
||||
}
|
||||
'@
|
||||
},
|
||||
@{
|
||||
Id = 'L1L'
|
||||
Rule = 'The door delegates to the one grammar, org_repo_text whole: a loosening that accepts any string carrying a slash still depends on the input, but admits a second slash and an over-long value the real grammar refuses.'
|
||||
Test = 'tests::the_org_repo_and_label_doors_are_the_grammar'
|
||||
Old = @'
|
||||
fn org_repo_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
match value.split_once('/') {
|
||||
Some((org, repo))
|
||||
if !org.is_empty() && !repo.is_empty() && !repo.contains('/') && value.len() <= 200 =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format_error(field, value)),
|
||||
}
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
fn org_repo_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.contains('/') { Ok(()) } else { Err(format_error(field, value)) }
|
||||
}
|
||||
'@
|
||||
},
|
||||
@{
|
||||
Id = 'L2'
|
||||
Rule = 'The public door onto the label grammar is the grammar, not a formality: a door that answers Ok to every label string leaves a caller holding a bare Label (the read side vocabulary''s cut alias, RS-3) unable to tell a malformed one from a missing document.'
|
||||
Test = 'tests::the_org_repo_and_label_doors_are_the_grammar'
|
||||
Old = @'
|
||||
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
|
||||
Bounded::validate(self, "label")
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
|
||||
Ok(())
|
||||
}
|
||||
'@
|
||||
},
|
||||
@{
|
||||
Id = 'L2L'
|
||||
Rule = 'The door delegates to the label grammar and not to some other one: a door wired to dotted_text instead of label_text admits a dotted name a label never allows, since a label composes no key of its own and carries no dot.'
|
||||
Test = 'tests::the_org_repo_and_label_doors_are_the_grammar'
|
||||
Old = @'
|
||||
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
|
||||
Bounded::validate(self, "label")
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
pub fn validate_label(&self) -> Result<(), PipelineRefusal> {
|
||||
dotted_text("label", &self.0)
|
||||
}
|
||||
'@
|
||||
},
|
||||
@{
|
||||
Id = 'L3'
|
||||
Rule = 'Q-RS1 (ruled B): a title is never empty. Removing the non-empty check leaves only the 200-byte bound, so a bare leaf with an empty campaign, cut spec, question or ruling title would validate.'
|
||||
Test = 'tests::question_and_ruling_carry_a_title'
|
||||
Old = @'
|
||||
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.is_empty() {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
within(200)(field, value)
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
within(200)(field, value)
|
||||
}
|
||||
'@
|
||||
},
|
||||
@{
|
||||
Id = 'L3L'
|
||||
Rule = 'The title bound is exactly 200 bytes, not 201: a loosening that still depends on the input''s length dies on the pinned 201-byte case, which the real bound refuses and the widened one would accept.'
|
||||
Test = 'tests::question_and_ruling_carry_a_title'
|
||||
Old = @'
|
||||
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.is_empty() {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
within(200)(field, value)
|
||||
}
|
||||
'@
|
||||
New = @'
|
||||
fn title_text(field: &str, value: &str) -> Result<(), PipelineRefusal> {
|
||||
if value.is_empty() {
|
||||
return Err(format_error(field, value));
|
||||
}
|
||||
within(201)(field, value)
|
||||
}
|
||||
'@
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in a new issue