Skip to content

Phase 14 plan — triggers and the interpreter - #58

Merged
mmacy merged 3 commits into
mainfrom
phase-14-plan
Aug 7, 2026
Merged

Phase 14 plan — triggers and the interpreter#58
mmacy merged 3 commits into
mainfrom
phase-14-plan

Conversation

@mmacy

@mmacy mmacy commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Implementation plan for phase 14 of the spec: the trigger spec, authored triggers on the adventure document, and the library-shipped interpreter listener — document-order matching, once-only fired-state, the depth-4 cascade bound with truncation notes, and dropped-consequence recording; validate_adventure grows trigger reference checks. Milestone: the lever-opens-portcullis scenario authored as data replays identically with no listeners, and a trigger spawn colliding with an open encounter drops and records its note.

Notable decisions

  • The interpreter is the example listener's pattern promoted to library code, and two session seams harden first. Re-entrant session.execute() inside handle() is the proven issuance channel (FetchQuestListener), but nested-command events never reach the outer CommandResult (a front end would render nothing of the portcullis opening), and _persist_sight runs after the listener loop (a relocating consequence would make the seen map diverge under replay). The plan fixes both at the seam: the listener-loop splice and the sight-persistence reorder — no stored golden changes, since goldens snapshot state and logs, never result envelopes.
  • Emits nothing, holds nothing — with the one exemption pinned. The interpreter returns ([], {}) from every handle() and keeps no state the engine can't reconstruct; the phase golden asserts live-vs-listener-free-replay equality on every state block except listener_state (whose interpreter entry is asserted to be exactly {} — the phase 5 modulo-listener_state precedent, strengthened), with command and event logs byte-equal.
  • The fired beat's carrier is a pinned interpretation. Two spec paragraphs collide (fired text as a display beat vs. trigger-fired events being referee-visibility); the visibility ruling wins. The fired beat rides MarkTriggerFired/TriggerFiredEvent at referee visibility via the phase 11 event-beat mechanism; a trigger's player-facing voice is its journal form.
  • Character targets in authored consequences are selectors. A document can never name a session-scoped id, so character_id in an authored consequence must be @party or @first, expanded by the interpreter to living members at issue time — the log stays concrete and replays exactly. The consequence surface itself is a typed sub-union (ConsequenceCommand, nine classes) so lifecycle and player commands fail at parse; the exclusions plus selectors land as a two-sentence spec amendment with the implementation PR.
  • Zero new commands, events, rejection codes, or message templates. Phase 13 shipped the whole substrate; this phase adds two modules (crawl/triggers.py, crawl/interpreter.py), three additive fields (Adventure.triggers aside), and the seam work.

Review provenance

Drafted, then rubber-ducked by a skeptical reviewer with the spec, phases 10–13 plans, and the code as evidence. The review found one blocking hole and three non-blocking items, all accepted on the merits:

  • Blocking: the draft claimed byte-identical live-vs-replay full state, but register_listener itself creates the interpreter's empty listener_state entry a listener-free replay never has — the phase 5 golden compares modulo listener_state for exactly this reason. Fixed by pinning the comparison shape (above).
  • Non-blocking: the spec amendment should carry the consequence-surface exclusions per the phase 12 altitude test (an author would mispredict RollDice/IdentifyItem from the spec's current sentence); a census cross-check miscounted its exclusions; and phase 13's handed-off decision on MarkTriggerFired.trigger_id validation needed an on-record answer (stays open, SetFlag's key-domain precedent).

The re-review verified each fix against the code and returned SOLID, with one cosmetic naming remnant folded in as a sign-off tweak. The reviewer's verified-good list confirmed, among others: both seam defects are real; the event-log interleaving argument holds against the existing phase 5 byte-for-byte replay test; the collision beat works (SpawnMonsters mode-legal in encounter but rejected encounter_in_progress while mark/note/journal land); and the greenfield sweep found no shims, dual paths, or unconsumed scaffolding.

https://claude.ai/code/session_01AH3H26T16968mmY5e4ZDJB

mmacy added 3 commits August 6, 2026 19:08
The review found one blocking hole: the plan claimed live-with-interpreter
final state equals listener-free replay state byte-for-byte, but
register_listener itself creates the interpreter's (empty) listener_state
entry, which a listener-free replay never has — the phase 5 milestone
golden already compares modulo listener_state for exactly this reason.
The comparison is now pinned: equality on every block except
listener_state, with the interpreter's entry asserted to be exactly {},
and command/event logs byte-equal.

Non-blocking fixes: the spec amendment now carries the consequence-surface
exclusions (IdentifyItem, RollDice, the lifecycle family) alongside the
selectors, per the phase 12 altitude test; the census cross-check says two
named exclusions, not three; and phase 13's handed-off decision on
MarkTriggerFired.trigger_id validation is answered on the record (stays
open, SetFlag's key precedent).

Claude-Session: https://claude.ai/code/session_01AH3H26T16968mmY5e4ZDJB
@mmacy
mmacy merged commit 5ebea67 into main Aug 7, 2026
5 checks passed
@mmacy
mmacy deleted the phase-14-plan branch August 7, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant