- collision-safe ID repair: duplicate ids inside stored data are repaired
deterministically (first keeps id, twins get id#2, id#3, ... scanning past
owned suffixes); every distinct local record survives, never dropped or
silently merged; hostile id types (Symbol/BigInt/objects) repair onto
fresh deterministic ids instead of throwing
- transactional import: parse+merge into a candidate ledger, persist first,
then commit memory; quota/error rolls back in-memory state and localStorage
together with explicit user feedback; total 16MiB portability budget
enforced before mutation on export, import (post-migration expansion), and
storage writes
- strict Timmy legacy contract for bare top-level arrays: nonempty array of
plain rows each carrying a nonempty string id and integer Bristol 1-7;
arbitrary unrelated arrays are rejected wholesale - no invented medical
defaults from foreign JSON
- canonical raster photo validation: strict JPEG/PNG/WebP grammar, canonical
base64 (linear scan, no regex on multi-MB strings), atob round-trip decode,
declared-format magic bytes, 32B-4MiB decoded bounds; mislabeled SVG/HTML
and noncanonical tiny junk are stripped while genuine photos survive
byte-for-byte
- migrateStoredLedger: localStorage is validated and migrated before render;
invalid dates become safe ISO timestamps, duplicate ids repaired, junk rows
dropped (never fabricated into default records); healthy storage is
byte-stable and never rewritten
- sanitizeEntry absorbs Symbol/BigInt/hostile dates/throwing toString,
valueOf, getTime, toJSON without throwing; results stay serializable
- browser regression suite: quota rollback, pre-render migration, array
rejection, photo contract, duplicate-ID preservation in the real app flow
- staging-health startup-rejection budget anchored to measured server
cold-start instead of a fixed 800ms (fixes load-sensitive flake)
- provenance origin set is own-safe exact membership (Set.has); inherited
toString/constructor/__proto__ names can never become origins
- import/export symmetry restored with an explicit bounded policy:
MAX_IMPORT_BYTES raised 2 MiB -> 16 MiB UTF-8 bytes, above any export
this app can produce (photos capped at 4 MiB binary), so valid exports
always re-import without silent data loss while hostile files stay bounded
- byte limit is byte-exact now: utf8ByteLength() measures real UTF-8 bytes
(multibyte boundaries tested), and the browser rejects oversized files
by File.size BEFORE File.text() reads user data
- collision-safe deterministic mergeLedgers(): existing user-owned rows
win, incoming rows only ever added for new ids, intra-file duplicates
collapse deterministically, every collision reported explicitly in the
import toast (no duplicate/overwrite/shadow of user records)
- base-path Delete Everything is namespace-scoped: root still cleans/
migrates the legacy store to prevent resurrection, /timmy-staging no
longer erases another namespace's global legacy ledger (browser
regression covers deletion with root legacy data present)
- strict current-schema values: Bristol 1-7 / urgency 0-4 / discomfort
0-4 must be true integers (out-of-range falls back instead of silent
clamping), photos restricted to JPEG/PNG/WebP base64 raster data URLs
(SVG/GIF/non-base64 dropped), invalid dates never throw or persist
Invalid Date values
Verification: npm test 91/91, test:ui/test:photo/test:sleek/test:portability
PASS, staging-deploy 20/20 OK, check:syntax clean, npm audit 0 high,
check_diff clean, adversarial probe battery (exact-byte boundary at cap,
prototype pollution via JSON, lone surrogates, data-URL strictness) green.
Implements #35.
- importLedger migrates prior schema versions (v0 bare-array legacy
exports and the v1 envelope) and fails safely on future versions,
malformed JSON, wrong-product envelopes, and oversized files with a
new 2 MiB MAX_IMPORT_BYTES guard applied before parsing.
- exportLedger normalizes entries through sanitizeEntry so confirmed
values and bounded provenance round-trip while smuggled secrets and
unknown fields never enter the portable file.
- Entries may carry a whitelisted provenance origin ('user' or
'ai-suggestion'); mergeVisualSuggestion records 'ai-suggestion' only
when a suggestion is actually applied, keeping nonvisual fields
user-owned.
- App import now merges into the existing ledger instead of replacing
it, so a failed or partial import can never silently drop
user-owned records.
- Service-worker shell cache bumped to v6 (per base-path namespace)
so installed PWAs receive the migration code; old v5 caches are
purged on activation.
- New tests/ledger-portability.acceptance.mjs browser gate covers
export round trip, merge import, safe-failure surfacing, root vs
/timmy-staging storage isolation, and Delete Everything for both
namespaces; wired into package.json test:portability and CI quality.yml.
Deterministic medical safety unchanged: urgent-flag detection, red-flag
copy, and chat escalation paths are untouched; all fixtures synthetic.