From f501d3ff539220d3216c6854e94863f4f4639b20 Mon Sep 17 00:00:00 2001 From: kavemang <66108693+kavemang@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:23:13 -0500 Subject: [PATCH 1/2] test(analytics): cover the production recalibration fold Refs #2708 --- .../Sources/StrandAnalytics/Baselines.swift | 24 ------ .../BaselineEpochDiagnosticTests.swift | 82 +++++++++++-------- 2 files changed, 47 insertions(+), 59 deletions(-) diff --git a/Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift b/Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift index ff4fa689cd..d4a489905e 100644 --- a/Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift +++ b/Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift @@ -475,30 +475,6 @@ public enum Baselines { /// When `baselineEpoch <= 0` (the default / no recalibration) this is byte-identical to the plain /// `foldHistory(_:cfg:)`. When `baselineEpoch` is nil it is read from UserDefaults via /// `hrvBaselineEpoch()` so callers that already use the HRV config get recalibration for free. - /// Diagnostic companion to the epoch-aware `foldHistory` (#731): how many nights that fold DROPS - /// because they predate the recalibration epoch, plus the epoch itself as a `yyyy-MM-dd` day key - /// (nil when no recalibration is set). - /// - /// Exists because a "Charge is stuck" report is otherwise unexplainable from a strap log. A user who - /// taps "Recalibrate baseline" discards every earlier night and must re-earn `minNightsSeed` nights; - /// tapping it again resets that progress to zero. A reporter did exactly that for two weeks — 15 valid - /// HRV nights on file, but `nValid=3` and no Charge — and nothing in the log said the epoch was why. - /// Mirrors the fold's drop rule EXACTLY (same UTC day-start parse, same strict `<` comparison), so the - /// number reported is the number actually dropped. - public static func epochDropDiagnostic(dayKeys: [String], - baselineEpoch: Double? = nil) -> (dropped: Int, epochDay: String?) { - let epoch = baselineEpoch ?? hrvBaselineEpoch() - guard epoch > 0 else { return (0, nil) } - let fmt = DateFormatter() - fmt.calendar = Calendar(identifier: .gregorian) - fmt.timeZone = TimeZone(secondsFromGMT: 0) - fmt.dateFormat = "yyyy-MM-dd" - let dropped = dayKeys.reduce(into: 0) { acc, key in - if let d = fmt.date(from: key), d.timeIntervalSince1970 < epoch { acc += 1 } - } - return (dropped, fmt.string(from: Date(timeIntervalSince1970: epoch))) - } - public static func foldHistory(_ values: [Double?], dayKeys: [String], cfg: MetricCfg, baselineEpoch: Double? = nil) -> BaselineState { let epoch = baselineEpoch ?? hrvBaselineEpoch() diff --git a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BaselineEpochDiagnosticTests.swift b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BaselineEpochDiagnosticTests.swift index 829b3f6418..0b7f866e59 100644 --- a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BaselineEpochDiagnosticTests.swift +++ b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BaselineEpochDiagnosticTests.swift @@ -1,50 +1,62 @@ import XCTest @testable import StrandAnalytics -/// #731: the epoch-drop diagnostic must report exactly what the epoch-aware fold drops, so a "Charge is -/// stuck" strap log explains itself. Pinned against `foldHistory`'s own behaviour, not just in isolation. -final class BaselineEpochDiagnosticTests: XCTestCase { +/// Recalibration must discard earlier history before it can affect the production baseline state. +final class BaselineEpochTests: XCTestCase { + private let epoch: Double = 1_784_419_200 // 2026-07-19 00:00:00 UTC + private let days = ["2026-07-16", "2026-07-17", "2026-07-18", + "2026-07-19", "2026-07-20", "2026-07-21"] + private let values: [Double?] = [40, 41, 42, 43, 44, 45] + private let cfg = Baselines.metricCfg["hrv"]! - /// 2026-07-19 00:00:00 UTC — the recalibration instant used throughout. - private let epoch: Double = 1_784_419_200 + func testNonPositiveEpochPreservesTheEntireFoldState() { + let expected = Baselines.foldHistory(values, cfg: cfg) + for anchor in [0.0, -1.0] { + XCTAssertEqual(Baselines.foldHistory(values, dayKeys: days, cfg: cfg, + baselineEpoch: anchor), expected) + } + } - private let days = ["2026-07-16", "2026-07-17", "2026-07-18", // before the epoch → dropped - "2026-07-19", "2026-07-20", "2026-07-21"] // on/after → kept + func testMidnightEpochKeepsTheEpochDayAndReseedsFromItsValue() { + let actual = Baselines.foldHistory(values, dayKeys: days, cfg: cfg, baselineEpoch: epoch) + XCTAssertEqual(actual, Baselines.foldHistory(Array(values.suffix(3)), cfg: cfg)) + XCTAssertEqual(actual.nValid, 3) + } - func testNoEpochDropsNothing() { - let d = Baselines.epochDropDiagnostic(dayKeys: days, baselineEpoch: 0) - XCTAssertEqual(d.dropped, 0) - XCTAssertNil(d.epochDay, "no recalibration → no epoch day to report") + func testEpochAfterMidnightDropsThatDaysNightToo() { + let actual = Baselines.foldHistory(values, dayKeys: days, cfg: cfg, baselineEpoch: epoch + 1) + XCTAssertEqual(actual, Baselines.foldHistory(Array(values.suffix(2)), cfg: cfg)) + XCTAssertEqual(actual.nValid, 2, "night membership uses UTC day start, not its date label alone") } - func testDropsOnlyNightsStrictlyBeforeTheEpoch() { - let d = Baselines.epochDropDiagnostic(dayKeys: days, baselineEpoch: epoch) - XCTAssertEqual(d.dropped, 3, "16/17/18 precede the epoch; the epoch day itself is KEPT") - XCTAssertEqual(d.epochDay, "2026-07-19") + func testDroppedMissingAndOutlyingValuesCannotAgeOrAnchorTheNewBaseline() { + let kept: [Double?] = [43, 44, 45] + let history: [Double?] = [nil, 250, 1] + kept + let actual = Baselines.foldHistory(history, dayKeys: days, cfg: cfg, baselineEpoch: epoch) + XCTAssertEqual(actual, Baselines.foldHistory(kept, cfg: cfg), + "discarded nights must never reach update, including skip-and-hold bookkeeping") } - /// The diagnostic is only useful if it agrees with the fold it describes: dropped + used == total. - func testAgreesWithFoldHistory() { - let cfg = Baselines.metricCfg["hrv"]! - let values: [Double?] = [40, 41, 42, 43, 44, 45] // all in-range, so every KEPT night is valid - let state = Baselines.foldHistory(values, dayKeys: days, cfg: cfg, baselineEpoch: epoch) - let d = Baselines.epochDropDiagnostic(dayKeys: days, baselineEpoch: epoch) - XCTAssertEqual(d.dropped + state.nValid, days.count, - "every night is either dropped by the epoch or counted by the fold") - XCTAssertEqual(state.nValid, 3, "the reporter's exact signature: 6 nights on file, 3 usable") + func testEpochAfterAllHistoryReturnsAnEmptyCalibratingBaseline() { + let actual = Baselines.foldHistory(values, dayKeys: days, cfg: cfg, + baselineEpoch: epoch + 3 * 86400) + XCTAssertEqual(actual, Baselines.foldHistory([], cfg: cfg)) + XCTAssertEqual(actual.nValid, 0) + XCTAssertFalse(actual.usable) } - /// The reporter's case (#731): plenty of valid history, but a recent recalibration leaves the - /// baseline one night short of `minNightsSeed` — which read as "stuck" with no explanation. - func testReproducesTheStuckAtThreeSignature() { - let cfg = Baselines.metricCfg["hrv"]! - // 15 nights of good HRV, recalibrated so only the last 3 survive. + func testThreeSurvivingNightsStayUnusableUntilTheFourthNightArrives() { + // #731: fifteen good nights, with recalibration retaining only July 20–22. let allDays = (8...22).map { String(format: "2026-07-%02d", $0) } - let values: [Double?] = Array(repeating: 45.0, count: allDays.count) - let state = Baselines.foldHistory(values, dayKeys: allDays, cfg: cfg, baselineEpoch: 1_784_505_600) // 07-20 - let d = Baselines.epochDropDiagnostic(dayKeys: allDays, baselineEpoch: 1_784_505_600) - XCTAssertEqual(d.dropped, 12) - XCTAssertEqual(state.nValid, 3) - XCTAssertFalse(state.usable, "3 < minNightsSeed(4) → Charge stays nil, exactly as reported") + let history: [Double?] = Array(repeating: 45, count: allDays.count) + let anchor = epoch + 86400 + let three = Baselines.foldHistory(history, dayKeys: allDays, cfg: cfg, baselineEpoch: anchor) + XCTAssertEqual(three, Baselines.foldHistory(Array(history.suffix(3)), cfg: cfg)) + XCTAssertEqual(three.nValid, 3) + XCTAssertFalse(three.usable, "three is below minNightsSeed") + let four = Baselines.foldHistory(history + [45], dayKeys: allDays + ["2026-07-23"], + cfg: cfg, baselineEpoch: anchor) + XCTAssertEqual(four.nValid, 4) + XCTAssertTrue(four.usable) } } From b148754a1b4558debe474a4d21833a2010b63e31 Mon Sep 17 00:00:00 2001 From: kavemang <66108693+kavemang@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:42:18 -0500 Subject: [PATCH 2/2] chore(parity): refresh snapshots after diagnostic removal Refs #2708 --- Tools/parity_ledger_baseline.json | 2 +- Tools/parity_twin_map.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tools/parity_ledger_baseline.json b/Tools/parity_ledger_baseline.json index 361e74c26a..6d856a1a41 100644 --- a/Tools/parity_ledger_baseline.json +++ b/Tools/parity_ledger_baseline.json @@ -15,7 +15,7 @@ {"rule": "duplicate-implementation", "scope": "android/ingest", "reason": "Exact platform-local implementation is retained as visible parity debt pending a source-level consolidation decision.", "provenance": "Exact identities emitted by parity_ledger rule duplicate-implementation for current upstream scope android/ingest; no wildcard matching.", "count": 2, "identities_sha256": "8b3126b470191bb155e6d7d3caf111423e5738edab66643558805020a0b1123d"}, {"rule": "duplicate-implementation", "scope": "android/ui", "reason": "Exact platform-local implementation is retained as visible parity debt pending a source-level consolidation decision.", "provenance": "Exact identities emitted by parity_ledger rule duplicate-implementation for current upstream scope android/ui; no wildcard matching.", "count": 4, "identities_sha256": "b9f579c3d30221ff72e40282c29baab20d57d95acf044e0f71d2338e37c52b1b"}, {"rule": "test-only-callsite", "scope": "Packages/OuraProtocol", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/OuraProtocol; no wildcard matching.", "count": 5, "identities_sha256": "ff6404cd4d27782334dd844223be3ec86bb3260bef7f7e29c658fb6ad6e7938e"}, - {"rule": "test-only-callsite", "scope": "Packages/StrandAnalytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandAnalytics; no wildcard matching.", "count": 44, "identities_sha256": "fff777acbc66e7dad92bd0a6c8b7644e5c7ab6ddc0a7106dfecc49319da6127b"}, + {"rule": "test-only-callsite", "scope": "Packages/StrandAnalytics", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandAnalytics; no wildcard matching.", "count": 43, "identities_sha256": "4df279f81c5f4d9ac595863b061f5c200b4f64e08bb72bf35b4fde40aba0d6d1"}, {"rule": "test-only-callsite", "scope": "Packages/StrandImport", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/StrandImport; no wildcard matching.", "count": 14, "identities_sha256": "1acdc71178e0a555f000c42980345e8c38495f9e3806a866f31b260865d3ac98"}, {"rule": "test-only-callsite", "scope": "Packages/WhoopProtocol", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopProtocol; no wildcard matching.", "count": 30, "identities_sha256": "a9dc115534e8710217db9cd39bcceaa1419741737bfd3b733007dc0215cd5af2"}, {"rule": "test-only-callsite", "scope": "Packages/WhoopStore", "reason": "Exact declaration is reached only from tests in the conservative lexical call graph; dynamic, callback and external entry points are intentionally not inferred.", "provenance": "Exact identities emitted by parity_ledger rule test-only-callsite for current upstream scope Packages/WhoopStore; no wildcard matching.", "count": 37, "identities_sha256": "2a62c1ece1c541b662dd407f0668d9a02782bb4d21f9bb181518272ff12f0e40"}, diff --git a/Tools/parity_twin_map.json b/Tools/parity_twin_map.json index f462057855..d80c5e91fb 100644 --- a/Tools/parity_twin_map.json +++ b/Tools/parity_twin_map.json @@ -19,7 +19,7 @@ }, "authority": { "files": {"count": 510, "sha256": "b1c73338e79009d984d1313d8b3166e52672712c68074f5fee190c503e299b18"}, - "functions": {"count": 4534, "sha256": "91a1670dfe86761a907dc0d1fa73b219e9ca8b5b6d90c36310db21c1657dc884"}, + "functions": {"count": 4533, "sha256": "f062b276a21116b5c826baa72abe1e980599347a0eab9e22bd1ccf67db87944a"}, "properties": {"count": 488, "sha256": "77d60fe0f6218252233b9e60da8bf5135968e49be3c6d2027d0973ea20ddf792"}, "constants": {"count": 1991, "sha256": "ab5fad0319f0ee260993445c0388fbcd89801491c316977095e01014888f9228"}, "file_pairs": {"count": 80, "sha256": "eeeb9e0f4adabed6e6c3c5886e980ea3a3656b65b5bd2a9b3b9e43b5ea3b051c"}, @@ -27,7 +27,7 @@ "property_pairs": {"count": 163, "sha256": "b7d4a29086beec661993370657c0bcdae86f79b221fdc0043206161df5e1117f"}, "constant_pairs": {"count": 697, "sha256": "1f02bdcb642db9d6b24a4cdcbe1351fefbf5ec269830900eba0df49104745b5a"}, "unpaired_files": {"count": 372, "sha256": "37aa705af1be255d3d18adfe912b93bcfeaa3760e1513412aad81388a4c1410b"}, - "unpaired_functions": {"count": 4099, "sha256": "3ee3e74d901e17225e294c309c1b00a7a5b178bb3164bff43eaaa5309d574169"}, + "unpaired_functions": {"count": 4098, "sha256": "c46adefe4e132f44fcbb7181f3b23676d3f9158238898ae3542895315a49706e"}, "unpaired_properties": {"count": 162, "sha256": "dabff97ba9bce0aebdc8ef84b9c4f107ce574020e8b0455d8f9214082948246b"}, "unpaired_constants": {"count": 597, "sha256": "578c607cc05b1cd1f867c206c9fab70237ac40707cd49d4e0c5cc66b400184c7"} }