Skip to content

Test day-cache invalidation for late-arriving streams - #103

Closed
bhelm wants to merge 3 commits into
mainfrom
feature/noop-night-fork-ci
Closed

bhelm wants to merge 3 commits into
mainfrom
feature/noop-night-fork-ci

Conversation

@bhelm

@bhelm bhelm commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Purpose

Draft CI test for #29 on this fork's existing main. Only the day-cache fix is ported; this does not synchronize the fork with upstream.

The per-day reuse key previously watched HR rows only. Late-arriving scored streams could change a night's result without invalidating that key. The fix adds a device/window-scoped count/max witness for the other consumed streams, including sleep state, on Swift and Kotlin. It preserves cache reuse for unchanged inputs and avoids bypassing on the default force=true path.

Verification

On the upstream-based candidate: actual HRV-changing red reproduction before the fix; 9 targeted Swift cache/oracle tests, 2 targeted Swift store tests and 8 Kotlin cache tests passed. Independent source review found the missing sleep-state arm; that small correction has a further targeted Swift red/green test. The final Kotlin SQL delta and this older fork integration have now passed this PR's CI. Existing results are not claimed as green for a different fork revision.

Fork adaptation preserves the key/store tests. The newer end-to-end StreamWitness test is omitted here because its hrOnlySessions API does not exist on this older fork; its original reproduction remains part of the upstream candidate. No missing upstream feature was imported just to make this test compile.

Expected CI: Swift Packages, Android, Apple app compile, source hygiene and i18n. No device verification claimed. No local full-suite reruns; this PR is intended to obtain the missing platform/integration evidence through CI.

Fork CI follow-up

The first fork run passed all Swift packages and both Apple app builds. Android compiled and ran 4,512 tests, with only the existing JaCoCo method-size budget failing. The exact fork base had passed that test; the newer upstream candidate also passed Android CI.

The fork-only follow-up extracts the same two cache-key reads into one private suspend helper in IntelligenceEngine.kt, reducing the large caller's coroutine body without changing the key or weakening the budget. Existing tests remain the regression gate. No local build or full-suite rerun was performed; the new CI run has now passed all 16 checks, including the existing JaCoCo method-size budget and both Apple app builds. Prior green checks cover unchanged relevant content but are not presented as newly executed at this commit.

…HR alone

A history offload does not commit its channels together — HR can land first and
R-R, respiration or SpO2 minutes later — and an offloaded HR row duplicating a
live one is dropped on conflict. So a banked night could be scored once from HR
alone and then gain its R-R with the night's HR (count, maxTs) completely
unmoved. The per-day reuse key was HR-only, so it said "reuse" and re-served the
HRV-less scan for the rest of the session, force refreshes included: force only
bypasses the whole-pass watermark gate, never the day cache.

The whole-pass gate had already been widened to every raw stream
(analysisFingerprint v2). This is the same widening at day granularity, which is
where the reuse decision is actually made: a new per-day, per-owner witness over
ppgHrSample, rrInterval, respSample, spo2Sample, gravitySample, stepSample,
skinTempSample and event, folded into the key.

ppgHrSample is in it because the day's HR read is measured-union-PPG while the
key's fingerprint counted hrSample only, so a PPG-derived second changed the
scored series without moving the key — the same defect, second instance.

COUNT(*) is the load-bearing half: it moves when a backfill lands rows inside a
window already covered, which MAX(ts) alone misses. Every arm is an index range
walk over the same (deviceId, ts) keys the HR fingerprint uses, materialising no
rows, against the 7 full stream reads a hit skips. rrInterval is filtered exactly
as the R-R read filters, so the witness counts the beats actually scored.

The alternative the report also listed — let force bypass the day cache — was
rejected: force defaults to true and every real update path uses the default, so
it would disable the cache on essentially every pass.

Ported onto this fork (behind upstream): the upstream commit's whole-pass
`analysisFingerprint`/`ANALYSIS_FINGERPRINT_SQL` widening is not present here, so only the
per-day witness is taken and the doc lines that named it were reworded to describe the
whole-pass gate generically. The upstream integration test
AnalyzeRecentDayCacheStreamWitnessTests.swift is left out: it drives
SleepStager.hrOnlySessions, which this fork does not have. The key contract is still pinned by
AnalyzeRecentDayCacheTests / AnalyzeRecentDayCacheTest and the witness by ReadTests
testDayStreamFingerprint*.

Not verified locally: nothing was built or run for this port beyond Tools/doc_comment_lint.py
(clean) and static checks that every cacheKey caller, table, column and RR read filter it relies
on exists on this fork. CI on the pull request is the evidence.
analyzeDay is handed nine streams and the day witness covered eight. The
missing one is the band's own sleep_state, and it sits on exactly the
mechanism this witness exists for: HR and sleep_state are appended from the
SAME v18 record at the same ts. When a re-offloaded HR row duplicates a live
one it is dropped on conflict while the sleepStateSample row is new — HR
count and maxTs unmoved, the day key unmoved, and the night re-served though
its re-onset confirm and persisted per-session band state change. The
whole-pass gate already sees it; the day key defeated it. Same defect as the
per-day witness itself, one stream later.

One COUNT(*) + COALESCE(MAX(ts), 0) arm per platform, letter `b` (band) —
`s` is spent on the version prefix `s1|`. Same cost profile as the other
arms: an index range walk over the (deviceId, ts) primary key.

The regression loop in ReadTests committed six of the eight streams, so
ppgHrSample and stepSample had no test at all: an arm reading a NEIGHBOURING
table is valid SQL, ships, and is invisible. All nine streams are committed
now.

One doc claim corrected: WhoopDao said Room and a plain-JVM SQLite test run
the same statement — the day SQL carries binds no parameterless harness can
execute, so Room's KSP verification is what checks it.

Ported onto this fork from the original-branch delta. The upstream commit's
comment-only edit to AnalyzeRecentDayCacheStreamWitnessTests.swift does not
apply: that test is not on this fork (it drives SleepStager.hrOnlySessions,
which this fork does not have). The corrected WhoopDao wording is written
without naming ANALYSIS_FINGERPRINT_SQL, which this fork also does not have.

Not verified locally: nothing was built or run for this port beyond
Tools/doc_comment_lint.py (clean) and source checks that sleepStateSample,
PpgHrSample(ts:bpm:conf:), StepSample(ts:counter:) and
SleepStateSample(ts:state:) exist on this fork in both the GRDB and the Room
schema. The red/green run belongs to the original branch. CI on the pull
request is the evidence here.
The per-day cache key gained a second suspend store read (dayStreamFingerprint)
alongside hrFingerprintWindow. Each suspension point enlarges analyzeRecentOnCpu's
single coroutine state machine, and the Jacoco-instrumented method crossed the
61,535-byte budget IntelligenceEngineJacocoBudgetTest asserts.

Move the key assembly into a private suspend helper so both reads live in their
own state machine. The call site keeps one awaited call in the same position, with
the same read order, window, arguments and miss fallback, and cacheKey itself is
untouched, so the key string stays byte-identical.
@bhelm bhelm closed this Sep 15, 2026
@bhelm
bhelm deleted the feature/noop-night-fork-ci branch September 15, 2026 22:38
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