Record a capture that can never become a Dataset as a Shortfall - #798
Open
xmap wants to merge 2 commits into
Open
Record a capture that can never become a Dataset as a Shortfall#798xmap wants to merge 2 commits into
xmap wants to merge 2 commits into
Conversation
CORA's record could not distinguish "no scan ran" from "a scan ran and produced nothing": both looked like a terminated Run with no Dataset. Three real 2-BM Runs hold files with 1 projection of a commanded 1541 and no rotation angles, and the sweep retried them 387 times in one log window at a measured 6.3s each, writing nothing. The reader had already computed the frame counts and threw them away at the refusal, so the only non-substrate evidence was discarded precisely when it disagreed with the substrate. Shortfall is a terminal-at-genesis fact-chain beside Acquisition and Attestation, appended from ingest_scan as its second outcome so it needs no new command, grant, route or MCP tool. The 400 still raises: the caller asked for a Dataset and did not get one, so the refusal remains the truthful answer, and recording what CORA learned while answering is a separate act. The sweep's code is unchanged; the candidate simply stops being selected once the fact lands. Three preconditions gate it, all evidentiary rather than identity-based, so a human POSTing an indirect locator is treated exactly like the sweep: a resolved capture_path_id to key the stream on, a terminal Run, and a file whose mtime is strictly older than that terminal. Finality rests on a measurement, not a belief: across 329 files on the detector host every one settled 7.0 to 8.8s before its Run's terminal, so no stability window or attempt counter is needed, and neither could be afforded anyway since reading an mtime costs the same round trip this change exists to stop paying. The design memo assumed both sides of that comparison were already in hand. They were not: Run state carries no terminal timestamp and load_run discards the envelopes, so load_run_ended_at reads it from the stream. Reading it lazily inside the refusal branch made the memo's handler-reordering unnecessary and leaves every existing error precedence untouched. The stream id derives from capture_path_id rather than being minted, so uniqueness comes from expected_version=0 rather than from a projection read: the sweep re-selects a candidate until proj_data_shortfall_summary catches up, and that duplicate append is an expected race, swallowed quietly rather than surfaced as a conflict nobody can act on. Only StructurallyIncomplete records a Shortfall. Unrecognized is permanent too but yields no Description, so its Shortfall would carry a verdict and no evidence; that is a modelling question with no observed instance yet. Keying on the exception class instead of the cause would have permanently condemned every file caught mid-transfer, which is the one mistake here that loses data silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
ingest_scan only ever appends a Shortfall, so from_stored, evolve, fold and load_shortfall shipped written but never run. In an event-sourced system that is a latent bug rather than a coverage statistic: an event that cannot be read back is not really recorded. The generic net does not catch it either, since test_event_union_from_stored_coverage.py skips single-event aggregates, which is exactly this shape. The round trip compares the WHOLE folded state against an explicitly constructed Shortfall. A per-field assertion is blind to a field the writer forgot to carry, because the reader's default quietly fills it in; whole-dataclass equality fails the moment to_payload, from_stored or evolve drops one between them. Also covers the naive-terminal guard, built the only way that stream can actually arise: to_payload isoformats without an offset and fromisoformat reads it back naive. Diff coverage 86% to 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third and final slice of the capture-shortfall design (#796 widened the PII fitness test, #797 gave the refusal a discriminated reason). Both prerequisites are on main.
What this fixes
CORA could not distinguish "no scan ran" from "a scan ran and produced nothing". Both look like a terminated Run with no Dataset. Three real 2-BM Runs hold files with 1 projection of a commanded 1541 and no
/exchange/theta; the sweep retried them 387 times in one log window at a measured 6.3s each and wrote nothing. The reader had already computed the frame counts and discarded them at the refusal, so the only non-substrate evidence was thrown away precisely when it disagreed with the substrate.Shortfallis a terminal-at-genesis fact-chain besideAcquisitionandAttestation, appended fromingest_scanas its second outcome. No new command, grant, route or MCP tool.The refusal is unchanged
The 400 still raises. The caller asked for a Dataset and did not get one, so the refusal remains the truthful answer; recording what CORA learned while answering is a separate act (a failed login returns 401 and still writes an audit row). Nothing about the response shape, the route, the MCP tool or
CaptureScanIngestor's code moves. The candidate simply stops being selected once the fact lands.This settles the memo's open question about manual POST semantics. The three preconditions are evidentiary, not identity-based: a resolved
capture_path_id, a terminal Run, and a file older than that terminal. A human POSTing an indirect locator is therefore treated exactly like the sweep; a hand-typedfile://path never qualifies, because it has no stream key.Reviewer attention, in priority order
1. The memo's finality lock rested on a false claim. It said both sides of the comparison were "already in hand, so the check costs no additional I/O." They were not:
Runstate carries no terminal timestamp,load_runfolds and discards the envelopes, andproj_run_summary.updated_atis projection-write time (mutable, re-derived on replay). All four terminal events do carryoccurred_at, soload_run_ended_atreads it from the stream. Reading it lazily inside the refusal branch made the memo's handler-reordering lock unnecessary, so no existing error precedence changes and a successful ingest pays nothing.2. The stream id is derived, against the sibling pattern.
AcquisitionandAttestationmint a fresh UUIDv7. This one derives fromcapture_path_idvia uuid5, because the sweep re-selects a candidate untilproj_data_shortfall_summarycatches up: with a minted id, every tick of projection lag would add another duplicate Shortfall. Deriving makes the second append a version conflict, so uniqueness comes fromexpected_version=0rather than from a read that can be stale. TheConcurrencyErroris swallowed quietly, which is why the memo'sShortfallAlreadyRecordedError/409 is not implemented: a duplicate here is an expected race, not a conflict an operator can act on.3. Only
StructurallyIncompleterecords a Shortfall.Unrecognizedis permanent too, but yields noDescription, so its Shortfall would carry a verdict and no counts. What a countless Shortfall means is a modelling question with zero observed instances. Keying onInvalidScanFileErroritself would have permanently condemned every file caught mid-transfer, which is the one mistake in this design that loses data silently;test_ingest_transient_refusal_records_nothingpins that.4. The event carries both sides of the judgement, beyond the memo's payload list. "Can never become a Dataset" is unfalsifiable otherwise, and this row can never be erased.
Verification
Four mutations, each caught by exactly one test and no others:
..._still_being_written_records_nothing..._at_a_direct_path_records_nothingConcurrencyErrorno longer swallowed..._repeated_incomplete_reads_record_one_shortfallrun_id..._one_location_still_leaves_the_other_a_candidateThat last one is why the
_CANDIDATE_SQLchange has a Postgres test rather than a fake: the obvious single-location test passes under the wrong key. Only the two-location case discriminates, and without it one bad copy on the acquisition tier would silently condemn every other copy of the same Run.The fixture guards itself too.
test_shortfall_fixture_instants_are_independentfails if the clock, the Run terminal and the file mtime are ever collapsed, so the finality check cannot drift into agreeing by construction. The test's nanosecond conversion walks atimedeltafrom the epoch while production usesdivmod+fromtimestamp, so a rounding bug in one is not reproduced by the other.Full local run: 51,726 unit + architecture, 3,457 contract, 110 deployments, 16 Postgres integration. ruff, format and pyright clean over
srcandtests.Known, deliberate
hostandrootgenerate asdrop:text, so they are dropped from the published record. Barestr, and the generator cannot tell a storage tier from free text.AttestationRecorded.kind/.outcomesit the same way, and withholding a facility's internal hostname from an external publication is right by default. No override added;state.pysays why.docs/architecture/model.mdstill says "forty-three" aggregates. Not an oversight: 20 architecture doc pages already differ from a fresh render (the Trust page says "Five aggregates" against six, with its slices table at the literal_Generated from the code at build time._placeholder). The committed marker bodies are caches the mkdocs hook replaces at build time.ingest_scannow constructs exactly one event class directly, so the scan reads it as single-event); the two fold-symmetry ones have different rationales, sincefile_modified_athas no CORA actor at all whilerun_ended_at's actor lives on the Run's own stream.Still open
promote_datasetgates publication onproducing_run_end_state == "Completed". All three affected 2-BM runs areCompleted, so a dataset from a run that captured 1 frame of 1541 passes that gate today. Recording the Shortfall does not change that unlesspromote_datasetconsults it. That is a behaviour change to a publication gate and wants its own slice.