From 8a598ebbed1632062ff605fa475805c2ecfe52f6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 02:03:14 +0200 Subject: [PATCH] Pin the two restored rules with mutations, and match anchors to the file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M9 and M10 are the mutation half of the previous commit: a rule with no mutation is a claim, not a pinned rule. M9 replaces the `ForeignStore` refusal in `decode` with a payload format error. The envelope is still refused, so a test asserting only "this fails" would pass either way; the test asserts the refusal itself, and dies. M10 registers the pipeline document types into `runtime_spine_schema_cache`, which is exactly the mechanism the spine refuses a pipeline store with — the registry, not the epoch check. With the types registered the store opens and the test dies. Both aim at the rule rather than at an arbitrary neighbouring line, which is what makes the kill mean something. The suite now spans two files, so every entry names its own, and the first run caught a rake worth keeping: the anchors here are written with plain newlines and the working tree is CRLF, so a multi-line anchor matched nothing. M1 to M9 are single-line and never spanned a newline, which is why the flaw surfaced only when M10 arrived. Normalizing towards LF was backwards; the anchor is now converted to whatever endings the file has. The stale-anchor throw did its job: it refused to report a pass it had not earned. Ten mutations, ten killed, each by its own named test alone. Sources are restored byte-identical to HEAD afterwards. Builds: no code change; the script is run by hand and is wired into no target. Co-Authored-By: Claude Opus 5 --- tools/eureka-cut4-mutations.ps1 | 61 +++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/tools/eureka-cut4-mutations.ps1 b/tools/eureka-cut4-mutations.ps1 index c9d257b1..6521ff43 100644 --- a/tools/eureka-cut4-mutations.ps1 +++ b/tools/eureka-cut4-mutations.ps1 @@ -3,22 +3,27 @@ # Each entry below is one rule that a surviving test claims to pin, together # with the exact source change that removes the rule. A mutation counts only if # the named test fails while the mutation is applied. The script applies one -# mutation at a time, runs that test alone, and restores the file from HEAD +# mutation at a time, runs that test alone, and restores the file from disk # before the next one, so a failed run never leaves the tree mutated. # # pwsh -File tools/eureka-cut4-mutations.ps1 # # Requires CARGO_TARGET_DIR to be set the way the cut ran it, or it will build # into the repo-local target/. +# +# M9 and M10 arrived with Soul's Cut 4 findings F2 and F4: the two rules that +# survived the store deletion with no test left standing over them. They pin +# refusals in two different files, so every entry names its own file. $ErrorActionPreference = 'Stop' $repo = Split-Path -Parent $PSScriptRoot -$file = 'epiphany-core/src/pipeline_documents.rs' -$path = Join-Path $repo $file +$documents = 'epiphany-core/src/pipeline_documents.rs' +$spine = 'epiphany-core/src/runtime_spine.rs' $mutations = @( @{ Id = 'M1' + File = $documents Rule = 'A document decodes back only from an envelope of its own type id.' Test = 'pipeline_documents::tests::every_pipeline_kind_round_trips_through_named_slot_zero' Old = '$(if envelope.r#type == <$document as DatabaseEntry>::TYPE {' @@ -26,6 +31,7 @@ $mutations = @( }, @{ Id = 'M2' + File = $documents Rule = 'Text bounds are measured in UTF-8 bytes, not characters.' Test = 'pipeline_documents::tests::bounds_refuse_in_utf8_bytes' Old = ' move |field: &str, value: &str| bound(field, limit, value.len())' @@ -33,6 +39,7 @@ $mutations = @( }, @{ Id = 'M3' + File = $documents Rule = 'An OrgRepo carries exactly one slash.' Test = 'pipeline_documents::tests::repo_fields_must_be_org_slash_repo' Old = " if !org.is_empty() && !repo.is_empty() && !repo.contains('/') && value.len() <= 200 =>" @@ -40,6 +47,7 @@ $mutations = @( }, @{ Id = 'M4' + File = $documents Rule = 'A write envelope''s key is recomputed from the value and a mismatch refuses.' Test = 'pipeline_documents::tests::keys_are_derived_and_mismatch_refuses' Old = ' if envelope.key != expected {' @@ -47,6 +55,7 @@ $mutations = @( }, @{ Id = 'M5' + File = $documents Rule = 'A finding label carries no dot, so a composed key segments one way.' Test = 'pipeline_documents::tests::composed_keys_cannot_collide' Old = ' label_text("finding.label", &value.label.0)?;' @@ -54,6 +63,7 @@ $mutations = @( }, @{ Id = 'M6' + File = $documents Rule = 'A parent local must carry its own kind''s marker character.' Test = 'pipeline_documents::tests::parent_ids_are_parsed_strictly' Old = ' let digits = suffix.strip_prefix(marker).ok_or_else(invalid)?;' @@ -61,6 +71,7 @@ $mutations = @( }, @{ Id = 'M7' + File = $documents Rule = 'A PipelineRef''s id is parsed as a full id of its declared kind.' Test = 'pipeline_documents::tests::resolution_subject_is_a_full_id_of_its_kind' Old = ' pipeline_id(&format!("{at}.id"), &self.id.0, self.kind).map(|_| ())' @@ -68,20 +79,58 @@ $mutations = @( }, @{ Id = 'M8' + File = $documents Rule = 'Published schemas are the Rust derivation, byte for byte.' Test = 'pipeline_documents::tests::pipeline_published_schemas_match_derivation' Old = ' Short = 200, |field, value| within(200)(field, value);' New = ' Short = 201, |field, value| within(200)(field, value);' + }, + # Soul F2. M1 removes the positive match; this removes the refusal that + # catches everything the positive match misses, leaving a decode that says + # "malformed payload" about an envelope that was never a document at all. + @{ + Id = 'M9' + File = $documents + Rule = 'An envelope whose type id belongs to another kind is refused as ForeignStore.' + Test = 'pipeline_documents::tests::decode_refuses_an_envelope_of_a_foreign_type' + Old = 'Err(PipelineRefusal::ForeignStore { r#type: envelope.r#type.clone() })' + New = 'Err(format_error("payload", &envelope.r#type))' + }, + # Soul F4. The spine cache refuses a pipeline store by registering its own + # types and nothing else, so the mutation that removes the rule is the one + # that registers the pipeline types into it. The registration function is + # `cfg(test)`, which is exactly the build this mutation compiles under. + @{ + Id = 'M10' + File = $spine + Rule = 'The runtime spine cache refuses an epiphany.pipeline.* envelope.' + Test = 'runtime_spine::tests::runtime_spine_cache_refuses_a_pipeline_store' + Old = @' +fn runtime_spine_schema_cache() -> Result { + let mut cache = CultCache::new(); +'@ + New = @' +fn runtime_spine_schema_cache() -> Result { + let mut cache = CultCache::new(); + crate::pipeline_documents::register_pipeline_document_types(&mut cache)?; +'@ } ) $results = @() foreach ($mutation in $mutations) { + $path = Join-Path $repo $mutation.File $original = [IO.File]::ReadAllText($path) - if (-not $original.Contains($mutation.Old)) { - throw "$($mutation.Id): anchor not found in $file. The mutation is stale." + # The anchors here are written with plain newlines and the working tree is + # CRLF, so a multi-line anchor matches nothing unless it is converted to the + # line endings the file actually has. Convert towards the file. + $eol = if ($original.Contains("`r`n")) { "`r`n" } else { "`n" } + $old = ($mutation.Old -replace "`r`n", "`n") -replace "`n", $eol + $new = ($mutation.New -replace "`r`n", "`n") -replace "`n", $eol + if (-not $original.Contains($old)) { + throw "$($mutation.Id): anchor not found in $($mutation.File). The mutation is stale." } - $mutated = $original.Replace($mutation.Old, $mutation.New) + $mutated = $original.Replace($old, $new) if ($mutated -eq $original) { throw "$($mutation.Id): replacement changed nothing." }