From 1bddd2acb3aed5aea98d0abafde888f0f0b2fc48 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 13:43:09 +0200 Subject: [PATCH] Cut 6b: one key grammar for every kind Every key is now `::`, three segments with no kind excused. The root is a `Slug`, the kind is its literal name, and the local is `Label`s joined by `.`, so no part carries the separator and a reader recovers the parts by splitting. Roots take the constant local `self`; a resolution is keyed inside its subject's root with the subject's kind and local as its own local, so a resolution of a resolution is an ordinary id bounded by the 64-byte local and needs no guard. - `local` is the one composer: every part through `label_text`, the join bounded whole, once. `pipeline_key` has thirteen arms and one exit. - `pipeline_id` is the grammar read backwards, with no per-kind branch. - `repo_segment` becomes `key_segment`: the same injective escape, now for any `Slug` or `OrgRepo` entering a local. `hand_off.to_instance`, the only raw `Slug` that reached a local, goes through it. - `parent_cut` indexes the split parts instead of splitting from the right. Every key moves, and nothing outside this file reads one. No schema file, kind, type, field, dependency or epoch changes. Tests: six new (`every_key_has_exactly_three_segments`, `roots_of_different_kinds_do_not_share_a_key`, `a_resolution_names_its_subjects_kind`, `a_resolution_of_a_resolution_reads_back`, `a_composed_local_is_bounded_whole`, `no_local_part_carries_the_separator`); thirteen expected keys move in the samples; `composed_keys_cannot_collide`'s composer probe moves into the separator test and its dotted receiver now escapes. Spec deviations, in the report: `local` takes `&[&str]` rather than a const array because the resolution and finding arms carry a parent's parts; the whole-local bound refuses `InvalidFormat` to match the spec's own test and the reader's refusal of the same string; `key_segment` is a pure escape and each caller validates its source type by its own field. This commit builds and its tests pass. Co-Authored-By: Claude Fable 5.1 --- epiphany-pipeline/src/lib.rs | 421 ++++++++++++++++++++++++++--------- 1 file changed, 310 insertions(+), 111 deletions(-) diff --git a/epiphany-pipeline/src/lib.rs b/epiphany-pipeline/src/lib.rs index 74641d6c..b3752552 100644 --- a/epiphany-pipeline/src/lib.rs +++ b/epiphany-pipeline/src/lib.rs @@ -12,6 +12,12 @@ //! schema describes. Keys are semantic: `pipeline_key` derives them from the //! value, and the write validator refuses any envelope whose key differs. //! +//! Every key is `::`, three segments for every kind with +//! none excused: the root is a `Slug`, the kind is the literal kind name, and +//! the local is `Label`s joined by `.`, so no local part carries a dot and a +//! reader recovers the parts by splitting. A root's local is the constant +//! `self`; a resolution's local is its subject's kind and local. +//! //! **Validation follows the type.** `value_types!` is the single field list: it //! emits the struct and its `Bounded` impl from the same tokens, so a field //! cannot be declared without being validated. Format rules live in the field's @@ -114,7 +120,7 @@ fn hex(field: &str, value: &str, lengths: std::ops::RangeInclusive) -> Re } /// A key label: `[A-Za-z0-9_-]{1,64}`. Dots are excluded so that a composed key -/// segments unambiguously; see `parent_cut` and `KeyParts::key`. +/// segments unambiguously; see `local`. fn label_text(field: &str, value: &str) -> Result<(), PipelineRefusal> { let valid = value .bytes() @@ -523,18 +529,20 @@ pipeline_kinds! { "epiphany.pipeline.hand_off.v1", "EpiphanyPipelineHandOffDocument"; } -/// Parses a full document id. A key and an id are the same string, so this -/// accepts exactly what `pipeline_key` derives: `::` for -/// every kind but the roots and a resolution. The segment count is exact, the -/// kind segment must match `kind`, and both the root and the local segment are -/// validated, so `..`, an empty part, spaces and trailing junk are all refused. +/// Parses a full document id: the grammar read backwards. A key and an id are +/// the same string, so this accepts exactly what `pipeline_key` derives: +/// `::`, three segments for every kind. The kind segment +/// must equal `kind`'s name, the root is a `Slug`, and the local is `Label`s +/// joined by `.` and bounded whole, so `..`, an empty part, spaces and trailing +/// junk are all refused. Returns the root and the local; nothing is inferred +/// from either, and no kind is read any other way. fn pipeline_id<'a>( field: &str, id: &'a str, kind: PipelineKind, ) -> Result<(&'a str, &'a str), PipelineRefusal> { let mut segments = id.split(':'); - let (Some(campaign), Some(name), Some(local), None) = ( + let (Some(root), Some(name), Some(local), None) = ( segments.next(), segments.next(), segments.next(), @@ -545,9 +553,9 @@ fn pipeline_id<'a>( if name != kind.name() { return Err(format_error(field, id)); } - dotted_text(field, campaign)?; + dotted_text(field, root)?; dotted_text(field, local)?; - Ok((campaign, local)) + Ok((root, local)) } /// The local segment of a parent id in `campaign`, of the expected kind. @@ -564,15 +572,15 @@ fn parent_local<'a>( Ok(local) } -/// The cut label inside a parent local `cut-