feat(training): separate opt-in contribution receipts with withdrawable tombstones #83

Merged
timmy merged 1 commits from timmy/24-training-contribution-consent into main 2026-10-21 12:01:54 +00:00
Member

Closes #24

Parent epic: #3. Evidence for the three acceptance criteria is below; nothing here transmits data, changes the medical/non-diagnostic boundary, or adds infrastructure.

What changed

  • src/training-contribution.js — pure contract: consent terms (training-consent-v1), eligibility (confirmed log + saved photo bytes), receipt preview/commit, duplicate guard, derived-artifact listing, withdrawal tombstone.
  • app.js — privacy page gains a Training contribution section: consent version and per-log opt-in review, a consent sheet that shows the version and the receipt before the confirm control can be enabled, a queued-local receipt row with Withdraw, and withdrawn tombstones that name every derived artifact by contribution id. Delete-all now clears receipts with the ledger and photos.
  • docs/PRODUCT-DECISIONS.md — new Training contribution consent decision recording the second-consent rule, the receipt contents, the tombstone, and the honest statement that this build records intent without transmitting.

Acceptance criteria

  • Journal save defaults to no training contribution — receipts live in their own store (timmy:<base>:training-contributions-v1); a real save through the app's logging flow leaves it null (asserted in the browser test and in the demo, which throws otherwise).
  • Consent version and receipt shown before contribution — the sheet names training-consent-v1, the receipt id, included and never-included fields, and #contribute-confirm stays disabled until #training-consent-check is ticked; no receipt exists before that.
  • Withdrawal/deletion can address every derived artifact by contribution ID — withdrawContribution returns a tombstone whose artifacts (raw-image, confirmed-label) each carry contributionId and state: withdrawn; the UI shows that tombstone, and delete-all removes the receipt store too.

TDD evidence (RED before GREEN)

  • node --test tests/training-contribution.test.js first run: ERR_MODULE_NOT_FOUND ... src/training-contribution.js (12 tests, all failing on the missing module). After implementation: 12/12 pass.
  • node tests/training-contribution.acceptance.mjs against the running app first run: 0 !== 1 at "the privacy page must offer the training contribution section" ([data-training-consent] count 0 → 1 after implementation). The journal-save assertion already held on that run.
  • node --test tests/product-decisions.test.js failed on the new decision-record assertions before the doc section was added, then passed.
  • Demo/visual gates were updated after the feature, and the recording was re-run end-to-end (below) rather than assumed.

Verification

  • npm test → 207 tests, 207 pass, 0 fail (includes new tests/training-contribution.test.js and the new release-demo assertions).
  • Browser acceptance, all passing against npm start: test:ui, test:photo, test:mobile-capture, test:sleek, test:manual-fallback, test:provisional, test:followup, test:vision-refusal, test:worker-readiness, test:pattern-summary, test:grounded-chat, test:photo-lifecycle, and new test:training-contribution.
  • npm run check:syntax → pass. npm run check:diff → pass. npm audit --audit-level=high → 0 vulnerabilities.
  • node scripts/record_release_demo.mjs re-recorded the full demo from the working app: 65 captions verified as painted on screen, zero console/page errors. The three new captions are verified in the receipt: Training contribution is off by default: a saved log is never training data, The consent version and the receipt are on screen before the contribute button works, Withdrawing keeps a tombstone naming every derived artifact by contribution ID.

Demonstration path

scripts/record_release_demo.mjs now drives the real privacy section on camera: nothing queued after ordinary saves → review opt-in → consent sheet with version + receipt (confirm disabled until accepted) → one receipt queued on the device → Withdraw → tombstone naming raw image and confirmed label by contribution id → delete-all clearing the receipts. Every claim is asserted in the browser before its caption; the new captions are added to DEMO_REQUIRED_CAPTIONS and the manifest gate training_contribution_consent.

Privacy / safety disposition

  • No real medical images, no model weights, no generated binaries in the diff (acceptance screenshots stay untracked and are excluded from the release package).
  • The receipt deliberately excludes note text, symptom answers, urgency/discomfort, identity, and photo bytes — asserted in the unit test, the browser test, and the demo.
  • Transport is honest: the section says nothing is uploaded in this build; the corpus pipeline (#25) and privacy copy review (#22) remain the gates before any transmission exists.
  • No change to the non-diagnostic boundary, deterministic escalation, or model behaviour.
Closes #24 Parent epic: #3. Evidence for the three acceptance criteria is below; nothing here transmits data, changes the medical/non-diagnostic boundary, or adds infrastructure. ## What changed - `src/training-contribution.js` — pure contract: consent terms (`training-consent-v1`), eligibility (confirmed log + saved photo bytes), receipt preview/commit, duplicate guard, derived-artifact listing, withdrawal tombstone. - `app.js` — privacy page gains a **Training contribution** section: consent version and per-log opt-in review, a consent sheet that shows the version and the receipt *before* the confirm control can be enabled, a `queued-local` receipt row with **Withdraw**, and withdrawn tombstones that name every derived artifact by contribution id. Delete-all now clears receipts with the ledger and photos. - `docs/PRODUCT-DECISIONS.md` — new *Training contribution consent* decision recording the second-consent rule, the receipt contents, the tombstone, and the honest statement that this build records intent without transmitting. ## Acceptance criteria - **Journal save defaults to no training contribution** — receipts live in their own store (`timmy:<base>:training-contributions-v1`); a real save through the app's logging flow leaves it `null` (asserted in the browser test and in the demo, which throws otherwise). - **Consent version and receipt shown before contribution** — the sheet names `training-consent-v1`, the receipt id, included and never-included fields, and `#contribute-confirm` stays disabled until `#training-consent-check` is ticked; no receipt exists before that. - **Withdrawal/deletion can address every derived artifact by contribution ID** — `withdrawContribution` returns a tombstone whose `artifacts` (`raw-image`, `confirmed-label`) each carry `contributionId` and `state: withdrawn`; the UI shows that tombstone, and delete-all removes the receipt store too. ## TDD evidence (RED before GREEN) - `node --test tests/training-contribution.test.js` first run: `ERR_MODULE_NOT_FOUND ... src/training-contribution.js` (12 tests, all failing on the missing module). After implementation: 12/12 pass. - `node tests/training-contribution.acceptance.mjs` against the running app first run: `0 !== 1` at "the privacy page must offer the training contribution section" (`[data-training-consent]` count 0 → 1 after implementation). The journal-save assertion already held on that run. - `node --test tests/product-decisions.test.js` failed on the new decision-record assertions before the doc section was added, then passed. - Demo/visual gates were updated after the feature, and the recording was re-run end-to-end (below) rather than assumed. ## Verification - `npm test` → **207 tests, 207 pass, 0 fail** (includes new `tests/training-contribution.test.js` and the new release-demo assertions). - Browser acceptance, all passing against `npm start`: `test:ui`, `test:photo`, `test:mobile-capture`, `test:sleek`, `test:manual-fallback`, `test:provisional`, `test:followup`, `test:vision-refusal`, `test:worker-readiness`, `test:pattern-summary`, `test:grounded-chat`, `test:photo-lifecycle`, and new `test:training-contribution`. - `npm run check:syntax` → pass. `npm run check:diff` → pass. `npm audit --audit-level=high` → 0 vulnerabilities. - `node scripts/record_release_demo.mjs` re-recorded the full demo from the working app: **65 captions verified as painted on screen, zero console/page errors**. The three new captions are verified in the receipt: *Training contribution is off by default: a saved log is never training data*, *The consent version and the receipt are on screen before the contribute button works*, *Withdrawing keeps a tombstone naming every derived artifact by contribution ID*. ## Demonstration path `scripts/record_release_demo.mjs` now drives the real privacy section on camera: nothing queued after ordinary saves → review opt-in → consent sheet with version + receipt (confirm disabled until accepted) → one receipt queued on the device → **Withdraw** → tombstone naming `raw image` and `confirmed label` by contribution id → delete-all clearing the receipts. Every claim is asserted in the browser before its caption; the new captions are added to `DEMO_REQUIRED_CAPTIONS` and the manifest gate `training_contribution_consent`. ## Privacy / safety disposition - No real medical images, no model weights, no generated binaries in the diff (acceptance screenshots stay untracked and are excluded from the release package). - The receipt deliberately excludes note text, symptom answers, urgency/discomfort, identity, and photo bytes — asserted in the unit test, the browser test, and the demo. - Transport is honest: the section says nothing is uploaded in this build; the corpus pipeline (#25) and privacy copy review (#22) remain the gates before any transmission exists. - No change to the non-diagnostic boundary, deterministic escalation, or model behaviour.
rockachopa added 1 commit 2026-10-21 10:59:07 +00:00
A journal save now records no training contribution. A contribution is a second
consent: the privacy page states consent version training-consent-v1 and the
receipt (contribution id, included fields, never-included fields) before the
confirm control is enabled, the receipt is queued on this device only and copies
no photo bytes, note text, or symptom answers, and withdrawal keeps a tombstone
naming every derived artifact by contribution id. Delete-all clears the receipts
with the ledger.

Closes #24
timmy merged commit 11c63a8d25 into main 2026-10-21 12:01:54 +00:00
Sign in to join this conversation.