Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions Packages/StrandAnalytics/Sources/StrandAnalytics/Baselines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion Tools/parity_ledger_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": 29, "identities_sha256": "a13306c37eb1a9272b8666d09700d5120fb5fe0e629f3aaaeef649357f7f8b45"},
{"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"},
Expand Down
4 changes: 2 additions & 2 deletions Tools/parity_twin_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
},
"authority": {
"files": {"count": 509, "sha256": "0a0b02d39eb26ad0635933ad6fca51fb4c33db98178410bfe6676ab9d0fdcaee"},
"functions": {"count": 4537, "sha256": "a10084c830d9b91c0f02bb308fa7ac0f1466c7537de3303669e0f4fbeffdf6f2"},
"functions": {"count": 4536, "sha256": "b6e0f5f15d9a367e60261551f6090d94ce6173ca5ca6721e0c5e061b7fd59683"},
"properties": {"count": 488, "sha256": "77d60fe0f6218252233b9e60da8bf5135968e49be3c6d2027d0973ea20ddf792"},
"constants": {"count": 2001, "sha256": "a9c1f36d2acc614dd747fd6cc0b3ea48079b81517afda5591d219f9591e353c3"},
"file_pairs": {"count": 81, "sha256": "3c94824ccff843da495a212a973d89ce61e536b6795dbee2d0e496a7e7a498f0"},
"function_pairs": {"count": 226, "sha256": "7c7f2298ad7921101241f39d86ecab774d21eadeb89d52d4d5db0397878905f2"},
"property_pairs": {"count": 163, "sha256": "b7d4a29086beec661993370657c0bcdae86f79b221fdc0043206161df5e1117f"},
"constant_pairs": {"count": 702, "sha256": "e3de4c1b84b8500c5700292648bf23dc680e5e14cde055c36c5d1ef4ecc39dc2"},
"unpaired_files": {"count": 369, "sha256": "3462a2a8135bfd793dc6493f10fb72b4fae9028ad9ec47c8dec84b9f5a4dc01a"},
"unpaired_functions": {"count": 4098, "sha256": "a7c3215172bf4d1cd33b8df390684d4d65fc1109f8ee52b3d344b04d231f08fd"},
"unpaired_functions": {"count": 4097, "sha256": "876282d6151cbc736e315a587388b598aee7e4a72e76cf26d08890de9d628324"},
"unpaired_properties": {"count": 162, "sha256": "dabff97ba9bce0aebdc8ef84b9c4f107ce574020e8b0455d8f9214082948246b"},
"unpaired_constants": {"count": 597, "sha256": "578c607cc05b1cd1f867c206c9fab70237ac40707cd49d4e0c5cc66b400184c7"}
}
Expand Down
Loading