You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Two more band-less tools emit exactly one tepna.verdict/1 each, criteria pre-stated with their sources —
The PAT hat rows now render what the solver actually decided per corner, and the drift-removed hat gets (PAT-HAT-DRIFT-DIFFERENCED-2026-09-27-BRIEF.md)
npm run gates:browser:local — one command for the browser lane, and --label to prove a named
The PAT three-cornered hat now says which kind of negative corner it found, and adds a drift-removed (PAT-HAT-DRIFT-DIFFERENCED-2026-09-27-BRIEF.md)
A mutation survivor no longer lives and dies with one PR. tools/mutation-survivors.mjs records every survivor a gate run reports into a committed ledger keyed on lane, module, function and the mutant's own diff pair — content, never a mutmut index — with the PR that reported it and its state, and its open count is a two-sided ratchet in the repo check. Ingest refuses a run it cannot attribute to a PR, refuses to read an absent measurement as an empty survivor list, and flags a survivor list as a lower bound while mutants went undecided. The first twelve entries are the real unanswered findings on loss_audit.py::_has_worn_evidence, re-derived because the only record of them had already drifted. (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
Every unmeasurable PAT quantity now carries its own reason beside the null — the same sentence in the
Pin the treatment-response qualifying cohort at the paper's stated configuration, decomposed so a moved yield names whether the generator or the detectors moved.
SOLID-NIGHT §A5's unrecorded-shift tripwire is built, and with it the timebase band can PASS. Until now (SOLID-NIGHT-2026-09-23-BRIEF.md)
audits/ABSENCE-SURVEY-2026-09-22.json had 290 confirmed findings and nowhere to record what happened
The night-loss verdict judges worn-but-not-recorded against the owner's 1 % bar instead of reporting it as UNKNOWN.
**Seven nights folded into the committed trio corpus — 2026-09-22 … 2026-09-28 — closing a backlog that
The journal's clock-event lines now travel with the night. LOSS-AUDIT.json has always carried (SOLID-NIGHT-2026-09-23-BRIEF.md)
The diff-scoped mutation gate can now be told, once and in writing, that a function's measurement cost is not the diff's: a declared bounded list of exact module.py::function keys — each carrying a reason, a date and a cost whose provenance is required — lets the gate report NOT_APPLICABLE instead of an UNKNOWN that every future PR on that function rediscovers. The declaration is allowed exactly one transition, UNKNOWN → NOT_APPLICABLE: it can never rewrite a FAIL, never yields PASS, short-circuits while any survivor is blocking, requires every refused function in the run to be declared, and fails closed on a refusal line it cannot decode exactly. Functions the gate refused are also ingested into an unmeasured ledger with a growth gate, so a function that was never mutated leaves a trace where the survivor ledger — which records survivors — can hold none. (MUTATION-DECLARED-EXCLUSION-2026-10-03-BRIEF.md)
The capture-host test fixtures now say where their bytes came from: one registry classifies every test file that carries a timestamp, a long hex literal or a data fixture as captured, derived, format, synthetic or authored, and a test holds it equal to the tree. tools/codex-export.mjs builds the source-only export an external code reader may see, omitting the data and record trees and every captured or derived fixture, and refuses if any remain.
The PAT three-cornered hat is solved on three arrival-FLOOR axes instead of raw receive stamps: E11's (PAT-HAT-RING-FLOOR-2026-10-04-BRIEF.md)
**The O2Ring's 156 rows are its own beat detections, and PpgDex now reports agreement between its
**The O2Ring's PPG frames now land on the SAME arrival sidecar the Polars use, so the finger finally has
A config-selectable ring wave-poll mode, o2ring.wave_poll_mode, default OFF (dual is byte-identical (RING-POLL-SPLIT-2026-10-04-BRIEF.md)
trio-batch reads a .superseded marker before refusing a night as ambiguous (owner ruling 2026-10-04).
tools/withdraw-fragment-verdict.mjs withdraws a verdict that judged a FRAGMENT of one recording, with (NIGHT-IS-THE-RECORDING-2026-10-05-BRIEF.md)
Changed
The 82 stamped uploads/trio nights are re-folded under the current compute closure, on the owner's 2026-09-28 ruling prompted by #3162 closing the integrator-dsp.js omission in CODE_DIGEST. 82/82 folded from the capture root the committed stamps themselves record (/srv/data/tepna-corpus, read from trio-batch-verdicts.json's evidence rather than assumed). Every night moved and integrator-dsp.js explains none of it: that file has zero commits since 2026-09-22, so the owner's control — nights whose integrator-dsp.js was unchanged since their stamp must be byte-identical — holds for all 82, and all 82 moved anyway. The cause is the rest of the closure, oxydex-dsp.js (10 commits since 2026-09-22), ecgdex-dsp.js (8) and ppgdex-dsp.js (2), which were never omitted from the digest; shown by digest rather than argued, since 2026-08-19's committed stamp (7b44db6fbd394099) matches neither today's closure (7fb220f140806c93) nor the closure without integrator-dsp.js (acb667efbc3941e6), so it predates the node-DSP work and the omission could not have been what staled it. #3162's fix is therefore prospective — it makes a future integrator-dsp.js edit stale the stamps, which is the hole it closed — and the re-fold was overdue on its own merits. What the movement consists of is mostly measurement provenance the old exports did not carry: measured, measuredSec, recoveredCount, nsiComponents and readinessBasis.weightTotal appear as new keys across all 82 nights, and "recovery": 0 becomes null where a zero had stood for "not measured" — §∅ corrections reaching the corpus, which is the argument the re-fold paid for itself whatever the digest did. Scope is held at the 82: the 58 unstamped night directories are not folded here, because with no prior stamp there is nothing for a byte-identical control to compare against, so they are a first fold with their own acceptance and their own residue row.
The capture-host tree is ruff format clean — 423 of 471 files reformatted under the owner's 2026-09-28 ruling, with no logic change: every file is structurally AST-identical and the only semantic movement is thirteen docstrings re-indented by one constant each. The mutation ledgers travel with it rather than being orphaned by it: every equivalence and survivor entry whose keyed text moved is re-keyed from its pre-format mutant to its post-format one by content, arguments untouched, so the reformat creates no new orphans and no answered mutant becomes an unclassified survivor under the now-required gate.
The mutation gate's declared exclusion becomes one module-level declaration, checked before any work starts (owner ruling). The cost it records is a property of the module — mutmut applies a glob when it runs mutants and never when it generates them, so every capture.py diff pays for the whole module's population whatever it touched — so a list of function keys each citing the same module-level measurement stated one fact as many. The gate now consults the declaration at selection rather than after spending the budget, turning a 7693 s run that reached no mutant into a verdict in seconds, and it prints on every run how many functions the declaration covers (measured from the module's AST, not quoted) and how many of this diff's functions fell under it, so the size of the blind spot is re-counted per PR. Two defects in the first version are fixed with it: the substitution never fired when a diff's entire scope was one declared function, because that case reports NOT_RUN rather than UNKNOWN; and the skipped functions stopped reaching the unmeasured ledger once the check moved earlier, which would have removed the record of what the gate no longer measures. (MUTATION-DECLARED-EXCLUSION-2026-10-03-BRIEF.md)
**The committed trio corpus is re-folded under the per-segment host axis, and the published
capture-host/check.sh's format leg is now BLOCKING and checks the WHOLE TREE, on the owner's
Fixed
tools/pat-drift-attribution.mjs no longer spends a DRAWN device axis as a second clock: effectivePpm (KNOWN-CLOCK-ADVERSARIAL-CAPTURE-2026-08-14-BRIEF.md)
An H10 lying off the body is no longer held worn by the heart rate its own algorithm reads out of (CAPTURE-LOSS-PRECEDENCE-AUDIT-2026-09-22-BRIEF.md)
**Both hat rows on PAT Classic vs Fused.html have rendered REFUSED on every night since the page
The diff-scoped mutation gate runs from a worktree (interpreter resolved from an override, then the primary checkout, refusing with the paths it tried), stops emitting PASS over a population of zero, no longer double-counts survivors across globs, and asserts its own result block is possible.
mutation_diff.py's pre-existing mutation survivors drained 73 to 8 — the selftest's twenty-two duplicated failure paths become one that its fault-injection tests actually reach, the literal scan's quote-adjacent boundaries are pinned by searched cases, and the eight that remain are recorded as equivalent with the probe that establishes each; the audit tool also gains the worktree interpreter resolution the gate got in #3171.
The fourteen mutation survivors on #3181's new lines are drained: the wall-cap arithmetic is extracted as cap_remaining with a named floor so it can be asserted as numbers rather than through a clock that cannot afford a one-second bound, the abandoned reader is pinned as a daemon with a bounded join (an unbounded one would turn a refusal that just fired into a hang), and the post-kill reap's bound is asserted; one survivor, a one-second change to the join bound on an already-finished thread, is recorded as no-distinguishing-input with its argument.
An edited test invalidates mutmut's SELECTION as well as its results, so a test rewritten to kill a mutant is no longer ignored on a reused scratch; and the gate's prose and verdict name the scope it actually uses — functions, not lines.
The CPAP spool-wiring tests stop reading coroutine frame locals to find the supervised loop's factory, so they survive mutmut's trampoline and mutmut's stats pass no longer fails on every capture.py diff; and the mutation tool's wall cap becomes reachable — it was applied at a proc.wait() placed after an unbounded read of the child's stdout, so a run that never finished was never bounded, which is how a traced stats pass reached hours against a two-hour budget. A budget exhausted before a single mutant was decided now reads NOT_RUN with the elapsed time in the verdict, not UNKNOWN.
nightFilesFor now guards the shape it needs instead of truthiness, so no index entry of any shape
The Nights page's "PAT fused" pill now opens the page, and a click that fails can no longer fail
A ring stall is judged by the witness the ring does carry, instead of counting as lost signal by default. The O2Ring's _SPO2.csv primary has no device column and its raw streams' sensor timestamp [ns] is drawn (sample_index × 7,953,045 ns, one delta value at 100.0 % across 16 files), so the delay test — which compares a host gap against a device-counter step — had nothing to read and every event-loop stall fell to loss. Note the wording carefully: the ring HAS a clock, an onboard RTC deliberately disciplined by SET_UTC_TIME (0xC0); what its exported axis carries is no per-sample readings of it. loss_audit.signal_witness assembles duration_s from every OXYFRAME.txt of the night — a counter of seconds of signal PRODUCED, not seconds of time, which is precisely what makes it a witness here — and witness_judges reads it either side of a gap: advancing by the gap's length within one count means the ring produced those seconds and only the delivery was lost, not advancing means they never existed. A gap the counter cannot bracket, a night with no frame file, and a counter that went backwards because a new session reset it are all unwitnessed, reported under unwitnessed_min and deliberately NOT in lost_min, which feeds worn_lost_min and from there the verdict's worn_but_not_recorded_fraction — loss is a claim that the signal does not exist, and an unjudgeable gap is not that claim. Measured over the four corpus nights whose ring SpO2 actually stalls: claimed loss 4.5 min → 1.6 min of demonstrable silence, 2.3 min of delivery delay, 0.5 min unjudgeable (2026-09-20 0.2→0.1, 09-21 0.4→0.1, 09-22 1.0→0.2, 09-24 2.9→1.2). Scoped to the ring's own primary, not to "any stream without a counter": duration_s says nothing about an H10, so a Polar file that carries no device column keeps exactly the behaviour it had, and #3157's delay classification and #3163's fragment totals are unchanged. gaps, by_cause and lost_min still come from one list — lost_min subtracts the unwitnessed portion computed from that same list rather than filtering one of the three. (SOLID-NIGHT-2026-09-23-BRIEF.md)
A seam sidecar's totals now agree with its rows by construction, and a reader that ever finds them disagreeing refuses the file by name. _SeamSidecar.feed incremented self.seams before writing the row, inside a try whose handler deliberately swallows because an annotation must never end a recording — so a write that raised would have left the count one ahead of the rows and the final line would claim a seam the file does not carry. The count now follows the row. solid_night_inputs.clocks sums the seams across ALL of a file's final lines (a resumed file-set writes one per writer instance — the 2026-09-24 H10 carries three) and returns UNKNOWN with a named reason when that sum does not equal the rows, rather than trusting either side. This closes the narrow surviving risk from 2026-09-25-seam-row-without-its-writers-final-line, which is WITHDRAWN: its own cited file carries three final lines including seams=1 examined=3577 for the seam row it says went uncounted, and across 132 corpus sidecars (66 rig, 66 box) none disagree. The tripwire therefore fires on nothing today and exists to notice the day it does.
tools/trio-power-headless.mjs emits one tepna.verdict/1 and **refuses instead of quietly substituting
A QTc trend point whose tMin is absent or non-numeric is dropped, where it used to be placed at ECG
**A worktree that holds nothing is removed by wt-done <path> with no flag, and a refusal now names the
regressLnRmssd now requires a finitetMin. It guarded isFinite(e.tMin), and **isFinite(null) is
A token tools/trio-batch.mjs cannot interpret now refuses with exit 2, naming the token and the nearest documented flag, before anything is planned or written. The idiom across tools/ is flag = (n) => argv.includes(n) and opt(n, d), each looking up only the names it knows, so a token nobody looks up was indistinguishable from an absent one — and the default it fell back to was not neutral but whatever the tool does when the flag is off, which for a dry-run flag is the WRITING branch. Measured: --dry for the documented --dry-run printed a per-night plan, computed, and wrote two exports of an unstamped night, while the tool's own help promised "compute nothing, write nothing". The refusal lives in a new shared tools/argv-guard.mjs rather than as a bespoke block, so the next adopter costs two lines; trio-batch is the only adopter here. Arity is part of the spec, because a guard that walked tokens without knowing which flags consume a value would refuse --night 2026-06-20 and turn a correctness fix into a tool nobody can run — so a valued flag with no value after it, and a valued flag that would swallow the next flag, are refusals of their own, and --flag=value is refused because opt reads the next token and would otherwise ignore a form it accepted. The declared set is what the code READS, not what the header documents: trio-batch reads four flags its help never mentions (--cpap, --only-node, --allow-partial, --child), one of them passed by its own child dispatcher, so a spec built from the help would have refused its own children — and a test asserts that every flag reachable from a flag(/opt(/optAll( call site is declared. A suggestion prefers a flag the token is a PREFIX of over the closest by edit distance, because --dry is distance 2 from --src and 4 from --dry-run, so a pure-distance answer names the wrong flag for the one slip this exists to catch.
**Of 45 tools whose no-argument path is provably inert, ZERO could tell an unknown flag from an absent
The diff-scoped mutation gate stops mutating functions a formatter only moved: scope is now the intersection of the changed lines with the functions whose AST actually differs from the base, so a file carrying one real edit beside a page of reflow scopes that one function instead of every function the lines touched. Comparison is full AST equality with no string blanking, so a changed log line or format string still scopes and a re-indented docstring does too; a module whose base revision cannot be read or parsed narrows nothing.
mmeta.generated_under_glob counted mutants with ^\s*def, which does not match async def, so it
When a survivor ledger close label matches several entries, the refusal now names every candidate with its full qualified label and its mutant id, instead of reporting only a count — the missing information was that a label may be qualified with lane:module::function, which is what made a shared one-line mutation look unaddressable. close also accepts an entry's own mutant id as a shorter handle. (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
A function whose only difference from the base is its docstring no longer contributes mutation scope: mutmut generates no mutants for a docstring node, so the change carries nothing the gate could test, and scoping it re-mutates a body that did not move. A docstring edit accompanied by any other change still scopes, and every non-docstring string constant still scopes, so the exemption is the one node with no mutants behind it rather than a judgement about which strings matter.
One index scan answers where does an HTML element end. The parse census added in #3209 carried its
Four analysis tools shipped a SyntaxError and could not boot. tools/build-analysis.mjs injected its
**A required mutation gate burned a 180-minute job, decided ZERO mutants, and the two guards that existed
PAT Classic vs Fused pairs each arrival sidecar and ACC file with the session of its own device's waveform. It (PAT-HAT-DRIFT-DIFFERENCED-2026-09-27-BRIEF.md)
PAT Classic vs Fused now shows what the worker actually decided about a night, and says which axes its hat (PAT-HAT-DRIFT-DIFFERENCED-2026-09-27-BRIEF.md)
The capture box's offline-alert poller no longer calls every quiet device a "ring". After a completed
The mutation scratch prune stops deleting directories other sessions are still using — each scratch records its owner's PID and process start time when it is claimed, and a stale tree is removed only once that owner is provably gone and past an age floor, with an unmarked tree outlived rather than judged. And a mutation run whose projected peak memory does not fit refuses before it starts, naming the generated-module size, the assumed RSS factor, the worker count and the cap it was measured against, instead of beginning and being reaped under box memory pressure with nothing to report.
A night's session span no longer depends on the zone of the box reading it. nightqc.summarize subtracted a FLOATING civil filename stamp from an ABSOLUTE mtime, so one real night measured 116,853 s in UTC, 102,453 s in America/New_York and 149,253 s in Asia/Tokyo — one offset apart each time, and 32.5 h for a single session in the UTC reading. One rule now governs every hop: each civil stamp the box writes is read as floating (the components as written), and the night's RECOVERED writer UTC offset is the single place a floating value becomes an instant; absent it, every span that needs one is UNKNOWN with a named reason rather than a guess. Six conversion sites moved together, not the three the residue row named — and two of the three newly found were live defects of their own, because _midnight_of and the cross-midnight contiguity probe both made POOLING move with the reader's zone, so the same night pooled in one zone and not another. The offset is recovered as the modal bucket of mtime − floating(last row) at the 15-minute quantum of the quantity (every UTC offset in use since 1972 is a whole multiple of it), with a ±14 h refusal bound checked before any vote is counted — measured need, since the suite's own fixtures write a constant host stamp that recovered a 67-day "offset" behind a clean majority — and a support floor of ≥2 unanimous voters or ≥3 with a strict majority, whose revision is recorded in three steps on the constant. Two decisions committed to earlier turned out to be wrong and are recorded as such: flooring the vote (the true value sits exactly ON a bucket boundary, so a one-millisecond shortfall moved a recovered zone fifteen minutes on the real 2026-09-27), and a floor whose own rationale — outvote a single outlier — did not bind the unanimous case it refused. Corpus control over 82 folders: 72 of 72 votable nights recover +14,400 s with no second value anywhere, outliers 0.81 % of 12,786 voters, and ten refusals each explained (7 non-night folders, a ring offline-sync tree with no host stamps at all, one two-voter disagreement, and one three-session night whose two killed sessions split the vote three ways). summarize/build also gain an optional writer_offset so a caller that KNOWS the frame can declare it — published as basis: "declared", never blended with "recovered" — which is the seam a recorded offset will enter through when the writer records one. Fixes a crash found on the way: in the floating frame a file with no start stamp cannot be placed, so a night could hold rows and yield no session, and reading the judged session off that raised TypeError inside the QC poller, costing the night its entire summary. (SESSION-SPAN-WRITER-OFFSET-2026-09-28-BRIEF.md)
The survivor ledger stops calling a stale artifact a regression. Ingest places a run against each closure using the commit every tepna.verdict/1 already carries: a verdict whose code does not contain the closing commit reads NOT_APPLICABLE with both shas named, one that does and still shows the mutant surviving is the real regression and the only thing that reds, and a verdict that does not say which code it judged refuses with NOT_RUN rather than assuming the current one. The closing commit is resolved from the closing PR's number so it survives a squash merge, which a recorded branch sha does not. (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
The survivor ledger's key becomes content plus position, so two distinct survivors in one function whose mutation text is identical are two findings instead of one. The occurrence ordinal is derived from the mutated line's position, which the gate's artifact already carries in each survivor's diff hunk; the raw line is recorded as a breadcrumb but kept out of the key, because a line shifts on any edit above it while an ordinal moves only when a twin appears or goes. Ordinal zero adds nothing to the key string, so every existing entry keys identically and the migration verifies that rather than assuming it. (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
The twelve mutation survivors on loss_audit.py::_has_worn_evidence, reported on #3022 and unanswered for four days, are drained: five killed by tests that pin behaviour the function already documents in a comment — an unreadable or column-less file must not end the search, a torn tail row is not a measured beat, and the evidence read names its encoding — and seven recorded as no-distinguishing-input with the argument for each. The survivor ledger's close path runs for the first time on real entries and the open count falls twelve to zero; the ratchet's own verdict object stops mis-reporting the answered case as examined-nothing. (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
tools/corpus-tier.mjs resolved ../verdict.js from dirname(import.meta.url), which is this file's
A counting helper's 0 could not be told from a scan that never saw the construct. generated_under_glob (MUTATION-SURVIVOR-LEDGER-2026-09-28-BRIEF.md)
The §3 crash refusal in tools/mutate_diff.py printed its prose and returned an exit code without
mutation-crawl.mjs ran 8 h 1 min under --max-hours 5 and was ended by its unit's 8 h
Re-score CVHR on the fold's night selection, and the sweep's number does not survive it.
Restore the 413 equivalence-ledger entries a bad three-way merge collapsed, and count entries per module so a silent drop cannot recur.
The monitor's fragment count now states **the threshold it counted above and the seconds it actually (NIGHT-0928-ERRORS-2026-09-29-BRIEF.md)
A refused finger-PPI CVHR leg now leaves the Integrator's bus as a refusal, with its reason, where
A mutation-gate deadline now reaps the mutmut process GROUP, and the results read is bounded and caught as NOT_RUN phase: results instead of a traceback that discards the record.
PAT coupling refuses times that are out of order, and names the stream, the inversion count and the first
PpgDex's mid-file clock-resync detector tested only FORWARD device-counter steps, so the 2026-09-28
Every PpgDex CVHR refusal now names which guard fired, and the reason reaches the export block the
The O2Ring dumps its buffer on reconnect, so the offset (hostElapsed − devElapsed) STEPS — three
The 14-night run stopped discarding PASS nights over a file written next to them. solid_night.history (NIGHT-0928-ERRORS-2026-09-29-BRIEF.md)
The SOLID-NIGHT completeness band stopped manufacturing a denominator. rate × span measures (SOLID-NIGHT-2026-09-23-BRIEF.md)
The cpap_stream detail line said "775 of 776 therapy min" beside a 6.75 h EDF — 776 min is 12.9 h,
Owner ruling 2026-10-03, "No, if declared": a pause the daemon itself declares, with its reason, inside (SOLID-NIGHT-2026-09-23-BRIEF.md)
A night becomes eligible for its loss verdict when its DEVICE DATA goes quiet, so the poller's own
A night's coverage bar is measured by the host stamps the file itself carries rather than by the samples that arrived: timeline._placed now prefers host_span_sec over rows / fs, which measures RECEIVED samples and shortened every dropping stream by exactly its losses — 28 minutes of real Verity ACC recording read as nothing on 2026-09-28. The file's own device clock (span_sec) still outranks both, rows / fs is still there for a file carrying neither, and no basis at all is still None rather than a zero. Three tests now observe offset_sec — its default, an explicit None and the sign of the addition — which nothing in the suite looked at before.
The capture box polls the O2Ring's live vitals on a fixed 1.000 s schedule. It used to sleep a fixed
The mutation gate's scratch answered a read differently from the checkout it is a copy of. root_reads (SOLID-NIGHT-2026-09-23-BRIEF.md)
The Vigil Nights page is readable again. nights_index's per-file cache was keyed on size + mtime
Night QC no longer reports an unreadable capture file as zero rows delivered: the row count is null, the stream is listed under unreadable (the file under unreadable_files), no row sum includes it, and the verdict reads UNKNOWN naming it — before, the stream landed in missing and the device was failed for sending nothing. The ABSENCE-SURVEY drain also gets its ratchet: npm run verify:absence holds the open-finding count equal to a committed ceiling (285 of 290 after this change) and emits one tepna.verdict/1. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The capture box stops publishing two absences as numbers: an unsized filesystem reports its free fraction as unknown (it read 0 %, a full disk), and the live monitor receives an irregular stream rate as 0 and an undeclared one as null instead of an invented 1 Hz. Open ABSENCE-SURVEY findings: 283 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
A resumed ECG recording whose original time anchor cannot be read back now leaves its relative-time column empty for the rest of the file instead of restarting it at 0.0 mid-file; the device-clock column still carries every sample time. Open ABSENCE-SURVEY findings: 282 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Five absences in the capture daemon stop reading as observations: a failed presence scan no longer ages a present ring to absent, Heart Rate samples no longer carry a fabricated device clock, an unread rate menu no longer erases the one the web page had seen, an unreadable auto-start record no longer re-arms an auto-start the operator stopped, and a failed MTU acquire is logged as an unknown MTU. Open ABSENCE-SURVEY findings: 276 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The SOLID-NIGHT timebase band and §A5 judged each device's completeness PRIMARY. For the O2Ring that is (SOLID-NIGHT-2026-09-23-BRIEF.md)
A waveform file with zero data rows is an ABSENT session, not an undecided one. validity iterated (SOLID-NIGHT-2026-09-23-BRIEF.md)
Six type errors that had drifted onto main are fixed with narrow annotations rather than a baseline move, so a branch cut from main no longer reads mypy=RISEN before it has changed anything. blind_spots._record is typed to the three node kinds its caller already guarantees instead of the base ast.AST that has none of the attributes it uses, and the isinstance test moves into the branch it protects because a narrowing cannot travel through a bool variable; its swallowed flag now carries the name it found, so the later attribute access and the guard that makes it safe are the same expression. ppg_grid_check keys max on a subscript rather than dict.get, which cannot return None for a key taken from that same dict. In capture.py a log label is typed as the label it is, and a tuple that is reassigned wide is annotated wide. Two of the six were introduced by the mutation-exclusion work itself, where a list[dict] was bound to a name already holding a list[str] in the same function.
The O2Ring's stream-position field is device-dead, and the consumer now says so by name. (PAT-HAT-RING-FLOOR-2026-10-04-BRIEF.md)
**Eleven of nightqc.summarize's forty-one open mutation survivors are killed, and the ratchet moves
Five absences on the capture box stop reading as observations: a live frame pushed before its rate is negotiated no longer carries the vendor default, an undatable CPAP reading reads unknown, a faulted ring probe is wear unknown rather than no finger, an O2Ring USB reply that does not decrypt is withheld instead of being written into the pulled file, and a one-channel CPAP batch no longer shifts flow against pressure in the EDF. Open ABSENCE-SURVEY findings: 270 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
When bluetoothctl does not answer, the capture box now treats a sensor's bond as unknown rather than lost, so it no longer forces a re-pair or spends a re-bond attempt on no evidence, and the adapter watchdog says BlueZ did not answer. A Bluetooth scan also ranks a 0 dBm reading as the strongest signal instead of treating it as absent. Open ABSENCE-SURVEY findings: 269 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
When the capture box cannot read its own clock state, the monitor's clock panel now says "unknown" instead of reporting network time as off and asking you to check the zone. Open ABSENCE-SURVEY findings: 268 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
A night seal the capture box cannot read is now reported as unknown for that night and left exactly as it is, instead of being silently replaced by a new seal under the current card key; re-sealing is an operator's decision. A seal missing its revision or key number is also unknown, never treated as 0. Open ABSENCE-SURVEY findings: 267 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The mutation gate now refuses a module it could not read or parse, and a mutant count it could not read back, instead of treating either as zero. A live CPAP recording is now marked complete only when every kind of data loss was actually measured; two kinds (stalls and post-drop tails) are not yet counted, so live CPAP nights read unknown completeness until they are. The Codex source export's self-test also reports why an export was refused instead of crashing in its cleanup. Open ABSENCE-SURVEY findings: 263 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Five hand-run device probes stop reporting an unread device as a device answer: an unread state is reported as unread rather than unchanged, an unanswered settings query is no longer "no settings", a clock test with no reading before the change says inconclusive, files with no size are counted rather than added as 0 bytes, and a truncated ring reply is no longer read as zero records. The Codex source export also creates its output directory. Open ABSENCE-SURVEY findings: 255 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The ring's 0x03 rate probe now publishes how many pulse-rate readings its average rests on, so an average of a dozen readings no longer looks like one of hundreds. Open ABSENCE-SURVEY findings: 254 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
A judge pass that examined no night reports NOT_RUN with null counts instead of three zeros, a replaced verdict is withdrawn with its reason instead of silently overwritten, and the night band's lower edge moves 20:00 to 18:00 by owner ruling so a recording that begins in the evening is one night. (NIGHT-IS-THE-RECORDING-2026-10-05-BRIEF.md)
The judge scopes a verdict to the recording rather than the folder: the folder set is resolved once and pooled, sessions are attributed by their start stamp, and the worn interval is truncated at the band edges so the completeness denominator describes one night. (NIGHT-IS-THE-RECORDING-2026-10-05-BRIEF.md)
The device-probe opcode-sweep tests no longer wait out real Bluetooth timeouts: a test setting that never reached the code it was meant to shorten is fixed, cutting that test file from about 7 minutes to under 2 seconds. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)