Commit graph

102 commits

Author SHA1 Message Date
Meta Vens
e770fb9f21 BP-2: move Huginn to CultLib 6251cbc and the RS-L leaf 4891f2e5
One pin move: huginn-mind pins cultcache-rs at 6251cbc and the leaf at
4891f2e5; huginn-daemon pins cultnet-rs at 6251cbc. cargo tree shows one
cultcache-rs and one cultnet-rs per crate.

Adopting RS-L, which the new leaf carries:
- Campaign, cut-spec, question and ruling fixtures carry a Title; the
  question and ruling fixtures gain the new required title field.
- The epoch literals in daemon and envelope tests move v1 -> v2. A store
  written at an earlier epoch is refused at open (ForeignEpoch); no key
  migration.
- Derived stewardship, hand-off and resolution keys are now the lowercased
  OrgRepo::identity(); test key literals follow.
- The two published wire schemas are regenerated (Title, OrgRepo bound and
  description); the byte-for-byte pin test passes.

CultLib 6251cbc gives CultNetMessage::Error code and details; the four
construction sites and one pattern in huginn-daemon take None and `..`. No
behaviour change: None is skipped on the wire. mind 70 and daemon 18 pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:56:03 +02:00
Meta Vens
8c5f784606 Fix stale test-support and test-door prose
The test-support feature comment in huginn-mind/Cargo.toml named only
store_path_for; it also gates open_with. The RS-1 grammar-door test's
doc called open_with "the door open shares" -- open and open_with
both call open_checked, which is the actual shared construction site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:57:37 +02:00
Meta Vens
c2c9ff77e8 Correct open_with's release-binary doc: no Idunn recipe pins the build yet
The doc on open_with claimed it never compiles into a release binary.
That is only true for `cargo build --release -p huginn-daemon`; the
test-support feature is unified in by `--all-targets` and `cargo
test`, which do produce a huginn-daemon binary carrying open_with.
Checked F:\Projects\gamecult-ops\deployment\idunn-bindings and
F:\Projects\gamecult-ops\idunn for a Huginn deploy recipe: none
exists yet, so nothing currently pins which command ships. State the
actual condition instead of a release-only guarantee no recipe backs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:57:20 +02:00
Meta Vens
68c4f0e1b4 Narrow Mind::receipts to pub(crate) and true up head's doc
Every caller of Mind::receipts lives inside huginn-mind (admission.rs,
query.rs, receipt.rs); nothing outside the crate needs raw,
unchecked receipt ordinals. Narrowing it to pub(crate) closes the
only door that let a caller read ordinals without going through
receipt::head's density check, which is what head's own doc claimed
already held. Update that doc to say so explicitly instead of
asserting a door was closed that was, in fact, still open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:57:06 +02:00
Meta Vens
fbf793fdbc Un-degenerate the R5b/R5c gate-order fixtures
The two fixtures pinning the epoch-first gate order used
"epiphany.pipeline.campaign.v1" as their foreign type. That id is the
pinned leaf's real, currently-registered campaign kind, so
is_known_type accepts it and a gate-order mutant that ran the type
gate first still passed both fixtures by accident (confirmed on
Yggdrasil: both hand-applied mutants survived 70/70 before this fix).
Re-point both at a new NEVER_A_KNOWN_TYPE constant that cannot collide
with any real PipelineKind id at any epoch this leaf pin carries, so
the fixtures actually exercise the epoch-first order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:56:50 +02:00
Meta Vens
7771c37c32 Delete every committed mutation suite
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove tools/eureka-cut8-mutations.psd1
(825 lines), eureka-cut9-mutations.psd1 (371), eureka-cut10-mutations.psd1
(994) and eureka-readside-mutations.psd1 (225). cut9, cut10 and readside
carried uncommitted edits that are now moot and are dropped with the
files. The harness that ran them, eureka-mutations.ps1, was already
deleted from the Eureka skill (37c80e4).

Code comments in crates/huginn-mind/src/mind.rs and
crates/huginn-daemon/src/serve.rs that cited entry ids from the
deleted suites (R5b, R5c, N1, N3, S2, S4, S3wide, Residue S1) are
reworded to drop the id or name the test that pins the rule instead;
each comment already sits beside that test. Comments citing F- and
other S/A-prefixed labels that do not match any entry in the deleted
files (F2-F6, S1, S5, A1, and the D3/D4/D5/D8 in query.rs, which name
a different rule than the D3/D4/D5/D8 entries in the deleted files)
are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:45:53 +02:00
Meta Vens
056db28792 RS fix batch: read-side density, the smallest seal, R5's two shapes, stale prose
F1: AdmissionIndex::build now calls receipt::head before copying any
ordinal, so query and view refuse a receipt chain that is not exactly
{1..=N} (duplicate, gap, zero, or above-N) instead of trusting a stored
ordinal unchecked. New test query::tests::query_and_view_refuse_a_receipt_chain_that_is_not_dense
pins it behaviourally: it plants each broken chain directly (bypassing
admission) and asserts query and view both refuse. The schema's ordinal
gets minimum: 1 (was 0), matched by #[schemars(range(min = 1))] on
AdmissionFacts::ordinal. head's doc now says AdmissionIndex::build asks
it too.

F2: Mind::open_with is renamed to an internal open_checked, and a
test-only Mind::open_with is gated behind cfg(test) or the test-support
feature, exactly like store_path_for. Production reaches open_checked
only through Mind::open, which takes the store's per-path lock first.
mind::tests::a_mind_has_one_owner_at_a_time (pre-existing) still pins
that a second Mind::open on the same path is refused. The full store
seal against a second in-process owner over an arbitrary MindStore stays
a recorded follow-up. lib.rs's crate doc now names Mind::open as the
owner of "may this store be this instance's mind".

F3: R5 is not binary. Two loosenings are pinned as R5b and R5c in
mind.rs, with behavioural fixtures: a foreign type with no epoch record
at all still refuses MissingIdentity, not ForeignStore (R5b); an epoch
record at the current key with a foreign value, beside a foreign type,
still refuses ForeignEpoch, not ForeignStore (R5c).

F5: gate step numbering in mind.rs's doc comment and its test comments
now read 2 (epoch), 3 (type), matching the code's actual order. docs.rs
no longer credits resolutions_of/assignments_of with reading history,
which RS-2 deleted. The two-door test in query.rs is renamed from
a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors to
..._by_view, since view is the only door left.

Per the operator's ruling retiring the anchor-based mutation harness,
this batch carries no .psd1 changes; the cut9/cut10/readside mutation
files have uncommitted edits from the earlier pass (stale references to
the old test names, and R3L's target) left for a later pass to fold in
or drop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:34:47 +02:00
Meta Vens
0d6f9a60d7 Cut RS-1: the receipt ordinal, receipt v2, and the epoch-first opener
Per eureka-read-side-cut.md. The receipt goes to v2 (type id and schema
version both huginn.mind_commit_receipt.v2) and gains ordinal: u64 at key
7. candidate takes the ordinal; digest stays unchanged, so an exact
replay keeps the ordinal its first admission assigned. validate refuses
ordinal == 0. New receipt::head(mind) decodes the receipts and requires
their ordinals to be exactly {1..=N}, refusing Unavailable otherwise;
admission is the only caller, passing head(self)? + 1, and no other code
computes an ordinal. AdmissionFacts gains ordinal, filled by
AdmissionIndex::build; the order query.rs sorts by is unchanged (RS-3
deletes it, not this cut).

mind.rs's opener now calls refuse_foreign_epoch before
refuse_foreign_types, so a store written at a foreign epoch is refused
as ForeignEpoch rather than ForeignStore: a real epoch bump always moves
every type id, so the type gate would otherwise fire first and the
epoch gate would never be reached for the one case it exists for. The
doc comment is updated to match. Regenerated both published wire
schemas.

New tools/eureka-readside-mutations.psd1 (R1-R5, R1 and R3 with L
variants, R1 also with an off-by-one-head R1L2; R2 and R5 documented as
having no loosening, on the same grounds cut10's D9 and D12 give). Five
new tests carry them: ordinals_are_admission_order_not_clock_or_id_order
and an_exact_replay_keeps_its_ordinal in admission.rs,
a_chain_that_is_not_dense_refuses_admission_and_reads_alike in
receipt.rs, and a_v1_receipt_store_is_refused_at_open plus
a_store_written_at_the_previous_epoch_is_refused_by_the_epoch_gate in
mind.rs. cut8's H6 is re-anchored on the new gate order (its own rule,
every gate before attach, is unchanged); cut8 (67), cut9 (27) and cut10
(65) all rerun clean, because candidate's signature and the opener's
order both changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:56:04 +02:00
Meta Vens
92ac16c5b0 Cut RS-2: delete open_items, history, and the admission window
Subtraction only, per eureka-read-side-cut.md. Deletes PipelineOpenItems,
HistoryScope, open_items, history, and the admitted_after/admitted_before
window from the read side, their wire variants and arms, the daemon arms
and tests that exercised only these, the OpenItems/History lines from the
instance-gate tests (each keeps its query and view cases), and the README
clause. serve.rs's N4 gate fixtures use whoami in place of open_items as
the long operation name; the case is kept.

Deleted the now-dead Reader::views() helper and the 'a lifetime it forced
on Reader, and stripped the History-only assertions from two tests the
map did not name (a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors
and views_read_admitted_at_from_the_receipt_not_stored_at) that also called
the deleted API. Regenerated both published wire schemas.

Removes cut9 V9-V15, V22 and V22L (12 entries incl. V13L/V14L), and cut10
D19, D19L, D23, D23L, S1 and S1d (6 entries). D13 and D13L are re-anchored
on the surviving view/query arms; D13L uses a near-miss spelling rather
than a pure casing change, since PowerShell's default -eq made a casing
mutant read as "changes nothing" to the harness's own guard. V24 collapses
from a two-door Edits array to a single edit now that view is the only
door left. cut9 (27 entries) and cut10 (65 entries) both rerun clean with
M0 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:51:32 +02:00
Meta Vens
f7e0d020ed Move the epiphany-pipeline pin to the leaf commit carrying S5 and S8
Epiphany codex/eureka-pipeline-state b3ae787b extends the leaf's door
test with the 64-byte whole-name and per-label bounds and NUL, adds
three matching mutation entries, and corrects the entries file header
that still named the deleted repo-local harness path. One cultcache-rs
throughout: epiphany-pipeline, cultnet-rs and huginn-mind all still
resolve to CultLib a0813c6e.

Verified: cargo test --workspace (huginn-mind 65/65, huginn-daemon
20/20, eureka-state 0/0), cargo tree -i cultcache-rs shows one node.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:21:56 +02:00
Meta Vens
65c46aa59d Residue batch on Cut 10's fourth fix batch: seal path_for, fill N1's gaps, pin the gate's other axis
S3: Mind::path_for bypassed the grammar door and the daemon's own tests
constructed stores through it directly. It is now private; a test-only
Mind::store_path_for validates through require_grammatical_slug first,
gated behind cfg(test) here and a test-support feature dev-dependents
opt into (huginn-daemon's Cargo.toml). The four daemon test call sites
and one huginn-mind test call site now go through the door.

S2: N1's door, require_grammatical_slug, had no mutation entry for two
of its three callers (open_with and require_instance survived deleting
it; open already had N1's own escape test). Added a test per caller and
a mutation entry for each of the three deletions (N1open, N1openwith,
N1requireinstance) in tools/eureka-cut10-mutations.psd1.

S4: a Unicode hyphen (U+2010) folded to ASCII '-' before the grammar
check survived. Added a fixture and mutation entry (S4fold) pinning
that no such fold runs before Slug::validate_slug.

S1: the size-gate fixtures varied operation and runtime id together
(4/16, 4/16, 10/19), so a formula tracking one and dropping the other
still passed. Added a fixture varying them independently (10/16 and
4/19) and two mutation entries (N4c, N4d) for the two formulas that
each drop one term.

Verified: cargo test -p huginn-mind --lib (65/65), cargo test -p
huginn-daemon --lib (20/20, one more than before: the new
independent-axis gate test). Mutation suites through the Eureka
harness: Cut 8 67/67 killed, Cut 9 38/38 killed, Cut 10 77/77 killed
(71 before this batch, +6: N1open, N1openwith, N1requireinstance,
S4fold, N4c, N4d). M0 green in every suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:21:16 +02:00
Meta Vens
4f7e3b5edc N3 and N4: one generated separator test, and gate fixtures that vary
operation and runtime id

N3 (medium-low). One generated test, mind::tests::every_separator_variant_
of_a_declared_name_is_foreign_not_the_mind, replaces the separator-only
fixture pairs' narrower coverage. A base name carrying all three separator
bytes a Slug permits at three non-adjacent positions yields 26 same-length
variants by cartesian substitution, plus 3 dash-padded and 1 doubled-dash
variant (30 total); every one must be refused as ForeignInstance. Kills
four mutants of require_instance's comparison: folding `_`/`.` into each
other on both sides, folding `_` to `-` on the mind's own side alone,
trim_matches('-'), and collapsing `--` to `-` (N3a-d).

N4 (low). The size-gate boundary fixtures (the_gates_boundary_* in
serve.rs) held operation and source_runtime_id fixed at "view" and
"huginn-yggdrasil", varying only message_id -- so a formula of message_id's
length alone, shaped to fit exactly the suite's two lengths, would never be
wrong there. synthetic_with_id and sized_exactly_with_id are now thin
wrappers over synthetic_full/sized_exactly_full, which also take operation
and runtime_id; a new test, the_gates_boundary_holds_across_operation_and_
runtime_id, holds the boundary at view and open_items over two runtime
ids. Kills two coincidental overhead formulas that fit the old fixture's
two message_id lengths by construction: payload+228+id.len()+(id.len()>31)
(N4a) and payload+228+id.len()*38/37 (N4b).

All three suites rerun clean: cut8 67/67, cut9 38/38, cut10 71/71 (65 plus
N3a-d and N4a-b), every M0 green, restores hash-verified. cargo test
--workspace: huginn-mind 62/62, huginn-daemon 19/19.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:15:30 +02:00
Meta Vens
cc71a569dc Fourth fix batch: fill N1 in its owner, and correct two ledger claims
N1 (medium-low). Moves the epiphany-pipeline pin to the leaf's new commit
(9d3a3efd), which opens Slug::validate_slug. Huginn's own cultcache-rs pin
is untouched; cargo tree -d still carries exactly one cultcache-rs.

Mind::require_instance now validates the declared name's grammar before
comparing identity, so admission's A1 and every read refuse a bad name
identically, through one door. Mind::open and open_with validate the
declared instance before path_for ever runs, so a Slug carrying `..` and a
path separator (Soul's outer/inner probe) is refused before any store is
created, not after it has already escaped the state root.

require_grammatical_instance and its daemon-side branch in daemon.rs are
deleted: the throwaway PipelineInstance wrapper it built is no longer the
only door onto the leaf's grammar. The refusal now names the field a
caller actually held the declared name under (declared, or instance) and
the whole declared name, not the old wrapper's instance.instance field or
a single failing label.

Adds a leaf-escape regression test pinning Soul's probe and asserting no
store is created, and a fullwidth-instance test showing Admit and Query
refuse identically. Updates the cut10 grammar-fold mutant (S3wide) to the
new function shape and the affected daemon test to the new field name.

N5 and N6, prose only. The cut10 entries gain a note by S1 and S1d: neither
mutant "dies on all three" faults its shared fixture carries, contrary to
an earlier claim; S1d exempts the orphan fault by construction and is
killed by the undecodable-document block instead, the next one the test
body reaches. The cut9 header's self-contradiction at lines 18-25 is
resolved (nothing in the suite exercises the door, not "nothing reaches
it"), its self-reference now points at store.rs:18-19, and its line-1
range is corrected to V1-V24L.

All three suites rerun clean after the pin move: cut8 67/67, cut9 38/38,
cut10 65/65, every M0 green, restores hash-verified. cargo test --workspace:
huginn-mind 61/61 (was 60), huginn-daemon 18/18 (was 17).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:14:34 +02:00
Meta Vens
7fc8ed3610 Correct store.rs and the cut9 header's no-second-writer prose
F7: store.rs claimed "no non-test code outside this crate touches a row
directly." False: MindStore's supertrait CacheBackingStore and the public
compare_and_swap_batch already let it. Rewrite the comment to say so, and
name the re-export honestly -- two traits, CacheBackingStore and
DatabaseEntry, plus DatabaseEntry's own derive macro riding along under the
same name in a different namespace. Sealing the store so admission is its
only write path stays a recorded follow-up, its own cut; nothing here seals
it.

The cut9 entries header rested the same false premise: "no behaviour this
Body can reach" for assignments_of and resolutions_of's two weakenings
assumed admission is the only write path. Corrected to "not yet reached,"
with the reason -- a row planted through the store port directly bypasses
A5's instance check and the leaf's kind/id grammar alike.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:22:41 +02:00
Meta Vens
56143c36ea Name the harness's real path in the cut8 entries header
F5: cut8's entries header still named the deleted
F:\Projects\Epiphany\tools\eureka-mutations.ps1. The harness moved to
C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1 on 2026-09-17;
cut9 and cut10's headers already needed the same fix and are corrected
alongside their own entries in this batch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:22:30 +02:00
Meta Vens
7f60310039 Pin the size gate for a second message_id length
F2: the boundary test always used message_id "m-0", so a gate that measures
payload.len() + 231 -- exactly that id's fixed overhead -- passed unnoticed.
message_id rides the encoded envelope and echoes the client's own value, so a
longer id changes the overhead the gate must measure honestly.

Parametrize synthetic/sized_exactly by message_id and add a second boundary
test at a 40-byte id: 1,228,800 bytes still passes and 1,228,801 still
refuses. Add mutation entry D20L6, the "+231" formula, which dies against the
new test and survived the old one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:22:09 +02:00
Meta Vens
4fb5742a7d Add D1L6 and D1L7, the separator-fold loosenings
F1: two mutation entries against Mind::require_instance, folding `_` and `.`
into `-` before comparing. Both die against
require_instance_is_the_one_check_admission_and_the_daemon_share's new
separator-fold assertions (the prior commit); neither had an entry before,
since no fixture pair differed by separator alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:22:05 +02:00
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
7611d1bbf1 Give the instance check a fixture pair that differs by separator alone
F1: thought-cage, thought_cage and thought.cage are three legal Slug names,
not one, so a comparison that folds `_` or `.` into `-` at
Mind::require_instance answers one mind for another. Add the fixture pair
and a mind opened under a separator-bearing name to
require_instance_is_the_one_check_admission_and_the_daemon_share, and two
mutation entries, D1L6 and D1L7, that fold each separator into `-`; both die
against the new assertions.

F4: fixtures.rs described Label's grammar on the CASED_INSTANCE comment as
though it were Slug's. Corrected to say what Slug actually is at the pinned
Epiphany revision: dot-joined labels, bounded to 64 bytes whole.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:19:30 +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
a235304ae5 Describe the daemon that exists instead of the stub that does not
`huginn-daemon` serves a mind, so the README and AGENTS say what it serves,
over which envelope, in which order it starts, and where its two published
schemas live. `eureka-state` is the remaining stub. The command block gains
the daemon's own invocation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:00:28 +02:00
Meta Vens
6b619d6bbd Serve one mind over CultNet, and no rule beside it
`huginn-daemon` becomes a library and a binary. `Daemon` holds one mind and
dispatches a `HuginnMindRequest` to the `Mind` method that answers it, with no
socket and no clock: `now` is the caller's. Every read that names an instance
is put to `Mind::require_instance` before the method runs, so ruling 14 is
checked once, by its owner, whichever side of the wire the request came from.
`IndexSink` is where Cut 11 attaches; its error is logged and the outcome is
what the mind decided.

`envelope` carries the vocabulary in CultNet's `cultnet.operation_request.v0`
and `cultnet.operation_response.v0` as base64 of named MessagePack, which is
what the C# reference and Eve's plugin ABI speak. The envelope's operation
string is checked against the decoded payload rather than trusted. A message
that does not decode is answered with a typed failure naming its code, and
reaches no mind.

`serve` binds one non-blocking socket on CultNet's operation connection id and
answers each frame on the session it arrived on. A refusal is an answer with a
`rejected` status, never a transport error. The two wire schemas go out on a
schema catalog request. A hostile datagram and a departed peer are logged and
served past. `startup` is the one place the mind opens and the socket binds,
in that order, so a mind that will not open leaves the port free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:59:33 +02:00
Meta Vens
c4b2a0fa2e Give the mind a wire vocabulary and publish its two schemas
`huginn-mind::wire` names one request per `Mind` method plus `whoami`, and one
response per method plus the read side's refusal. The payloads are the
admission and read types unchanged, so a client speaks the organ's own
vocabulary rather than a projection of it. `operation()` and `instance()` say
what the envelope carries; `status()` derives the envelope's status from the
response, and a refusal is data in that response rather than a transport
failure.

`Mind::status` answers `whoami` from the image every time it is asked: the
instance, the schema epoch, and how many pipeline documents and receipts the
mind holds. Nothing stores a status and no configuration supplies one.

`schemas/cultnet/` publishes the JSON schema of each wire type, derived from
the type and pinned to it byte for byte by a test, so the schema catalog
cannot advertise a shape the crate does not speak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:52:53 +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
49cc6e3e62 Read a mind: views, queries, open items and history
`query.rs` is the read side, over a `&Mind<S>` with no store file, no
daemon and no index. A `PipelineDocumentView` is four fields: the id,
because a `PipelineDocument` does not carry its key and a client must
never derive one; the document; the facts of its admission; and its
status.

The facts come from the commit receipts and from their `writes` alone,
so a document a later batch cited as a strong read keeps the receipt
that wrote it, and `admitted_at` is that receipt's `committed_at` -- the
`now` admission was passed -- never the store's `stored_at`. A pipeline
document the image holds and no receipt wrote is an integrity fault and
refuses; the store has one writer and the receipt is its proof.

Status is derived per call through `docs::Docs`, the same owner the
admission rules ask, so a withdrawn resolution reopens its subject for a
reader exactly when it reopens it for a rule. `closing_resolution` is
`in_force_unless`'s own body asked for the resolution rather than the
bool, so there is no second derivation to drift.

Four reads: `view` by id, `query` over the filters (root, repo, cut,
kinds, in force, faculty, an admission window), `open_items` for one
campaign's unanswered questions, standing findings, follow-ups,
unreported specs and unjudged reports, and `history` for every
resolution a subject ever had or every assignment of a repo to this
mind, withdrawn ones included with their reasons.

A query is ordered by `(admitted_at, id)`, capped at 200, and carries
`matched`, the count before the cap, so a truncation is never a short
page that reads like the whole answer. `semantic` is accepted by the
type and refused typed until Cut 11 owns the index; no substring filter
lives here to become a second answer to "the text of a document".

`cargo test -p huginn-mind --lib`: 56 passed, 0 failed, 0 warnings
(46 + the ten tests). `cargo check --workspace` clean. No dependency, no
target, no kind, no schema, no epoch; `Cargo.toml` and `Cargo.lock`
untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:22:41 +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
f566fe6bb3 Re-derive what the batch derived, and stop re-raising an earlier record
Two rules Soul's follow-up pass falsified, both in admission.

A batch that derives a write could not be replayed. `derive` set every
derived sequence to `latest + 1` over an image that already held the first
admission's derived record, so the second admission produced a different
document, A8 refused it -- `AlreadyResolved` on the source side, and
`AlreadyStewarded` on the receiving one -- and A9 never saw the digest it
would have recognised. `derive` now reproduces the record it already made
when image or batch holds one, found by what the derivation writes into it: a
resolution `Answered` by this ruling, a withdrawal whose reason is this
hand-off's key, an assignment whose note is. The same document comes back
byte for byte, A9 answers with the first receipt, and only a batch that never
landed takes `latest + 1`.

A withdrawal could re-raise an earlier record over a later one, which is the
rationale Q19 A's cap rests on and the cap alone does not carry. Withdrawing
a resolution puts its subject back in force, so a repo handed away and handed
back could have the hand-off's withdrawal withdrawn and stand under two
assignments at once; a supersession withdrawn while the revision that
superseded it stood left two revisions in force, which is what the finding
rule reads for its invariant vocabulary. `WouldReinstateOverLater` refuses
that: the record the withdrawal would reinstate, against anything in force
and later in the scope that record is sequenced in -- `(instance, repo)` for
a stewardship, the subject for a resolution, the campaign for a target and
the cut for a cut spec.

Four tests, one per fix and kind: the ruling's resolution and both sides of a
hand-off replayed to `AlreadyAdmitted` with the first receipt and no second
receipt in the store; the retaken repo and the superseded revision refused,
with the later record still the only one in force.

Four more tests for rules the code already held and the suite could not tell
from weaker ones: a sequence gap refuses on both sequenced kinds rather than
only a taken sequence; the latest sequence counts the batch as well as the
image; the Q19 cap reads a chain landing in one batch; and the hand-off's
second withdrawal of one assignment takes the next sequence, not `1`.

41 tests, 0 warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:21:04 +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
3efc0dc82d Take the sequenced keys and make in force mean what it says
The pin moves to Epiphany `d5a36c2a`, where a resolution's key ends in its
per-subject sequence and a stewardship's in its per-(instance, repo) one. Two
fields arrive with it, so `stewardship` and `resolution` in the fixtures set
`sequence: 1` and `stewardship_n`/`resolution_n` say otherwise when a test
needs a second record. Every literal key that gained `.n1` moved with it,
`id_of` included.

The rules the sequence was for:

- `in_force` is recursive. A document is in force when no resolution that is
  itself in force names it, so a withdrawn resolution stops closing its
  subject. The old one-level version counted a withdrawal as if it still
  stood, which is half of why a reopened subject could not be resolved again.
- The other half was A10. `refuse_collisions` no longer has a `Resolution`
  arm: a resolution key is not one-per-subject any more, so a collision on it
  means what it means for every other kind, and whether a subject is closed
  is A8's question.
- A8 asks it, for both sequenced kinds, in the same two steps. The sequence
  is the writer's and must be the previous plus one over image and batch,
  counting every record of that scope but this document --
  `ResolutionOutOfSequence`, `StewardshipOutOfSequence`. Then no record of
  that scope may still be in force -- `AlreadyResolved`, `AlreadyStewarded`.
  With both, at most one stewardship of a repo stands on a mind, which is
  what `stewardship_of` has been assuming.
- Q19 A is enforced rather than described: a resolution whose subject is a
  resolution of a resolution is `IncompatibleResolution`. A withdrawal cannot
  be withdrawn; a closure is reinstated by resolving the subject again.
- `derive` sets a sequence on each write it makes, from the same image the
  rule reads.

Two existing tests move with the rules. `subject_resolves_at_most_once` is
now `a_subject_with_a_resolution_in_force_refuses_another` and separates the
two refusals. The hand-off test's last case used to be an A4 collision
between a batch's own stewardship and the derived one; the derived write now
takes the sequence after the batch's, so there is nothing left to collide and
the batch is refused as out of sequence instead.

31 tests, 0 warnings; `cargo check --workspace` clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:28:17 +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
acd32f35cc Restore the withdrawn resolution: the operator ruled Q17 the other way
The previous commit carried Self's default reading of Q17, that a subject
resolves at most once and a resolution is not itself a subject. The
operator ruled the other way: a withdrawn resolution is kept, attached to
the subject it withdrew, and the subject may then be resolved again.

So the matrix row comes back exactly as it was and
`the_resolution_matrix_is_admissions` returns to admitting the withdrawal
of a resolution. Only the comment above `matrix` is new, and it says whose
ruling this is and what it still needs: a second resolution of one subject
needs a key the first does not already hold, which is a sequence in the
leaf's resolution key and is mapped elsewhere.

`EmptyRepos` and the opener's epoch record stand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:58:42 +02:00
Meta Vens
1cfa81d571 Close the epoch record, the minted leaf refusal and the nested resolution
Soul's Cut 8 pass found three rules the code only half held and one
document that promised more than the crate does.

The opener's step 3 said "exactly one epoch record at the current epoch"
and checked neither the count nor the key: a record keyed anything at all
was read as the epoch record, and a second one was never looked at, so a
current record beside a foreign one passed or refused on nothing but the
order the store returned them in. Step 3 now counts before it reads,
refuses a record under any key but the epoch, and names the defect in
`found`: the stored epoch, the offending key, or the count.

The campaign row minted `PipelineRefusal::FieldBound` for an empty
`repos`. The organ does not get to invent the leaf's refusals; an empty
`repos` is admission's own rule and is now `MindRefusal::EmptyRepos`.

The matrix admitted `Withdrawn` on a resolution, which made a subject
resolvable twice over: withdraw the resolution and the subject is open
again. A subject resolves at most once, and a resolution is not itself a
subject, so `resolution` joins `campaign`, `cut_report`, `verdict`,
`instance` and `hand_off` as not resolvable. The key grammar still spells
a resolution of a resolution; admission refuses it here. Self's default
ruling on Q17, standing until the operator rules.

`README.md` and `AGENTS.md` claimed CultNet publication and a Qdrant
connection in the present tense and called the store `.cc`. The daemon is
a stub, nothing connects to Qdrant, and the live crate writes
`mind.redb`. They say so now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:53:08 +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
Meta Vens
0bd71337fd Admit a batch through one ordered path with every rule in it
Cut 8, Huginn half, commit 3 of 4: `admission.rs`. `Mind::admit`
prepares typed documents through the leaf and calls `admit_prepared`,
the single commit path for this cut, Cut 10's sink, Cut 12's hand-off and
import, and Cut 13's tools. The steps run in order and the first failing
step is the outcome: the batch names this mind (A1), one to sixty-four
envelopes (A2), the leaf's bounds, formats and key recomputation (A3),
unique identities (A4), every instance-carrying document names this mind
(A5), an empty mind's first batch carries exactly one `instance` document
and a batch carrying one derives the epoch record (A6), every reference
resolves in image or batch by `(kind, id)` (A7), the derived writes pass
A3-A7 themselves, the per-kind rules and the resolution matrix (A8), an
exact replay answers with the stored receipt only after validation (A9),
no write collides with the image (A10), the commit (A11).

Derived writes: a ruling that answers derives the `Answered` resolution
of its question; a hand-off derives the stewardship withdrawal on its
source side and the stewardship assignment on its receiving side. "In
force" is computed at rule time from the resolutions in image and batch
and never stored; the two derivations are the only resolutions a rule
ignores when judging its own document.

Two places the leaf is thinner than the spec assumed. `FindingWithoutRange`
is unreachable: a finding envelope without `range` fails the leaf's decode
as `Document(InvalidFormat { field: "payload" })` before any organ rule,
and the test asserts that. A campaign with no repos has no refusal of its
own in the spec's list; it is refused as `Document(FieldBound)` on
`campaign.repos` with a minimum of one. `PipelineAdmissionBatch` cannot
derive `JsonSchema` because the leaf's `PipelineDocument` does not; the
outcome and refusal types do.

The epoch record is derived whenever the batch carries the `instance`
document, which only the first batch may, so a replay of the first batch
digests to the same receipt and answers `AlreadyAdmitted` instead of
colliding.

Tests: 26 (the spec's 25 plus the replay-planting test, the receipt's
deliberate second construction site). `cargo check -p huginn-mind --lib
--tests` and `cargo test -p huginn-mind --lib` pass with zero warnings;
`cargo check --workspace` passes. README says what the crate does now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:13:44 +02:00
Meta Vens
a4c5b79d52 Land a batch whole with a receipt naming its exact bytes
Cut 8, Huginn half, commit 2 of 4: the commit primitive in `receipt.rs`.
`candidate` is the one construction site of a receipt: its id is the
digest of `(instance, strong_reads, writes)`, each list in identity order,
computed before anything is stored so admission can look up an exact
replay. `replay` finds a stored receipt with that id and checks its reads
and writes equal the candidate's. `commit` is one compare-and-swap over
the store, the strong reads as its expectation and the writes plus the
receipt as its replacements; the image is re-pulled either way, and a lost
swap is diffed against the fresh image and typed `Conflict`.

Provenance and `committed_at` are on the receipt and outside the digest,
so the same bytes replayed by another faculty in another session answer
with the first receipt. The store requires every expected identity to be
replaced, so a strong read is re-inserted unchanged beside the writes, as
Epiphany's receipt path does.

Builds: `cargo check -p huginn-mind --lib --tests`, `cargo test -p
huginn-mind --lib` (4 tests). Dead-code warnings remain on the primitive
and the refusing test store until admission consumes them in the next
commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:05:30 +02:00
Meta Vens
946758f666 Open a mind fail-closed over an owned redb store
Cut 8, Huginn half, commit 1 of 4: the store port, the opener and the
identity rules. `huginn-mind` gains its dependencies (`cultcache-rs` at
Epiphany's pin `a0813c6`, `epiphany-pipeline` at `a65c6420`, the Epiphany
half of this cut), `store.rs` with the one narrow `MindStore` trait that
injects compare-and-swap, `mind.rs` with `Mind::open` and `open_with`, and
`refusal.rs` with the organ's refusal vocabulary.

The opener refuses in order and attaches nothing until every gate passes:
a foreign type, a foreign epoch, a missing identity, an identity naming
another instance. The path under the state root is derived from the slug;
the `instance` document is the authority, so a store copied into another
instance's directory is refused. The owned store's exclusive lock is the
single-writer mechanism: a second `Mind::open` of the same path is
`MindAlreadyOwned`.

The receipt type and the two provenance shapes (`Faculty`,
`PipelineProvenance`) land here rather than in the later commits because
the opener registers the receipt's type id, and their only consumer is the
receipt; the commit primitive itself is the next commit. `MindRefusal`
carries a serde remote definition of the leaf's `PipelineRefusal`, which
derives neither `Serialize` nor `JsonSchema` at the pinned rev; it is
compiled against the leaf's type so a leaf change breaks the build rather
than drifting.

`Cargo.lock` is regenerated and gains 90 packages. The map predicted a
CRLF-to-LF rewrite of the whole file; the file was already LF in the index
and stays LF, so the diff is additions only.

README and AGENTS now say `huginn-mind` is live and the other two crates
are stubs.

Builds: `cargo check -p huginn-mind --lib --tests` and `cargo test -p
huginn-mind --lib` (4 tests) pass. This commit carries dead-code warnings
on the store and cache accessors and the refusing test store; commits 2 and
3 consume them.

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