Skip to content

An unreadable journey is named, never faked - #185

Merged
blisspixel merged 2 commits into
mainfrom
fix/progress-truth
Aug 9, 2026
Merged

An unreadable journey is named, never faked#185
blisspixel merged 2 commits into
mainfrom
fix/progress-truth

Conversation

@blisspixel

Copy link
Copy Markdown
Owner

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_delta reads 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

  • 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. load_journey_file stays for callers that genuinely do not speak to the player, with a doc comment saying why the distinction matters.
  • The terminal says what happened once, on stderr, names the file, and states plainly that nothing will be written over it. It announces no crossing it cannot see, because the comparison would be against a journey this run never read.
  • When a save is refused, the refusal is the whole story: the banners stop, with one line saying this run's earnings were not recorded.

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.py where 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.

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.
Copilot AI lite review requested due to automatic review settings August 9, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_file in 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.

Comment thread faces/cli/src/main.rs
Comment thread faces/cli/src/main.rs
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.
@blisspixel
blisspixel merged commit 660bef0 into main Aug 9, 2026
17 checks passed
@blisspixel
blisspixel deleted the fix/progress-truth branch August 9, 2026 16:25
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.

2 participants