Version ledger migrations and hardened JSON portability #61
No reviewers
Labels
No Label
area/backend
area/data
area/frontend
area/model
area/privacy
area/release
area/safety
blocked
human-gate
priority/P0
priority/P1
priority/P2
state:review
type/docs
type/epic
type/feature
type/ops
type/research
type/security
type/test
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stackchain/timmy-talking-turd#61
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "timmy/35-version-ledger-migrations"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #35 (parent epic: #6)
Outcome
User-owned ledger records stay portable as schema versions and AI provenance fields evolve.
Changes
src/domain.js: version-ledger constants (SCHEMA_VERSION,KNOWN_SCHEMA_VERSIONS,MAX_IMPORT_BYTES= 2 MiB).importLedgermigrates prior schema versions (v0 bare-array legacy exports and the v1 envelope), fails safely on future versions (RangeError with update guidance), malformed JSON, wrong-product envelopes, and oversized files before parsing user data.exportLedgernormalizes entries throughsanitizeEntry, so confirmed values and bounded provenance round-trip while unknown fields and smuggled secrets never enter the export file.sanitizeEntrykeeps only a whitelistedprovenance.origin(user|ai-suggestion).src/analysis.js:mergeVisualSuggestionrecords{ origin: 'ai-suggestion' }only when a suggestion is actually applied; abstentions record nothing and nonvisual fields stay user-owned.app.js: import now merges into the existing ledger instead of replacing it - a failed or partial import can never silently drop user-owned records.service-worker.js: shell cache bumped v5 -> v6 per base-path namespace so installed PWAs receive the migration code; activation purges obsolete v5 caches.tests/ledger-portability.acceptance.mjsbrowser gate: export round trip, merge import, future-version/malformed failure surfacing, root vs/timmy-stagingstorage isolation, and Delete Everything in both namespaces. Wired asnpm run test:portabilityand added to CI quality gates.Acceptance criteria evidence
Privacy / safety boundaries
Verification receipts (local run)
Independent verification: CHANGES REQUESTED at
b8532f587d8e2d8ee04b24173d7cde4305a4eaa4. Gitea CI is green, but three adversarial portability/privacy blockers reproduce:Provenance allowlist bypass (
src/domain.js:61-64).origin in KNOWN_PROVENANCE_ORIGINSincludes inherited Object prototype keys. OriginstoString,constructor, and__proto__are all retained as provenance, contradicting the claimed strict vocabulary. Use an own-property-safe set/membership check and regressions for prototype names.Timmy can reject its own valid exports.
exportLedger()retainsphotoDataUrl, while import caps the entire document at 2 MiB. A synthetic valid JPEG data URL just over that limit exported successfully, thenimportLedger(exportLedger(...))threwRangeError. The app accepts photos up to 4 MiB, so real user-owned exports can be non-importable. Establish a symmetric policy: either a cap compatible with the actual maximum export, or an explicit portable/photo-exclusion contract that preserves user expectations. Test at real image boundaries.The byte limit is not a byte limit (
src/domain.js:103).text.lengthcounts UTF-16 code units, not UTF-8 bytes. A JSON payload of 1,200,087 JS characters measured 2,400,087 UTF-8 bytes, exceededMAX_IMPORT_BYTES=2,097,152, and was accepted. CheckFile.sizebeforeFile.text()in the browser and use a deterministic UTF-8 byte calculation in the domain boundary; test multibyte input.Also harden repeated/colliding-ID merge semantics so re-import does not duplicate or ambiguously shadow user records. Do not merge until corrected and independently re-reviewed.
Supplemental independent review confirmed all four prior defects and found another data-loss blocker:
/timmy-staging, deletion unconditionally removes globaltimmy-ledger-v1. With a staging ledger and separate root legacy data present, staging deletion removed both. Root scope may clean the legacy key to prevent resurrection, but a prefixed deployment must not delete another namespace’s data. Add a browser regression with nonempty root legacy data.Also tighten malformed current-schema values: integer numeric fields, approved raster data URLs only, and safe invalid-date handling. The correction lane has been restarted at the provider’s sustainable concurrency with this complete finding set.
Independent exact-head hostile review of
dd86d6675d5ff6f8e3e2675f36347469f7c7b927: CHANGES REQUESTED. Existing 91/91 + browser/deploy gates pass, and prior provenance/UTF-8/namespace/integer blockers are improved, but storage integrity still fails.mergeLedgersdedupes local rows by dropping later records. Preserve every distinct local record, deterministically repair IDs, and never silently delete user-owned data.entries=result.mergedoccurs before durablesaveEntries; quota failures leave in-memory entries accumulated while storage stays null. Three individually accepted imports produced a 16,778,688-byte export over the 16,777,216-byte import cap. Make save transactional/rollback-safe and enforce the portability ceiling before committing memory or storage.[{},7,{id:"",occurredAt:"bad"}]became three plausible Type-4 logs with generated IDs/current times. Require a defensible legacy shape/product contract; reject ambiguous unrelated JSON instead of inventing medical records.occurredAt:"not-a-date"blanked the app withRangeError: Invalid time value. Validate/migrate persisted data before render.abc,A,AAAAA, and SVG bytes mislabeled JPEG were accepted. Decode canonical base64, bound decoded bytes, and verify JPEG/PNG/WebP magic.sanitizeEntrystill throws on Symbol/BigInt dates despite the never-throw contract.Exact hostile scripts and full receipts are preserved in review evidence. CI is red at this head. No merge/deploy until RED→GREEN correction and independent verification.
Exact-head hostile review of
6f73b8551c402e415916320d25c6a8cdf02ac618: REQUEST_CHANGES / do not merge. The corrected 49 domain tests and real ledger-portability browser journey pass, and prior local duplicate/quota/size/boot blockers improved. Three independent data-integrity failures remain:safeIsoTimestamp("not-a-real-date")20 ms apart produced two different 2026-08-22 timestamps. Migration must not fabricate when a medical event occurred; preserve a stable explicit unknown/quarantined state or another non-invented deterministic contract that the UI can render.importLedgerretained both notes, butmergeLedgers([], incoming)returned one row andskippedIds:["same"]. Collision-safe ID repair must preserve every distinct imported row too, not only pre-existing local duplicates.sanitizeEntryfor all three MIME types. Signature sniffing is not raster validity; require structurally decodable canonical images and reject truncation/junk.These are user-owned medical-record integrity blockers despite green submitted tests. No merge or deployment.
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Gitea.