The log answers why: the journal, fired-marks, and the source stamp - #57
Merged
Conversation
Four non-blocking sign-off notes from the review, each folded in: the journal load expression now shows the per-entry model_validate pattern (death_records precedent) instead of a raw-dict get; the golden-regeneration fact cites the generators' own command dumps as the serialization site and names phase11_gates's refusals block as part of the diff; the interpreter framing for the command docstrings cites the spec rather than a module docstring that never carried it; and the JournalEntry.rounds naming divergence from DeathRecord.round is acknowledged with its rationale. Claude-Session: https://claude.ai/code/session_013aS1Fbhon3y9aipXJ8s24a
Every command gains `source: str | None` — an annotation naming the authored object or game system on whose behalf the command was issued. Execution never reads it; it rides the log so a save or a replay alone answers why a command happened. `min_length=1` keeps the empty string unrepresentable: absent is None. The four goldens whose payloads store command logs — phase4_delve, phase5_milestone, phase11_gates (its refusals block included), and phase12_wipe — are regenerated. The diff is exactly one added `"source": null` key per stored command and nothing else, verified structurally: stripping the null source keys from each new file reproduces the old file byte for byte. The creation, RNG-vector, and kernel battle goldens store no commands and are untouched.
MarkTriggerFired, AddJournalEntry, and RecordNote join the referee surface with their events (session.trigger.fired and session.note.recorded at referee visibility, session.journal.entry_added player-visible with the beat and its clock stamp) and their message templates. Two new engine-owned state blocks come with them: fired_triggers, a list in first-fired order, and journal, a list of frozen JournalEntry beats each stamped with the clock position it landed at. All three are legal in every mode, terminal modes included: they are bookkeeping, not play. None draws a die or spends a round, and a re-mark of an already-marked trigger appends nothing while still emitting its event — state records that a trigger has fired, the log records each firing, and a replay of a repeatable trigger's marks has to be accepted.
…player view Two new payload keys, fired_triggers and journal, ride the save beside the flag store, and load restores both with empty defaults: additive within schema version 3, so no bump and no migration, and a save written before they existed loads with both empty and starts remembering from there. Replay needs no code — the handlers rebuild both blocks by re-executing the log. PlayerView gains `journal`, the entries verbatim with their clock stamps. The fired-marks and the note texts stay out of it: trigger wiring and referee annotations are the game's secret, pinned by the leak test.
The three guides that teach the command loop, the views, and the listener pattern gain the lifecycle vocabulary: the three commands and the source stamp in sessions-commands-events, the journal in the player-view whitelist and the journal/trigger visibility split in views-and-visibility, and the mark/journal/note vocabulary a listener writes with in listeners-and-flags. Each page's runnable complete program grew the matching lines, so the fragments stay excerpts of code that runs. One Added bullet under [Unreleased]. The new golden, phase13_journal.json, records a short delve driven the way an authored trigger layer drives one, with no listeners registered: a mark, its flag, its journal beat, a turn's breather, a re-mark of the same trigger, its second beat, a referee note, and a quest-stamped grant. It asserts the milestone — save/load and replay rebuild the journal and the fired-marks exactly, and every source stamp survives the command log through both.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the phase 13 plan (#56): the engine substrate for authored triggers and quests, with no interpreter — the
sourcestamp on every command, the three lifecycle commands with their events, the journal and trigger fired-marks as engine session state, the journal in the player view, and additive persistence for both blocks. The milestone runs in the phase golden: with no listeners registered, save/load and replay rebuildfired_triggersand the journal exactly, and everysourcestamp survives the command log through both.What shipped
source: str | Noneon the baseCommand— one field, inherited by every subclass, never read by execution (pinned by a twin-session state-equality test and by grep: nothing insrc/reads it). It is logged and replayed with the command, so the log alone answers "why did this happen" — the seam phase 14's interpreter and phase 15's quest rewards will stamp.min_length=1makes the empty string unrepresentable; absent isNone. The four goldens that store command logs (phase4_delve,phase5_milestone,phase11_gates,phase12_wipe) regenerated with added"source": nullkeys as the entire diff — verified structurally: stripping those keys reproduces the old files byte for byte. The kernel, creation, and RNG-vector goldens store no commands and did not change.MarkTriggerFired,AddJournalEntry,RecordNote— theSetFlagpattern applied three times: referee commands, legal in all six modes (terminal modes included — phase 15's rewards land after the victory transition), no rejections, total handlers, no RNG draws, no clock time. Re-marking an already-fired trigger is accepted, appends nothing, and still emits — state records that a trigger has fired, the log records each firing — so replay accepts every logged mark under phase 14's repeatable-trigger semantics.fired_triggersis alist[str]in first-fired order (ordered and deterministic where a set is banned).JournalEntry(frozen:text,rounds) carries its own clock stamp because append-time is capturable only at append: front ends never replay the event log, and a compacted save sheds the log entirely.PlayerView.journalships the entries verbatim. Visibility follows the flag precedent:JournalEntryAddedEventplayer-visible (authored text is content data in a structured field, the spec's carve-out),TriggerFiredEventandNoteRecordedEventreferee-visibility (marks and notes are content wiring). The player view leaks neither fired-marks nor note text, pinned by test.fired_triggers,journal) with empty-default loads: noschema_versionbump, no migration; a pre-phase save loads with empty blocks and starts remembering — pinned by a test that deletes both keys from a real save document.phase13_journal.json): a short delve driven the way phase 14's interpreter will drive one — a lever-pull asMarkTriggerFired+SetFlag+AddJournalEntryall stampedtrigger:lever-east, a later re-mark of the same trigger pinning idempotence in the stored artifact, aRecordNotemargin, and aGrantItemstampedquest:the-jade-idol. Asserts finalfired_triggersand journal exact (stamps included), replay identical,load(save) == replay(seed, commands), and all tensourcevalues verbatim in the reloaded log. ARest(kind="turn")between the two beats puts them a clean 60 rounds apart — chosen for legibility (pacing the two-cell corridor ~34 moves could also spend a turn, but no one could read that golden).No
schema_versionbump, no new rejection codes, no spec edit, no adaptations entry — the last two silences argued in the plan (nothing here touches SRD text; B/X has no journal, no triggers, no session bookkeeping).Review provenance
Implementation and review both ran as Opus subagents under coordination, per the phase loop. Round 1 came back SOLID with six non-blocking findings; four were addressed as
address rubber-duck review findings— a guide sentence asserting the library-shipped interpreter exists (restored to the plan-sanctioned forward reference), the same guide calling the fired-marks list "a set of ids", a vacuous rounds assertion in the visibility test, and undocumented weighting/coupling in the fuzzer samples — and two resolved as no-change on the merits: thestr(entry)coercion inload_gameis the plan's own pinned code, and the golden'sRestneeded narrative honesty, not a code change. The duck's verified-good list actively checked the golden regenerations byte-for-byte, the no-reader claim onsource, handler purity (includingRecordNoteinside a dungeon, beyond the shipped test), import-cycle freedom, docs-gate compliance, and golden reproducibility. Round 2: re-verification of each fix, SOLID.Full gate green locally:
ruff format --check,ruff check,pyright,pytest(1964 passed),mkdocs build --strict.https://claude.ai/code/session_013aS1Fbhon3y9aipXJ8s24a