Commit graph Ghostlight/crates
Author SHA1 Message Date
Meta Vens
872ba3525c Drop the answer digest from same_answer (L1 Cut 4, c4.s2.f3)
`step_session` compares a row found by command id against the fresh session
with `same_answer`. The command id is `session_command_id(world_id,
jurisdiction, answer_digest)`, so a row under the fresh session's id carries
the fresh session's digest unless its bytes were forged or the hash collided;
the digest clause never decided and survived as an equivalent mutant in two
Soul passes. It is deleted; world, jurisdiction and answer remain.

The id derivation is unchanged, and
`soul_the_session_command_id_derivation_moved_under_the_refactor` still pins
that a moved digest moves the id. Suite counts are unchanged and the distinct
warning messages equal those at 01312dd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:52:35 +02:00
Meta Vens
2b4b833d1d Pin that a retryable fault does not stop the sweep (L1 Cut 4, c4.s2.f1)
The session permit's rule that a retryable error does not raise the sweep's
stop was unpinned: the existing retryable test uses a pool of two, so both
sessions spawn before either fails, and `held.stop = outcome.is_err()`
survived. Under a pool of one the second entry is spawned only after the
first session's permit is released; the first call faults as retryable and
the second session must still run and commit in the same sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:45:24 +02:00
Meta Vens
635123fbf4 Sweep Uncovered beside a root in deficit in a test (L1 Cut 4, c4.s2.f2)
`Uncovered` was pinned only for a world with no deficit row, so pushing it
only when no jurisdiction was named survived. The new test sweeps a world
whose commons root is in deficit and whose unpositioned debtor holds a
MissingStructure boundary no root covers; one sweep must run the deficit
session and a session under `Uncovered` for that boundary.

The unpositioned-debtor world moves into `unpositioned_debtor_world`, taking
the scale intent, and the existing no-root test uses it unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:43:51 +02:00
Meta Vens
0764842f6b Return a session panic from the sweep instead of unwinding it (L1 Cut 4, c4.s1.f4)
A panic inside one elaboration session used to resume-unwind out of
sweep, abort its siblings mid-inference, and end the runtime's
elaboration driver task, whose handle is dropped: elaboration stopped
silently until restart.

Now each session holds its permit inside a SessionPermit guard whose
Drop raises the sweep's stop before the permit is released, on every
exit but a clean return. A quarantine-class error disarms nothing, and a
panicking task never reaches the line that disarms it, so no session
starts after a panic, by the same path that stops sessions after a
quarantine-class error. The join loop turns a task that did not return
into ControllerError::Serialization("an elaboration session did not
return: ...") and returns it after every started sibling has finished.

The class is quarantine-class on purpose: sweep returns only
quarantine-class errors and debug-logs the rest, so this keeps that one
rule, and the driver's handling of any returned error stays exactly as
it is (debug log). Whether that error quarantines cognition is L1.f1's,
and is not decided here. Serialization is the variant the elaboration
lane already uses for its local invariants.

elaborate_world takes its interval, as drive_cover_tick does, so a test
can drive it at one millisecond; production passes
ELABORATION_SWEEP_INTERVAL unchanged.

Tests: a_panicking_session_lets_its_sibling_finish (the held sibling
commits in the panicking sweep, the error is returned, permits are
free, and a second sweep resumes and commits the panicked session);
no_session_starts_after_a_panic; and in Dungeon
the_elaboration_driver_survives_a_panicking_sweep, which waits for a
second sweep's listing and inference against the driver task ending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:19:34 +02:00
Meta Vens
4b5013050f Prove pool independence for player controller turns (L1 Cut 4, c4.s1.f2)
a_speak_turn_and_a_sweep_draw_only_from_their_own_pools reads both pools
from inside the provider boundary, with the call held there by a gated
port. A world.controller.act turn through the HTTP route holds one
simulation permit and no elaboration permit, and still reaches the
provider with every elaboration permit held. A sweep holds no simulation
permit and at least one elaboration permit, and still reaches the
provider with every simulation permit held. The readings, not timing,
carry the proof; a timeout only turns a lane blocked on a pool into a
failure instead of a hang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:12:59 +02:00
Meta Vens
a3d109bd5e Sweep work under the Uncovered jurisdiction in a test (L1 Cut 4, c4.s1.f7)
the_sweep_claims_work_no_root_covers builds a world with no declared
root, so no deficit row names any jurisdiction, and one open boundary
only Uncovered can claim: a positionless debtor's obligation to a
creditor who can neither command nor sue it (MissingStructure). One
sweep runs one session for it, recorded under Uncovered with that
boundary as its answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:10:49 +02:00
Meta Vens
c90b5a046c Pin the sweep's Active-only gate (L1 Cut 4, c4.s1.f3)
a_draft_world_is_not_swept sweeps a Draft world holding an open dead end
and a deficit: no listing read (so no draw), no inference, no row, the
pool whole, and the world's revision unchanged. WorldPhase has only
Draft and Active, so Draft is the whole non-Active case.

dead_end_world now delegates to dead_end_world_in(phase, ...), which
skips approval and activation for Draft; every existing caller still
gets the Active world it had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:08:35 +02:00
Meta Vens
6f35229f24 Pin Cut 3's adoption rule on the NoPatch path (L1 Cut 4, c4.s1.f1)
The sweep hands step_session an adopted row's own session, so the
adoption rule is reachable with a differing fresh session only through
rows the in-flight listing excludes: ReadyToSubmit and NoPatch. Until
now only the ReadyToSubmit test killed whole-session equality (M3.1).

a_finished_boundary_session_stays_finished_after_the_world_moves ends a
boundary session NoPatch, narrows the weights to a lens it did not draw
(moving the ancestry), and steps the entry again: the row is found by
id, recognised as this answer's row, ends NoPatch with no inference, and
its bytes are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:06:52 +02:00
Meta Vens
3e35b3d006 Pin the id-only resume path and single-session permits (L1 Cut 4)
Test-only; no production change.

- a_boundary_row_ready_to_submit_is_submitted_after_a_tick: the listing
  returns only ElaboratorInFlight rows, so a boundary session that stopped
  between ReadyToSubmit and its submit is found by its id alone, and Cut 3's
  adoption rule decides whether it resumes. The test advances a refused
  session to that row through the real store and ticks the clock. The next
  sweep must submit the row under its id with no new inference.
  The test targets the mutation that restores whole-session equality in
  step_session. At bc31d11 that mutation survived every test, because
  adopting from the listing hands step_session the row's own session.
- one_answer_is_one_session_per_sweep now also records how many permits
  are free while its one session infers, and expects one. The target is a
  claim set that holds one entry twice. At bc31d11 that mutant passed this
  test, because the duplicate session resumed the first session's committed
  row.

This commit builds. Library 458 + 1 ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:02:14 +02:00
Meta Vens
bc31d1196f Put the elaboration call site and the quarantine stop under test (L1 Cut 4)
- Dungeon: the body of elaborate_world's loop becomes
  run_elaboration_sweep(&state). It checks quarantine, runs the sweep on
  state.elaboration_permits, and logs any error at debug, exactly as before.
  The interval loop is unchanged.
- saturating_one_pool_never_delays_the_other now runs its second half
  through run_elaboration_sweep. The production call site is therefore the
  one tested: passing controller_permits there fails the test, which was
  not true when the test called sweep directly.
- Library: no_session_starts_after_a_quarantine_error pins the sweep's stop
  rule. The pool holds one permit and the first session faults with an
  integrity violation, so the second entry never reaches the provider.

This commit builds. Library 457 + 1 ignored, Dungeon 53 + 1 ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:51:31 +02:00
Meta Vens
b3963f911d Run elaboration sessions concurrently over a per-sweep claim set (L1 Cut 4)
Rulings L1-Q4 (revised) and L1-Q9 (A).

Library (elaboration.rs):
- The sequential per-jurisdiction loop and step(jurisdiction) are deleted.
  ElaborationOutcome loses Clean and Inactive, which nothing produces now.
- sweep(permits) takes one snapshot and returns Ok unless the world is
  Active. It reads the store's in-flight listing once, builds the claim set,
  and spawns one step_session per entry into a JoinSet. Each session holds a
  permit from the caller's pool for its whole step.
- Every spawned session finishes before sweep returns. A quarantine-class
  error is returned after that, and no session starts after one has returned
  such an error. The sequential loop's early return also started no further
  session. Any other error is logged at debug.
- demand_entries is the claim set. For each swept jurisdiction it takes
  every covered boundary in derivation order, then the deficit if nonzero.
  Each entry is matched against the listing by world, jurisdiction and
  answer before anything is drawn. A match adopts the row's session and
  command id; if several match, the oldest is adopted and the rest are
  logged as orphans. An unmatched entry derives its id and draws its lens
  (fresh_session). An answer is claimed once, and so is a command id.
- step_session(command_id, session, snapshot) is the old step body. Its
  lookup and Cut 3's adoption rule are unchanged.
- step_in(jurisdiction), test-only, steps the first entry of one
  jurisdiction's claim set, so the Cut 3 tests keep reading one session's
  outcome.

Store: the listing now returns only ElaboratorInFlight rows. A committed
session ends in ReadyToSubmit, so listing that stage adopted a committed
deficit row, and every later sweep resubmitted it (AlreadyApplied) instead
of opening the next deficit session. That departs from the map's listing
contract.

Dungeon: elaborate_world and the ignored road test pass
state.elaboration_permits to sweep. The driver doc states that sweeps never
overlap.

Tests: 12 library tests.
- Concurrency: distinct_entries_run_at_once_under_the_ceiling,
  the_sweep_never_exceeds_its_ceiling, a_sweep_holds_a_permit_while_it_infers,
  one_answer_is_one_session_per_sweep.
- Claim set: a_boundary_under_nested_roots_is_claimed_once,
  a_deficit_entry_is_one_per_jurisdiction_per_ancestry.
- Errors: a_retryable_fault_ends_only_its_own_session,
  a_quarantine_error_is_returned_after_every_session_finishes.
- Rediscovery: a_stranded_deficit_session_is_adopted_with_its_refusal_intact,
  a_boundary_session_is_repaired_after_a_commit_and_a_tick,
  the_store_gains_no_orphan_per_commit, a_row_for_another_answer_is_not_adopted.

Dungeon adds saturating_one_pool_never_delays_the_other. The dead-end
fixture can now declare its commons (or commons and a ward) as a
jurisdiction root, with a subject that holds a goal, because a sweep visits
only jurisdictions that have a deficit row. The Cut 3 tests call step_in.

Docs: ghostlight-world-ontology.md states the concurrent sweep as the
shipped rule.

Counts: library 456 + 1 ignored, Dungeon 53 + 1 ignored. This commit
builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:44:04 +02:00
Meta Vens
6150bd7667 Split Dungeon's controller pool into simulation and elaboration (L1 Cut 4)
Ruling L1-Q4: neither lane may starve the other, and the elaboration pool is
a ceiling rather than a budget.

- AppState gains elaboration_permits beside controller_permits.
  controller_permits stays the simulation budget, which speak turns and
  cover cells draw from.
- configured_elaboration_ceiling() reads GHOSTLIGHT_ELABORATION_MAX_CONCURRENT
  at open, in the same shape as the simulation pool: default 2, minimum 1.
- The speak handler's comment now names both pools.
- The test fixture opens the second pool with TEST_ELABORATION_CEILING = 2.
- docs/architecture/ghostlight-dungeon-mvp.md names both variables and what
  each pool means.

This commit builds with one warning more than its parent:
elaboration_permits is never read. The next commit passes the pool to the
elaboration sweep, and the warning goes away there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:09:43 +02:00
Meta Vens
8538fab9a7 List in-flight elaboration sessions from the work store (L1 Cut 4)
ControllerWorkStore gains elaboration_in_flight(), a required method with
no default body. It returns every elaboration row in ElaboratorInFlight or
ReadyToSubmit, never NoPatch, with its command id, least recently stored
first.

- The CultCache store answers from its in-memory work map in one pass,
  after the same custody check lookup makes, and orders the rows by their
  stored_at.
- RecordingWorkStore answers over its map, in map order, because it keeps
  no write time.
- Dungeon's AlwaysFreshWorkStore returns an empty list.
- ElaboratorSession is now re-exported, because Dungeon implements the
  trait and must name the type. ElaborationCheckpoint::session is
  pub(super).

Nothing calls the method yet; the elaboration sweep reads it in a later
commit. This commit builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:09:00 +02:00
Meta Vens
2c3b513b17 Pin that a resumed session keeps its stored text (L1 c3.s1.f3)
Ruling L1-Q7 (a) persisted the instruction text so that editing a lens's
text in code neither breaks nor alters an in-flight session, but no test
proved a resume reads the stored text. The new test takes a real refused
row, rewrites it as a later build would find it (stored text differs from
the recorded lens's current text, invocation prepared under the stored
text), pushes it through the real store file, reopens, and resumes with a
fresh runner. Every inference after the resume carries the stored text,
the session commits under the same command id, and the row still records
the stored text and the drawn lens.

It pins resume only; nothing ties stored text to the recorded lens, which
remains a recorded cost.

Test-only; no production code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:52:26 +02:00
Meta Vens
b3b5053dab Pin that a fresh session draws its lens from its own command id (L1 c3.s1.f2)
Every lens-observing controller test used a single-lens world, so the
call-site draw was forced and a draw from an unrelated command id survived.
The new test runs one refused round in each of eight fresh worlds under the
stock weights, where every lens can be drawn, and asserts the recorded lens
equals lens::draw over that world and the id the row is keyed by, with that
lens's text. It asserts its premise: the weights give every lens weight, and
in each world some other id draws another lens. An unrelated random id
agrees with the session's in all eight worlds about once in 8^8 runs.

Test-only; no production code changes.

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