Commit graph

1,199 commits

Author SHA1 Message Date
Meta Vens
a6ceacaa30 Record play agent library fix batch 7c
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:54:58 +02:00
Meta Vens
e383cdec7d Sharpen the play table's own test suite (PA.f75)
table_view_prints_every_id_the_tools_take's pressure and dependency checks
passed trivially: the counterparty id is also on the Subjects line, and a
commitment index of 0 matched almost any id's own "0". Both now check the
Pressures section and the holder's own "depends on" field directly, through
new view_section/subject_row helpers, and the commitment-key check matches
the exact "command/index" pairing instead of the index digit alone.
subject_row scopes its search to the Subjects section, since PA.f74 put an
id beside every occupant and knower label too and an unscoped search could
return a slice spanning unrelated sections. The tool list drops communicate
(removed from PLAY_TOOLS; Dungeon drops it too) and channel printing gets
its own direct assertion against the Channels section, since communicate
was the loop's only source of channel-id coverage.

Adds PA.f57's missing case: a failed precondition over a fact the actor
does not hold must reveal neither the fact's statement, its id, nor its
label, proven against a fixture where a real fact with all three exists.
Widens the actor-facing "every other error" coverage to all 31 KernelError
variants (all constructible), including PatchRejected, which is the one a
regression toward Display formatting would most plausibly hit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:54:20 +02:00
Meta Vens
6d6897445e Name a call and a subject's civic standing in the agent-facing refusal and table (PA.f73, PA.f74)
describe_refusal's generic patch-mismatch path dropped detail three ways:
UnresolvedDraft lost the handle that never resolved and the kind it was
expected to resolve to; a handle declared twice across two calls could only
ever be blamed on one of them, through the same "the call that declared
`handle`" wording a single declaration gets; and EmptyEvidence named no
call at all, because DecodedBatch never recorded which call contributed
which evidence entry. UnresolvedDraft and DuplicateHandle now get their own
rendering, and DecodedBatch carries evidence_calls alongside site_calls so
EmptyEvidence can name its call the same way a declaration or operation
site does.

table_view printed nothing about a subject's own authority grants, the
offices it holds or constitutes, or the forums whose standing covers it,
even though create_commitment's own checks and a restricted route's
requires all resolve through exactly that structure. It also printed a
commitment's kind as Rust Debug, occupant and known-by lists as labels with
no id to disambiguate a duplicate label, and re-sorted a subject's persona
values and memories out of their authored order. Each subject row now
prints its authority grants, offices held/granted (with incumbent), and
forums (with grievance kind); commitments print their kind in words;
occupant and known-by entries carry an id beside the label; and persona
values/memories keep their stored order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:44:45 +02:00
Meta Vens
dc43be5d8a Refuse a mismatched actor prefix and stop garbling roleless preconditions (PA.f71, PA.f72)
decode_actor_call fell back to the bare tool name whenever strip_prefix
failed, so a name actor_tools never emits under a given prefix (the bare
granted-entry name, unprefixed) still decoded. A non-empty prefix must now
be carried, or the call is refused.

precondition_roles dropped CanBroadcast's and CanReach's channel role
entirely, and describe_refusal_to_actor's roleless wording spliced "a
precondition with no role" into the "a precondition over role ..." template,
producing a garbled sentence for NoStanding, NoAudience, and an
out-of-range precondition index. precondition_roles now reads the channel
role through a shared audience_role helper, and the actor-facing roleless
case gets its own plain sentence instead of being spliced into the
role-bearing template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:30:12 +02:00
Meta Vens
e85de6e68b Record play agent kernel fix batch 7b
1d50509, 7d59e41, 56020d4 and 275b0f2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:18:09 +02:00
Meta Vens
275b0f21e6 Require word boundaries at both ends of a located source span (PA.f67)
`SourceSpan::locate` in ghostlight-persona-projection was a raw substring
`find`: "I can" byte-exact matched inside "I cannot go" and was accepted as
the quoted span, even though it is not the word the model claims to be
quoting. "Speech is a verbatim span" only holds if the span's edges are word
edges.

`locate` now walks every occurrence of the quote (`match_indices`) and takes
the first one whose start and end each land on a word boundary: the edge of
the source, or a character that is not Unicode-alphanumeric
(`starts_on_word_boundary` / `ends_on_word_boundary`). A first occurrence
that fails either edge is skipped in favor of a later one.

The Interpreter (crates/ghostlight/src/controllers.rs) calls the same
function through `SourceRange`; `cargo test -p ghostlight --lib` still passes
at 603, so no existing test relied on a mid-word span.

Added five tests: a mid-word quote is refused, a whole-word quote is
accepted, a span equal to the whole source is accepted, a later
boundary-valid occurrence is found after an invalid first one (the first
occurrence fails only the start edge), and punctuation at a span's edges is
handled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:15:22 +02:00
Meta Vens
56020d4155 Pin the Persona checkpoint's request check and the forged-effect replay refusal (PA.f50, PA.f51)
PA.f50: nothing pinned `NarrativeCheckpoint::Persona`'s integrity check —
replacing its `prepared_matches_request` call with `true` failed no test.
Added a synchronous `persona_checkpoint` fixture plus a test that persists a
Persona checkpoint, tampers its `guidance` after `invocation` was already
prepared from the honest one, and checks both `integrity_is_valid` and the
real store's own resume gate (`CultCacheControllerWorkStore::open`, which
decodes and re-checks every row's integrity) refuse it.

PA.f51: `a_forged_mirror_acted_event_is_refused_by_verify_history` in
consumer.rs recomputed the forged commit's own digest but left the head
snapshot's `last_commit_digest` pointing at the original, honest digest. That
mismatch alone made `verify_history` refuse the forgery earlier, as "head
state does not equal replayed history", so the test passed even with the
effect-equality check in `apply_committed_command` (journal.rs) disabled.
Pointing `head.last_commit_digest` at the forged digest closes that
unrelated gap, leaving the effect-equality check as the only clause that can
still refuse it.

Doc fix: the comment on `SubjectRow::last_acted_at` (lib.rs) still described
the recency marker `projector_knowledge` stamps as `minted_at > last_acted_at`;
the code (controllers.rs) has read `entry.acquired_at > acted_at` since
PA.f44. Corrected to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:07:55 +02:00
Meta Vens
e929d130e4 Record the operator's review of the PA.f61 ruling
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:05:22 +02:00
Meta Vens
98451c9b69 Record Soul on play agent fix batch 7a: invariants 2 and 8 hold
P7.1 is falsified in a small way: a bare name still decodes (PA.f71).
Refusal wording, the table's missing authority, and test gaps are
PA.f72 to PA.f75, to fix in batch 7c. PA.f76 is recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:01:00 +02:00
Meta Vens
7d59e41869 Stamp Knowledge.acquired_at with the revision a commit actually resolves to (PA.f49)
Every `Knowledge.acquired_at` write computed `state.revision + 1` inline, which
is right for every commit except genesis: genesis applies at revision 0 and
resolves to revision 0 (`resulting_revision: 0`, `state.revision` never
advances past 0 for the creating commit), so a fact a genesis patch both
declared and granted knowledge of landed at `acquired_at: 1` — one revision
ahead of the state it was written into.

`apply_operation` and `apply_operations` now take the resolution revision as
an explicit `resolves_to` parameter instead of deriving it locally. Genesis
passes `state.revision` (0); every other caller passes
`state.revision + 1` through a small `resolution_revision` helper, computed
once per `apply_effect` branch instead of four times inline. Callers that
invoke `apply_operation`/`apply_operations` directly for a forged effect
(tests, and `action.rs`'s component-writer probe) now supply the same value
explicitly.

`verify_state_shape` in journal.rs gained a companion check: a knowledge
row's `acquired_at` must not exceed the world's own revision, so a forged or
corrupt row of this shape cannot pass replay silently.

Added three tests: a genesis-seeded knowledge row is acquired at revision 0,
a later commit's row is acquired at that commit's resolved revision, and the
shape check refuses a row forged ahead of the world's revision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:52:37 +02:00
Meta Vens
1d50509535 Share one predicate for a retired institution between the resolver and the kernel (PA.f48)
`candidate_effective_authority` in patch.rs, the resolver's twin of
`delegated_authority`, still counted a retired institution's delegated
authority after fix batch 5 taught the canonical reducer to skip it. The
resolver and the kernel held two different opinions about whether a dead
institution's office still lent authority: a grant directly to the reeve
after retiring the treasury was refused as OverlappingJurisdiction against a
lend that no longer existed, and a Relocate admitted by the resolver on the
strength of that same dead lend blew up at apply time as a
KernelError::Invariant instead of a normal resolver refusal.

`candidate_effective_authority` now takes the same `retired` set the
resolver already threads through patch resolution and skips a retired
institution's office exactly as `delegated_authority` does. Both callers
(`graph_overlaps` and the Relocate restricted-route check) pass it through.

Added two tests: a grant to the reeve after retiring the treasury is
admitted, and a Relocate through a restricted route that relies only on a
retired institution's delegated authority is refused by the resolver as an
ordinary RouteAccessRestricted mismatch, not a later Invariant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:31:20 +02:00
Meta Vens
2391fd7a92 Record play agent library fix batch 7a
8de082a, 71a5c81, 1847974 and 2035fe8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:14:19 +02:00
Meta Vens
2035fe8722 Widen the shared-decoder test to every tool and route two more copies through it (PA.f59)
`the_elaborator_and_the_table_share_one_decoder` proved agreement between
`apply_tool_call` and `decode_authoring_call` for exactly one tool,
`open_route`. Renamed to `the_elaborator_and_the_table_agree_over_every_tool`
and widened to every non-session `PATCH_TOOLS` entry's own generated
example, asserting the whole `WorldPatch` — declarations, operations, and
evidence — agrees, not only operations.

patch.rs's `decodes` carried its own copy of the tag-injection-then-
deserialize logic `decode_authoring_call` already owns; it now builds the
same arguments string and calls `decode_authoring_call` directly (`Session`
still short-circuits to `Ok(())`, matching `decode_authoring_call`'s own
refusal of that shape). `every_declaration` and `every_component_op`
similarly deserialized `arguments_for`'s output straight into `Declaration`/
`ComponentOp`, bypassing the table's decoder entirely; both now decode
through `decode_authoring_call` and pull the single declaration or operation
out of its `WorldPatch`.

Corrected `authoring_tools_admits_every_known_name`'s doc comment, which
described the unknown-name-refusal test above it rather than its own
assertion.

Added `describe_refusal_never_swaps_declaration_and_operation_site_wording`
(mutation X6): a declaration site (`EmptyHandle`'s `position`) and an
operation site (`SubjectNotAtOrigin`'s `operation`) render "declaration tool
call #N" and "operation tool call #N" respectively, and must never trade
words.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:12:57 +02:00
Meta Vens
1847974b62 Fix refusal text, split its actor-facing twin, and read a fault's disposition (PA.f56, PA.f57, PA.f60)
PA.f56: `describe_refusal` had six separate correctness bugs. `EmptyEvidence`'s
`position` is an index into the patch's evidence list, but the generic site
reader printed it as "declaration tool call #N" — the same text `EmptyHandle`'s
own, differently-meaning `position` produces; the two are now told apart before
the generic reader runs. `WrongKind` dropped the expected and actual kinds
entirely; it now renders both, in plain words. `CustodyNotConserved` printed no
site at all; it now names the resource whose ledger failed to balance.
`TargetKindMismatch` printed Rust `Debug` for its `RefKind`s; both `Mismatch`
and `ActionMismatch` render kinds in plain words now. The dead `RefName`
branch inside the old `describe_referent` — unreachable because `WrongKind`,
the only variant that carries a `RefName` referent, also carries a `site`
that the site-detection chain always matched first — is deleted; `WrongKind`'s
own arm resolves its `RefName` referent directly, through a new
`describe_ref_name`. `ActionRejected` read no precondition detail at all;
`describe_action_mismatch` now takes the failed entry and the actor's own
invocation and, when both are given, appends the label of what the actor
actually bound to the failed precondition's role. Declaration and operation
site indexes were raw indexes into the patch, not call indexes; when the
caller holds the `DecodedBatch` that produced the rejected patch,
`describe_refusal` now resolves them through its site map.

PA.f57, ruling: two functions. `describe_refusal` is for the agent, with full
detail (it gained `entry`, `invocation`, and `batch` parameters for the
PA.f56 fixes above). `describe_refusal_to_actor` returns only what the
actor's own attempt reveals: for `ActionRejected`, the failed precondition's
role names — bare, from the entry's own precondition list, never resolved to
a label, an id, or a fact's statement, because the actor's own call already
supplied whatever it bound there and nothing else is handed back. Every
other `KernelError` — `MissingApprovals`, a store or journal error, anything
else — returns one fixed "refused" line that names nothing.

PA.f60: Dungeon had no way to read an `InferenceFault`'s disposition —
`requires_recovery` and `integrity_was_violated` are both `pub(super)`.
`InferenceFaultDisposition` is now `pub`, and `InferenceFault::disposition()`
is a public, read-only accessor of the existing three-way value; no
constructor changed and no new one was added.

Dungeon's `play.rs` has one call site of `describe_refusal`, for an actor
call's own refusal; per the rule not to redesign Dungeon's dispatch in this
cut, it now passes `None` for `entry`, `invocation`, and `batch`, keeping
its current behaviour exactly (full detail, not yet the actor-narrow
function) until Cut 8b wires the new parameters and switches this call to
`describe_refusal_to_actor` properly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:05:04 +02:00
Meta Vens
71a5c81240 Add the batch decoder and fix the actor vocabulary (PA.f54, PA.f55, PA.f58)
PA.f54, ruling: in a round, each maximal run of consecutive authoring
calls is one patch, committed atomically under one command id, with
draft handles shared inside the run. Adds `decode_authoring_calls`,
which decodes a whole run into one shared `WorldPatch` plus a
`site_calls` map from each declaration/operation site back to the call
index that produced it, so a later call's reference to an earlier
call's draft handle resolves within the run and a decode error names
its own call index. `decode_authoring_call` stays as the one-call entry
point with its own unprefixed error text; both now read the same
`decode_one_authoring_call`, so there is one decoder rather than two
copies.

PA.f55: `actor_tools` offered `record_need` and `finish_without_proposal`
through `catalog_tools`, but those end a turn — the controller lane's own
vocabulary — and `decode_actor_call` has no arm for either, so the play
agent could be handed a tool call it would always refuse. Splits
`catalog_tools` into `affordance_tools` (one tool per granted entry, no
turn-enders) plus the two pushes `catalog_tools` still makes for its own
callers; `actor_tools` now reads `affordance_tools`. `decode_actor_call`
also gains a `prefix` parameter and strips it from the incoming name
before matching, so a name `actor_tools` emits (`{prefix}{kind}`) decodes
as emitted through the one decoder rather than a caller stripping the
prefix itself first.

PA.f58: `decode_actor_call` silently dropped a non-string `display`
(`and_then(Value::as_str)` swallows the wrong type) and the shared
`decode_catalog_call` silently dropped a `text` field on an entry that
carries no speech. Both are now refused. Also widens the
`table_view_never_reaches_persona_or_projector_code` pin from grepping
only `controllers.rs` to scanning every library source file except
`table.rs` for a `table_view(` call.

Dungeon's `play.rs` is adjusted only for the new `decode_actor_call`
signature: its one dispatch call site and one test call site pass `""`
for the new `prefix` parameter, since Dungeon already strips the
`<handle>__` prefix itself before calling. Behaviour is unchanged; Cut 8b
is the one that wires Dungeon's own dispatch through the table's prefix
handling properly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:42:43 +02:00
Meta Vens
8de082a36c Widen table_view to the whole PLAY_TOOLS vocabulary (PA.f53, PA.f47)
table_view omitted every id `PLAY_TOOLS` takes outside the plain
subject/place/route/resource/affordance rows: no commitment keys
(discharge_commitment), no pressure rows (advance_pressure,
reduce_pressure), no channels (communicate's audience), no persona
material (set_persona_material), and no subject dependencies (bind,
release). Facts were gathered only from subjects' own knowledge rows, so
a fact nobody held was invisible to the one reader that is supposed to
see everything.

WorldSnapshot gains two omniscient, crate-private fields built straight
from world state in `snapshot()`: `channels` (every declared channel,
independent of who controls it) and `facts` (every fact the world holds,
independent of who perceives it). SubjectSnapshot gains `dependencies`,
lowered from the private `components.dependencies` the same way
`commitments`, `pressures`, and `material` already are. `table_view` now
prints all of it, plus route access, subject kind, and controller mode in
plain words instead of Rust `Debug`.

`FactStandingView` gains a `Ruled` variant. `snapshot()`'s per-subject
knowledge projection still collapses `Ruled` to `Canonical` (a subject's
own perception cannot tell a ruling from an ordinarily canonical fact),
and `controllers.rs`'s `typed_knowledge` keeps that promise explicit with
its own arm. The new omniscient `WorldSnapshot::facts` does not collapse
it, so the play table's own view prints `ruled` for what it is.

Replaces `table_view_prints_every_id_the_tools_take` with a fixture
holding one of every kind PA.f53 named — a claimed fact, a ruled fact
nobody knows, a channel, a commitment, and a pressure row — and takes a
real example per `PLAY_TOOLS` entry, composite fields included, rather
than the placeholder `field_example` composites the old loop silently
accepted. Adds direct tests for the named mutations: dropping the
granted-affordance filter (X1), printing a claimed fact as canonical
(X3), dropping the known-by list (X4), and printing `Ruled` as canonical
in the table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:21:35 +02:00
Meta Vens
d63caccc01 State PA.f61's persona-material rule in the play agent target
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:04:35 +02:00
Meta Vens
cddbef48d8 Record Soul on play agent Cut 8a: P8.4 falsified
Answers and continues arrive with new request keys and discard the open
turn (PA.f62). Persona prose never reaches the agent (PA.f64). An empty
answer wedges the turn (PA.f65). communicate and set_persona_material
break invariants 3, 4 and 2 in substance (PA.f61): communicate leaves
PLAY_TOOLS, and persona material is limited to new subjects. Self's
rulings under the standing go are recorded in each finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:03:57 +02:00
Meta Vens
ec4ac5ee88 Record play agent Cut 8a as landed at 6b69270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:44:34 +02:00
Meta Vens
6b69270a34 Add the play table's store and loop (Cut 8a)
PlayTurnStore (one row, over service/play-turn-v1.cc) and PlayTable run
a role-playing turn over Dungeon's PLAY_TOOLS subset of the library's
authoring catalog plus dispatch/advance_time/ask_player/end_turn. The
agent's conversation re-derives each round from opening_prompt, the
recorded InferenceOutputs, and each call's persisted body/result, so
nothing but world state crosses a turn boundary. Authoring calls commit
in maximal per-round runs through one commit_authoring_run seam (fix
batch 7a's batching rule); each call still submits as its own one-item
patch until the library ships a batch decoder. Actor calls quote-check
against the player's own texts or a dispatched Persona's recorded turn
before submitting through submit_principal or ControllerPort, and a
kernel Invariant from any commit poisons the table until restart.

Wiring into AppState, GHOSTLIGHT_PLAY_MODEL, world.play routing, and
the Eve schema is Cut 8b.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:43:18 +02:00
Meta Vens
4d071abc48 Record Soul on play agent Cut 7: P7.1 falsified, P7.2 unproven
The table view omits ids the play tools need (PA.f53), and decoded
calls cannot join one patch (PA.f54). Actor tools emit tools the decoder
refuses (PA.f55). Refusal text is wrong and serves both the agent and the
player (PA.f56, PA.f57). Self's rulings: one patch per run of authoring
calls, and an actor-facing refusal form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:28:55 +02:00
Meta Vens
4a6adb9181 Record Soul on play agent fix batches 5 and 6
P4.1 is falsified: the resolver still counts a retired institution's
authority, and the kernel does not (PA.f48). The genesis acquired_at stamp
is off by one (PA.f49). The checkpoint check and the PA.f40 test are not
properly pinned (PA.f50, PA.f51). These are to fix; PA.f52 is recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:04:02 +02:00
Meta Vens
9187facb3d Record play agent Cut 7 as landed at 7d40393
PA.f47: table_view cannot print ruled, because the snapshot collapses
Ruled to Canonical for every reader. The omniscient view gains Ruled;
subject-facing renderings keep it canonical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:47:25 +02:00
Meta Vens
7d40393bcd Add the table module: one consumer-neutral vocabulary for the play agent (Cut 7)
Dungeon's play agent needs to build tools, decode calls, and read inference
output without ever touching a pub(crate) kernel type. crates/ghostlight/src/table.rs
is now the library's one owner of that vocabulary:

- authoring_tools/decode_authoring_call wrap PATCH_TOOLS. apply_tool_call in
  elaboration.rs now calls decode_authoring_call for its Declare/Operate arms
  instead of carrying a second copy of the same decode.
- actor_tools/decode_actor_call wrap catalog_tools/decode_catalog_call
  (decode_catalog_call widened controllers.rs-crate-private to pub(super) so
  the table module can reuse it). decode_actor_call resolves the subject's own
  currently issued opportunity from the snapshot, never a value a caller
  supplies (PA.f41's rule, applied here).
- describe_refusal renders ActionRejected by precondition/slot/role and
  PatchRejected by the offending tool call's site; every other KernelError
  falls back to Display.
- InferenceRequest::play and InferenceOutput::events/receipt_digest are the
  two inference seams Dungeon needs to run and read a Play-purpose inference.
- table_view is the play agent's own richer renderer (ids, occupants,
  holdings, granted names, retired flag, facts with standing and knowers,
  clock, brief) and does not share text with render_world_structure, which
  moved here unchanged so the seed prompt's bytes did not move with it.

Known gap: FactStanding::Ruled collapses to FactStandingView::Canonical at
snapshot construction (lib.rs), so table_view cannot yet distinguish a ruled
fact from an ordinarily canonical one. Widening that is a lib.rs change
outside this cut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:42:44 +02:00
Meta Vens
d2de5766e8 Carry PA.f13, PA.f14 and the command-id rule into play agent Cut 8
There is no world quarantine at the play table. Inference faults become
tool results for the agent, and only a kernel invariant stops the table.
Cut 8 is split into 8a (the loop) and 8b (the wiring).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:50:15 +02:00
Meta Vens
dd398fbd2a Record play agent fix batch 6 (14d85da, 3b1f412, 55f8bc6)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:48:56 +02:00
Meta Vens
55f8bc679d Cover the assembled Projector request and three untested PersonaLane paths (PA.f42, PA.f43)
Tests only.

PA.f42: no prior test inspected the Projector request build_projector_invocation
assembles — the existing leak tests cover the Persona request built from its
prose, not the Projector request both turn and narrate build from a
SelectedDecision. the_projector_request_carries_no_structured_state_through_turn_or_narrate
asserts no subject id, controller id, world id, scope digest, state digest or
revision reaches it through either path. Kills the mutation that put the
full typed view into the Projector's guidance instead of the world's brief.

PA.f43, three untested PersonaLane paths, one test each:
- turn_refuses_an_operational_agent pins that turn refuses OperationalAgent
  before any inference runs. Kills the mutation that let turn accept it
  alongside NarrativePersona.
- turn_refuses_a_stale_scope_digest pins that a scope digest which moved
  since the opportunity was issued is refused. Kills the mutation that
  swapped PersonaLane::select's digest requirement for select_fresh's
  looser scope-only match.
- narrate_succeeds_for_a_human_subject_with_only_its_own_slice pins the
  success path for a real Human subject: its Projector request carries its
  own heard line and never another subject's knowledge or an absent
  subject's label.

mixed_controller_world adds a Human, a NarrativePersona and an
OperationalAgent subject to one fixture — reusing crate::tests's
human-approval draft flow lowered onto WorldMailbox — since no existing
fixture carried all three controller modes PA.f43 needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:47:55 +02:00
Meta Vens
3b1f412b92 Stamp every knowledge row with its own acquisition revision (PA.f44)
The recency marker was `minted_at > last_acted_at`, and minted_at is set only
by a speech or a display event's own fan-out. A row AcquireKnowledge lands, a
row Witness lands, and a row declaration-time seeding lands all carry no
minted_at, so `new` computed with `>` a `None` was always false for them: a
fact landed by a patch, a seed, or a ruling could never surface to a Persona
as new, however long after the subject's last act it arrived.

Knowledge now carries acquired_at: u64, the revision this write's own commit
resolves to (matching how DecisionEvent.revision and the minted_at scan built
from it are computed: one past the pre-increment state.revision apply runs
against). Every writer of the knowledge map sets it: AcquireKnowledge,
Communicate's fan-out, Display's fan-out, and Witness. The marker becomes
`acquired_at > last_acted_at`, with a subject that never acted (last_acted_at
is None) seeing every row it holds as new outright, matching the ruling
exactly rather than leaning on Option ordering.

minted_at keeps its own meaning — which act minted a spoken or displayed claim
— and stays on KnowledgeSnapshot and on the wire; no schema bump, since no
consumer.v6 store exists yet. AcquireKnowledge's own no-op refusal now
compares confidence and source alone, not the whole row, so a later
acquired_at cannot make an identical re-assertion stop refusing as
"knowledge operation changes nothing".

Tests: a row AcquireKnowledge lands after the subject's last act is new; one
that lands before it is not; the acquisition revision survives a journal
reopen; and the Cut 0 stimulus test's third row — seeded with no minted_at,
previously "new": false — now reads "new": true, because its subject never
acted and PA.f44 says every row such a subject holds is new.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:33:13 +02:00
Meta Vens
14d85dac59 Refuse a forged opportunity in the Persona lane, and cut its duplication (PA.f41, PA.f46)
select_one matches subject, mode, controller, scope and digest, then
substitutes the caller's own copy of the opportunity into its result: correct
for ControllerRunner, which persists and resubmits one run's bound
opportunity, wrong for PersonaLane, which persists nothing. A forged
opportunity (new world_id, revision 999999, emptied affordance_ids) passed
select_scope's scope-and-digest match untouched and landed straight in the
PersonaTurnBinding turn records.

PersonaLane::select now calls select_scope directly, requires the caller's
opportunity to equal the snapshot's own issued copy exactly, and returns that
kernel copy. turn builds its binding from selected.opportunity, never from
the caller's parameter.

Cut the duplication PA.f41 touched alongside it: prepare_inference,
run_inference and snapshot_for_selection are now the one body
ControllerRunner and PersonaLane both call; record_persona_turn is the one
place a PersonaTurnBinding is built from an opportunity and a receipt pair,
used by both run_persona and PersonaLane::turn; and the NarrativeCheckpoint
Persona integrity check now proves its persisted invocation against
persona_inference_request, the same unprepared builder build_persona_invocation
prepares from, instead of rebuilding the prompt a second time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:18:46 +02:00
Meta Vens
c9aa340db3 Record play agent fix batch 5 (bc1852d, 33fc9c6, 662a51f)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:35:47 +02:00
Meta Vens
662a51f647 Close four retirement test gaps and route the mirror-forgery test through replay (PA.f39-f40)
PA.f39: each surviving mutation gets a test that fails under it.
- M2: `AcquireKnowledge` now has a test with an `Evidenced` source, so a
  narrowing of the retired check to `source != Evidenced` is caught.
- M6: a mirror declared into a `Reach::Subjects` channel now has a test
  proving it stays in the audience, so `is_retired` matching
  `ExternallyControlled` is caught.
- M9: the Draft-only approval shape clause in `verify_state_shape` is now
  pinned directly with a forged `draft_approvals` entry outside
  `required_approvers`, rather than only exercised indirectly.
- Soul's probe S2: a placeless subject reachable only through a
  `Reach::Subjects` channel is now proven reachable while live and refused
  once retired, exercising `audience()`'s channel side, which PA.f33's
  Colocated-only `CanReach` test never reached.

`WorldJournal` gains a `#[cfg(test)]` `commits_for_test` accessor and
`verify_history` becomes `pub(super)` (matching `verify_state_shape`'s
existing visibility) so a Soul-style forgery test outside `journal.rs` can
route its falsification through the function that actually judges committed
history.

PA.f40: the "mirror acted" test at consumer.rs claimed the shape check
refused it on a controller-id mismatch. That clause was deleted in PA.f31,
and the test passed only by accident through the unrelated speech-claim
clause. Renamed and routed through `verify_history`: it forges the committed
effect's event scope onto the mirror, recomputes the commit digest, and
proves replaying the original command no longer reproduces that effect.

All four PA.f39 mutations verified as killed by direct mutation testing
against the committed code, then restored by reverse edit. Lib census: 549
before batch 5, 555 after bc1852d/33fc9c6 (PA.f36-f38), 559 after this commit
(PA.f39 adds 4; PA.f40 renames one test in place).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:34:52 +02:00
Meta Vens
089538ef42 Record Soul on play agent Cut 6: invariant 2 holds
A forged opportunity is recorded in a turn's binding (PA.f41). The
assembled Projector request and three lane paths are untested (PA.f42,
PA.f43). Rows with no minted_at never read as new, so the marker moves to
an acquired_at revision (PA.f44). The lane duplicates runner code
(PA.f46). The world brief is named in invariant 2 as world-public
(PA.f45).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:07:16 +02:00
Meta Vens
33fc9c671d Cover the dependency loop's retired-subject freeze (PA.f37)
The first PA.f37 freeze test only retires the farmer, who holds a routine and
an obligation but no dependency, so it never exercised `derive_motion`'s
dependency-loop retired check. Mutation testing against commit bc1852d
confirmed the gap: disabling that loop's `is_retired` skip passed the full
suite. This adds the other half — retiring the reeve, who holds the goal and
the Bind dependency, while the farmer stays live — which kills that mutation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:05:22 +02:00
Meta Vens
bc1852dfef Refuse a genesis self-retiring human, a retired promisor, and a retired institution's lent authority (PA.f36-f38)
PA.f36: the resolver tracked a freshly declared mirror in `mirrors` but never
tracked a freshly declared human, so a genesis patch that declared and retired
the human in the same patch was admitted; only the apply-time re-check caught
it, after `open_owner` had already committed an empty world.cc. The resolver
now tracks a `humans` candidate set the same way, seeded from the committed
controller assignment and extended for a subject this patch declares as
`NewController::Human`, and refuses its retirement in Draft with
`RetiresAnApprover`.

PA.f37: a retired subject could still act through the clock and through
commitments. `CreateCommitment` now refuses a retired promisor with
`RetiredSubjectActed`, and `derive_motion` now skips a retired subject's
commitments and dependencies entirely, so its routines stop rolling and its
obligations, goals, and dependencies stop pressing. Nothing is discharged or
deleted; a retired subject's commitments stay on record exactly as they were
the moment it retired.

PA.f38: `delegated_authority` read a retired institution's grants, so a live
office-holder kept a dissolved institution's authority. `Retire` does not
vacate offices — the incumbency stays on the ledger — but a retired
institution now lends nothing: `delegated_authority` skips a retired
institution's offices at the one site that turns an office into effective
authority.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:53:53 +02:00
Meta Vens
85bdf965c2 Record play agent Cut 6 as landed at c4334cc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:39:11 +02:00
Meta Vens
c4334cce3b Add the Persona lane and the recency marker (Cut 6)
PersonaLane (crates/ghostlight/src/controllers.rs) is the unpersisted
Projector-then-Persona turn PA-Q3 A called for: no work-store row, no
persisted state, a restart just repeats the two inferences. Its `turn`
runs Projector then Persona for a NarrativePersona opportunity and
returns a receipt-bound PersonaTurn; `narrate` runs the Projector alone
over a Human opportunity for the player's own view. Both refuse the
wrong mode before touching the mailbox, and a retired subject is
refused by `select` itself (no live opportunity survives retirement).

The Projector-then-Persona prompt builders in `run_narrative` and
`run_projector` move into two private functions, `build_projector_invocation`
and `build_persona_invocation`, so PersonaLane and the narrative lane
build both prompts through the same one seam: a Persona's input is
Projector prose and nothing else (invariant 2).

PA-Q2's recency marker: `SubjectSnapshot.last_acted_at` is derived in
`snapshot` from one pass over `state.events` (the greatest
DecisionEvent.revision scoped to that subject), and `projector_knowledge`
stamps each row `"new": minted_at > last_acted_at`. A subject that never
acted sees every minted row as new; an unminted background fact (no
revision) is never new, having no arrival time to compare.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:38:06 +02:00
Meta Vens
d392c088c3 Record Soul on play agent fix batches 3 and 4: all promises hold
PA.f36 to PA.f40 are to fix in batch 5. Self's rulings under the standing
go: retired subjects' commitments stop moving, a retired promisor is
refused, and a retired institution lends no authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:25:22 +02:00
Meta Vens
497abb0054 Record play agent fix batch 4 (fd64595, e18aa03)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:54:48 +02:00
Meta Vens
e18aa03878 Refuse retirement in channel reach, speech, knowledge, and office (PA.f33-f35)
`audience()`'s Reach::Subjects arm returned channel members without
filtering retirement, so a retired subject remained a Communicate audience
and a CanReach target (audience() is the one statement both preconditions
and Communicate read, so one filter, `is_retired`, fixes both). A retired
Communicate speaker was also resolved rather than refused. Audience
derivation stays the one owner of reachability, so a channel may still
declare a retired member; retirement is filtered at that single read site.

AcquireKnowledge, Forget, and InstallIncumbent all accepted a retired
subject as the one learning, forgetting, or taking office. All four ops now
push the new `Mismatch::RetiredSubjectActed`. Transfer is untouched: a
retired holder's holdings remain transferable, so a body can still be
looted.

Adds permanent tests for PA.f33-f35, including named pins for the five
mutations Soul's probes found live: MuA (retire-then-relocate), MuB (an
elaborator revoking outside its jurisdiction), MuC (a mirror flagged
retired), MuD (three revokes in one patch), and MuE (retiring the same
subject twice in one patch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:53:47 +02:00
Meta Vens
fd64595908 Stop retirement and revocation from corrupting a subject's own history (PA.f31, PA.f32)
`verify_state_shape` judged every past DecisionEvent against the subject's
*current* controller and affordance grants, so retiring a subject that had
acted, or revoking a verb it had used, failed `verify_append` with
CorruptJournal. Authority is judged once, at commit, by `reduce`, and after
that by replay (`verify_history` re-runs `reduce` for every commit) and the
digest chain; the shape check now holds only the time-invariant facts (scope
resolves to a live assignment, affordance is a catalog entry). Every
production caller of `verify_state_shape` (`verify_append`, `verify_history`,
both reached from `recover`/`initialize`) already replays, so no load path
loses coverage.

`required_approvers` reads the live controller assignment, and `Retired`
carries no human principal, so an owner could retire the human's subject in
Draft, approve alone, and activate without the human's approval. `Retire` now
refuses a Human-controlled subject in Draft with a new `RetiresAnApprover`
mismatch, re-decided at apply beside the resolver. In Active the human's
subject may die: the journal's approval-shape check now holds only in Draft,
since Active approvals are frozen history judged once by `ActivateWorld`'s
`reduce` arm and after that by replay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:49:24 +02:00
Meta Vens
a6e949219c Record play agent fix batch 3 (c8b9aad, c97fad2, 6e7c73b)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:02:10 +02:00
Meta Vens
6e7c73bdc4 Give the local port's response id the same null tolerance as tool_calls (PA.f23, PA.f24)
PA.f23: LocalChatResponse.id already tolerated an absent field through
#[serde(default)], but not an explicit "id": null, since #[serde(default)]
only fires when the key is missing. Give it the same null_as_default
treatment tool_calls already has, with a_null_response_id_decodes_as_empty
mirroring the existing tool_calls coverage.

PA.f24: add ipv6_loopback_opens_and_a_non_loopback_ipv6_address_is_refused,
pinning that the loopback check is IP-address loopback and not "any IPv6
address": [::1] opens and a routable IPv6 address such as
[2001:db8::1]:8080 is refused the same way a non-loopback IPv4 address is.
Test-only; the loopback check itself was already correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 06:01:07 +02:00
Meta Vens
c97fad2483 Add missing coverage for the play authority's admission gates (PA.f27, PA.f29, PA.f30)
Test-only. No production code changes: nothing here found a real bug.

PA.f27: require_ruler's own coverage gaps.
- apply_effect_re_decides_the_ruler_for_the_owner_too: a forged
  PatchAdmitted under the owner (CallerId::Principal), carrying a Ruled
  fact, is refused at apply exactly as it is for an elaborator. Kills an
  exemption for Principal in apply_effect's re-decision.
- require_ruler_catches_a_mint_past_the_first_operation: a harmless
  Relocate at index 0 followed by a Mint at index 1, from a non-Play
  author, is refused at index 1. Kills a scan narrowed to .take(1).
- require_ruler_reports_every_ruled_fact: two Ruled facts from a non-Play
  author are both reported. Kills a check narrowed to the first fact.

PA.f29: the evidence gate, extended and carried to the document path.
- the_evidence_gate_is_unchanged_for_every_author now also covers the
  owner in Draft (the seed's caller), the owner in Active with a real
  boundary answer, an answered elaborator, and a consumer on its own
  mirror. Each pair proves an uncited Canonical fact or Admit is refused
  (FactWithoutEvidence / AdmitWithoutEvidence) and the same content, cited,
  is admitted. The owner/elaborator "cited" patches also seat a fresh
  occupant at the dead end so the Boundary answer they must supply in
  Active is genuinely cleared, rather than masked by AnswerNotSatisfied.
- a_document_carrying_a_mint_is_refused_through_admit_document (in
  consumer.rs): a consumer document carrying a Mint is refused through the
  real decode path, admit_document, not only the in-process as_consumer
  fixture caller other tests use.

PA.f30: confinement and controller edge cases.
- a_consumer_cannot_retire_grant_or_revoke (consumer.rs): Retire,
  GrantAffordance and RevokeAffordance from a consumer are refused.
  Soul: every subject choice available to a consumer is also caught by an
  upstream structural gate (a mirror can never hold a grant or be retired;
  no non-mirror subject can ever bind to a consumer's own ground), so this
  specific narrowing — confine_to_ground confining only for an elaborator
  jurisdiction, not a consumer — is not independently killable from the
  consumer side. The refusal itself is real and the finding calls for the
  test, so it is kept; the redundancy is recorded in the test's own
  comment rather than dropped silently.
- a_revoke_cannot_leave_a_human_subject_mute: the last-verb refusal for
  RevokeAffordance holds for a Human-controlled subject exactly as for any
  other controller kind.
- retired_expects_no_caller: a direct unit pin that
  ControllerAssignment::Retired.expected_caller() is None.

All three production-touching mutations described by the finding (the two
require_ruler scans, and a hypothetical Human exemption on the last-verb
check) were applied by hand, confirmed to fail the new test, and reverted;
git status is clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:56:20 +02:00
Meta Vens
b849f4d566 Record Soul verdicts on play agent Cuts 4-5: P4.1 and P4.2 falsified
The journal's shape check judges past events against present assignments,
so a subject that acted cannot be retired and a used verb cannot be
revoked (PA.f31). The owner can retire the human in Draft to skip
approval (PA.f32). Retired subjects still hear and speak through subject
channels (PA.f33) and can gain or forget knowledge or hold office
(PA.f34). Self's design rulings under the standing go are recorded in each
finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 05:37:47 +02:00
Meta Vens
c8b9aad5f1 Close the genesis lane's require_ruler gap (play agent PA.f26)
prepare_creation and WorldState::genesis both admitted a creation patch
without calling require_ruler, so a genesis patch could carry a Ruled fact
or a Mint outright, and journal replay would happily reproduce it. Call
require_ruler in both places: prepare_creation maps its Vec<Mismatch> to
KernelError::PatchRejected, the same way every other reduce-site refusal
does; WorldState::genesis maps it to KernelError::Invariant, the same way
its own derivation check already does, since genesis has no patch-rejection
channel of its own.

Three new tests. Two exercise the full WorldKernel::create path with a
Ruled-fact and a Mint-carrying genesis patch. A third calls
WorldState::genesis directly with a hand-resolved effect, bypassing
prepare_creation entirely, so it is not satisfied by prepare_creation's own
check alone — this is what actually protects journal replay of a forged or
corrupted genesis commit, not just a freshly admitted one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 05:18:49 +02:00
Meta Vens
c90c634c60 Record play agent Cuts 4 and 5 as landed (3725ba8, 732b1d3)
Three loosening mutations survived and are recorded as PA.f30, to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 05:07:57 +02:00
Meta Vens
732b1d33ad Add granting and revoking affordances to the world kernel (play agent Cut 5, PA-Q10)
ComponentOp::GrantAffordance{subject, affordance} and RevokeAffordance
resolve beside GrantAuthority/RevokeAuthority: a retired or externally
controlled subject, or an unknown catalog entry, is refused (the new
Mismatch::GrantsOutsideControl); a grant already held or a revoke not held is
NoOperationEffect; a revoke that would leave a controlled subject with no
affordance at all is refused with the new Mismatch::WouldLeaveSubjectMute,
because derive_opportunities requires at least one. Applying either writes
state.affordance_grants directly, re-deciding the same three refusals against
committed state. confine_to_ground now refuses both, unconditionally, beside
Retire: a grant is authority over the catalog, not structure over ground, so
the owner (including the seed lane, the owner in Draft) and the play
authority are the only callers.

PATCH_TOOLS gains grant_affordance and revoke_affordance (Operate, subject
plus an affordance reference); catalog counts move to (7, 34, 43). Same
consumer.v6 / controller_work.v17 as Cuts 3 and 4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 04:54:35 +02:00
Meta Vens
3725ba80fb Add subject retirement to the world kernel (play agent Cut 4, PA-Q6)
ControllerAssignment::Retired is a new value of the existing controller
assignment map, not a second partition: id() and mode() return None for it
like a mirror, so derive_opportunities mints nothing and it is never an
audience once its position is gone. expected_caller becomes Option<CallerId>
(None only for Retired; a mirror still returns Some, the one caller that
could ever act for it) and both exact-caller comparison sites move to
Some(&caller).

ComponentOp::Retire{subject} resolves beside Relocate: refuses an already
retired subject with NoOperationEffect and a mirror with the new
Mismatch::RetiresAMirror. Applying it sets the assignment to Retired and
removes the subject's positions and affordance_grants entries, so a witness
at its former place never reaches it while its holdings, commitments, and
knowledge stay untouched. confine_to_ground refuses Retire unconditionally
for every confined author, the same unconditional shape require_ruler already
gave a Ruled fact and a Mint: the owner and the play authority are the two
callers left. SubjectSnapshot gains pub retired: bool, set in snapshot().

PATCH_TOOLS gains retire (Operate, one subject field); catalog counts move to
(7, 32, 41). Schema and controller_work row version are unchanged: this rides
Cut 3's consumer.v6 / controller_work.v17.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 04:46:43 +02:00
Meta Vens
7791bad845 Record Soul verdicts on play agent Cut 3
No promise was falsified. Genesis bypasses require_ruler (PA.f26), three
mutations survive the author-rule tests (PA.f27), and two promised tests
are missing (PA.f29); all three are to fix. PlayPort's convention sealing
(PA.f28) is defaulted and recorded under the standing go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 04:27:31 +02:00
Meta Vens
b0cfd1e9bc Record Soul's recheck of the Cut 2 fixes: P2.2 now holds
PA.f23 (null response id quarantines the lane) and PA.f24 (no IPv6
loopback test) are to fix; PA.f25 is recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 04:13:36 +02:00