Skip to content

Slice 12: genesis baseline, read a witnessed Run's PVs once at BEGUN - #665

Merged
xmap merged 11 commits into
mainfrom
worktree-witnessed-run-slice12-baseline
Aug 16, 2026
Merged

Slice 12: genesis baseline, read a witnessed Run's PVs once at BEGUN#665
xmap merged 11 commits into
mainfrom
worktree-witnessed-run-slice12-baseline

Conversation

@xmap

@xmap xmap commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the conditions-snapshot gap identified in the pre-live plan: when a capture promotes to a witnessed Run, CaptureBaselineReader reads a deployment-declared set of PVs ONCE through the existing ControlPort and writes them as Observation rows via the existing AppendObservations command, with sampling_procedure="baseline". No new event, no new projection, no migration — the Plan's declared defaults stay in effective_parameters as declared intent (D1 stands), and the baseline rows carry the observed truth.

  • Setting: capture_baseline_pvs (dict[str, dict[str, str]], code -> channel_name -> PV), a sibling of capture_watch_pvs with an open inner-key vocabulary since baseline needs ~20 PVs per code and doesn't fit the closed role set. Reasoning is written into the setting's own comment.
  • Fourth kill switch: capture_baseline_recording_enabled, defaulting False, refusing to boot when True without run_witness_recording_enabled (mirrors the existing capture_progress_recording_enabled gate).
  • New component: CaptureBaselineReader (cora/api/_capture_baseline_reader.py) reads every declared channel concurrently, skips a reading for Bad quality, absent substrate time, non-numeric value, or oversized units, and appends whatever survives as one batch. Every failure is caught and logged — a baseline read must never prevent or unwind the promotion that triggered it.
  • New Agent principal: CaptureBaselineReader, a separate principal from RunWitness and CaptureProgressFeeder, so baseline-writing can be revoked without blinding either.
  • F2 preflight (capture_watch_preflight.py) extended to sweep capture_baseline_pvs too: no per-channel decoder, so it reports kind/value/units with verdict n/a, flagging only a non-numeric value as BAD.
  • Separate commit: fixed the CAPTURE_STATUS_PHASES example, which listed "Scan aborted": "Aborted", a literal decarlof/tomoscan@master never actually writes.

Energy has no located readback PV and is deliberately left undeclared (not guessed).

Gate review

Ran a 3-baseline + security-specialist panel before the final commits. All four lenses came back clean except two real findings, both fixed:

  • Security (moderate): PostgresObservationStore.append writes a whole batch in one executemany; an oversized units string would have failed every other reading in the same promotion's batch, not just the offending channel. Now rejected per-reading, ahead of the batch (READING_UNITS_MAX_LENGTH).
  • Architecture (moderate): per-PV reads ran sequentially inside run_witness_loop's single consumer path. Now concurrent via asyncio.gather (_read_one never raises, so this is safe).
  • Test coverage: closed two gaps — pinning that the kill switch is read fresh at call time (not cached at construction), and pinning the preflight's verdict contrast with the real reader (Bad quality / no substrate time are NOT flagged there, only non-numeric is).

A naming review (naming-r3-reviewer) also caught and fixed two issues: _PvReport.role was misnamed for baseline lines (renamed to pv_key), and CaptureBaselineReader.read_baseline stuttered at its call site (renamed to bare read, matching CaptureProgressFeeder's offer/flush).

Test plan

  • make test-noio — 47,876 passed, 630 skipped
  • make test-db — 1,196 passed, 1 skipped
  • make lint / make typecheck — clean across the full tree
  • App boots cleanly with create_app()
  • capture_watch_preflight run against the live 2-BM host once ioc2bmb1 is reachable (currently down per the pre-live plan's testing note)

🤖 Generated with Claude Code

xmap added 11 commits August 15, 2026 22:40
Adds capture_baseline_pvs, a sibling of capture_watch_pvs shaped
code -> channel_name -> PV rather than code -> role -> PV: baseline
needs on the order of twenty PVs per code (scan geometry plus
beamline conditions TomoScan never reports), which does not fit the
closed role vocabulary capture_watch_pvs already dispatches on by
name. Adds the fourth independent kill switch,
capture_baseline_recording_enabled, mirroring
capture_progress_recording_enabled's shape. Neither is wired to
anything yet; the boot guard and reader land in follow-up commits.
…_recording_enabled

Fourth kill switch, same shape as capture_progress_recording_enabled's
existing gate: a baseline read has no promoted Run to attach to
without recording turned on.
A separate principal from RunWitness and CaptureProgressFeeder,
mirroring why CaptureProgressFeeder got its own: an operator can
revoke baseline-writing without blinding either the witness or the
progress feeder, and actor_id tells the three runtimes' rows apart.
Opens the ba5e ("BASE") mnemonic UUID block. Not wired to any runtime
yet; the reader itself lands next.
Reads every channel declared for a capture_code exactly once and
appends whatever survives as one AppendObservations batch with
sampling_procedure="baseline". Three skip rules per reading: Bad
quality, no substrate time, non-numeric value; a MAJOR/MINOR-collapsed
Uncertain quality is kept, mirroring the alarm-vs-fault split already
shipped elsewhere. Every failure (a dead PV, an uncoercible reading,
or the append call itself) is caught and logged, mirroring
CaptureProgressFeeder's posture: a baseline read must never prevent or
unwind the promotion that triggers it. Not wired into RunWitnessRecorder
yet.
RunWitnessRecorder._promote calls the configured baseline reader
exactly once, right after a successful promotion, gated on the fourth
kill switch (capture_baseline_recording_enabled) read directly off
settings the same way run_witness_recording_enabled already is. A
baseline-read failure is caught and logged, never unwinding the
promotion that already committed.

run_witness_lifespan builds the reader when capture_baseline_pvs is
declared, requiring record_witnessed_run, control_port, and
append_observations, mirroring the existing capture_progress_recording_enabled
gate shape.
Seeds the CaptureBaselineReader agent alongside RunWitness and
CaptureProgressFeeder, and passes the shared ControlPort plus
capture_baseline_pvs into run_witness_lifespan so a deployment that
declares CAPTURE_BASELINE_PVS gets a real reader without any other
wiring change.
Baseline channels have no per-role decoder in production (every
channel is treated identically), so the sweep reports kind/value/units
with verdict n/a, except a non-numeric value is flagged BAD -- the one
defect checkable ahead of a real append attempt, since Observation.value
is float. Reuses finite_float from _capture_observer so this can never
drift from what CaptureBaselineReader itself accepts. Baseline lines
are tagged group="baseline" and rendered with a "baseline:" role
prefix so a human reading the combined report can tell the two groups
apart.
"Scan aborted" -> "Aborted" was never a real mapping target:
decarlof/tomoscan@master never calls ScanStatus.put("Scan aborted"),
verified by extracting every ScanStatus.put() call across
tomoscan.py, tomoscan_2bm.py, and tomoscan_pso.py (an Aborted phase
reaches the recorder through the separate abort role instead). Two
literals upstream DOES write, "Error writing configuration" and
"Config File Write Error", are absent from the example and stay that
way: UNRECOGNIZED is the honest answer until an operator decides what
phase, if any, a config-write failure maps to.

Comment-only change, no behavior difference.
…seline()

Two findings from the naming reviewer:

1. _PvReport.role was populated with a channel_name for baseline
   lines, which are not roles; renamed to pv_key, the field's actual
   structural meaning across both groups.
2. CaptureBaselineReader.read_baseline stuttered at every call site
   (self._baseline_reader.read_baseline(...)). Renamed to the bare
   read(), matching CaptureProgressFeeder's bare-verb methods
   (offer, flush).

No behavior change.
…rently

Gate review findings:

Security (moderate): PostgresObservationStore.append writes an entire
batch in one executemany call, so a single reading whose units exceeds
the DB's 64-char CHECK bound would fail EVERY other reading in the same
promotion's batch, not just the offending channel -- silently
undermining this module's own per-PV-independence guarantee. Reject an
oversized units string per-reading, ahead of the batch, mirroring the
existing non-numeric-value rejection.

Architecture (moderate): read() looped sequentially over every declared
channel inside RunWitnessRecorder._promote, itself on run_witness_loop's
single consumer path, so a slow or partially-unreachable control system
could block the loop from reacting to the next lifecycle observation for
the sum of every PV's own timeout. _read_one already never raises, so a
plain asyncio.gather over the per-channel reads is safe and removes the
sequential blocking without changing any failure-posture guarantee.
1. Pin that capture_baseline_recording_enabled is read fresh on every
   promotion, not cached at RunWitnessRecorder construction: flips the
   flag on a live Settings object between two promotions on the same
   recorder instance and asserts the second one alone reads the baseline.
2. Pin the F2 preflight's baseline verdict contrast with the real
   reader: a Bad-quality or timestamp-less reading still reports
   ok=True / verdict="n/a" in the preflight sweep, since only
   non-numeric is checkable ahead of a real append attempt.
@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/agent
  __init__.py
  seed_capture_baseline_reader.py
  apps/api/src/cora/api
  _capture_baseline_reader.py 167, 200
  _run_witness.py 544
  capture_watch_preflight.py 296-297
  main.py
  apps/api/src/cora/infrastructure
  config.py
Project Total  

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

@xmap
xmap merged commit 83d76e4 into main Aug 16, 2026
19 checks passed
@xmap
xmap deleted the worktree-witnessed-run-slice12-baseline branch August 16, 2026 08:54
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