Skip to content

fix(cli): a journal states the schema it was written in, and the reader reads it - #776

Merged
blafourcade merged 1 commit into
nextfrom
fix/a-journal-states-the-schema-it-was-written-in
Sep 5, 2026
Merged

fix(cli): a journal states the schema it was written in, and the reader reads it#776
blafourcade merged 1 commit into
nextfrom
fix/a-journal-states-the-schema-it-was-written-in

Conversation

@blafourcade

Copy link
Copy Markdown
Contributor

Closes two of #657's boxes, and reports the other three as already met. See the audit at the end.

The defect

record.cjs stamps schema_version on every session_start it writes:

const SCHEMA_VERSION = 2;
...
  return { type: "session_start", at, schema_version: SCHEMA_VERSION, run_id: runId, ... };

parseSessionStart never read it, and RunJournalSessionStart had nowhere to put it. A journal written under a schema whose line shapes had changed was therefore read as if it were this one — the silent misreading a version field exists to prevent. #657 states the box exactly: "a reader given schema_version: 2 behaves predictably and says so, rather than failing or silently misreading."

The change

The reader carries the stated schema, and refuses one it does not read. Version 1 was a mutable record rather than this append-only line log, so its lines are another shape entirely; a later version can change any line the same way. Reading either would mean guessing that whatever lines this parser still recognises mean what they used to.

Absence is not a disagreement. Every journal on disk before this reader looked at the field states no schema, and refusing those would drop attribution this reader has always given — "an unknown is never a zero", applied to the reader rather than to a figure.

A refusal does not cost the fact that the file is there. This is the part that needed more than a return null. Dropped from list() alone, a refused journal leaves check falling through to a branch that is false about it:

  • recorder-declared-nowhere claims no run file exists — one does.
  • anchorless-run-file blames a torn write or a hooks block missing SessionStart — the header parsed perfectly well.

RunJournalReader.listForeignSchemas carries the stated versions across, and hook-fired gains journal-in-another-schema, read ahead of the anchorless one: a build that cannot read a journal's schema cannot tell a missing session_start from one shaped differently, so blaming a torn write would assert what it just said it cannot see.

READABLE_JOURNAL_SCHEMA_VERSION is pinned against the hook's own exported SCHEMA_VERSION by a test, not copied — the same mirror sanitizePathSegment already is. A reader whose constant is a second copy goes on claiming it reads a schema the writer has moved past.

Guards

Mutations run, all killed:

Mutation Result
the reader no longer refuses 2 red
absence read as a disagreement 8 red
the new reason ordered after anchorless-run-file 1 red

The additive-key box has its own test: a key this reader has never heard of, under a schema it knows, costs it neither the header nor the boundaries.

The skill action 02-check/actions/02-diagnose.md teaches the new reason in step 6 — required, not optional: telemetry-claim.unit.test.ts fails any reason step 6 does not account for in the same sentence as its verdict token.

#657 audit, checked rather than assumed

Box State
aidd_docs/runs/ tracked or ignored by an explicit rule already met.gitignore carries .aidd/* with !.aidd/config.json, and aidd_docs/runs/* with !.gitkeep, !README.md. The issue's "neither tracked nor gitignored" is stale
A reader given another schema_version behaves predictably and says so this PR
An additive key does not break a reader, asserted with a fixture this PR
A rotation path that does not rewrite history, exercised in a test already metaidd telemetry forget removes run files by name through deleteRunFile, confined by isBareFileName, with e2e coverage
disable states what happens to data already written already mettelemetry off names what stays and forget is what removes it

Gates

pnpm test 3471 passed / 309 files · typecheck · biome ci exit 0 (2 pre-existing warnings) · knip · jscpd · layering · repository scripts 371 passed · bundle 595.8 KB, budget raised 596 → 598 with the measurement recorded beside the three raises before it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

…er reads it

The hook stamps `schema_version` on every `session_start` it writes
(`record.cjs`'s own `SCHEMA_VERSION`, 2). The reader dropped the field on the
floor: `parseSessionStart` never looked at it, and `RunJournalSessionStart` had
nowhere to put it. So a journal written under a schema whose line shapes had
changed was read as if it were this one — a silent misreading, which is the one
thing a version field exists to prevent.

The reader now carries the stated schema and refuses a journal stating one it
does not read. Version 1 was a mutable record rather than this append-only line
log, so its lines are another shape entirely, and a later version can change any
line the same way; reading either would mean guessing that whatever lines this
parser still recognises mean what they used to.

Absence is not a disagreement. Every journal written before this reader looked
at the field states no schema, and refusing those would drop attribution this
reader has always given — "an unknown is never a zero", applied to the reader
rather than to a figure.

A refusal must not cost the fact that the file is there. Dropped from `list()`
alone, a refused journal leaves `check` to fall through to a branch that is
false about it: "the recorder is declared nowhere" claims no file exists, and
"none carry a readable session_start" blames a torn write for a version
disagreement. `RunJournalReader.listForeignSchemas` carries the stated versions
across, and `hook-fired` gains `journal-in-another-schema`, read ahead of the
anchorless one — a build that cannot read a journal's schema cannot tell a
missing `session_start` from one shaped differently.

Mutations run, all killed: the reader no longer refusing, absence read as a
disagreement, and the new reason ordered after the anchorless one.

Bundle budget 596 -> 598 KB, measured 594.3 -> 595.8, recorded beside the three
raises before it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade requested a review from a team as a code owner September 5, 2026 06:21
@blafourcade
blafourcade merged commit 5788ad8 into next Sep 5, 2026
17 checks passed
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