Skip to content

Register specimens and containers through one module (slice 1: bulk combined import) - #90

Draft
m-murphy wants to merge 1 commit into
chore/agent-guardrailsfrom
refactor/specimen-registration
Draft

m-murphy wants to merge 1 commit into
chore/agent-guardrailsfrom
refactor/specimen-registration

Conversation

@m-murphy

@m-murphy m-murphy commented Oct 6, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on #89. First slice of ADR 0010: Specimen registration goes behind one module, and validation becomes the write itself, rolled back.

 POST /imports/bulk-combined/validate
-  validateBulkCombinedPayload          # its own checks
-    prepareCombinedSubjectContainerBatch
-    lookupWriteInputCollectionId       # barcode miss → null, no occupancy check
-    collectContainerPlacementErrors    # rejects 2 papers per Sheet
+  withRolledBackTransaction
+    registerSubjects                   # same code as the import

 POST /imports/bulk-combined
-  prepareSubjectWithSpecimens
-  revalidatePreparedSubjectInTx
-  createContainerForSpecimen
-    lookupGridCollectionId             # barcode miss → falls back to name
+  withWriteTransaction
+    registerSubjects                   # find or create Subjects
+      registerSpecimens                # plan all → refuse with every error, or write
lib/specimen-registration/
├── index.ts        # registerSpecimens, checkSpecimenRegistration
├── plan.ts         # Specimen reuse, units, positions, barcodes, sublabels; reads only
├── collections.ts  # every identifier given must name the same Collection
└── write.ts        # inserts; a constraint hit becomes a 409

Rules now enforced in one place:

  • Specimen reuse: a Specimen is reused by (Source, type, date).
  • Collection identity: a row's name, barcode and id must agree on one Collection. A Collection is created only when nothing matches and a location is given.
  • Paper on Sheets: several papers can share a Sheet, but a sublabel can't repeat on one Sheet.
  • Grid position clashes: checked per resolved Collection, so naming one plate by name and by barcode is caught.

createContainerForSpecimen now wraps registerSpecimens, so derivations, control batches, POST /specimens and add-container already write with these rules. Their separate validators are replaced in slices 2–4.

Evidence

All three mismatches were reproduced against the old code before this change.

  • Before: existing plate P1 (barcode P1-BC, tube at A01) and a row { name: 'P1', barcode: 'UNKNOWN', position: 'A01' } → validate { valid: true }, import fails "Position A01 is already used". At A02 the import put the tube in P1.
    After: both report Micronix plate 'P1' has barcode 'P1-BC', not 'UNKNOWN'.
  • Before: two subjects with papers on Sheet-1 in one box → validate rejects "Sheet name 'Sheet-1' in box is used more than once", import writes both papers on one sheet.
    After: both accept; a repeated sublabel on that sheet is rejected by both.
  • New fixture test, 8 payloads: validate(payload).valid === importSucceeds(payload), and each matches its expected validity.
  • 18 module tests. bun run ci:verify passes locally (API: 1069 pass, 0 fail).

Merge Danger

Door: two-way

No schema or wire-shape change. The validate and import response bodies keep their shapes. A full_file import failure now also carries details.errors with every problem.

Blast Radius: every container write

Every path that creates a Container goes through the new module. Inputs that used to pass now fail:

  • Contradictory identifiers: a collection name and barcode that point at different plates, or a barcode that misses while the name matches.
  • Repeated sublabel: the same sublabel twice on one Sheet.
  • Ambiguous new plate: one new plate given two different locations in one request.

Messages for some failures changed wording. Each failure now returns every error in one response.

🤖 Generated with Claude Code

Bulk combined import checked each row in four places (its validator, a
prepare step, an in-transaction revalidate, and the container writer),
and the copies disagreed with the write:

- A row naming an existing plate by name with an unknown collection
  barcode passed validation without an occupancy check, then failed on
  import, or landed in a plate whose barcode the row contradicted.
- Two papers on one Sheet in different subjects failed validation but
  imported.
- One plate named by name in one row and by barcode in another escaped
  the in-file position check.

lib/specimen-registration owns Specimen registration (ADR 0010):
registerSpecimens plans every entry, refuses with every error, or
writes; checkSpecimenRegistration is the same write, rolled back. It
finds or reuses the Specimen by (Source, type, date), resolves each
Collection so every identifier given must name the same one, checks
Grid positions, barcodes, and sublabels against the database and the
rest of the request by resolved Collection, and applies unit defaults.

Bulk combined import now finds or creates Subjects and hands the rest
to the module; its validate route is the full_file import rolled back.
createContainerForSpecimen wraps registerSpecimens, so every container
write follows the same rules while the other paths' validators wait
for their own slices.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant