diff --git a/notes/eureka-pipeline-state-cut.md b/notes/eureka-pipeline-state-cut.md index 7d6b3f08..eb29dc21 100644 --- a/notes/eureka-pipeline-state-cut.md +++ b/notes/eureka-pipeline-state-cut.md @@ -34,9 +34,13 @@ pause saying exactly why it stopped there. `b6f6e802`, `80db5db6`, `3ee78e05`, second fix batch at `9b68d83c`, `4cccb45c`, `4a654351`. `epiphany-pipeline` is a leaf library Huginn can depend on without `epiphany-core`. -- **Cut 6b in Hands** against `4a654351`. The second fix batch's Soul pass found - the same invariant open one level up for the third time, so the key grammar - is redesigned rather than patched a fourth time. +- **Cut 6b landed** at `602ffd9f` (deletes), `1bddd2ac` (grammar), `95ee551a` + (mutation suite); fix batch in Hands. The second fix batch's Soul pass had + found the same invariant open one level up for the third time, so the key + grammar was redesigned rather than patched a fourth time. Soul's first pass + on the grammar found no collision in 212,450 adversarial keys and four + unpinned checks. +- **Cut 6c specified** against `95ee551a`; waits on the 6b fix batch. - **Target rewritten** at `5fb4eb22`. **Cut 6c is the last cut inside Epiphany.** Cuts 7-16 build Huginn's Rust @@ -448,6 +452,69 @@ in this repo and in Ghostlight: Cut 6b's suite is the first with a control (M0) and the rule is now in the Eureka skill. +**Cut 6b landed** at `602ffd9f` (the three key-shape exceptions deleted; +does not build, by design), `1bddd2ac` (one grammar for every kind; 20 tests, +0 warnings) and `95ee551a` (`tools/eureka-cut6b-mutations.ps1`, the first +suite with a no-op control). Source outside tests 774 → 726, tests 734 → 881, +no schema, dependency, kind, field or epoch moved. Hands' spec discrepancies, +all kept: `local` takes `&[&str]` rather than a fixed array because the +Resolution and Finding arms carry a parent's parts; the whole-local bound +refuses `InvalidFormat` to match the reader; `key_segment` is a pure escape +and each caller validates its own field first; two "unchanged" tests carried +literal keys that moved; and `{CAMPAIGN}:resolution:R8` is now asserted +*accepted* with the Q11 rationale in its doc comment, so a future runtime +guard is visible. + +**Soul's first pass on the grammar** (Fable, script +`soul-cut6b-mutations.ps1` in the session scratchpad, M0 green twice) built a +whole-key injectivity probe: 212,450 distinct keys across all thirteen kinds +with adversarial slugs, repos, labels and dates and resolutions nested to +depth four, **zero collisions**, every key three segments, every key reading +back to its own kind and identity, 157,010 resolution keys recovering exactly +their subject. `self` collides with nothing as a slug, a label or a campaign. +Hands' M1-M6 all killed on rerun; Soul's own S1, S6, S7, S8, S9 killed; S3 +(`splitn(3, ':')`) survived as an equivalent mutant because `label_text` +refuses a colon in the third segment anyway. Three survived for real: + +- **F2 (medium): the escape's `_`→`__` half is pinned only through an + `OrgRepo`.** A mutant escaping only `.` when the value has no `/` survives, + and hand-offs to `a_db` and to `a.b` then share a key. That is the pass-2 + collision class, one edit from reopening. +- **F3 (medium-low): the composer's root check is pinned for one byte.** Under + `trim_end_matches('.')`, campaign slug `eureka-state.` composes a key the + reader refuses, and the write validator recomputes and compares without + reading back, so a document is admitted under an id no `PipelineRef` can + name. +- **F4 (medium-low): the 64-byte boundary is unpinned on both sides.** + Fixtures sit at 55 and 66+, so a `> 65` mutant survives and the writer + admits a 65-byte local the reader refuses. The code is right; the pin was + missing. + +And four findings that are not code defects: **F1**, the "depth at most five" +claim is false (corrected above); **F5**, a dotted finding label now refuses as +`finding.key` rather than `finding.label`, consistent with every other kind +and accepted as the behaviour; **F6**, the authority map overstated the escape +rule for `Date` (corrected above); **F7**, Hands' "twelve fixed call sites" is +eleven literal slices plus two computed vectors, the Finding one fixed in +practice by `parent_cut` pinning the verdict local to two parts. **F8**, +pre-existing and informational: a cut-report key drops the spec revision +(a standing ruling) and a verdict key drops the report attempt (implied, not +stated). `pipeline_id` accepting `c:campaign:notself` and the like is grammar +versus reachability, admission's job, as the spec says. + +The stale Cut 6 script entries M5 and M12-M15 match zero anchors at HEAD and +the script throws at its line 234 rather than reporting a kill. Loud, as the +rule requires. Target dir: Hands' growth to 9,279 was one debug profile; +Soul left 8,991 because cargo rotated out one of Hands' incremental sessions, +which cannot be recreated and is not a deletion. + +**The fix batch is in Hands:** pin F2, F3 and F4 with Soul's S2, S4 and S5 as +the mutations; state the depth arithmetic in the arm comment and the test; and +factor the mutation harness into one `tools/eureka-mutations.ps1` with a +built-in M0 and per-cut entries files, deleting the stale Cut 6 entries with a +note naming the 6b mutation that now pins each rule. Cuts 5, 6, 6b and 6c +would otherwise carry four copies of the same scaffold. + ## Probes and source reads this pass No cargo build ran this pass. Every new mechanism claim below was settled by a @@ -1174,8 +1241,11 @@ successor because there is nothing left to guess. Roots take the reserved constant `self` as local; a reader recovers a root's identity from segment 1 and never consults it. A resolution is keyed inside its subject's root with the subject's kind and local as its own local. Nesting is -bounded by construction: each level costs eleven bytes of `resolution.` against -the 64-byte local, so depth is at most five, and `pipeline_id` does not recurse. +bounded by construction, not by a guard: a nested local is +`11 * (depth - 1) + len(subject local)` bytes against the 64-byte bound, so a +one-byte subject local nests six deep and `question.Q1` five, and +`pipeline_id` does not recurse. *(Corrected after Soul F1: this first said +"at most five"; the bound is the local, and the depth follows from the subject.)* **The one escape, generalised.** `repo_segment` (`:619-636`, `_`→`__`, `/`→`_-`, `.`→`_d`, injectivity verified by Soul) becomes `key_segment` and @@ -1258,8 +1328,10 @@ stays. - **Derived state:** the key is derived, never stored. `pipeline_id` is the key read backwards and holds no state. `declared_kind`'s inference is deleted. - **Forbidden writers:** no arm of `pipeline_key` may `return` a key; no local - part may be a raw `Slug`, `Short`, `OrgRepo` or `Date`; nothing may infer a - kind from an id's shape; reachability ("does this document exist", "may this + part may be a raw `Slug`, `Short` or `OrgRepo` (a validated `Date` is + `[0-9-]{10}`, already a label, and passes raw; Soul F6 corrected the first + wording, which claimed every non-label value was escaped); nothing may infer + a kind from an id's shape; reachability ("does this document exist", "may this kind be resolved") stays with admission in `huginn-mind` per the resolution matrix above. Cross-field rules stay out of `Bounded`. - **Shared paths:** `pipeline_key`, `pipeline_id`, `PipelineRef::validate` @@ -1340,16 +1412,261 @@ stored-document re-key. ## Cut 6c. The Ghostlight shapes -Specified 2026-09-16 (zero new kinds: two unit enums, one new value type, one -deleted, five fields, three retypings; six of thirteen schemas regenerate; no -kind widens, so the epoch holds; about −17 source, +115 including 75 test -lines). Rulings taken: Soul measures every promise and `Unproven` carries the -ones it could not reach; `Fixed { commit: Sha }` is admitted with its referent -outside the document set; multi-supersession closes as `Vec[8]`, -cardinality being an admission rule and not a shape, with Q10 asked against -Cut 8. **The full spec is reissued by Imagination after Cut 6b lands** and is -folded in here then; until it is, the operative text is -`scratchpad/cut-6b-spec.md` under that earlier name. +- **Repo/branch:** Epiphany `codex/eureka-pipeline-state`. Depends on Cut 6b + (landed at `1bddd2ac`, mutation suite at `95ee551a`) and its fix batch. + Blocks Cut 8. Lands before Cut 8 (ruling C) and is the last cut inside + Epiphany before the pause. +- **Anchors.** Every `file:line` below is against `epiphany-pipeline/src/lib.rs` + at `95ee551a`, 1,608 lines, twenty tests, re-found by content after the key + grammar shifted everything from the old `:526` down. The 6b fix batch lands + on `lib.rs` before Hands starts, so **re-anchor by content again**: every + anchor is a `struct`, `impl`, `fn` or sample opener that greps cleanly. +- **Reissued** 2026-09-16 by Imagination against the landed grammar. Three + edits from the first issue: anchors re-found; the Keeps claim about keys + rewritten; one fixture added that only the new grammar makes available, a + `Superseded` entry naming a resolution by the key it has. Deletes, adds, + `Promise`, the two unit enums, `StructuralDelta`, M17/M19/M20/M21, the + authority map and the subtraction estimate carry over; M16 and M18 keep + their rule and gain sharper forgeries; M22 is new. + +**Why this cut exists, and why now.** Ghostlight's independently run campaign +(D9) named six pains. Four were already built: finding identity, the ledger +derivation, cut status and the direction of the supersession relation are +covered by keys, joins and `PipelineResolution` as they stand. What is left is +one genuine hole (promises), two fields wearing one shape (ruling authority, +finding origin), and four retypings that are breaking, not additive. Under +ruling 8 a retyping costs an epoch bump the moment a pinned reader exists. +Nothing reads these schemas today: a grep for `SubtractionEstimate`, +`MutationRecord`, `ResolutionOutcome`, `VerdictClaim`, `StructuralDelta` and +`subtraction_estimate` over Epiphany finds `lib.rs`, the four schema files +that embed them, and one prose mention in this map; over `F:\Projects\Huginn` +and the Eureka skill checkout it finds nothing (source read). A whole-drive +search timed out, so any other tree is unchecked. Cut 8 pins a git rev and +closes the window. **The four retypings cost nothing today and an epoch +tomorrow.** + +**Rulings carried in:** A, Soul measures every promise and `Unproven` carries +the ones it could not reach; B, `Fixed { commit: Sha }` is admitted with its +referent outside the document set; C, this lands before Cut 8. +Multi-supersession is `Vec[8]`, cardinality being admission's +(Q10 asked against Cut 8). Q11 A: nesting stays. Q12: `OrgRepo` untouched; the +tightening is its own commit and is not folded in. No kind widens, so +`epiphany.pipeline.epoch.v1` holds. **Zero new kinds.** + +### Deletes first + +| Path | Lines | What dies | +|---|---:|---| +| `lib.rs:290-294` | 5 | `SubtractionEstimate` and the two-line comment on its list default. Its consumer becomes `StructuralDelta`. | +| `lib.rs:388-396` | 9 | `impl Bounded for ResolutionOutcome`. Replaced whole, not patched: every arm changes referent type, and a patched impl is how a length-only check survives. | +| `lib.rs:826-828` | 3 | The cut-spec sample's `SubtractionEstimate { … }` literal. | +| `lib.rs:296` | 1 | `MutationRecord`'s `mutation: Line` field, rewritten in place as `location`/`before`/`after`. | + +The comment at `:298-299` names the dead type and is reworded to stand alone. +No file or schema file dies; `index.json` is untouched because no kind is +added and no type id renamed. Deletes go before any field is added: if +`SubtractionEstimate` and `estimate: StructuralDelta` coexist at any commit, +the cut has two estimate shapes. + +### Keeps + +- **All thirteen kinds, `pipeline_key`, `pipeline_id`, `local`, `key_segment` + and the epoch.** Keys moved in Cut 6b and are settled; this cut moves none + further, because it adds, retypes or removes no field a key is derived from. + The check is mechanical: `git diff --stat -- schemas/cultnet/` at the + end names exactly the six regenerated schema files. `index.json` carries no + hashes (its entries are id, kind, wire contracts, version, type, title and + path, and the derivation test asserts that shape), so it is byte-identical. +- **`PipelineRef` and its `Bounded` impl (`:363-376`).** It becomes + load-bearing for four more referents. It already parses the id against its + declared kind through `pipeline_id`, whose kind-segment check (`:553-555`) + refuses a mismatch; that is why retyping 1 is cheap and what the new fixture + rides on. +- **The whole key suite**, `keys_read_back_as_ids_of_their_kind` included. All + twenty tests stay unchanged in name and assertion. They are blind to this + cut, which is why it carries its own tests. +- **`Short`** stays the type for full-id fields already parsed at key + derivation (`cut_report.cut_spec`, `verdict.cut_report`, `finding.verdict`, + `cut_spec.rulings`, `ruling.answers`). Retyping those is a larger cut. +- **The outcome-invariance assertion** in `keys_are_derived_and_mismatch_refuses` + (`:1029-1037`): a subject has one resolution key whatever the outcome. + +### Adds + +Two `unit_enums!` entries and one `value_types!` entry. No kind, module, +dependency or target. + +``` +RulingAuthority { Operator, Standing, Defaulted } +FindingOrigin { Introduced, PreExisting } +pub struct Promise { label: Label, text: Line } +``` + +`Promise` is not a document and has no key; it is identified by its report's +id plus its label, as `TargetInvariant` is by its target's (`:276`). It is not +resolvable and does not enter the resolution matrix. + +### Per-file changes, `epiphany-pipeline/src/lib.rs` + +| Line | Change | +|---|---| +| `:262-267` | Add `RulingAuthority` and `FindingOrigin` to the `unit_enums!` block. | +| `:290-294` | Delete `SubtractionEstimate` and its comment. | +| `:296` | `pub struct MutationRecord { label: Label, rule: Line, location: CodeLocation, before: Line, after: Line, commit: Sha, failed_as_expected: bool }` | +| `:298-299` | Reword the comment so it no longer names `SubtractionEstimate`. | +| after `:307` | `pub struct Promise { label: Label, text: Line }` beside `LandedName`. | +| `:308` | `VerdictClaim` gains `promise: Option