Let the SpO2 promote gate read a NOOP app store, so the app can answer its own question (#845, #103) - #942
Conversation
…apture (#845, #103) #845 asked for the `spo2_candidate_82` instrumentation because "the app itself cannot currently contribute the data needed to resolve its own open question, only the offline tooling can". Half of that is now fixed and half was not: #848 banks the raw @82 byte into `v18AuxSample`, so the value IS retained — but `validate_spo2_candidate.py` could only read a capture.json or the CAPTURE TOOLING's frame store, and its own docstring said so: NOTE: this is the CAPTURE TOOLING's database, not the app's. Neither shipped app has a `frames` table [...] so this cannot be pointed at a phone's store. So a NOOP user who synced a strap held the evidence #103 has been waiting on and had no way to run the gate over it. Only someone running linux-capture could contribute. This closes that. The harness now accepts a third input kind: the app's own SQLite, read from `v18AuxSample` joined to `sleepStateSample.state` for the asleep gate. An app store is told apart from a capture store by the `v18AuxSample` table rather than by extension or a flag, so a user does not need to know which file they hold. Opened `mode=ro&immutable=1` — no WAL recovery write against a file pulled off a phone, the same reasoning Tools/SleepBench documents. `unpack_v18_aux` is a Python port of Swift `V18AuxCodec.unpack`. It is pinned against the SHIPPED codec rather than round-tripped against itself: both test fixtures are literal `V18AuxCodec.pack` output captured from `Packages/WhoopStore`, so a wire-format change fails the test instead of silently mis-slicing real rows into plausible-looking numbers. ONE THING AN APP STORE CANNOT DO, reported rather than papered over. The app banks decoded slots, not frame bytes, so there is no neighbour byte to rank @82 against and the OFFSET SPECIFICITY SCAN cannot run. Recording `offset_82_wins: false` there would fail the gate for a reason that is about the input format, and "@82 did not win" is precisely the wrong thing to record about a byte that was never ranked — so it is n/a, with `source` and `specificity_scan` naming which of the two happened. A capture is still required to settle specificity. `byte_at_offset` returns None (not 0) for unknowable offsets, because 0 is a legitimate @82 reading outside the duty window and "cannot know" read as "read zero" would manufacture out-of-band samples and depress coverage. Nothing about the promote gate is relaxed: no card, no score, no `spo2Pct`, and the multi-device thresholds are untouched. This only widens who can produce the evidence. tools/linux-capture: 50 tests, 0 failures (39 before, 11 new).
ryanbr
left a comment
There was a problem hiding this comment.
Ran the suite rather than take the number on trust — 50 tests, all pass. Checked the file can't write to
anyone's phone database too: both opens are mode=ro&immutable=1, and there is no INSERT, UPDATE, DELETE
or commit anywhere in it. Refusing to guess when a store holds two straps is right, and so is reporting
the specificity scan as "not applicable" instead of "failed" — a byte that was never ranked didn't lose.
One thing, and it isn't yours: nothing in CI runs these tests. No workflow touches
tools/linux-capture, so those 50 tests only ever run when someone remembers to. Same gap as #935.
Not a reason to hold this up, but it's the second PR this week adding good tests that nothing will
execute again.
Approving.
Closes the
spo2_candidate_82item from #845 — the one asked for twice and called "the strongest item here". Unblocks #103.What the ask turned out to be, precisely
The framing in #845 was "the app itself cannot currently contribute the data needed to resolve its own open question, only the offline tooling can." That was two problems, and only one of them is still open.
The persistence half is already done. #848 (merged
2026-07-27T05:50:10Z) banks the raw @82 byte intov18AuxSample. The value is retained — the strap trimming its history no longer destroys it. That landed 89 minutes after the "still open" note on #845, so the note was accurate when written and has quietly stopped being so.The read-back half was not.
validate_spo2_candidate.pycould read acapture.jsonor the capture tooling's frame store, and nothing else. Its own docstring says why:So a NOOP user who synced a 5/MG was holding the evidence #103 has been waiting on with no way to run the gate over it. Contributing still required running linux-capture. That is the actual blocker, and it is what this PR removes.
What it does
A third input kind: the app's own SQLite, read from
v18AuxSamplejoined tosleepStateSample.statefor the asleep gate.v18AuxSampletable, not by extension or a flag — a user does not have to know which kind of file they were handed.mode=ro&immutable=1, so no WAL recovery write is attempted against a file pulled off a phone (the reasoningTools/SleepBenchalready documents).--app-devicepicks a strap. Required when the store holds several: pooling straps would average away the very per-device difference the promote gate exists to test, so it refuses rather than guessing.The one thing an app store cannot do — reported, not papered over
The app banks decoded slots, not frame bytes, so there is no neighbour byte to rank @82 against and the offset specificity scan cannot run.
Recording
offset_82_wins: falsethere would fail the gate for a reason that is about the input format rather than the data — and "@82 did not win the scan" is precisely the wrong thing to record about a byte that was never ranked. It is n/a, andsource/specificity_scanname which of the two happened so a batch pooling both kinds can tell them apart without reopening the files. A capture is still required to settle specificity. Correlation, MAE, bias, duty-cycle detection and window coverage all work from either.Related:
byte_at_offsetreturnsNone, not0, for an unknowable offset.0is a legitimate @82 reading outside the duty window, so "cannot know" read as "read zero" would manufacture out-of-band samples and silently depress coverage.The codec is pinned against the shipped one, not against itself
unpack_v18_auxis a Python port of SwiftV18AuxCodec.unpack. Round-tripping it against a Python packer would prove nothing, so both test fixtures are literalV18AuxCodec.packoutput captured by running the real codec inPackages/WhoopStore:A wire-format change now fails the test rather than silently mis-slicing real rows into plausible-looking numbers. Absence stays first-class — a clear presence bit means "the strap did not report this", never
0.Not relaxed
No card, no score, no
spo2Pct, no change to any promote threshold, no schema change, no app change, nothing on the BLE path. This only widens who can produce the evidence.Verified
tools/linux-capture: 50 tests, 0 failures (39 before, 11 new) —python3 -m unittest test_validate_spo2_candidate. The new tests cover the codec against the Swift fixtures, malformed/truncated blobs, the multi-strap refusal, out-of-band values, that only @82 is answerable without a frame, an end-to-endvalidate_devicerun over a synthetic app store, and — in the other direction — that a real capture still runs the specificity scan.Corpus credit stays with @digitalerdude, whose capture is behind the duty-cycle figures this harness relies on.