Cut 9 finished the play loop and gave the player an Eve card. Soul's
own probe over the vendored browser lowering found the player could
not actually use any of it: every bound command submitted
{"bindings":{}}, the card never woke after the first refresh, and one
play commit poisoned the version the kernel's compare-and-swap is
derived from.
One owner per version (PA.f148/PA.f160). The surface document's own
`version` names world.revision alone -- the one meaning
routeHint.sourceVersion and the kernel's compare-and-swap require of
it. The play row's own revision travels beside it as its own
`playRevision` field instead of folded in. The SSE wake signal
(`state.revisions`, `/api/eve/events`) is the one place the combined
value still lives, renamed `authenticated_wake_version` -- it is
never echoed back by a client, so folding is exactly its job.
The client round trip (PA.f149). `eve::local_draft` now emits the
vendored lowering's own EveStateBindingDescriptor shape -- copied
field-for-field from the vendor's own test fixture
(test/host-isolation.test.mjs), which builds exactly this under a
`ghostlight.play` surface id for its own local-draft binding -- so
`bindingName` actually matches what captureBindings names, instead of
falling back to the control's own node id. runtime.rs now unwraps the
lowering's own payload.bindings envelope before deserializing (the
documented gamecult.eve.command_invocation.v1 shape), and JSON-typed
textarea fields (targets/jurisdictions/lens_weights) accept the JSON
string a real textarea actually captures, not only an already-parsed
value. world.create's own form was also missing a control for
`brief`, a required field CREATE_BINDINGS had always advertised --
found by driving the real lowering over the real surface
(tools/eve_client_bridge.mjs), which no hand-built payload could ever
have caught.
The card wakes (PA.f150). publish_projection now runs after a play
turn's own execution finishes, on the same door eve_command's Ok arm
uses, not only at startup and after a world commit.
The question id never reaches the player (PA.f151). The lowering has
no authored, non-editable binding value ("hidden" is not a prop any
renderer reads), so the answers control is deleted outright --
server-side, execute_world's world.play arm resolves the open
question itself, and refuses a stale routeHint.sourceVersion (one
that predates the question) instead of trusting a client-named id.
PA.f84's guarantee survives: an answer cannot land on a question the
player never saw.
Play failures reach the player (PA.f153/PA.f154). PlayTable::run is
split into admit (replay/stale-answer/empty-answer/busy-turn/empty-
opening, synchronous) and execute (the round loop, spawned). The
route now awaits admit and answers denied with the reason instead of
discarding every refusal behind an already-sent accepted response.
Ownership (PA.f152/PA.f159). The play card, its controls, and the
world.play descriptor are owner-gated like world.advance_time and
world.seed, on both the surface and the route. player_subject now
resolves by human_controller matching the calling principal, not
merely by ControllerMode::Human alone. world.summary drops the
subject census; phase and clock stay.
Cleanup (PA.f155-PA.f158): stale doc references to the deleted
world.speak/story surface; PlayTurnView::turn_id and .state given
production readers (an error log line, and the admission gate's own
condition); the play row's revision surviving a restart pinned by a
new assertion; mutation M9.1 retired with the reason it can no longer
be expressed (the parameter it needed was deleted in Cut 9).
Tests: Dungeon bin 139 -> 145 (6 new), lib 622 -> 622 (untouched).
Discrepancies and known gaps, reported rather than resolved silently:
- The envelope-unwrap fix extends past the literal "envelope" wording
into payload *shape*: the JSON-textarea deserializer
(deserialize_json_capture) is an extrapolation beyond what the map
named, needed because the lowering has no JSON-typed control and so
can only ever capture such a field as text.
- The stale-answer refusal (execute_world's source_version check) has
no dedicated runtime.rs test: reaching AwaitingPlayer needs a
working inference backend, and the crate's only scripted one
(ScriptedPort) is private to play.rs's own test module, not
reachable from runtime.rs. The round-trip test's own "answer" leg is
scoped down to create/seed/activate/play for the same reason; PA.f151's
server-resolved-answer mechanism is proven instead at the play-table
layer, in play.rs's own (updated)
a_question_is_answered_through_the_cards_own_binding_and_the_turn_closes_with_narration.
- world.play's own route now checks world ownership explicitly before
admission; this was not named as an explicit sub-item but follows
directly from "owner-gated like world.advance_time and world.seed."
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>