Files
Timmy c5fa2123e7
Quality gates / quality (pull_request) Canceled after 0s
feat(review): blind reviewer and adjudication tool with an append-only label ledger (#26)
The corpus manifest records what was ingested and whether the person still allows it; it does not
record whether the labels are right. This adds the labeling control the data-flow threat model
named: scripts/review_corpus.py, a command-line tool with no app screen.

- The review queue shows the contributor's proposal and carries no model field at all: blindness is
  structural, because build_queue() never receives predictions.
- reveal() refuses a contribution with no committed review, so the model answer cannot steer the
  human who labels it. Revealing appends nothing.
- A review is one append-only line carrying the proposal it judged; a correction must change it. An
  adjudication is a second reviewer's line that names the record it resolves; the review it
  overrules survives untouched.
- A withdrawn contribution cannot be queued, reviewed, revealed, adjudicated, or trained on, and the
  refusal says it was withdrawn rather than that it is already reviewed.
- trainable_records() returns only active samples with a human decision that was not an exclusion,
  so unreviewed proposals are never training data.

Tests are written first against every refusal (tests/corpus-review.test.py, 21 cases); the release
builder runs the suite and gates four on-camera captions, and the demo now runs the real queue ->
review -> reveal -> adjudicate -> withdraw sequence against the committed synthetic sample and draws
the evidence card from that output. docs/REVIEWER-TOOL.md is the contract; no model was measured.
2026-10-27 11:43:47 +00:00

94 lines
5.2 KiB
Markdown

# Blind review and adjudication tool
Contract for `scripts/review_corpus.py`, the labeling control named in the
[photo data-flow threat model](PHOTO-DATAFLOW-THREAT-MODEL.md) and the gate in front of the
[training corpus](CORPUS-MANIFEST.md). Issue [#26](https://forge.alexanderwhitestone.com/git/stackchain/timmy-talking-turd/issues/26).
The tool decides which samples a trainer is allowed to read. It is a command-line tool, not a
screen in the app: there is no reviewer UI, and nothing here implies one.
## The four invariants
1. **The model answer is withheld until a human commits.** `build_queue()` takes no predictions at
all, so the queue cannot leak an answer it never receives. `reveal()` refuses to return anything
for a contribution with no committed review.
2. **History is append-only.** A review is a line in `reviews.jsonl`. An adjudication is another
line that names the record it resolves (`resolvesRecord`, `resolves`). The review being
overruled is never edited, and no record is deleted.
3. **A withdrawal beats every other answer.** A revoked contribution cannot be queued, reviewed,
revealed, adjudicated, or trained on. The refusal says it was withdrawn — not that it is
already reviewed — because the person's withdrawal is the fact that matters.
4. **Unreviewed and excluded samples are not training data.** `trainable_records()` returns only
samples that are still active, carry a human decision, and were not excluded.
## Review ledger records
One JSON object per line in `<dataset>/reviews.jsonl`:
```json
{"schemaVersion":1,"recordType":"review","contributionId":"...","imageId":"<sha256>","proposed":{"bristolType":4,"color":"brown","quality":"good"},"reviewer":"reviewer-a","decision":"corrected","labels":{"bristolType":5,"color":"green","quality":"fair"},"committedAt":"2026-10-27T09:00:00+00:00"}
{"schemaVersion":1,"recordType":"adjudication","contributionId":"...","imageId":"<sha256>","resolvesRecord":1,"resolves":{"reviewer":"reviewer-a","decision":"corrected"},"reviewer":"reviewer-b","decision":"upheld","labels":{"bristolType":5,"color":"green","quality":"fair"},"adjudicatedAt":"2026-10-27T10:00:00+00:00"}
```
- `proposed` copies the labels the reviewer judged, so a later manifest change cannot rewrite what
was reviewed.
- `decision` is `agreed`, `corrected`, or `excluded` for a review, and `upheld`, `corrected`, or
`excluded` for an adjudication. An exclusion carries `exclusionReason` and no labels.
- The ledger never carries `subjectKey`, note text, symptom answers, or image bytes.
## A model answer file
`reveal` reads predictions only to show what the reviewer could not see. A file may be one JSON
document or one object per line:
```json
{"model":"<name>","answers":{"<contributionId>":{"bristolType":6,"color":"brown","quality":"good","confidence":0.71}}}
```
Keys may be contribution ids or derived image digests. `reveal` is a read: it appends nothing.
When no answer file is supplied, `model` is `null` — the tool does not invent an answer.
## Usage
```bash
python3 scripts/review_corpus.py queue --dataset /srv/timmy-corpus
python3 scripts/review_corpus.py review --dataset /srv/timmy-corpus \
--contribution "$CONTRIBUTION_ID" --reviewer reviewer-a --bristol 5 --color green --quality fair
python3 scripts/review_corpus.py reveal --dataset /srv/timmy-corpus \
--contribution "$CONTRIBUTION_ID" --predictions answers.json
python3 scripts/review_corpus.py adjudicate --dataset /srv/timmy-corpus \
--contribution "$CONTRIBUTION_ID" --reviewer reviewer-b --upheld
python3 scripts/review_corpus.py trainable --dataset /srv/timmy-corpus
```
Every mutating command prints the record it appended. A refusal exits `2` and prints one line of
plain copy on stderr.
## Refusals (all covered by `tests/corpus-review.test.py`)
| Attempt | Result |
|---|---|
| Review or reveal a withdrawn contribution | refused: `was withdrawn and cannot be reviewed, revealed, or trained on` |
| Reveal before any review is committed | refused: `stays hidden until a review is committed` |
| Review the same contribution twice | refused; adjudication is the only way to change a decision |
| Correct a label to the value it already has | refused: a correction must change it |
| Review with a blank reviewer id | refused |
| Exclude without a reason | refused |
| Review a contribution that has no active sample | refused |
| Queue an active sample whose image is missing | refused: a reviewer cannot judge a file that is not there |
| Adjudicate your own review | refused: adjudication needs a second reviewer |
| Adjudicate twice | refused |
| Adjudicate a contribution with no review | refused |
## Known limits
- One review per contribution, resolved by at most one adjudication. Re-review after an
adjudication would need a new record type; it is not implemented.
- The tool does not measure or validate a model. It records what a human decided and what a model
answered, so the disagreement can be counted later.
- It does not train, split, or export a dataset; `trainable_records()` is the list a trainer may
read, not a trainer.
`npm run test:review` runs the contract suite. Every fixture is the committed synthetic image; no
real medical image is read, written, or committed.