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 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:304 documents "Kotlin twin: HrvAnalyzer.sdnnIndex" — that function does not exist; sdnnIndex/avgSdnn have zero matches under android/.
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 dayString implementations 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).
Linux enablement (~10 lines) — two #if canImport gates (RawOutbox.swift / BackupSettings.isBoolean) plus one XCTExpectFailure test 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 run swift test on 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 analyzeDay differential 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 that docs/CROSS_PLATFORM.md already 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/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.
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(currentmain).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:
[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.HRVAnalyzer.swift:304documents "Kotlin twin:HrvAnalyzer.sdnnIndex" — that function does not exist;sdnnIndex/avgSdnnhave zero matches underandroid/.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).dayStringimplementations and ≥4 independent Pearson correlations in Android UI files alone (CompareScreen.kt:345,LabBookScreen.kt:750,MindSection.kt:309,InsightsScreen.kt:1712).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).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.#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.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):
doc_comment_lint.py; it mechanizes the value-for-value discipline thatdocs/CROSS_PLATFORM.mdalready promises.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 upstreamf40897a) so it stays clean for a later upstream PR; each merges intodrift/meta(the integration branch) where everything is tested together. Fork PRs targetdrift/meta; upstream PRs are made later from the individual branches. Dependencies:04builds on03;05builds on04(its red-proof needs the harness);06+ integrate ondrift/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.