`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>
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>
`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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
`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>
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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
`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>