Skip to content

Implement data portability Stage 2 verification - #46

Open
CodeWithOz wants to merge 3 commits into
mainfrom
codex/data-portability-stage-2
Open

Implement data portability Stage 2 verification#46
CodeWithOz wants to merge 3 commits into
mainfrom
codex/data-portability-stage-2

Conversation

@CodeWithOz

@CodeWithOz CodeWithOz commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • shadow-read and compare the localStorage-authoritative topic archive and saved-scenario datasets against their IndexedDB mirrors
  • detect missing, extra, differing, relationship-invalid, and legacy-shape records
  • reconcile each mirror independently while keeping all production UI reads on localStorage
  • persist mismatch, repair, pre-repair integrity, post-repair integrity, verification status, and timestamps per dataset
  • invalidate stale verified metadata after any failed mutation mirror, with per-dataset dirty latches for periods when IndexedDB cannot update its own metadata
  • preserve Stage 1 compatibility and document the Stage 2 implementation/deployment handoff

Why

Stage 2 must prove that both IndexedDB mirrors are complete and equivalent before a later deployment can make IndexedDB primary. Count-only checks cannot detect records with the same IDs but different content, and stale verified metadata after a failed mirror mutation could incorrectly authorize Stage 3.

Review fixes

  • mutation mirror failures now transition the affected dataset from verified to failed and successful reconciliation restores verified
  • relationship and legacy-shape observations are recorded separately before and after repair so Stage 3 readiness uses canonical post-repair integrity
  • dirty tokens include a UUID and use shared compare-and-clear semantics to avoid cross-tab ownership collisions
  • added explicit coverage for independently matching stores with zero mismatches

Validation

  • NODE_OPTIONS=--no-experimental-webstorage npm test -- --run __tests__/tefArchiveStage1Mirror.test.ts __tests__/tefArchiveService.test.ts __tests__/scenarioService.roadmapSteps.test.ts — 32 tests passed
  • NODE_OPTIONS=--no-experimental-webstorage npm test -- --run — 625 tests passed
  • npm run build — passed (existing large-chunk warning only)
  • manual browser testing confirmed differing, missing, extra, localStorage-authoritative, deletion, and malformed-JSON reconciliation cases
  • CodeRabbit light review completed; its documentation-only finding was addressed. A Web Locks suggestion was evaluated but not adopted because the dirty latch must be written synchronously before any asynchronous lock wait; shared tokens, IndexedDB transaction serialization, and stability rechecks provide the cross-tab safety mechanism.

Deployment note

This PR intentionally does not switch UI reads to IndexedDB and does not authorize Stage 3. Stage 2 still requires deployment observation and recorded per-dataset results before advancing.

Summary by CodeRabbit

  • New Features

    • Added automatic background verification and reconciliation for durable data mirrors at app startup.
    • Detects missing, extra, conflicting, and invalid records, then repairs mirror data when needed.
    • Added detailed verification results, repair counts, integrity checks, and failure status tracking.
    • Preserves authoritative local data when verification or repair fails.
  • Documentation

    • Updated data portability documentation to reflect Stage 2 shadow verification as implemented and locally tested, with deployment verification pending.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
parle 14cd181 Commit Preview URL

Branch Preview URL
Jul 27 2026, 09:55 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
parle-personal 14cd181 Commit Preview URL

Branch Preview URL
Jul 27 2026, 09:56 AM

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Stage 2 durable mirror verification now compares localStorage with IndexedDB, repairs mismatches, records integrity and failure metadata, invalidates stale verification through dirty tokens, runs during app startup, and adds comprehensive tests and deployment documentation.

Changes

Durable mirror verification

Layer / File(s) Summary
Verification contracts and diagnostics
types.ts, services/tefArchiveService.ts
Verification metadata and diagnostics now include failed states, mismatch, repair, integrity, and shadow-verification details.
Shadow comparison and reconciliation
services/tefArchiveService.ts
The service compares authoritative and mirrored records, detects relationship and legacy-shape issues, repairs IndexedDB, validates the result, and persists enriched metadata.
Failure invalidation and verification entry points
services/tefArchiveService.ts
Failures persist diagnostics or dirty tokens, metadata reads expose invalidated verification, and individual or combined verification functions are exported.
Startup wiring and Stage 2 coverage
App.tsx, __tests__/tefArchiveStage1Mirror.test.ts, docs/data-portability/*
Startup invokes verification; tests cover repairs, malformed data, transaction failures, dirty states, and re-verification; documentation records Stage 2 status.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant verifyDurableDataMirrors
  participant localStorage
  participant IndexedDB
  App->>verifyDurableDataMirrors: verify both durable mirrors
  verifyDurableDataMirrors->>localStorage: read authoritative datasets
  verifyDurableDataMirrors->>IndexedDB: read mirror datasets
  verifyDurableDataMirrors->>IndexedDB: reconcile discrepancies
  verifyDurableDataMirrors->>localStorage: persist verification metadata
Loading

Poem

I’m a rabbit with mirrors, checking each byte,
Comparing the burrows from morning to night.
Missing hops repaired, old records made neat,
Dirty tokens warn when the paths misalign feet.
Now startup verifies with a soft data cheer!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: implementing Stage 2 data portability verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/data-portability-stage-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CodeWithOz
CodeWithOz marked this pull request as ready for review July 27, 2026 09:59

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (4)
__tests__/tefArchiveStage1Mirror.test.ts (1)

181-197: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the prototype spy in a teardown hook, not only inline.

vi.spyOn(IDBDatabase.prototype, 'transaction') mutates shared global state. If an awaited assertion or waitFor* rejects before the inline mockRestore() (lines 703, 738), the patched prototype leaks into every subsequent test in the file, turning one failure into a cascade. Same concern for vi.stubGlobal('indexedDB', undefined) at line 775.

♻️ Suggested teardown
   beforeEach(() => {
     localStorage.clear();
     vi.resetModules();
     vi.stubGlobal('indexedDB', new IDBFactory());
   });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+    vi.unstubAllGlobals();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/tefArchiveStage1Mirror.test.ts` around lines 181 - 197, Restore the
shared `IDBDatabase.prototype.transaction` spy created by
`failReadWriteTransactionsForStore` in an `afterEach` teardown so cleanup runs
even when awaited assertions fail; also restore `indexedDB` after tests that
call `vi.stubGlobal('indexedDB', undefined)`. Remove reliance on the inline
`mockRestore()` calls while preserving the existing failure-injection behavior.
services/tefArchiveService.ts (2)

503-546: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

writeFailedVerificationMetadata drops prior diagnostic counts.

On failure, the written metadata only carries verificationError/lastReconciledAt (from previous); mismatchCounts, repairCounts, and integrity counts are all omitted rather than carried forward from the last known-good previous record. Not a correctness bug, but a reader inspecting a 'failed' record loses visibility into the last observed mismatch/integrity state, which is useful for troubleshooting recurring failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/tefArchiveService.ts` around lines 503 - 546, The metadata
constructed in writeFailedVerificationMetadata must preserve prior diagnostic
counts from previous. Carry forward mismatchCounts, repairCounts, and integrity
counts from the existing previous metadata when creating the failed record,
while retaining the current verificationError, timestamps, and record counts.

362-425: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant full-table reads of SAVED_ADS_STORE on every topic-archive mirror pass.

inspectRecordIntegrityfindArchivesWithMissingAds performs a full readAllFromStore(SAVED_ADS_STORE) with no memoization. Within a single mirrorLatestSource attempt it's invoked up to 3 times for the topic-archive config: once via createShadowComparison (Line 570-574), once for postRepairIntegrity (Line 589), and once for integrityAfterMetadata (Line 609). Since mirrorLatestSource now runs on every topic-archive save/delete/shadow-verify (not just background verification), this means every single archive mutation triggers up to 3 full ads-table scans, and each retry attempt (up to MAX_MIRROR_STABILITY_ATTEMPTS) multiplies that further. This will scale poorly as the saved-ads collection grows.

Consider fetching the ads collection once per attempt and threading it through the three integrity checks instead of re-reading per call.

♻️ Sketch: memoize per-attempt ads read
-async function findArchivesWithMissingAds(archives: TefTopicArchive[]): Promise<string[]> {
-  const ads = await readAllFromStore<TefSavedAd>(SAVED_ADS_STORE);
-  const savedAdIds = new Set(ads.map((ad) => ad.id));
-  return archives
-    .filter((archive) => !savedAdIds.has(archive.adId))
-    .map((archive) => archive.id);
-}
+function findArchivesWithMissingAds(archives: TefTopicArchive[], savedAdIds: Set<string>): string[] {
+  return archives
+    .filter((archive) => !savedAdIds.has(archive.adId))
+    .map((archive) => archive.id);
+}

Then in mirrorLatestSource, fetch savedAdIds once per attempt (when config.inspectRelationships applies) and pass it down through createShadowComparison/inspectRecordIntegrity for all three call sites within that attempt.

Also applies to: 567-624

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/tefArchiveService.ts` around lines 362 - 425, Memoize the saved-ad
lookup once per mirror attempt instead of rereading SAVED_ADS_STORE in
findArchivesWithMissingAds. Update mirrorLatestSource to fetch and pass the
saved-ad IDs through createShadowComparison and every inspectRecordIntegrity
call, and adjust findArchivesWithMissingAds/relationship inspection to reuse
that per-attempt data across all three integrity checks and retries.
types.ts (1)

164-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a discriminated union for stronger verification-state typing.

All Stage 2 fields (lastVerifiedAt, mismatchCounts, repairCounts, integrity counts) are optional regardless of verificationStatus, so nothing at the type level prevents a consumer from reading a 'verified' record with missing counts (or a 'failed' record with stale counts left over). Every current write site (writeVerifiedMirrorMetadata/writeFailedVerificationMetadata) already sets these consistently, so this is not an active bug, but a discriminated union keyed on verificationStatus would make that invariant compiler-enforced for future writers/readers (e.g. App.tsx).

♻️ Sketch of a discriminated union
interface DurableDataMigrationMetadataBase {
  name: DurableDataMigrationName;
  version: 1;
  state: 'mirroring';
  lastReconciledAt: number;
  sourceRecordCount: number;
  destinationRecordCount: number;
}

export type DurableDataMigrationMetadata =
  | (DurableDataMigrationMetadataBase & {
      verificationStatus: 'verified';
      lastVerifiedAt: number;
      mismatchCounts: DurableDataMismatchCounts;
      repairCounts: DurableDataRepairCounts;
      preRepairIntegrityCounts: DurableDataIntegrityCounts;
      postRepairIntegrityCounts: DurableDataIntegrityCounts;
      relationshipInvalidRecordCount: number;
      legacyShapeRecordCount: number;
    })
  | (DurableDataMigrationMetadataBase & {
      verificationStatus: 'failed';
      lastVerifiedAt?: number;
      verificationError: string;
    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@types.ts` around lines 164 - 184, Replace DurableDataMigrationMetadata with a
discriminated union keyed by verificationStatus, using a shared base for the
common migration fields. Require all verification, mismatch, repair, integrity,
and flat post-repair count fields on the 'verified' variant, while the 'failed'
variant must require verificationError and may only retain lastVerifiedAt as
optional. Update dependent type references as needed while preserving the
existing writeVerifiedMirrorMetadata and writeFailedVerificationMetadata
contracts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@__tests__/tefArchiveStage1Mirror.test.ts`:
- Around line 181-197: Restore the shared `IDBDatabase.prototype.transaction`
spy created by `failReadWriteTransactionsForStore` in an `afterEach` teardown so
cleanup runs even when awaited assertions fail; also restore `indexedDB` after
tests that call `vi.stubGlobal('indexedDB', undefined)`. Remove reliance on the
inline `mockRestore()` calls while preserving the existing failure-injection
behavior.

In `@services/tefArchiveService.ts`:
- Around line 503-546: The metadata constructed in
writeFailedVerificationMetadata must preserve prior diagnostic counts from
previous. Carry forward mismatchCounts, repairCounts, and integrity counts from
the existing previous metadata when creating the failed record, while retaining
the current verificationError, timestamps, and record counts.
- Around line 362-425: Memoize the saved-ad lookup once per mirror attempt
instead of rereading SAVED_ADS_STORE in findArchivesWithMissingAds. Update
mirrorLatestSource to fetch and pass the saved-ad IDs through
createShadowComparison and every inspectRecordIntegrity call, and adjust
findArchivesWithMissingAds/relationship inspection to reuse that per-attempt
data across all three integrity checks and retries.

In `@types.ts`:
- Around line 164-184: Replace DurableDataMigrationMetadata with a discriminated
union keyed by verificationStatus, using a shared base for the common migration
fields. Require all verification, mismatch, repair, integrity, and flat
post-repair count fields on the 'verified' variant, while the 'failed' variant
must require verificationError and may only retain lastVerifiedAt as optional.
Update dependent type references as needed while preserving the existing
writeVerifiedMirrorMetadata and writeFailedVerificationMetadata contracts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54a3c2ad-9bd5-4548-88f8-0902026318f0

📥 Commits

Reviewing files that changed from the base of the PR and between 424d614 and 14cd181.

📒 Files selected for processing (6)
  • App.tsx
  • __tests__/tefArchiveStage1Mirror.test.ts
  • docs/data-portability/README.md
  • docs/data-portability/stages/02-shadow-verification.md
  • services/tefArchiveService.ts
  • types.ts

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