An unreadable journey is named, never faked - #185
Merged
Conversation
A journey file that exists and cannot be read came back as a default, which is indistinguishable from a first run. So a player whose file went bad was silently demoted: the rank fell, the veil closed on someone who had crossed it, earned trophies disappeared, and because the same condition also stops the write, the identical level-up and trophy were announced again on every run, forever. A trophy that arrives every time is not a trophy. Nothing was ever at risk of being overwritten, and that is now proved rather than assumed: the delta writer fails closed against the same read, and the new gate check asserts the player's bytes are still on disk after a run that could not read them. Core grew the fallible read the faces needed (read_journey_file keeps a missing file as a fresh player and returns everything else as the error it is). The terminal says what happened once, names the file, states plainly that nothing will be written over it, and announces no crossing it cannot see. When a save is refused mid-run, the refusal is the whole story: the banners stop, with a line saying this run's earnings were not recorded, instead of celebrating progress the disk never took. Held at the process boundary in game-truth, where this behavior actually lives, and mutation-verified: silencing the warning fails the gate.
There was a problem hiding this comment.
Pull request overview
This PR fixes a persistence edge case where an existing but unreadable journey file was treated as a fresh player, which caused silent progress loss in the UI and repeated level and trophy announcements across runs. It introduces a fallible journey read API in core, updates the CLI to communicate unreadable progress clearly and avoid announcing progress it cannot validate, and adds a process-boundary regression gate in scripts/game-truth.py.
Changes:
- Add
read_journey_filein core to distinguish missing journey files (fresh start) from unreadable ones (error). - Update the CLI journey load and end-of-run announcement logic to handle unreadable journeys and failed saves without celebratory output.
- Add a game-truth gate check that asserts unreadable journeys are named on stderr and are not overwritten.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/game-truth.py | Adds an integration-style gate asserting unreadable journeys are reported and not overwritten. |
| faces/cli/src/main.rs | Switches CLI to fallible journey loading, improves diagnostics, and suppresses announcements when progress cannot be validated or saved. |
| crates/core/src/persistence.rs | Documents the semantic difference between “missing” and “unreadable”, and adds read_journey_file API. |
| crates/core/src/lib.rs | Re-exports the new read_journey_file API from core. |
| CHANGELOG.md | Documents the player-visible behavior change for unreadable journeys and failed saves. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A run that could not read the ledger had already said so at the door, then tried the write anyway, which can only fail against the same condition, so the player heard one cause explained three times in three different voices. The write is skipped now: one cause, one telling. The diagnostic also carried a run of spaces from a wrapped string literal, the same shape as the error the exit tease had. It is built as two plain lines, and the path is resolved once instead of twice. The gate holds both: the unreadable-journey check now fails if a save-refused message rides along with the read message, and fails if any player copy on that path carries a double space.
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.
Two confirmed hunt findings about the same thing: what the product says when it cannot see your progress.
The defect
A journey file that exists and cannot be read (invalid UTF-8, oversized, permission denied) came back as a default journey, which is indistinguishable from a first run. So a player whose file went bad was silently demoted: the rank fell, the veil closed on someone who had crossed it, earned trophies vanished from the tally, and because the same condition also stops the write, the identical level-up and the identical trophy were announced again on every run, forever. A trophy that arrives every time is not a trophy.
Separately, when a save was refused mid-run, the terminal warned and then celebrated anyway, printing TROPHY EARNED and LEVEL UP for progress the disk had just refused, which is the same repeat-forever loop from the other direction.
What was not wrong, now proved rather than assumed
Nothing was ever at risk of being overwritten:
persist_journey_deltareads through the same fallible path and fails closed rather than replacing a file it could not read. The new gate check asserts it, reading the player's bytes back off disk after a run that could not parse them.The fix
read_journey_filekeeps a missing file as a fresh player and returns everything else as the error it is.load_journey_filestays for callers that genuinely do not speak to the player, with a doc comment saying why the distinction matters.The lock
This behavior only exists across a process boundary (an env-isolated profile, a real binary, a file that is bytes rather than text), so it is held in
scripts/game-truth.pywhere the rest of that class lives. Mutation-verified: silencing the warning fails the gate.Full workspace gates green through the pre-commit hook, plus the game-truth gate and its unit tests.