Skip to content

feat(recovery): detect incompatible indexes, refuse stale reads, and recover the records - #60

Closed
pablontiv wants to merge 23 commits into
mainfrom
rescue/recovered-source-accounting
Closed

feat(recovery): detect incompatible indexes, refuse stale reads, and recover the records#60
pablontiv wants to merge 23 commits into
mainfrom
rescue/recovered-source-accounting

Conversation

@pablontiv

@pablontiv pablontiv commented Aug 26, 2026

Copy link
Copy Markdown
Owner

What this does

Makes an incompatible or stale index a reachable condition instead of a silent data hazard: it is detected, reads are refused, and the records are recoverable.

Compatibility

  • Hermetic inventory of released schema shapes, checked-in lineage signatures, 18 verified testdata schemas (internal/compat).
  • Semantically equivalent schema shapes are canonicalized, so a cosmetic release difference is not reported as a break.

Refusal

  • Every stale-index consumer is blocked instead of reading incoherent data (cmd/backscroll/index_policy.go). This is the one deliberate behaviour change: commands that previously returned data from an incompatible index now stop, and the diagnostic points at recover --from <path> --dry-run.

Recovery

  • New recover command with a genuinely read-only dry run (cmd/backscroll/recover.go).
  • Builds a verified union destination, then atomically replaces the active index with the preserved backup (internal/recovery), with per-platform no-clobber guards: RENAME_EXCL on darwin, RENAME_NOREPLACE on linux, and a safe fallback.
  • Backup is preserved and restored on every failure path, with explicit phase tracking.
  • Legacy migration hardened; inspected lineages migrate atomically.

Performance

  • internal/sequences/prefixspan.go: PrefixSpan now recurses on the projected database rather than the full one. Previously every node of the search tree re-scanned and re-projected all sessions and events, which is why patterns --kind sequences --all-projects hung. Pinned by a 114-line equivalence test.

Known limitation — read before merging

A recovered database does not yet pass validate --indexed-only.

CreateRecoveryDestination installs the canonical union of records but leaves indexed_files empty, and verifyRecoveryDestinationRows currently requires that emptiness (recovery_destination.go:243). Database.Validate requires every search_items.source_path to have an indexed_files row, so the recovered database trips the public orphan check.

The fix is designed and planned in this very branch — docs/superpowers/specs/2026-08-19-recovered-source-accounting-design.md and the matching plan specify a reserved backscroll:recovered marker with a NULL last_indexed, one row per distinct planned path, plus consumer branches in GetFileHashes, GetStats and BackfillDerived — but it is not implemented here: recoveredSourceHash, isRecoveredSourceHash, recoveryDestinationSourcePaths and insertRecoveryDestinationAccounting do not exist yet.

So this PR delivers detection, refusal, atomic recovery and the perf fix; the accounting that makes a recovered database validate is a follow-up.

Verification

  • go build ./... clean; go test ./... — 22 packages pass, 0 failures, same as main.
  • 11 new test files; no existing test file deleted.

Reviewer notes

Worth a look at: whether the refusal in index_policy.go blocks every consumer it should without blocking ones it shouldn't; whether the atomic replace stays atomic on partial write / interrupted rename / disk full; and whether the three no-clobber implementations are behaviourally equivalent.

pablontiv added 23 commits July 25, 2026 08:00
PrefixSpan's defining property is that recursion works on a database that shrinks at
each level. mineExtensions passed the original sequences down instead, so every node
of the search tree re-scanned and re-projected all 305 sessions and 36,956 events
with a longer prefix each time. That is why 'patterns --kind sequences
--all-projects' hung.

Project incrementally: the top level projects once per frequent item, and each
recursive step projects the current database by the single new item. This is
equivalent because project() completes a pattern greedily left-to-right, so
projecting by [a,b] equals projecting by [a] then by [b]. Pinned by an equivalence
test that runs 200 random corpora through both this implementation and the original
full-database recursion and requires identical output.

Measured on the real corpus at --min-support 20: max-length 4 now returns in 2s and
5 in 14s. The default of 6 still exceeds a reasonable wait — the frequent-pattern
set is genuinely exponential in length here — so this fix makes the command usable
at explicit lengths without yet making its default usable.

Claude-Session: https://claude.ai/code/session_01XsmeJSWsTTZPSzfk5kgXo7
@pablontiv pablontiv changed the title feat(recovery): detect incompatible indexes, refuse stale reads, and recover the data feat(recovery): detect incompatible indexes, refuse stale reads, and recover the records Aug 26, 2026
@pablontiv

Copy link
Copy Markdown
Owner Author

Review found the branch ships the design and plan for recovered source accounting but not the implementation — the four planned functions (recoveredSourceHash, isRecoveredSourceHash, recoveryDestinationSourcePaths, insertRecoveryDestinationAccounting) do not exist, and verifyRecoveryDestinationRows still requires indexed_files to be empty. Split out as #61 and documented as a known limitation in the description rather than left implicit.

@pablontiv

Copy link
Copy Markdown
Owner Author

Superseded. Every capability this branch carries has since landed on main through #59 (fix(compat): close semantic lineages and remediate recovery) and the seven commits around it. Verified: cmd/backscroll/recover.go, cmd/backscroll/index_policy.go, internal/recovery/recovery.go and internal/compat/catalog.go all exist on main, and internal/sequences/prefixspan.go is byte-identical to this branch's version.

The known limitation this PR documented is also already fixed on main: internal/storage/recovery_accounting.go implements the backscroll:recovered marker, so a recovered database now carries its indexed_files rows. Closing #61 with it.

Rebasing would mean resolving 260 conflicts to land nothing new.

@pablontiv pablontiv closed this Aug 26, 2026
@pablontiv
pablontiv deleted the rescue/recovered-source-accounting branch August 26, 2026 19:34
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