Commit graph Huginn/tools
Author SHA1 Message Date
Meta Vens
d2b74f49af Drive open_items with faults inside the requested campaign
F3: the only existing fault for the open-items dispatch arm (an orphaned
instance document) sits outside the campaign a request asks about, so two
loosenings survive unnoticed: one that swallows an Unavailable naming a
document in the requested campaign, and one that swallows every Unavailable
except a missing receipt.

Add open_items_refuses_on_a_fault_inside_the_requested_campaign, driving a
missing receipt, an undecodable stored document (docs.rs:46) and a document
written by two receipts (query.rs:145), each on a cut spec rooted in the
requested campaign. Add mutation entries S1 and S1d against
Mind::open_items; both die against the new test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:21:38 +02:00
Meta Vens
4ecbf53056 Check a declared instance against the slug grammar on the read path
F6: no declared instance Slug was ever checked against Slug's own grammar
before reaching Mind::require_instance's byte comparison, so a name outside
the grammar read as merely a foreign mind rather than being refused by its
own name.

Slug's grammar check is a crate-private trait in epiphany_pipeline, reachable
only through a real document, so require_grammatical_instance wraps the
declared name in a throwaway PipelineInstance and reads the answer back
through PipelineDocument::validate, the leaf's own public door; nothing here
re-derives the grammar. Daemon::handle asks it on the read path, before
Mind::require_instance, for every request that names an instance.

Add a_declared_instance_outside_the_grammar_is_refused_by_name_before_the_identity_check,
driving a fullwidth Latin small letter y, and mutation entry S3wide, which
folds a fullwidth character to plain ASCII before validating and dies
against it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:20:59 +02:00
Meta Vens
202e5e3ba6 Give the instance check a name differing in case alone
Every foreign fixture differed from the mind's name in its bytes, so a check
folding case passed both suites. A slug label is `[A-Za-z0-9_-]{1,64}`:
uppercase is legal, `Yggdrasil` and `yggdrasil` are two names a client may
declare and two minds a host may hold, and a comparison that folds case
answers one of them for the other. Soul found the mutant alive.

`CASED_INSTANCE` stands beside the near and prefixed names in the mind
fixtures and `CASED` beside them in the daemon's, and each test asserts the
relation it relies on before using it: the cased name lowercases to the mind's
and is not equal to it.

`D1L5` folds case against the mind suite and `D1DL2` against the daemon suite,
so both fixture sets are load-bearing rather than only the one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 01:13:49 +02:00
Meta Vens
731b542354 Pin the size gate on the boundary instead of near it
The document test measures two real answers, one over the window and one
39,305 bytes under it. That gap is wide enough to hide two mutants, and Soul
found both alive: a gate measuring the payload string rather than the encoded
envelope, 231 bytes smaller, and a gate comparing `<` where the transport
compares `<=`. Neither is reachable by a test whose answers sit tens of
thousands of bytes from the edge.

The boundary is known exactly, so a test sits on it. A synthetic reply sized
to exactly `MAX_RESPONSE_BYTES` passes the gate unchanged and is accepted by a
hub `bind` configured; one byte more is refused by the gate, by name and with
both numbers, and refused by the transport with its own queue error. The
refusal is measured too -- 299 bytes, one packet -- so the answer that says an
answer did not fit always fits itself, and a one-byte reply after the accepted
one is refused, which shows the accepted answer filled the window it was
measured against. This is Soul's probe, with the assertions its `eprintln`s
were standing in for.

`D20L4` stops the gate one byte short of the limit and `D20L5` measures the
payload inside the envelope rather than the envelope. Both survived the suite
before this commit.

The session is settled before anything is sent, and the module documentation
now says why: the hub's own accept is unacknowledged reliable traffic, so a
session that has just connected holds 1023 packets rather than 1024 until the
client's acknowledgement arrives. Nothing in the loop depends on it -- on
loopback the acknowledgement precedes the first request -- but a boundary test
written without it measures 1023 and fails for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 01:09:47 +02:00
Meta Vens
a96c386716 Pin the fourth dispatch arm, which was recorded as unpinnable
`open_items` was the one read arm carrying no mutation entry. The reasoning
was that it raises no refusal of its own and its only failing paths are store
integrity inside the reader, which nothing on the daemon's surface can drive.
The second half was wrong, and Soul falsified it: a mind whose commit receipt
has been deleted still opens, because the opener reads the store's types,
epoch and identity and not its receipts, and then a read that joins a document
to its receipt refuses by name over the orphan. That store is reachable from
this crate's own surface.

Soul's probe lands as the test, unchanged in substance. It admits an identity,
drops the daemon, deletes the receipt row, reopens, and asserts the open-items
answer is the reader's own sentence whole:
`Unavailable { detail: "document epiphany.pipeline.instance.v1/yggdrasil:instance:self has no commit receipt" }`.

Reaching a row needs CultCache's `CacheBackingStore` and `DatabaseEntry`, which
`huginn-daemon` could not name. It does not gain a second `cultcache-rs` entry
for them: `huginn-mind::store` re-exports the two traits beside the store type
it already re-exports, for the reason stated there, so the workspace still pins
one revision through one dependency. That is a widening of `huginn-mind`'s
public surface by two re-exports, and no non-test code outside that crate
touches a row.

`D23` rewraps the arm's refusal as unavailable and `D23L` swallows it into an
empty open set, which is what a healthy campaign with nothing open answers.
Both survived the whole suite before this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 01:07:44 +02:00
Meta Vens
44ed8a9026 Give the oversize refusal its own loosening
`D21` reverted the refusal to a failure envelope and had no weaker sibling.
`D21L` keeps the response schema and drops the client's correlation key, which
leaves a refusal the client cannot match to the request that earned it. It
dies, and the suite's 52 entries and M0 are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:24:13 +02:00
Meta Vens
9bbe746719 Refuse an answer that will not fit, by name
A reply larger than one send could carry was handed to the hub anyway. The hub
refused it, the loop printed the error and served past, and the client waited
for an answer nothing would ever send. A real-sized read was lost in silence,
which is the one thing this surface promises not to do: a refusal is typed, and
that has to hold at every size.

`within_window` now encodes the reply and measures it against the window this
module configures, before a send it can already see will fail. Over the limit,
the answer becomes `MindRefusal::ResponseTooLarge { bytes, limit }` on the
response schema, reaching the client exactly as every other refusal does, with
the encoded size and the limit so a caller can narrow its own request. Nothing
is truncated, paginated or retried.

The arithmetic, measured rather than estimated. One `PipelineCutSpec` with every
list at the leaf's bound -- 256 file changes, 64 deletes, keeps, adds, builds,
tests, negative checks and operator checks, a full authority map and structural
delta -- is about 980,000 bytes of field content. As a `View` response, base64
in the operation envelope and MessagePack around that, it encodes to 1,315,551
bytes. The window is `MAX_PENDING_RELIABLE_PACKETS` (1024) times
`MAX_FRAGMENT_BYTES` (1200), so 1,228,800. A single document at the leaf's own
bounds therefore does not fit, by 86,751 bytes.

The window's values are unchanged by ruling: what a list read should return is
an open question, and raising the window would bake multi-megabyte answers in
as ordinary traffic before that question is answered. The module documentation
states 1,228,800 as the current bound rather than as a design target.

The test admits two cut specs into a real mind: one at 256 file changes and one
narrowed to 180, whose view encodes to 1,189,495 bytes. Over one loopback
session the query, the wide view and the open items are each refused by name,
and the narrow view is then delivered whole. Both halves are load-bearing: the
delivered answer sits 39,305 bytes under the limit, so `D22` and `D22L`, which
shrink the configured window, break it.

`D20` removes the gate, `D20L` measures against a larger number than the hub
carries, `D20L2` and `D20L3` collapse the two fields the refusal reports, `D21`
sends it as a failure envelope instead, and `D22`, `D22L` and `D22L2` shrink
each half of the window the gate trusts. All eight die. The whole suite, 51
entries and M0, is green.

Two reads pipelined on one session are still not both answered, and the module
documentation says so: the window is per session and counts what is
unacknowledged, so a second large reply sent before the first is acknowledged
fails to send even though each passes the gate alone. The gate measures one
answer against the window, not against what the session still has room for.
That is not fixed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:08:10 +02:00
Meta Vens
25a841dc6d Pin a read's refusal as crossing the dispatch unchanged
The dispatch was honest and nothing forbade it flattening a refusal the mind
raised: mutants that rewrapped a view refusal as unavailable, swallowed one
into an empty answer, and rewrapped a query refusal all survived both suites.
Flattening is the failure this cut exists to prevent, so it is now a test.

The new daemon test drives each read method into a refusal the method itself
raises, through `Daemon::handle`, with the shapes Soul reached over the wire:
a reference whose kind and id disagree for `view` and for a subject `history`,
and a semantic query for `query`, which is unavailable until Cut 11 wires an
index. Each must arrive as the mind spelled it. The same reads answer when
asked something answerable, so the arms are refusing on the mind's word rather
than on their own shape.

`D17`-`D19` rewrap each refusal and `D17L`-`D19L` swallow each into the empty
answer of its own kind.

`open_items` is not pinned and says so rather than carrying a weak entry: it
raises no refusal of its own, and its only failing paths are store integrity
inside the reader, which nothing on the daemon's surface can drive without a
corrupt store. A mutant against that arm could not fail.

The operation comparison is pinned in the same pass, because it shares this
entries file. Its mismatch case compared two words that differ under any
comparison, so a case-insensitive comparison survived; `Admit` now stands
beside `query` as a malformed envelope, and `D5L2` folds case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:50:35 +02:00
Meta Vens
4fba6cc6d4 Pin the instance check against names that resemble the mind's own
Every fixture pair differed in length, so a comparison of lengths, of first
bytes, or one gated on the declared name being the longer passed both suites.
The check was pinned by the fixtures' luck, not by the test.

Two foreign names now stand beside `thought-cage` in the mind test and the
daemon test: `yggdrasix`, which is `yggdrasil`'s own length and differs in its
last byte alone, and `yggdrasil-two`, which carries the whole name as its
prefix. The near name kills the three comparisons above; the prefixed one kills
a `starts_with`.

`D1L` is the loosening the brief asked for, comparing lengths, restored to that
shape; `D1L2` compares first bytes, `D1L3` runs the gate only when the declared
name is longer, `D1L4` asks whether one name begins with the other, and `D1DL`
runs the length comparison against the daemon suite so the daemon test's own
fixtures are load-bearing. The refusal-naming mutant that had taken `D1L`'s
place is kept as `D1N`; it attacks a weaker rule and is no longer standing in
for one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:48:10 +02:00
Meta Vens
f0fee0b0ac Give the CultNet surface a mutant per rule, and two per rule where one exists
`eureka-cut10-mutations.psd1` carries thirty-one entries over sixteen rules:
each rule has a revert that removes it and, where the rule can be weakened
rather than removed, a loosening that leaves it standing but wrong. The
connection id and the startup order say plainly why they have only the one
mutation each. `D1` and `D1D` are one edit run against both suites, because
the instance check has one owner and two callers.

Three tests grew to hold their mutants. The instance test now admits a batch
whose documents name no instance, so nothing between the wire and A1 can
quietly rewrite the declaration into the mind's own. The malformed-envelope
test now sends a refused read through the whole answer path, so a refusal
must stay on the response schema. The startup test now holds the mind and the
port at once, so a startup that binds first reports the port rather than the
mind it never asked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:14:45 +02:00
Meta Vens
f7170fadf9 Give the instance check one owner and drop the dead serde mirror
Ruling 14's comparison moves out of `admit_steps` into
`Mind::require_instance`, so admission and the CultNet surface's reads ask one
function rather than two copies of one rule. A1 calls it where the inline
comparison was and behaves as before.

The leaf derives `Serialize`, `Deserialize` and `JsonSchema` for
`PipelineRefusal` at the pinned revision, so serde's remote definition of it
is deleted and `MindRefusal::Document` wraps the leaf's value directly.
`PipelineAdmissionBatch` derives the same three and loses the doc sentence
that claimed it could not.

The crate re-exports `epiphany_pipeline` and the owned redb store type, so the
daemon and the client name the leaf's types and the store through one
dependency entry at one revision instead of declaring the git dependency
twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:49:49 +02:00
Meta Vens
5ba7b0c6e5 Refuse a ref that is no ref, rather than answering over it
The pin moves to Epiphany `5cda0886`, where `PipelineRef::validate_ref` opens
the leaf's grammar to a reader. `view` and `history`'s subject scope ask it
before they look, so a ref whose kind and id disagree is refused instead of
reporting the nothing a lookup finds. `query`'s `campaign`, `cut` and `repo`
filters take a `Slug`, a `Label` and an `OrgRepo`, not refs, and are unchanged.

The refusal is `MindRefusal::Document`, the leaf's `InvalidFormat` wrapped
where the organ already wraps the leaf's four. A new variant would be a second
name for one rule, and the grammar it names lives in the leaf.

`a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors` pins both doors on
a disagreeing kind and on a local no writer composes, and pins that absence is
still absence: a well-formed id of a document this mind does not hold answers
`None` and an empty history as it always did. The history test's malformed
scope asserts the refusal where it asserted an empty answer.

`V24` and `V24L` in `tools/eureka-cut9-mutations.psd1` are the validation
dropped and the validation applied at `view` alone. `V10L` leaves the suite
with its reason in the header: a subject compared by `id` alone can no longer
differ from one compared whole, since both sides' kind is now a function of
the id, so the weakening is unreachable and a mutation that cannot fail is not
a proof.

`cargo test -p huginn-mind --lib` 57 passed, 0 warnings; `cargo check
--workspace` clean; Cut 8 and Cut 9 suites green-and-killed through the
harness. The lock moves the one pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:41:17 +02:00
Meta Vens
333e1c4314 Give the two failable loosenings entries of their own
Cut 9's entries file recorded three weakenings as unfailable. Two of them
are reachable in this Body, so they are entries now:

- The `semantic` check below `Reader::new` changes the answer for a store
  the reader refuses -- a doubled receipt, a row that does not decode --
  from the semantic refusal to the integrity one. `V8L` is that move, and
  the semantic test asks a semantic query of a mind whose receipts name one
  write twice.
- `resolutions_of` comparing a subject by `id` alone is reachable because
  the read side validates no `PipelineRef` it is handed. `V10L` is that
  comparison, and the subject-history test asks for a `Ruling` ref carrying
  a question's id after that question has ten resolutions.

`assignments_of` ignoring the instance stays recorded as unfailable, with
A5 and the derivations scoped to this mind as the reason.

`specs_without_report` compares the citation by exact id, but every fixture
report cited `.r1`, so the test could not tell an exact id from a cut
prefix. cut-9 now carries a report of the revision it superseded and cut-12
is a superseded revision no report names, so standing and citation are each
still visible on their own, and `V13L` compares by cut prefix.

Fork, not closed here: Soul asked that `view` and `history` validate the
ref through the leaf and refuse a malformed one typed rather than answering
empty. The leaf's `Bounded` is `pub(crate)` at the pinned rev d5a36c2, so
`PipelineRef::validate` cannot be called from this crate and the fix is an
Epiphany change. The history test pins today's empty answer and names the
reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:19:42 +02:00
Meta Vens
8fc39b127e Give the read side a mutant per rule
`tools/eureka-cut9-mutations.psd1`, V1-V23: every rule the views decide,
with the edit that removes it and the test that must fail while it is
applied. A rule with two weakenings gets two entries, `Vn` for the rule
absent and `VnL` for the rule weakened rather than removed, so a suite
that only catches the crude form does not pass for the subtle one. 34
entries; M0 green and every one killed.

V1 is the proof that in force has one owner: its mutant lands in
`docs.rs`, and V1L is H41's own edit run against the views instead of
the rules. One mutant, two suites.

Four tests grew to put a mutant in reach of a real failure, which is the
point of writing the entries before trusting the suite:

- Two receipts naming one write, forged and planted, so the integrity
  check that refuses them is not dead text.
- A cut spec whose withdrawal is itself withdrawn, so `base` has two
  steps to walk and stopping at one is visible.
- A `cut-1` spec beside `cut-9` and `cut-10`, so the prefix that ends at
  the dot is not the same answer as the prefix that does not.
- A second report of one cut, judged by nothing, so "this cut has a
  verdict" and "this report has one" come apart.

Three weakenings the map names have no entry and say why in the file's
header: the `semantic` check moved below the filters still refuses;
`resolutions_of` ignoring a subject's kind cannot matter while an id
carries its own kind segment; `assignments_of` ignoring the instance
cannot matter while A5 refuses a stewardship naming another. A mutation
that cannot fail is not a proof, and listing it would only make the
suite look thorough.

Nine cut-8 entries moved with their code in the first commit; that suite
is green as well, H1-H67, M0 included.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:30:34 +02:00
Meta Vens
5ae34f8147 Move every derivation out of admission into docs.rs
`Docs`, `Staged`, `Held`, `kind_of_type` and `later_than` leave
`admission.rs` whole and land in `src/docs.rs`, so the read side of Cut 9
can reach the derivations without admission exporting its internals. One
owner for in force: the views will call the same `Docs` the rules do,
never a second copy.

Three edits ride along, each an extraction the views need and admission
now runs through:

- `closing_resolution_unless` is `in_force_unless`'s body, returning the
  resolution that closes a document rather than a bool; `in_force_unless`
  is `.is_none()` over it.
- `resolutions_of` and `assignments_of` are the scope selectors
  `latest_resolution`, `latest_stewardship` and `stewardships_of` already
  spelled out one at a time.

Nothing else changes: same bodies, same call sites, `admission.rs` keeps
every rule and every `docs.` call reads as before. Its module doc says
where in force lives now.

Nine cut-8 entries re-anchor because their code moved: H33, H41, H62,
H63, H65, H66 and H67 by file, H47 and H60 on the scope selectors the
extraction introduced. Their rules are unchanged, and H49's mutant still
compiles because `Docs`'s fields are `pub(crate)`.

`cargo test -p huginn-mind --lib`: 46 passed, 0 failed, 0 warnings.
`eureka-cut8-mutations.psd1` through Epiphany's harness: M0 green,
H1-H67 all killed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:13:03 +02:00
Meta Vens
1bcb7ceb3e Pick the assignment a replayed hand-off withdraws by content
A hand-off's derived withdrawal read whichever stewardship `stewardships_of`
listed first. While the withdrawal is being derived the hand-off's own is
excluded from what stands, so two assignments are in force at once: the one
this hand-off withdrew, and whatever stewards the repo now. Key order does not
tell them apart -- once a repo has changed hands ten times, `n11` sorts before
`n2` -- so replaying an old hand-off derived a second withdrawal of a standing
assignment and died on the identity collision instead of answering with its
first receipt. `stewardship_of` now takes the stewardship whose withdrawal
already carries this hand-off's key, the same content match the ruling's answer
and the receiving side's assignment already use, and falls back to the one in
force. The comment claiming at most one is ever in force said the opposite of
what the exclusion does; it is gone.

`later_than`'s resolution arm is deleted. Nothing could reach it: the Q19 cap
refuses a withdrawal whose subject is a resolution of a resolution before the
reinstatement rule asks what stands later, so the arm was dead and the entry
Soul wrote against it could not fail. Dead code with a test that cannot reach
it is what subtraction is for -- an arm no caller enters is a rule nobody can
break and nobody can rely on, and leaving it would have bought a green mutation
line for a branch the machine never runs. The two doc sentences that named the
resolution scope go with it.

Four fixtures close gaps where the code was right and the suite could not tell
it from something weaker: a ruling answering a reopened question takes the next
sequence rather than reproducing the withdrawn answer's; a cut spec's revisions
are read within its own cut, so cut B's second does not block reinstating cut
A's first; `later_in_force` counts only what stands, and reads the batch, so an
assignment derived beside a withdrawal still blocks it.

Entries H62-H67 pin all six. H1-H67 killed by their own tests, M0 green;
46 tests pass with no warnings; `cargo check --workspace` clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:58:34 +02:00
Meta Vens
7b6773020d Re-anchor H45 and H46 on the derivation they still pin
Both anchored on the `latest_resolution`/`latest_stewardship` call `derive`
no longer makes, so the harness stopped at H45 with no verdict rather than
running. The rule each entry states is unchanged -- a derived record takes
the next sequence of its scope, not always the first -- so each moves to the
line that now decides it and keeps its killer.

H1-H61 all killed, M0 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:29:38 +02:00
Meta Vens
564d492853 Carry the follow-up's ten mutants in the suite that found them
H52-H61, each restoring one permissiveness the follow-up closed and naming
the test that must fail while it is applied.

H52-H54 revert the three derivation sites to an unconditional `latest + 1`,
one per derived kind, killed by the two replay tests. H55 and H56 drop the
reinstatement check against the retaken repo and the superseded revision.
H57 and H58 let each sequence rule accept a gap, which only a test whose
sequence is above the expected one can tell from the rule. H59 reads the Q19
cap's subject from the image alone, H60 `latest_resolution` from the image
alone, and H61 sets the hand-off's derived withdrawal to sequence 1 -- the
three Soul's second pass could restore with the suite still green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:21:16 +02:00
Meta Vens
fcb7fdd061 Pin the reopened subject, the retaken repo, and six blind spots
Two tests for the rules the last commit landed.
`a_withdrawn_resolution_reopens_its_subject_and_stays_readable` withdraws a
closure, admits the next one at `n2`, reads the first back with its outcome
intact, refuses a third while `n2` stands, refuses the withdrawal of the
withdrawal, and has a ruling answer the reopened question -- the derived
resolution lands at `n2`, not `n1`.
`a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer` refuses a
second assignment while the first stands and a second claiming the taken
sequence, hands the repo away, finds nothing stewarding it, hands it back and
gets `n2` with the second `handed_on`, the first still readable by key.
Entries H40-H46 carry the mutants: the two sequence rules, the recursive
in-force, `AlreadyStewarded`, the Q19 cap, and `derive`'s two sequences.

Then the six places Soul found the code right and the suite blind, each a
fixture and an entry (H47-H51): a handed-away repo is not stewarded, so
`stewardship_of`'s in-force half is pinned; a document two batch documents
cite is pinned once rather than cancelled; what a derived write cites in the
image is pinned, which is H32's surviving half; a finding may cite the
vocabulary of the target landing beside it; `Answered` coherence is checked
against a ruling in the image as against one in the batch; a branch differing
only in case is a mismatch.

H32 is re-anchored rather than left to rot. Its A4 half is unreachable now: a
derived write takes the sequence after the batch's own records, so a derived
identity cannot collide with one the batch holds. What survives is that
derived writes are validated, staged and resolved like any other, and the
transfer test's strong read is what fails when they are not.

README stops claiming the two stub crates do anything.

H1-H51 all killed, M0 green. 33 tests, 0 warnings; `cargo check --workspace`
clean; `git diff --stat 4094e68 -- .voidbot` empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:44:15 +02:00
Meta Vens
30daff8822 Pin the nineteen rules Cut 8 held but nothing tested
Soul's pass found the suite claimed more than it proved: H1-H20 killed
twenty mutations, and nineteen further permissivenesses could be restored
with every test still green. A rule no test reaches is a rule the next
pass may delete by accident.

Two of them were about the receipt's atomicity and its reach, and needed
new machinery. A store in `receipt.rs`'s tests records the replacement set
of every swap it is asked for, which lets one test say what no assertion
over the resulting image could: a commit is one compare-and-swap, and the
documents and their receipt go in it together. Splitting the receipt into
a second swap now fails. Separately, a batch citing three image documents
is pinned against all three, so truncating the strong reads to the first
of them fails.

The rest are one fixture apiece, each closing a row the per-kind rules
already held: stewardship looked up in image as well as batch; the cut
report's repo as well as its branch; a promise measured by the claim that
names that label; the invariant vocabulary taken from the in-force target
and not a superseded one; a question that is answered or withdrawn but
never superseded, and a target never answered; the derived stewardship
dated by the hand-off; derived writes passing A3-A7, so a derived identity
the batch already holds is A4's refusal and not a store error; A4 and A2
themselves; a question's two distinct options and its recommendation among
them; unique labels on a question's options and a target's invariants; a
finding's locations; and an `Answered` naming the ruling that actually
answers its subject.

`tools/eureka-cut8-mutations.psd1` grows to H1-H39. H21 and H22 pin the
opener's epoch key and count, H23 `EmptyRepos`; the rest are Soul's own
survivors, adopted as entries so the thing that found them is the thing
that guards them.

Harness: M0 green at 31 tests, H1-H39 all killed by their own test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:06:51 +02:00
Meta Vens
ca30d3eae5 Pin Cut 8's rules with twenty mutations
Cut 8, Huginn half, commit 4 of 4: `tools/eureka-cut8-mutations.psd1`,
entries H1-H20, each the exact source change that restores one
permissiveness this cut closed and the one test that must fail while it
is applied. The suite runs through Epiphany's `tools/eureka-mutations.ps1`
with `-Repo F:\Projects\Huginn`; Huginn holds only the entries.

H5 is a four-edit entry: `Mind::open` is switched to the transient redb
store, which needs a `MindStore` impl the live crate deliberately lacks,
so the entry supplies it in `store.rs`. That makes `store.rs` a fourth
harness target beyond the three the spec names; the file's header says
so. H7 moves the replay check above the per-kind rules rather than above
A7, since the digest it looks up needs A7's strong reads to exist.

Run 2026-09-16 against `0bd7133`: M0 green with 26 tests, every entry
killed by its named test, every target restored byte for byte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:17:00 +02:00