Skip to content

An objective says what it is called: display names end the raw-id transcript - #66

Merged
mmacy merged 1 commit into
mainfrom
objective-names
Aug 8, 2026
Merged

An objective says what it is called: display names end the raw-id transcript#66
mmacy merged 1 commit into
mainfrom
objective-names

Conversation

@mmacy

@mmacy mmacy commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Closes #65.

An objective finally has a player-facing display name everywhere its id used to stand alone: on the spec, in the player view, on the lifecycle events, and in the default formatter's lines.

The fallback design

  • ObjectiveSpec.name is optional, defaulting empty — not required like QuestSpec.name. The additive rule decided this: quests shipped in 1.5.0 with name required from birth, but ObjectiveSpec already exists in the wild under schema 3, and a required field would brick every document written before it. Empty means unauthored, and the fallback is documented on the field: every surface that shows a label degrades to the objective's id.
  • ObjectiveView.name is always populated — the authored name, or the id when the document authors none — so the view's docstring promise ("what it is called") is true and a quest-log renderer never ships an empty caption.
  • ObjectiveRevealedEvent / ObjectiveCompletedEvent carry name and quest_name, both resolved at emission (a wire client holds no document to resolve them from). quest_name follows the issue's symmetry argument: QuestActivatedEvent already ships the quest's name, so the objective events do too rather than making hosts resolve it. AdventureCompletedEvent gains name for the same reason — its template leaked the raw quest id the same way. All three new event fields default "" purely because events are serialized artifacts under the additive-only rule: a log written before the fields existed still parses. The engine always fills them.
  • Templates fall back to the ids: Quest {quest_name or quest_id}: objective {name or objective_id} is done. — a freshly emitted event prints names, an old artifact prints exactly the pre-change wording, and format_message stays total.
  • validate_adventure gains no rule: quest-name validation is parse-time only (min_length=1), and an objective's name is deliberately allowed to be absent — there is no reference to resolve and no invalid state to catch.

Tests and goldens

  • test_quests.py: spec default and pre-name-document parse, resolved names on the events, the id fallback on events and view, the new template wordings, old-artifact formatting fallback, and the leak pin extended — a hidden objective's display name is exactly as secret as its id.
  • phase15_quest.json and phase5_milestone.json regenerated with their own generators: the diffs are exactly the added event fields and the renamed transcript lines — no command, draw, or state block moved. The other goldens came out byte-identical.
  • The authored examples (examples/tui_crawler/content.py, tests/crawl_fixtures.py) now name their objectives ("Recover the idol", "Bring it home", "Speak the rite", …), the TUI status line prints names instead of slugs, and the TUI chapter's captured transcript is re-captured from the real milestone run.
  • docs/spec.md records the field in the quests section with its fallback, in the section already covered by the additive-within-schema_version sentence; the views guide and CHANGELOG move with it.

Full uv run pytest (2246 passed), uv run ruff format --check, and uv run ruff check are green.

https://claude.ai/code/session_01GSYzXCjpPnVDLTC2rpmhTZ

…nscript

ObjectiveSpec gains name, the display label a quest log shows beside its
checkbox (#65) — optional and defaulting empty, so every existing schema-3
document loads unchanged; empty means unauthored, and every surface that
shows a label falls back to the objective's id. ObjectiveView.name carries
the resolved label, which makes the view's own "what it is called"
docstring true at last. ObjectiveRevealedEvent and ObjectiveCompletedEvent
carry name and quest_name — the symmetry QuestActivatedEvent already
shipped — AdventureCompletedEvent carries the concluding quest's name, and
the default templates print the names in place of the raw ids, falling
back to the ids so an event logged before the fields existed still formats
in the old wording. The event fields default empty only for that older
artifact's sake; the engine always fills them at emission.
validate_adventure needs no new rule: the quest walk resolves references,
and a display name references nothing.

Goldens: phase15_quest.json and phase5_milestone.json regenerate because
the objective and adventure-completed events now carry the name fields and
the formatter renders them — the transcript reads "Quest The Votive Idol:
objective Recover the idol is done." where it read raw ids. No command,
draw, or state block changed; the diffs are exactly the added fields and
the renamed lines. The examples and fixtures author objective names so
those transcripts read as authored content, and the TUI chapter's captured
output is re-captured from the same milestone run.

Claude-Session: https://claude.ai/code/session_01GSYzXCjpPnVDLTC2rpmhTZ

@mmacy mmacy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — pass

Read the full diff and spot-checked the load-bearing claims at the head commit:

  • The fallback design is coherent end to end. ObjectiveSpec.name optional-empty is the right call over #65's "presumably required": QuestSpec.name was required from birth in 1.5.0, but ObjectiveSpec predates the field under schema 3, and the additive rule decides it. Every label surface degrades to the id, and the three places that promise a label (ObjectiveView.name, the two objective events, the templates) each keep the promise — the view via objective.name or objective.id at construction, the events resolved at emission, the templates via or-fallback so a pre-field artifact prints exactly the old wording. Verified the emission sites in session.py fill from the owning quest's spec (quest_name=spec.name) and AdventureCompletedEvent.name from a field that is required non-empty, so a freshly emitted event never carries an empty label.
  • The symmetry claim checks out: QuestActivatedEvent ships a required name, so the objective events carrying quest_name follows the established shape rather than inventing one.
  • The leak pin is real: _quest_views skips unrevealed objectives before ObjectiveView is ever built, and test_the_view_carries_no_quest_wiring now asserts the hidden objective's display name is absent from the serialized view — the name is exactly as secret as its id.
  • No validation rule is correct, not an omission: there is no reference to resolve and no invalid state — empty means unauthored by design. Documented on the field.
  • Formatter totality holds: format_message still never raises, and the executed old-artifact tests (model_validate without the fields) prove the additive-parse claim rather than asserting it.

One note for the record: the PR body says the golden diffs are exactly the added fields and renamed lines, but phase5_milestone.json also catches up party_document.engine_version 1.4.0 → 1.5.0 — expected regeneration fallout, harmless.

CI green on both platforms, docs job green, mergeable. Merging squash; this unblocks osr-web phase 2 (mmacy/osr-web#44).

https://claude.ai/code/session_01GSYzXCjpPnVDLTC2rpmhTZ

@mmacy
mmacy merged commit a9a0bdc into main Aug 8, 2026
5 checks passed
@mmacy
mmacy deleted the objective-names branch August 8, 2026 11:13
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.

An objective has no display name anywhere: views and templates print raw objective ids

1 participant