feat(portability): versioned backups, v1 migration, and AI provenance on the record (#35) #84

Merged
rockachopa merged 1 commits from timmy/35-portable-backup-versions into main 2026-10-22 12:02:22 +00:00
Owner

Closes #35

What changed

  • Versioned export. exportLedgerBackup writes schema version 2 with the confirmed logs and the training contribution receipts (including withdrawn tombstones). Both are written through an allowlist, so unknown keys and credentials are never copied out of the device.
  • Migration on import. importLedgerBackup accepts version 1 and 2. Version 1 entries are upgraded, the notice names the version the file came from, and logs that predate provenance keep provenance: null rather than being given a source they never recorded.
  • Fail closed. A file from a newer Timmy is refused with an update instruction; malformed JSON, non-Timmy files, oversized files and over-limit entry counts are refused before anything is replaced. A refused import leaves the ledger unchanged.
  • Provenance on the record. A log saved from a confirmed or corrected AI suggestion records source, the suggested visible fields, the answering model, and confirmedAt. The entry sheet states it in the app's own words.
  • Shell cache v5 -> v6 so installed clients receive the changed app.js / src/domain.js.

Evidence

  • tests/portability.test.js — 7 unit cases (export version + provenance, v1 upgrade notice, receipt/tombstone round trip with secret stripping, future-version refusal, malformed/non-Timmy refusal, oversized/over-limit refusal, unreadable-receipt notice).
  • tests/portability.acceptance.mjs — real browser against the real server: v1 backup upgraded with the version on screen, v3 file refused with the ledger byte-identical, provenance recorded by an actual confirm-and-save, export at version 2, and a full round trip.
  • tests/release-demo.test.js — new gate for the four on-camera captions and the portable_backup_restore release key.
  • scripts/record_release_demo.mjs — new scenes record the real export file, import a fictional v1 backup, refuse a v3 backup, and reopen a restored log showing its AI provenance. All claims are asserted in the browser before the caption; the run recorded 69/69 captions with pixel proof.
  • scripts/build_release.py — requires those captions and runs npm run test:portability.

Boundary

No change to the medical/non-diagnostic boundary, no inference exposure, no model weights, no real medical images (synthetic Type 4 fixture and fictional logs only), no production services touched. Not merged — RC approval stays human.

Test results

npm test 215/216 on this branch. The one failure, tests/staging-health.test.js «malformed and escaping base paths are rejected at startup», is a pre-existing timing flake: it fails the same way on unmodified main under the full parallel suite (206/207) and passes standalone on both branches.

Closes #35 ## What changed - **Versioned export.** `exportLedgerBackup` writes schema version 2 with the confirmed logs *and* the training contribution receipts (including withdrawn tombstones). Both are written through an allowlist, so unknown keys and credentials are never copied out of the device. - **Migration on import.** `importLedgerBackup` accepts version 1 and 2. Version 1 entries are upgraded, the notice names the version the file came from, and logs that predate provenance keep `provenance: null` rather than being given a source they never recorded. - **Fail closed.** A file from a newer Timmy is refused with an update instruction; malformed JSON, non-Timmy files, oversized files and over-limit entry counts are refused before anything is replaced. A refused import leaves the ledger unchanged. - **Provenance on the record.** A log saved from a confirmed or corrected AI suggestion records `source`, the suggested visible fields, the answering model, and `confirmedAt`. The entry sheet states it in the app's own words. - **Shell cache v5 -> v6** so installed clients receive the changed `app.js` / `src/domain.js`. ## Evidence - `tests/portability.test.js` — 7 unit cases (export version + provenance, v1 upgrade notice, receipt/tombstone round trip with secret stripping, future-version refusal, malformed/non-Timmy refusal, oversized/over-limit refusal, unreadable-receipt notice). - `tests/portability.acceptance.mjs` — real browser against the real server: v1 backup upgraded with the version on screen, v3 file refused with the ledger byte-identical, provenance recorded by an actual confirm-and-save, export at version 2, and a full round trip. - `tests/release-demo.test.js` — new gate for the four on-camera captions and the `portable_backup_restore` release key. - `scripts/record_release_demo.mjs` — new scenes record the real export file, import a fictional v1 backup, refuse a v3 backup, and reopen a restored log showing its AI provenance. All claims are asserted in the browser before the caption; the run recorded 69/69 captions with pixel proof. - `scripts/build_release.py` — requires those captions and runs `npm run test:portability`. ## Boundary No change to the medical/non-diagnostic boundary, no inference exposure, no model weights, no real medical images (synthetic Type 4 fixture and fictional logs only), no production services touched. Not merged — RC approval stays human. ## Test results `npm test` 215/216 on this branch. The one failure, `tests/staging-health.test.js` «malformed and escaping base paths are rejected at startup», is a pre-existing timing flake: it fails the same way on unmodified `main` under the full parallel suite (206/207) and passes standalone on both branches.
timmy added 1 commit 2026-10-22 11:09:13 +00:00
A backup is the person's own record and has to keep working as the AI fields evolve.

- Export stamps schema version 2 and carries the confirmed logs plus the training
  contribution receipts, including withdrawn tombstones; both are written through an
  allowlist so unknown keys and credentials never leave the device.
- Import accepts version 1 and 2. A version 1 file is upgraded, the notice names the
  version it came from, and entries that predate provenance stay null instead of being
  given a source they never recorded.
- A file from a newer Timmy is refused with an update instruction, malformed, oversized
  and over-limit files fail closed, and a refused import leaves the ledger unchanged.
- A log saved from a confirmed or corrected AI suggestion records its provenance
  (source, suggested visible fields, model, confirmedAt) and the entry sheet states it.
- Service worker shell cache v5 -> v6 so installed clients actually receive the change.

Evidence: tests/portability.test.js (7 unit cases), tests/portability.acceptance.mjs
(real browser: v1 upgrade, future refusal with unchanged ledger, provenance recorded,
export v2, round trip), release demo scenes with on-camera caption proof, and the
release builder gate portable_backup_restore.

Refs #35
mercury approved these changes 2026-10-22 12:02:06 +00:00
mercury left a comment
First-time contributor

Reviewed the full 16-file diff: export/import go through allowlists, provenance is bounded to source/suggested visible fields/model/confirmedAt, v1 files keep provenance: null instead of inventing a source, newer-version and oversized/malformed files fail before replacement, and the demo asserts each measured claim before its caption (version-2 export, v1 upgrade notice, ledger bytes unchanged after refusal, restored provenance line). CI quality gate is green on 2094a07. Approving.

Reviewed the full 16-file diff: export/import go through allowlists, provenance is bounded to source/suggested visible fields/model/confirmedAt, v1 files keep `provenance: null` instead of inventing a source, newer-version and oversized/malformed files fail before replacement, and the demo asserts each measured claim before its caption (version-2 export, v1 upgrade notice, ledger bytes unchanged after refusal, restored provenance line). CI quality gate is green on 2094a07. Approving.
mercury approved these changes 2026-10-22 12:02:08 +00:00
rockachopa merged commit 5a15fa660e into main 2026-10-22 12:02:22 +00:00
Sign in to join this conversation.