Repository navigation
docs: refresh stale READMEs (android/README.md + root README architecture) - #779
Merged
Merged
Conversation
…unbuilt draft android/README.md was the initial-port README and had drifted badly: - '`/path/to/NOOP`' placeholder never filled in (intro + build cmd) -> Strand/. - 'This project has NOT been compiled or run on a device' / 'authored without a JDK or Android SDK' -> false: it's a shipped, CI-built app running on real devices (e.g. the WHOOP 4.0 users in the tracker). - Gradle 8.5 -> 8.7 (matches the wrapper); WHOOP 5.0/MG now shipped/verified. - assembleDebug/installDebug + app-debug.apk path -> the flavored assembleFullDebug/installFullDebug + app-full-debug.apk. - Double-Strand path (Strand/Strand/BLE/...) -> Strand/BLE/BLEManager.swift. - Package layout + screen names (Live/History/Support) brought current. Kept the still-accurate BLE contract table + hardware-verification checklist; reframed 'must be validated' as 'new BLE changes need hardware' (it IS validated).
Owner
Author
|
Expanded this PR to also fix two stale items in the root
Verified-accurate and left untouched: |
taylor-cao
pushed a commit
to taylor-cao/noop
that referenced
this pull request
Jul 28, 2026
…calibration count (ryanbr#530) * Liquid Today: carry Charge like Rest, honest state pill, battery-ring truth table (B3/A3/B2/B1) Liquid Today is the DEFAULT screen (iOS + macOS), but the v8 rebuild dropped carry-overs the classic TodayView has. On an unscored-today day (post-04:00 rollover, or a no-wear night) the Charge and Effort heroes read `displayDay?.recovery`/`.strain` RAW and blanked, while the Rest hero (freshRestScore) and vitals (Repository.lastVitalsDay) carried right beside them — and the Charge pill still said "Calibrating" at a wearer 14 nights past the seed gate. Liquid was the only surface that blanked; classic carries via lastScoredCharge (ryanbr#543), widget/watch/Live Activity via widgetAnchor (ryanbr#911), Android via TodayScreen.lastScoredRecoveryDay. B3: ChargeDisplay.resolve carries the last scored night's REAL Charge, stamped "Last night · <date>" (or "Latest sleep" once ryanbr#779-stale) via the SAME TodayView.lastScoredRecoveryDay + carriedCaption the classic screen uses, so the two Today screens can't drift. The state pill tells the truth — only a genuinely calibrating baseline reads "Calibrating"; a trusted baseline with no scored night reads "No data". The Key-Metrics Recovery tile reads the same resolved value so tile and hero can't disagree. Effort deliberately does NOT carry: it is today's accumulation, so yesterday's number would be a false statement, not a stale one. A3/B2: the strap-battery ring resolves through StrapBatteryDisplay so a known charging state survives a missing %, and a % never outlives its link. B1: LiquidSyncStatusRow surfaces backfill progress in the Data Sources card. Adds a DEBUG-only `--demo-screen liquidtoday` (plain "today" renders classic). Tests: LiquidChargeCarryTests (9), LiquidBatteryDisplayTests. App-target Swift, so no CI covers it — verified the full macOS StrandTests suite green, the iOS target builds, and drove it in the simulator on the real on-device DB with today's row nulled: before = Charge "–"/pill "Calibrating"; after = Charge 47 carried from Jul 14/pill "Last night". * Liquid Today: surface the "N of 4 nights" calibration count The v8 Liquid Today (the default iOS Today) showed a bare "Calibrating" for a pre-seed-gate baseline, dropping the "N of 4 nights" progress the classic TodayView surfaces (calibrationDetail / ScoreStatePill). A brand-new user in their first 4 nights saw the progress on classic but not on Liquid. ChargeDisplay already carried the count (.calibrating(nights:), 9251ae2); this adds ChargeDisplay.calibrationDetail returning classic's exact copy ("Learning your baseline, N of 4 nights.") and swaps it into the synthesis card in place of the readiness one-liner (chargeDisplay.calibrationDetail ?? synthLine) — the same replacement classic makes (calibrationDetail ?? synthesisCardDetail). The compact greeting pill stays short ("Calibrating") per its fixedSize row, so the count lives in the card. Reuses classic's String Catalog key verbatim; both Today screens now read identically. New-users-only, no owner impact. Android TodayScreen already surfaces this, so no Kotlin twin is needed — iOS Liquid catching up to existing behaviour. Verified (app-target Swift, not covered by CI): LiquidChargeCarryTests (test-first, RED→GREEN, 11/11) on the resolver/label; macOS Strand + iOS NOOPiOS app targets compile; --demo-screen liquidtoday on a clean sim renders "Learning your baseline, 0 of 4 nights." in the synthesis card with the short "Calibrating" pill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
vipulog
pushed a commit
to vipulog/noop
that referenced
this pull request
Jul 31, 2026
…ghts (ryanbr#401) * Baselines: imported rows with nil values no longer shadow computed nights The nightly-into-history merge only checked KEY presence before letting a computed night in. A Health Connect / backup import that creates a row for a day with a nil HRV/RHR value therefore shadowed the real strap-computed night forever: zero nights reached the baseline fold, the baseline never left CALIBRATING, and Today rendered "No Data" / "Needs the strap" despite scored sleep (the reported bug). Fix, per-field and identical on both platforms: imported non-nil wins; a nil (or absent) slot is backfilled by the computed value. - Swift: hist[day] == nil on [String: Double?] is only true for an absent key (.some(.none) passed the old check). Also mark the helper nonisolated so it is testable off the main actor. - Kotlin: day !in hist had the same shape; hist[day] == null covers both absent keys and null values. Tests (RED first on both platforms): IntelligenceBaselineShadowTests (Swift) and IntelligenceBaselineShadowTest (Kotlin) cover imported-non-nil wins, nil backfilled, no-computed stays missing, and the 7/7 strap-night starvation end-to-end shape. * Tests: render LocalizedStringKey titles instead of comparing keys The carriedLastNight titles are now interpolated ("Last night · %@") so the string-catalog extractor sees the format key (ryanbr#779), which makes LocalizedStringKey equality against a literal key always false. Resolve the key + arguments via Mirror and pin the rendered user-visible copy. --------- Co-authored-by: Boro Vukovic <hello@loudtronix.ca>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ghts (ryanbr#401) * Baselines: imported rows with nil values no longer shadow computed nights The nightly-into-history merge only checked KEY presence before letting a computed night in. A Health Connect / backup import that creates a row for a day with a nil HRV/RHR value therefore shadowed the real strap-computed night forever: zero nights reached the baseline fold, the baseline never left CALIBRATING, and Today rendered "No Data" / "Needs the strap" despite scored sleep (the reported bug). Fix, per-field and identical on both platforms: imported non-nil wins; a nil (or absent) slot is backfilled by the computed value. - Swift: hist[day] == nil on [String: Double?] is only true for an absent key (.some(.none) passed the old check). Also mark the helper nonisolated so it is testable off the main actor. - Kotlin: day !in hist had the same shape; hist[day] == null covers both absent keys and null values. Tests (RED first on both platforms): IntelligenceBaselineShadowTests (Swift) and IntelligenceBaselineShadowTest (Kotlin) cover imported-non-nil wins, nil backfilled, no-computed stays missing, and the 7/7 strap-night starvation end-to-end shape. * Tests: render LocalizedStringKey titles instead of comparing keys The carriedLastNight titles are now interpolated ("Last night · %@") so the string-catalog extractor sees the format key (ryanbr#779), which makes LocalizedStringKey equality against a literal key always false. Resolve the key + arguments via Mirror and pin the rendered user-visible copy. --------- Co-authored-by: Boro Vukovic <hello@loudtronix.ca>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…calibration count (ryanbr#530) * Liquid Today: carry Charge like Rest, honest state pill, battery-ring truth table (B3/A3/B2/B1) Liquid Today is the DEFAULT screen (iOS + macOS), but the v8 rebuild dropped carry-overs the classic TodayView has. On an unscored-today day (post-04:00 rollover, or a no-wear night) the Charge and Effort heroes read `displayDay?.recovery`/`.strain` RAW and blanked, while the Rest hero (freshRestScore) and vitals (Repository.lastVitalsDay) carried right beside them — and the Charge pill still said "Calibrating" at a wearer 14 nights past the seed gate. Liquid was the only surface that blanked; classic carries via lastScoredCharge (ryanbr#543), widget/watch/Live Activity via widgetAnchor (ryanbr#911), Android via TodayScreen.lastScoredRecoveryDay. B3: ChargeDisplay.resolve carries the last scored night's REAL Charge, stamped "Last night · <date>" (or "Latest sleep" once ryanbr#779-stale) via the SAME TodayView.lastScoredRecoveryDay + carriedCaption the classic screen uses, so the two Today screens can't drift. The state pill tells the truth — only a genuinely calibrating baseline reads "Calibrating"; a trusted baseline with no scored night reads "No data". The Key-Metrics Recovery tile reads the same resolved value so tile and hero can't disagree. Effort deliberately does NOT carry: it is today's accumulation, so yesterday's number would be a false statement, not a stale one. A3/B2: the strap-battery ring resolves through StrapBatteryDisplay so a known charging state survives a missing %, and a % never outlives its link. B1: LiquidSyncStatusRow surfaces backfill progress in the Data Sources card. Adds a DEBUG-only `--demo-screen liquidtoday` (plain "today" renders classic). Tests: LiquidChargeCarryTests (9), LiquidBatteryDisplayTests. App-target Swift, so no CI covers it — verified the full macOS StrandTests suite green, the iOS target builds, and drove it in the simulator on the real on-device DB with today's row nulled: before = Charge "–"/pill "Calibrating"; after = Charge 47 carried from Jul 14/pill "Last night". * Liquid Today: surface the "N of 4 nights" calibration count The v8 Liquid Today (the default iOS Today) showed a bare "Calibrating" for a pre-seed-gate baseline, dropping the "N of 4 nights" progress the classic TodayView surfaces (calibrationDetail / ScoreStatePill). A brand-new user in their first 4 nights saw the progress on classic but not on Liquid. ChargeDisplay already carried the count (.calibrating(nights:), 9251ae2); this adds ChargeDisplay.calibrationDetail returning classic's exact copy ("Learning your baseline, N of 4 nights.") and swaps it into the synthesis card in place of the readiness one-liner (chargeDisplay.calibrationDetail ?? synthLine) — the same replacement classic makes (calibrationDetail ?? synthesisCardDetail). The compact greeting pill stays short ("Calibrating") per its fixedSize row, so the count lives in the card. Reuses classic's String Catalog key verbatim; both Today screens now read identically. New-users-only, no owner impact. Android TodayScreen already surfaces this, so no Kotlin twin is needed — iOS Liquid catching up to existing behaviour. Verified (app-target Swift, not covered by CI): LiquidChargeCarryTests (test-first, RED→GREEN, 11/11) on the resolver/label; macOS Strand + iOS NOOPiOS app targets compile; --demo-screen liquidtoday on a clean sim renders "Learning your baseline, 0 of 4 nights." in the synthesis card with the short "Calibrating" pill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…unbuilt draft (ryanbr#779) android/README.md was the initial-port README and had drifted badly: - '`/path/to/NOOP`' placeholder never filled in (intro + build cmd) -> Strand/. - 'This project has NOT been compiled or run on a device' / 'authored without a JDK or Android SDK' -> false: it's a shipped, CI-built app running on real devices (e.g. the WHOOP 4.0 users in the tracker). - Gradle 8.5 -> 8.7 (matches the wrapper); WHOOP 5.0/MG now shipped/verified. - assembleDebug/installDebug + app-debug.apk path -> the flavored assembleFullDebug/installFullDebug + app-full-debug.apk. - Double-Strand path (Strand/Strand/BLE/...) -> Strand/BLE/BLEManager.swift. - Package layout + screen names (Live/History/Support) brought current. Kept the still-accurate BLE contract table + hardware-verification checklist; reframed 'must be validated' as 'new BLE changes need hardware' (it IS validated).
ryanbr
added a commit
that referenced
this pull request
Sep 3, 2026
Parity pass on the Kotlin fix. Apple has the same OR predicate —
($0.avgHrv != nil || $0.restingHr != nil || $0.respRateBpm != nil) && $0.day < todayKey
— so `lastVitalsDay` resolves the freshest row with ANY of the three and blanks
whichever one that row happens to lack. Three live surfaces read it for HRV and
resting HR: `TodayView.recoveryVitalsCard`, and both `recoveryVitalsSection` and
`keyMetricsSection` on Liquid.
`TodayView.dashboardValue` was worse than the Android card that was reported. Its
`.hrv` and `.restingHr` cases had NO carry at all — today's row or "—" — while
`.respiratory`, `.bloodOxygen` and `.skinTemp` beside them all carry, and the Key
Metrics tile has carried via `carriedVital(perField:)` the whole time. So that
card blanked every rollover next to a tile showing a number, which is the
reported symptom with a different cause.
`lastHrvDay` / `lastRestingHrDay` are the fourth and fifth per-field twins after
SpO₂, skin temp and respiratory, same shape and same future-clock bound.
Deliberately NOT staleness-bounded, and the doc says why so it doesn't get
"fixed" later: respiratory took a hard `vitalCarryDays` bound because one CSV
import's 15.6 read as today's for a fortnight (#1331), whereas HRV/RHR disclose
age through the caption instead — `carriedCaption` relabels a weeks-old carry to
"Latest sleep" (#779). Bounding them here would blank values that render today.
The provenance footnote needed a decision. One card-level caption now covers
vitals that can come from DIFFERENT rows, so it stamps the OLDEST row any SHOWN
carried vital came from: erring old is the only safe direction for a caption
whose whole job is to stop a stale read passing as today's, and it keeps the
"Latest sleep" relabel firing on the value that actually is weeks old. A row
counts only if it SUPPLIED the value — `lastVitalsDay` can hold a nil
respiratory, which carries nothing and must stamp nothing.
Tests: the four Kotlin cases as XCTest in DailyMetricLastVitalsDayTests, pinning
the bug (lastVitalsDay picks the respiratory-only row, yielding no HRV) as well
as the fix. GRDB's CSQLite will not build on Linux, so these were also run here
by extracting the five selector bodies VERBATIM into an isolated harness —
identical fixtures to the Kotlin, all pass, twins confirmed behaviourally. The
XCTest file itself is unrun until app-build.
ryanbr
added a commit
that referenced
this pull request
Sep 3, 2026
…rinting -0.0 (#1843) Reported on #1842: the HRV and Resting HR cards read "No data" while tapping through to the same metric graphed a value, and Skin Temp showed -0.0 dC. Cause is one level below the cards. The vitals carry predicate is an OR across HRV / resting-HR / respiratory, so it resolves the freshest row holding ANY of the three -- including a respiratory-only row whose HRV is null. The card reads that null and blanks while a tile carrying a different row shows a value on the same screen. Same failure the SpO2 and skin-temp per-field rows were split out to correct; HRV and resting HR were the two fields still on the shared predicate. Adds lastHrvRow / lastRestingHrRow (Kotlin) and lastHrvDay / lastRestingHrDay (Swift) as the fourth and fifth per-field twins, same shape and same future-clock bound. Apple needed more than a straight port. TodayView.dashboardValue's .hrv and .restingHr had no carry at all -- today's row or an em-dash -- while .respiratory, .bloodOxygen and .skinTemp beside them all carried, and the Key Metrics tile had carried via carriedVital(perField:) throughout. Same symptom, different cause. Liquid's recoveryVitalsSection and keyMetricsSection had the OR-predicate form. Two judgement calls, both documented in place: the new carries are deliberately NOT staleness-bounded (respiratory took a hard bound because one CSV import's value read as today's for a fortnight, #1331; HRV/RHR disclose age through the caption's "Latest sleep" relabel instead, #779), and the single card-level provenance footnote now stamps the OLDEST row any shown carried vital came from, since vitals can come from different rows and erring old is the only safe direction for a caption meant to stop a stale read passing as today's. Skin Temp's -0.0 was a signed zero: a deviation rounding to zero from below kept its minus through %+.1f. Both platforms re-read the formatted string and drop the sign at zero magnitude, so it renders 0.0 -- unsigned, not a fake positive. Tests pin the bug as well as the fix: each asserts the shared predicate picks the respiratory-only row and yields no HRV, so a future simplification back onto it fails in CI rather than in the field. Kotlin full suite 5098/0; Swift package tests green in CI; app-build green on both legs.
ryanbr
added a commit
that referenced
this pull request
Oct 3, 2026
#2525 made a Charge baseline read only nights inside `ChargeBaselines.windowDays`, so after a gap longer than that the folded history is empty, the calibration count is non-nil (0, which #335 deliberately includes) and calibrating takes precedence over the carry. The Today Charge tile therefore reads "Calibrating" on such a gap where #779 had it read "Latest sleep". That is a reasonable call, but it arrived as a side effect: the state mappers were not touched, only their input, so the existing mapper tests kept passing because they drive the count directly with synthetic values. Nothing asserted that a gap this long now yields a non-nil count, which left the display free to be flipped back silently in either direction. Each test pins BOTH displays from the SAME fourteen nights, so the contrast is the test rather than a claim about it. Fourteen matters: fewer than `minNightsSeed` banked nights calibrate under either rule and would pin nothing, so these would have been trusted by the old whole-history fold and the carry genuinely used to win. The Swift side converts the count with the same expression TodayView uses, so the nil that lets the carry through is the production one. Mutation-checked rather than assumed: neutralising `windowDays` turns the pin red (expected 0 but was 14). An earlier draft of this test used a single night, which calibrated under BOTH rules and so pinned nothing; that is why the night count is spelled out above. No production code changes.
ryanbr
added a commit
that referenced
this pull request
Oct 3, 2026
#2525 made a Charge baseline read only nights inside `ChargeBaselines.windowDays`, so after a gap longer than that the folded history is empty, the calibration count is non-nil (0, which #335 deliberately includes) and calibrating takes precedence over the carry. The Today Charge tile therefore reads "Calibrating" on such a gap where #779 had it read "Latest sleep". That is a reasonable call, but it arrived as a side effect: the state mappers were not touched, only their input, so the existing mapper tests kept passing because they drive the count directly with synthetic values. Nothing asserted that a gap this long now yields a non-nil count, which left the display free to be flipped back silently in either direction. Each test pins BOTH displays from the SAME fourteen nights, so the contrast is the test rather than a claim about it. Fourteen matters: fewer than `minNightsSeed` banked nights calibrate under either rule and would pin nothing, so these would have been trusted by the old whole-history fold and the carry genuinely used to win. The Swift side converts the count with the same expression TodayView uses, so the nil that lets the carry through is the production one. Mutation-checked rather than assumed: neutralising `windowDays` turns the pin red (expected 0 but was 14). An earlier draft of this test used a single night, which calibrated under BOTH rules and so pinned nothing; that is why the night count is spelled out above. No production code changes.
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 8, 2026
ryanbr#2525 made a Charge baseline read only nights inside `ChargeBaselines.windowDays`, so after a gap longer than that the folded history is empty, the calibration count is non-nil (0, which ryanbr#335 deliberately includes) and calibrating takes precedence over the carry. The Today Charge tile therefore reads "Calibrating" on such a gap where ryanbr#779 had it read "Latest sleep". That is a reasonable call, but it arrived as a side effect: the state mappers were not touched, only their input, so the existing mapper tests kept passing because they drive the count directly with synthetic values. Nothing asserted that a gap this long now yields a non-nil count, which left the display free to be flipped back silently in either direction. Each test pins BOTH displays from the SAME fourteen nights, so the contrast is the test rather than a claim about it. Fourteen matters: fewer than `minNightsSeed` banked nights calibrate under either rule and would pin nothing, so these would have been trusted by the old whole-history fold and the carry genuinely used to win. The Swift side converts the count with the same expression TodayView uses, so the nil that lets the carry through is the production one. Mutation-checked rather than assumed: neutralising `windowDays` turns the pin red (expected 0 but was 14). An earlier draft of this test used a single night, which calibrated under BOTH rules and so pinned nothing; that is why the night count is spelled out above. No production code changes. (cherry picked from commit 22ce207)
Iskrata
pushed a commit
to Iskrata/noop
that referenced
this pull request
Oct 12, 2026
ryanbr#2525 made a Charge baseline read only nights inside `ChargeBaselines.windowDays`, so after a gap longer than that the folded history is empty, the calibration count is non-nil (0, which ryanbr#335 deliberately includes) and calibrating takes precedence over the carry. The Today Charge tile therefore reads "Calibrating" on such a gap where ryanbr#779 had it read "Latest sleep". That is a reasonable call, but it arrived as a side effect: the state mappers were not touched, only their input, so the existing mapper tests kept passing because they drive the count directly with synthetic values. Nothing asserted that a gap this long now yields a non-nil count, which left the display free to be flipped back silently in either direction. Each test pins BOTH displays from the SAME fourteen nights, so the contrast is the test rather than a claim about it. Fourteen matters: fewer than `minNightsSeed` banked nights calibrate under either rule and would pin nothing, so these would have been trusted by the old whole-history fold and the carry genuinely used to win. The Swift side converts the count with the same expression TodayView uses, so the nil that lets the carry through is the production one. Mutation-checked rather than assumed: neutralising `windowDays` turns the pin red (expected 0 but was 14). An earlier draft of this test used a single night, which calibrated under BOTH rules and so pinned nothing; that is why the night count is spelled out above. No production code changes. (cherry picked from commit 22ce207)
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.
android/README.mdwas the original initial-port README and had drifted badly out of date. Fixes:/path/to/NOOPplaceholder (intro +cdcommand) — never filled in → nowStrand//<your-noop-clone>.assembleDebug/installDebug+app-debug.apk→ the Full/Demo-flavoredassembleFullDebug/installFullDebug+app-full-debug.apk.Strandpath (Strand/Strand/BLE/…) →Strand/BLE/BLEManager.swift.ingest/oura/polar/alarm/notif/…packages) brought current.Kept the still-accurate BLE contract table, permissions section, and hardware-verification checklist. Docs-only.