Skip to content

F-08 (Medium): Journal sequence numbers and deterministic ordering#8

Merged
adamburmister merged 8 commits into
mainfrom
fix/f08-journal-sequence
Jul 18, 2026
Merged

F-08 (Medium): Journal sequence numbers and deterministic ordering#8
adamburmister merged 8 commits into
mainfrom
fix/f08-journal-sequence

Conversation

@adamburmister

Copy link
Copy Markdown
Owner

Finding

F-08 (Medium) from the ledger audit: entries were keyed by random UUID with no monotonic sequence. allEntries ordered by date only — same-day entries returned in nondeterministic order; amountsForAccount ordered by entry_id ASC, i.e. by random UUID, a meaningless sequence for a statement view. There was no gap-detectable numbering: nothing could show "no entries have been removed."

Why it matters: a journal is by definition a chronological, numbered record. Sequential numbering is what lets an auditor assert completeness ("the sequence has no gaps") and gives each posting a citable identity ("JE-142"). Two runs of the same journal report listing the same day's entries differently reads as a control deficiency.

Fix

  • pluts_entries.seq (INTEGER, unique index): assigned as MAX(seq)+1 inside the existing posting transaction — safe under the Durable Object single-writer model. Carried on Entry and EntryDTO.
  • Legacy upgrade in migrate(): table_info probe → ALTER TABLE → backfill by rowid (insertion order = posting order; the library never deletes). Idempotent; re-runs never renumber. Runs before the DDL loop so the unique index applies cleanly.
  • Deterministic ordering everywhere: allEntries and entriesForAccount order by (date, seq); amountsForAccount now joins pluts_entries and orders an account's activity by the owning entry's (date, seq) — a proper statement view. The in-memory test repository mirrors all of it.
  • Ledger.verifyJournalComplete(): MAX(seq) === COUNT(*) — the auditor's completeness assertion as a one-call check (backed by the new Repository.entrySequenceStats()). Combined with the append-only triggers in F-04 (High) + F-14: Append-only triggers and row-validity checks #4, deletion becomes both impossible and detectable.

Compatibility note with #4: its pluts_entries UPDATE trigger is deliberately column-scoped to the financial fields, so this PR's seq backfill works on databases that already have the triggers, in either merge order.

Testing

Written first and watched fail: increasing seq assignment; same-date entries ordered by posting sequence in both directions; date-then-seq ordering; verifyJournalComplete on populated and empty ledgers; and real-DDL migration tests via node:sqlite — fresh databases get the column + unique index, legacy databases are backfilled 1..N in insertion order and re-migrating never renumbers.

Full suite: 85 passed. tsc --noEmit and biome check clean.

🤖 Generated with Claude Code

Entries were keyed by random UUID with no sequence: same-date entries
returned in nondeterministic order, an account's amounts were ordered
by UUID (meaningless), and nothing could prove completeness. A journal
is by definition a chronological, numbered record — sequential
numbering is what lets an auditor assert "no entries are missing" and
cite a posting ("JE 142").

- pluts_entries gains seq INTEGER with a unique index; assigned as
  MAX(seq)+1 inside the posting transaction (safe under the DO
  single-writer model); Entry/EntryDTO carry it
- migrate() upgrades legacy databases via a table_info probe: ALTER,
  then backfill by rowid (insertion order = posting order; the library
  never deletes), idempotently
- Deterministic ordering everywhere: allEntries and entriesForAccount
  order by (date, seq); amountsForAccount now joins entries and orders
  by the owning entry's (date, seq) instead of random UUID
- Ledger.verifyJournalComplete(): MAX(seq) === COUNT(*) — the
  auditor's completeness assertion as a one-call check, backed by
  Repository.entrySequenceStats()

Audit finding F-08 (Medium).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ec6a7c85e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/domain/ledger.ts
Comment thread src/db/schema.ts Outdated
adamburmister and others added 2 commits July 18, 2026 18:02
Two review findings on journal sequencing:

- The seq backfill only ran when the column was just added, so a deploy
  interrupted between ALTER TABLE and the backfill left NULL rows behind a
  'column exists' guard forever. The backfill now runs on every migrate,
  numbering unnumbered rows after the highest existing seq in rowid order —
  continuing from MAX(seq) also avoids unique-index collisions with rows
  posted after such an interruption (plain seq = rowid could collide).

- MAX(seq) === COUNT(*) detects entries missing between surviving rows but
  not a contiguous tail truncation, which shifts both together. Renamed
  verifyJournalComplete to verifyNoSequenceGaps and re-documented it (and
  README) to claim exactly what it proves; a true completeness assertion
  would need a high-water mark persisted outside the entry rows.

Addresses PR #8 review (2x P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c224241462

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
Comment thread src/domain/entry.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9da2b50301

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
adamburmister and others added 2 commits July 19, 2026 07:56
Fixes the biome formatting failure from the last push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fake's exec(query) dropped bind arguments, so the seq backfill's
parameterized UPDATE ran with unbound placeholders — silently a no-op on
this node:sqlite (NULL binds) and an outright failure on versions that
reject missing values. The fake now forwards ...binds like the
journal-sequence spec's fake.

Addresses PR #8 review (P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamburmister

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 5775dd88e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

migrate() now composes all three features in dependency order: meta table
+ schema-version rollback guard first (refuse before any DDL), main's
legacy negative-rowid repair (before the triggers exist), this branch's
seq column ALTER (before the DDL loop creates the seq unique index), the
DDL loop, the seq backfill, then the scale/version/currency stamps and
payload_hash backfill. pluts_entries keeps both the seq column and main's
calendar-date CHECK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamburmister
adamburmister merged commit f89b70d into main Jul 18, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9678096f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/db/schema.ts
description TEXT NOT NULL,
date TEXT NOT NULL,
posted_at TEXT NOT NULL,
seq INTEGER,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make assigned journal sequences immutable

When consumer code or a maintenance script uses the raw ctx.storage.sql handle, this new audit field is still freely updatable because the existing pluts_entries_no_update trigger only guards id, description, date, and posted_at. That means existing entries can be renumbered, e.g. swapped through temporary sequence values, while preserving uniqueness and MAX(seq) = COUNT(*), so JE identities and statement ordering can be rewritten without detection; add a migration-safe trigger/check that allows the initial NULL-to-number backfill but blocks later seq changes.

Useful? React with 👍 / 👎.

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