Skip to content

Build tickets: Curation

Stage 3's second chart, for the curator-side work build-tickets.md deliberately left out of the application chart. Its "Deliberately not ticketed" section names four pieces — page-image verification (D-56), the cross-section Restriction detector (D-49), span selection (D-50) and QR label generation (D-63) — and says "It needs its own chart." This is that chart.

Tickets are one file each under issues/, per docs/agents/issue-tracker.md. Numbering continues from the application chart — ids are never reused, and the two charts share one issues/ directory and one number line.

Work the frontier: any ticket whose blockers are all resolved. Take one by setting Status: in progress — <why> before doing any work. Run ./init.sh before starting and after finishing.

Why this is a separate chart

Mixing it into build-tickets.md would make that chart's frontier misleading: an agent taking the next ticket would move between building the product and building an internal tool. They are different products with different users. A Handler uses one; a Safety Officer uses the other.

What already exists — do not re-ticket

The entire curation data model is built and migrated: Sds, Provenance, SdsSection, SourceSpan, ChemicalRecord, CuratedFieldEntry, PpeItem, FirstAidMeasure, SpillResponseBranch, SpillResponseStep, Restriction, Reconciliation, CuratedTranslation, CuratedExplanation, CorpusSnapshot — all in app/chemical-safety-assistant-api/prisma/models/corpus.prisma. The file-upload module and its three storage adapters are built. The read side is built: /api/v1/corpus/snapshot and /version.

What does not exist is any write path at all. src/modules/corpus/ holds queries and no commands. Every row that model can hold, today, can only arrive by hand. That is the gap this chart closes, and it is why ticket 25 comes before the four named pieces: they have nowhere to write.

The acceptance test already exists

scripts/corpus_check.py, the CORPUS gate, asserts what a Corpus must be true of — whole-document Reconciliation coverage, every Reconciliation naming a curator, every displayed span naming its SDS Section, complete Provenance, no substituted revision date, no two records sharing an identity, branch step order preserved. A Corpus this tooling produces must pass that gate. The gate is the chart's definition of done, and it reports "nothing curated yet" until ticket 33 publishes one.

Dependency order

#TicketBlocked by
25The curation write surface, and Provenanceresolved
26Text verified against the page imageresolved25
32Curated Explanation authoringresolved25
27Span selectionresolved26
31Curated Translationresolved26
28The cross-section Restriction detectorresolved27
29Reconciliation, and the gate that stops an unresolved spanresolved27
30QR label generationresolved27
33Snapshot publish, and the version the device pullsresolved29
34Mount Curation, and only on a machine that is not serving anyoneresolved
35Serve the stored copy back, and give upload a policy firstresolved34
36The admission surface, and the one-way door behind itresolved35
37A document's verified page text, served to the curatorresolved
38Reconciliation gaps, served to the curator — and a gate as strict as the write pathresolved
39The selection workbench: passage, Restrictions, span, translation — in progress (built, awaiting review)37
40The reconciliation page, for gaps a later scan opened — in progress (built, awaiting review)38
41The Curated Explanation pageresolved
42The publish page, and the gate's answer in wordsresolved
43The unreadable answer, in the five curation resources that still hide it — open

Frontier at chart creation: 25. Resolved 2026-09-11 (API edc7140); the frontier is now 26 and 32. Both resolved 2026-09-11 — 32 at API 95be08d, 26 across frontend eea4aa6 and API 4c5eb02. The frontier is now 27 and 31. both resolved 2026-09-11 — 27 at API dd4fde7 and 5da0b91, 31 at 76ecdc1. All nine are resolved, all on 2026-09-11. A tenth was added the same day once blocker 9 was ruled: 34 — Mount Curation, and only on a machine that is not serving anyone. Everything the nine built was unreachable until that ruling existed. 35 follows from what 34 found: mounting file/ and upload/ is three lines, and everything around them is missing — no upload policy, a Content-Type derived from the client's filename, and an unpoliced delete riding in on the module that carries the read Curation needs.

37–42 added 2026-09-11 — every write path gets a surface, and no curator types an id. Owner ruling, same day: four pages and two loopback-only reads rather than one page per command. The six remaining commands chain by ids nothing serves back — select-span returns a span id, the detector returns Restriction ids, reconcile and translate need both — so one page per command would have a curator carrying ids between screens by hand, the hole ticket 35 closed for files. Frontier: 37, 38, 41, 42. Resolved the same day: 37 at API 1c3824b, 38 at API 59d3191 with the gate made pairwise in scripts/corpus_check.py, 41 at frontend 3a73896, and 42 at API 3bf9ac0 plus frontend 85379a9. The frontier is now 39 and 40. Both were built the same day (frontend 3c68076 and 9536361, gates green) and paused at the owner's request before review. What each still needs is in its issue's Progress section.

43 added 2026-09-20, out of the ticket-40 work rather than out of the grill. Five curation resources still report a success body they cannot parse as "the service may not be running"; the same defect was closed in two others the same day. It is open and unblocked, and it is deliberately not closable by sharing a parser — the five hand-rolled parses are a recorded decision, so unifying them would be a ruling rather than a refactor. It was ticketed rather than built because the Thai copy on five surfaces needs a reviewer, and this repository has no message catalogue.

(Historical, closed since by ADR-0053 and ADR-0056:) What it leaves open, and it is one thing rather than a list: a snapshot links to Sds rows only, so Curated Explanations are read live and unscoped. Ticket 33's checkbox 3 — a curator mid-selection must not have partial work reach a device — therefore holds for documents and not for explanations. A second link or a createdAt <= publishedAt filter would settle in code a question nobody has ruled, so neither was written.

One open question, and it is the owner's

Who may write, and from where. ADR-0007 rules that no safety capability sits behind an account, and ADR-0029 cuts accounts, sync and favourites from the prototype. Both are rulings about the reader app. Neither says anything about the curator surface, and curation writes the content every safety answer is drawn from.

Until a ruling exists, ticket 25 carries the constraint and does not decide it: the curation write routes live under their own prefix and are not to be deployed on a publicly reachable deployment. Assuming the reader app's anonymity ruling extends to the write side would mean anyone who can reach the API can alter first-aid text. That is not a default worth taking silently.

Sizing

ADR-0028 measures curation at roughly a day per document, and calls that figure a floor. This chart builds the tooling; it does not change that number, and no ticket here may claim it does. Tooling that removes a curator's judgement is out of scope by construction — D-49 and D-44 exist to keep judgement human.

Still not ticketed

  • Curating documents. Handoff blocker 3, and the Study Area is unchosen. The spec is explicit that this "cannot be sized as engineering work at all."
  • Camera text recognition. Risks item 1 needs a spike against photographs of real drum labels, and those photographs do not exist.