Files
pad/scripts/diagnose-version-ties
xarmian dab3acd1f1 feat(store): a read-only diagnostic counts version tie groups migration 054 may have misordered (BUG-3751) (#2136)
* feat(store): a read-only diagnostic counts version tie groups whose order breaks history (BUG-3751)

Postgres migration 054 backfilled item_versions.version_seq ordering
same-second ties by ctid, a physical location rather than chronology. No
column records finer chronology (created_at is second-precision, id a
random UUIDv4), so whether a tie group was numbered out of order shows
only in whether its reverse patches apply in the order history reads
them.

DiagnoseVersionTies walks each item with a tie group from its current
content and classifies every group: current order only, ambiguous,
misordered (another order applies, the current one does not),
unresolvable, skipped (over 5 rows), plus items whose chain broke
outside any tie. Counts only, no ids or content, inside a read-only
transaction. `pad db diagnose-version-ties --pg URL | --sqlite PATH`
runs it without the store's migrate-on-open and prints JSON. It is
measurement first, by the lead's ruling, before any corrective rewrite
of history.

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

* refactor(store): the version tie diagnostic is a standalone script, read-only by construction (BUG-3751)

Per the lead, for running against production Postgres:
- No product surface: `pad db diagnose-version-ties` is removed. The
  operator runs `go run ./scripts/diagnose-version-ties -pg DSN`.
- Read-only by construction: one transaction opened READ ONLY through
  TxOptions and again with SET TRANSACTION READ ONLY. A test shows a
  write inside it refused (SQLSTATE 25006); with both removed (mutant on
  test-pg) the write goes through and the test fails. SQLite opens
  query_only.
- Bounded: SET LOCAL statement_timeout (-timeout, default 30s), one item
  at a time, progress on stderr.
- Counts only as JSON on stdout: no ids, titles or content.

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

* fix(store): the tie diagnostic bounds its whole run and does not judge groups a snapshot masks (BUG-3751)

Codex round 1:
- Per-statement timeouts did not bound the run, or the read-only
  snapshot it holds on a production database. -max-duration (default
  15m) stops the run and reports what it scanned, with truncated: true.
- A full-content row in a tie group resets the content whatever the
  order, so such groups are counted as with_snapshot and never as
  ordered or misordered.

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

* fix(store): the tie diagnostic's deadline is a context from the first statement, and permutations are checked against the rest of the chain (BUG-3751)

Codex round 2:
- The whole-run bound was checked only between items, after the first
  scan. It is now a context deadline set before the first statement, so
  it cancels any query (SQLite included) and ends the snapshot. Reaching
  it, wherever, returns a partial report with truncated set.
- An order was judged against only the next older version. It is now
  checked against every older version in stored order, up to the next
  tie group or full snapshot.

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

* fix(store): the tie diagnostic's deadline bounds classification too, and a sub-millisecond timeout is never 0 (BUG-3751)

Codex on the diagnostic: the run deadline was checked only between
items, so one item with many tie groups could run past -max-duration
and still report a complete result; and a positive -timeout under 1ms
truncated to statement_timeout = 0, which PostgreSQL reads as no limit.
Classification now checks the context per group, inside the permutation
search and before judging, and each item is folded into the report only
when it finishes. The timeout rounds up to whole milliseconds.

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

* fix(store): the tie diagnostic's deadline bounds every version it applies (BUG-3751)

Codex on the diagnostic: applyOrder walked and applied versions with no
deadline check, and the singleton, snapshot and oversized paths could
cross the deadline on their last step and still report the item
complete. applyOrder now checks the context before each version, and
every successful return of classifyTieGroups re-checks it. The checks
in the permutation search and before judging a group are removed: once
the deadline passes applyOrder applies nothing, and the return re-check
keeps the item out of the counts, so no mutant of them can be observed.

The test now ties the deadline to the work: it expires during the n-th
version application, for every n, across histories that take every
path. An earlier form tripped on the n-th Err call and could not see a
removed check.

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

* fix(store): the tie diagnostic sets its statement timeout first and requires a run deadline (BUG-3751)

Codex on the diagnostic: BEGIN and SET TRANSACTION READ ONLY ran before
statement_timeout was set. The timeout is now the first statement in the
transaction. BEGIN and that SET cannot be capped by statement_timeout;
they are bounded by the run deadline, which the operator entry point now
requires (a max duration of zero or less is refused). On Postgres the
test reads back statement_timeout and transaction_read_only from inside
the transaction.

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

* fix(store): a deadline that closes the tie diagnostic's rows before a scan is a truncated report (BUG-3751)

Codex on the diagnostic: database/sql closes the rows when the deadline
passes, and if that lands between Next and Scan the scan fails. Both
row loops returned that as an error; they now report it as truncated,
like every other deadline path. A test hook runs before each scan so a
test can put the deadline there.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-13 16:43:16 -04:00
..