Skip to content

Turn a finished scan file into the record, atomically - #585

Merged
xmap merged 1 commit into
mainfrom
worktree-scan-ingest-stage1
Jul 29, 2026
Merged

Turn a finished scan file into the record, atomically#585
xmap merged 1 commit into
mainfrom
worktree-scan-ingest-stage1

Conversation

@xmap

@xmap xmap commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Stage 1 of the scan-ingest seam, implementing the gate-reviewed design (four axes: layout, finality, correlation, transport; one port, one adapter, one composing slice). The Data BC's registration machinery was complete and format-free; this lands the one adapter that reads a real file, and the slice that drives it.

What it does

POST /scans/ingest (and the ingest_scan MCP tool) takes a locator plus the facts only the caller knows (producing Asset, storage Supply, optional Run), and:

  • reads frame roles, counts, angles, and structural completeness through the new ScanReader port; /exchange/* and image_key vocabulary dies at the DataExchangeScanReader adapter
  • digests the bytes through the new ChecksumComputer surface (the posix adapter grew a second protocol over one shared root-confinement helper)
  • registers Dataset + Distribution + Acquisition by composing the three existing DECIDERS and persisting all three genesis streams in ONE append_streams transaction; a mid-composition rejection leaves zero events
  • refuses files that are unreadable, unrecognized, structurally incomplete, still changing between the structural read and the digest pass, or missing an acquisition timestamp; refuses bytes already ingested, on a checksum natural key, naming the existing dataset id

Design decisions with teeth

Finality is respected, not assumed. /exchange/theta present means post-processing completed; the ScanStatus transfer message marks transfer initiation (both tomoscan paths are fire-and-forget), so this PR also carries an erratum to the 2-BM operations page that previously claimed the signal marks arrival.

captured_at is never fabricated. A parseable file start_date wins; with none, the operator supplies it as the same caller-asserted provenance the base slice already accepts, or the ingest refuses naming the remedy; both at once is refused as ambiguous. captured_at_source is recorded in evidence either way.

Counts distinguish zero from unknowable. dropped_frame_count is optional: absence means the source cannot know (a crashed file has no commanded counts; NXtomo never does), zero means verified-none. Commanded counts come from /process/acquisition, written on clean close by the areaDetector layout XML, because the /defaults frame ids can never show tail truncation.

The reader cannot break the beamline. HDF5 opens read-only with locking=False: a reader holding a shared lock can make tomoscan's own add_theta reopen fail, a failure tomoscan swallows, so observation would otherwise silently break the pipeline it observes.

Supporting changes

  • Migration 20260729120000 adds checksum columns + index to the dataset summary projection (no queryable checksum surface existed), with the schema boot gate's version pin moved in the same commit
  • Scan enters the glossary and the slice-subject allowlist as one deliberate convention extension, rationale in conventions.md
  • The composition core lives at the BC root (cora.data._ingest) per the conductor precedent, since slices must not import siblings
  • Descriptor data: section (verified facts only: layout, finality) plus its schema; DATA-9 widens to the deployed layout XML; DATA-10 asks the timestamp PV's timezone

Verification

17 reader tests on synthetic Data Exchange files, 13 handler tests pinning zero-events-on-every-refusal, 5 contract tests, integration tests landing a real HDF5 file as three streams in real Postgres and refusing a duplicate via the new columns, all 952 deployment-descriptor tests, full tiers green (45,757 fast + 1,084 integration/e2e), docs build clean.

🤖 Generated with Claude Code

The Data BC's machinery was complete and format-free; what was missing
was the one adapter that reads a real file. This lands the scan-ingest
seam designed and gate-reviewed in project_scan_ingest_design: four
independently varying axes (layout, finality, correlation, transport),
one port, one adapter, one composing slice.

ScanReader speaks frame roles, counts, angles, and structural
completeness; /exchange/* and image_key die at the adapter. The role
vocabulary carries invalid because NXtomo marks it (image_key=3), and
the Data Exchange adapter reports zero meaning none-marked. Shortfall
stays distinct from invalid, and dropped_frame_count is optional:
absence means the source cannot know (a crashed file has no commanded
counts, NXtomo never does), zero means verified-none. Commanded counts
come from /process/acquisition, written on clean close by the file's
second author, the areaDetector layout XML; the /defaults frame ids
alone can never show tail truncation because a missed trigger never
receives an id. Flat and dark expectations use tomoscan's own
arithmetic: configured count times the phases the mode names.

The adapter opens read-only with HDF5 locking disabled, for two
reasons: flock is unreliable on the network filesystems the analysis
tier lives on, and a reader holding a shared lock can make tomoscan's
own add_theta append reopen fail, a failure tomoscan swallows, so
observation would silently break the pipeline it observes.

ingest_scan composes the register_dataset, register_distribution, and
record_acquisition DECIDERS and persists all three genesis streams in
one append_streams transaction. It never chains those slices' handlers:
each handler is its own transaction, and a mid-chain rejection would
strand a Dataset without its Acquisition in a system whose pitch is
trustworthy provenance. Any refusal or rejection leaves zero events.
The composition core lives at the BC root (cora.data._ingest) because
slices must not import siblings; the conductor set that precedent.

Re-ingest refuses on a checksum natural key, naming the existing
dataset id: register_dataset mints a fresh UUID per call, so nothing
else stops double-ingest from silently duplicating the chain. The
checksum rather than the uri because the same bytes are legitimately
multi-homed across tiers. No queryable checksum surface existed, so the
dataset summary projection gains the column pair, an index, and a
migration, with the boot gate's version pin moving in the same commit.

captured_at never comes from the ingest clock. A parseable file
start_date wins; with none, the operator supplies it as the same
caller-asserted provenance the base slice already accepts, or the
ingest refuses naming the remedy; supplying both is refused as
ambiguous. CORA records claims, it does not manufacture them.

Ships with an erratum the design work surfaced: operations.md claimed
the ScanStatus transfer message marks when the analysis-tier copy
exists, but both transfer paths are fire-and-forget at tomoscan HEAD,
so the signal marks initiation and arrival must be verified
independently. DATA-9 widens to the deployed layout XML and DATA-10
asks the timezone of the timestamp PV.

Scan enters the glossary and the slice-subject allowlist as the one
deliberate convention extension: the slice's subject is the external
artifact itself, before any record of it exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/data
  _ingest.py 107
  errors.py
  routes.py
  tools.py
  wire.py 151-152
  apps/api/src/cora/data/adapters
  _file_uri.py
  data_exchange_scan_reader.py 100-109, 126-127, 230, 232-233, 242-243, 246-255, 279, 282-283
  posix_checksum.py 119-126, 147, 160-166, 170
  apps/api/src/cora/data/aggregates/dataset
  state.py
  apps/api/src/cora/data/features/ingest_scan
  __init__.py
  command.py
  handler.py 217, 254, 257, 260-264, 425
  route.py 163
  tool.py 102
  apps/api/src/cora/data/ports
  checksum_computer.py
  scan_reader.py
  apps/api/src/cora/data/projections
  summary.py
  apps/api/src/cora/infrastructure
  schema_version.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit b38c815 into main Jul 29, 2026
16 checks passed
@xmap
xmap deleted the worktree-scan-ingest-stage1 branch July 29, 2026 11:20
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