Skip to content

The log answers why: the journal, fired-marks, and the source stamp - #57

Merged
mmacy merged 7 commits into
mainfrom
phase-13-impl
Aug 7, 2026
Merged

The log answers why: the journal, fired-marks, and the source stamp#57
mmacy merged 7 commits into
mainfrom
phase-13-impl

Conversation

@mmacy

@mmacy mmacy commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Implements the phase 13 plan (#56): the engine substrate for authored triggers and quests, with no interpreter — the source stamp 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 rebuild fired_triggers and the journal exactly, and every source stamp survives the command log through both.

Stacked on #56 (base: phase-13-plan) because the plan PR can't merge during the GitHub outage. When #56 merges and its branch is deleted, GitHub retargets this PR to main automatically; the diff shown is implementation-only either way.

What shipped

  • source: str | None on the base Command — one field, inherited by every subclass, never read by execution (pinned by a twin-session state-equality test and by grep: nothing in src/ 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=1 makes the empty string unrepresentable; absent is None. The four goldens that store command logs (phase4_delve, phase5_milestone, phase11_gates, phase12_wipe) regenerated with added "source": null keys 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 — the SetFlag pattern 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_triggers is a list[str] in first-fired order (ordered and deterministic where a set is banned).
  • The journal is state, never derivedJournalEntry (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.journal ships the entries verbatim. Visibility follows the flag precedent: JournalEntryAddedEvent player-visible (authored text is content data in a structured field, the spec's carve-out), TriggerFiredEvent and NoteRecordedEvent referee-visibility (marks and notes are content wiring). The player view leaks neither fired-marks nor note text, pinned by test.
  • Persistence is two additive payload keys (fired_triggers, journal) with empty-default loads: no schema_version bump, 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.
  • The phase golden (phase13_journal.json): a short delve driven the way phase 14's interpreter will drive one — a lever-pull as MarkTriggerFired + SetFlag + AddJournalEntry all stamped trigger:lever-east, a later re-mark of the same trigger pinning idempotence in the stored artifact, a RecordNote margin, and a GrantItem stamped quest:the-jade-idol. Asserts final fired_triggers and journal exact (stamps included), replay identical, load(save) == replay(seed, commands), and all ten source values verbatim in the reloaded log. A Rest(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_version bump, 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: the str(entry) coercion in load_game is the plan's own pinned code, and the golden's Rest needed narrative honesty, not a code change. The duck's verified-good list actively checked the golden regenerations byte-for-byte, the no-reader claim on source, handler purity (including RecordNote inside 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

mmacy added 7 commits August 6, 2026 08:46
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.
Base automatically changed from phase-13-plan to main August 7, 2026 01:45
@mmacy
mmacy merged commit cf8e763 into main Aug 7, 2026
5 checks passed
@mmacy
mmacy deleted the phase-13-impl branch August 7, 2026 01:45
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