Commit graph Ghostlight/crates
Author SHA1 Message Date
Meta Vens
f63dfd12fc Rewrite the lens to one the fixture did not draw (L1 c3.s1.f1)
The illegal-transition pin rewrote the recorded lens to Numen, but its
fixture draws under uniform weights with a random world id, so about one
run in eight the lens was already Numen and assert_ne! failed. The rewrite
now picks Patina when the recorded lens is Numen and Numen otherwise, so
the lens rewrite always differs from the row.

Measured: at ef64008, 3 of 40 runs failed at controllers.rs:10230 with the
recorded lens Numen; after the change, 120 of 120 runs pass.

Test-only; no production code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:47:00 +02:00
Meta Vens
2bf81645ed Pin that adoption refuses a row under another world or jurisdiction (L1 Cut 3)
`a_row_for_the_same_answer_under_another_world_or_jurisdiction_is_not_adopted`:
a real refused session's row, with its session's world moved to another real
world, or its jurisdiction moved to another real place of the same world, and
then stored under this answer's derived id. The step is `Superseded`, no
inference runs, and the row is unchanged. The row as it was written is then
adopted and commits, so the test is not an inert harness.

The command id already implies world, jurisdiction and digest, so only a row
whose key and session disagree reaches this rule. The test builds exactly
that row.

Builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:13:37 +02:00
Meta Vens
4457fde138 Remove the dead-code expectations from the lens draw and text (L1 c1.s1.f3)
Cut 1 marked `Lens::instructions` and `lens::draw` with
`cfg_attr(not(test), expect(dead_code))` while only their tests read them.
`select_answer` reads both now, so each marker produced an unfulfilled lint
expectation warning. `reaches_for`, `leads_with` and the moved
`ELABORATION_INSTRUCTIONS` carried no markers. No `expect(dead_code)`
remains in `lens.rs`.

Builds. The library's warning count returns to 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:07:47 +02:00
Meta Vens
18b1098c3f Record the lens and its text on the session; adopt rows by answer (L1 Cut 3)
A new elaborator session draws its lens and authors its instruction text
once, and the checkpoint row carries both from then on. A resume reads the
row's session instead of demanding the fresh derivation equal it.

Session:
- `ElaboratorSession` gains `lens: Lens` and `instructions: String`
  (`deny_unknown_fields` kept).
- `select_answer` derives the command id from world, jurisdiction and
  answer digest, draws `lens::draw(snapshot.lens_weights, world, id)`, and
  authors `lens.instructions()`. It returns the id with the session, and
  `step` uses that id instead of deriving it a second time.
- `session_command_id` takes the three identity fields instead of a session,
  because the lens is drawn from the id and so the id exists before the
  session does. The derivation bytes are unchanged: the Cut 0 id
  `56dbcdb2-9337-ac07-f322-37376b58cbcb` is pinned in
  `a_session_identity_is_derived_from_its_answer`.
- `ELABORATION_INSTRUCTIONS` moves into `lens.rs` as a private constant.
  `elaboration_request` takes the text, and `step`, `persist_repair` and
  `integrity_is_valid` pass `session.instructions`. Integrity binds the
  stored invocation to the stored text, not to code (L1-Q7 (a)).

Adoption rule (L1-Q9, boundary half): the whole-session supersession
equality in `step` is deleted. A found row is this answer's row when world,
jurisdiction, answer and answer digest match (`same_answer`). Its session,
including ancestry, lens and text, replaces the fresh one. A boundary session
is no longer `Superseded` forever after an unrelated commit.
`valid_elaboration_progression` is unchanged and still compares whole
sessions row to row; the doc comments on both rules say why they differ.

Store: `controller_work.v15` -> `v16`; v15 rows are refused at open, not
migrated. The world ontology doc names v16.

Tests (controllers): `a_boundary_session_resumes_after_an_unrelated_commit`
(at 1a402e1 it reported Superseded),
`a_resumed_session_keeps_its_recorded_lens_when_the_weights_change`,
`a_checkpoint_that_changes_the_lens_or_its_text_is_an_illegal_transition`,
`a_row_whose_text_and_invocation_disagree_is_refused_at_open`,
`a_controller_work_row_from_v15_is_refused_at_open`,
`soul_the_same_patch_admits_the_same_under_every_lens` (pinned to the
Cut 0 mismatch JSON), and `the_catalog_takes_no_lens`. The single-dead-end
fixture now delegates to `dead_end_mailbox(weights, dead_ends)`, which builds
the first dead end with the same handles and labels as before.

Builds, but with two "this lint expectation is unfulfilled" warnings from
the `expect(dead_code)` markers on `Lens::instructions` and `lens::draw`. The
next commit removes those markers (c1.s1.f3).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:07:25 +02:00
Meta Vens
4334dd7394 Compare lens sets by effective weight in the no-op check (L1 c2.s2.f1)
`SetLensWeights` refused an identical set by comparing raw maps, but a
lens a map does not name weighs zero. `{charter:3}` to
`{patina:0, charter:3}` therefore committed, and changing back committed
again, though both sets draw identically.

Equality is now `LensWeights::weighs_the_same_as`, owned in `lens.rs`: every
stock lens's effective weight is equal. The reducer's arm calls it; the
stored spelling is whatever the last committed set said.

Test, through the public mailbox in both directions:
`a_lens_set_spelled_with_or_without_its_zero_weights_commits_nothing`. A
sparse world set refuses a one-zero padded twin and an all-eight padded
twin; a padded world set refuses two sparse twins; each refusal is
`NoCanonicalChange` with revision, digests, operator log and stored
spelling unchanged; real changes commit.

Builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:55:23 +02:00
Meta Vens
dfb3c52b01 Guard the lens draw's divisor directly (L1 c1.s1.f2)
The draw checked `draws()` over the map's values and then divided by a
sum over `Lens::ALL`. Those were two computations that happened to agree:
weakening the guard alone reached a modulo by zero. The draw now sums the
total first and refuses a zero total, as `select_band` does; `draws()`
stays the admission rule and no longer guards the draw.

Builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:53:04 +02:00
Meta Vens
acb9b3379d Re-seal the forged lens weights rows so only the rule refuses (L1 c2.s1.f4)
a_forged_lens_weights_row_fails_replay re-sealed only the commit digest, so
its all-zero forgery was refused by the resulting-state-digest mismatch and
still passed with draws() weakened. Each forgery now re-seals the whole store:
the head takes the effect's weights, its state digest is recomputed, the
commit's resulting state digest and digest follow, and the head's last commit
digest names the re-sealed commit; every seal is asserted consistent.

A drawing control re-sealed the same way replays, so no digest is left to
answer. The caller forgeries assert an ownership refusal and the all-zero
forgery asserts a draw-rule refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:31:09 +02:00
Meta Vens
afca403fca Refuse identical lens weights as no change (L1 c2.s1.f1)
An owner's SetLensWeights equal to the world's current weights committed:
the revision advanced and the digests moved though nothing canonical changed.
`reduce` now refuses it with PatchRejected([NoCanonicalChange]) after
admission, so the live submit and replay share the rule and nothing commits.

The order is capability, owner, draw rule, then equality: a non-owner's
identical set stays Unauthorized, and an identical set that never draws stays
LensWeightsNeverDraw. The SetLensWeights doc stated the old rule and now
states this one.

Tests: the in-crate soul test's pin of the old commit becomes the refusal,
with the order pinned (non-owner identical is Unauthorized; an identical
all-zero set against a hand-built state is LensWeightsNeverDraw) and the
store reopened to the same head. external_admission gains
identical_lens_weights_from_the_owner_commit_nothing through the public
mailbox: revision, state digest, last commit digest and operator log length
unchanged, non-owner identical Unauthorized, a different set commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:31:09 +02:00
Meta Vens
d06fd7ec2e Put lens weights in the world (plan step 15, L1 Cut 2)
Lens weights are world data (L1-Q2), replaced only by an owner command
(L1-Q3), and required at creation with no library default (L1-Q6).

Library:
- WorldState.lens_weights, written by genesis through
  ResolvedPatch.lens_weights (Some on the genesis lane only) and carried
  on CreateWorld and CreateWorldIntent (required).
- CommandBody::SetLensWeights and WorldEffect::LensWeightsReplaced: owner
  only, any phase; reduce decides and apply_effect re-decides owner and
  draw rule. A set that never draws is Mismatch::LensWeightsNeverDraw at
  genesis and in the command; verify_state_shape refuses a stored one.
- WorldSnapshot.lens_weights.
- State and commit schemas go to consumer.v5; v4 stores are refused.
- resolve_patch takes the weights; the action lane passes None.

Dungeon:
- CreatePayload.lens_weights required; world_create.v4 in the descriptor
  and operation_schema; the eighth create binding and a lens weights
  control defaulted to uniform_lens_weights(), which is Dungeon policy.
- The smoke harness states uniform weights. The stale-payload test now
  denies v3, a v4 payload without weights (as a payload error) and one
  without a brief; new tests deny an unknown lens and an all-zero set.

Tests: genesis writes and replays weights; an all-zero genesis is refused
and writes no file; only the owner replaces weights (non-owner, stranger,
elaborator, clock, consumer, stale revision, all-zero, hand-built effects);
weights enter state_digest and no scope digest; a history with a weight
change replays to the same digest; forged weight rows fail replay; a v4
store and an all-zero state row are refused; external non-owner refusal
and unknown-lens decode.

This commit builds (Windows only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:00:45 +02:00
Meta Vens
835ea4d019 Add the stock lens module (plan step 15, L1 Cut 1)
crates/ghostlight/src/lens.rs owns the eight stock lenses, each lens's
"reaches for" text and leads_with list of existing PATCH_TOOLS names, the
per-lens instruction text, LensWeights (a transparent BTreeMap<Lens, u32>
with an empty Default and no uniform constructor), and draw: a digest over
LensPreimage { world_id, command_id }, first sixteen hex as u64, modulo the
weight total, cumulative walk over Lens::ALL, Invariant error when no
weight is nonzero. Same recipe as select_band; no rand.

lib.rs declares the module, exports Lens and LensWeights, and gives the
library test module stock_weights(). ELABORATION_INSTRUCTIONS stays in
elaboration.rs (Cut 3 moves it) and becomes pub(super) so the lens text can
read it. instructions() and draw() carry cfg_attr(not(test),
expect(dead_code)) until Cut 3 reads them, following controllers.rs:759.

Nothing outside its tests reads the module yet. PATCH_TOOLS, the catalog
functions and the sidecar fixture are untouched. Builds on Windows; the
Linux build is Idunn's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:26:21 +02:00
Meta Vens
ab95d7897c Name the library's test subject human, not player
Players are a Dungeon concept. The library's own term for a
human-controlled subject is ControllerMode::Human, so its test code now
says so: the principal fixture player() becomes human_principal() (a
local `human` subject binding in action.rs would shadow a bare human()),
and the genesis subject handle, labels, principal address, variables and
one test name follow:

- "player" handle -> "human"; "The Player" -> "The Human";
  "Another Player" -> "Another Human";
  "player@example.test" -> "human@example.test"
- player_opportunity, after_player, genesis_player ->
  human_opportunity, after_human, genesis_human
- draft_activation_player_and_autonomous_actions_share_one_reducer ->
  draft_activation_human_and_autonomous_actions_share_one_reducer

Test code only: no production code, schema string, serde name, pinned
digest or fixture file carried any of these values. The library test
count stays 419 passed and 1 ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:19:04 +02:00
Meta Vens
b14507c894 Pin the scope digest comparison to exact case and label
The stale-digest admission test now submits, besides the one-digit
forgery, the current issued digest with one hex letter uppercased, its
bare 64-hex value, and the same hex under a blake3: label. Each forgery
asserts its own premise before submission, and each must be refused as
ScopeChanged with revision, state digest, last commit digest and
operator-log length unchanged.

This kills the case-insensitive, hex-only (rsplit) and sha256:-stripping
mutants of the comparison in the kernel, which the previous eight tests
admitted, while the length, 7-character prefix and all-but-last mutants
stay killed. The cases extend the existing stale-digest test; the
external admission test count stays eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:14:52 +02:00
Meta Vens
af9c95a5bf Name the consumer, not Dungeon's session type, in library docs
SeedPort's evidence is minted by the consumer authenticating a live
principal, and a quarantined controller leaves WorldMailbox and its
consumer authoritative. Neither comment needs to know the consumer is
Dungeon's AppSession.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:56:55 +02:00
Meta Vens
9e514ebf15 Close InferenceRequest::provider_model to the crate
Its only caller is the SDK inference router inside the library, and no
consumer names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:56:55 +02:00
Meta Vens
bde05de4e3 Refuse a digest shaped exactly like the real one
Both existing scope-digest forgeries differ in length from a real
`sha256:` + 64-hex digest, so a length-only comparison in
`exact_opportunity` passed every external admission test.

The new test takes two genuinely issued controller opportunities, lets
one subject speak (genesis places every subject in the commons, so the
telling lands as knowledge on the other and moves its scope digest), then
submits the listener's now-stale opportunity. It asserts the stale and
current digests differ and share a length, and that the submission is
refused with ScopeChanged and commits nothing.

A stale SHA-256 digest differs from the current one almost everywhere, so
it cannot catch a comparison that skips the last character. The test also
submits the current digest with only its final hex digit changed.

Mutants on the comparison, each killed: length-only, first seven
characters only, all but the last character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:56:55 +02:00
Meta Vens
2865ea738d Close the two snapshot rows Dungeon never reads
An item stays pub only when the compiler requires it for an API Dungeon
calls. WorldSnapshot's resources and routes fields were pub, which forced
ResourceSnapshot and RouteSnapshot pub; Dungeon reads neither field and only
the library's own controllers do. The fields and both types are now
pub(crate). The workspace builds with the same unique warning set.

Every other bare-pub identifier Dungeon never names was closed one at a time
under cargo check --workspace --all-targets. Each either fails to compile
because Dungeon or the admission tests use it through a public path, or adds
a private_interfaces warning from a public enum variant field or method that
Dungeon does use (KernelError, CommandBody, CausalBoundary, RefKind,
ControllerWorkCustody, NarrativeCheckpoint, WorldMailbox), or strands library
code as dead. Those stay pub.

ScopeDigest, WorkLane and ScopeComponents are in the second group: the
warnings come from KernelError::ScopeChanged, ControllerWorkCustody::Uncertain
and the NarrativeCheckpoint variants. NarrativeInterruption's
bound_scope_digest and fresh_scope_digest return &str, so they never forced
ScopeDigest open, and Dungeon calls both at runtime.rs:1286-1287 and
2022-2023, so they stay pub too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:13:56 +02:00
Meta Vens
7cbb9e37a6 Say what the library means, not who consumes it
Four error strings and four comments named Eve, which is a consumer's
interface and not a concept the kernel has. The errors now say what they
refuse — a command ID, an opportunity, persisted controller work — and the
comments name an operator interface or a consumer's payload.

Nothing pins these strings: the four `#[error(...)]` literals appear only at
their definitions, and no test reads a `Display` of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:59:31 +02:00
Meta Vens
fac90e6f4b Follow the minter by name, not by one literal
The old count read one non-recursive directory, split each file at one exact
`#[cfg(test)]\nmod tests {` marker, and counted one literal. Soul walked past
it with an alias; a qualified `<T>::new(`, a file in a subdirectory, and a
production item placed after the test module walked past it too.

The check now walks `src` recursively and removes what each `#[cfg(test)]`
attribute guards by its own extent — balanced braces for an item with a body,
the first `;` or `,` outside parentheses for one without — so a renamed test
module is stripped and nothing after it is hidden. Construction is recognised
by scanning what follows the type's name, which admits `<T>::new(` and any
whitespace, and the one site found must be this owner's file.

Following the type by name means production may not give it a second name: an
`as` import or a type alias is refused outright rather than counted. What the
check still cannot see is a call that never spells the type, such as a macro
expansion or a generic instantiated elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:59:31 +02:00
Meta Vens
a8d3377072 Prove admission from outside the crate
Q1-9 rules the seal as unforgeable admission rather than unconstructible
values, so the proof belongs where a consumer stands. An integration test
compiles as its own crate against the public API alone; these seven mint
their forgeries through serde, which visibility does not gate, and assert on
the typed error rather than on any message.

Six rejections — a forged scope digest, the digest string the deleted
ScopeDigest::fixture used to produce, a forged controller ID, a minted
SubjectId, a minted AffordanceId, and a wholly deserialized opportunity —
each assert that revision, state digest, last commit digest and the operator
log's length are unchanged. One control commits through the same port with an
issued opportunity, so the rejections cannot be an inert harness.

No manifest change: serde_json, uuid, chrono and tokio are already library
dependencies, and Cargo gives test targets a package's dependencies as well
as its dev-dependencies. No new production dependency and no feature.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:50:35 +02:00
Meta Vens
f859d2c4e3 Give the world library a sealed public boundary
Completes the move begun in the previous commit, which does not build on its
own. `ghostlight` is now a library crate with an explicit public surface;
`ghostlight-dungeon` is a consumer that reaches the kernel through it and
cannot reach past it.

Deleted first: `mod world;`, the nine unused re-exports, the `#[cfg(test)]`
re-export block, `fixture_controller_opportunities`, the five inference
`fixture_*` items, Dungeon's own `VerifiedPrincipalEvidence`, and Dungeon's
direct `ghostlight-persona-projection` dependency.

The seal. `WorldKernel`, `WorldState`, `reduce`, `apply_effect`,
`CommandEnvelope`, `AuthenticatedCaller`, the journal writer, every ID
`issue()`, and `ScopeDigest::fixture` carry no `pub` of any kind and no
feature gate; the crate declares no `[features]`, so no build configuration
can lift them. Ten `compile_fail` doc-tests stand outside the crate and pin
each one to its error code.

The principal seam. The library owns `VerifiedPrincipalEvidence` with a public
`new`; `mailbox` still bounds it by `valid_until` at ingress. A Dungeon test
counts the minters in production source and fails at anything but one.

The port contract (Q1-7). `PreparedInference::prepare` (renamed from the free
`prepare_invocation`), `InferenceOutput::new`, and
`InferenceFault::{retryable, recovery_required, integrity_violation}` are now
the port's public constructors, so a consumer-implemented `InferencePort` can
answer without test-only wrappers. `TracingInferencePort` is an ungated
decorator.

The tick-ordering test (Q1-8) derives its cover from a real active world
through `derive_cover` and asserts two cells; its ordering and shared-tick
assertions are unchanged.

Behaviour is unchanged. The library's 420 unit tests are the base `world::`
set with the prefix stripped — the names diff empty. Dungeon keeps all 49 of
its names and gains two Soul tests the boundary requires. No schema string,
digest preimage, or store layout is touched.

Windows verification only; the Linux release is Idunn's build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:06:07 +02:00
Meta Vens
108b691015 Move the world module to crates/ghostlight
Pure `git mv` of the thirteen files under
`crates/ghostlight-dungeon/src/world/` into `crates/ghostlight/src/`, with
`mod.rs` becoming `lib.rs`. No byte of any file changes; all thirteen are
detected as renames so history follows.

This commit does not build. The crate graph has no `ghostlight` package yet
and `ghostlight-dungeon` still declares `mod world;`. The next commit adds
the manifest, the public boundary, and the consumer rewrite; the two are
pushed together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:32:21 +02:00
Meta Vens
855727f8a4 Take a turn's address as its spans in prose order
The Interpreter was citing correctly and the fold was discarding: an
utterance split by narration lost its second half and a turn's second
visible act became an ambiguity gap. Every span cited for a kind now joins
that kind's one statement in prose order; only an overlapping span is an
ambiguity. NarrativeCapture carries the spans, the invocation still carries
one utterance and one display. controller_work.v15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:20:27 +02:00
Meta Vens
81a1f53c2e Report the interrupted cell: subject and both scope digests
A committed turn that re-lowered once carries its pair of scope digests on
the receipt; the cover tick logs it per cell and keeps it on the tick
summary, and the smoke harness prints one "interrupted cell" line per
re-lowered cell after the tick line. The operator feed line shows a display
beside or instead of speech.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 14:00:40 +02:00
Meta Vens
0582321e2c Let a person be seen: nonverbal display by quote
An invocation may carry a display, the actor's own visible sentence, beside
or instead of its utterance. The kernel lowers it as speech's twin: an
AssertClaim and a Display whose room is re-derived at apply, landing
Seen { by } on everyone standing in the actor's place and on nobody else,
never over a channel. A speech-carrying entry given a display alone is an
address, not SpeechRequired; an empty display is EmptyDisplay; a placeless
actor has no room. The Interpreter gains a display tool quoting the visible
clause alone; the narrative capture carries both spans; the fold allows one
of each. Overheard rows and Projector knowledge rows say how a thing was
perceived: said by, seen doing, or known. The journal's event-shape rule
counts both lowerings. Kernel-only lowering: no patch tool, catalog stays
thirty. Schemas: world_state.consumer.v4, world_commit.consumer.v4,
controller_work.v14. Six tests pin the room, the silence, the refusals,
replay, the fold, and the interruption block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:21:53 +02:00
Meta Vens
d4d7bc3c07 Give a world its brief: authored once, guidance on every lane
world_create.v3 carries a required brief, trimmed at ingress, stored on the
state and its snapshot, and projected as the guidance of the Projector,
Persona, Interpreter, operational, and grouped lanes and both authoring
prompts. The two narrative stages that rebuild prompts on resume carry it
(controller_work.v13). The smoke titles its world by the seed root label and
hands its brief to the world rather than to each seed session, so a vow can
no longer name a test fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 03:04:02 +02:00
Meta Vens
a9d6a1c980 Tell the model why a call was refused, describe the controller, reopen on the SDK lane
Run 7's seed authored six distinct people with memories, reads, and worded
promises, and every declare_subject carried an empty controller: the field
had no description and the tool result said only "recorded as a gap". The
controller variant now describes its two shapes, and a decode failure's
reason goes back to the model in the tool result. The SDK lowering renders a
later user turn into its transcript, so a refusal continuation reaches the
sidecar instead of faulting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 02:24:28 +02:00
Meta Vens
c31f25415a Let the seed author meaning: persona material, commitment statements, presence
Plan step 12. PersonaMaterial is the ontology's thirteenth row, built:
values, voice, memories, and reads keyed by subject, set whole by
set_persona_material, refused for padded text or a doubled read, no-op when
identical, outside every scope digest. A commitment carries a statement of
what is promised, required at authoring; an affordance-made promise states
the action it came through. The Projector context carries material by label,
each promise's statement, and who else stands in the same place; the
Persona's identity is label, voice, and values. Schemas move to consumer.v3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 02:12:13 +02:00
Meta Vens
0a6a7216e3 Record runs 4 to 6 and map the seed authoring meaning as proposed step 12
The membrane traced end to end: the Projector's borrowed voice, the
contentless commitment, the unbuilt PersonaMaterial, and the missing
co-presence are recorded as findings; the harness no longer claims to fail
on silence, which is now the honest baseline. The map and handoff point at
the decision the operator owns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 01:31:20 +02:00
Meta Vens
19609ee06c Render absence as flatness, and due as time
The Projector and Persona prompts asked for what this person remembers,
wants, and fears, with no component to ground any of it, so every seeded
person spoke in the model's one borrowed voice. Both prompts now render only
what the context establishes, say plainly what is unknown, and treat the
word budget as a ceiling. A commitment's due reaches the Projector as
minutes from now, since a bare 1000 was read as a sum of money.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 01:17:40 +02:00
Meta Vens
f3d1904428 Let the smoke show the whole membrane
A test-only TracingInferencePort wraps any inference port and appends each
request's purpose, model, instructions, input turns, tool catalog, and
output to the file GHOSTLIGHT_SMOKE_TRACE names. The logs redact prose by
design and the store is a temp directory, so this is the one way to read
what the Projector, the Persona, and the Interpreter were each given and
what each returned on a road run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:41:27 +02:00
Meta Vens
4297b7c0c2 Project a seeded person's own life to its Persona, by label and never by id
The Projector context carried a name, a clock, and permission to speak, so a
seeded person reasoned correctly from nothing and said "I'm here". It now
carries the acting subject's own place, routes, holdings, dependencies,
authority shape, offices, redress, promises with their counterparties, and
pressure on self, resolved to labels. Another subject is named only where
this subject's own components bind them. Ids, keys, digests, revisions, and
every other subject's state stay out; the leak tests pin it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:27:11 +02:00
Meta Vens
4f7513bcba Cite Persona prose by quoting it, and make a bad quote a gap rather than a quarantine
The Interpreter's speak and record_gap tools took byte offsets the model
counted itself; on the Claude lane three of eight utterances came through
cut mid-word and one empty span reached invocation as a non-canonical
utterance and quarantined its cell. Both tools now take `source_quote`; the
harness locates the quote in the preserved prose and derives the span. A
quote not in the prose, empty, or padded is a translation gap at capture.
The invariant is unchanged: only words in the prose become speech.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 23:57:17 +02:00
Meta Vens
936aa71b8d Continue the conversation on refusal, and give the claude lane its descriptions
The first SDK-backed seeded runs. Run 1 refused both seed sessions: the
sidecar's schema conversion dropped every field description, `due` was an
undescribed integer against a clock the model never saw, and the subject
tool did not say a grant is required. Run 2 seeded six people in one round
and ticked three times.

A refusal used to reopen the session with an empty draft and a prompt that
named sites from a draft the model could no longer see. It now continues
the same conversation: `Refusal { after_round, mismatches }` on both
authoring checkpoints (controller_work.v12), the refusal as the next user
turn, and the draft rebuilt by one DraftFold shared by evaluator and oracle
from the calls after it. The harness takes the connector only when its
address is set, so an all-claude run needs none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 23:50:28 +02:00
Meta Vens
d72c246b1f Reclaim the lend on every exit path, and cut three leftovers
`take_oracle` now runs above the caller-identity and expiry gates, so a
refused `infer` reclaims the lend instead of leaving a dead lane's
tool-result owner in the map under a request id nothing will call again.
The port's doc already claimed this; the test that recorded the
divergence now asserts the reclaim.

`fixture_prepared` was `prepare_invocation`'s body a third time, with
the same caller id and the same expiry as `fixture_prepared_inference`
beside it. Its call sites use that one instead.

`ElaboratorSession`, `ElaborationCheckpoint`, `SeedSession`, and
`SeedCheckpoint` go back to `pub(super)`: nothing outside `world` names
them, and `pub(crate)` was reach they never needed. `private_interfaces`
then reports them the way it already reports the grouped, narrative, and
operational checkpoints, which are private under the same `pub(crate)`
`ControllerWork`.

Runtime binds the SDK model prefix from `DEFAULT_SDK_MODEL_PREFIX`
rather than spelling `claude` twice more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 02:34:19 +02:00
Meta Vens
3514d76667 Make the port's source-scanning test line-ending neutral
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 02:15:31 +02:00
Meta Vens
0fa411bd81 Falsify the SDK port: adversarial oracle equality, gates, and the Zod divergence
Tests only. Per lane, an adversarial call sequence — decode failure, duplicate
terminal, call after terminal, unknown tool name, oversized argument — is
answered by the oracle with exactly the strings the evaluator recomputes for the
same transcript, and the transcripts the oracle refuses are the transcripts the
evaluator refuses. Identity is pinned across every request builder and one
checkpoint variant. The wire's closed enums, the fault disposition table, the
lowering gate table, the dispatch-order gate, the receipt's contents, and the
routing refusal each get a test.

Two recorded findings, characterized and not fixed:

- The emitted grammar's top-level object is not strict in Zod, so a property the
  connector's `deny_unknown_fields` decoders record as a gap is silently
  stripped and the call succeeds; and a missing or mistyped required property is
  answered by the SDK itself, leaving a registered `tool_use` block with no
  dispatch, which `assembleEvents` raises as `protocol_violation` and Rust maps
  to an integrity violation. Both are proven against the real in-process MCP
  server with no credential and no network.
- A lend refused by the caller-identity or expiry gate is never reclaimed: those
  gates run before `take_oracle`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 02:10:33 +02:00
Meta Vens
2ff1d435d8 Add the Claude SDK sidecar: framing, the closed schema converter, the query
`sidecar/claude-sdk` is the repo's second TypeScript package, wired to the
root only through `npm --prefix`. It reads length-prefixed MessagePack
frames on stdin, runs one SDK query, forwards every tool call to Rust, and
returns Rust's answer to the model verbatim. It computes no tool result,
reads no credential, logs no prompt, argument, or result, and writes nothing
outside its own stdout.

The query is opened with a custom system prompt, `settingSources: []`,
`tools: []`, `strictMcpConfig`, `persistSession: false`, and `alwaysLoad` on
every tool, so no CLAUDE.md, settings file, output style, built-in tool, or
persisted transcript reaches a caller-derived prompt.

`schema.ts` converts exactly the grammar `tool_schema.rs` emits and throws on
anything else, so an emitter that outgrows it fails a test rather than
registering a looser tool. Two Rust tests publish the fixtures the sidecar
tests read — the frame bytes and every real `parameters_json` — so the halves
cannot drift apart silently.

Dependencies are exact-pinned: @anthropic-ai/claude-agent-sdk 0.3.261,
@msgpack/msgpack 3.1.3, zod 4.5.4, and dev typescript 5.9.3 with
@types/node 24.13.3. `engines.node` is the installed major.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 02:10:33 +02:00
Meta Vens
b150b5e38e Add the SDK inference port, its oracle, and the model-name routing rule
`SdkInferencePort` runs one Claude Agent SDK query per round through a Node
sidecar and returns it as one `InferenceOutput`. It prepares through the
same `prepare_invocation` the connector port uses, so there is one identity
scheme and every `integrity_is_valid` variant is unchanged.

The sidecar computes no tool result. Every call is answered by
`ToolResultOracle`, which is the lane's own evaluator fold replayed over the
calls made so far — the same function that will recompute the string at
re-derivation. A lane lends its oracle keyed by the prepared request id
immediately before `infer`; the connector port ignores the lend.

Rust owns the transcript lowering, the receipt digest, and the fault
disposition. The sidecar names a closed set of reasons and cannot assert an
integrity violation, a request identity, or a tool result.

`open_inference` routes each configured model: a `GHOSTLIGHT_SDK_MODEL_PREFIX`
model reaches the SDK port, anything else the connector, and a model no
built backend claims fails at open rather than at its first tick. Ghostlight
reads no credential for this transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 02:10:33 +02:00
Meta Vens
708d621c0c Collapse the two prepared-identity bodies and the two constructors
`prepare_invocation` is now the one place a `PreparedInference` is built:
the connector port's `prepare_request` and `fixture_prepared_inference`
were the same body twice, and a second transport would have made it three.

`ControllerRunner::open` takes its ports. `with_test_ports` existed only
to bypass the two transport-specific parameters `open` hard-coded, so
removing them deletes the `#[cfg(test)]` twin rather than adding a third
constructor. Runtime binds the transport and the controller-work owner
through `open_inference` and `open_controller_work`, and
`GHOSTLIGHT_CONTROLLER_CREDENTIAL` is no longer unconditionally required.

Each tool lane's per-call body moves whole into a named fold —
`interpreter_tool_result`, `operational_tool_result`,
`grouped_tool_result` — which the batch evaluator now calls. No result
string changes. `apply_tool_call` was already this shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 02:10:33 +02:00
Meta Vens
651bfb2d34 Delete the unreachable SPEAK_KIND guard; test the driver's Interrupted arm
Fork D was dead code: affordance_grants is insert-only and a checkpoint
that reached ReadyToSubmit/NoProposal already had speech, so a fresh
opportunity on the same scope always carries it too. Delete the guard,
the overtaken(Some(..)) call it fed, and note why in `interrupted`.

Widen InferencePurpose, PreparedInference::purpose, and InferenceEvent
to pub(crate) (plus fixture_inference_events/fixture_recovery_required
test seams) so runtime::tests can script an inference port by purpose.
Add a driver-level end-to-end test: through run_cover_tick, the world's
operational agent speaks once between the narrative cell's Persona turn
and its first submit (a re-lowering) and once more between the
re-lowered submit and its own commit, spending the one re-lowering the
turn is owed. The second change lands on the driver's own Interrupted
arm rather than ControllerRunner::run_narrative directly, and the tick
commits exactly the operational agent's two acts.

WorldMailbox::submit_controller stays pub(super); the test reaches
speech through the already pub(crate) ControllerPort wrapper instead.

Soul's follow-up on step 10 (the witness pass): add a MidTurnCommit::
Witness cause and a controllers.rs test proving a Witness landing
between the actor's bound turn and its submit re-lowers with only the
anonymous "what this person knows changed" line and no Overheard block
- KnowledgeSource::Witnessed writes no spoken_at, so overheard_since
finds nothing.

Rebased onto origin/codex/ghostlight-dungeon-mvp (step 10 integrated,
5cd8853) after step 9's own follow-ups landed; no conflicts at this
branch's controllers.rs/runtime.rs seams.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:59:42 +02:00
Meta Vens
70ea6b1323 Falsify the witness pass over the divergences it could hide
Seven tests, no production change. Two lanes, one selection: a
`Reach::Place` channel's `fan_out` and a witness over the same place
agree over every root and every speaker, so the two callers of
`under_place` and `unheld` are pinned by behaviour and not only by grep.
`under_place` covers a place itself, covers below it transitively, stops
at its edge, and never reaches a subject with no position — including
under the widest root in the world.

Two patches built so the resolver's candidate model and the apply arm
could disagree, and do not: a witness whose only recipient is a subject
the same patch declares and places, and a witness whose only recipient
is a subject the same patch forgot the fact. Each is refused at one
layer if the layers part; both land.

What a witness hands the narrative lane, pinned at the kernel because
the lane's fixtures are another pass's: exactly one scope component
moves, `knows`, which is the anonymous line, and the row it writes
carries no `spoken_at`, which is the only gate on an attributable
`Overheard` row.

The no-`Told` pin named its knowledge-touching kinds by example; an
exhaustive match now breaks the build when a `ComponentOpKind` is added
without saying whether it touches knowledge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:41:22 +02:00
Meta Vens
ecdeb9214a Witness: one speakerless event over a place subtree
An event a region saw cost one AcquireKnowledge per witness against a
128-operation patch cap, so a hemisphere did not fit in a patch. `Witness`
lands `Witnessed` knowledge of one fact on everyone standing anywhere under
one place.

The operation stores the place, never the recipients: the subtree is
re-derived at apply from live positions through `under_place`, and holders
are dropped through `unheld`, so a forged effect cannot assert a landing the
world does not have and a repeat cannot reset a knower's credence. It names
no speaker, so `Told` stays unrepresentable outside `Communicate`.

A witnessed event is only its reception, so a witness that reaches nobody is
refused — at resolve over the candidate graph, which now models the fan-out
including this patch's own relocations, and at apply over committed state.
Confinement is by the named place, which is not weaker than confining by the
recipients: a consumer and an uncovered jurisdiction refuse every witness
through the closure that already refuses every place.

Both store schemas move to consumer.v2 and prior stores are refused at open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:41:22 +02:00
Meta Vens
307d8a5520 One owner for "under a place", one for "already a knower"
`audience`'s `Reach::Place` arm and `fan_out`'s already-holds filter were
inlined bodies. They become `under_place` and `unheld`, so the subtree read
and the never-overwrite rule each have one owner before a second caller
exists. No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:41:22 +02:00
Meta Vens
d20dc51cca Falsify the interruption pass over its untested causes
Soul review of plan step 9. Tests only; no production code changed.

Five additions, each aimed at a claim the pass makes and does not
check.

The leak invariant, over the causes the pass leaves untested: a
transfer out of a neighbour's custody, a route closed under the
actor's feet, and the actor's own grant revoked. The existing fixture
world has no route, no resource, and no jurisdiction, so its subjects
can only ever be interrupted through `knows` and `commitments`; a
world carrying that furniture is what makes the other component kinds
reachable at all. Each cause re-lowers once, reports its own anonymous
line, produces no overheard block, and carries no label, id, or value
belonging to anyone.

The membrane's exact boundary. A mid-turn commitment names its
counterparty by subject id, that value really is inside the persisted
`Interruption`, and no prompt byte carries it. That is the difference
between "the renderer takes values rather than a snapshot" and "the
renderer emits no value at all", and only the second is what the
Interpreter sees.

Fork B against the digest rather than against itself. The pinned
preimage fixes the serialization; this reproduces every live
opportunity's scope digest from the snapshot's own `components` field,
which is what "view and digest cannot drift" has to mean.

Fork A at the call sites it deleted. Both detail lanes resume a
persisted in-flight row after the world moved under it. The
operational lane still aborts at its detector and spends nothing; the
narrative lane spends one Interpreter round on a binding already known
to be stale, is refused at submit, re-lowers, and commits. The fork's
accepted cost is one extra round and it is bounded at one.

The receipt bump. A v2 receipt digest, rebuilt from the previous
version's own preimage rather than derived from the live function, does
not rehydrate under v3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:20:34 +02:00
Meta Vens
ad124eab0d Drop the interruption report's unread capture
Nothing reads it: the driver arm reports the subject, both digests, and
the gap detail, and the Eve receipt carries the prose and the gap. The
capture is still derived where the gap's source span comes from, and is
not stored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:20:34 +02:00
Meta Vens
1a19fb2231 Prove the interruption over real committed state
Eight tests, each producing its interruption from a real commit that
lands on the world mid-turn rather than a hand-built digest: a
co-located neighbour's speech, and an owner patch that moves a component
naming no actor anywhere.

They hold the leak boundary at the delta surface, which no existing test
covered: the section names a speaker only for a telling the subject
actually received, carries no fact id, revision, digest or `spoken_at`,
and an out-of-reach actor's label and utterance reach no surface of the
turn. They hold the bound: four inferences for one re-lowering, and a
second refusal ends the turn with the overtaken gap having spent nothing
and submitted nothing. They hold the shape: a forged ancestry, a chained
re-lowering, and a row that disagrees with its own turn are each refused
at the gate that owns them, and the re-lowered row rebuilds its request
byte for byte at the derived round with no snapshot.

`the_scope_digest_is_unchanged_by_this_pass` pins the preimage digest so
a future field, order, or serde-attribute change fails there rather than
silently reissuing every bound proposal in every live world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:20:33 +02:00
Meta Vens
1ef973a861 Interrupt the narrative turn instead of losing it
A scope digest that moved between a subject's prose and its commit is one
event with one detector — the kernel — and one handler, which renews the
turn's binding without re-running the Persona and lowers the same prose a
second time against an anonymous account of what moved.

Cut first. The narrative lane's three `ensure_scope_unchanged` calls are
gone: two detectors reporting different errors for one fact is what made
the pass need an entry point per stage. `exact_opportunity` is now the
sole authority on whether a scope moved, which is where the binding
already lives, and the operational lane keeps its early abort because it
holds nothing to preserve. `SubjectSnapshot` loses `holdings`,
`dependencies`, `incident_routes`, `authority`, and `controls` — each a
projection of a `ScopeComponents` field — and carries the components
whole instead.

The turn is re-recorded, never re-run. `PersonaTurnBinding` gains
`interrupted_from`, and a re-lowered turn's receipt commits to the
receipt it replaced by value; the prose and both inference receipts are
copied. One re-lowering is a property of the persisted shape rather than
a counter: a turn whose binding carries `interrupted_from` has no
successor arm.

What the Interpreter is shown is the typed component diff of the
subject's *own* components, named by meaning with no value, id, count or
actor, plus every statement told to it since its bound revision. The
renderer takes values rather than a snapshot, so a neighbour's state is
a compile error rather than a review note.

`controller_work.v11` and `persona_turn_receipt.v3` refuse prior rows
with no shim. No kernel arm changes: `ScopeComponents` gains
`Deserialize` and no field, so every scope digest is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:20:33 +02:00
Meta Vens
79206dfce7 Carry the road's rules from the seeded live smoke
The seeded smoke passed on the twelfth run: six persons authored into
Low Sere from the Kalsa vault in one round, the deficit to zero, nine
subjects and six boundaries at activation, eight cells per tick, the
seeded people speaking to each other. Each earlier failure became a rule
in the tree: strict function schemas (anyOf, typed tags) with an offline
totality test over both authoring lanes; a content-addressed provider
request identity so repair rounds do not collide with the connector's
replay record; the connector's expiry skew separated from the response
read timeout; the seed lane's own round budget with parallel tool calls
and a budget-end submit; canonical ids and the placement rule in the
brief; the commitment shape rule in its tool description; inference
faults and cell outcomes logged where the operator can read them; and a
harness that fails on an empty seed or a silent world.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 19:38:01 +02:00
Meta Vens
491b06e9b0 Land the six seed-lane Soul follow-ups
Vault: collect_notes canonicalizes every directory and note against the
vault's canonical root during the walk and refuses to descend into a
reparse-point directory entry, closing the junction escape. VaultError
no longer carries an absolute filesystem path: Root names no path and
Scope carries only the caller-supplied scope string. The link-fanout
receipt cap is now checked per receipt, not per link.

Mailbox: SeedPort::submit_seed takes its own snapshot at submission
time, refuses anything off Draft with KernelError::WrongPhase, and
binds expected_revision to that fresh snapshot instead of a caller-
supplied one — closing the window between a runner's initial Draft
check and a multi-round session's eventual submit. submit_principal
refuses evidence whose valid_until has passed, read at the mailbox
ingress boundary rather than in a reduce arm.

App session: VerifiedPrincipalEvidence carries valid_until, minted from
the session's own access_expires_at, so a long-lived SeedPort cannot
hold authority past the session that vouched for it.

Elaboration: SEED_INSTRUCTIONS now requires an obligation with a
counterparty from every authored subject, since a goal alone derives no
MissingStructure boundary; the goals-only reproducer stays as a live
test of that structural fact.

Controllers: updated the two require_answer/phase tests and the two
seed-lane spec tests whose expectations changed now that submit_seed
owns its own phase gate ahead of require_answer.

334 world / 379 crate baseline with 5 FALSIFIED reproducers ignored is
now 385 crate tests passing (2 unrelated live-infra ignores remain), no
FALSIFIED ignore left, fmt clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 18:31:31 +02:00
Meta Vens
697ed07081 Falsify the seed lane: five reproducers, eight bounds
Soul's pass over the seed producer. Tests only.

Five claims fail and are committed as ignored reproducers, each
naming its defect in the ignore reason:

- `collect_notes` follows a directory junction out of the vault
  root, and `strip_prefix` then dresses the escaped note in a
  vault-relative reference.
- `VaultError::Root` carries the server's absolute path into the
  owner's command receipt.
- `require_answer`'s `declares` reads declarations and evidence and
  not operations, so an operations-only patch passes the Active gate
  through `submit_seed`. The runner's phase check is the only gate,
  not the first of two.
- A `Goal` derives no boundary, so a world seeded to full strength
  out of goals alone activates with nothing for the elaborator.
- The mailbox never re-verifies principal evidence: a revoked
  session's evidence still commits as the owner.

Eight bounds hold and are asserted: an oversized note is never read
and the reader never writes, the receipt cap survives the link
fanout, `require_answer` still owns both phase rules, a session that
never submits stops at the shared round budget and its row stays a
fixed point, one derived id commits once, two landed sessions hold
two ids, `valid_seed_progression` refuses every backward transition,
and the custody probe counts a seed row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 17:56:13 +02:00