Commit graph

144 commits

Author SHA1 Message Date
Meta Vens
295b602b2e eureka-state tests: common names PipelineRef
8041d82 did not build on its own: the moved Inert index names PipelineRef and
the import stayed behind in client.rs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:42:07 +02:00
Meta Vens
03eb670db3 eureka-state: drop the imports the fixture move left unused, and expect the router field
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:39:03 +02:00
Meta Vens
e0f07c3f53 eureka-state: the MCP server, four tools 1:1 with the wire
The eureka-state binary serves whoami, admit, view and query over stdio (rmcp
2.2.0), each one HuginnClient call. EUREKA_INSTANCE and HUGINN_ENDPOINT are
process configuration: the server fills in the instance and provenance.tool, so
input can name neither. Bad configuration never stops startup; whoami reports
it and every other tool refuses with the typed body. A mind's refusal is a
normal result; Unavailable, Rejected and Misconfigured are results with
isError. The query input schema bundles CultLib's selection schema into $defs
(vendored; a test pins it to the cultnet-rs revision).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:36:33 +02:00
Meta Vens
8041d82fe1 eureka-state tests: the in-process daemon fixtures move to tests/common
The daemon half of the client's fixtures (an in-process daemon over a temporary
mind on a loopback port, and the scripted server) becomes tests/common/mod.rs,
so the MCP binary's tests (Cut 13b) drive the same daemon instead of a second
copy of it. No behaviour change; client.rs shrinks by the moved lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:36:18 +02:00
Meta Vens
33641f40a6 Merge Cut 13a: envelope to huginn-mind, eureka-state client core
Soul pass found no blocker; its four findings (whole-call deadline,
answer correlation, Rejected vs Unavailable, disconnect) are fixed at
3dc50ee with mutation-killed tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:22:15 +02:00
Meta Vens
3dc50eec8e eureka-state: whole-call deadline, correlated answers, Rejected, disconnect
The timeout now bounds the whole call: one deadline at call start, every wait
gets what remains, so a chunk-by-chunk deferred body cannot outlive it. The
response's message id and operation are checked against the request. An
envelope-level OperationFailure is ClientError::Rejected, distinct from the
transport's Unavailable, so a retry on Unavailable cannot loop on a permanent
protocol mismatch. Every call disconnects its session, success or failure.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 00:16:33 +02:00
Meta Vens
26f4137d55 Pin the client's configured instance against a mutation that replaces it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:46:33 +02:00
Meta Vens
272dc09800 Add the eureka-state client core: HuginnClient::call
One call carries one HuginnMindRequest to a daemon over CultNet RUDP on the
operation connection and returns the mind's own HuginnMindResponse. Each call
is its own session and nothing survives it. A Deferred answer is fetched on
that session through cultnet-rs's fetch_content, capped at huginn-mind's
MAX_DEFERRED_BODY_BYTES, and decoded, so a caller never sees the deferral.
Refusals are answers. ClientError::Unavailable is the only error and names the
endpoint. The client opens no store and admits nothing.

Tests run a real daemon in-process on a loopback port over a temporary mind,
plus scripted servers for the failures a daemon cannot be made to produce.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:41:25 +02:00
Meta Vens
4f5d0b1a7e Move the deferred-body bound to the wire vocabulary both sides read
MAX_DEFERRED_BODY_BYTES was the daemon's constant, but a client caps its
content fetch at the same number, and the client crate cannot depend on the
daemon. It moves to huginn-mind::wire next to the response schema it bounds;
the daemon imports it, the pin test stays in bodies.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:32:54 +02:00
Meta Vens
debe7ce9e7 Move the operation envelope to huginn-mind: one spelling for client and daemon
envelope.rs (encode/decode request and response, OperationFailure,
FAILURE_SCHEMA) leaves huginn-daemon for huginn-mind, next to the wire types
it wraps. The daemon imports it and no longer re-exports it. base64 moves
with it; the daemon keeps base64 because serve.rs measures and decodes
payloads. schemars leaves the daemon, whose only user was the envelope's
OperationFailure derive. The two envelope tests run unchanged from their new
home on huginn-mind fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:30:21 +02:00
Meta Vens
ff39d80297 Merge the Cut 11b follow-ups: read-your-writes, rebuild refusal, one deadline
A search waits behind acknowledged admissions and answers; a recreated
index refuses until refilled; a first build is lag; the sink owns the
search deadline; every search-path call is bounded by it. Soul pass 4:
merge; its two pins landed. Follow-up: an ask-time watermark would stop
sustained admissions from starving searches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:03:15 +02:00
Meta Vens
9497001d34 Pin settle-at-reconcile and the first-reconcile wait in take_search
Two mutants survived Soul pass 4: dropping `self.settle()` from reconcile,
and dropping `!projector.stale()` from take_search. One test each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:59:57 +02:00
Meta Vens
cfd9694de0 Pin that an index found whole at start-up was built
Losing a collection the worker found whole at start-up is a rebuild. The
mutation that left Build::Unbuilt in place on a matching label survived,
because nothing observed the difference until the collection was lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:03:56 +02:00
Meta Vens
8a9ade959d Drop a racy queue peek from the hung-embedder test
The test read the queued search's due moment through the sink's lock right
after asking, but the worker can take the search first: it failed under the
load of cargo-mutants' baseline run. The stamp is pinned without a worker in
the_sink_owns_the_searchs_deadline_and_tells_the_loop, which is the test that
owns it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:36:51 +02:00
Meta Vens
e99dc4404e Decide and take a search under one guard; a refill is a rebuild; a first build is lag
S1. serve_search absorbed the inbox and judged pending under one lock and
popped the search under a second, so an admission acknowledged before the
search was asked could be missed. The absorb, the judgement and the take are
now one critical section, and take_search never takes a search while the inbox
is non-empty or anything ahead of it is owed.

S2. The projector carries Build (Unbuilt, Built, Rebuilding). A collection made
again (label mismatch, or lost after it was once built) is Rebuilding until the
entries it owes are written; settle() is the one place that clears it, so a
failure mid-refill does not end the rebuild. Rebuilding reports Reconciling.
unsearchable stays the only refusal predicate.

S3. A mind whose index was never built is Unbuilt: its first build reports
Behind, so the search waits and answers whichever side looked first. Only a
rebuild of an index that exists refuses.

S5. ServeOptions::search_timeout is deleted. The sink owns the deadline
(Backoff::search_deadline, stamped on every search) and the serve loop asks
the daemon for it (IndexSink::search_deadline), so the loop's wait and the
worker's bound are one value. A mind with no identity and documents owed is
Reconciling, so a search is taken and refused rather than left for a spinning
worker.

S4. A test pins that a later call's bound shrinks as the search spends time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:28:22 +02:00
Meta Vens
b48f0cb5ec Give the model-changed refusal test documents still owed
The refusal names the pending count; with every document written the count is
zero and the test proved nothing about it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:43:46 +02:00
Meta Vens
4a4d8b029e Bound the whole search path by the search's own time; lag is not a rebuild
Soul pass 2 follow-ups F1, F2, F4, F5 on the semantic index.

F1: every call a search makes (identity reads, label read, embed, query) is
bounded by what remains of one Budget, and the queued ticket carries the moment
SEARCH_DEADLINE after it was asked. The ports take the bound per call
(model_identity, describe, search); QUERY_EMBED_TIMEOUT is gone, since the
ticket's deadline is the one number. A hung identity read frees the worker
within the search's time.

F2: one refusal predicate. Behind is lag and no longer refuses; only
Reconciling, Failing and Refused do, for the sink and the projector alike. The
worker absorbs the inbox before taking a search and leaves the search queued
while the writes ahead of it are unfinished, so a client that admits and then
searches reads its own writes. An index being rebuilt is still refused.

F4: the model-changed refusal and the worker's failure answer to queued
searches name the state and the pending count.

F5: a search that fails for its own reasons no longer forgets the
reconciliation; only a changed model or label does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:42:03 +02:00
Meta Vens
e3e0c38aeb Merge Cut 11b: the semantic read joined through the mind
Soul pass 2 verdict: merge. Follow-ups: the search path's model identity
reads are not bounded by the search deadline, searches are refused on
ordinary live lag, and two refusals omit the state and pending count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:28:47 +02:00
Meta Vens
e899c65774 Give the live index smoke the embed bound
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:53:24 +02:00
Meta Vens
01925b829f Bound the query embed by the search deadline; one check-then-embed for search and flush
Embedder::embed takes the bound it must answer within. The query embed gets
QUERY_EMBED_TIMEOUT, derived from SEARCH_DEADLINE, which is also the serve
loop's default search timeout. The bulk flush embed keeps 300 s
(BULK_EMBED_TIMEOUT); it does not block serving.

Projector::embed_verified reads the model identity and the collection label,
embeds, re-reads the identity and refuses on change. Search and flush both use
it; the inline copies and step()'s separate identity read are gone. Flush now
also refuses a collection that is no longer this mind's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:50:34 +02:00
Meta Vens
de2344da95 Name the queries in the abandonment test
The three searches now carry their own text, so the test asserts which two were
embedded, in what order, not only how many.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:16:03 +02:00
Meta Vens
2052d314e3 Pin what the mutant run left open; make failing tests fail fast
cargo-mutants on 11b's diff: three timeouts (a drain loop that never settled
under Projector::reconcile -> Ok(()), a dead worker leaving thirty-second
waits to add up past the harness deadline) and three survivors. Now:
- drain is bounded, the status and gate waits are ten seconds, so a mutant
  fails inside the harness deadline instead of hanging it;
- the worker's first step is due immediately rather than one interval after
  a startup that is itself a verification, and an idle worker is pinned to
  one recheck per interval (a recheck that did not move its own deadline
  would list the collection in a loop);
- the log line for hits the mind does not hold is a function with a test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:54:18 +02:00
Meta Vens
4b5eaf9b8f Semantic search refuses while the index rebuilds; bound, cancel and re-verify
Operator ruling 2026-09-30: while the index is rebuilding (a re-pulled model,
a lost collection, Reconciling or Behind) a semantic search refuses with a
typed Unavailable naming the state and the pending count, at the sink and
again in Projector::search, and never answers from a half-filled collection.

Soul's findings on 11b:
- a failed search clears the reconciliation, so whoami stops reporting Current
  and the worker verifies again; the idle recheck is an absolute time that
  searches no longer push out;
- the queue holds at most SEARCHES_QUEUED_MAX and refuses the rest by name;
  the worker serves one search per turn and returns to its writes; the serve
  loop takes back a search it timed out (IndexSink::abandon), so a queued one
  is never embedded;
- the model identity is read again after the query is embedded, and the
  collection's label is re-read before it: a search is one decision;
- a failure report's pending count is refreshed under the lock that takes the
  inbox (absorb);
- wait_status has one overall deadline, and the test gate and worker-stop
  waits are bounded, so a mutant fails instead of hanging.

Tests pin each rule: ticket uniqueness, searched() returning exactly what the
worker answered, the refuse_searches call site, serving between batches, the
bound, abandonment, the rechecks and the label.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:25:55 +02:00
Meta Vens
7df718d643 Mind::rank returns the refs it dropped; delete Daemon::unheld
Daemon::unheld called mind.view once per hit, and every call decodes the
whole mind: 3.27 s on the loop thread at 2000 documents and 200 hits, to feed
one log line, and a second judge of whether the mind holds a document. rank
already reads the rows once and is the only judge now: it returns the hits it
found no document for in Ranked { page, dropped }, and Daemon::finish reports
a count and the first five.

Also in rank: candidates are filtered to held documents before they are cut to
the best 200 by score, so an id the mind lacks cannot take a place from one it
holds and select never sees more than it can answer (its own cut is by
ordinal). A semantic selection with descending is refused SelectionInvalid,
as cursor is: the answer's order is the score's and was silently ignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:25:46 +02:00
Meta Vens
b692b901d2 Mind::rank leaves the cursor to the door that already refused it
The narrowed selection cleared a cursor that semantic_selection has already
refused, and an empty cursor is no cursor to the evaluator; the mutation run
showed the clear could be deleted with every test still green. A test pins
that an empty cursor still ranks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:07:35 +02:00
Meta Vens
5eea49f756 Cut 11b: the semantic read, joined back through the mind
Qdrant supplies candidate ids and scores and nothing else. Mind::rank drops
every hit the mind does not hold, then lets CultNet's selection decide
membership over the whole mind with its keys narrowed to the candidates, so
in_force and cited mean what they mean in query. Order is score then ordinal;
the page is cut to the selection's limit and top_k; next is never set. A
cursor with semantic, blank text and an out-of-range top_k are refused by name
before anything is embedded. Mind::query loses its semantic parameter and the
"index is not wired" refusal; the published request schema's description of
SemanticQuery follows.

The serve loop never waits on the embedder: a semantic query becomes a Search
held beside its session, the worker embeds (behind the model's query
instruction) and searches between batches, and the loop answers when the
worker has or when ServeOptions::search_timeout passes. A worker that is
Failing or Refused refuses at once, typed Unavailable naming its own error.
VectorIndex gains search; Qdrant's adapter sends no filter.

Model identity is re-read before every flush and before every search: a model
re-pulled under the same dimensions but a new digest forgets the
reconciliation instead of mixing vectors, and the next step rebuilds. An idle
worker forgets the reconciliation every Backoff::recheck (60 s), so a lost
collection is found without a write. Both go through Projector::reconcile,
still the one judge.

Also fixes a race in the worker loop found by the mutation runs: entries were
taken from the inbox under one lock and published as wanted under another, so
whoami could read a stale Current with nothing queued while the worker held
them. Taking, absorbing and publishing are now one critical section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:37:01 +02:00
Meta Vens
efaeebffc0 Merge Cut 11a: the index write side
huginn-mind::index defines each kind's search text (pure). The daemon gains
Ollama and Qdrant adapters (ureq 3, no TLS) behind Embedder/VectorIndex
ports, and one worker thread whose Projector owns reconcile. Admission
never waits on the index. Reconcile is the only judge of missing points,
and it runs again after any failure. The collection is labelled with the
mind's genesis receipt id and keyed on model identity (name, digest,
dims, text version), never the URL. whoami reports IndexStatus. The live
smoke passed against Qdrant on Yggdrasil and Raven's Ollama
(qwen3-embedding:0.6b, 1024 dims).

Soul closed it after one fix batch. For Cut 11b: every search hit is
joined back through the mind and filtered by in-force; model identity is
re-checked per flush. Noted limit: Ollama truncates at 4096 tokens.
mind 96, daemon 47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:31:03 +02:00
Meta Vens
745803c785 Pin the identified-but-unreconciled status the mutant run found unpinned
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:26:15 +02:00
Meta Vens
108ed8168c Live index smoke labels its scratch collections with a mind
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:09:43 +02:00
Meta Vens
98c959f0f5 Index: reconcile after every failure, label collections with the mind, pin the backoff
F1 the 30 s to 10 min backoff curve is pinned by a pure test.
F2 any failed step forgets the reconciliation; the projector keeps every entry
it was given so a lost collection or a changed model is rebuilt and refilled
without a restart.
F3 the collection label carries the mind genesis receipt id; a collection of
another mind with the same instance name is refused and untouched. A mind with
no receipt yet reconciles nothing.
F4 the effective input limit (model context, not 16 KiB) and the Cut 11b join
requirement are recorded in the module docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:08:38 +02:00
Meta Vens
1c1466aa5b Mind exposes its identity: the genesis receipt id
The instance name is only a label; two stores may share it. The receipt of the
first admission never changes and digests the first batch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:08:37 +02:00
Meta Vens
f4416e659b Merge eureka/memory-organ (BP-3) into hands/cut11a
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:04:04 +02:00
Meta Vens
48518a5df3 Cut 11a: pin the collection name, status arithmetic, worker shutdown and name-only tag entries
Mutation survivors from the first cargo-mutants pass; the equivalent ones
(point-id bit shaping, a byte-limit boundary) are left.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:46:12 +02:00
Meta Vens
29952e281f Merge BP-3: an oversize answer is deferred to the body plane
An answer too large for one send becomes HuginnMindResponse::Deferred,
carrying only the reference's CultMeshCdnArtifactManifest (Q-BP2 A). The
client fetches the body over the session it already holds with
cultnet-rs fetch_content (Q-BP1 A) and gets bytes identical to the mind's
answer. Retention is in memory only: 256 MiB counted by distinct chunk,
a sliding 60 s TTL, and LRU eviction. ResponseTooLarge stays the backstop
above 64 MiB. Every echoed field is bounded at 256 bytes, so every reply
fits the window. A deferred envelope carries its answer's status.

Soul closed it after one fix batch. Named limit: there is no per-peer
budget, so a client can evict another client's body mid-fetch. That is
acceptable for trusted WireGuard clients. mind 91, daemon 27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:29:32 +02:00
Meta Vens
18de1bc6d7 BP-3 3: bound every echoed field, and defer under the answer's own status
A reply echoes the request's message_id (and on failures its service_id,
operation and payload_schema; on chunk answers its chunk_hash). answer now
refuses any of them over MAX_ECHOED_FIELD_BYTES (256) at the door with a
transport Error that carries none, so every reply, including the largest
Deferred and ResponseTooLarge, fits the window. A deferred answer's envelope
carries the status of the answer it stands for. The retention docs name the
missing per-peer budget and that expiry runs only when a frame arrives.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:06:41 +02:00
Meta Vens
38059c9599 Cut 11a: a live adapter smoke against a scratch collection (ignored by default)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:05:27 +02:00
Meta Vens
78f8c6ee2f Cut 11a: the index write side
huginn-mind::index owns what of a document is searchable: index_text is a
pure function per kind, Mind::index_entries builds the entries with the
writing receipt's ordinal, INDEX_TEXT_VERSION says when the rule changed.

huginn-daemon::index owns the projection. A Projector reconciles the mind
against the collection at startup (the only place a point is judged
missing: set difference on point ids, rebuild when the model name, digest,
dimensions or text version differ, refuse a collection that is not this
instance's) and one worker thread embeds and upserts in batches of 32,
retrying with a 30 s to 10 min backoff. IndexSink::committed builds entries
and leaves them in an inbox; admission never waits on the embedder or on
Qdrant. Whoami reports the worker's health as IndexStatus. The Ollama and
Qdrant adapters are the only files holding HTTP or JSON (ureq 3, no TLS).
The daemon now requires --qdrant-url, --ollama-url and --embedding-model;
NoIndex leaves the binary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:05:26 +02:00
Meta Vens
15fc24d6c6 BP-3 2: pin the deferral constants
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 07:45:45 +02:00
Meta Vens
7636737d60 BP-3 1: an oversize answer is deferred to the body plane
deliver replaces within_window as the one owner of how an answer leaves:
whole when the envelope fits the window, deferred when the payload is within
MAX_DEFERRED_BODY_BYTES (64 MiB), refused by name past it. A deferred answer
is HuginnMindResponse::Deferred(DeferredAnswer { manifest }), the reference's
CultMeshCdnArtifactManifest; the chunks are asked for on the session the client
holds and answered by cultnet-rs's answer_content_chunk_request. The body is
held by DeferredBodies (256 MiB budget, sliding 60 s TTL, least-recently-
touched eviction, shared chunks stored once), in memory only. The mind knows
nothing of deferral.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 07:29:17 +02:00
Meta Vens
9673ab8c9e RS-3 3: pin the vocabulary tables through the substrate's trait
Each kind declares exactly its aliases and roles, role targets are the field's
typed kinds, and no alias is numeric. Found by cargo-mutants over the RS-3
diff: declared_on, declared_roles and is_numeric survived.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 06:32:27 +02:00
Meta Vens
eb57bdba40 RS-3 2: the read side is CultNet's selection over the organ's rows
The four hand-written filters, the order and the cap are deleted:
PipelineQuery, PipelineQueryPage, QUERY_LIMIT_MAX, Reader::matches, ordered,
repo_matches. Mind::query(selection, semantic) validates through
cultnet_rs::validate, closes the values door (rows.rs refuse_values, S6),
takes the snapshot from the cursor's own asOf (an asOf beyond the head is
CursorInvalid), builds Reader::at(as_of) so every derivation runs over the
documents admitted at or before it, and evaluates through cultnet_rs::select.
The page carries matched, asOf, the next cursor, headers or whole views by
projection, and typed edges when the selection hops.

rows.rs is the organ's vocabulary: SelectionRow (a Row), Vocabulary (a
RowSet: thirteen schemas, eleven aliases, fifteen roles) and the value door.
huginn-mind links cultnet-rs at the rev the daemon pins. Selection refusals
map totally onto SelectionInvalid, CursorInvalid or Unavailable. The wire's
Query carries CultLib's selection by $ref, and both published schemas are
regenerated. The relations RS-2 unpinned are re-pinned as selections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:56:37 +02:00
Meta Vens
511afa2f44 RS-3 1: the edge list moves to its owner (docs.rs citations)
docs.rs::citations and CitationRole are the one edge list: the fifteen
roles, one per referring leaf field. Admission's A7 references() is a map
over them, carrying the Missing classification by role and the hand-off
source-side filter, with the same refusals in the same order. kind_of_id and
outcome_references leave admission; the CitesResolvedDocument check reads
outcome_citations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 05:16:05 +02:00
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