Skip to content

br doctor --repair silently discards all append-only history tables #471

Description

@Gerry9000

Version: br 0.5.7


Summary

br doctor --repair rebuilds the database from the JSONL export. The export carries
issue state only, so every append-only history table is emptied by the repair and the
resulting database reports integrity_check: ok — the operation looks like a success.

Lost on every repair:

table role
events who changed what, and when
gate_result_history which gate passed/failed, by which reviewer, at which revision
gate_results current gate state
close_metadata who closed an issue, whether policy was bypassed, which gates fired
capacity_occupancy capacity/exemption records

Preserved (because the export carries them): issues, comments, labels,
dependencies, export_hashes, child_counters, blocked_issues_cache.

No issue content is lost. What is lost is the entire provenance layer — the record of how
issues reached their current state.

Reproduction

mkdir -p /tmp/br-repro && cd /tmp/br-repro
br init --prefix repro
br q "first"                       # -> repro-1lc
br q "second"                      # -> repro-nbm
br update repro-1lc --status=in_progress --actor a
br update repro-1lc --status=in_review  --actor a

# before
sqlite3 .beads/*.db 'select count(*) from events;'              # 4
sqlite3 .beads/*.db 'select count(*) from capacity_occupancy;'  # 2

# induce a hard integrity fault (the trigger condition)
python3 - <<'EOF'
import glob
p = glob.glob('.beads/*.db')[0]
with open(p, 'r+b') as f:
    f.seek(4096); f.write(b'\xde\xad\xbe\xef' * 512)
EOF

br doctor --repair

Observed output:

ERROR sqlite.integrity_check: database disk image is malformed:
      index `sqlite_autoindex_issues_1` root: page 2 header invalid
ERROR db.write_probe: Failed to select probe issue
Repairing: rebuilding DB from JSONL...
Repair complete: imported 1, skipped 0

After:

integrity_check              ok
events                        0     (was 4)
capacity_occupancy            0     (was 2)
issues                        1     (was 2)

Two distinct problems

1. History tables are silently dropped. The JSONL export schema contains 31 issue-level
keys and no history tables — verified exhaustively across a 2,713-record export. Nothing in
the repair output says history was discarded; the run reports Repair complete and the
database then passes integrity_check.

2. The JSONL is treated as authoritative for issues too. In the repro the database held 2
issues and the export held 1, and the rebuild produced 1. Any issue present in the database
but not yet exported is destroyed by a repair.

Trigger conditions (narrowed by experiment)

state result
clean database + --repair No errors detected; nothing to repair — no rebuild
database/JSONL count mismatch WARN counts.db_vs_jsonl — no rebuild
corrupt database ERROR sqlite.integrity_checkrebuild fires

So this only triggers on a hard integrity failure — but that is exactly the situation where a
user runs --repair, and exactly when the history is least replaceable.

Where it happens

rebuild_database_family() in src/config/mod.rs calls
import_from_jsonl_snapshot_into_fresh_replacement(). The subsequent
VACUUM / REINDEX / VACUUM INTO steps are cosmetic (free-space accounting, per the
comments referencing issues #237, #246, #248) and are not the cause of the loss.

br doctor --help does state --repair "rebuilds DB from JSONL", so the mechanism is
documented — but the consequence for history tables is not, and the success output does not
mention it.

Impact observed in practice

On one active tracker, successive repairs reduced the event history to zero. Recovery
snapshots left in .br_recovery/ show each repair starting a fresh window:

snapshot A   20,506 events   616 gate results   354 close records   (5 months of history)
snapshot B    5,441 events   256               87
snapshot C      750 events     8                2
live                0 events    0                0

Each window begins within minutes of the previous one ending — i.e. each repair zeroes the
table and it refills from that instant. The apparent decay is not progressive loss; every
repair has always discarded everything.

Across a fleet of ~39 trackers, 11 currently show the signature (near-zero events with recent
oldest-event timestamps). Eight of those have no recovery snapshot retaining the history.

Suggested directions

  1. Warn loudly. If the repair will discard non-exported tables, say so before and after,
    with row counts. A silent success is the core problem.
  2. Carry history in the export, or write a sidecar during repair so it can be reattached.
  3. Preserve the pre-repair database under a name that is not rotated, and say where it is.
    .br_recovery/ snapshots exist but are reused, so a second repair can age out the only
    copy of the history.
  4. Consider a narrower recovery for integrity faults that does not require a full
    re-import — analogous to the existing --repair-indexes.

This is the third defect of the same shape in br doctor

Two earlier reports, both since closed, describe the same underlying problem — the
diagnostic's output does not track reality
, in both directions:

  • br doctor reported OK schema.tables while three tables were missing. A migration set
    PRAGMA user_version = 17 without creating capacity_occupancy, capacity_exemptions,
    capacity_exemption_history and five indexes; doctor (0.2.19) reported OK throughout
    because it trusted the version pragma rather than checking the tables. Reports OK when
    something is wrong.
  • br doctor --repair reported a false failure after a successful rebuild. It rebuilt the
    database from issues.jsonl, then exited non-zero on ERROR db.sidecars: WAL sidecar exists without a matching SHM sidecar, though the rebuilt database was fine. Reports failure when
    nothing is wrong.
  • This report: --repair reports success while destroying five tables. Reports success
    when something is very wrong.

Note the first of those concerns capacity_occupancy — one of the tables this report shows
being silently emptied. The two issues meet on the same table from opposite directions.

The narrow fix is a warning on the repair path. The broader one is that doctor's verdicts
are derived from proxies — a version pragma, a sidecar file, an exit code — rather than from
the state they claim to describe.
Each individual fix leaves that intact.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions