chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
@@ -0,0 +1,50 @@
import Foundation
/// Informational summary of optional coding-activity observations in a trace's `decision` events.
/// It reports how many decisions carried activity data and how many sampled decisions were below
/// `activeThresholdSeconds` for either CLI.
public struct ActivityCoverageStats: Sendable, Equatable {
public let decisionCount: Int
public let sampledCount: Int
public let activeCount: Int
public init(decisionCount: Int, sampledCount: Int, activeCount: Int) {
self.decisionCount = decisionCount
self.sampledCount = sampledCount
self.activeCount = activeCount
}
/// Fraction of `decision` events that carried at least one non-nil activity field.
public var sampledFraction: Double {
self.decisionCount == 0 ? 0 : Double(self.sampledCount) / Double(self.decisionCount)
}
/// Fraction of the *sampled* decisions (not all decisions) that looked like active coding.
public var activeFraction: Double {
self.sampledCount == 0 ? 0 : Double(self.activeCount) / Double(self.sampledCount)
}
/// - Parameter activeThresholdSeconds: below this many seconds since the newest transcript
/// write, a CLI counts as "active coding at decision time". Defaults to 5 minutes.
public static func compute(
from records: [AdaptiveRefreshTraceRecord],
activeThresholdSeconds: TimeInterval = 300) -> Self
{
var sampledCount = 0
var activeCount = 0
var decisionCount = 0
for record in records where record.kind == .decision {
decisionCount += 1
let codexSeconds = record.codexActivitySeconds
let claudeSeconds = record.claudeActivitySeconds
guard codexSeconds != nil || claudeSeconds != nil else { continue }
sampledCount += 1
let isActive = (codexSeconds ?? .infinity) < activeThresholdSeconds
|| (claudeSeconds ?? .infinity) < activeThresholdSeconds
if isActive {
activeCount += 1
}
}
return Self(decisionCount: decisionCount, sampledCount: sampledCount, activeCount: activeCount)
}
}
@@ -0,0 +1,201 @@
import Foundation
/// The event kinds a trace records. `decision` events capture a full policy tick (the
/// signals it saw plus what it chose). `menuOpen` and `refreshCompleted` capture the two
/// ground-truth events the replay engine anchors a simulation to, independent of any candidate
/// policy. `timerAdvanced` captures the one place live behavior *isn't* a plain tick loop: when
/// opening the menu makes `UsageStore.noteMenuOpened(at:)` pull the next adaptive refresh forward
/// (see `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`). Recording it separately
/// from `decision` lets a trace answer "did an advance happen, and to when" without relying on
/// fragile inference from decision-timestamp gaps.
public enum AdaptiveRefreshTraceEventKind: String, Sendable, Codable {
case decision
case menuOpen
case refreshCompleted
case timerAdvanced
/// Every live advance comparison, including the cases correctly rejected because the current
/// timer was already earlier. This is distinct from counterfactual replay advances.
case timerAdvanceEvaluated
}
/// One line of a JSONL adaptive-refresh trace. Field presence depends on `kind`: `decision`
/// records populate `menuAgeSeconds`, `lowPowerModeEnabled`, `thermalState`, `reason`, and
/// `delaySeconds`, plus optional activity observations supplied by the input trace
/// (`codexActivitySeconds`/`claudeActivitySeconds`, the seconds-since-newest-transcript fields,
/// and the per-file intensity fields alongside them); timer advance records populate
/// `previousScheduledAt`, `candidateScheduledAt`, `reason`, and `delaySeconds`; `menuOpen` and
/// `refreshCompleted` carry only `kind` and `timestamp`.
public struct AdaptiveRefreshTraceRecord: Sendable, Codable, Equatable {
public let kind: AdaptiveRefreshTraceEventKind
public let timestamp: Date
public let menuAgeSeconds: TimeInterval?
public let lowPowerModeEnabled: Bool?
public let thermalState: ReplayThermalState?
public let reason: String?
public let delaySeconds: TimeInterval?
/// Timer advance records only: the adaptive timer's scheduled refresh time before the
/// comparison, or `nil` when no refresh had been scheduled yet (matches
/// `UsageStore.shouldAdvanceAdaptiveTimer`'s "always advance when nothing is scheduled" rule).
public let previousScheduledAt: Date?
/// Timer advance records only: the candidate refresh time, i.e. the menu-open timestamp plus
/// the freshly computed decision's delay.
public let candidateScheduledAt: Date?
/// `timerAdvanceEvaluated` only: whether the live schedule comparison accepted the candidate.
public let timerAdvanceAccepted: Bool?
/// `timerAdvanceEvaluated` only: `previousScheduledAt - candidateScheduledAt`, captured before
/// whole-second ISO-8601 serialization. Positive means the candidate was earlier. Optional for
/// compatibility with traces recorded before exact comparison deltas were added.
public let scheduleLeadSeconds: TimeInterval?
/// `timerAdvanceEvaluated` only: whether another refresh was in flight at comparison time.
public let refreshInFlight: Bool?
/// `decision` only: seconds since the newest observed Codex session transcript modification,
/// or `nil` when unavailable. Optional so old trace lines without this field keep decoding.
public let codexActivitySeconds: TimeInterval?
/// `decision` only: the Claude Code counterpart of `codexActivitySeconds`.
public let claudeActivitySeconds: TimeInterval?
/// `decision` only: how long the newest Codex transcript has been
/// growing (its mtime minus its creationDate), or `nil` when unavailable. Not a separate
/// session-age field age is `codexActivitySeconds` + `codexSessionDurationSeconds`.
public let codexSessionDurationSeconds: TimeInterval?
/// `decision` only: the Claude Code counterpart of `codexSessionDurationSeconds`.
public let claudeSessionDurationSeconds: TimeInterval?
/// `decision` only: size in bytes of the newest Codex transcript, as a stateless raw value.
public let codexTranscriptBytes: Int64?
/// `decision` only: the Claude Code counterpart of `codexTranscriptBytes`.
public let claudeTranscriptBytes: Int64?
/// `decision` only: count of Codex `.jsonl` transcripts modified in the observation window.
public let codexActiveTranscriptCount: Int?
/// `decision` only: the Claude Code counterpart of `codexActiveTranscriptCount`.
public let claudeActiveTranscriptCount: Int?
public init(
kind: AdaptiveRefreshTraceEventKind,
timestamp: Date,
menuAgeSeconds: TimeInterval? = nil,
lowPowerModeEnabled: Bool? = nil,
thermalState: ReplayThermalState? = nil,
reason: String? = nil,
delaySeconds: TimeInterval? = nil,
previousScheduledAt: Date? = nil,
candidateScheduledAt: Date? = nil,
timerAdvanceAccepted: Bool? = nil,
scheduleLeadSeconds: TimeInterval? = nil,
refreshInFlight: Bool? = nil,
codexActivitySeconds: TimeInterval? = nil,
claudeActivitySeconds: TimeInterval? = nil,
codexSessionDurationSeconds: TimeInterval? = nil,
claudeSessionDurationSeconds: TimeInterval? = nil,
codexTranscriptBytes: Int64? = nil,
claudeTranscriptBytes: Int64? = nil,
codexActiveTranscriptCount: Int? = nil,
claudeActiveTranscriptCount: Int? = nil)
{
self.kind = kind
self.timestamp = timestamp
self.menuAgeSeconds = menuAgeSeconds
self.lowPowerModeEnabled = lowPowerModeEnabled
self.thermalState = thermalState
self.reason = reason
self.delaySeconds = delaySeconds
self.previousScheduledAt = previousScheduledAt
self.candidateScheduledAt = candidateScheduledAt
self.timerAdvanceAccepted = timerAdvanceAccepted
self.scheduleLeadSeconds = scheduleLeadSeconds
self.refreshInFlight = refreshInFlight
self.codexActivitySeconds = codexActivitySeconds
self.claudeActivitySeconds = claudeActivitySeconds
self.codexSessionDurationSeconds = codexSessionDurationSeconds
self.claudeSessionDurationSeconds = claudeSessionDurationSeconds
self.codexTranscriptBytes = codexTranscriptBytes
self.claudeTranscriptBytes = claudeTranscriptBytes
self.codexActiveTranscriptCount = codexActiveTranscriptCount
self.claudeActiveTranscriptCount = claudeActiveTranscriptCount
}
// swiftlint:disable:next function_parameter_count
public static func decision(
timestamp: Date,
menuAgeSeconds: TimeInterval?,
lowPowerModeEnabled: Bool,
thermalState: ReplayThermalState,
reason: String,
delaySeconds: TimeInterval,
codexActivitySeconds: TimeInterval? = nil,
claudeActivitySeconds: TimeInterval? = nil,
codexSessionDurationSeconds: TimeInterval? = nil,
claudeSessionDurationSeconds: TimeInterval? = nil,
codexTranscriptBytes: Int64? = nil,
claudeTranscriptBytes: Int64? = nil,
codexActiveTranscriptCount: Int? = nil,
claudeActiveTranscriptCount: Int? = nil) -> Self
{
Self(
kind: .decision,
timestamp: timestamp,
menuAgeSeconds: menuAgeSeconds,
lowPowerModeEnabled: lowPowerModeEnabled,
thermalState: thermalState,
reason: reason,
delaySeconds: delaySeconds,
codexActivitySeconds: codexActivitySeconds,
claudeActivitySeconds: claudeActivitySeconds,
codexSessionDurationSeconds: codexSessionDurationSeconds,
claudeSessionDurationSeconds: claudeSessionDurationSeconds,
codexTranscriptBytes: codexTranscriptBytes,
claudeTranscriptBytes: claudeTranscriptBytes,
codexActiveTranscriptCount: codexActiveTranscriptCount,
claudeActiveTranscriptCount: claudeActiveTranscriptCount)
}
public static func menuOpen(timestamp: Date) -> Self {
Self(kind: .menuOpen, timestamp: timestamp)
}
public static func refreshCompleted(timestamp: Date) -> Self {
Self(kind: .refreshCompleted, timestamp: timestamp)
}
/// - Parameters:
/// - timestamp: When the menu open that triggered the advance occurred.
/// - previousScheduledAt: The timer's scheduled refresh time immediately before the advance.
/// - candidateScheduledAt: The refresh time the timer advanced to (`timestamp + delaySeconds`).
/// - reason: The freshly computed decision's reason (e.g. `"recentInteraction"`).
/// - delaySeconds: The freshly computed decision's delay.
public static func timerAdvanced(
timestamp: Date,
previousScheduledAt: Date?,
candidateScheduledAt: Date,
reason: String,
delaySeconds: TimeInterval) -> Self
{
Self(
kind: .timerAdvanced,
timestamp: timestamp,
reason: reason,
delaySeconds: delaySeconds,
previousScheduledAt: previousScheduledAt,
candidateScheduledAt: candidateScheduledAt)
}
// swiftlint:disable:next function_parameter_count
public static func timerAdvanceEvaluated(
timestamp: Date,
previousScheduledAt: Date?,
candidateScheduledAt: Date,
reason: String,
delaySeconds: TimeInterval,
accepted: Bool,
refreshInFlight: Bool) -> Self
{
Self(
kind: .timerAdvanceEvaluated,
timestamp: timestamp,
reason: reason,
delaySeconds: delaySeconds,
previousScheduledAt: previousScheduledAt,
candidateScheduledAt: candidateScheduledAt,
timerAdvanceAccepted: accepted,
scheduleLeadSeconds: previousScheduledAt.map { $0.timeIntervalSince(candidateScheduledAt) },
refreshInFlight: refreshInFlight)
}
}
@@ -0,0 +1,85 @@
import Foundation
/// A malformed trace line, with enough context to find and fix it.
public struct AdaptiveRefreshTraceParseError: Error, Sendable, Equatable, CustomStringConvertible {
public let lineNumber: Int
public let content: String
public let underlyingDescription: String
public init(lineNumber: Int, content: String, underlyingDescription: String) {
self.lineNumber = lineNumber
self.content = content
self.underlyingDescription = underlyingDescription
}
public var description: String {
"trace line \(self.lineNumber) is malformed: \(self.underlyingDescription) (content: \(self.content))"
}
}
/// Parses newline-delimited JSON adaptive-refresh traces.
///
/// Deliberate choice: a malformed line **fails the whole parse** rather than being silently
/// skipped. A trace is acceptance evidence if a line is corrupt (truncated write, disk-full
/// mid-append, hand-edited fixture with a typo), the honest answer is "this trace is untrustworthy
/// as a whole", not "here are metrics computed from however much of it happened to parse". A
/// silently-shortened trace would still produce a superficially plausible replay report, which is
/// worse than a loud failure: it hides exactly the kind of gap that would bias staleness/refresh
/// counts. Callers that genuinely want best-effort parsing can catch the error and fall back to
/// `AdaptiveRefreshTraceParser.parseTolerantly`, which skips bad lines and returns what parsed.
public enum AdaptiveRefreshTraceParser {
public static func parse(_ text: String) throws -> [AdaptiveRefreshTraceRecord] {
let decoder = Self.makeDecoder()
var records: [AdaptiveRefreshTraceRecord] = []
for (index, line) in text.split(
omittingEmptySubsequences: false,
whereSeparator: \.isNewline).enumerated()
{
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { continue }
guard let data = trimmed.data(using: .utf8) else {
throw AdaptiveRefreshTraceParseError(
lineNumber: index + 1,
content: trimmed,
underlyingDescription: "not valid UTF-8")
}
do {
try records.append(decoder.decode(AdaptiveRefreshTraceRecord.self, from: data))
} catch {
throw AdaptiveRefreshTraceParseError(
lineNumber: index + 1,
content: trimmed,
underlyingDescription: String(describing: error))
}
}
return records
}
public static func parse(contentsOf url: URL) throws -> [AdaptiveRefreshTraceRecord] {
let text = try String(contentsOf: url, encoding: .utf8)
return try self.parse(text)
}
/// Best-effort variant: skips lines that fail to parse instead of throwing. Not the default
/// see the type-level documentation for why silent skipping is the wrong default for
/// acceptance-evidence traces. Exists for callers (future exploratory tooling) that explicitly
/// want partial data over none.
public static func parseTolerantly(_ text: String) -> [AdaptiveRefreshTraceRecord] {
let decoder = Self.makeDecoder()
var records: [AdaptiveRefreshTraceRecord] = []
for line in text.split(omittingEmptySubsequences: false, whereSeparator: \.isNewline) {
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let data = trimmed.data(using: .utf8) else { continue }
if let record = try? decoder.decode(AdaptiveRefreshTraceRecord.self, from: data) {
records.append(record)
}
}
return records
}
private static func makeDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}
}
@@ -0,0 +1,57 @@
import AdaptiveRefreshCore
import Foundation
/// Replay adapter for the same canonical policy core used by the CodexBar app.
public struct AdaptiveReplayPolicy: ReplayPolicy, Sendable {
public let name = "adaptive"
/// Matches `UsageStore.noteMenuOpened(at:)`'s adaptive-only advance guard: this is the one
/// baseline that actually models the interaction-advance path, so it is the only one that
/// overrides the protocol's `false` default.
public let advancesOnInteraction = true
public init() {}
public func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision {
let decision = AdaptiveRefreshPolicyCore().nextDelay(for: AdaptiveRefreshPolicyCore.Input(
now: input.now,
lastMenuOpenAt: input.lastMenuOpenAt,
lowPowerModeEnabled: input.lowPowerModeEnabled,
thermalPressure: input.thermalState.isConstrained ? .constrained : .nominal))
return ReplayPolicyDecision(
delaySeconds: TimeInterval(decision.delay.components.seconds),
reason: decision.reason.rawValue)
}
}
/// A fixed-cadence baseline: always waits the same interval, regardless of signals. Used to
/// compare the adaptive policy against the flat refresh frequencies CodexBar also offers
/// (2/5/15/30 minutes). Never advances on interaction (`advancesOnInteraction` stays the protocol
/// default of `false`), matching the real app: fixed-cadence refresh frequencies never wire up
/// `noteMenuOpened`'s advance check.
public struct FixedIntervalPolicy: ReplayPolicy, Sendable {
public let name: String
private let intervalSeconds: TimeInterval
public init(minutes: Int) {
self.name = "fixed-\(minutes)m"
self.intervalSeconds = TimeInterval(minutes) * 60
}
public func decide(_: ReplayPolicyInput) -> ReplayPolicyDecision {
ReplayPolicyDecision(delaySeconds: self.intervalSeconds, reason: "fixed")
}
}
/// The degenerate floor: never schedules a refresh. A trace replayed against this policy always
/// reports zero refreshes, which is the point it establishes the worst-case staleness bound the
/// other policies are compared against.
public struct ManualPolicy: ReplayPolicy, Sendable {
public let name = "manual"
public init() {}
public func decide(_: ReplayPolicyInput) -> ReplayPolicyDecision {
ReplayPolicyDecision(delaySeconds: nil, reason: "manual")
}
}
@@ -0,0 +1,27 @@
import Foundation
/// Replay-only candidate used to test whether stat-only coding activity can close the accepted
/// "active work is never slower than five minutes" gap. It is not a production policy approval.
public struct CodingActivityAdaptivePolicy: ReplayPolicy, Sendable {
public let name = "adaptive-activity"
public let advancesOnInteraction = true
private let base = AdaptiveReplayPolicy()
private static let activeThreshold: TimeInterval = 5 * 60
private static let activeDelayCap: TimeInterval = 5 * 60
public init() {}
public func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision {
let baseDecision = self.base.decide(input)
guard !input.isConstrained,
let activityAge = input.codingActivityAgeSeconds,
activityAge < Self.activeThreshold,
let baseDelay = baseDecision.delaySeconds,
baseDelay > Self.activeDelayCap
else {
return baseDecision
}
return ReplayPolicyDecision(delaySeconds: Self.activeDelayCap, reason: "codingActivity")
}
}
+39
View File
@@ -0,0 +1,39 @@
# AdaptiveReplayKit
`AdaptiveReplayKit` is an offline harness for comparing refresh-timing policies against an
explicit JSONL trace. `AdaptiveReplayCLI` is the command-line wrapper around the library.
## Scope
The replay targets do not import `CodexBar` or `CodexBarCore`; they share only the package-internal,
Foundation-only `AdaptiveRefreshCore` target with the app. They do not record app behavior, scan
Codex or Claude transcript directories, write trace files, call providers, or change the production
refresh policy. Trace capture and lifecycle management are deliberately outside this PR; callers
provide an existing trace path to the CLI.
Optional activity fields in the trace schema are inputs only. The replay kit never discovers or
collects them. Old records without those fields continue to decode.
## Components
- `AdaptiveRefreshTrace.swift` defines the version-tolerant trace schema.
- `AdaptiveRefreshTraceParser.swift` parses JSONL strictly by default. The tolerant entry point is
available for exploratory work that explicitly accepts skipped malformed records.
- `AdaptiveRefreshCore` owns the production decision table. `ReplayPolicy.swift`,
`BaselinePolicies.swift`, and `CandidatePolicies.swift` provide replay adapters, fixed/manual
baselines, and the replay-only activity candidate.
- `ReplayEngine.swift` and `ReplayMetrics.swift` calculate simulated refresh cadence, menu-open
staleness, interaction advances, and constrained-state compliance.
- `ReplayTraceSegmentation.swift` excludes legacy deadline-overrun gaps with an explicit heuristic
and reports the excluded duration.
- `RecordedScheduleAudit.swift` audits recorded timer-advance events independently from the replay
clock.
- `Sources/AdaptiveReplayCLI` formats table or JSON reports.
`interactionAdvanceCount` is counterfactual. Replay assumes a zero-duration refresh, while the
live app waits for provider work and may already have a refresh in flight. Recorded schedule events
therefore have a separate audit instead of a direct count comparison.
The legacy gap heuristic cannot distinguish sleep or reboot from a long refresh or event-loop
stall. Reports expose the segment count, grace interval, and excluded time rather than assigning a
cause.
@@ -0,0 +1,153 @@
import Foundation
public struct RecordedScheduleAudit: Sendable, Equatable {
public let recordedAdvanceCount: Int
public let evaluatedCount: Int
public let acceptedEvaluationCount: Int
public let rejectedEvaluationCount: Int
public let payloadMismatchCount: Int
public let decisionMismatchCount: Int
public let menuLinkMismatchCount: Int
public let ambiguousComparisonCount: Int
public var isValid: Bool {
self.payloadMismatchCount == 0
&& self.decisionMismatchCount == 0
&& self.menuLinkMismatchCount == 0
&& self.ambiguousComparisonCount == 0
}
}
/// Audits the live schedule records without equating them to ReplayEngine's counterfactual clock.
public enum RecordedScheduleAuditor {
public static func audit(
_ records: [AdaptiveRefreshTraceRecord],
timestampTolerance: TimeInterval = 1) -> RecordedScheduleAudit
{
let sorted = records.sorted { $0.timestamp < $1.timestamp }
let menuTimestamps = sorted.filter { $0.kind == .menuOpen }.map(\.timestamp)
let advances = sorted.filter { $0.kind == .timerAdvanced }
let evaluations = sorted.filter { $0.kind == .timerAdvanceEvaluated }
var payloadMismatchCount = advances.count(where: { !Self.payloadIsValid($0) })
let evaluationOutcomes = evaluations.map(Self.evaluationOutcome)
let decisionMismatchCount = evaluationOutcomes.count(where: { $0 == .mismatch })
let ambiguousComparisonCount = evaluationOutcomes.count(where: { $0 == .ambiguous })
// Every evaluation is caused by one menu open. Before evaluation records existed, an
// accepted advance was the only causal record, so retain those legacy advances as linkage
// events. Modern accepted advances are reconciled against evaluations below instead of
// consuming the same menu open twice.
let legacyAdvances = evaluations.first.map { firstEvaluation in
advances.filter { $0.timestamp < firstEvaluation.timestamp }
} ?? advances
let menuLinkMismatchCount = Self.unmatchedEventCount(
evaluations + legacyAdvances,
menuTimestamps: menuTimestamps,
timestampTolerance: timestampTolerance)
if let firstEvaluationAt = evaluations.first?.timestamp {
let accepted = evaluations.filter { $0.timerAdvanceAccepted == true }
let auditableAdvances = advances.filter { $0.timestamp >= firstEvaluationAt }
payloadMismatchCount += Self.scheduleMultiplicityDifference(accepted, auditableAdvances)
}
return RecordedScheduleAudit(
recordedAdvanceCount: advances.count,
evaluatedCount: evaluations.count,
acceptedEvaluationCount: evaluations.count(where: { $0.timerAdvanceAccepted == true }),
rejectedEvaluationCount: evaluations.count(where: { $0.timerAdvanceAccepted == false }),
payloadMismatchCount: payloadMismatchCount,
decisionMismatchCount: decisionMismatchCount,
menuLinkMismatchCount: menuLinkMismatchCount,
ambiguousComparisonCount: ambiguousComparisonCount)
}
private static func payloadIsValid(_ record: AdaptiveRefreshTraceRecord) -> Bool {
guard let candidate = record.candidateScheduledAt,
let delay = record.delaySeconds,
abs(candidate.timeIntervalSince(record.timestamp) - delay) < 0.001
else { return false }
// Whole-second legacy timestamps can collapse a sub-second accepted lead to equality.
return record.previousScheduledAt.map { candidate <= $0 } ?? true
}
private enum EvaluationOutcome: Equatable {
case valid
case mismatch
case ambiguous
}
private static func evaluationOutcome(_ record: AdaptiveRefreshTraceRecord) -> EvaluationOutcome {
guard let accepted = record.timerAdvanceAccepted,
let candidate = record.candidateScheduledAt,
let delay = record.delaySeconds,
abs(candidate.timeIntervalSince(record.timestamp) - delay) < 0.001
else { return .mismatch }
guard let previous = record.previousScheduledAt else { return accepted ? .valid : .mismatch }
if candidate != previous {
return accepted == (candidate < previous) ? .valid : .mismatch
}
guard let lead = record.scheduleLeadSeconds else { return .ambiguous }
return accepted == (lead > 0) ? .valid : .mismatch
}
private struct ScheduleKey: Hashable {
let timestamp: Date
let previousScheduledAt: Date?
let candidateScheduledAt: Date?
let reason: String?
let delaySeconds: TimeInterval?
}
private static func scheduleMultiplicityDifference(
_ lhs: [AdaptiveRefreshTraceRecord],
_ rhs: [AdaptiveRefreshTraceRecord]) -> Int
{
func counts(_ records: [AdaptiveRefreshTraceRecord]) -> [ScheduleKey: Int] {
Dictionary(grouping: records, by: scheduleKey).mapValues(\.count)
}
let lhsCounts = counts(lhs)
let rhsCounts = counts(rhs)
return Set(lhsCounts.keys).union(rhsCounts.keys).reduce(0) { difference, key in
difference + abs(lhsCounts[key, default: 0] - rhsCounts[key, default: 0])
}
}
/// Maximum one-to-one matching for sorted points with a symmetric tolerance window. Extra
/// menu opens are valid because fixed/manual modes do not emit schedule evaluations; only an
/// event without its own causal menu open is a mismatch.
private static func unmatchedEventCount(
_ records: [AdaptiveRefreshTraceRecord],
menuTimestamps: [Date],
timestampTolerance: TimeInterval) -> Int
{
let eventTimestamps = records.map(\.timestamp).sorted()
var eventIndex = 0
var menuIndex = 0
var unmatched = 0
while eventIndex < eventTimestamps.count, menuIndex < menuTimestamps.count {
let eventTimestamp = eventTimestamps[eventIndex]
let menuTimestamp = menuTimestamps[menuIndex]
if menuTimestamp < eventTimestamp.addingTimeInterval(-timestampTolerance) {
menuIndex += 1
} else if menuTimestamp > eventTimestamp.addingTimeInterval(timestampTolerance) {
unmatched += 1
eventIndex += 1
} else {
eventIndex += 1
menuIndex += 1
}
}
return unmatched + eventTimestamps.count - eventIndex
}
private static func scheduleKey(_ record: AdaptiveRefreshTraceRecord) -> ScheduleKey {
ScheduleKey(
timestamp: record.timestamp,
previousScheduledAt: record.previousScheduledAt,
candidateScheduledAt: record.candidateScheduledAt,
reason: record.reason,
delaySeconds: record.delaySeconds)
}
}
@@ -0,0 +1,303 @@
import Foundation
/// Simulates the live timer loop (`decide` sleep refresh `decide` ...) over a trace's
/// observed span for a given `ReplayPolicy`, pure and deterministic: the same trace and policy
/// always produce the same `ReplayMetrics`, since every input the policy sees comes from the
/// trace, never from a live clock.
///
/// Ground truth vs. reconstructed signal: `menuOpen` events are ground truth a menu either
/// opened at a timestamp or it didn't, independent of any policy. `lowPowerModeEnabled` and
/// `thermalState`, by contrast, are only *sampled* at the timestamps the trace's original
/// `decision` events happened to occur at (whatever policy produced the trace). When a candidate
/// policy's own tick times fall between those samples, the engine holds the most recent known
/// value (step function). This is the phase-1 approximation: without a continuous power/thermal
/// signal in the trace, "most recent sample" is the best available reconstruction. Before the
/// first known sample, the earliest available sample is used (hold-first).
///
/// Interaction advances: this is a *counterfactual* replay, not a literal replay of whatever the
/// recording policy happened to do each candidate policy gets its own tick schedule computed
/// fresh from `policy.decide(_:)`. To reproduce `UsageStore.noteMenuOpened(at:)`'s "pull the timer
/// forward" behavior (see `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`) for
/// *any* candidate policy, every `menuOpen` event that falls inside a policy's current tick window
/// is independently re-evaluated: if `policy.advancesOnInteraction` and the decision computed as of
/// that menu open would land earlier than the already-scheduled next tick, the schedule advances to
/// that earlier time, exactly like `startTimer(preservingResetBoundaryRefresh: true)` replacing a
/// pending sleep with a shorter one. Recorded `timerAdvanced` events are audited separately: their
/// count is not expected to equal
/// this counterfactual schedule because live refresh work has non-zero duration and can coalesce.
public enum ReplayEngine {
/// Safety valve against a pathological policy (e.g. a zero-or-negative delay bug) turning a
/// long trace into an unbounded loop.
private static let maxIterations = 2_000_000
/// The trace-derived, replay-invariant inputs the simulation loop reads on every tick:
/// menu-open ground truth plus the sampled power/thermal signal, both precomputed and sorted
/// once per `run` so the per-tick lookups stay O(log n).
private struct TraceSignals {
let menuOpenTimestamps: [Date]
let signalSamples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)]
let signalTimestamps: [Date]
let activitySamples: [ActivityObservation]
let activityTimestamps: [Date]
}
private struct ActivityObservation {
let timestamp: Date
let lastCodingActivityAt: Date?
}
public static func run(trace: [AdaptiveRefreshTraceRecord], policy: some ReplayPolicy) -> ReplayMetrics {
self.runDetailed(trace: trace, policy: policy).metrics
}
static func runDetailed(
trace: [AdaptiveRefreshTraceRecord],
policy: some ReplayPolicy,
stalenessStartAt: Date? = nil) -> ReplayRun
{
guard let start = trace.map(\.timestamp).min(), let end = trace.map(\.timestamp).max() else {
return ReplayRun(
metrics: ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: 0,
totalRefreshCount: 0,
refreshCountPer24h: 0,
stalenessAtMenuOpen: nil,
constrainedCompliance: ConstrainedCompliance(constrainedDecisionCount: 0, violationCount: 0)),
stalenessSamples: [])
}
let menuOpenTimestamps = trace
.filter { $0.kind == .menuOpen }
.map(\.timestamp)
.sorted()
let signalSamples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)] = trace
.filter { $0.kind == .decision }
.compactMap { record in
guard let lowPower = record.lowPowerModeEnabled, let thermal = record.thermalState else {
return nil
}
return (timestamp: record.timestamp, lowPower: lowPower, thermal: thermal)
}
.sorted { $0.timestamp < $1.timestamp }
let activitySamples = trace
.filter { $0.kind == .decision }
.map { record in
let activityDates = [record.codexActivitySeconds, record.claudeActivitySeconds]
.compactMap(\.self)
.map { record.timestamp.addingTimeInterval(-max(0, $0)) }
return ActivityObservation(
timestamp: record.timestamp,
lastCodingActivityAt: activityDates.max())
}
.sorted { $0.timestamp < $1.timestamp }
let signals = TraceSignals(
menuOpenTimestamps: menuOpenTimestamps,
signalSamples: signalSamples,
signalTimestamps: signalSamples.map(\.timestamp),
activitySamples: activitySamples,
activityTimestamps: activitySamples.map(\.timestamp))
var cursor = start
var refreshTimestamps: [Date] = []
var constrainedDecisionCount = 0
var violationCount = 0
var interactionAdvanceCount = 0
var codingActiveDecisionCount = 0
var codingActiveDelayViolationCount = 0
var iterations = 0
// Monotonic pointer into `menuOpenTimestamps`: the scan below considers each menu open for
// an advance at most once, in the single tick window (cursor, next] it falls into.
var menuOpenScanIndex = 0
while cursor <= end, iterations < self.maxIterations {
iterations += 1
let (lowPower, thermal) = self.signal(
signals.signalSamples,
timestamps: signals.signalTimestamps,
at: cursor)
let input = ReplayPolicyInput(
now: cursor,
lastMenuOpenAt: self.lastValue(menuOpenTimestamps, atOrBefore: cursor),
lastCodingActivityAt: self.lastActivity(
signals.activitySamples,
timestamps: signals.activityTimestamps,
at: cursor),
lowPowerModeEnabled: lowPower,
thermalState: thermal)
let decision = policy.decide(input)
if input.isConstrained {
constrainedDecisionCount += 1
if let delay = decision.delaySeconds, delay < 1800 {
violationCount += 1
}
}
if !input.isConstrained,
let activityAge = input.codingActivityAgeSeconds,
activityAge < 5 * 60
{
codingActiveDecisionCount += 1
if decision.delaySeconds.map({ $0 <= 0 || $0 > 5 * 60 }) ?? true {
codingActiveDelayViolationCount += 1
}
}
guard let delay = decision.delaySeconds, delay > 0 else { break }
var next = cursor.addingTimeInterval(delay)
if policy.advancesOnInteraction {
let advanced = self.applyInteractionAdvances(
policy: policy,
signals: signals,
scanIndex: &menuOpenScanIndex,
windowStart: cursor,
scheduledAt: next)
next = advanced.scheduledAt
interactionAdvanceCount += advanced.advanceCount
}
guard next <= end else { break }
refreshTimestamps.append(next)
cursor = next
}
let span = end.timeIntervalSince(start)
let refreshCountPer24h = span > 0 ? Double(refreshTimestamps.count) * 86400 / span : 0
let stalenessMenuTimestamps = stalenessStartAt.map { start in
menuOpenTimestamps.filter { $0 >= start }
} ?? menuOpenTimestamps
let stalenessSamples = stalenessMenuTimestamps.isEmpty ? [] : self.stalenessSamples(
menuOpenTimestamps: stalenessMenuTimestamps,
refreshTimestamps: refreshTimestamps,
initialFreshAt: stalenessStartAt ?? start)
return ReplayRun(
metrics: ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: span,
totalRefreshCount: refreshTimestamps.count,
refreshCountPer24h: refreshCountPer24h,
stalenessAtMenuOpen: StalenessStats(samples: stalenessSamples),
constrainedCompliance: ConstrainedCompliance(
constrainedDecisionCount: constrainedDecisionCount,
violationCount: violationCount),
interactionAdvanceCount: interactionAdvanceCount,
codingActiveDecisionCount: codingActiveDecisionCount,
codingActiveDelayViolationCount: codingActiveDelayViolationCount),
stalenessSamples: stalenessSamples)
}
/// Re-evaluates every not-yet-scanned menu open that falls in `(windowStart, scheduledAt]`
/// against `policy`, mirroring `UsageStore.shouldAdvanceAdaptiveTimer(scheduledAt:candidate:)`:
/// a menu open at time `T` computes `policy.decide(now: T, lastMenuOpenAt: T, ...)` (age zero,
/// exactly as `noteMenuOpened(at:)` does with `self.lastMenuOpenAt = date` already applied), and
/// if the resulting candidate (`T + delay`) lands earlier than the currently scheduled refresh,
/// the schedule advances to that candidate. Later menu opens in the same window are then
/// compared against the *advanced* schedule, same as a real second interaction tightening an
/// already-shortened sleep. Returns the (possibly advanced) scheduled time plus how many
/// advances were taken in this window.
private static func applyInteractionAdvances(
policy: some ReplayPolicy,
signals: TraceSignals,
scanIndex: inout Int,
windowStart: Date,
scheduledAt: Date) -> (scheduledAt: Date, advanceCount: Int)
{
var next = scheduledAt
var advanceCount = 0
while scanIndex < signals.menuOpenTimestamps.count {
let menuOpenAt = signals.menuOpenTimestamps[scanIndex]
guard menuOpenAt > windowStart else {
scanIndex += 1
continue
}
guard menuOpenAt <= next else { break }
let (lowPower, thermal) = self.signal(
signals.signalSamples,
timestamps: signals.signalTimestamps,
at: menuOpenAt)
let advanceDecision = policy.decide(ReplayPolicyInput(
now: menuOpenAt,
lastMenuOpenAt: menuOpenAt,
lastCodingActivityAt: self.lastActivity(
signals.activitySamples,
timestamps: signals.activityTimestamps,
at: menuOpenAt),
lowPowerModeEnabled: lowPower,
thermalState: thermal))
scanIndex += 1
guard let advanceDelay = advanceDecision.delaySeconds, advanceDelay > 0 else { continue }
let candidate = menuOpenAt.addingTimeInterval(advanceDelay)
if candidate < next {
next = candidate
advanceCount += 1
}
}
return (next, advanceCount)
}
private static func stalenessSamples(
menuOpenTimestamps: [Date],
refreshTimestamps: [Date],
initialFreshAt: Date) -> [Double]
{
menuOpenTimestamps.map { menuOpenAt in
let simulatedRefresh = self.lastValue(refreshTimestamps, atOrBefore: menuOpenAt)
let freshestAt = simulatedRefresh.map { max($0, initialFreshAt) } ?? initialFreshAt
return menuOpenAt.timeIntervalSince(freshestAt)
}
}
private static func lastActivity(
_ samples: [ActivityObservation],
timestamps: [Date],
at time: Date) -> Date?
{
guard let index = self.lastIndex(timestamps, atOrBefore: time) else { return nil }
return samples[index].lastCodingActivityAt
}
/// Binds the most recent power/thermal sample at or before `time` (hold-last), falling back
/// to the earliest known sample when `time` precedes every sample (hold-first), and to
/// nominal/not-low-power when no samples exist at all.
private static func signal(
_ samples: [(timestamp: Date, lowPower: Bool, thermal: ReplayThermalState)],
timestamps: [Date],
at time: Date) -> (Bool, ReplayThermalState)
{
guard !samples.isEmpty else { return (false, .nominal) }
if let index = self.lastIndex(timestamps, atOrBefore: time) {
return (samples[index].lowPower, samples[index].thermal)
}
return (samples[0].lowPower, samples[0].thermal)
}
private static func lastValue(_ timestamps: [Date], atOrBefore time: Date) -> Date? {
guard let index = self.lastIndex(timestamps, atOrBefore: time) else { return nil }
return timestamps[index]
}
/// Binary search for the last index whose timestamp is `<= time`, assuming `timestamps` is
/// sorted ascending. O(log n) so a long trace (thousands of decisions) stays fast to replay.
private static func lastIndex(_ timestamps: [Date], atOrBefore time: Date) -> Int? {
var low = 0
var high = timestamps.count - 1
var result: Int?
while low <= high {
let mid = (low + high) / 2
if timestamps[mid] <= time {
result = mid
low = mid + 1
} else {
high = mid - 1
}
}
return result
}
}
@@ -0,0 +1,108 @@
import Foundation
/// Mean/median/p95 of staleness (seconds since the last simulated refresh) observed at each
/// historical menu-open event. `p95` uses nearest-rank: samples are sorted ascending and index
/// `ceil(0.95 * n) - 1` (clamped to the last index) is reported the same convention most
/// dashboards use for small-to-medium sample counts, and simple enough to hand-verify in tests.
public struct StalenessStats: Sendable, Equatable {
public let mean: Double
public let median: Double
public let p95: Double
public let sampleCount: Int
public init(mean: Double, median: Double, p95: Double, sampleCount: Int) {
self.mean = mean
self.median = median
self.p95 = p95
self.sampleCount = sampleCount
}
init?(samples: [Double]) {
guard !samples.isEmpty else { return nil }
let sorted = samples.sorted()
self.init(
mean: sorted.reduce(0, +) / Double(sorted.count),
median: Self.percentile(sorted, fraction: 0.5),
p95: Self.percentile(sorted, fraction: 0.95),
sampleCount: sorted.count)
}
private static func percentile(_ sorted: [Double], fraction: Double) -> Double {
let rank = Int((fraction * Double(sorted.count)).rounded(.up))
return sorted[max(0, min(sorted.count - 1, rank - 1))]
}
}
/// Whether a policy honored the "never refresh faster than 30 minutes while constrained (low
/// power or serious/critical thermal)" rule at every simulated decision point where the input was
/// constrained.
public struct ConstrainedCompliance: Sendable, Equatable {
public let constrainedDecisionCount: Int
public let violationCount: Int
public init(constrainedDecisionCount: Int, violationCount: Int) {
self.constrainedDecisionCount = constrainedDecisionCount
self.violationCount = violationCount
}
public var isCompliant: Bool {
self.violationCount == 0
}
}
public struct ReplayMetrics: Sendable, Equatable {
public let policyName: String
public let simulatedSpanSeconds: TimeInterval
public let totalRefreshCount: Int
public let refreshCountPer24h: Double
public let stalenessAtMenuOpen: StalenessStats?
public let constrainedCompliance: ConstrainedCompliance
/// How many of `totalRefreshCount` were pulled forward by a menu-open interaction rather than
/// firing on the policy's own previously scheduled cadence i.e. how many times
/// `ReplayEngine.run` took the `advancesOnInteraction` branch for this policy. Always `0` for
/// policies that report `advancesOnInteraction == false` (see `ReplayPolicy`).
public let interactionAdvanceCount: Int
/// Unconstrained replayed decisions with a known transcript-write observation under five minutes old.
public let codingActiveDecisionCount: Int
/// Unconstrained active decisions whose selected delay exceeded the five-minute acceptance cap.
public let codingActiveDelayViolationCount: Int
/// Number of independently simulated awake/run segments contributing to these metrics.
public let segmentCount: Int
/// Wall-clock time excluded after an expected timer deadline because the app was unobserved.
public let excludedGapSeconds: TimeInterval
/// Menu opens before a segment's first recorded refresh, excluded equally for every policy.
public let boundaryCensoredMenuOpenCount: Int
public init(
policyName: String,
simulatedSpanSeconds: TimeInterval,
totalRefreshCount: Int,
refreshCountPer24h: Double,
stalenessAtMenuOpen: StalenessStats?,
constrainedCompliance: ConstrainedCompliance,
interactionAdvanceCount: Int = 0,
codingActiveDecisionCount: Int = 0,
codingActiveDelayViolationCount: Int = 0,
segmentCount: Int = 1,
excludedGapSeconds: TimeInterval = 0,
boundaryCensoredMenuOpenCount: Int = 0)
{
self.policyName = policyName
self.simulatedSpanSeconds = simulatedSpanSeconds
self.totalRefreshCount = totalRefreshCount
self.refreshCountPer24h = refreshCountPer24h
self.stalenessAtMenuOpen = stalenessAtMenuOpen
self.constrainedCompliance = constrainedCompliance
self.interactionAdvanceCount = interactionAdvanceCount
self.codingActiveDecisionCount = codingActiveDecisionCount
self.codingActiveDelayViolationCount = codingActiveDelayViolationCount
self.segmentCount = segmentCount
self.excludedGapSeconds = excludedGapSeconds
self.boundaryCensoredMenuOpenCount = boundaryCensoredMenuOpenCount
}
}
struct ReplayRun: Sendable {
let metrics: ReplayMetrics
let stalenessSamples: [Double]
}
@@ -0,0 +1,90 @@
import Foundation
// Replay harness for the adaptive refresh policy shipped in the `CodexBar` app target. The app
// and replay adapter both call `AdaptiveRefreshPolicyCore`; these types only normalize replay
// inputs and report replay-friendly output.
/// Coarse thermal-pressure signal matching the two `ProcessInfo.ThermalState` cases the policy
/// distinguishes (`.serious`/`.critical` vs everything else), expressed independently so this
/// library never needs Darwin-only APIs and can build on any platform.
public enum ReplayThermalState: String, Sendable, Codable, CaseIterable {
case nominal
case fair
case serious
case critical
public var isConstrained: Bool {
self == .serious || self == .critical
}
}
/// The inputs a refresh-timing policy needs to decide how long to wait before the next refresh.
/// Replay-specific policy input. Platform-independent fields map into the shared policy core.
public struct ReplayPolicyInput: Sendable, Equatable {
public let now: Date
public let lastMenuOpenAt: Date?
/// Most recent transcript write reconstructed from the latest activity observation available
/// at or before `now`. This is nil when that observation could not see either CLI.
public let lastCodingActivityAt: Date?
public let lowPowerModeEnabled: Bool
public let thermalState: ReplayThermalState
public init(
now: Date,
lastMenuOpenAt: Date?,
lastCodingActivityAt: Date? = nil,
lowPowerModeEnabled: Bool,
thermalState: ReplayThermalState)
{
self.now = now
self.lastMenuOpenAt = lastMenuOpenAt
self.lastCodingActivityAt = lastCodingActivityAt
self.lowPowerModeEnabled = lowPowerModeEnabled
self.thermalState = thermalState
}
/// Whether this input represents a power/thermal-constrained moment, independent of which
/// policy is deciding. Used by the replay engine to score constrained-tier compliance without
/// depending on any single policy's own notion of "constrained".
public var isConstrained: Bool {
self.lowPowerModeEnabled || self.thermalState.isConstrained
}
public var codingActivityAgeSeconds: TimeInterval? {
self.lastCodingActivityAt.map { max(0, self.now.timeIntervalSince($0)) }
}
}
/// A policy's decision: how long to wait, and a short human-readable reason code for reporting.
/// `delaySeconds == nil` means "never schedule another refresh" the degenerate floor used by
/// `ManualPolicy`.
public struct ReplayPolicyDecision: Sendable, Equatable {
public let delaySeconds: TimeInterval?
public let reason: String
public init(delaySeconds: TimeInterval?, reason: String) {
self.delaySeconds = delaySeconds
self.reason = reason
}
}
/// A pure, deterministic function from `ReplayPolicyInput` to `ReplayPolicyDecision`.
public protocol ReplayPolicy: Sendable {
var name: String { get }
/// Whether opening the menu can pull this policy's next refresh forward, mirroring
/// `UsageStore.noteMenuOpened(at:)`'s guard on `settings.refreshFrequency == .adaptive`: in the
/// real app, only adaptive mode ever advances the timer from an interaction fixed-cadence and
/// manual modes just record `lastMenuOpenAt` and let the existing schedule run. Defaults to
/// `false` so baseline policies (`FixedIntervalPolicy`, `ManualPolicy`) need no override; only
/// policies that actually model the adaptive table set this to `true`.
var advancesOnInteraction: Bool { get }
func decide(_ input: ReplayPolicyInput) -> ReplayPolicyDecision
}
extension ReplayPolicy {
public var advancesOnInteraction: Bool {
false
}
}
@@ -0,0 +1,123 @@
import Foundation
public struct ReplayTraceSegment: Sendable, Equatable {
public let records: [AdaptiveRefreshTraceRecord]
public let start: Date
public let end: Date
var replayRecords: [AdaptiveRefreshTraceRecord] {
guard self.records.last?.timestamp != self.end else { return self.records }
return self.records + [.refreshCompleted(timestamp: self.end)]
}
}
public struct ReplaySegmentationReport: Sendable, Equatable {
public let segments: [ReplayTraceSegment]
public let excludedGapSeconds: TimeInterval
public let breakCount: Int
public let graceSeconds: TimeInterval
public var includedSpanSeconds: TimeInterval {
self.segments.reduce(0) { $0 + max(0, $1.end.timeIntervalSince($1.start)) }
}
}
/// Splits legacy traces only when observation resumes well after the last timer deadline. The
/// normal scheduled wait remains inside the preceding segment; only overdue wall time is excluded.
public enum ReplayTraceSegmenter {
public static let defaultGraceSeconds: TimeInterval = 5 * 60
public static func automatic(
_ records: [AdaptiveRefreshTraceRecord],
graceSeconds: TimeInterval = Self.defaultGraceSeconds) -> ReplaySegmentationReport
{
let sorted = records.sorted { $0.timestamp < $1.timestamp }
guard let first = sorted.first else {
return ReplaySegmentationReport(
segments: [], excludedGapSeconds: 0, breakCount: 0, graceSeconds: graceSeconds)
}
var segments: [ReplayTraceSegment] = []
var currentRecords: [AdaptiveRefreshTraceRecord] = []
var currentStart = first.timestamp
var expectedDeadline: Date?
var excludedGapSeconds: TimeInterval = 0
for record in sorted {
if let deadline = expectedDeadline,
record.timestamp.timeIntervalSince(deadline) > graceSeconds,
!currentRecords.isEmpty
{
let end = max(currentRecords.last!.timestamp, deadline)
segments.append(ReplayTraceSegment(records: currentRecords, start: currentStart, end: end))
excludedGapSeconds += max(0, record.timestamp.timeIntervalSince(end))
currentRecords = []
currentStart = record.timestamp
expectedDeadline = nil
}
currentRecords.append(record)
if record.kind == .decision, let delay = record.delaySeconds, delay > 0 {
expectedDeadline = record.timestamp.addingTimeInterval(delay)
} else if record.kind == .timerAdvanced, let candidate = record.candidateScheduledAt {
expectedDeadline = candidate
}
}
if let last = currentRecords.last {
segments.append(ReplayTraceSegment(records: currentRecords, start: currentStart, end: last.timestamp))
}
return ReplaySegmentationReport(
segments: segments,
excludedGapSeconds: excludedGapSeconds,
breakCount: max(0, segments.count - 1),
graceSeconds: graceSeconds)
}
}
extension ReplayEngine {
public static func runSegmented(
trace: [AdaptiveRefreshTraceRecord],
policy: some ReplayPolicy,
graceSeconds: TimeInterval = ReplayTraceSegmenter.defaultGraceSeconds) -> ReplayMetrics
{
let report = ReplayTraceSegmenter.automatic(trace, graceSeconds: graceSeconds)
let stalenessStarts = report.segments.map { segment in
segment.records.first(where: { $0.kind == .refreshCompleted })?.timestamp
}
let runs = zip(report.segments, stalenessStarts).map { segment, stalenessStart in
self.runDetailed(
trace: segment.replayRecords,
policy: policy,
stalenessStartAt: stalenessStart ?? .distantFuture)
}
let boundaryCensoredMenuOpenCount = zip(report.segments, stalenessStarts).reduce(0) { partial, pair in
let (segment, stalenessStart) = pair
return partial + segment.records.count(where: { record in
record.kind == .menuOpen && (stalenessStart.map { record.timestamp < $0 } ?? true)
})
}
let span = report.includedSpanSeconds
let refreshCount = runs.reduce(0) { $0 + $1.metrics.totalRefreshCount }
let stalenessSamples = runs.flatMap(\.stalenessSamples)
return ReplayMetrics(
policyName: policy.name,
simulatedSpanSeconds: span,
totalRefreshCount: refreshCount,
refreshCountPer24h: span > 0 ? Double(refreshCount) * 86400 / span : 0,
stalenessAtMenuOpen: StalenessStats(samples: stalenessSamples),
constrainedCompliance: ConstrainedCompliance(
constrainedDecisionCount: runs.reduce(0) {
$0 + $1.metrics.constrainedCompliance.constrainedDecisionCount
},
violationCount: runs.reduce(0) { $0 + $1.metrics.constrainedCompliance.violationCount }),
interactionAdvanceCount: runs.reduce(0) { $0 + $1.metrics.interactionAdvanceCount },
codingActiveDecisionCount: runs.reduce(0) { $0 + $1.metrics.codingActiveDecisionCount },
codingActiveDelayViolationCount: runs.reduce(0) {
$0 + $1.metrics.codingActiveDelayViolationCount
},
segmentCount: report.segments.count,
excludedGapSeconds: report.excludedGapSeconds,
boundaryCensoredMenuOpenCount: boundaryCensoredMenuOpenCount)
}
}