Parent epic: #3. Evidence for the three acceptance criteria is below; nothing here transmits data, changes the medical/non-diagnostic boundary, or adds infrastructure.
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.
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 main2026-10-21 12:01:54 +00:00
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 #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, aqueued-localreceipt 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
timmy:<base>:training-contributions-v1); a real save through the app's logging flow leaves itnull(asserted in the browser test and in the demo, which throws otherwise).training-consent-v1, the receipt id, included and never-included fields, and#contribute-confirmstays disabled until#training-consent-checkis ticked; no receipt exists before that.withdrawContributionreturns a tombstone whoseartifacts(raw-image,confirmed-label) each carrycontributionIdandstate: 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.jsfirst 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.mjsagainst the running app first run:0 !== 1at "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.jsfailed on the new decision-record assertions before the doc section was added, then passed.Verification
npm test→ 207 tests, 207 pass, 0 fail (includes newtests/training-contribution.test.jsand the new release-demo assertions).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 newtest:training-contribution.npm run check:syntax→ pass.npm run check:diff→ pass.npm audit --audit-level=high→ 0 vulnerabilities.node scripts/record_release_demo.mjsre-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.mjsnow 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 namingraw imageandconfirmed labelby contribution id → delete-all clearing the receipts. Every claim is asserted in the browser before its caption; the new captions are added toDEMO_REQUIRED_CAPTIONSand the manifest gatetraining_contribution_consent.Privacy / safety disposition