Repository navigation
Keep Android intelligence analysis JaCoCo-instrumentable - #1524
Conversation
|
Reviewed. This is a clean piece of work and I'd take it as-is. One cross-PR note at the end that's worth sequencing around. The extraction is a pure move, verified mechanicallyThe thing that matters for a 50 KB method being carved up is whether anything changed on the way out. I diffed removed against added lines, normalised for whitespace:
That is exactly what I wanted to see and it's rare — no smuggled edits, no reordering, no defaults quietly changed. Specifically:
The guard is well builtThe part I probed hardest was whether the budget test warns or just waits for the cliff. It warns: The ordering tests are the right complement: exactly one declaration, exactly one call, positioned after
Verified on the merged treeYour branch is based on
One note, and it's about your other PRThis adds a new one-sided Kotlin function — But that is precisely the shape your parity ledger in #1534 reports as Not a change request. Ready to merge from my side. |
…1626) A scoring pass walks a fixed recent window, so a day whose raw HR never arrived is re-read, re-skipped and re-logged on every pass, forever. The strap banks days rather than weeks of raw HR while an import can supply a much longer spine of daily rows, so for an importing user the steady state is a permanent block of un-scoreable days. In a field capture that came to 1262 lines — 21 days re-skipped across 63 passes in two hours, about a fifth of everything the log had to say. The strap log is a fixed-size rolling buffer, so this is not merely noise: it evicts the older lines an investigation needs. Collect the skipped days and emit ONE line per pass, grouped by sample count. Lossless — every day and its own HR count still prints, listed rather than reduced to a range, because a range would hide a gap and which days lack raw HR is exactly what an investigation is looking for. A day just under the floor still shows its number. Nothing skipped stays silent. The Android side carries a mechanical constraint worth recording. Putting the accumulator in a local blew the JVM method-size budget #1524 guards by 444 bytes, seven times the 64 bytes of headroom the method had — not because of the three added lines, but because analyzeRecentOnCpu is `suspend`, so one more local live across a loop full of suspension points is saved and restored at every one of them. Moving it to a field (safe for the same reason dayScanCache is a field: every pass runs under analyzeGate) and moving the null-check into the collector leaves the method 40 bytes under budget rather than 1. Swift keeps a local, and the declaration says why the two shapes differ so nobody aligns them. Swift also gains the named minHrSamples constant Kotlin already had, since this change would otherwise have added a second bare 200 literal on that side. Both platforms, byte-identical output, verified by diffing the two implementations across five cases rather than by eye. Verified: app-build green both legs; full com.noop.analytics suite (1685 tests) green; 5 JVM tests and a twinned Swift test in StrandAnalytics that default CI runs; #1524 budget test at 61495/61535; doc_comment_lint and i18n_audit clean.
The calibration fits one coefficient from sixty days of strap motion, and StepsMotionCache already stops it re-folding all sixty on every pass within a process. The cache was in-memory only, so a relaunch re-paid the whole fold: 32864ms cold against 3330ms warm on a worn 60-day library, once per launch. Persist it. dayMotionIntensity is a pure fold over one day's gravity stream, so unlike the day-scan cache there is no pass-config to carry: that one keys on dayScanCacheConfigSig, which folds in baselines1 and the habitual-sleep terms, and sleepConsistency (1-CV over 28 nights) and habitualMidsleepSec (a circular mean) both shift with any night moving, so a persisted day scan would invalidate wholesale on exactly the passes it would need to survive. Adds a fold version to the payload, because cacheKey witnesses the inputs only: a day whose gravity has not moved keys identically across an app update that changed the fold, and without the version check the pre-update volume would be served until that day's stream happened to change. Bump it on any change to dayMotionIntensity. The payload stays a derived cache that nothing else reads, so the whole failure surface is one re-fold. A payload that is missing, truncated, over-long or written by an older fold is discarded rather than repaired, and a malformed line costs only its own day. Storage is UserDefaults / SharedPreferences, not the record schema, so there is no migration. Android keeps the analytics layer Context-free by threading accessors down from the caller, mirroring manualStepCoefficient and persistStepsCalibration; they default to no-ops, so an unwired caller keeps today's per-process behaviour. Load and save sit in the analyzeRecent wrapper under analyzeGate, never in analyzeRecentOnCpu, so the #1524 method budget is untouched. The store is written only when the payload moved. A pass that reused every day renders the string already stored, and those are the back-to-back passes an offload storm is made of, so the write is skipped rather than relying on the store to discard it. The Android accessors are nullable and the one-shot load latches only when a store was actually consulted, so a caller that does not wire them cannot spend the single load on nothing and leave the rest of the process unpersisted. Both platforms pin the same rendered payload as a literal, so a one-sided change to the header, the separators or the number rendering fails a test rather than quietly ending reuse after an update.
The calibration fits one coefficient from sixty days of strap motion, and StepsMotionCache already stops it re-folding all sixty on every pass within a process. The cache was in-memory only, so a relaunch re-paid the whole fold: 32864ms cold against 3330ms warm on a worn 60-day library, once per launch. Persist it. dayMotionIntensity is a pure fold over one day's gravity stream, so unlike the day-scan cache there is no pass-config to carry: that one keys on dayScanCacheConfigSig, which folds in baselines1 and the habitual-sleep terms, and sleepConsistency (1-CV over 28 nights) and habitualMidsleepSec (a circular mean) both shift with any night moving, so a persisted day scan would invalidate wholesale on exactly the passes it would need to survive. Adds a fold version to the payload, because cacheKey witnesses the inputs only: a day whose gravity has not moved keys identically across an app update that changed the fold, and without the version check the pre-update volume would be served until that day's stream happened to change. Bump it on any change to dayMotionIntensity. The payload stays a derived cache that nothing else reads, so the whole failure surface is one re-fold. A payload that is missing, truncated, over-long or written by an older fold is discarded rather than repaired, and a malformed line costs only its own day. Storage is UserDefaults / SharedPreferences, not the record schema, so there is no migration. Android keeps the analytics layer Context-free by threading accessors down from the caller, mirroring manualStepCoefficient and persistStepsCalibration; they default to no-ops, so an unwired caller keeps today's per-process behaviour. Load and save sit in the analyzeRecent wrapper under analyzeGate, never in analyzeRecentOnCpu, so the #1524 method budget is untouched. The store is written only when the payload moved. A pass that reused every day renders the string already stored, and those are the back-to-back passes an offload storm is made of, so the write is skipped rather than relying on the store to discard it. The Android accessors are nullable and the one-shot load latches only when a store was actually consulted, so a caller that does not wire them cannot spend the single load on nothing and leave the rest of the process unpersisted. Both platforms pin the same rendered payload as a literal, so a one-sided change to the header, the separators or the number rendering fails a test rather than quietly ending reuse after an update.
StepsMotionCache stopped the calibration re-folding sixty days of strap motion on every pass, but only within a process, so a relaunch re-paid the whole fold: 32864ms cold against 3330ms warm on a worn 60-day library. dayMotionIntensity is a pure fold over one day's gravity, so unlike the day-scan cache there is no pass-config to carry across the process boundary. That one keys on dayScanCacheConfigSig, which folds in baselines1 and the habitual-sleep terms, and sleepConsistency and habitualMidsleepSec both shift with any night moving, so a persisted day scan would invalidate wholesale on exactly the passes it would need to survive. A fold version rides in the payload header because cacheKey witnesses the inputs only: a day whose gravity has not moved keys identically across an app update that changed the maths. The payload stays a derived cache nothing else reads, so the whole failure surface is one re-fold, and it deliberately stays out of both BackupSettings whitelists because it describes gravity rows as they were on one device. Storage is UserDefaults / SharedPreferences, not the record schema, so there is no migration. The store is written only when the payload moved, and Android's accessors are nullable so a caller that does not wire them cannot spend the one-shot load on nothing. Load and save sit in the analyzeRecent wrapper, never in analyzeRecentOnCpu, so the #1524 method budget is untouched. Both platforms pin the same rendered payload as a literal, and rendering is a fixpoint over a payload this build wrote.
…for how far tonight's data can be trusted (ryanbr#15/ryanbr#27) Composes the honesty signals that were scattered across consumers into ONE per-night figure: stream coverage, PPG-derived-HR fill share (ryanbr#156), gravity span sparsity and >20-min dropouts (the ryanbr#345 sparse test), and off-wrist seconds inside the bed window (ryanbr#500/ryanbr#504). Channels a device never carries (thermal/SpO2 on WHOOP 4.0) are ABSENT from the weighted mean, never scored zero - a 4.0 night is not punished for hardware it does not own; absent components renormalize the weights (the Charge nil-renorm idiom). Persisted as shadow keys night_trust / night_wear_frac under the computed source: instrumentation only, never shown, never fed to any score, accruing on real devices so the trust surface and later alert gating ship on measured data. Wiring respects the ryanbr#1524 JaCoCo per-method budget: analyzeRecentOnCpu measured 57,412 instrumented bytes inline against its 55,700 ceiling, so the census (recordNightTrust/nightTrustCensus), the six per-night shadow-row appends (shadowMetricRows/nightTrustShadowRows), the cache-hit replay (replayCachedNight), the cache store (storeCachedNightScan) and the sleep-diag line (sleepDayDiagLine) are lifted into named helpers; the census call site is one statement. Gates: NightlyTrustTest 13/13 (clean night, off-wrist, PPG-fill, gravity gap/sparse, device-without-thermal renorm, degenerate inputs); IntelligenceEngineJacocoBudgetTest 4/4; full com.noop.analytics.* suite green; doc_comment_lint OK; i18n audit: Android section clean (no new hardcoded copy) - its iOS xcstrings ru/zh-Hant FAILs are pre-existing at HEAD and this diff touches android/ + CHANGELOG.md only.
Summary
Verification
Fixes bhelm#102