Version ledger migrations and hardened JSON portability #61
Open
rockachopa
wants to merge 3 commits from
timmy/35-version-ledger-migrations into main
pull from: timmy/35-version-ledger-migrations
merge into: stackchain:main
stackchain:main
stackchain:timmy/19-metrics-operator-report
stackchain:timmy/16-image-ingress-hardening
stackchain:timmy/40-security-rc-evidence
stackchain:timmy/27-dataset-card
stackchain:timmy/26-review-adjudication
stackchain:timmy/32-abstention-policy
stackchain:timmy/29-evaluation-harness
stackchain:timmy/35-portable-backup-versions
stackchain:timmy/24-training-contribution-consent
stackchain:fix/demo-caption-pixels
stackchain:timmy/36-photo-lifecycle-controls
stackchain:timmy/release-notes-grounded-chat
stackchain:timmy/37-grounded-chat-citations
stackchain:timmy/21-escalation-regression
stackchain:timmy/34-explainable-pattern-summaries
stackchain:timmy/15-container-worker-receipts
stackchain:timmy/73-blank-first-ordering
stackchain:timmy/18-vision-corpus
stackchain:timmy/20-photo-dataflow-threat-model
stackchain:timmy/67-hard-stool-followup
stackchain:timmy/13-provisional-suggestion-state
stackchain:timmy/12-manual-fallback-draft-recovery
stackchain:timmy/11-mobile-capture-recovery
stackchain:free/hy3-timmy-16
stackchain:timmy/41-release-observability-dashboard
stackchain:free/step-timmy-14
stackchain:timmy/21-escalation-regression-suite
stackchain:timmy/17-bounded-inference-queue
stackchain:timmy/58-strict-csp-runtime-config
stackchain:timmy/55-staging-deployment-tooling
stackchain:timmy/53-private-subpage-staging
stackchain:timmy/10-reproducible-smolvlm-bootstrap
stackchain:timmy/48-vomiting-phrase-hotfix
stackchain:timmy/48-server-safety-override
stackchain:timmy/47-sleek-hermes-agent
stackchain:timmy/9-product-boundary-decisions
stackchain:timmy/8-ci-gates
Labels
Clear labels
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
Application API and storage
Dataset and provenance
PWA and interaction design
Vision model and inference
Privacy and consent
Release engineering and distribution
Clinical safety boundary
Blocked on an explicit dependency or decision
Requires explicit human approval
Release-critical
High priority
Planned
Documentation and evidence
Roadmap epic
Product feature
Infrastructure and operations
Research or evaluation
Security hardening
Verification and quality gate
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stackchain/timmy-talking-turd#61
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.