chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import AdaptiveReplayKit
|
||||
import Foundation
|
||||
|
||||
enum ReplayPolicyName: String, CaseIterable, Sendable {
|
||||
case adaptive
|
||||
case adaptiveActivity = "adaptive-activity"
|
||||
case fixed2Minutes = "fixed-2m"
|
||||
case fixed5Minutes = "fixed-5m"
|
||||
case fixed15Minutes = "fixed-15m"
|
||||
case fixed30Minutes = "fixed-30m"
|
||||
case manual
|
||||
|
||||
var policy: any ReplayPolicy {
|
||||
switch self {
|
||||
case .adaptive:
|
||||
AdaptiveReplayPolicy()
|
||||
case .adaptiveActivity:
|
||||
CodingActivityAdaptivePolicy()
|
||||
case .fixed2Minutes:
|
||||
FixedIntervalPolicy(minutes: 2)
|
||||
case .fixed5Minutes:
|
||||
FixedIntervalPolicy(minutes: 5)
|
||||
case .fixed15Minutes:
|
||||
FixedIntervalPolicy(minutes: 15)
|
||||
case .fixed30Minutes:
|
||||
FixedIntervalPolicy(minutes: 30)
|
||||
case .manual:
|
||||
ManualPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
static var expectedValues: String {
|
||||
allCases.map(\.rawValue).joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
enum CLIArguments {
|
||||
case run(
|
||||
tracePath: String,
|
||||
policyNames: [ReplayPolicyName],
|
||||
jsonOutput: Bool,
|
||||
gapGraceSeconds: TimeInterval?)
|
||||
case help(exitCode: Int32)
|
||||
case invalid(message: String)
|
||||
|
||||
static func parse(_ arguments: [String]) -> Self {
|
||||
if arguments.contains("-h") || arguments.contains("--help") {
|
||||
return .help(exitCode: EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
var tracePath: String?
|
||||
var policyNames: [ReplayPolicyName] = []
|
||||
var jsonOutput = false
|
||||
var gapGraceSeconds: TimeInterval? = ReplayTraceSegmenter.defaultGraceSeconds
|
||||
var index = 0
|
||||
while index < arguments.count {
|
||||
let argument = arguments[index]
|
||||
switch argument {
|
||||
case "--json":
|
||||
jsonOutput = true
|
||||
case "--raw-wall-clock":
|
||||
gapGraceSeconds = nil
|
||||
case "--gap-grace":
|
||||
index += 1
|
||||
guard index < arguments.count,
|
||||
let seconds = TimeInterval(arguments[index]),
|
||||
seconds >= 0,
|
||||
seconds.isFinite
|
||||
else { return .invalid(message: "--gap-grace requires non-negative finite seconds") }
|
||||
gapGraceSeconds = seconds
|
||||
case "--policy":
|
||||
index += 1
|
||||
guard index < arguments.count else { return .invalid(message: "--policy requires a value") }
|
||||
let rawPolicyName = arguments[index]
|
||||
guard let policyName = ReplayPolicyName(rawValue: rawPolicyName) else {
|
||||
return .invalid(
|
||||
message: "unknown policy '\(rawPolicyName)' (expected: \(ReplayPolicyName.expectedValues))")
|
||||
}
|
||||
policyNames.append(policyName)
|
||||
default:
|
||||
guard tracePath == nil else {
|
||||
return .invalid(message: "unexpected argument '\(argument)'")
|
||||
}
|
||||
tracePath = argument
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
guard let tracePath else {
|
||||
return .help(exitCode: EXIT_FAILURE)
|
||||
}
|
||||
return .run(
|
||||
tracePath: tracePath,
|
||||
policyNames: policyNames.isEmpty ? ReplayPolicyName.allCases : policyNames,
|
||||
jsonOutput: jsonOutput,
|
||||
gapGraceSeconds: gapGraceSeconds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import AdaptiveReplayKit
|
||||
import Foundation
|
||||
|
||||
/// Thin CLI shell over `AdaptiveReplayKit`: parses a trace path and a policy name, runs the
|
||||
/// replay, and prints the resulting `ReplayMetrics`. All parsing/replay/metrics logic lives in
|
||||
/// the library — this file only routes arguments to it and formats the result.
|
||||
enum AdaptiveReplayCLI {
|
||||
static func main() {
|
||||
let arguments = CLIArguments.parse(Array(CommandLine.arguments.dropFirst()))
|
||||
|
||||
switch arguments {
|
||||
case let .help(exitCode):
|
||||
print(Self.helpText)
|
||||
exit(exitCode)
|
||||
case let .invalid(message):
|
||||
FileHandle.standardError.write(Data("error: \(message)\n\n\(Self.helpText)\n".utf8))
|
||||
exit(EXIT_FAILURE)
|
||||
case let .run(tracePath, policyNames, jsonOutput, gapGraceSeconds):
|
||||
Self.run(
|
||||
tracePath: tracePath,
|
||||
policyNames: policyNames,
|
||||
jsonOutput: jsonOutput,
|
||||
gapGraceSeconds: gapGraceSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
private static func run(
|
||||
tracePath: String,
|
||||
policyNames: [ReplayPolicyName],
|
||||
jsonOutput: Bool,
|
||||
gapGraceSeconds: TimeInterval?)
|
||||
{
|
||||
let records: [AdaptiveRefreshTraceRecord]
|
||||
do {
|
||||
records = try AdaptiveRefreshTraceParser.parse(contentsOf: URL(fileURLWithPath: tracePath))
|
||||
} catch {
|
||||
FileHandle.standardError.write(Data("error: failed to parse trace: \(error)\n".utf8))
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
|
||||
let policies = policyNames.map(\.policy)
|
||||
|
||||
let results = policies.map { policy in
|
||||
gapGraceSeconds.map {
|
||||
ReplayEngine.runSegmented(trace: records, policy: policy, graceSeconds: $0)
|
||||
} ?? ReplayEngine.run(trace: records, policy: policy)
|
||||
}
|
||||
let activityCoverage = ActivityCoverageStats.compute(from: records)
|
||||
let recordedScheduleAudit = RecordedScheduleAuditor.audit(records)
|
||||
|
||||
if jsonOutput {
|
||||
print(Self.renderJSON(
|
||||
results,
|
||||
activityCoverage: activityCoverage,
|
||||
recordedScheduleAudit: recordedScheduleAudit,
|
||||
gapGraceSeconds: gapGraceSeconds))
|
||||
} else {
|
||||
print(Self.renderTable(results))
|
||||
print(Self.renderActivityCoverage(activityCoverage))
|
||||
print(Self.renderRecordedScheduleAudit(recordedScheduleAudit))
|
||||
if let gapGraceSeconds, let first = results.first {
|
||||
print(String(
|
||||
format: "segmentation: %d segments, %.2fh excluded (legacy heuristic, %.0fs grace)",
|
||||
first.segmentCount,
|
||||
first.excludedGapSeconds / 3600,
|
||||
gapGraceSeconds))
|
||||
} else {
|
||||
print("segmentation: disabled (raw wall clock)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func renderRecordedScheduleAudit(_ audit: RecordedScheduleAudit) -> String {
|
||||
"recorded schedule: \(audit.recordedAdvanceCount) advances, "
|
||||
+ "\(audit.acceptedEvaluationCount)/\(audit.evaluatedCount) evaluations accepted, "
|
||||
+ "payload=\(audit.payloadMismatchCount) decision=\(audit.decisionMismatchCount) "
|
||||
+ "menu-link=\(audit.menuLinkMismatchCount) mismatches, "
|
||||
+ "ambiguous=\(audit.ambiguousComparisonCount)"
|
||||
}
|
||||
|
||||
/// Reports coverage of optional activity observations already present in the input trace.
|
||||
private static func renderActivityCoverage(_ stats: ActivityCoverageStats) -> String {
|
||||
guard stats.decisionCount > 0 else {
|
||||
return "activity telemetry: no decision events in trace"
|
||||
}
|
||||
let sampledSummary = String(
|
||||
format: "%d/%d decisions sampled (%.0f%%)",
|
||||
stats.sampledCount,
|
||||
stats.decisionCount,
|
||||
stats.sampledFraction * 100)
|
||||
let activeSummary = String(
|
||||
format: "%d/%d active coding at decision time (%.0f%%)",
|
||||
stats.activeCount,
|
||||
stats.sampledCount,
|
||||
stats.activeFraction * 100)
|
||||
return "activity telemetry: \(sampledSummary), \(activeSummary)"
|
||||
}
|
||||
|
||||
private static func renderTable(_ results: [ReplayMetrics]) -> String {
|
||||
var lines: [String] = []
|
||||
let header = [
|
||||
"policy", "refreshes", "per24h", "sim advances", "active >5m", "staleness p50",
|
||||
"staleness p95", "constrained ok",
|
||||
]
|
||||
lines.append(header.joined(separator: "\t"))
|
||||
for metrics in results {
|
||||
let staleness = metrics.stalenessAtMenuOpen
|
||||
lines.append([
|
||||
metrics.policyName,
|
||||
String(metrics.totalRefreshCount),
|
||||
String(format: "%.2f", metrics.refreshCountPer24h),
|
||||
String(metrics.interactionAdvanceCount),
|
||||
"\(metrics.codingActiveDelayViolationCount)/\(metrics.codingActiveDecisionCount)",
|
||||
staleness.map { String(format: "%.0fs", $0.median) } ?? "n/a",
|
||||
staleness.map { String(format: "%.0fs", $0.p95) } ?? "n/a",
|
||||
metrics.constrainedCompliance
|
||||
.isCompliant ? "yes" : "NO (\(metrics.constrainedCompliance.violationCount))",
|
||||
].joined(separator: "\t"))
|
||||
}
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func renderJSON(
|
||||
_ results: [ReplayMetrics],
|
||||
activityCoverage: ActivityCoverageStats,
|
||||
recordedScheduleAudit: RecordedScheduleAudit,
|
||||
gapGraceSeconds: TimeInterval?) -> String
|
||||
{
|
||||
let policies = results.map { metrics -> [String: Any] in
|
||||
var dict: [String: Any] = [
|
||||
"policy": metrics.policyName,
|
||||
"simulatedSpanSeconds": metrics.simulatedSpanSeconds,
|
||||
"totalRefreshCount": metrics.totalRefreshCount,
|
||||
"refreshCountPer24h": metrics.refreshCountPer24h,
|
||||
"interactionAdvanceCount": metrics.interactionAdvanceCount,
|
||||
"codingActiveDecisionCount": metrics.codingActiveDecisionCount,
|
||||
"codingActiveDelayViolationCount": metrics.codingActiveDelayViolationCount,
|
||||
"segmentCount": metrics.segmentCount,
|
||||
"excludedGapSeconds": metrics.excludedGapSeconds,
|
||||
"boundaryCensoredMenuOpenCount": metrics.boundaryCensoredMenuOpenCount,
|
||||
"constrainedDecisionCount": metrics.constrainedCompliance.constrainedDecisionCount,
|
||||
"constrainedViolationCount": metrics.constrainedCompliance.violationCount,
|
||||
"constrainedCompliant": metrics.constrainedCompliance.isCompliant,
|
||||
]
|
||||
if let staleness = metrics.stalenessAtMenuOpen {
|
||||
dict["stalenessMeanSeconds"] = staleness.mean
|
||||
dict["stalenessMedianSeconds"] = staleness.median
|
||||
dict["stalenessP95Seconds"] = staleness.p95
|
||||
dict["stalenessSampleCount"] = staleness.sampleCount
|
||||
}
|
||||
return dict
|
||||
}
|
||||
let segmentation: [String: Any] = [
|
||||
"mode": gapGraceSeconds == nil ? "rawWallClock" : "legacyGapHeuristic",
|
||||
"gapGraceSeconds": gapGraceSeconds.map { $0 as Any } ?? NSNull(),
|
||||
]
|
||||
let payload: [String: Any] = [
|
||||
"policies": policies,
|
||||
"activityCoverage": [
|
||||
"decisionCount": activityCoverage.decisionCount,
|
||||
"sampledCount": activityCoverage.sampledCount,
|
||||
"activeCount": activityCoverage.activeCount,
|
||||
"sampledFraction": activityCoverage.sampledFraction,
|
||||
"activeFraction": activityCoverage.activeFraction,
|
||||
],
|
||||
"recordedScheduleAudit": [
|
||||
"recordedAdvanceCount": recordedScheduleAudit.recordedAdvanceCount,
|
||||
"evaluatedCount": recordedScheduleAudit.evaluatedCount,
|
||||
"acceptedEvaluationCount": recordedScheduleAudit.acceptedEvaluationCount,
|
||||
"rejectedEvaluationCount": recordedScheduleAudit.rejectedEvaluationCount,
|
||||
"payloadMismatchCount": recordedScheduleAudit.payloadMismatchCount,
|
||||
"decisionMismatchCount": recordedScheduleAudit.decisionMismatchCount,
|
||||
"menuLinkMismatchCount": recordedScheduleAudit.menuLinkMismatchCount,
|
||||
"ambiguousComparisonCount": recordedScheduleAudit.ambiguousComparisonCount,
|
||||
"isValid": recordedScheduleAudit.isValid,
|
||||
],
|
||||
"segmentation": segmentation,
|
||||
]
|
||||
guard let data = try? JSONSerialization.data(
|
||||
withJSONObject: payload,
|
||||
options: [.prettyPrinted, .sortedKeys]),
|
||||
let text = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return "{}"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private static let helpText = """
|
||||
Usage: AdaptiveReplayCLI <trace.jsonl> [--policy <name>]... [--gap-grace <seconds>] [--raw-wall-clock] [--json]
|
||||
|
||||
Replays a JSONL adaptive-refresh trace against one or more refresh-timing policies and prints
|
||||
per-policy metrics over automatically segmented observed time. Simulated advances are
|
||||
counterfactual policy events; recorded live schedule evaluations are audited separately.
|
||||
|
||||
Policies:
|
||||
adaptive The shared production adaptive policy. Advances on menu-open interactions,
|
||||
same as UsageStore.noteMenuOpened(at:).
|
||||
adaptive-activity Experimental adaptive table capped at 5m after observed coding activity.
|
||||
fixed-2m Fixed 2 minute cadence. Unaffected by menu-open interactions.
|
||||
fixed-5m Fixed 5 minute cadence.
|
||||
fixed-15m Fixed 15 minute cadence.
|
||||
fixed-30m Fixed 30 minute cadence.
|
||||
manual Never refreshes (degenerate floor).
|
||||
|
||||
Defaults to comparing all seven policies when --policy is omitted.
|
||||
|
||||
Options:
|
||||
--policy <name> Restrict to one listed policy; repeat to compare a specific subset.
|
||||
--gap-grace <s> Split legacy gaps this many seconds after the last timer deadline (default 300).
|
||||
--raw-wall-clock Disable gap segmentation; useful only for auditing the old behavior.
|
||||
--json Print a machine-readable report including replay, activity, and audit data.
|
||||
-h, --help Print this help text.
|
||||
"""
|
||||
}
|
||||
|
||||
AdaptiveReplayCLI.main()
|
||||
Reference in New Issue
Block a user