Repository navigation
Description
This is a meta-issue: the platform drift behind #5–#16 is systemic, and fixing findings one at a time will not converge. This lays out a drift-control plan for this fork, built as small single-concern pieces so the useful parts can be offered upstream.
All references verified against f2271cd (current main).
The problem is structural, not a pile of one-off bugs
The parity model (docs/CROSS_PLATFORM.md: "parity, not shared code" — a hand-ported Kotlin mirror kept honest by discipline) has no machine behind the discipline. Twin tests pin each platform's own interpretation, so both suites stay green while the implementations drift apart. Evidence that this is systemic:
- One function, three independent divergences. Rolling RMSSD ([P1] N-05: Rolling RMSSD splices across rejected beats; Swift/Kotlin windows drift #9): window boundary — Swift keeps
[t−w,t](HRVAnalyzer.swift:362), Kotlin uses(t−w,t](HrvAnalyzer.kt:715-719); cleaning scope — Swift cleans per window (HRVAnalyzer.swift:365-369), Kotlin cleans the whole series once (HrvAnalyzer.kt:695-708); plus Kotlin's value-based timestamp rematching (HrvAnalyzer.kt:700-708). The Kotlin comment says "matching the Swift twin" — over code that demonstrably doesn't. - A one-sided function whose comment claims a twin.
HRVAnalyzer.swift:304documents "Kotlin twin:HrvAnalyzer.sdnnIndex" — that function does not exist;sdnnIndex/avgSdnnhave zero matches underandroid/. - Perfect parity on the wrong function. Both platforms wire production to the ungated resting-HR path and leave the hardened one test-only ([P1] N-08: Production resting-HR path has no artifact gate — a single-sample bin can set daily RHR #10) — in lockstep, and both wrong. No amount of cross-platform comparison can see this class; only a callsite inventory can.
- Drift where Kotlin is right and Swift silently loses data.
backfillWorkoutrebuildsWorkoutRowand dropssteps([P1] backfillWorkout rebuilds WorkoutRow and drops stored steps — silent data loss + Kotlin parity drift #16,WorkoutDetector.swift:378-387); the Kotlin twin usesreal.copy(...)and is correct (IntelligenceEngine.kt:1739-1744). - Fix-once-miss-the-copies has already happened. The day-zone fix landed in one copy while its duplicate stayed broken ([P2] N-06: Daily windows use one UTC offset and fixed 86,400 s days (DST/travel drift) #11). The tree currently holds ≥8
dayStringimplementations and ≥4 independent Pearson correlations in Android UI files alone (CompareScreen.kt:345,LabBookScreen.kt:750,MindSection.kt:309,InsightsScreen.kt:1712). - The surface is large. Counting same-name function declarations in mirrored file pairs across
Packages/{StrandAnalytics,StrandImport,WhoopStore}↔com.noop.{analytics,ingest,data}: ~95 file pairs, ~650 twin function pairs. Nothing machine-checks any of them.
Each fixed finding is O(1); the drift source (every twin-touching commit) is continuous. Without a system, this issue tracker refills itself.
The plan (this fork; small single-concern PRs)
Design principle: every tool is a local command first (matching Tools/doc_comment_lint.py + baseline idiom); CI wiring is fork-side convenience, since Actions is deliberately off upstream (docs/SAFEGUARDS.md).
- Twin ledger lint — a stdlib-Python check over both trees: every declared twin reference in source comments must resolve on the other side (the tree already carries hundreds of such annotations); constants pair up value-for-value; "has test callsites but no production callsite" is an error (catches the [P1] N-08: Production resting-HR path has no artifact gate — a single-sample bin can set daily RHR #10 class); duplicate/callsite counters ratchet against a baseline (catches the [P2] N-06: Daily windows use one UTC offset and fixed 86,400 s days (DST/travel drift) #11 class and the Pearson copies).
- Targeted fixes, one PR each — [P1] backfillWorkout rebuilds WorkoutRow and drops stored steps — silent data loss + Kotlin parity drift #16, the missing
avgSdnndecision, [P1] N-08: Production resting-HR path has no artifact gate — a single-sample bin can set daily RHR #10 wiring, each with a test that fails before and passes after. - Linux enablement (~10 lines) — two
#if canImportgates (RawOutbox.swift/BackupSettings.isBoolean) plus oneXCTExpectFailuretest gate. Verified locally: with a snapshot-enabled SQLite,Packages/StrandAnalyticsbuilds on Linux and all 1,424 tests pass. Standalone benefit regardless of the rest: Linux contributors can finally runswift teston the analytics package. - Differential harness — with both implementations runnable on one Linux machine, feed them the same inputs (curated edge cases + seeded random generators) and compare outputs directly. The implementations become each other's expected values: no golden-file curation, no macOS requirement. Sharpness is proven, not assumed: side-specific sabotage runs must go red, and the harness must reproduce the three known RMSSD divergences before any of them is fixed.
- RMSSD convention decisions — pick one semantics per divergence, align, prove via the harness (red → green → red on revert).
- Coverage ratchet — every twin function is differential-covered, platform-test-covered (with executed-line proof from coverage data, not a label), or explicitly exempted with a reason; exemption lists compare against the merge-base so they cannot be widened in the same PR that violates them. Armed module-by-module, never all at once.
- Rollout — module by module (HRV → strain → recovery → sleep), up to whole-pipeline
analyzeDaydifferential runs (both sides are pure functions with mirrored signatures:AnalyticsEngine.swift:12,AnalyticsEngine.kt:239).
Honest limits, stated up front: a direct comparison cannot see bugs both platforms share (that stays with independently-derived oracle cases), and Linux-Swift ↔ JVM is a proxy for the shipped Darwin/ART pair (mitigated, not eliminated, by occasional macOS runs).
Upstream intent
cc @ryanbr — no action needed on this issue; flagging it early so nothing lands as a surprise, and early feedback can still shape scope and conventions. The idea is to offer pieces upstream in digestible, single-concern order, each independently useful and each a local command (no CI required):
- Bug-fix PRs first (from step 2/5 above) — smallest possible diffs, each with its red-before-fix test.
- The twin ledger lint — same shape as the existing
doc_comment_lint.py; it mechanizes the value-for-value discipline thatdocs/CROSS_PLATFORM.mdalready promises. - The Linux gates — ~10 lines, Darwin behavior untouched.
- The differential harness only after a dedicated coordination issue, with the evidence table of what it caught by then — adopt or decline as you see fit.
Enforcement (required checks, workflow gating) stays on this fork unless there's appetite for it upstream.
Branch plan & checklist
Integration model: every work branch is cut from main (synced to upstream f40897a) so it stays clean for a later upstream PR; each merges into drift/meta (the integration branch) where everything is tested together. Fork PRs target drift/meta; upstream PRs are made later from the individual branches. Dependencies: 04 builds on 03; 05 builds on 04 (its red-proof needs the harness); 06+ integrate on drift/meta.
-
drift/meta— integration branch (created first, collects everything) -
drift/01-twin-ledger— twin ledger lint + baselines (plan step 1) -
drift/02a-fix-backfill-steps— [P1] backfillWorkout rebuilds WorkoutRow and drops stored steps — silent data loss + Kotlin parity drift #16backfillWorkoutdropssteps(plan step 2) -
drift/02b-avgsdnn— resolve the phantomsdnnIndex/avgSdnntwin: port or correct the claim (plan step 2) -
drift/02c-fix-restinghr-wiring— [P1] N-08: Production resting-HR path has no artifact gate — a single-sample bin can set daily RHR #10 production uses the ungated resting-HR path (plan step 2) -
drift/03-linux-enablement—#ifgates + CI toolchain notes; StrandAnalytics builds & tests on Linux (plan step 3) -
drift/04-differential-harness— runners + comparator + generators + sabotage proofs; must reproduce the three RMSSD divergences (plan step 4) -
drift/05-rmssd-alignment— convention decisions + alignment, proven red → green → red-on-revert (plan step 5) -
drift/06-coverage-ratchet— coverage-proven ratchet with merge-base comparison, armed module-by-module (plan step 6) -
drift/07-rollout-<module>— one branch per module (hrv, strain, recovery, sleep, pipeline) as coverage expands (plan step 7)
Upstream offering happens per branch, in the order described above (fixes → lint → Linux gates → harness after coordination), each re-based cleanly because no work branch contains another branch's commits except where declared.
Activity
bhelm commentedon Aug 18, 2026
Progress: three branches are merged into
drift/meta(each with an independent read-only Codex review before merge, findings folded in):drift/03-linux-enablement(drift/03: StrandAnalytics builds and tests on Linux #19) — StrandAnalytics builds and tests on Linux: 1424 tests, 1 skipped, 0 failures. Known limit: the standalone WhoopStore test target references the gated raw-outbox API and doesn't compile on Linux (possible follow-up: gate those three test files).drift/02a-fix-backfill-steps(drift/02a: backfillWorkout no longer drops stored steps #20) — fixes [P1] backfillWorkout rebuilds WorkoutRow and drops stored steps — silent data loss + Kotlin parity drift #16; red-before-fix, integrated suite ondrift/meta: 1426 tests, 0 failures.drift/02b-avgsdnn(drift/02b: retire the phantom sdnnIndex twin claim #21) — retires the phantomsdnnIndextwin claim; the port-or-declare decision is avgSdnn/sdnnIndex: Swift-only metric whose doc comment claims a nonexistent Kotlin twin #18.drift/01-twin-ledgeris built and already earning its keep — on today's tree it finds, beyond the known items (thesdnnIndexdead reference, the test-only hardened resting-HR path on both platforms, 4 duplicate Android-UI Pearson implementations, 12dayStringimplementations), two previously unreported constant drifts:SleepStagerV2padLo/padHiare 330/390 in Swift vs 360/420 in Kotlin. Its own review round returned findings on the ledger's parser coverage (expression constants like48 * 3_600, trailing-comma arity, extension receivers, computed properties, CI path filters, inventory scope excludingWhoopProtocol); a fix round is running before it merges — the PR follows once it's green again.Android toolchain on the build host is verified:
testFullDebugUnitTest= 3974 tests, 0 failures, 5 skipped (baseline for02cand the harness work). Next up: ledger fix round, thendrift/02c-fix-restinghr-wiringanddrift/04-differential-harness.bhelm commentedon Aug 18, 2026
Progress, part 2:
drift/01-twin-ledger(drift/01: the twin-parity ledger lint #23) is merged. The first review round (independent Codex, verdict red) found real parser/enforcement gaps — expression constants read as their first literal, trailing-comma arity, extension receivers, computed properties, CI path filters missing the scanned sources, inventory scope excludingWhoopProtocol, baseline identities without value fingerprints. All eleven findings were fixed; the focused re-review came back green with zero findings. On the integrated branch the ledger now reports 3 baselined findings as disappeared (thesdnnIndexdead reference retired by02b, the test-only resting-HR path wired by02c) — the improve/ratchet semantics working on real history.drift/04-differential-harness(drift/04: differential parity pilot — the twins test each other #24) is merged. The pilot reproduces all three known RMSSD divergences as machine diffs (SUMMARY cases=14 diffs=3) withtrimpToStraingreen as control — verified end-to-end with the real Gradle runner on both negative-side sabotage proofs as well. Hardening from its review round: a parity run can no longer be up-to-date-skipped or served from Gradle's build cache, and a half-set env contract is a hard error instead of a silent skip. Honest limit, stated in the PR: output equality ≠ proof that production code ran — that attestation is step 6's coverage ratchet.drift/05-rmssd-alignmentis building now (conventions: adopt(t−w,t], per-window cleaning, drop value rematching; proven red → green → red-on-revert through the pilot).drift/06-coverage-ratchetfollows.Ledger: calls that omit defaulted parameters count as callsites (#17)
Ledger: calls that omit defaulted parameters count as callsites (#17)
Completeness ratchet with executed-lines coverage proof (#17)
bhelm commentedon Aug 18, 2026
Final progress for this build cycle — every planned branch (steps 1–6) is built, independently reviewed, merged into
drift/meta, and measured on the integrated tree: #19 (Linux enablement), #20 (#16 steps fix), #21 (#18 phantom twin), #22 (#10 resting-HR wiring), #23 (twin ledger), #24 (differential harness), #25 (RMSSD alignment), #26 (coverage ratchet), plusdrift/01b-callsite-defaults(ledger callsite matching learns defaulted parameters; merged directly, its PR had no delta left).Integrated state of
drift/meta:testFullDebugUnitTest3977 / 0 / 6.(t−w,t], per-window cleaning, no rematching; red → green → red-on-revert).verifygreen with fresh real coverage from both runners.The fund table this cycle (all machine-found or machine-confirmed):
stepsdata loss (#16, fixed), phantomsdnnIndextwin (#18, decision pending), production resting HR ungated on both platforms (#10, fixed — behavior change spelled out in #22), 3 RMSSD divergences (#9, fixed), 2 previously unreported constant drifts (SleepStagerV2padLo/padHi330/390 vs 360/420 — fix PR still to come), the genuinely test-only API surface now inventoried in the ledger baseline (e.g.BatteryPackInfo.decode, theWhoopStorehighwater family), and the documented one-sidedwindowSecdefault.Still open, deliberately:
drift/07-rollout-<module>(module-by-module expansion as coverage grows), the padLo/padHi fix, the #18 decision, the WhoopStore-test-target Linux gates, and every upstream offering — each of those waits for its own go, per the branch plan above.bhelm commentedon Aug 18, 2026
drift/06b merged (#27,
373b75d): the six confirmed tooling findings from the independentreview of the integrated state are fixed (pair-scoped differential registry, CI llvm-cov
derivation + twin path filter, baseline
issueround-trip,stale-constant-pairrule, lexermultiline-signature/constructor edges, CLAUDE.md Linux note), plus three findings from the
branch's own review round (resolved-symlink llvm-cov with fail-fast, declaration-aware
continuation scan, multi-pair counter-case tests).
Re-measured on the merged state:
Tools/tests70/70, ledger exit 0 (274 baselined, 0 new),ratchet/verifyexit 0 against real llvm-cov + JaCoCo artifacts.Reliability proof of the detection itself, run today on this state: differential pilot 14/14
cases bit-identical (0 diffs); sabotage runs
PARITY_NEGATIVE_SIDE=swiftand=kotlineachreported exactly the mutated
trimpToStrainprobe as an exact-class bit diff; coverage proofgreen with real artifacts. The pipeline detects, localizes, and survives its own negative
controls.
bhelm commentedon Aug 18, 2026
drift/07 rollout package 1 (HRV) merged (#33,
0e25d11): 17 additional pure HRV twinsare differential (registrations 2 → 19, exempt 2719 → 2685), each with curated boundary
cases + seeded generators. Native result: 121/121 cases bit-identical, 0 diffs — the HRV
module is drift-free after #25.
Tooling hardening on the new surface, per the acceptance catalog: two source mutants
(Kotlin
rangeFilterlower bound, Swift ectopic threshold — both §4.4-style boundarymutations) were killed precisely by their curated boundary cases, flagging exactly the
mutated function plus its registered caller; the coverage negative probe (stale artifacts)
goes red with 26 errors; verify is green against fresh llvm-cov + JaCoCo artifacts. One
ratchet fix fell out of this: file-unique method records outside the lexer extent
(Kotlin expression bodies) now match, ambiguous ones stay fail-closed.
Known limitation, carried forward:
analyze/analyzeRaw(and with them theMIN_BEATSgate),median, andHRVResult.emptyare not uniquely registrable under thename-only registry. Next tooling increment: arity/signature-qualified registry keys, then
the 19/20-beat boundary case. Next module per plan order: strain.
bhelm commentedon Aug 18, 2026
drift/07b merged (#34, registry increment): differential registry keys may now be
arity-qualified (
name/arity); name-only entries keep their semantics, ambiguous hitsstay fail-closed, malformed labels are explicit errors. First use:
analyze/3isregistered on both sides — the documented
MIN_BEATSgate is machine-compared(registrations 19 → 20, exempt 2685 → 2683). Native: 125/125 cases bit-identical;
MIN_BEATS mutant killed precisely by the new 20-beat curated case; coverage verify green.
Remaining named gaps: cross-name mapping (
analyze(rawRR:)↔analyzeRaw),median(same-name same-arity overloads). Rollout can proceed with strain as the next module.
bhelm commentedon Aug 18, 2026
drift/07c — registry mapping complete
PR #35 has been merged into
drift/meta.What changed
HRVAnalyzer.analyze/2=HrvAnalyzer.analyzeRaw/2.HRVAnalyzer.median/1=HrvAnalyzer.median/1.Verification
differential=22 platform-test=0 exempt=2679 errors=0.mediansource mutant was detected with four targeted diffs and then fully reverted.Merge commit:
d190e5ebhelm commentedon Aug 18, 2026
Upstream rollout — parity-backed bug fixes
Four independently scoped fixes have now been proposed upstream:
BreathStageduration parity only; the remaining findings stay out of scopeEach PR contains deterministic regression coverage and requires no manual device or strap testing. Fork issues will remain open until the corresponding upstream changes merge; #9 and #32 additionally retain unresolved out-of-scope findings.
bhelm commentedon Aug 19, 2026
Rollout update: Strain and Recovery
Two more parity slices are now merged into
drift/meta:The current ratchet covers 44 differential functions. Fresh Swift and Kotlin coverage passed, stale coverage was rejected, and targeted mutants were detected on both platforms.
Findings
trimpToStrainlogarithm-boundary divergence.effectiveEffort.Next
Continue Recovery as separate one-concern slices: Charge Drivers, Forecast, Watch Recovery, and workout Heart Rate Recovery. Sleep and the named-adapter
analyzeDaypipeline rollout follow after those foundations are green.bhelm commentedon Aug 19, 2026
Recovery rollout update
The next Recovery slices are merged into
drift/meta:Both slices passed fresh Swift/Kotlin coverage, stale-output rejection, targeted mutants on both sides, and independent review.
Findings and fixes
bhelm commentedon Aug 19, 2026
Recovery complete; Sleep rollout started
Recovery is now covered as separate merged slices:
All slices passed fresh Swift/Kotlin coverage, stale-output rejection, targeted mutants on both sides, and independent review. Shared behavior issues #55, #61, and #62 remain open because parity characterizes them but does not decide or fix their product semantics.
Sleep Wave S1 is also merged in #65: seven foundational helpers, 414/414 native cases with zero differences. Exact differential fixtures now complete #41 and #42, which are closed.
Next: SleepStageTotals (Wave S2), then the public SleepStager/cache surface (Wave S3). The final named-adapter
analyzeDaypipeline rollout follows after Sleep is green.bhelm commentedon Aug 19, 2026
SleepStager S3 rollout complete
Merged #73 into
drift/metaas76a7cc11eb74d0f0f2f72e5def7c3ae00b745cd2.Differential surface
SleepStager.detectSleep/10SleepStager.stageSession/6SleepStager.sessionEpochMotion/3SleepStager.sessionEpochSleepState/3SleepStager.remFunnelDiagnostic/6SleepStager.hypnogramMetrics/1SleepStagerV2.stageSession/6Exactly seven exemptions were removed per platform and seven twin mappings were added. The rollout is test/harness-only; it preserves the centralized upstream
StreamFingerprint.gravityQuantimplementation and its existing platform-test governance.Evidence
CI:
fastandcoverageboth passed.