@xerg/cli 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -122,23 +122,6 @@ import { closeSync, openSync, readSync } from "fs";
122
122
  function sha1(input) {
123
123
  return createHash("sha1").update(input).digest("hex");
124
124
  }
125
- function sha1File(path) {
126
- const hash = createHash("sha1");
127
- const fd = openSync(path, "r");
128
- const buffer = Buffer.allocUnsafe(64 * 1024);
129
- try {
130
- let bytesRead = 0;
131
- do {
132
- bytesRead = readSync(fd, buffer, 0, buffer.length, null);
133
- if (bytesRead > 0) {
134
- hash.update(buffer.subarray(0, bytesRead));
135
- }
136
- } while (bytesRead > 0);
137
- } finally {
138
- closeSync(fd);
139
- }
140
- return hash.digest("hex");
141
- }
142
125
 
143
126
  // ../core/src/utils/time.ts
144
127
  function parseSince(value) {
@@ -736,354 +719,80 @@ async function inspectCursorUsageCsv(options) {
736
719
  }
737
720
 
738
721
  // ../core/src/db/client.ts
739
- import { mkdirSync } from "fs";
722
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
740
723
  import { dirname } from "path";
741
- import Database from "better-sqlite3";
742
-
743
- // ../core/src/db/schema.ts
744
- var SCHEMA_VERSION = 1;
745
- var SCHEMA_SQL = `
746
- CREATE TABLE IF NOT EXISTS source_files (
747
- id TEXT PRIMARY KEY,
748
- path TEXT NOT NULL,
749
- kind TEXT NOT NULL,
750
- file_hash TEXT NOT NULL,
751
- mtime_ms INTEGER NOT NULL,
752
- size_bytes INTEGER NOT NULL,
753
- imported_at TEXT NOT NULL
754
- );
755
-
756
- CREATE TABLE IF NOT EXISTS runs (
757
- id TEXT PRIMARY KEY,
758
- source_path TEXT NOT NULL,
759
- source_kind TEXT NOT NULL,
760
- timestamp TEXT NOT NULL,
761
- workflow TEXT NOT NULL,
762
- environment TEXT NOT NULL,
763
- tags_json TEXT NOT NULL,
764
- total_cost_usd REAL NOT NULL,
765
- total_tokens INTEGER NOT NULL,
766
- observed_cost_usd REAL NOT NULL,
767
- estimated_cost_usd REAL NOT NULL
768
- );
769
-
770
- CREATE TABLE IF NOT EXISTS calls (
771
- id TEXT PRIMARY KEY,
772
- run_id TEXT NOT NULL,
773
- timestamp TEXT NOT NULL,
774
- provider TEXT NOT NULL,
775
- model TEXT NOT NULL,
776
- input_tokens INTEGER NOT NULL,
777
- output_tokens INTEGER NOT NULL,
778
- cost_usd REAL NOT NULL,
779
- cost_source TEXT NOT NULL,
780
- latency_ms INTEGER,
781
- tool_calls INTEGER NOT NULL,
782
- retries INTEGER NOT NULL,
783
- attempt INTEGER,
784
- iteration INTEGER,
785
- status TEXT,
786
- task_class TEXT,
787
- cache_hit INTEGER NOT NULL,
788
- cache_cost_usd REAL,
789
- metadata_json TEXT NOT NULL
790
- );
791
-
792
- CREATE TABLE IF NOT EXISTS findings (
793
- id TEXT PRIMARY KEY,
794
- audit_id TEXT NOT NULL,
795
- classification TEXT NOT NULL,
796
- confidence TEXT NOT NULL,
797
- kind TEXT NOT NULL,
798
- title TEXT NOT NULL,
799
- summary TEXT NOT NULL,
800
- scope TEXT NOT NULL,
801
- scope_id TEXT NOT NULL,
802
- cost_impact_usd REAL NOT NULL,
803
- details_json TEXT NOT NULL
804
- );
805
-
806
- CREATE TABLE IF NOT EXISTS pricing_catalog (
807
- id TEXT PRIMARY KEY,
808
- provider TEXT NOT NULL,
809
- model TEXT NOT NULL,
810
- effective_date TEXT NOT NULL,
811
- input_per_1m REAL NOT NULL,
812
- output_per_1m REAL NOT NULL,
813
- cached_input_per_1m REAL
814
- );
815
-
816
- CREATE TABLE IF NOT EXISTS audit_snapshots (
817
- id TEXT PRIMARY KEY,
818
- created_at TEXT NOT NULL,
819
- summary_json TEXT NOT NULL
820
- );
821
- `;
822
-
823
- // ../core/src/db/client.ts
824
- function createDb(path) {
825
- mkdirSync(dirname(path), { recursive: true });
826
- const sqlite = new Database(path);
827
- const currentVersion = sqlite.pragma("user_version", { simple: true });
828
- if (currentVersion > SCHEMA_VERSION) {
829
- sqlite.close();
830
- throw new Error(
831
- `Unsupported Xerg database schema version ${currentVersion}. This build supports up to ${SCHEMA_VERSION}.`
724
+ var SNAPSHOT_STORE_VERSION = 1;
725
+ function readStoredSnapshotRows(path) {
726
+ if (!existsSync(path)) {
727
+ return [];
728
+ }
729
+ try {
730
+ return normalizeStore(JSON.parse(readFileSync2(path, "utf8"))).auditSnapshots.sort(
731
+ (left, right) => right.createdAt.localeCompare(left.createdAt)
832
732
  );
733
+ } catch (error) {
734
+ const message = error instanceof Error ? error.message : "Unknown error";
735
+ process.stderr.write(`Warning: skipping unreadable local snapshot store ${path}: ${message}
736
+ `);
737
+ return [];
833
738
  }
834
- sqlite.exec(SCHEMA_SQL);
835
- if (currentVersion < SCHEMA_VERSION) {
836
- sqlite.pragma(`user_version = ${SCHEMA_VERSION}`);
739
+ }
740
+ function writeStoredSnapshotRows(path, rows) {
741
+ mkdirSync(dirname(path), { recursive: true });
742
+ const store = {
743
+ version: SNAPSHOT_STORE_VERSION,
744
+ auditSnapshots: rows.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
745
+ };
746
+ const tempPath = `${path}.tmp-${process.pid}-${Date.now()}`;
747
+ writeFileSync(tempPath, `${JSON.stringify(store, null, 2)}
748
+ `, "utf8");
749
+ renameSync(tempPath, path);
750
+ }
751
+ function upsertStoredAuditSnapshot(path, row) {
752
+ const rows = readStoredSnapshotRows(path);
753
+ if (rows.some((existing) => existing.id === row.id)) {
754
+ return;
837
755
  }
838
- return { sqlite };
756
+ writeStoredSnapshotRows(path, [...rows, row]);
839
757
  }
840
-
841
- // ../core/src/db/persist.ts
842
- function persistAudit(audit, dbPath) {
843
- const { sqlite } = createDb(dbPath);
844
- const importedAt = isoNow();
845
- const pricingRows = audit.pricingCatalog.map((entry) => ({
846
- ...entry,
847
- cachedInputPer1m: entry.cachedInputPer1m ?? null
848
- }));
849
- const sourceFileRows = audit.summary.sourceFiles.map((file) => ({
850
- id: sha1(`${file.path}:${file.mtimeMs}:${file.sizeBytes}`),
851
- path: file.path,
852
- kind: file.kind,
853
- fileHash: sha1File(file.path),
854
- mtimeMs: Math.trunc(file.mtimeMs),
855
- sizeBytes: file.sizeBytes,
856
- importedAt
857
- }));
858
- const runRows = audit.runs.map((run2) => ({
859
- id: run2.id,
860
- sourcePath: run2.sourcePath,
861
- sourceKind: run2.sourceKind,
862
- timestamp: run2.timestamp,
863
- workflow: run2.workflow,
864
- environment: run2.environment,
865
- tagsJson: JSON.stringify(run2.tags),
866
- totalCostUsd: run2.totalCostUsd,
867
- totalTokens: run2.totalTokens,
868
- observedCostUsd: run2.observedCostUsd,
869
- estimatedCostUsd: run2.estimatedCostUsd
870
- }));
871
- const callRows = audit.runs.flatMap(
872
- (run2) => run2.calls.map((call) => ({
873
- id: call.id,
874
- runId: call.runId,
875
- timestamp: call.timestamp,
876
- provider: call.provider,
877
- model: call.model,
878
- inputTokens: call.inputTokens,
879
- outputTokens: call.outputTokens,
880
- costUsd: call.costUsd,
881
- costSource: call.costSource,
882
- latencyMs: call.latencyMs,
883
- toolCalls: call.toolCalls,
884
- retries: call.retries,
885
- attempt: call.attempt,
886
- iteration: call.iteration,
887
- status: call.status,
888
- taskClass: call.taskClass,
889
- cacheHit: call.cacheHit,
890
- cacheCostUsd: call.cacheCostUsd,
891
- metadataJson: JSON.stringify(call.metadata)
892
- }))
893
- );
894
- const findingRows = audit.summary.findings.map((finding) => ({
895
- id: finding.id,
896
- auditId: audit.summary.auditId,
897
- classification: finding.classification,
898
- confidence: finding.confidence,
899
- kind: finding.kind,
900
- title: finding.title,
901
- summary: finding.summary,
902
- scope: finding.scope,
903
- scopeId: finding.scopeId,
904
- costImpactUsd: finding.costImpactUsd,
905
- detailsJson: JSON.stringify(finding.details)
906
- }));
907
- const persistTransaction = sqlite.transaction(() => {
908
- insertMany(
909
- sqlite,
910
- `
911
- INSERT OR IGNORE INTO pricing_catalog (
912
- id,
913
- provider,
914
- model,
915
- effective_date,
916
- input_per_1m,
917
- output_per_1m,
918
- cached_input_per_1m
919
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
920
- `,
921
- pricingRows.map((row) => [
922
- row.id,
923
- row.provider,
924
- row.model,
925
- row.effectiveDate,
926
- row.inputPer1m,
927
- row.outputPer1m,
928
- row.cachedInputPer1m
929
- ])
930
- );
931
- insertMany(
932
- sqlite,
933
- `
934
- INSERT OR IGNORE INTO source_files (
935
- id,
936
- path,
937
- kind,
938
- file_hash,
939
- mtime_ms,
940
- size_bytes,
941
- imported_at
942
- ) VALUES (?, ?, ?, ?, ?, ?, ?)
943
- `,
944
- sourceFileRows.map((row) => [
945
- row.id,
946
- row.path,
947
- row.kind,
948
- row.fileHash,
949
- row.mtimeMs,
950
- row.sizeBytes,
951
- row.importedAt
952
- ])
953
- );
954
- insertMany(
955
- sqlite,
956
- `
957
- INSERT OR IGNORE INTO runs (
958
- id,
959
- source_path,
960
- source_kind,
961
- timestamp,
962
- workflow,
963
- environment,
964
- tags_json,
965
- total_cost_usd,
966
- total_tokens,
967
- observed_cost_usd,
968
- estimated_cost_usd
969
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
970
- `,
971
- runRows.map((row) => [
972
- row.id,
973
- row.sourcePath,
974
- row.sourceKind,
975
- row.timestamp,
976
- row.workflow,
977
- row.environment,
978
- row.tagsJson,
979
- row.totalCostUsd,
980
- row.totalTokens,
981
- row.observedCostUsd,
982
- row.estimatedCostUsd
983
- ])
984
- );
985
- insertMany(
986
- sqlite,
987
- `
988
- INSERT OR IGNORE INTO calls (
989
- id,
990
- run_id,
991
- timestamp,
992
- provider,
993
- model,
994
- input_tokens,
995
- output_tokens,
996
- cost_usd,
997
- cost_source,
998
- latency_ms,
999
- tool_calls,
1000
- retries,
1001
- attempt,
1002
- iteration,
1003
- status,
1004
- task_class,
1005
- cache_hit,
1006
- cache_cost_usd,
1007
- metadata_json
1008
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1009
- `,
1010
- callRows.map((row) => [
1011
- row.id,
1012
- row.runId,
1013
- row.timestamp,
1014
- row.provider,
1015
- row.model,
1016
- row.inputTokens,
1017
- row.outputTokens,
1018
- row.costUsd,
1019
- row.costSource,
1020
- row.latencyMs ?? null,
1021
- row.toolCalls,
1022
- row.retries,
1023
- row.attempt ?? null,
1024
- row.iteration ?? null,
1025
- row.status ?? null,
1026
- row.taskClass ?? null,
1027
- row.cacheHit ? 1 : 0,
1028
- row.cacheCostUsd ?? null,
1029
- row.metadataJson
1030
- ])
1031
- );
1032
- insertMany(
1033
- sqlite,
1034
- `
1035
- INSERT OR IGNORE INTO findings (
1036
- id,
1037
- audit_id,
1038
- classification,
1039
- confidence,
1040
- kind,
1041
- title,
1042
- summary,
1043
- scope,
1044
- scope_id,
1045
- cost_impact_usd,
1046
- details_json
1047
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1048
- `,
1049
- findingRows.map((row) => [
1050
- row.id,
1051
- row.auditId,
1052
- row.classification,
1053
- row.confidence,
1054
- row.kind,
1055
- row.title,
1056
- row.summary,
1057
- row.scope,
1058
- row.scopeId,
1059
- row.costImpactUsd,
1060
- row.detailsJson
1061
- ])
1062
- );
1063
- sqlite.prepare(
1064
- `
1065
- INSERT OR IGNORE INTO audit_snapshots (
1066
- id,
1067
- created_at,
1068
- summary_json
1069
- ) VALUES (?, ?, ?)
1070
- `
1071
- ).run(audit.summary.auditId, audit.summary.generatedAt, JSON.stringify(audit.summary));
1072
- });
1073
- try {
1074
- persistTransaction();
1075
- } finally {
1076
- sqlite.close();
758
+ function normalizeStore(input) {
759
+ if (!input || typeof input !== "object") {
760
+ throw new Error("Snapshot store is not an object.");
1077
761
  }
762
+ const store = input;
763
+ if (store.version !== SNAPSHOT_STORE_VERSION) {
764
+ throw new Error(`Unsupported snapshot store version ${String(store.version)}.`);
765
+ }
766
+ if (!Array.isArray(store.auditSnapshots)) {
767
+ throw new Error("Snapshot store auditSnapshots field is missing or invalid.");
768
+ }
769
+ return {
770
+ version: SNAPSHOT_STORE_VERSION,
771
+ auditSnapshots: store.auditSnapshots.map(normalizeSnapshotRow)
772
+ };
1078
773
  }
1079
- function insertMany(sqlite, sql, rows) {
1080
- if (rows.length === 0) {
1081
- return;
774
+ function normalizeSnapshotRow(input) {
775
+ if (!input || typeof input !== "object") {
776
+ throw new Error("Snapshot row is not an object.");
1082
777
  }
1083
- const statement = sqlite.prepare(sql);
1084
- for (const row of rows) {
1085
- statement.run(...row);
778
+ const row = input;
779
+ if (typeof row.id !== "string" || typeof row.createdAt !== "string" || typeof row.summaryJson !== "string") {
780
+ throw new Error("Snapshot row is missing id, createdAt, or summaryJson.");
1086
781
  }
782
+ return {
783
+ id: row.id,
784
+ createdAt: row.createdAt,
785
+ summaryJson: row.summaryJson
786
+ };
787
+ }
788
+
789
+ // ../core/src/db/persist.ts
790
+ function persistAudit(audit, dbPath) {
791
+ upsertStoredAuditSnapshot(dbPath, {
792
+ id: audit.summary.auditId,
793
+ createdAt: audit.summary.generatedAt,
794
+ summaryJson: JSON.stringify(audit.summary)
795
+ });
1087
796
  }
1088
797
 
1089
798
  // ../core/src/report/comparison.ts
@@ -1165,6 +874,36 @@ function buildTaxonomyBuckets(findings, classification) {
1165
874
  }
1166
875
  return Array.from(buckets.values()).sort((left, right) => right.spendUsd - left.spendUsd);
1167
876
  }
877
+ function buildWasteBySignalSource(findings) {
878
+ const rollup = {
879
+ observedUsd: 0,
880
+ inferredUsd: 0,
881
+ declaredUsd: 0,
882
+ unknownUsd: 0,
883
+ inferredShare: null
884
+ };
885
+ for (const finding of findings) {
886
+ if (finding.classification !== "waste") {
887
+ continue;
888
+ }
889
+ if (finding.signalSource === "observed") {
890
+ rollup.observedUsd = round2(rollup.observedUsd + finding.costImpactUsd);
891
+ continue;
892
+ }
893
+ if (finding.signalSource === "inferred") {
894
+ rollup.inferredUsd = round2(rollup.inferredUsd + finding.costImpactUsd);
895
+ continue;
896
+ }
897
+ if (finding.signalSource === "declared") {
898
+ rollup.declaredUsd = round2(rollup.declaredUsd + finding.costImpactUsd);
899
+ continue;
900
+ }
901
+ rollup.unknownUsd = round2(rollup.unknownUsd + finding.costImpactUsd);
902
+ }
903
+ const knownTotal = rollup.observedUsd + rollup.inferredUsd + rollup.declaredUsd;
904
+ rollup.inferredShare = rollup.unknownUsd > 0 ? null : Number((knownTotal === 0 ? 0 : rollup.inferredUsd / knownTotal).toFixed(4));
905
+ return rollup;
906
+ }
1168
907
  function toSpendMap(rows) {
1169
908
  return new Map(rows.map((row) => [row.key, row.spendUsd]));
1170
909
  }
@@ -1280,6 +1019,7 @@ function hydrateAuditSummary(summary) {
1280
1019
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
1281
1020
  spendByDay: summary.spendByDay ?? [],
1282
1021
  wasteByDay: summary.wasteByDay ?? [],
1022
+ wasteBySignalSource: summary.wasteBySignalSource ?? buildWasteBySignalSource(summary.findings),
1283
1023
  recommendations: summary.recommendations ?? [],
1284
1024
  notes: summary.notes ?? [],
1285
1025
  pricingCoverage: summary.pricingCoverage ?? null,
@@ -1300,6 +1040,7 @@ function buildAuditComparison(current, baseline) {
1300
1040
  baselineWasteSpendUsd: baseline.wasteSpendUsd,
1301
1041
  baselineOpportunitySpendUsd: baseline.opportunitySpendUsd,
1302
1042
  baselineStructuralWasteRate: baseline.structuralWasteRate,
1043
+ baselineWasteBySignalSource: baseline.wasteBySignalSource ?? buildWasteBySignalSource(baseline.findings),
1303
1044
  deltaTotalSpendUsd: round2(current.totalSpendUsd - baseline.totalSpendUsd),
1304
1045
  deltaObservedSpendUsd: round2(current.observedSpendUsd - baseline.observedSpendUsd),
1305
1046
  deltaEstimatedSpendUsd: round2(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
@@ -1326,19 +1067,7 @@ function parseAuditSummary(row) {
1326
1067
  }
1327
1068
  }
1328
1069
  function listStoredAuditSummaries(dbPath) {
1329
- const { sqlite } = createDb(dbPath);
1330
- try {
1331
- const rows = sqlite.prepare(
1332
- `
1333
- SELECT id, summary_json AS summaryJson
1334
- FROM audit_snapshots
1335
- ORDER BY created_at DESC
1336
- `
1337
- ).all();
1338
- return rows.map((row) => parseAuditSummary(row)).filter((summary) => summary !== null);
1339
- } finally {
1340
- sqlite.close();
1341
- }
1070
+ return readStoredSnapshotRows(dbPath).map((row) => parseAuditSummary(row)).filter((summary) => summary !== null);
1342
1071
  }
1343
1072
  function readLatestComparableAuditSummary(input) {
1344
1073
  return listStoredAuditSummaries(input.dbPath).find((summary) => {
@@ -1350,6 +1079,8 @@ function readLatestComparableAuditSummary(input) {
1350
1079
  }
1351
1080
 
1352
1081
  // ../core/src/findings/cursor.ts
1082
+ var CACHE_CARRYOVER_RULE_ID = "cursor_cache_ratio_v1";
1083
+ var MAX_MODE_CONCENTRATION_RULE_ID = "cursor_max_mode_concentration_v1";
1353
1084
  function round3(value) {
1354
1085
  return Number(value.toFixed(6));
1355
1086
  }
@@ -1419,6 +1150,13 @@ function buildCursorUsageFindings(runs) {
1419
1150
  scopeId: "all",
1420
1151
  scopeLabel: "Cursor usage",
1421
1152
  costImpactUsd: cacheImpactUsd,
1153
+ signalSource: "observed",
1154
+ ruleId: CACHE_CARRYOVER_RULE_ID,
1155
+ evidence: {
1156
+ callIds: cacheAwareCalls.map((call) => call.id).sort(),
1157
+ runIds: Array.from(new Set(cacheAwareCalls.map((call) => call.runId))).sort(),
1158
+ sourceKinds: ["cursor-usage-csv"]
1159
+ },
1422
1160
  details: {
1423
1161
  cacheReadShare: round3(cacheReadShare),
1424
1162
  cacheCoverageShare: round3(cacheCoverageShare),
@@ -1447,6 +1185,13 @@ function buildCursorUsageFindings(runs) {
1447
1185
  scopeId: "all",
1448
1186
  scopeLabel: "Cursor usage",
1449
1187
  costImpactUsd: round3(maxModeSpendUsd * 0.2),
1188
+ signalSource: "observed",
1189
+ ruleId: MAX_MODE_CONCENTRATION_RULE_ID,
1190
+ evidence: {
1191
+ callIds: maxModeCalls.map((call) => call.id).sort(),
1192
+ runIds: Array.from(new Set(maxModeCalls.map((call) => call.runId))).sort(),
1193
+ sourceKinds: ["cursor-usage-csv"]
1194
+ },
1450
1195
  details: {
1451
1196
  maxModeSpendUsd: round3(maxModeSpendUsd),
1452
1197
  maxModeSpendShare: round3(maxModeSpendShare),
@@ -1468,55 +1213,178 @@ function buildCursorUsageFindings(runs) {
1468
1213
  }
1469
1214
 
1470
1215
  // ../core/src/findings/engine.ts
1216
+ var RETRY_OBSERVED_RULE_ID = "retry_explicit_failed_attempt_v1";
1217
+ var RETRY_INFERRED_RULE_ID = "retry_later_attempt_proxy_v1";
1218
+ var LOOP_RULE_ID = "loop_iteration_threshold_v1";
1219
+ var CONTEXT_OUTLIER_RULE_ID = "context_outlier_tokens_v1";
1220
+ var IDLE_SPEND_RULE_ID = "idle_workflow_name_v1";
1221
+ var CANDIDATE_DOWNGRADE_RULE_ID = "candidate_downgrade_task_model_v1";
1222
+ var LOOP_WASTE_START_ITERATION = 6;
1223
+ var LOOP_FINDING_MIN_ITERATION = 7;
1471
1224
  function createFinding2(input) {
1472
1225
  return {
1473
1226
  ...input,
1474
1227
  id: sha1(
1475
- `${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
1228
+ `${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}:${input.signalSource ?? "unknown"}:${input.ruleId ?? "none"}`
1476
1229
  )
1477
1230
  };
1478
1231
  }
1479
1232
  function round4(value) {
1480
1233
  return Number(value.toFixed(6));
1481
1234
  }
1235
+ function isFailedOrAborted(call) {
1236
+ const status = (call.status ?? "").toLowerCase();
1237
+ return status.includes("error") || status.includes("fail") || status.includes("abort");
1238
+ }
1239
+ function hasExplicitRetrySignal(call) {
1240
+ return (call.attempt ?? 1) > 1 || call.retries > 0;
1241
+ }
1242
+ function toTimestampMs(call) {
1243
+ const timestamp = new Date(call.timestamp).getTime();
1244
+ return Number.isFinite(timestamp) ? timestamp : Number.POSITIVE_INFINITY;
1245
+ }
1246
+ function sortCallsByTime(calls) {
1247
+ return calls.map((call, index) => ({ call, index })).sort((left, right) => {
1248
+ const delta = toTimestampMs(left.call) - toTimestampMs(right.call);
1249
+ return delta === 0 ? left.index - right.index : delta;
1250
+ });
1251
+ }
1252
+ function canUseStructuralSignals(sourceKind) {
1253
+ return sourceKind === "gateway";
1254
+ }
1255
+ function hasLaterExplicitRetryAttempt(sortedCalls, currentIndex) {
1256
+ const current = sortedCalls[currentIndex]?.call;
1257
+ if (!current) {
1258
+ return false;
1259
+ }
1260
+ return sortedCalls.slice(currentIndex + 1).some(({ call }) => {
1261
+ if (!hasExplicitRetrySignal(call)) {
1262
+ return false;
1263
+ }
1264
+ if (current.attempt !== null && call.attempt !== null) {
1265
+ return call.attempt > current.attempt;
1266
+ }
1267
+ return true;
1268
+ });
1269
+ }
1270
+ function uniqueSourceKinds(calls, runs) {
1271
+ const runById = new Map(runs.map((run2) => [run2.id, run2]));
1272
+ return Array.from(
1273
+ new Set(
1274
+ calls.map((call) => runById.get(call.runId)?.sourceKind).filter((sourceKind) => Boolean(sourceKind))
1275
+ )
1276
+ ).sort();
1277
+ }
1278
+ function buildRetryFinding(input) {
1279
+ const retryCost = input.calls.reduce((sum, call) => sum + call.costUsd, 0);
1280
+ const observed = input.signalSource === "observed";
1281
+ return createFinding2({
1282
+ classification: "waste",
1283
+ confidence: observed ? "high" : "medium",
1284
+ kind: "retry-waste",
1285
+ title: observed ? "Retry waste is consuming measurable spend" : "Retry waste is likely present from later retry attempts",
1286
+ summary: observed ? `${input.calls.length} failed or aborted call${input.calls.length === 1 ? "" : "s"} were followed by explicit retry attempts, making their spend retry overhead.` : `${input.calls.length} later retry attempt${input.calls.length === 1 ? "" : "s"} were counted as proxy retry overhead because the earlier failed attempt was not separately countable.`,
1287
+ scope: "global",
1288
+ scopeId: "all",
1289
+ scopeLabel: "workspace",
1290
+ costImpactUsd: round4(retryCost),
1291
+ signalSource: input.signalSource,
1292
+ ruleId: input.ruleId,
1293
+ evidence: {
1294
+ callIds: input.calls.map((call) => call.id).sort(),
1295
+ runIds: Array.from(new Set(input.calls.map((call) => call.runId))).sort(),
1296
+ sourceKinds: uniqueSourceKinds(input.calls, input.runs)
1297
+ },
1298
+ details: {
1299
+ retryCallCount: input.calls.length
1300
+ }
1301
+ });
1302
+ }
1482
1303
  function buildFindings(runs) {
1483
1304
  const findings = [];
1484
1305
  const wasteAttributions = [];
1485
- const allCalls = runs.flatMap((run2) => run2.calls.map((call) => ({ run: run2, call })));
1486
- const retryCandidates = allCalls.filter(({ call }) => {
1487
- const status = (call.status ?? "").toLowerCase();
1488
- return status.includes("error") || status.includes("fail");
1489
- });
1490
- const retryCost = retryCandidates.reduce((sum, item) => sum + item.call.costUsd, 0);
1491
- if (retryCost > 0) {
1306
+ const observedRetryCalls = [];
1307
+ const inferredRetryCalls = [];
1308
+ const retryCoveredCallIds = /* @__PURE__ */ new Set();
1309
+ for (const run2 of runs.filter((candidate) => canUseStructuralSignals(candidate.sourceKind))) {
1310
+ const sortedCalls = sortCallsByTime(run2.calls);
1311
+ sortedCalls.forEach(({ call }, index) => {
1312
+ if (!isFailedOrAborted(call)) {
1313
+ return;
1314
+ }
1315
+ if (!hasExplicitRetrySignal(call) && !hasLaterExplicitRetryAttempt(sortedCalls, index)) {
1316
+ return;
1317
+ }
1318
+ if (!hasLaterExplicitRetryAttempt(sortedCalls, index)) {
1319
+ return;
1320
+ }
1321
+ observedRetryCalls.push(call);
1322
+ retryCoveredCallIds.add(call.id);
1323
+ const later = sortedCalls.slice(index + 1).find(({ call: laterCall }) => hasExplicitRetrySignal(laterCall));
1324
+ if (later) {
1325
+ retryCoveredCallIds.add(later.call.id);
1326
+ }
1327
+ });
1328
+ for (const { call } of sortedCalls) {
1329
+ if (!hasExplicitRetrySignal(call) || retryCoveredCallIds.has(call.id)) {
1330
+ continue;
1331
+ }
1332
+ const hasEarlierCountableFailure = sortedCalls.some(({ call: earlier }) => {
1333
+ if (earlier.id === call.id) {
1334
+ return false;
1335
+ }
1336
+ return toTimestampMs(earlier) < toTimestampMs(call) && isFailedOrAborted(earlier);
1337
+ });
1338
+ if (!hasEarlierCountableFailure) {
1339
+ inferredRetryCalls.push(call);
1340
+ retryCoveredCallIds.add(call.id);
1341
+ }
1342
+ }
1343
+ }
1344
+ if (observedRetryCalls.length > 0) {
1492
1345
  wasteAttributions.push(
1493
- ...retryCandidates.map(({ call }) => ({
1346
+ ...observedRetryCalls.map((call) => ({
1494
1347
  kind: "retry-waste",
1495
1348
  timestamp: call.timestamp,
1496
1349
  wasteUsd: call.costUsd
1497
1350
  }))
1498
1351
  );
1499
1352
  findings.push(
1500
- createFinding2({
1501
- classification: "waste",
1502
- confidence: "high",
1353
+ buildRetryFinding({
1354
+ calls: observedRetryCalls,
1355
+ runs,
1356
+ signalSource: "observed",
1357
+ ruleId: RETRY_OBSERVED_RULE_ID
1358
+ })
1359
+ );
1360
+ }
1361
+ if (inferredRetryCalls.length > 0) {
1362
+ wasteAttributions.push(
1363
+ ...inferredRetryCalls.map((call) => ({
1503
1364
  kind: "retry-waste",
1504
- title: "Retry waste is consuming measurable spend",
1505
- summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
1506
- scope: "global",
1507
- scopeId: "all",
1508
- scopeLabel: "workspace",
1509
- costImpactUsd: round4(retryCost),
1510
- details: {
1511
- failedCallCount: retryCandidates.length
1512
- }
1365
+ timestamp: call.timestamp,
1366
+ wasteUsd: call.costUsd
1367
+ }))
1368
+ );
1369
+ findings.push(
1370
+ buildRetryFinding({
1371
+ calls: inferredRetryCalls,
1372
+ runs,
1373
+ signalSource: "inferred",
1374
+ ruleId: RETRY_INFERRED_RULE_ID
1513
1375
  })
1514
1376
  );
1515
1377
  }
1516
- for (const run2 of runs) {
1517
- const maxIteration = Math.max(...run2.calls.map((call) => call.iteration ?? 0));
1518
- if (maxIteration >= 7) {
1519
- const loopCalls = run2.calls.filter((call) => (call.iteration ?? 0) > 5);
1378
+ for (const run2 of runs.filter((candidate) => canUseStructuralSignals(candidate.sourceKind))) {
1379
+ const iterations = run2.calls.map((call) => call.iteration).filter((iteration) => iteration !== null);
1380
+ if (iterations.length === 0) {
1381
+ continue;
1382
+ }
1383
+ const maxIteration = Math.max(...iterations);
1384
+ if (maxIteration >= LOOP_FINDING_MIN_ITERATION) {
1385
+ const loopCalls = run2.calls.filter(
1386
+ (call) => (call.iteration ?? 0) >= LOOP_WASTE_START_ITERATION
1387
+ );
1520
1388
  const loopCost = loopCalls.reduce((sum, call) => sum + call.costUsd, 0);
1521
1389
  wasteAttributions.push(
1522
1390
  ...loopCalls.map((call) => ({
@@ -1531,14 +1399,22 @@ function buildFindings(runs) {
1531
1399
  confidence: "high",
1532
1400
  kind: "loop-waste",
1533
1401
  title: `Workflow "${run2.workflow}" ran beyond efficient loop bounds`,
1534
- summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
1402
+ summary: `This run reached ${maxIteration} iterations. Xerg treats spend from iteration ${LOOP_WASTE_START_ITERATION} onward as loop waste.`,
1535
1403
  scope: "run",
1536
1404
  scopeId: run2.workflow,
1537
1405
  scopeLabel: run2.workflow,
1538
1406
  costImpactUsd: round4(loopCost),
1407
+ signalSource: "observed",
1408
+ ruleId: LOOP_RULE_ID,
1409
+ evidence: {
1410
+ callIds: loopCalls.map((call) => call.id).sort(),
1411
+ runIds: [run2.id],
1412
+ sourceKinds: [run2.sourceKind]
1413
+ },
1539
1414
  details: {
1540
1415
  workflow: run2.workflow,
1541
- maxIteration
1416
+ maxIteration,
1417
+ thresholdIteration: LOOP_WASTE_START_ITERATION
1542
1418
  }
1543
1419
  })
1544
1420
  );
@@ -1573,6 +1449,12 @@ function buildFindings(runs) {
1573
1449
  scopeId: workflow,
1574
1450
  scopeLabel: workflow,
1575
1451
  costImpactUsd: round4(outlierCost),
1452
+ signalSource: "observed",
1453
+ ruleId: CONTEXT_OUTLIER_RULE_ID,
1454
+ evidence: {
1455
+ runIds: outlierRuns.map((run2) => run2.id).sort(),
1456
+ sourceKinds: Array.from(new Set(outlierRuns.map((run2) => run2.sourceKind))).sort()
1457
+ },
1576
1458
  details: {
1577
1459
  workflow,
1578
1460
  averageInputTokens: round4(average),
@@ -1598,6 +1480,12 @@ function buildFindings(runs) {
1598
1480
  scopeId: workflow,
1599
1481
  scopeLabel: workflow,
1600
1482
  costImpactUsd: round4(idleCost),
1483
+ signalSource: "observed",
1484
+ ruleId: IDLE_SPEND_RULE_ID,
1485
+ evidence: {
1486
+ runIds: idleRuns.map((run2) => run2.id).sort(),
1487
+ sourceKinds: Array.from(new Set(idleRuns.map((run2) => run2.sourceKind))).sort()
1488
+ },
1601
1489
  details: {
1602
1490
  workflow
1603
1491
  }
@@ -1620,6 +1508,13 @@ function buildFindings(runs) {
1620
1508
  scopeId: workflow,
1621
1509
  scopeLabel: workflow,
1622
1510
  costImpactUsd: round4(spend * 0.3),
1511
+ signalSource: "observed",
1512
+ ruleId: CANDIDATE_DOWNGRADE_RULE_ID,
1513
+ evidence: {
1514
+ callIds: downgradeCalls.map((call) => call.id).sort(),
1515
+ runIds: Array.from(new Set(downgradeCalls.map((call) => call.runId))).sort(),
1516
+ sourceKinds: uniqueSourceKinds(downgradeCalls, runs)
1517
+ },
1623
1518
  details: {
1624
1519
  workflow,
1625
1520
  expensiveCallCount: downgradeCalls.length,
@@ -1781,7 +1676,7 @@ var templatesByKind = {
1781
1676
  severity: "high",
1782
1677
  effort: "low",
1783
1678
  titleFn: (finding) => `Reduce retry waste in ${formatScopeLabel(finding)}`,
1784
- summaryFn: (finding) => `${finding.summary} This is confirmed retry overhead, so it is a fix-now issue rather than an experiment.`,
1679
+ summaryFn: (finding) => finding.signalSource === "observed" ? `${finding.summary} This is confirmed retry overhead, so it is a fix-now issue rather than an experiment.` : `${finding.summary} Treat this as likely retry overhead and inspect the retry wrapper before classifying the full amount as proven waste.`,
1785
1680
  whereToChangeFn: (finding) => `Reduce retries or add exponential backoff in the retry wrapper for ${formatScopeLabel(finding)}.`,
1786
1681
  validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push` against the same source. Retry waste should drop materially on the next audit.",
1787
1682
  actionsFn: () => [
@@ -2128,6 +2023,7 @@ function buildAuditSummary(input) {
2128
2023
  structuralWasteRate: Number(
2129
2024
  (totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
2130
2025
  ),
2026
+ wasteBySignalSource: buildWasteBySignalSource(input.findings),
2131
2027
  wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
2132
2028
  opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
2133
2029
  spendByWorkflow: buildBreakdown(
@@ -2169,7 +2065,7 @@ import { homedir as homedir2 } from "os";
2169
2065
  import { basename as basename2, join as join3 } from "path";
2170
2066
 
2171
2067
  // ../core/src/normalize/hermes.ts
2172
- import { readFileSync as readFileSync2 } from "fs";
2068
+ import { readFileSync as readFileSync3 } from "fs";
2173
2069
  import { basename } from "path";
2174
2070
 
2175
2071
  // ../core/src/utils/records.ts
@@ -2255,7 +2151,7 @@ function parseJsonLine(line) {
2255
2151
  }
2256
2152
  }
2257
2153
  function parseJsonLines(path) {
2258
- const content = readFileSync2(path, "utf8");
2154
+ const content = readFileSync3(path, "utf8");
2259
2155
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2260
2156
  const records = [];
2261
2157
  for (const line of lines) {
@@ -2608,7 +2504,7 @@ function getAppPaths() {
2608
2504
  };
2609
2505
  }
2610
2506
  function getDefaultDbPath() {
2611
- return join(getAppPaths().data, "xerg.db");
2507
+ return join(getAppPaths().data, "xerg-snapshots.json");
2612
2508
  }
2613
2509
  function getDefaultOpenClawSessionsPattern() {
2614
2510
  return join(getUserHome(), ".openclaw", "agents", "*", "sessions", "*.jsonl");
@@ -2799,10 +2695,10 @@ async function detectOpenClawSources(options) {
2799
2695
  }
2800
2696
 
2801
2697
  // ../core/src/normalize/openclaw.ts
2802
- import { readFileSync as readFileSync3 } from "fs";
2698
+ import { readFileSync as readFileSync4 } from "fs";
2803
2699
  import { basename as basename3 } from "path";
2804
2700
  function parseJsonLines2(path) {
2805
- const content = readFileSync3(path, "utf8");
2701
+ const content = readFileSync4(path, "utf8");
2806
2702
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2807
2703
  const records = [];
2808
2704
  for (const line of lines) {
@@ -3445,9 +3341,18 @@ function formatUsdDelta(value) {
3445
3341
  const sign = value > 0 ? "+" : "";
3446
3342
  return `${sign}${formatUsd(value)}`;
3447
3343
  }
3344
+ function formatUsdRate(value) {
3345
+ return formatUsd(value);
3346
+ }
3448
3347
  function isCursorUsageSummary(summary) {
3449
3348
  return summary.sourceFiles.some((source) => source.kind === "cursor-usage-csv");
3450
3349
  }
3350
+ function divideOrZero(numerator, denominator) {
3351
+ return denominator === 0 ? 0 : numerator / denominator;
3352
+ }
3353
+ function formatInferredShare(value) {
3354
+ return value === null || value === void 0 ? "unavailable" : formatPercent(value);
3355
+ }
3451
3356
  function topRows(rows, limit = 5) {
3452
3357
  return rows.slice(0, limit).map((row) => {
3453
3358
  return `- ${row.key}: ${formatUsd(row.spendUsd)} (${formatPercent(row.observedShare)} observed)`;
@@ -3532,6 +3437,35 @@ function renderFindingChange(change, state) {
3532
3437
  }
3533
3438
  return `- New: ${change.title} (${formatUsd(change.currentCostImpactUsd ?? 0)})`;
3534
3439
  }
3440
+ function renderCompareCoreRows(summary) {
3441
+ if (!summary.comparison) {
3442
+ return [];
3443
+ }
3444
+ const comparison = summary.comparison;
3445
+ const baselineWastePerRun = divideOrZero(
3446
+ comparison.baselineWasteSpendUsd,
3447
+ comparison.baselineRunCount
3448
+ );
3449
+ const currentWastePerRun = divideOrZero(summary.wasteSpendUsd, summary.runCount);
3450
+ const baselineWastePer1kCalls = divideOrZero(
3451
+ comparison.baselineWasteSpendUsd,
3452
+ comparison.baselineCallCount / 1e3
3453
+ );
3454
+ const currentWastePer1kCalls = divideOrZero(summary.wasteSpendUsd, summary.callCount / 1e3);
3455
+ return [
3456
+ "## Before / after",
3457
+ `Compared against ${comparison.baselineGeneratedAt}`,
3458
+ `- Waste rate: ${formatPercent(comparison.baselineStructuralWasteRate)} -> ${formatPercent(summary.structuralWasteRate)} (${formatPercentDelta(comparison.deltaStructuralWasteRate)})`,
3459
+ `- Waste per run: ${formatUsdRate(baselineWastePerRun)} -> ${formatUsdRate(currentWastePerRun)} (${formatUsdDelta(currentWastePerRun - baselineWastePerRun)})`,
3460
+ `- Waste per 1k calls: ${formatUsdRate(baselineWastePer1kCalls)} -> ${formatUsdRate(currentWastePer1kCalls)} (${formatUsdDelta(currentWastePer1kCalls - baselineWastePer1kCalls)})`,
3461
+ `- Inferred waste share: ${formatInferredShare(comparison.baselineWasteBySignalSource?.inferredShare)} -> ${formatInferredShare(summary.wasteBySignalSource?.inferredShare)}`,
3462
+ "- CPO: unavailable (no outcome signal)",
3463
+ `- Total spend (workload-dependent): ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
3464
+ `- Structural waste (workload-dependent): ${formatUsd(comparison.baselineWasteSpendUsd)} -> ${formatUsd(summary.wasteSpendUsd)} (${formatUsdDelta(comparison.deltaWasteSpendUsd)})`,
3465
+ `- Runs analyzed: ${comparison.baselineRunCount} -> ${summary.runCount} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
3466
+ `- Model calls: ${comparison.baselineCallCount} -> ${summary.callCount} (${comparison.deltaCallCount > 0 ? "+" : ""}${comparison.deltaCallCount})`
3467
+ ];
3468
+ }
3535
3469
  function renderCompareBlock(summary) {
3536
3470
  if (!summary.comparison) {
3537
3471
  return [];
@@ -3552,13 +3486,7 @@ function renderCompareBlock(summary) {
3552
3486
  )
3553
3487
  ].slice(0, 5);
3554
3488
  return [
3555
- "## Before / after",
3556
- `Compared against ${comparison.baselineGeneratedAt}`,
3557
- `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
3558
- `- Structural waste: ${formatUsd(comparison.baselineWasteSpendUsd)} -> ${formatUsd(summary.wasteSpendUsd)} (${formatUsdDelta(comparison.deltaWasteSpendUsd)})`,
3559
- `- Waste rate: ${formatPercent(comparison.baselineStructuralWasteRate)} -> ${formatPercent(summary.structuralWasteRate)} (${formatPercentDelta(comparison.deltaStructuralWasteRate)})`,
3560
- `- Runs analyzed: ${comparison.baselineRunCount} -> ${summary.runCount} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
3561
- `- Model calls: ${comparison.baselineCallCount} -> ${summary.callCount} (${comparison.deltaCallCount > 0 ? "+" : ""}${comparison.deltaCallCount})`,
3489
+ ...renderCompareCoreRows(summary),
3562
3490
  biggestImprovement ? `- Biggest improvement: ${describeSpendDelta(biggestImprovement)}` : "- Biggest improvement: none detected",
3563
3491
  biggestRegression ? `- Biggest regression: ${describeSpendDelta(biggestRegression)}` : "- Biggest regression: none detected",
3564
3492
  firstWorkflowToInspect ? `- First workflow to inspect now: ${firstWorkflowToInspect}` : "- First workflow to inspect now: no workflow delta available",
@@ -3682,10 +3610,7 @@ function renderCursorCompareBlock(summary) {
3682
3610
  const modeSwing = comparison.workflowDeltas[0];
3683
3611
  const modelSwing = comparison.modelDeltas[0];
3684
3612
  return [
3685
- "## Before / after",
3686
- `Compared against ${comparison.baselineGeneratedAt}`,
3687
- `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
3688
- `- Rows analyzed: ${formatCount(comparison.baselineRunCount)} -> ${formatCount(summary.runCount)} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
3613
+ ...renderCompareCoreRows(summary),
3689
3614
  `- Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)}`,
3690
3615
  modeSwing ? `- Mode swing to inspect: ${describeSpendDelta(modeSwing)}` : "- Mode swing to inspect: none",
3691
3616
  modelSwing ? `- Model swing to inspect: ${describeSpendDelta(modelSwing)}` : "- Model swing to inspect: none"
@@ -3779,7 +3704,7 @@ function renderCursorMarkdownSummary(summary) {
3779
3704
  "",
3780
3705
  "## Findings",
3781
3706
  ...summary.findings.slice(0, 10).map((finding) => {
3782
- return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3707
+ return `- **${finding.title}** (${finding.classification}, ${finding.confidence}). ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3783
3708
  }),
3784
3709
  "",
3785
3710
  ...renderActionQueue(summary),
@@ -3862,21 +3787,13 @@ function renderMarkdownSummary(summary) {
3862
3787
  "",
3863
3788
  "## Findings",
3864
3789
  ...summary.findings.slice(0, 10).map((finding) => {
3865
- return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3790
+ return `- **${finding.title}** (${finding.classification}, ${finding.confidence}). ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3866
3791
  }),
3867
3792
  "",
3868
3793
  ...renderActionQueue(summary)
3869
3794
  ];
3870
3795
  if (summary.comparison) {
3871
- const comparison = summary.comparison;
3872
- lines.push(
3873
- "",
3874
- "## Before / after",
3875
- `- Compared against: ${comparison.baselineGeneratedAt}`,
3876
- `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
3877
- `- Structural waste: ${formatUsd(comparison.baselineWasteSpendUsd)} -> ${formatUsd(summary.wasteSpendUsd)} (${formatUsdDelta(comparison.deltaWasteSpendUsd)})`,
3878
- `- Waste rate: ${formatPercent(comparison.baselineStructuralWasteRate)} -> ${formatPercent(summary.structuralWasteRate)} (${formatPercentDelta(comparison.deltaStructuralWasteRate)})`
3879
- );
3796
+ lines.push("", ...renderCompareBlock(summary));
3880
3797
  }
3881
3798
  return lines.join("\n");
3882
3799
  }
@@ -4006,12 +3923,12 @@ async function pushAudit(payload, config) {
4006
3923
  }
4007
3924
 
4008
3925
  // src/push/config.ts
4009
- import { readFileSync as readFileSync5 } from "fs";
3926
+ import { readFileSync as readFileSync6 } from "fs";
4010
3927
  import { homedir as homedir4 } from "os";
4011
3928
  import { join as join5 } from "path";
4012
3929
 
4013
3930
  // src/auth/credentials.ts
4014
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
3931
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "fs";
4015
3932
  import { homedir as homedir3 } from "os";
4016
3933
  import { dirname as dirname2, join as join4 } from "path";
4017
3934
  function getCredentialsPath() {
@@ -4023,13 +3940,13 @@ function storeCredentials(token) {
4023
3940
  const dir = dirname2(credPath);
4024
3941
  mkdirSync3(dir, { recursive: true });
4025
3942
  const data = { token, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
4026
- writeFileSync(credPath, JSON.stringify(data, null, 2), { mode: 384 });
3943
+ writeFileSync2(credPath, JSON.stringify(data, null, 2), { mode: 384 });
4027
3944
  }
4028
3945
  function loadStoredCredentials() {
4029
3946
  const credPath = getCredentialsPath();
4030
3947
  try {
4031
- if (!existsSync(credPath)) return null;
4032
- const raw = readFileSync4(credPath, "utf8");
3948
+ if (!existsSync2(credPath)) return null;
3949
+ const raw = readFileSync5(credPath, "utf8");
4033
3950
  const parsed = JSON.parse(raw);
4034
3951
  return parsed.token || null;
4035
3952
  } catch {
@@ -4039,7 +3956,7 @@ function loadStoredCredentials() {
4039
3956
  function clearCredentials() {
4040
3957
  const credPath = getCredentialsPath();
4041
3958
  try {
4042
- if (!existsSync(credPath)) return false;
3959
+ if (!existsSync2(credPath)) return false;
4043
3960
  rmSync(credPath);
4044
3961
  return true;
4045
3962
  } catch {
@@ -4061,7 +3978,7 @@ function loadPushConfig() {
4061
3978
  };
4062
3979
  }
4063
3980
  try {
4064
- const raw = readFileSync5(CONFIG_PATH, "utf8");
3981
+ const raw = readFileSync6(CONFIG_PATH, "utf8");
4065
3982
  const parsed = JSON.parse(raw);
4066
3983
  if (parsed.apiKey) {
4067
3984
  return {
@@ -4880,13 +4797,13 @@ function formatBytes2(bytes) {
4880
4797
  }
4881
4798
 
4882
4799
  // src/transport/config.ts
4883
- import { readFileSync as readFileSync6 } from "fs";
4800
+ import { readFileSync as readFileSync7 } from "fs";
4884
4801
  import { resolve as resolve3 } from "path";
4885
4802
  function loadRemoteConfig(configPath) {
4886
4803
  const resolved = resolve3(configPath);
4887
4804
  let raw;
4888
4805
  try {
4889
- raw = readFileSync6(resolved, "utf8");
4806
+ raw = readFileSync7(resolved, "utf8");
4890
4807
  } catch {
4891
4808
  throw new Error(`Cannot read remote config at ${resolved}`);
4892
4809
  }
@@ -5033,11 +4950,11 @@ function resolveRemoteHost(target) {
5033
4950
  }
5034
4951
 
5035
4952
  // src/version.ts
5036
- import { readFileSync as readFileSync7 } from "fs";
4953
+ import { readFileSync as readFileSync8 } from "fs";
5037
4954
  function getCliVersion() {
5038
4955
  try {
5039
4956
  const packageJsonPath = new URL("../package.json", import.meta.url);
5040
- const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
4957
+ const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
5041
4958
  return packageJson.version ?? "0.0.0";
5042
4959
  } catch {
5043
4960
  return "0.0.0";
@@ -5573,7 +5490,8 @@ function renderMcpCredentialSourceMessage(config) {
5573
5490
  }
5574
5491
 
5575
5492
  // src/prompts.ts
5576
- import { confirm, select } from "@inquirer/prompts";
5493
+ import confirm from "@inquirer/confirm";
5494
+ import select from "@inquirer/select";
5577
5495
  function hasPromptTty() {
5578
5496
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
5579
5497
  }
@@ -5591,7 +5509,7 @@ async function promptSelect(message, choices) {
5591
5509
  }
5592
5510
 
5593
5511
  // src/commands/push.ts
5594
- import { readFileSync as readFileSync8 } from "fs";
5512
+ import { readFileSync as readFileSync9 } from "fs";
5595
5513
  async function runPushCommand(options) {
5596
5514
  const payload = options.file ? loadPayloadFromFile(options.file) : loadLatestCachedAuditPayload();
5597
5515
  if (options.dryRun) {
@@ -5615,7 +5533,7 @@ async function runPushCommand(options) {
5615
5533
  function loadPayloadFromFile(filePath) {
5616
5534
  let raw;
5617
5535
  try {
5618
- raw = readFileSync8(filePath, "utf8");
5536
+ raw = readFileSync9(filePath, "utf8");
5619
5537
  } catch {
5620
5538
  throw new Error(`Cannot read file: ${filePath}`);
5621
5539
  }
@@ -5986,7 +5904,7 @@ function renderRailwayDoctorReport(report) {
5986
5904
  }
5987
5905
 
5988
5906
  // src/commands/mcp-setup.ts
5989
- import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
5907
+ import { existsSync as existsSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
5990
5908
  import { dirname as dirname3, join as join8 } from "path";
5991
5909
  var HOSTED_MCP_URL = "https://mcp.xerg.ai/mcp";
5992
5910
  var MCP_SERVER_NAME = "xerg";
@@ -6072,7 +5990,7 @@ async function runMcpSetupFlow() {
6072
5990
  async function handleCursorSetup(snippet, config) {
6073
5991
  const cursorDir = join8(process.cwd(), ".cursor");
6074
5992
  const cursorConfigPath = join8(cursorDir, "mcp.json");
6075
- if (existsSync2(cursorDir)) {
5993
+ if (existsSync3(cursorDir)) {
6076
5994
  const shouldWrite = await promptConfirm(
6077
5995
  "Write a project-scoped Cursor MCP config to .cursor/mcp.json?",
6078
5996
  true
@@ -6119,9 +6037,9 @@ function tomlString(value) {
6119
6037
  function writeCursorConfig(filePath, config) {
6120
6038
  mkdirSync6(dirname3(filePath), { recursive: true });
6121
6039
  let parsed = {};
6122
- if (existsSync2(filePath)) {
6040
+ if (existsSync3(filePath)) {
6123
6041
  try {
6124
- parsed = JSON.parse(readFileSync9(filePath, "utf8"));
6042
+ parsed = JSON.parse(readFileSync10(filePath, "utf8"));
6125
6043
  } catch {
6126
6044
  throw new Error(`Cursor config is not valid JSON: ${filePath}`);
6127
6045
  }
@@ -6134,7 +6052,7 @@ function writeCursorConfig(filePath, config) {
6134
6052
  ...existingServers ?? {},
6135
6053
  xerg: buildHostedMcpConfig(config).mcpServers.xerg
6136
6054
  };
6137
- writeFileSync2(filePath, `${JSON.stringify(parsed, null, 2)}
6055
+ writeFileSync3(filePath, `${JSON.stringify(parsed, null, 2)}
6138
6056
  `);
6139
6057
  }
6140
6058
 
@@ -6346,7 +6264,7 @@ Options:
6346
6264
  --compare Compare this audit to the newest compatible prior local snapshot
6347
6265
  --json Render the report as JSON
6348
6266
  --markdown Render the report as Markdown
6349
- --db <path> Custom SQLite database path
6267
+ --db <path> Custom JSON snapshot path
6350
6268
  --no-db Skip local persistence
6351
6269
 
6352
6270
  Remote options (SSH, OpenClaw only):