From d74e36aa0656f9fc420df6d0bf12947986de4ea3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 22 Sep 2026 12:37:13 +0200 Subject: [PATCH] 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 --- epiphany-pipeline/src/lib.rs | 183 +++++++++++++++--- schemas/cultnet/README.md | 4 +- ...epiphany.pipeline.campaign.v2.schema.json} | 9 +- ...iphany.pipeline.cut_report.v2.schema.json} | 2 +- ...epiphany.pipeline.cut_spec.v2.schema.json} | 9 +- ... epiphany.pipeline.finding.v2.schema.json} | 2 +- ...piphany.pipeline.follow_up.v2.schema.json} | 2 +- ...epiphany.pipeline.hand_off.v2.schema.json} | 2 +- ...epiphany.pipeline.instance.v2.schema.json} | 2 +- ...epiphany.pipeline.question.v2.schema.json} | 11 +- ...iphany.pipeline.resolution.v2.schema.json} | 2 +- ...> epiphany.pipeline.ruling.v2.schema.json} | 11 +- ...phany.pipeline.stewardship.v2.schema.json} | 2 +- ...> epiphany.pipeline.target.v2.schema.json} | 2 +- ... epiphany.pipeline.verdict.v2.schema.json} | 2 +- schemas/cultnet/index.json | 130 ++++++------- tools/eureka-readside-leaf-mutations.psd1 | 137 +++++++++++++ 17 files changed, 404 insertions(+), 108 deletions(-) rename schemas/cultnet/{epiphany.pipeline.campaign.v1.schema.json => epiphany.pipeline.campaign.v2.schema.json} (78%) rename schemas/cultnet/{epiphany.pipeline.cut_report.v1.schema.json => epiphany.pipeline.cut_report.v2.schema.json} (99%) rename schemas/cultnet/{epiphany.pipeline.cut_spec.v1.schema.json => epiphany.pipeline.cut_spec.v2.schema.json} (94%) rename schemas/cultnet/{epiphany.pipeline.finding.v1.schema.json => epiphany.pipeline.finding.v2.schema.json} (99%) rename schemas/cultnet/{epiphany.pipeline.follow_up.v1.schema.json => epiphany.pipeline.follow_up.v2.schema.json} (98%) rename schemas/cultnet/{epiphany.pipeline.hand_off.v1.schema.json => epiphany.pipeline.hand_off.v2.schema.json} (98%) rename schemas/cultnet/{epiphany.pipeline.instance.v1.schema.json => epiphany.pipeline.instance.v2.schema.json} (97%) rename schemas/cultnet/{epiphany.pipeline.question.v1.schema.json => epiphany.pipeline.question.v2.schema.json} (85%) rename schemas/cultnet/{epiphany.pipeline.resolution.v1.schema.json => epiphany.pipeline.resolution.v2.schema.json} (99%) rename schemas/cultnet/{epiphany.pipeline.ruling.v1.schema.json => epiphany.pipeline.ruling.v2.schema.json} (87%) rename schemas/cultnet/{epiphany.pipeline.stewardship.v1.schema.json => epiphany.pipeline.stewardship.v2.schema.json} (98%) rename schemas/cultnet/{epiphany.pipeline.target.v1.schema.json => epiphany.pipeline.target.v2.schema.json} (98%) rename schemas/cultnet/{epiphany.pipeline.verdict.v1.schema.json => epiphany.pipeline.verdict.v2.schema.json} (99%) create mode 100644 tools/eureka-readside-leaf-mutations.psd1 diff --git a/epiphany-pipeline/src/lib.rs b/epiphany-pipeline/src/lib.rs index c4d28f20..1001e1d0 100644 --- a/epiphany-pipeline/src/lib.rs +++ b/epiphany-pipeline/src/lib.rs @@ -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