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
A new SOLID-NIGHT band, rtc: the device's onboard RTC judged against the host clock that disciplines (SOLID-NIGHT-RTC-2026-10-04-BRIEF.md)
E12's timer half: a tepna-trio-fold unit pair that folds the box nights the corpus does not hold yet, (NIGHT-0928-ERRORS-2026-09-29-BRIEF.md)
A second reader for mutation survivors, on exactly the form Codex was given, after Codex's free tier was exhausted. The reader is handed a survivor list and returns, per mutant, one concrete distinguishing input or an equivalence argument citing line numbers — never test code, and it decides nothing: every claim is verified against original and mutant by hand before any test is written. The key is read from the environment or from a 0600 file the owner places, and the tool refuses with a named reason when neither exists or when that file is group- or world-readable; the value is never logged and is stripped from every error text, including stack traces, because an exception that interpolates a request URL is the leak. Gemini has no working directory, which is the one place this cannot copy Codex literally: Codex read the export itself under a read-only sandbox, so for an HTTP reader the source must be sent instead, and the corpus boundary moves from the sandbox to this tool's refusal to read a byte from anywhere except an export built by tools/codex-export.mjs — verified by its marker files and by the absence of the data trees, so that a plain checkout, which would carry uploads/, is refused rather than read. (GEMINI-SECOND-READER-2026-10-05-BRIEF.md)
The capture host records its UTC offset per session in STARTS.csv, so a reader no longer has to infer (SESSION-SPAN-WRITER-OFFSET-2026-09-28-BRIEF.md)
A gate asserts that every registry id has a DECLARED binding — emitted by name, a *_DEFS entry with its get: accessor, or an explicit dormant: true — with the 163 ids that have none published as a ratcheting baseline, so a new phantom id reds instead of silently carrying a tier.
commit-shape refuses a commit that adds a 0-byte file at the repo root — the spaced-path trap that splits Data Unifier.html — with the three historical instances declared as a shrink-only baseline, and deletes the 0-byte Unifier.html that had been on main since 2026-09-24.
Add four CI gates: PR body + changeset compliance (Rule 0 line, Fleet-Session trailer, corpus sentence on computeHash movers), absence-regression (diff-based, fails on new tMs/|| 0 fabrications), xss-sink-gate (diff-based, fails on new unescaped innerHTML sinks), docs-constants (CLAUDE.md numeric claims match code). The rebuild-consistency gate (#1) already existed via build.mjs --check.
Make the capture box a stratum-1 clock on its own u-blox M10 GPS, the LAN's standard way: a PPS line-discipline (VIGIL-GPS-STRATUM1-2026-10-10-BRIEF.md)
Add a measured hybrid-search trial: tools/xerj-load.mjs writes doc-search's existing chunks and cached bge-m3 vectors into a xerj index, doc-search.mjs --hybrid queries it and prints the enclosing passage (Rule 0's default path unchanged), and tools/xerj-bench.mjs scores it against doc-search on 52 real past Rule-0 queries — 94.2 % vs 82.7 % hit@5 with 16x fewer bytes to an answer.
Give the two diff-scoped scanners a --full-tree mode (base = the empty tree) with a shrink-only backlog ledger and a nightly timer, never a PR gate — the first reading found 42 files with an unescaped innerHTML sink and 1 absence fabrication, and the CodeQL control exposed a scanner defect that had been hiding multi-line assignments as safe.
Add three user units so the local hybrid-search lane survives a reboot — a loopback-bound index server with Restart=on-failure, plus a reload service and timer that follow the hourly re-embed at :20 rather than racing it — with a setup doc the owner enables from; restart survival is measured (54,180 documents in, 54,180 served after a kill, and a kNN query afterwards to prove the vectors persisted and not just the row count).
Gate the evidence-tier vocabulary where §🎫's rule actually binds — on the 523 evidence: grades in the eight node registries, which nothing asserted — rather than on identifier names, where a gate would fire on correct code.
tools/usage-guard.mjs — what the fleet has spent, measured from each session's own transcript and each API reader run, and what no program here can read, said as UNKNOWN. (USAGE-GUARD-2026-10-11-BRIEF.md)
An anthropic provider row in the second-reader, and a truncation bug it would have inherited.
A text-network gap detector over the briefs — built, measured, SHORTFALL. (BRIEF-GAPS-2026-10-11-BRIEF.md)
The untimed-group ratchet, and three CI comments that had drifted ~2x.
Changed
Record 35 OxyDex ABSENCE-SURVEY findings (12 DSP, 23 render) as already fixed by the PRs that landed after the survey, each traced to its commit; open findings: 217 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The capture-host wiring scanner counts names in one pass instead of one search per name, giving identical results five times faster; the capture-host test suite runs about 30 % faster. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
AirSense 11 pairing follows the device's documented pairing sequence: one chosen radio, a Find CPAP scan, 30 s connect and reply bounds, the pre-notify reads, and a post-pair session check on a fresh link.
The four #3368 CI gates each emit one tepna.verdict/1 and carry a --selftest with a planted violation and its controls, and all four are binned decides/adopted — replacing three word-only/exempt rows whose stated reason (the exit code is the machine-readable result) the verdict contract rejects, and adding xss-sink-gate, which was not binned at all.
doc-search.mjs answers from the hybrid lane by default, falling back to the semantic lane when the index does not answer. (XERJ-HYBRID-SEARCH-2026-10-10-BRIEF.md)
Measure the O2Ring not-worn pull against the ring's awake tail: it fires at +50 s on every night (PASS, 7/7); the one miss was a BlueZ refusal with no in-window retry. (OXYII-DAT-AUTO-HARVEST-REFINEMENT-2026-08-24-BRIEF.md)
Other projects' names out of the tree, and a gate that keeps them out.
Fixed
The night timeline finds the ring's accelerometer again, and stops dropping the head of a session that began before midnight. A configured stream can legitimately be written under more than one file tag — acc arrives as _ACC. from the chest strap and _ACCRAW. from the ring — and the timeline compared the file tag against the stream name upper-cased, so a 10 MB _ACCRAW.txt matched nothing and that device's whole night was painted idle, which reads as a finding rather than a miss. The tag set now comes from nightqc.stream_file_tags, which already owned that mapping and its reasoning, rather than a second copy that could drift from it; a bare string is still accepted, so every existing caller is unchanged. Separately, the gate that decides whether to pool the previous day's folder was exclusive at its upper bound while merge_sessions — the authority on what one session is — measures the same question from the running coverage's end, which a session crossing midnight pushes past 00:00. A night opening exactly one session gap after midnight was therefore refused, and a measured 3669 s continuous session published as a 9 s window with no error and no refusal, reading as a short recording rather than a truncated one. The bound is now inclusive. That closes the boundary and not the class — a previous day running to 01:30 still merges with a 02:00 session this gate never looks for — so the remaining narrowness is recorded as an open residue row rather than described as fixed.
The mutation gate no longer instructs you to delete a correct equivalence claim because a contended run lost track of its mutant. REFUTED — the one verdict here that destroys work, since it tells a reader the recorded claim is false — was derived from ABSENCE: a mutant that mutmut generated and that did not come back a survivor was called killed. A reaped worker, a partial run and a shard that never reported all look exactly like that. Worse, mutmut's own status map records pytest's INTERNAL ERROR as a kill (3: "killed" # internal error in pytest means a kill), and contention produces that exit on a tree whose every test passes — so a loaded run could report entries refuted with no timeouts to show for it, while a quiet run on the same tree reported the same mutants surviving. Three things change. Absence can no longer reach refuted by any path: membership in the survivors and in the kills are both positive observations and are tested as such, and a mutant the run never settled is reported as unmeasured with the run reading UNKNOWN rather than FAIL. A kill is read from the raw pytest exit code rather than mutmut's status word, so only "tests ran and failed" counts as a test having observed the mutant, and anything else — including an exit code nobody has met yet — is not a detection. And a refutation must be corroborated by re-running that one mutant in isolation, with the re-run judged for load on the same terms, because it meets the same contention and "not a survivor on the second try" is not agreement. A real distinguishing input still reaches refuted, which is what keeps the gate able to report a stale claim at all.
The ring's opcode-sweep probe now marks an opcode's effect as unverified, and stops, when it cannot read the ring back after sending it, instead of recording "nothing changed". Its tests may no longer start a real process. Open ABSENCE-SURVEY findings: 253 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
The Polar opcode-sweep probe no longer reports "nothing left running" when its final status read goes unanswered; it reports that the state is unknown and sends no stop command blind. Open ABSENCE-SURVEY findings: 252 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
baevskySI refuses ambiguous AMo inputs with a reason: amo50>100 (impossible percent) and amo50<=1 (fraction-or-percent ambiguous) return null instead of a 100x-wrong SI.
timeline.bucket_stream no longer takes a sample rate it never used. It buckets intervals against a window, and the rate never entered the arithmetic, so every caller had to invent a value and every reader had to check whether it mattered; the signature now says what the function actually needs. The same change puts the whole function in the mutation gate's scope for the first time, and the drain that followed pinned the arithmetic underneath it: the one-bucket case, the bucket edges, the summation of several intervals inside one bucket, and both state thresholds at their exact boundaries — a dropping link writes two fragments in a bucket, and keeping only the last reported every reconnecting stream as worse than it was.
CI shard balance is measured on the shardable remainder with the one indivisible group excluded and proven to run alone, the committed group timings cover every declared group instead of 60 percent of them, and the Playwright chromium build is cached on its pinned version.
The pairwise clock fit stops reallocating its delta buffer per offset and stops holding a polymorphic null in its inner loop, so the fitClockClosure suite group runs 56 times faster with every answer byte-identical. (CLOCK-FIT-SEARCH-COST-2026-10-05-BRIEF.md)
The capture-host CPAP poller tests no longer try to reach the CPAP card over the network: they stub the card probe and fail if any test opens a real connection, which also removes about two minutes of waiting from each run of that test file. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
A recording that spans two night folders has its gap rows merged by file identity and the seam between the folders judged with the loss audit's own delay-versus-loss test, so the gap no per-file scan can see is no longer missing from continuity. (NIGHT-IS-THE-RECORDING-2026-10-05-BRIEF.md)
Stop fabricating tMs: 0 (1970-01-01) on Integrator findings with unknown time; timeless findings now carry tMs: null and render/export as null.
Null SpO2 is absence, not 0: shared measuredSpO2 helper; detector, hypoxic dose, T88/T85, desaturation profile, and SpO2 advanced metrics skip null explicitly; baseline histogram ignores nulls.
The one place the night timeline compares a file tag now accepts a SET of them, so one configured stream can be written under more than one tag. acc arrives as _ACC. from the chest strap and _ACCRAW. from the ring, and timeline._placed compared the file tag against a single string — so the ring's 10,137,042-byte _ACCRAW.txt matched nothing and that device's whole night was painted idle, which is the one state that reads as a finding rather than a miss. A bare string is still accepted and behaves exactly as before, so every existing caller is unchanged; this lands the capability on its own, ahead of the callers that will hand it a tag set, because until it exists a caller passing one skips every file in the night rather than failing.
Quantity.add propagates null: a null operand yields a null Quantity, never the surviving operand (fixes JS null+5===5 fabrication).
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)
The two public readers of a stream's session files now say in their signatures what _placed already accepts: a file tag, or a set of them. stream_intervals and unmeasurable_files pass the tag straight through, so their behaviour is unchanged and this is the type and the documentation catching up with the capability — but it is not cosmetic, because until a caller's signature admits a tag set, handing one over is a type error the ratcheting mypy baseline refuses. The reasoning lives on stream_intervals, where a caller reads it: the set is nightqc.stream_file_tags's and never a second copy, because two copies of that rule is how the timeline and the night QC would come to disagree about one night.
The Verity survey probe's tests no longer wait out its real 6-second reply timeout, cutting that test file from about 42 seconds to under one; they also pin how the probe writes to the sensor's control point. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop OxyDex's importer and CSV parser from turning an absent value into 0, and name an assumed 1 Hz gap span (new export fields dataGaps.gapSpanSource and tIdx basis tIdxBasis); open ABSENCE-SURVEY findings: 211 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop OxyDex's readiness, stability, stage-proxy and summary scores and its OK flag from scoring an unmeasured input, and name an assumed age (new export fields vo2est.ageAssumed, karv.scores.sleepMax and flag NOT_FULLY_ASSESSED); open ABSENCE-SURVEY findings: 201 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop ten OxyDex derived metrics from reporting an in-band default over an unmeasured input, and say how much of the sympathetic-surge index was measured (new export field ssi.ssiBasis); open ABSENCE-SURVEY findings: 191 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop OxyDex's ECG fusion from reading an unparseable stamp, a window with no duration, or an event with no depth as a measured 0, and record why the night window's pairing anchor stays; open ABSENCE-SURVEY findings: 179 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop the OxyDex profile from reading an empty elevation as sea level or a missing RMSSD as a measured adjustment, and make the training-zone chips and the resting-HR note report one source; open ABSENCE-SURVEY findings: 176 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
Stop OxyDex's review header, 7-day periodic-breathing chart, AAI tile, T-index rows and periodic-breathing verdict from drawing an unmeasured night as a 0 or a verdict; open ABSENCE-SURVEY findings: 186 of 290. (ABSENCE-SURVEY-2026-09-22-BRIEF.md)
ECGDex reports an unmeasured RMSSD as null with a named reason instead of 0, so lnrmssd no longer exports -Infinity and the firmware-comparison ratios refuse an absent or zero reference instead of publishing Infinity.
Three aggregates in oxydex-dsp.js read an absent sample as a number: the CDI state machine (null < loThresh is always true for SpO2, so a dropout manufactured a cyclical-desaturation index) and both spectral windows, where reduce summed nulls as 0 and v - m turned each into a -m impulse — publishing LF/HF power from a constant heart rate and a respiration rate read off the gap (7.8 to 8.4 bpm, peak power 0 to 14.2). The other three sites of the original six were fixed by #3321 while this waited behind it; their assertions remain as regression guards.
Six ECGDex registry ids are renamed to the names the code actually emits (beatsNN→nBeats, qrs→qrsDur, rAmp→Ramp, tAmp→Tamp, ventRuns→runsGE3, bigeminy→bigeminyCycles), so a cross-node lookup on the old name can no longer resolve to a tier for a quantity nothing produces; the browser test lane now carries every *-registry.js source.
OxyDex's rmssdArc declares the unit it actually emits — bpm/30min, the slope of a pulse-rate RMSSD over 30-minute windows — instead of ms/h, which was wrong in both the numerator and the denominator; the DSP is unchanged and the declaration is now pinned by a known answer.
A desaturation still open when the recording ends observed no resaturation, so its recSlope is null with the reason record-ended instead of 0, and meanRecSlope averages the measured recoveries with meanRecSlopeN beside it. A flat terminal desat used to publish recSlope 0 — a recovery nobody watched — and one such event dragged a true 0.35 mean to 0.175.
CLAUDE.md §🔒.7: the drawn-axis share is 67 % (CK_AXIS_DRAWN_SHARE in clock.js), not 99 %. The code has said 0.67 since the measured separation was recorded; the doc was stale.
condPctBelow94 divided a measured numerator by the full sample count, so absences inflated the denominator and the percentage under-reported (5.6 against a true 6.3). computeGatedNadir compared spo2 against Infinity, where null is less than Infinity, so one absence made the nadir null — and the guard missed it because the global isFinite(null) is true.
Registry backlog adjudication — systematic resolution of the 2026-10-06 projection findings. Two renames (correction→correctionRate, meanSqi→meanSQI) to match DSP-emitted names; five dormant markings with adjudication reasons (crCoupling duplicate of crcPLV, cmpResidSD nested path, activityCounts/sqiConf/recovIndex no compute site).
Gap uncertainty and safeguard quality (2026-10-08):
Follow-up review fixes (2026-10-08):
Review fixes (2026-10-08): six items from the safeguards review.
Reference-doc reconciliation: align three documented values with their code definitions.
mypy ratchet to zero (2026-10-09):
The mutation crawl now recovers a leftover mutant at its own startup and on every catchable exit, and refuses to start against a tree behind origin/main. The recovery was extracted to one shared module rather than copied, because mutate.mjs already had a working one that the crawl never reached.
The Nights page groups a row by the NIGHT BAND the recording begins in, not by the calendar folder its (NIGHT-IS-THE-RECORDING-2026-10-05-BRIEF.md)
Correct papers/timestamp-pathology.html, which stated a modal share of 99 % as the shipped drawn-axis rule when clock.js ships 0.67, and extend tools/docs-constants.mjs to assert that constant against every paper — distinguishing a shipped-rule claim from the legitimate mentions of the rejected 99 % detector, so the papers stop sitting outside the gate that pins CLAUDE.md.
PpgDex: suppress DFA α1 and sample entropy across internal RR gaps.
Adjudicate three of the five ids #3369 had recorded as "dormant" inside the resolvability gate's not-yet-adjudicated baseline — two get a declared nested binding (emittedAt, validated by walking the path on a committed export) and crCoupling's alias is re-pointed to crcPLV with the duplicate id removed — and fix two defects in that gate: a label-order-dependent extractor that hid 24 of 522 ids, and a dormant-entry window that could read a neighbour's flag.
Count what ran in browser-lane's selftest instead of printing a literal 13 that the lane aggregator reads as the tool's assertion count — demonstrated by planting a 14th assertion, which the fix reports as 14 and the literal still reported as 13.
Clamp a terminal desaturation's endIdx to a sample that exists in CPAPDex — it was one past the end, so spo2[endIdx] read undefined and any arithmetic on it was NaN — and publish terminal beside it, because the clamp alone would make "no resaturation was observed" indistinguishable from "it resaturated on the last sample".
Make geometry-scan exit 2 when a directory cannot be scanned instead of printing ERROR: and returning 0, emit one tepna.verdict/1 whose NOT_RUN says nothing was examined, name the no-ECG-file skip so it is excluded from the population rather than counted as a clean scan, and re-bin its now-false word-only adoption row.
Stop GlucoDex publishing a 5-minute cadence nothing measured: a daily or spot export now reports cadence: null with a named reason and a separately-named assumed grid step, and every minute total derived from that rate (activeMin, gapMin, warmupMin, compMin, durDays, longRec) reports the absence instead of a plausible number.
Teach the registry-id resolvability gate the d_<id> render-key convention its own regex could not see, plus six render keys the convention does not derive (each confirmed by its LABEL, which refuted a seventh), taking the hrvdex baseline from 25 unbound ids to 2.
Remove the last startMs || 0 fabrication in the Integrator — the one site a completed sweep's verification grep could not see because it searched for the shape it had just eliminated — and anchor that sweep's gate on the fabrication rather than on the syntax surrounding the instances already fixed.
capture-host/check.sh reads mypy's clean summary line ("Success: no issues found in N source files")
Write the literal NUL and EDF+ TAL control bytes in two tools as escape sequences so the sources are plain text rather than data (runtime output proven byte-identical both ways), and replace the analysis-tool selftest step's found >= 10 floor — which saw 156 — with a grow-only ratchet that fails when the count DROPS.
Carry the O2Ring's '- -' no-reading rows as spo2/hr null instead of deleting them, so coverage can report an absence the parser used to erase — and make ODI's denominator the measured count explicitly, which it had only equalled by accident, so the published Table 1 figures (17.7 and 33.1 /h) reproduce unchanged.
Stop the OxyDex CSV printing the literal text null for absent metrics and stop it inventing a 0 bpm spike rise out of null - null: absence is an empty field via a new csvNum, and the rise is computed only when both operands are present.
Stop an absent second counting as time below every SpO2 threshold and as a bradycardia event — null < 90 and null < 40 are both TRUE in JS, which #3408 made reachable by carrying absent rows — and mint the first OxyDex golden over an input with absent seconds, so this class is visible to every future PR.
A second module-level mutation exclusion, for webmon.py, authorized by the owner on 2026-10-11: its (MUTATION-SCOPED-GENERATION-2026-09-28-BRIEF.md)
A passing PpgDex assertion described the opposite of what correctRR does.
A verdict path that never executed under a green selftest.
Two residue rows whose fixes had landed, and the gap that let them sit OPEN.
Security
2026-10-05 — No-network gate fail-closed (§3)
Escape untrusted filenames, file bytes, and envelope strings at innerHTML sinks in OxyDex, PulseDex, and Integrator; delete the oxydex-fusion content-sniffed HTML bypass; route pulsedex-render to the canonical dex-escape.js; rename safeSet to setIfPresent.
Escape untrusted strings at three ATTRIBUTE sinks: OverDex's local escaper now delegates to the canonical dex-escape.js one (it omitted the quote, so a folder name closed a title="…"), and OxyDex's night-detail aria-label and flag pill escape n.date, f.sev and f.code.
scrubExport now reduces a per-element schema.provenance, not only the top-level one. A multi-record element carries a full v2.0 envelope, so with scrub ON every per-element input name and sha256 survived — against the SELF-INGEST §5 acceptance that a scrubbed JSON contains no device serial, filename or input sha256. (DEEP-AUDIT-VI-2026-09-01-BRIEF.md)