Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
2e6f3e2
refactor(activity): resolve sources with resolveSpecimenSources (ADR …
m-murphy Oct 6, 2026
34ba32f
test(db): close the snapshot test's database and note why it skips th…
m-murphy Oct 6, 2026
dd642a4
docs: cite ADRs as "ADR 0006" in the files this PR touches.
m-murphy Oct 6, 2026
bfa8d40
docs: write the coding standards as second-person imperatives.
m-murphy Oct 6, 2026
bf9746b
docs: allow read-only tools that must not migrate to skip openOperati…
m-murphy Oct 6, 2026
21cdf40
refactor(api): find derivation paper parents through the placement mo…
m-murphy Oct 6, 2026
d2797d8
docs: say when the hand-rolled session helpers are acceptable.
m-murphy Oct 6, 2026
08e081c
docs: put the reason for withWriteTransaction back next to the rule.
m-murphy Oct 6, 2026
85cb068
docs: restore the test rules the README rewrite dropped.
m-murphy Oct 6, 2026
acb19ec
docs: keep the whole-*Api override rule in CODING_STANDARDS.md only.
m-murphy Oct 6, 2026
8e96e89
docs: describe the pre-commit hook and ci:verify in README.md only.
m-murphy Oct 6, 2026
be5a547
docs: name the five triage labels in AGENTS.md.
m-murphy Oct 6, 2026
35f4f1d
docs: rename the AGENTS.md "Checks" heading so it isn't confused with…
m-murphy Oct 6, 2026
c0f3e55
docs: say what CODING_STANDARDS.md holds in the AGENTS.md pointer.
m-murphy Oct 6, 2026
063c188
docs: give AGENTS.md an H1 heading, as CODING_STANDARDS.md has.
m-murphy Oct 6, 2026
1e7285b
test(db): compare the snapshot's indexes, foreign keys, NOT NULL and …
m-murphy Oct 6, 2026
af31451
docs: make the root README the one place for coverage targets.
m-murphy Oct 6, 2026
d40df05
docs: restore the CURRENT_SCHEMA_VERSION bullet in the db README.
m-murphy Oct 6, 2026
31441fb
test(api): add collection and container subtype factories, and use them.
m-murphy Oct 6, 2026
ad24525
refactor(api): bundle a derivation row's child collection name and pa…
m-murphy Oct 6, 2026
1b26092
test(api): pin the derivation source-type warning when row 1 has no p…
m-murphy Oct 6, 2026
0f2c605
docs: scope the ADR rules to new code and match each ADR.
m-murphy Oct 6, 2026
3382d49
docs: describe formula neutralization in CONTEXT.md's CSV download en…
m-murphy Oct 6, 2026
14eb9fd
chore(api): lint any transaction callback that returns a Promise, in …
m-murphy Oct 6, 2026
6bc3c00
fix(web): say "control batches" in the wizard's fallback error.
m-murphy Oct 6, 2026
31be02a
refactor(api): move chunkArray and dateToUpperBound to lib/sql-helpers.
m-murphy Oct 6, 2026
e87c4ae
refactor(controls): share the control definition name check.
m-murphy Oct 6, 2026
4ebfa8c
fix(db): give each request its own transaction on the shared connection.
m-murphy Oct 6, 2026
1b73036
docs(db): document withRolledBackTransaction and the transaction lock.
m-murphy Oct 6, 2026
c524782
refactor(api): take an insert's returned row through one insertedRow …
m-murphy Oct 6, 2026
4a9cca4
refactor(export): replace the 'all' subject-date sentinel with a Subj…
m-murphy Oct 6, 2026
05ce00b
refactor(export): keep the "no date means all dates" rule in the cont…
m-murphy Oct 6, 2026
dd73488
fix(imports): keep every error on a bulk combined 409.
m-murphy Oct 6, 2026
0fbb943
refactor(auth): name the bcrypt cost once as BCRYPT_COST.
m-murphy Oct 6, 2026
ff791a1
docs(api): put the dashboard statistics doc comment on its function.
m-murphy Oct 6, 2026
c7d696e
refactor(api): branch on every collection type explicitly when deleti…
m-murphy Oct 6, 2026
28ee5e7
refactor(api): use one named chunk size, SQL_IN_CHUNK_SIZE, for IN li…
m-murphy Oct 6, 2026
753fcd4
fix(api): reply 409 when a container write hits a constraint.
m-murphy Oct 6, 2026
97b0327
refactor(web): list the command dependencies once and rebuild command…
m-murphy Oct 6, 2026
40f483f
fix(registration): blame a collection insert conflict on the row that…
m-murphy Oct 6, 2026
9b5aef1
refactor(web): load the user once per UserProvider with a ref, not mo…
m-murphy Oct 6, 2026
e2c8ef8
fix(registration): name the identifier that found a plate with anothe…
m-murphy Oct 6, 2026
6c761b7
refactor(web): fall back to one shared empty list instead of memoizin…
m-murphy Oct 6, 2026
7b5dc7d
fix(web): sign out on a 401 from /auth user and session admin requests.
m-murphy Oct 6, 2026
34f1167
refactor(web): share the box and bag sheet list and expand state in u…
m-murphy Oct 7, 2026
a9f4dad
fix(web): apply /specimens filters with Apply, so Back undoes one cha…
m-murphy Oct 7, 2026
244e1a1
fix(registration): reject a location that contradicts the collection'…
m-murphy Oct 7, 2026
83fd885
fix(web): rerun the batch-definition and allowed-container-type effec…
m-murphy Oct 7, 2026
c1d105f
fix(web): set up box and bag sheet expansion on load or collection ch…
m-murphy Oct 7, 2026
e8ae8fe
fix(api): drop the remaining study-sized IN lists from stats and export.
m-murphy Oct 7, 2026
764396d
fix(api): require a parent name for ambiguous source sheets in moves.
m-murphy Oct 7, 2026
b6320fd
refactor(api): validate and register /specimens/bulk through Specimen…
m-murphy Oct 7, 2026
3a511d6
fix(web): read chart colors for the theme being rendered and drop the…
m-murphy Oct 7, 2026
bb9bbe5
refactor(registration): drop collectionsToCreate from the check result.
m-murphy Oct 7, 2026
9dc98f9
fix(api): check sheet name conflicts inside the sheet move transaction.
m-murphy Oct 7, 2026
b8e5a0a
refactor(web): let the ContainerTypeUnitsManager effect call its load…
m-murphy Oct 7, 2026
4110ba2
fix(db): name the qPCR well result UNIQUE constraint as the schema does.
m-murphy Oct 7, 2026
c7b6e98
test(web): stub scrollIntoView in the web test setup instead of guard…
m-murphy Oct 7, 2026
c8938b0
style(web): drop trailing whitespace the lint fixes left behind.
m-murphy Oct 7, 2026
a14d39b
fix(web): say "Control definition not found" when the batch wizard's …
m-murphy Oct 7, 2026
4bac33c
test(web): wait for Apply to be enabled in the /specimens URL tests.
m-murphy Oct 7, 2026
4b33093
refactor(registration): remove the duplicated container type, error c…
m-murphy Oct 7, 2026
1db5aff
refactor(activity): drop null checks on the non-null container specim…
m-murphy Oct 7, 2026
402ebf5
refactor(registration): map container types to collections and tables…
m-murphy Oct 7, 2026
87aa5c5
refactor(registration): replace string claim keys, -1 sentinels, and …
m-murphy Oct 7, 2026
f6f92fa
Merge branch 'fix/audit-medium' into chore/agent-guardrails
m-murphy Oct 7, 2026
941bc0b
refactor(registration): rename only() to insertedRow().
m-murphy Oct 7, 2026
c67bd50
Merge branch 'docs/agents-md-restructure' into docs/adr-coding-standards
m-murphy Oct 7, 2026
e772047
refactor(registration): word errors for any caller and let the CSV im…
m-murphy Oct 7, 2026
74b0df2
test(imports): check validate against per_subject imports and documen…
m-murphy Oct 7, 2026
74f775a
Merge branch 'chore/agent-guardrails' into refactor/specimen-registra…
m-murphy Oct 7, 2026
1a1ea59
Merge branch 'refactor/specimen-registration' into docs/agents-md-res…
m-murphy Oct 7, 2026
d1fb016
Merge branch 'docs/agents-md-restructure' into docs/adr-coding-standards
m-murphy Oct 7, 2026
e6b4815
docs: list the registration paths still migrating after slice 1.
m-murphy Oct 7, 2026
94a2fb6
docs: record Container selection in ADR 0011 and the glossary.
m-murphy Oct 22, 2026
cb80630
feat(api): add the Container selection module from ADR 0011.
m-murphy Oct 22, 2026
b551370
feat(api): match every selected Tag in GET /containers.
m-murphy Oct 22, 2026
de3dc56
feat(export): select single-study GET export through Container select…
m-murphy Oct 22, 2026
4ce22ee
feat(web): label the dashboard's created filter Specimen created.
m-murphy Oct 22, 2026
b53074d
fix(export): return the rows from JSON POST container exports.
m-murphy Oct 22, 2026
1bf0076
feat(export): select single-study POST export through Container selec…
m-murphy Oct 22, 2026
16483fc
refactor(statistics): select dashboard statistics through Container s…
m-murphy Oct 22, 2026
ac28c65
refactor(export): select multi-study and barcode exports through Cont…
m-murphy Oct 22, 2026
8a06122
Merge branch 'feat/container-selection' into feat/container-selection-96
m-murphy Oct 22, 2026
0a95bf2
Merge branch 'feat/container-selection' into feat/container-selection-98
m-murphy Oct 22, 2026
e79d695
Merge branch 'feat/container-selection-96' into feat/container-selection
m-murphy Oct 22, 2026
bcf2cac
Merge branch 'feat/container-selection' into feat/container-selection-97
m-murphy Oct 22, 2026
57cd93a
refactor(export): take export study fields from the Source and delete…
m-murphy Oct 23, 2026
edbf023
docs: point export and statistics code at Container selection.
m-murphy Oct 23, 2026
46deaf8
refactor(api): let Container selection own its filter rules.
m-murphy Oct 23, 2026
d0c2b51
fix(export): select available types through Container selection.
m-murphy Oct 23, 2026
31b046a
test(api): seed Container selection tests through factories.
m-murphy Oct 23, 2026
974d85d
fix(api): bound the parameters of each subject-list query.
m-murphy Oct 23, 2026
3e79a11
test(api): cover Container selection filters across selection kinds.
m-murphy Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Agent guide

## Agent skills

- **Issue tracker**: GitHub Issues on `eppicenter/sampledb3`. "Publish to the issue tracker" is `gh issue create`; "fetch the relevant ticket" is `gh issue view <number> --comments`.
- **Triage labels**: each triage role is a GitHub label with the role's name.
- **Triage labels**: each of the five triage roles is a GitHub label with the role's name: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, and `wontfix`.
- **Domain docs**: `CONTEXT.md` is the glossary, `docs/adr/` holds the decisions, and `packages/docs/` is the user guide.

## Checks
## Running tests and checks

- Web tests: `cd packages/web && bun run test -- <files>`. Bun's own runner (`bun test`) hangs on React component tests; Vitest exits on its own, so piping it to `tail` or `grep` is safe.
- API tests: `cd packages/api && bun test src/...`.
- Lint is clean, so any lint error is yours.
- The pre-commit hook only typechecks and lints. Run `bun run ci:verify`, which CI runs on every PR, before pushing.
- Run `bun run ci:verify` before pushing. The end of Build in `README.md` says what it and the pre-commit hook check.

## Where to look

- Writing code, tests, issues, or proposals, or reviewing a change: `CODING_STANDARDS.md`.
- Rules for naming domain concepts, contradicting an ADR, failing tests, database access, calling the API from the web, and writing API and web tests: `CODING_STANDARDS.md`. The `code-review` skill checks changes against it.
- Writing an API test: `packages/api/src/__tests__/README.md`.
- Writing a web test: `packages/web/src/__tests__/README.md`.
- Changing the schema or adding a migration: `packages/api/src/db/README.md`.
Expand Down
68 changes: 43 additions & 25 deletions CODING_STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,76 @@

## Domain language

- Name a domain concept with the term `CONTEXT.md` defines for it, in code, test names, issue titles, and proposals. A term the glossary lists under _Avoid_ is a synonym to replace with the defined term.
- When a concept has no glossary entry, say so in the issue, PR, or proposal before introducing a term for it.
- A change that contradicts an ADR in `docs/adr/` names the ADR and gives the reason to reopen it, in the PR or proposal. For example: _Contradicts ADR-0006 (unified container inbound write), but worth reopening because…_
- Name a domain concept with the term `CONTEXT.md` defines for it, in code, test names, issue titles, and proposals. Treat a term the glossary lists under _Avoid_ as a synonym, and replace it with the defined term.
- When a concept has no glossary entry, say so in the issue, PR, or proposal before you introduce a term for it.
- When a change contradicts an ADR in `docs/adr/`, name the ADR and give the reason to reopen it, in the PR or proposal. For example: _Contradicts ADR 0006 (unified container inbound write), but worth reopening because…_

## Tests

- A test asserts the expected behaviour. When it fails, fix the code under test and keep the assertion as strict as before. A new test may land failing if it states the expected behaviour.
- Assert the expected behaviour. When a test fails, fix the code under test and keep the assertion as strict as before. You may land a new test that fails, if it states the expected behaviour.

## API

Paths are relative to `packages/api/`.

### Database

- A write of several statements that `await`s runs inside `withWriteTransaction` from `src/db/write-transaction.ts`.
- Change the schema with a numbered migration, following the recipe in `src/db/README.md`. DDL lives only in the migrations and `initial_schema.sql`.
- Code opens a database through `openOperationalDatabase`.
- Run a write of several statements that `await`s inside `withWriteTransaction` from `src/db/write-transaction.ts`. Drizzle's bun-sqlite `db.transaction(async () => …)` commits at the callback's first `await` instead of awaiting the returned Promise, so the rest of the write runs outside the transaction. ESLint rejects any callback passed to `.transaction(...)` that returns a Promise.
- Change the schema with a numbered migration, following the recipe in `src/db/README.md`. Keep DDL in the migrations and `initial_schema.sql` only.
- Open a database through `openOperationalDatabase`, which runs schema evolution. Open the file directly only when the code must not migrate it, and say why in a comment. For example, `src/scripts/check-integrity.ts` opens a backup with `new Database(path, { readonly: true })` so that checking it leaves it unchanged, and `src/db/__tests__/initial-schema-snapshot.test.ts` applies `initial_schema.sql` alone so that pending migrations can't hide a stale snapshot.

### Domain modules

- Create a Specimen or Container through `registerSpecimens` in `src/lib/specimen-registration/`. Validate a request with `checkSpecimenRegistration`, which is the same write rolled back (ADR 0010).
- A request that carries a container validates it with `containerWriteInputSchema` from `@sampledb/contract` (ADR 0006).
- Resolve a Specimen's Source with `resolveSpecimenSource` or `resolveSpecimenSources` from `src/lib/specimens/provenance.ts`, and hand every caller the full `SpecimenSource` shape (ADR 0007).
- Build a container payload (placement, Tags, Specimen, Source) from `loadContainerReadViews` in `src/lib/container-read-view.ts`, then project it (ADR 0009).
- In a new write path, create Specimens and Containers with `registerSpecimens` from `src/lib/specimen-registration/` (ADR 0010). Bulk combined import, `/subjects/with-specimens` and `/specimens/bulk` have migrated, and every container write goes through `createContainerForSpecimen`, which wraps it. These paths still insert Specimens directly until their slice in ADR 0010 lands: `POST /specimens`, which validates with `validateSpecimenData` (`src/routes/specimens.ts`); derivations (`src/lib/derivations.ts`); and control batches (`src/lib/controls/batch-with-specimens.ts`).
- In a new write path, validate a registration with `checkSpecimenRegistration`, or wrap the whole write in `withRolledBackTransaction` when the caller writes its own records first (ADR 0010).
- Validate an inbound container with a `@sampledb/contract` schema (`containerWriteInputSchema`, `bulkCombinedContainerSchema` or `optionalContainerInputSchema`), not an endpoint-specific container Zod schema (ADR 0006).
- Map a CSV row to `ContainerWriteInput` with `csvRowToContainerWriteInput` from `@sampledb/contract` (ADR 0006). The web bulk import and control-batch CSVs still map through `flatBulkContainerToWriteInput` and `flatControlBatchContainerToWriteInput` in `packages/web/src/lib/`.
- In a `ContainerWriteInput`, place a paper container on a `collection` of type `sheet`, not on a box or bag `collection` with a sibling `sheetName` (ADR 0006).
- Resolve a Specimen's Source with `resolveSpecimenSource` or `resolveSpecimenSources` from `src/lib/specimens/provenance.ts` (ADR 0007).
- Return the full `SpecimenSource` shape to every caller, not a per-endpoint subset (ADR 0007).
- Load a container read payload (placement, Tags, Specimen, Source) with `loadContainerReadViews` or `loadContainerReadViewsByIds` from `src/lib/container-read-view.ts` (ADR 0009).
- Project the loaded view to its read path's type: `EnrichedContainerApi` for list, derivations and PATCH, `EnrichedStorageContainer` for collection detail, and CSV row fields for Container export (ADR 0009).
- To choose which Containers an export or statistic covers, call `selectContainers` from `src/lib/container-selection/`, and filter a paginated Container query by Tags with `containerHasAllTags` (ADR 0011).

### Routes

- A route maps Drizzle rows to a wire DTO before `c.json` wherever the shape matters. `omitOnWireMiddleware` drops null keys from every response, so null omission stays with it (ADR 0005).
- A route reports an error by throwing a typed error from `src/lib/error-handler.ts` or returning `handleRouteError(error, c)` (ADR 0004).
- Structured stdout goes through `src/lib/observability.ts`. A failure meant for Admin Error Logs goes through `src/lib/error-logger.ts` (ADR 0004).
- In new or changed route code, report an error by throwing a typed error from `src/lib/error-handler.ts` or returning `handleRouteError(error, c)`, not with `return c.json({ error }, 4xx)` (ADR 0004).
- Throw `ExpectedNotFoundError` for a 404 caused by user input, such as an export preflight miss or a user-supplied lookup, and `NotFoundError` for a missing resource on a direct GET or a stale link (ADR 0004).
- Write no route-level `catch` block that calls `console.error`; return `handleRouteError(error, c)` from it instead (ADR 0004).
- In new code, write log lines to stdout with `logDebug`, `logInfo`, `logWarn` or `logError` from `src/lib/observability.ts`, not `console.*`. Startup banners and schema-evolution CLI output stay plain `console.log` (ADR 0004). The `console.warn` in `logTemplateError` in `src/routes/qpcr-experiments.ts` predates this rule.
- Persist a failure to the `error_logs` table with `src/lib/error-logger.ts`, not `observability.ts`. Both export `logError`, so check the import path (ADR 0004).
- In a new or changed route handler, do not spread a Drizzle row into a `c.json` payload (ADR 0005). `...newSpecimen` in `src/routes/specimens.ts` and `...locationRecord` in `src/routes/locations.ts` predate this rule.
- Map a response that has a schema in `@sampledb/contract/wire` (containers today) to that wire DTO before `c.json` (ADR 0005).
- Leave null omission to `omitOnWireMiddleware`: do not call `toWireJson()` in a route handler (ADR 0005).

### API tests

- Each test gets its own database from `setupTestDatabase()` and closes it with `cleanupTestDatabase()` in `afterEach`.
- Give each test its own database from `setupTestDatabase()`, and close it with `cleanupTestDatabase()` in `afterEach`.
- Seed rows with the factories in `src/__tests__/helpers/factories.ts`.
- A route behind session auth is tested through `setupAuthenticatedRouteTest`.
- A CRUD route with `checkInUse` has a delete test for both the in-use and the free case.
- Test a route behind session auth through `setupAuthenticatedRouteTest`. Log in by hand with `loginAndGetCookie` only when the login flow is under test, as in the auth route and middleware tests.
- For a CRUD route with `checkInUse`, write a delete test for both the in-use and the free case.
- Test a route's error cases as well as its success: validation errors, 404s, and duplicates.

## Web

Paths are relative to `packages/web/`.

- Call the API through its domain module, `src/lib/api/<domain>`. Its methods resolve to the response body, so read `res.study`. Reference-data `list()` helpers are the exception: their body is `{ data, meta? }` (ADR 0002).
- Read and write settings with `settingsApi.getValue` and `settingsApi.putValue` (ADR 0002).
- A Zod schema for a response marks an unset field `.optional()`, because the wire omits it (ADR 0005).
- Show a query's read error inline with `PageError` or `SectionMessage` from `src/ui/`. Toasts are for mutation errors (ADR 0001).
- A mutation goes through a domain hook that invalidates every query it affects, `dashboardKeys` included (ADR 0001).
- Scan move behaviour lands in the `src/lib/scan-move/` core with plain-data unit tests. The move pages only wire it up (ADR 0008).
- Read a setting with `settingsApi.getValue(key)` or `settingsApi.get(key).value`, never `api.get` on `/settings/:key` (ADR 0002).
- Write a setting with `settingsApi.putValue`, `settingsApi.update`, or the bulk wrappers `exportConfigurationsApi.update`, `scannerConfigurationsApi.update` and `tableViewConfigurationsApi.update`, never `api.put` on `/settings/:key` (ADR 0002).
- In a Zod parser for a container response, mark an unset field `.optional()`, not `.nullish()` (ADR 0005). Other response parsers are out of scope, such as `parseSettingsEnvelope` in `src/lib/api/parse-response.ts`.
- On a screen ADR 0001 lists as migrated, show a query's read error inline with `PageError` or `SectionMessage` from `src/ui/`, not a toast (ADR 0001). Workflow local state (CSV parse, server validate, submits) keeps its inline or toast errors.
- Prefer to send a new mutation through a domain hook, such as `useCreateStudy`, that invalidates the queries the mutation changes, including `dashboardKeys` when it changes dashboard data (ADR 0001). Direct `*Api.create`, `update` and `delete` calls in `Locations.tsx`, `QpcrExperimentDetail.tsx`, `StudyDetail.tsx`, `SubjectForm.tsx` and `ControlDefinitionForm.tsx` predate this rule.
- Put new Scan move behaviour in the `src/lib/scan-move/` core with plain-data unit tests, not in `ContainerMoveMicronix.tsx` or `ContainerMoveCryovial.tsx` (ADR 0008). The paper move page, `ContainerMovePapers.tsx`, is not a Scan move.
- Pass a Scan move effect a `ScanMoveGateway` instead of importing `collectionsApi` into it (ADR 0008).
- Test Scan move effects with plain stub gateway functions, not `vi.mock` (ADR 0008).
- Open a modal with `Modal` from `src/ui/`, not `ModalPortal` (ADR 0001). `CommandPalette` and `SearchModal` still use `ModalPortal` directly.

### Web tests

- Components render through `await render(...)` from `src/__tests__/helpers/render`.
- A test mocks the domain module the code imports with `createMockedDomainModule`. An override replaces a whole `*Api` object and resolves to the same body the real module returns.
- Render components with `await render(...)` from `src/__tests__/helpers/render`.
- Mock the domain module the code imports with `createMockedDomainModule`. Make each override replace a whole `*Api` object and resolve to the same body the real module returns.
- Query elements by role or label (`getByRole`, `getByLabelText`), and drive interactions with `@testing-library/user-event`.
- A mutation hook's test asserts which queries it invalidates.
- Wait for async results with `findBy*` queries or `waitFor`.
- Test a component's loading and error states, not only its loaded state.
- In a mutation hook's test, assert which queries it invalidates.
9 changes: 7 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ _Avoid_: Well (as the domain term); treating a missing position as occupying a s

### Export

**Container selection**:
The set of **Containers**, with their **Specimens**, that a set of filters matches: a **Study**, a subject list, or a micronix barcode list, narrowed by specimen type, collection date, container type, **Tags**, **Location**, and when the Specimen or the Container was created. Selecting several Tags matches Containers carrying every one of them. A barcode list selects exactly those tubes and takes no other filters. **Container export** and the statistics dashboard both count and list what the selection matches.
_Avoid_: Query (implementation term); treating Tag selection as "any of these Tags"; an unqualified "created" date (say Specimen created or Container created)

**Container export**:
Server-side export of operational **Container** records with specimen, source, and placement context, formatted using an **Export configuration**. Entry points include **Multi-Study Export** (page heading **Export Containers (Multi-Study)**), the Study export modal, and **Micronix Barcode Export**.
_Avoid_: Using "bulk export" as the umbrella for all container exports; collection table snapshot export; using "sample" in export column headers
Expand Down Expand Up @@ -170,8 +174,9 @@ Server-side flat export of inventory summary data (command palette or inventory.
_Avoid_: Container export; bulk export; collection table snapshot export

**CSV download**:
A comma-separated values file generated or downloaded from SampleDB — container/specimen/inventory exports, collection table snapshots, import templates, and other CSV outputs. All share the same wire conventions (UTF-8 with BOM, CRLF line endings, plain cell values per RFC 4180) so files behave consistently in Excel and in R/Python. **Container export** workflows additionally let lab staff override delimiter, BOM, and line ending before download.
_Avoid_: Excel formula cell wrappers (`="..."`); using CSV as the Excel text-preservation format (use **XLSX download** for that); different wire conventions for templates vs exports
A comma-separated values file generated or downloaded from SampleDB — container/specimen/inventory exports, collection table snapshots, import templates, and other CSV outputs. All share the same wire conventions (UTF-8 with BOM, CRLF line endings, cell values quoted per RFC 4180) so files behave consistently in Excel and in R/Python. **Container export** workflows additionally let lab staff override delimiter, BOM, and line ending before download.
Server-side data exports (**Container export**, **Specimen export**, **Inventory export**) apply **formula neutralization**: a text cell that a spreadsheet would run as a formula (starting with `=`, `+`, `@`, tab, carriage return, or `-` unless the whole cell is a number) gets a leading `'`, so stored text such as `=HYPERLINK(...)` cannot execute when the file is opened. Numbers, including negative ones like `-131`, are unchanged. Import templates and other files meant to be re-imported are not neutralized, since the `'` would become part of the imported value.
_Avoid_: Excel formula cell wrappers (`="..."`); using CSV as the Excel text-preservation format (use **XLSX download** for that); different wire conventions for templates vs exports; neutralizing import templates

**XLSX download**:
Excel workbook export from **Container export** workflows. Preserves identifier and code columns as text cells (leading zeros, long barcodes) without CSV workarounds.
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,15 @@ Put local overrides in **`.env.local`** (gitignored) at the repo root. Bun’s a

- **Unit tests (API + web)**: `bun run test`
- **End-to-end (Playwright)**: `bun run test:e2e`
- **API**: `bun --filter @sampledb/api test` (Bun). Coverage: `bun --filter @sampledb/api test:coverage` (report in `packages/api/coverage/`). Target 90% lines. Test helpers: `packages/api/src/__tests__/README.md`.
- **Web**: `bun --filter @sampledb/web test` (Vitest). Coverage: `bun --filter @sampledb/web test:coverage`. Coverage excludes `src/lib/api/**` and `src/**/*.css` so the 90% target applies to testable code. Thresholds in `packages/web/vitest.config.ts` can be raised in steps toward 90%. Test helpers: `packages/web/src/__tests__/README.md`.
- **API**: `bun --filter @sampledb/api test` (Bun). Coverage: `bun --filter @sampledb/api test:coverage` (report in `packages/api/coverage/`). Test helpers: `packages/api/src/__tests__/README.md`.
- **Web**: `bun --filter @sampledb/web test` (Vitest). Coverage: `bun --filter @sampledb/web test:coverage`. Test helpers: `packages/web/src/__tests__/README.md`.

### Coverage

This is the one place coverage targets are written down; the config files point here. Both packages target 90% line coverage.

- **API**: no threshold is enforced. `coverageThreshold` in `packages/api/bunfig.toml` is commented out; set it to make `test:coverage` fail below that line coverage. The thresholds in `packages/api/vitest.config.ts` apply only to Vitest coverage runs, and Vitest cannot run the tests that touch `bun:sqlite`.
- **Web**: the thresholds in `packages/web/vitest.config.ts` are a floor that fails `test:coverage`. Raise them in steps toward 90%. Coverage excludes `src/lib/api/**` and `src/**/*.css`, so the target applies to testable code.

## Database Configuration

Expand Down
2 changes: 2 additions & 0 deletions docs/adr/0006-unified-container-inbound-write.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ All persistence funnels through `createContainerForSpecimen` (accepting `Contain

Placement resolution (sheet find-or-create within box/bag, plate/box resolve) lives in one module — not duplicated across registration, control batch, and derivation CSV.

> **Amended by [ADR 0010](./0010-specimen-registration.md):** that module is `lib/specimen-registration/collections.ts`, and `createContainerForSpecimen` now wraps `registerSpecimens`. It replaced `container-write-placement.ts`, whose lookup fell back from a missing collection barcode to the name; that file is deleted. The derivation CSV and control batch validators still look collections up themselves until their ADR 0010 slices replace them.

## CSV

One contract mapper — `csvRowToContainerWriteInput` — maps flat CSV rows to `ContainerWriteInput`, then Zod validates.
Expand Down
Loading
Loading