The ingest script is now an auditable corpus pipeline backed by scripts/corpus_manifest.py.
Provenance (criterion 1). Each sample records consent (version + recordedAt), source.kind and the raw-input digest, both hashes (rawInput, derived), the derivation block (format, maxDimension, metadataStripped, pipeline: timmy-ingest/2), review status and timestamp, the salted-subjectKey split, and lifecycle: {state: active}. The caller's input path is never recorded — only its digest. consentVersion stays in place for older readers, so the shipped node ingest test needed only the new required --contribution-id.
Revocation (criterion 2).--revoke <contribution-id> --reason <text> deletes the derived image and any raw pre-derivation copy (matched by raw digest, never a user path), then appends a tombstone naming every removed artifact by contribution id + digest. The tombstone keeps identifiers and hashes and deliberately drops labels, subjectKey, and relativePath. The manifest stays append-only: the original sample record survives the withdrawal that documents it, and active_records() filters anything tombstoned so a trainer reading through it cannot train on withdrawn data. Deletion is path-guarded (_safe_dataset_path) against escaping the dataset root.
Illegal transitions (criterion 3). Refused, not silently applied: revoked → active, unknown state, revocation without a reason, revoking an unknown contribution, revoking twice, a duplicate contributionId, and a second label set for the same derived image. The duplicate guard runs before any byte is written, so a refused ingest leaves no orphan image in the active dataset.
TDD evidence (watched RED, then GREEN)
tests/corpus-manifest.test.py, one vertical slice at a time:
provenance → RED (unrecognized arguments: --contribution-id, record had no v2 fields) → GREEN.
tombstone + byte removal + append-only history + active_records → RED (--revoke unsupported; active_records missing) → GREEN.
illegal transitions → RED on the two real gaps (a second revoke appended a second tombstone; a duplicate contribution appended a second sample) → GREEN.
orphan-image gap found while reviewing the diff → RED (two files left in images/) → guard moved before the write → GREEN.
Honest disclosure: transition()'s backwards-move guards passed on first write. I report that as a hold-out check, not a RED cycle.
npm test — 217 tests, 216 pass; the one failure is staging-health.test.js → "malformed and escaping base paths are rejected at startup" ("/../git" was accepted), a timing-sensitive 800 ms spawn budget. Pre-existing: reproduced 1/5 runs on a clean git archive origin/main checkout with none of these changes, and 9 consecutive npm test runs passed with them. Not caused by, and not fixed by, this PR.
python3 tests/staging-deploy.test.py — 20/20; npm run check:syntax and npm run check:diff clean.
git status clean before commit; no dataset bytes, weights, or secrets added; __pycache__ stays ignored.
Demonstration path
No browser surface changed, so scripts/record_release_demo.mjs is untouched and makes no new claim. The nearest honest user-visible effect is the withdrawn-receipt tombstone already on camera for #24 (one receipt queued locally, then a tombstone naming every derived artifact by contribution ID) — this PR is the corpus-side counterpart of that same identifier, and the CLI accepts the UUIDs the app mints (verified with a uuid4() smoke run: ingest → active, revoke → one artifact removed, only manifest.jsonl left). The release delta should keep saying "no new user-visible feature is claimed" for this change.
Not merged: RC approval stays with the release job.
Closes #25
## What changed
The ingest script is now an auditable corpus pipeline backed by `scripts/corpus_manifest.py`.
- **Provenance (criterion 1).** Each sample records consent (`version` + `recordedAt`), `source.kind` and the raw-input digest, both hashes (`rawInput`, `derived`), the derivation block (`format`, `maxDimension`, `metadataStripped`, `pipeline: timmy-ingest/2`), review status and timestamp, the salted-`subjectKey` split, and `lifecycle: {state: active}`. The caller's input path is never recorded — only its digest. `consentVersion` stays in place for older readers, so the shipped node ingest test needed only the new required `--contribution-id`.
- **Revocation (criterion 2).** `--revoke <contribution-id> --reason <text>` deletes the derived image and any raw pre-derivation copy (matched by raw digest, never a user path), then appends a tombstone naming every removed artifact by contribution id + digest. The tombstone keeps identifiers and hashes and deliberately drops `labels`, `subjectKey`, and `relativePath`. The manifest stays append-only: the original sample record survives the withdrawal that documents it, and `active_records()` filters anything tombstoned so a trainer reading through it cannot train on withdrawn data. Deletion is path-guarded (`_safe_dataset_path`) against escaping the dataset root.
- **Illegal transitions (criterion 3).** Refused, not silently applied: `revoked → active`, unknown state, revocation without a reason, revoking an unknown contribution, revoking twice, a duplicate `contributionId`, and a second label set for the same derived image. The duplicate guard runs **before** any byte is written, so a refused ingest leaves no orphan image in the active dataset.
## TDD evidence (watched RED, then GREEN)
`tests/corpus-manifest.test.py`, one vertical slice at a time:
1. provenance → RED (`unrecognized arguments: --contribution-id`, record had no v2 fields) → GREEN.
2. tombstone + byte removal + append-only history + `active_records` → RED (`--revoke` unsupported; `active_records` missing) → GREEN.
3. illegal transitions → RED on the two real gaps (a second revoke appended a second tombstone; a duplicate contribution appended a second sample) → GREEN.
4. orphan-image gap found while reviewing the diff → RED (two files left in `images/`) → guard moved before the write → GREEN.
Honest disclosure: `transition()`'s backwards-move guards passed on first write. I report that as a **hold-out check**, not a RED cycle.
## Local gates
- `python3 tests/corpus-manifest.test.py -v` — 8/8 pass.
- `npm test` — 217 tests, 216 pass; the one failure is `staging-health.test.js` → "malformed and escaping base paths are rejected at startup" (`"/../git" was accepted`), a timing-sensitive 800 ms spawn budget. **Pre-existing**: reproduced 1/5 runs on a clean `git archive origin/main` checkout with none of these changes, and 9 consecutive `npm test` runs passed with them. Not caused by, and not fixed by, this PR.
- `python3 tests/staging-deploy.test.py` — 20/20; `npm run check:syntax` and `npm run check:diff` clean.
- `git status` clean before commit; no dataset bytes, weights, or secrets added; `__pycache__` stays ignored.
## Demonstration path
No browser surface changed, so `scripts/record_release_demo.mjs` is untouched and makes no new claim. The nearest honest user-visible effect is the withdrawn-receipt tombstone already on camera for #24 (one receipt queued locally, then a tombstone naming every derived artifact by contribution ID) — this PR is the corpus-side counterpart of that same identifier, and the CLI accepts the UUIDs the app mints (verified with a `uuid4()` smoke run: ingest → active, revoke → one artifact removed, only `manifest.jsonl` left). The release delta should keep saying "no new user-visible feature is claimed" for this change.
Not merged: RC approval stays with the release job.
Promote the ingest script into an auditable corpus pipeline. Each sample now
records consent (version + timestamp), source kind and raw-input digest, both
hashes, the derivation pipeline, review status, split, and lifecycle state, with
the caller's path never written down.
Withdrawal is a new `--revoke <contribution-id> --reason` operation: it deletes
the derived image and any raw pre-derivation copy, then appends a tombstone that
addresses each removed artifact by contribution id and digest while dropping
labels, subject key, and paths. History is append-only, so the original sample
record survives the withdrawal it documents.
Illegal moves are refused instead of silently applied: revoked -> active,
revoking an unknown or already-revoked contribution, an unknown state, a
duplicate contribution id, and a second label set for the same derived image.
The duplicate guard runs before any byte is written, so a refused ingest cannot
strand an image in the active dataset.
Tests written first and watched fail (tests/corpus-manifest.test.py): provenance,
tombstone + byte removal, active-record filtering, and the transition guards.
The transition helper itself passed on first write and is reported as a
hold-out check rather than a RED cycle. Wired into the quality workflow and
check:syntax; contract documented in docs/CORPUS-MANIFEST.md.
No browser surface changed, so record_release_demo.mjs is untouched and claims
nothing new: the nearest honest visible effect is the withdrawn-receipt
tombstone already recorded for #24.
CI's diff-hygiene gate rejects a trailing blank line at EOF; the local
check:diff run happened before the first commit existed, so it checked the
previous range and missed it.
Release-engineering review of 578cc9f (7 files, +651/-38).
Verified in a clean worktree at the PR head:
python3 tests/corpus-manifest.test.py -v -> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).
npm test -> 217/217 pass on this host.
Diff scope is the feature only: corpus_manifest.py, the ingester, its tests/docs, one workflow line, package.json. No secrets, no model weights, no medical images, no binaries (0 binary patches).
Read scripts/corpus_manifest.py end to end: records are append-only; the tombstone drops labels/subjectKey/relativePath and keeps only contribution id + digests; deletion goes through _safe_dataset_path so it cannot escape the dataset root; the raw pre-derivation copy is matched by raw digest, never by a caller path.
Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Approving for merge.
Release-engineering review of 578cc9f (7 files, +651/-38).
Verified in a clean worktree at the PR head:
- `python3 tests/corpus-manifest.test.py -v` -> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).
- `npm test` -> 217/217 pass on this host.
- Diff scope is the feature only: corpus_manifest.py, the ingester, its tests/docs, one workflow line, package.json. No secrets, no model weights, no medical images, no binaries (0 binary patches).
- Read scripts/corpus_manifest.py end to end: records are append-only; the tombstone drops labels/subjectKey/relativePath and keeps only contribution id + digests; deletion goes through _safe_dataset_path so it cannot escape the dataset root; the raw pre-derivation copy is matched by raw digest, never by a caller path.
Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Approving for merge.
Release-engineering review of 578cc9f (7 files, +651/-38).
Verified in a clean worktree at the PR head:
python3 tests/corpus-manifest.test.py -v -> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).
npm test -> 217/217 pass on this host.
Diff scope is the feature only: corpus_manifest.py, the ingester, its tests/docs, one workflow line, package.json. No secrets, no model weights, no medical images, no binaries (0 binary patches).
Read scripts/corpus_manifest.py end to end: records are append-only; the tombstone drops labels/subjectKey/relativePath and keeps only contribution id + digests; deletion goes through _safe_dataset_path so it cannot escape the dataset root; the raw pre-derivation copy is matched by raw digest, never by a caller path.
Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Verdict: mergeable. Formal GitHub-style self-approval is refused by this Gitea build (approve-your-own-pull is not allowed), so this review is recorded here and the merge is the release engineer action.
Release-engineering review of 578cc9f (7 files, +651/-38).
Verified in a clean worktree at the PR head:
- `python3 tests/corpus-manifest.test.py -v` -> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).
- `npm test` -> 217/217 pass on this host.
- Diff scope is the feature only: corpus_manifest.py, the ingester, its tests/docs, one workflow line, package.json. No secrets, no model weights, no medical images, no binaries (0 binary patches).
- Read scripts/corpus_manifest.py end to end: records are append-only; the tombstone drops labels/subjectKey/relativePath and keeps only contribution id + digests; deletion goes through _safe_dataset_path so it cannot escape the dataset root; the raw pre-derivation copy is matched by raw digest, never by a caller path.
Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Verdict: mergeable. Formal GitHub-style self-approval is refused by this Gitea build (approve-your-own-pull is not allowed), so this review is recorded here and the merge is the release engineer action.
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 #25
What changed
The ingest script is now an auditable corpus pipeline backed by
scripts/corpus_manifest.py.version+recordedAt),source.kindand the raw-input digest, both hashes (rawInput,derived), the derivation block (format,maxDimension,metadataStripped,pipeline: timmy-ingest/2), review status and timestamp, the salted-subjectKeysplit, andlifecycle: {state: active}. The caller's input path is never recorded — only its digest.consentVersionstays in place for older readers, so the shipped node ingest test needed only the new required--contribution-id.--revoke <contribution-id> --reason <text>deletes the derived image and any raw pre-derivation copy (matched by raw digest, never a user path), then appends a tombstone naming every removed artifact by contribution id + digest. The tombstone keeps identifiers and hashes and deliberately dropslabels,subjectKey, andrelativePath. The manifest stays append-only: the original sample record survives the withdrawal that documents it, andactive_records()filters anything tombstoned so a trainer reading through it cannot train on withdrawn data. Deletion is path-guarded (_safe_dataset_path) against escaping the dataset root.revoked → active, unknown state, revocation without a reason, revoking an unknown contribution, revoking twice, a duplicatecontributionId, and a second label set for the same derived image. The duplicate guard runs before any byte is written, so a refused ingest leaves no orphan image in the active dataset.TDD evidence (watched RED, then GREEN)
tests/corpus-manifest.test.py, one vertical slice at a time:unrecognized arguments: --contribution-id, record had no v2 fields) → GREEN.active_records→ RED (--revokeunsupported;active_recordsmissing) → GREEN.images/) → guard moved before the write → GREEN.Honest disclosure:
transition()'s backwards-move guards passed on first write. I report that as a hold-out check, not a RED cycle.Local gates
python3 tests/corpus-manifest.test.py -v— 8/8 pass.npm test— 217 tests, 216 pass; the one failure isstaging-health.test.js→ "malformed and escaping base paths are rejected at startup" ("/../git" was accepted), a timing-sensitive 800 ms spawn budget. Pre-existing: reproduced 1/5 runs on a cleangit archive origin/maincheckout with none of these changes, and 9 consecutivenpm testruns passed with them. Not caused by, and not fixed by, this PR.python3 tests/staging-deploy.test.py— 20/20;npm run check:syntaxandnpm run check:diffclean.git statusclean before commit; no dataset bytes, weights, or secrets added;__pycache__stays ignored.Demonstration path
No browser surface changed, so
scripts/record_release_demo.mjsis untouched and makes no new claim. The nearest honest user-visible effect is the withdrawn-receipt tombstone already on camera for #24 (one receipt queued locally, then a tombstone naming every derived artifact by contribution ID) — this PR is the corpus-side counterpart of that same identifier, and the CLI accepts the UUIDs the app mints (verified with auuid4()smoke run: ingest → active, revoke → one artifact removed, onlymanifest.jsonlleft). The release delta should keep saying "no new user-visible feature is claimed" for this change.Not merged: RC approval stays with the release job.
Release-engineering review of
578cc9f(7 files, +651/-38).Verified in a clean worktree at the PR head:
python3 tests/corpus-manifest.test.py -v-> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).npm test-> 217/217 pass on this host.Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Approving for merge.
Release-engineering review of
578cc9f(7 files, +651/-38).Verified in a clean worktree at the PR head:
python3 tests/corpus-manifest.test.py -v-> 8/8 OK (provenance, tombstone + byte removal, append-only history, active_records filtering, and every refused transition including the duplicate-ingest orphan-image guard).npm test-> 217/217 pass on this host.Non-blocking note (not a blocker, recorded for the next pass): revoke() unlinks bytes before appending the tombstone, so a crash in that window would delete data without leaving a record. Deleting first is the right privacy priority; a recovery note would make the window auditable.
Verdict: mergeable. Formal GitHub-style self-approval is refused by this Gitea build (approve-your-own-pull is not allowed), so this review is recorded here and the merge is the release engineer action.