SleepPSG: sync RecipeConfig.shipped with #987's awake row (main is red) - #997
Merged
Conversation
#987 changed SleepStagerV2's awake transition row; #991's port-equivalence guard landed before it and correctly failed on main: RecipeConfig.shipped no longer reproduces SleepStagerV2 — 195 of 12377 epoch labels differ. This is the guard working, not a defect in it. #991 merged AFTER #987 was opened, so #987's own green CI never ran the SleepPSG job — its branch predates the tool. MERGEABLE CLEAN only ever meant no textual conflict, and there was none: the two files never touch. The coupling is semantic, which is exactly the kind a test has to catch because review will not. RecipePort's own comment already called this out — the awake row "is the one line that legitimately differs between branches" and RecipeConfig.shipped "must always describe SleepStagerV2 AS COMPILED on this branch". So the fix is the single line it predicted, and Variants.pr987 needs no edit: it reads the shipped row and offers whichever one is not there, so the #987 ablation keeps working from the other side.
ryanbr
added a commit
that referenced
this pull request
Aug 15, 2026
…ent (#997 twin) (#1346) Swift already avoids re-reading each calendar day's HR/steps/gravity from the store during analyzeRecent's per-day scan: for a PAST day the night window it already read [dayStart-30h, nextMidnight] spans the day, so it slices the day streams out of the in-memory night lists (AnalyticsEngine.daySliceFromNight, #997). Android had no twin and re-read all three streams per day — ~60 redundant store reads per pass (including the big ~86k-row HR ones), every ~15 min while a strap is connected. Port daySliceFromNight to the Kotlin AnalyticsEngine (next to offWristIntervals, mirroring the Swift placement) and wire the three day-read sites to slice-then- fall-back. Safe by construction: the guard declines (-> direct read) for TODAY (day runs past the 18h night cap) and for a limit-truncated night read, so it can only ever skip work, never change data. Same owner, same inclusive bounds, same ts-ASC order, same range-independent HR coalesce -> byte-identical to the direct read. Also correct the Swift doc/test comments that named a Kotlin `IntelligenceEngine.daySliceFromNight` twin: it now exists, as `AnalyticsEngine.daySliceFromNight`. Adds AnalyticsEngineDaySliceTest, the twin of the macOS DaySliceFromNightTests (same bounds fixture). compileFullDebugKotlin + the new test + every analyzeRecent-exercising suite (day-owner, edit-scope, repeat-repro) green.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
… is red) (ryanbr#997) ryanbr#987 changed SleepStagerV2's awake transition row; ryanbr#991's port-equivalence guard landed before it and correctly failed on main: RecipeConfig.shipped no longer reproduces SleepStagerV2 — 195 of 12377 epoch labels differ. This is the guard working, not a defect in it. ryanbr#991 merged AFTER ryanbr#987 was opened, so ryanbr#987's own green CI never ran the SleepPSG job — its branch predates the tool. MERGEABLE CLEAN only ever meant no textual conflict, and there was none: the two files never touch. The coupling is semantic, which is exactly the kind a test has to catch because review will not. RecipePort's own comment already called this out — the awake row "is the one line that legitimately differs between branches" and RecipeConfig.shipped "must always describe SleepStagerV2 AS COMPILED on this branch". So the fix is the single line it predicted, and Variants.pr987 needs no edit: it reads the shipped row and offers whichever one is not there, so the ryanbr#987 ablation keeps working from the other side.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ent (ryanbr#997 twin) (ryanbr#1346) Swift already avoids re-reading each calendar day's HR/steps/gravity from the store during analyzeRecent's per-day scan: for a PAST day the night window it already read [dayStart-30h, nextMidnight] spans the day, so it slices the day streams out of the in-memory night lists (AnalyticsEngine.daySliceFromNight, ryanbr#997). Android had no twin and re-read all three streams per day — ~60 redundant store reads per pass (including the big ~86k-row HR ones), every ~15 min while a strap is connected. Port daySliceFromNight to the Kotlin AnalyticsEngine (next to offWristIntervals, mirroring the Swift placement) and wire the three day-read sites to slice-then- fall-back. Safe by construction: the guard declines (-> direct read) for TODAY (day runs past the 18h night cap) and for a limit-truncated night read, so it can only ever skip work, never change data. Same owner, same inclusive bounds, same ts-ASC order, same range-independent HR coalesce -> byte-identical to the direct read. Also correct the Swift doc/test comments that named a Kotlin `IntelligenceEngine.daySliceFromNight` twin: it now exists, as `AnalyticsEngine.daySliceFromNight`. Adds AnalyticsEngineDaySliceTest, the twin of the macOS DaySliceFromNightTests (same bounds fixture). compileFullDebugKotlin + the new test + every analyzeRecent-exercising suite (day-owner, edit-scope, repeat-repro) green.
ryanbr
added a commit
that referenced
this pull request
Aug 23, 2026
…ads (#1559) Pricing the "make the pass cheaper" option on the re-score work got as far as static analysis can and then hit a wall. Instrumentation only: no behaviour change, no scoring change, one new diagnostic line per pass. What the static analysis settled. Each day reads a 54-hour night window (dayStart-30h to dayStart+24h) on a 24-hour stride, so consecutive windows overlap by 30 hours and every row is materialised about 2.25 times per pass -- 1134 window-hours read against 534 distinct, so 53% of the read volume is redundant. Nine windowed store reads per day survive; the day-window reads (dayHr/daySteps/dayGrav) were already sliced out of the night lists by #997/#1346, so that part is done and should not be re-done. What it could not settle: whether reads or analyzeDay dominate. The pass has only ever timed itself end to end, so ~22.6 s per night is an undifferentiated number, and that single ratio decides whether narrowing the windows is worth building. The design this unlocks, and the one it rules out. Reading the whole 21-day span once and slicing does NOT work -- it would hold ~1.8M HR rows in memory, and OOM on big-import libraries is a known failure of this exact path. A sliding window would: the loop already walks newest-to-oldest, so it could read only the incremental 24 hours per step and drop the tail, keeping today's peak memory while materialising 2.25x fewer rows. Only worth building if reads dominate. So: time the two phases. `prep` brackets the nine windowed reads plus the session matching between them; `score` brackets analyzeDay. They deliberately do not sum to the pass total -- pass 2, the baseline folds and the reconciliation are outside this loop -- so the line reads as a RATIO, which is all the question needs. prep >> score means build the sliding window, ceiling ~53% of read time; score >> prep means narrowing windows is a dead end whatever the row counts suggest. Accounting verified complete on both platforms: the cache-hit continue precedes the prep timer, so a reused day contributes nothing to either phase; the only other early exit is the too-few-samples guard, which still records its read time because it paid for it; and analyzeDay is a single expression with the add on the next line, so nothing can exit between capture and accounting. Both platforms emit a byte-identical string and both truncate rather than round. Known limitation, recorded rather than hidden: Kotlin times with the monotonic System.nanoTime() while Swift uses Date(), matching how the pass total has measured itself since #1344. A clock step mid-pass would skew the Swift figure. The exposure is pre-existing and consistent within that file. Verified: Android CI (assembleFullDebug + testFullDebugUnitTest) green; local Android suite 4229 tests, 0 failures matching main; app-build green on both Strand macOS and NOOPiOS; doc lint clean. Not run on hardware, which is the entire point -- this exists so a real device can answer a question a desk cannot. Partial work on the re-score performance issue; #1538 stays open.
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.
main is failing
Swift Packages CI / tools (SleepPSG)after #987. This is the one-line fix.What happened
#987 changed
SleepStagerV2's awake transition row. #991's port-equivalence guard — merged before it — caught the divergence immediately:This is the guard doing precisely what it was built for, on its first real occasion, and it named both the cause and the file to fix. Without it the tool would have gone on printing confident numbers about a recipe that no longer existed — which is the failure mode #991's description called out.
Why it slipped through
#991 merged after #987 was opened, so #987's branch predates the tool and its 13/13 green never included a
tools (SleepPSG)job.MERGEABLE CLEANonly ever meant no textual conflict, and there genuinely was none — the two files never touch. The coupling is semantic, which is exactly the kind review does not catch and a test does.Worth noting for future merges: a green PR check plus a clean merge state does not imply main stays green when a guard has landed in between.
The fix
One line, and
RecipePort.swift's own comment predicted it:Variants.pr987needs no edit. It reads the shipped row and offers whichever one is absent:so the #987 ablation keeps working from the other side, comparing post-#987 against pre-#987 instead of the reverse.
Verification
Port row and
SleepStagerV2.swift:205now agree character-for-character:["deep": 0.0, "rem": 0.0, "light": 0.10, "awake": 0.90].swiftc -parseclean. The real check istools (SleepPSG)on this PR — it replays all 55 corpus nights against the compiled stager, so it passes only if the port genuinely reproduces it again.