@compaction/cli 0.1.0

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.
Files changed (143) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +319 -0
  3. package/dist/cli/commands/adapter.d.ts +2 -0
  4. package/dist/cli/commands/adapter.js +49 -0
  5. package/dist/cli/commands/aggregate.d.ts +2 -0
  6. package/dist/cli/commands/aggregate.js +60 -0
  7. package/dist/cli/commands/analyze.d.ts +2 -0
  8. package/dist/cli/commands/analyze.js +55 -0
  9. package/dist/cli/commands/apply-context.d.ts +12 -0
  10. package/dist/cli/commands/apply-context.js +80 -0
  11. package/dist/cli/commands/apply.d.ts +2 -0
  12. package/dist/cli/commands/apply.js +65 -0
  13. package/dist/cli/commands/approve.d.ts +2 -0
  14. package/dist/cli/commands/approve.js +55 -0
  15. package/dist/cli/commands/audit.d.ts +2 -0
  16. package/dist/cli/commands/audit.js +63 -0
  17. package/dist/cli/commands/billing-delta.d.ts +2 -0
  18. package/dist/cli/commands/billing-delta.js +246 -0
  19. package/dist/cli/commands/capture-claude-code.d.ts +19 -0
  20. package/dist/cli/commands/capture-claude-code.js +198 -0
  21. package/dist/cli/commands/capture-provider-usage.d.ts +35 -0
  22. package/dist/cli/commands/capture-provider-usage.js +113 -0
  23. package/dist/cli/commands/capture.d.ts +2 -0
  24. package/dist/cli/commands/capture.js +76 -0
  25. package/dist/cli/commands/compact.d.ts +2 -0
  26. package/dist/cli/commands/compact.js +206 -0
  27. package/dist/cli/commands/eval.d.ts +2 -0
  28. package/dist/cli/commands/eval.js +66 -0
  29. package/dist/cli/commands/export-session-seed.d.ts +11 -0
  30. package/dist/cli/commands/export-session-seed.js +42 -0
  31. package/dist/cli/commands/feedback.d.ts +2 -0
  32. package/dist/cli/commands/feedback.js +233 -0
  33. package/dist/cli/commands/import.d.ts +2 -0
  34. package/dist/cli/commands/import.js +60 -0
  35. package/dist/cli/commands/init.d.ts +2 -0
  36. package/dist/cli/commands/init.js +216 -0
  37. package/dist/cli/commands/integrations.d.ts +2 -0
  38. package/dist/cli/commands/integrations.js +29 -0
  39. package/dist/cli/commands/launch-treatment-session.d.ts +14 -0
  40. package/dist/cli/commands/launch-treatment-session.js +50 -0
  41. package/dist/cli/commands/optimize.d.ts +2 -0
  42. package/dist/cli/commands/optimize.js +33 -0
  43. package/dist/cli/commands/recommend.d.ts +4 -0
  44. package/dist/cli/commands/recommend.js +66 -0
  45. package/dist/cli/commands/review.d.ts +2 -0
  46. package/dist/cli/commands/review.js +60 -0
  47. package/dist/cli/commands/run.d.ts +2 -0
  48. package/dist/cli/commands/run.js +49 -0
  49. package/dist/cli/commands/spend.d.ts +2 -0
  50. package/dist/cli/commands/spend.js +24 -0
  51. package/dist/cli/commands/summary.d.ts +2 -0
  52. package/dist/cli/commands/summary.js +19 -0
  53. package/dist/cli/engine-degrade.d.ts +40 -0
  54. package/dist/cli/engine-degrade.js +59 -0
  55. package/dist/cli/index.d.ts +2 -0
  56. package/dist/cli/index.js +59 -0
  57. package/dist/core/adapter-validation.d.ts +32 -0
  58. package/dist/core/adapter-validation.js +185 -0
  59. package/dist/core/adapters/claude-code-adapter.d.ts +51 -0
  60. package/dist/core/adapters/claude-code-adapter.js +539 -0
  61. package/dist/core/adapters/claude-code-discovery.d.ts +40 -0
  62. package/dist/core/adapters/claude-code-discovery.js +100 -0
  63. package/dist/core/aggregate-format.d.ts +62 -0
  64. package/dist/core/aggregate-format.js +349 -0
  65. package/dist/core/api-client/client.d.ts +43 -0
  66. package/dist/core/api-client/client.js +143 -0
  67. package/dist/core/api-client/config.d.ts +36 -0
  68. package/dist/core/api-client/config.js +44 -0
  69. package/dist/core/api-client/index.d.ts +13 -0
  70. package/dist/core/api-client/index.js +14 -0
  71. package/dist/core/api-client/payload.d.ts +93 -0
  72. package/dist/core/api-client/payload.js +180 -0
  73. package/dist/core/api-client/types.d.ts +95 -0
  74. package/dist/core/api-client/types.js +21 -0
  75. package/dist/core/artifact-writer.d.ts +2 -0
  76. package/dist/core/artifact-writer.js +14 -0
  77. package/dist/core/audit-report.d.ts +91 -0
  78. package/dist/core/audit-report.js +422 -0
  79. package/dist/core/billing-delta/billing-delta-record.d.ts +290 -0
  80. package/dist/core/billing-delta/billing-delta-record.js +394 -0
  81. package/dist/core/capture-adapter.d.ts +74 -0
  82. package/dist/core/capture-adapter.js +2 -0
  83. package/dist/core/command-runner.d.ts +29 -0
  84. package/dist/core/command-runner.js +96 -0
  85. package/dist/core/content-hash.d.ts +1 -0
  86. package/dist/core/content-hash.js +5 -0
  87. package/dist/core/cost-calculator.d.ts +6 -0
  88. package/dist/core/cost-calculator.js +20 -0
  89. package/dist/core/feedback-bundle.d.ts +159 -0
  90. package/dist/core/feedback-bundle.js +406 -0
  91. package/dist/core/openai-agents-capture.d.ts +66 -0
  92. package/dist/core/openai-agents-capture.js +475 -0
  93. package/dist/core/policy-types.d.ts +85 -0
  94. package/dist/core/policy-types.js +29 -0
  95. package/dist/core/pricing.d.ts +23 -0
  96. package/dist/core/pricing.js +55 -0
  97. package/dist/core/provider-usage/credential-redaction.d.ts +23 -0
  98. package/dist/core/provider-usage/credential-redaction.js +60 -0
  99. package/dist/core/provider-usage/provider-usage-adapter.d.ts +65 -0
  100. package/dist/core/provider-usage/provider-usage-adapter.js +147 -0
  101. package/dist/core/provider-usage/provider-usage-client.d.ts +89 -0
  102. package/dist/core/provider-usage/provider-usage-client.js +96 -0
  103. package/dist/core/report-generator.d.ts +7 -0
  104. package/dist/core/report-generator.js +275 -0
  105. package/dist/core/run-aggregator.d.ts +50 -0
  106. package/dist/core/run-aggregator.js +251 -0
  107. package/dist/core/run-labels.d.ts +52 -0
  108. package/dist/core/run-labels.js +80 -0
  109. package/dist/core/run-trace-converter.d.ts +3 -0
  110. package/dist/core/run-trace-converter.js +94 -0
  111. package/dist/core/safety-report.d.ts +52 -0
  112. package/dist/core/safety-report.js +440 -0
  113. package/dist/core/savings.d.ts +22 -0
  114. package/dist/core/savings.js +36 -0
  115. package/dist/core/session-aggregate.d.ts +211 -0
  116. package/dist/core/session-aggregate.js +490 -0
  117. package/dist/core/skill-injection-detector.d.ts +51 -0
  118. package/dist/core/skill-injection-detector.js +146 -0
  119. package/dist/core/spend-attribution.d.ts +169 -0
  120. package/dist/core/spend-attribution.js +611 -0
  121. package/dist/core/standalone-capsule-provenance.d.ts +4 -0
  122. package/dist/core/standalone-capsule-provenance.js +62 -0
  123. package/dist/core/state-capsule.d.ts +3 -0
  124. package/dist/core/state-capsule.js +147 -0
  125. package/dist/core/token-accounting.d.ts +62 -0
  126. package/dist/core/token-accounting.js +81 -0
  127. package/dist/core/token-estimator.d.ts +3 -0
  128. package/dist/core/token-estimator.js +18 -0
  129. package/dist/core/trace-adapters.d.ts +53 -0
  130. package/dist/core/trace-adapters.js +714 -0
  131. package/dist/core/trace-fingerprint.d.ts +79 -0
  132. package/dist/core/trace-fingerprint.js +102 -0
  133. package/dist/core/trace-intake.d.ts +41 -0
  134. package/dist/core/trace-intake.js +164 -0
  135. package/dist/core/trace-parser.d.ts +203 -0
  136. package/dist/core/trace-parser.js +143 -0
  137. package/dist/core/types.d.ts +238 -0
  138. package/dist/core/types.js +2 -0
  139. package/dist/core/usage-metadata.d.ts +51 -0
  140. package/dist/core/usage-metadata.js +122 -0
  141. package/dist/core/waste-detector.d.ts +8 -0
  142. package/dist/core/waste-detector.js +58 -0
  143. package/package.json +90 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Session + multi-session LOCAL aggregate (Strong-MVP Track E).
3
+ *
4
+ * Rolls up the per-run artifacts a developer already produced —
5
+ * `.compaction/runs/<run>/report.json` and (when present) `strong-eval.json` plus an
6
+ * optional local `run-labels.json` — into:
7
+ * - a PER-SESSION summary (a session may contain multiple runs), and
8
+ * - a CROSS-SESSION aggregate (totals across every session/run).
9
+ *
10
+ * This is the future admin/buyer value WITHOUT a hosted dashboard: everything is LOCAL,
11
+ * file-based, read-only over existing artifacts. NO network, NO upload, NO telemetry,
12
+ * NO database, NO auth, NO server.
13
+ *
14
+ * EVIDENCE HONESTY (mvp-capability-matrix rows 2/3/10/13/21, README evidence ladder):
15
+ * - Token figures carry their per-figure source: provider-reported ONLY when the run's
16
+ * token accounting was `measured` (provider usage metadata present); otherwise
17
+ * local-estimate (chars/4). The aggregate states how many runs are in each tier and
18
+ * NEVER labels an estimate as provider-reported.
19
+ * - Cost / savings are ESTIMATED (price-table over the token figures). NEVER
20
+ * billing-confirmed, NEVER realized, NEVER extrapolated to a time period.
21
+ * - Output tokens are carried only where a run actually recorded them (from
22
+ * strong-eval token_accounting); runs without strong-eval contribute `unknown`
23
+ * output tokens and are counted separately, never inferred.
24
+ * - Verification outcome counts (ready/conditional/not_ready, recoverability /
25
+ * commitment / task-check pass/fail) are copied from strong-eval; runs without a
26
+ * strong-eval are counted as `not_evaluated`, never as a pass.
27
+ *
28
+ * The builders are PURE (no IO) so the rollup math, label tagging, and the content-free
29
+ * share bundle are unit-testable; the reader (`collectRunRecords`) does the only IO and
30
+ * touches nothing but local files.
31
+ */
32
+ import { randomUUID } from "node:crypto";
33
+ import { readdir, readFile } from "node:fs/promises";
34
+ import { join } from "node:path";
35
+ import { NO_LABEL, parseRunLabels } from "./run-labels.js";
36
+ export const AGGREGATE_ARTIFACT_DIRECTORY = ".compaction/aggregates";
37
+ export const AGGREGATE_EVIDENCE_LABELS = {
38
+ input_tokens: "input tokens: provider-reported where the run's token accounting was measured (provider usage " +
39
+ "metadata present), otherwise local estimate (chars/4); see the provider-reported / local-estimate " +
40
+ "run split. Never billing-confirmed.",
41
+ output_tokens: "output tokens: shown only for runs that recorded them (strong-eval token accounting); runs without " +
42
+ "a strong-eval contribute unknown output tokens and are counted separately — never inferred.",
43
+ estimated_cost: "estimated cost: price-table estimate over the token figures (token-estimated cost / local estimate). " +
44
+ "NOT a billed figure, NOT billing-confirmed.",
45
+ estimated_savings: "estimated savings: estimated, summed over the recorded runs only. NOT billing-confirmed, NOT " +
46
+ "realized, and NOT extrapolated to any time period.",
47
+ compactions: "compactions: count of recorded local runs rolled up (one compaction per recorded run). No run " +
48
+ "volume is assumed or projected.",
49
+ verification: "verification: readiness (ready/conditional/not_ready) and per-axis recoverability / commitment / " +
50
+ "task-check outcomes copied from each run's strong-eval; runs without a strong-eval are not_evaluated, " +
51
+ "never counted as a pass. These are DETERMINISTIC recoverability checks, NOT semantic guarantees.",
52
+ provider_runtime: "provider/runtime: operator-ASSERTED local label (never provider-verified); runs without a provider " +
53
+ "label are grouped under 'unlabeled'. This label is distinct from the token-evidence tier."
54
+ };
55
+ const STANDARD_LIMITATIONS = [
56
+ "Aggregated from LOCAL artifacts only (.compaction/runs/*/{report.json,strong-eval.json,run-labels.json}); no provider calls, no upload, no telemetry.",
57
+ "Estimated cost and savings are price-table local estimates summed over the recorded runs only; never billing-confirmed, never realized, never extrapolated to a time period.",
58
+ "Token figures are provider-reported only for runs whose token accounting was measured; all other runs contribute local estimates (chars/4). The split is reported, never hidden.",
59
+ "Output tokens are summed only over runs that recorded them; runs without a strong-eval contribute unknown output tokens and are counted separately, never inferred.",
60
+ "Verification outcomes are deterministic recoverability checks copied from each run's strong-eval; they are NOT semantic or meaning-preservation guarantees. Runs without a strong-eval are not_evaluated, never a pass.",
61
+ "Labels (project/workflow/provider/user-label/session) are operator-supplied local tags with no evidence weight; provider here is operator-asserted, not provider-verified.",
62
+ "This is a LOCAL, file-based rollup. There is no hosted dashboard, database, auth, billing, model routing, or provider integration."
63
+ ];
64
+ function round6(value) {
65
+ return Number((value + Number.EPSILON).toFixed(6));
66
+ }
67
+ function emptyTotals() {
68
+ return {
69
+ run_count: 0,
70
+ runs_provider_reported: 0,
71
+ runs_local_estimate: 0,
72
+ runs_unknown_token_evidence: 0,
73
+ total_input_tokens_before: 0,
74
+ total_input_tokens_after: 0,
75
+ total_input_tokens_saved: 0,
76
+ total_output_tokens_before: 0,
77
+ total_output_tokens_after: 0,
78
+ runs_with_output_tokens: 0,
79
+ runs_without_output_tokens: 0,
80
+ total_estimated_cost_before_usd: 0,
81
+ total_estimated_cost_after_usd: 0,
82
+ total_estimated_savings_usd: 0,
83
+ compactions: 0
84
+ };
85
+ }
86
+ function emptyVerification() {
87
+ return {
88
+ ready: 0,
89
+ conditional: 0,
90
+ not_ready: 0,
91
+ not_evaluated: 0,
92
+ recoverability_pass: 0,
93
+ recoverability_fail: 0,
94
+ recoverability_not_evaluated: 0,
95
+ commitment_pass: 0,
96
+ commitment_fail: 0,
97
+ commitment_not_evaluated: 0,
98
+ task_check_pass: 0,
99
+ task_check_fail: 0,
100
+ task_check_unsupported: 0,
101
+ task_check_not_evaluated: 0
102
+ };
103
+ }
104
+ function addRunToTotals(totals, run) {
105
+ totals.run_count += 1;
106
+ totals.compactions += 1;
107
+ if (run.token_evidence === "provider_reported")
108
+ totals.runs_provider_reported += 1;
109
+ else if (run.token_evidence === "local_estimate")
110
+ totals.runs_local_estimate += 1;
111
+ else
112
+ totals.runs_unknown_token_evidence += 1;
113
+ totals.total_input_tokens_before += run.input_tokens_before;
114
+ totals.total_input_tokens_after += run.input_tokens_after;
115
+ totals.total_input_tokens_saved += run.input_tokens_saved;
116
+ if (run.output_tokens_before !== "unknown" && run.output_tokens_after !== "unknown") {
117
+ totals.total_output_tokens_before += run.output_tokens_before;
118
+ totals.total_output_tokens_after += run.output_tokens_after;
119
+ totals.runs_with_output_tokens += 1;
120
+ }
121
+ else {
122
+ totals.runs_without_output_tokens += 1;
123
+ }
124
+ totals.total_estimated_cost_before_usd += run.estimated_cost_before_usd;
125
+ totals.total_estimated_cost_after_usd += run.estimated_cost_after_usd;
126
+ totals.total_estimated_savings_usd += run.estimated_saving_usd;
127
+ }
128
+ function normalizeTotals(totals) {
129
+ totals.total_estimated_cost_before_usd = round6(totals.total_estimated_cost_before_usd);
130
+ totals.total_estimated_cost_after_usd = round6(totals.total_estimated_cost_after_usd);
131
+ totals.total_estimated_savings_usd = round6(totals.total_estimated_savings_usd);
132
+ }
133
+ function addRunToVerification(v, run) {
134
+ v[run.readiness] += 1;
135
+ if (run.recoverability === "pass")
136
+ v.recoverability_pass += 1;
137
+ else if (run.recoverability === "fail")
138
+ v.recoverability_fail += 1;
139
+ else
140
+ v.recoverability_not_evaluated += 1;
141
+ if (run.commitment_preservation === "pass")
142
+ v.commitment_pass += 1;
143
+ else if (run.commitment_preservation === "fail")
144
+ v.commitment_fail += 1;
145
+ else
146
+ v.commitment_not_evaluated += 1;
147
+ if (run.task_check === "pass")
148
+ v.task_check_pass += 1;
149
+ else if (run.task_check === "fail")
150
+ v.task_check_fail += 1;
151
+ else if (run.task_check === "unsupported")
152
+ v.task_check_unsupported += 1;
153
+ else
154
+ v.task_check_not_evaluated += 1;
155
+ }
156
+ function distinct(values) {
157
+ return [...new Set(values.filter((v) => typeof v === "string" && v.length > 0))].sort();
158
+ }
159
+ function sessionIdFor(run) {
160
+ if (run.labels.session !== undefined)
161
+ return { id: run.labels.session, explicit: true };
162
+ return { id: run.run_id, explicit: false };
163
+ }
164
+ /** True when a run passes every supplied local-label filter (case-sensitive exact match). */
165
+ export function runMatchesFilters(run, filters) {
166
+ if (filters.project !== undefined && run.labels.project !== filters.project)
167
+ return false;
168
+ if (filters.workflow !== undefined && run.labels.workflow !== filters.workflow)
169
+ return false;
170
+ if (filters.provider !== undefined && run.labels.provider !== filters.provider)
171
+ return false;
172
+ if (filters.user_label !== undefined && run.labels.user_label !== filters.user_label)
173
+ return false;
174
+ if (filters.session !== undefined && run.labels.session !== filters.session)
175
+ return false;
176
+ return true;
177
+ }
178
+ function buildBreakdown(runs, keyOf) {
179
+ const buckets = new Map();
180
+ for (const run of runs) {
181
+ const key = keyOf(run);
182
+ let bucket = buckets.get(key);
183
+ if (!bucket) {
184
+ bucket = { totals: emptyTotals(), verification: emptyVerification() };
185
+ buckets.set(key, bucket);
186
+ }
187
+ addRunToTotals(bucket.totals, run);
188
+ addRunToVerification(bucket.verification, run);
189
+ }
190
+ return [...buckets.entries()]
191
+ .map(([key, bucket]) => {
192
+ normalizeTotals(bucket.totals);
193
+ return { key, totals: bucket.totals, verification: bucket.verification };
194
+ })
195
+ .sort((a, b) => b.totals.total_input_tokens_saved - a.totals.total_input_tokens_saved || a.key.localeCompare(b.key));
196
+ }
197
+ function buildSessions(runs) {
198
+ const sessions = new Map();
199
+ for (const run of runs) {
200
+ const { id, explicit } = sessionIdFor(run);
201
+ let session = sessions.get(id);
202
+ if (!session) {
203
+ session = { explicit, runs: [] };
204
+ sessions.set(id, session);
205
+ }
206
+ // An explicit label anywhere marks the session explicit.
207
+ session.explicit = session.explicit || explicit;
208
+ session.runs.push(run);
209
+ }
210
+ return [...sessions.entries()]
211
+ .map(([sessionId, { explicit, runs: sessionRuns }]) => {
212
+ const totals = emptyTotals();
213
+ const verification = emptyVerification();
214
+ for (const run of sessionRuns) {
215
+ addRunToTotals(totals, run);
216
+ addRunToVerification(verification, run);
217
+ }
218
+ normalizeTotals(totals);
219
+ return {
220
+ session_id: sessionId,
221
+ explicit_session_label: explicit,
222
+ run_ids: sessionRuns.map((r) => r.run_id).sort(),
223
+ labels: {
224
+ projects: distinct(sessionRuns.map((r) => r.labels.project)),
225
+ workflows: distinct(sessionRuns.map((r) => r.labels.workflow)),
226
+ providers: distinct(sessionRuns.map((r) => r.labels.provider)),
227
+ user_labels: distinct(sessionRuns.map((r) => r.labels.user_label))
228
+ },
229
+ totals,
230
+ verification
231
+ };
232
+ })
233
+ .sort((a, b) => b.totals.total_input_tokens_saved - a.totals.total_input_tokens_saved ||
234
+ a.session_id.localeCompare(b.session_id));
235
+ }
236
+ /**
237
+ * Build the aggregate id (which also becomes the output DIRECTORY name in `aggregate.ts`).
238
+ *
239
+ * Truncating `generatedAt` to second granularity (`YYYYMMDDHHMMSS`) made two rollups generated in
240
+ * the SAME second — e.g. a filtered then an unfiltered run back-to-back — collide on the same id,
241
+ * so the second silently OVERWROTE the first's directory (data loss). Include sub-second entropy so
242
+ * concurrent / back-to-back rollups get DISTINCT ids and paths:
243
+ * - the FULL timestamp digits (keeps milliseconds), and
244
+ * - a short random suffix, so even two rollups in the same millisecond (a reused explicit
245
+ * `generatedAt`, or sub-millisecond synchronous calls) still differ.
246
+ * The id stays stably sortable by time because the timestamp digits lead.
247
+ */
248
+ function aggregateIdFromGeneratedAt(generatedAt) {
249
+ const timestampDigits = generatedAt.replace(/[^0-9]/g, "");
250
+ const entropy = randomUUID().slice(0, 8);
251
+ return `aggregate-${timestampDigits}-${entropy}`;
252
+ }
253
+ /**
254
+ * PURE rollup: fold a set of normalized run records into the per-session + cross-session
255
+ * aggregate report. No IO. Applies the supplied local-label filters first.
256
+ */
257
+ export function buildAggregateReport(runs, options = {}) {
258
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
259
+ const filters = options.filters ?? {};
260
+ const matched = runs.filter((run) => runMatchesFilters(run, filters));
261
+ const totals = emptyTotals();
262
+ const verification = emptyVerification();
263
+ for (const run of matched) {
264
+ addRunToTotals(totals, run);
265
+ addRunToVerification(verification, run);
266
+ }
267
+ normalizeTotals(totals);
268
+ const sessions = buildSessions(matched);
269
+ return {
270
+ aggregate_id: aggregateIdFromGeneratedAt(generatedAt),
271
+ generated_at: generatedAt,
272
+ source_glob: options.sourceGlob ?? ".compaction/runs/*/report.json",
273
+ applied_filters: filters,
274
+ rollup_labels: options.rollupLabels ?? {},
275
+ session_count: sessions.length,
276
+ totals,
277
+ verification,
278
+ sessions,
279
+ by_provider: buildBreakdown(matched, (run) => run.labels.provider ?? NO_LABEL),
280
+ by_project: buildBreakdown(matched, (run) => run.labels.project ?? NO_LABEL),
281
+ by_workflow: buildBreakdown(matched, (run) => run.labels.workflow ?? NO_LABEL),
282
+ skipped_run_directories: options.skippedRunDirectories ?? [],
283
+ evidence_labels: AGGREGATE_EVIDENCE_LABELS,
284
+ limitations: STANDARD_LIMITATIONS
285
+ };
286
+ }
287
+ // ----------------------------------------------------------------------------------
288
+ // IO: read run records from the local .compaction/runs tree. The ONLY IO in this file.
289
+ // ----------------------------------------------------------------------------------
290
+ function isRecord(value) {
291
+ return typeof value === "object" && value !== null && !Array.isArray(value);
292
+ }
293
+ function num(value) {
294
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
295
+ }
296
+ /** Map a strong-eval token-accounting "measured" flag to a token-evidence tier. */
297
+ function tokenEvidenceFrom(measured) {
298
+ return measured === true ? "provider_reported" : "local_estimate";
299
+ }
300
+ function checkOutcomeFromStatus(status, kind) {
301
+ if (status === "passed")
302
+ return "pass";
303
+ if (status === "failed")
304
+ return "fail";
305
+ if (kind === "task" && status === "unsupported")
306
+ return "unsupported";
307
+ // recoverability "not_computed" / commitment "not_computed" / anything else → not_evaluated.
308
+ return "not_evaluated";
309
+ }
310
+ function readinessFrom(status) {
311
+ if (status === "ready" || status === "conditional" || status === "not_ready")
312
+ return status;
313
+ return "not_evaluated";
314
+ }
315
+ /**
316
+ * Assemble a normalized RunRecord from a run directory's local artifacts. report.json is
317
+ * REQUIRED (skipped otherwise). strong-eval.json and run-labels.json are OPTIONAL: absence
318
+ * is recorded honestly (not_evaluated verification, unknown output tokens), never inferred.
319
+ */
320
+ export function assembleRunRecord(input) {
321
+ const { report, strongEval } = input;
322
+ const notes = [];
323
+ let tokenEvidence = "local_estimate";
324
+ let inputBefore = report.original_input_tokens;
325
+ let inputAfter = report.compacted_input_tokens;
326
+ let inputSaved = report.tokens_saved;
327
+ let outputBefore = "unknown";
328
+ let outputAfter = "unknown";
329
+ let costBefore = report.cost_before_per_run;
330
+ let costAfter = report.cost_after_per_run;
331
+ let saving = report.saving_per_run;
332
+ let readiness = "not_evaluated";
333
+ let recoverability = "not_evaluated";
334
+ let commitment = "not_evaluated";
335
+ let taskCheck = "not_evaluated";
336
+ let strongEvalPresent = false;
337
+ let model = report.model;
338
+ if (isRecord(strongEval)) {
339
+ strongEvalPresent = true;
340
+ const tokenAccounting = isRecord(strongEval.token_accounting) ? strongEval.token_accounting : undefined;
341
+ if (tokenAccounting) {
342
+ tokenEvidence = tokenEvidenceFrom(tokenAccounting.measured);
343
+ if (typeof tokenAccounting.model === "string" && tokenAccounting.model.length > 0) {
344
+ model = tokenAccounting.model;
345
+ }
346
+ const ib = isRecord(tokenAccounting.input_tokens_before) ? num(tokenAccounting.input_tokens_before.value) : null;
347
+ const ia = isRecord(tokenAccounting.input_tokens_after) ? num(tokenAccounting.input_tokens_after.value) : null;
348
+ const ob = isRecord(tokenAccounting.output_tokens_before) ? num(tokenAccounting.output_tokens_before.value) : null;
349
+ const oa = isRecord(tokenAccounting.output_tokens_after) ? num(tokenAccounting.output_tokens_after.value) : null;
350
+ const cb = num(tokenAccounting.estimated_cost_before_usd);
351
+ const ca = num(tokenAccounting.estimated_cost_after_usd);
352
+ const sv = num(tokenAccounting.estimated_saving_per_run_usd);
353
+ const saved = num(tokenAccounting.input_tokens_saved);
354
+ if (ib !== null)
355
+ inputBefore = ib;
356
+ if (ia !== null)
357
+ inputAfter = ia;
358
+ if (saved !== null)
359
+ inputSaved = saved;
360
+ if (ob !== null)
361
+ outputBefore = ob;
362
+ if (oa !== null)
363
+ outputAfter = oa;
364
+ if (cb !== null)
365
+ costBefore = cb;
366
+ if (ca !== null)
367
+ costAfter = ca;
368
+ if (sv !== null)
369
+ saving = sv;
370
+ }
371
+ else {
372
+ notes.push("strong-eval.json present but token_accounting was missing; used report.json token/cost figures.");
373
+ }
374
+ const readinessBlock = isRecord(strongEval.readiness) ? strongEval.readiness : undefined;
375
+ readiness = readinessFrom(readinessBlock?.readiness);
376
+ const recovEval = isRecord(strongEval.recoverability_eval) ? strongEval.recoverability_eval : undefined;
377
+ recoverability = checkOutcomeFromStatus(recovEval?.recoverability, "recoverability");
378
+ const commitmentBlock = isRecord(strongEval.commitment_preservation) ? strongEval.commitment_preservation : undefined;
379
+ commitment = checkOutcomeFromStatus(commitmentBlock?.status, "commitment");
380
+ const taskBlock = isRecord(strongEval.task_check) ? strongEval.task_check : undefined;
381
+ taskCheck = checkOutcomeFromStatus(taskBlock?.status, "task");
382
+ }
383
+ else {
384
+ tokenEvidence = "local_estimate";
385
+ notes.push("No strong-eval.json: verification outcomes are not_evaluated and output tokens are unknown for this run.");
386
+ }
387
+ return {
388
+ run_id: input.runId,
389
+ run_directory: input.runDirectory,
390
+ report_path: input.reportPath,
391
+ model,
392
+ token_evidence: tokenEvidence,
393
+ input_tokens_before: inputBefore,
394
+ input_tokens_after: inputAfter,
395
+ input_tokens_saved: inputSaved,
396
+ output_tokens_before: outputBefore,
397
+ output_tokens_after: outputAfter,
398
+ estimated_cost_before_usd: costBefore,
399
+ estimated_cost_after_usd: costAfter,
400
+ estimated_saving_usd: saving,
401
+ readiness,
402
+ recoverability,
403
+ commitment_preservation: commitment,
404
+ task_check: taskCheck,
405
+ strong_eval_present: strongEvalPresent,
406
+ labels: input.labels,
407
+ notes
408
+ };
409
+ }
410
+ async function readJsonIfPresent(path) {
411
+ try {
412
+ return { value: JSON.parse(await readFile(path, "utf8")) };
413
+ }
414
+ catch (error) {
415
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
416
+ if (code === "ENOENT")
417
+ return {};
418
+ if (error instanceof SyntaxError)
419
+ return { reason: "invalid JSON" };
420
+ return { reason: "could not read file" };
421
+ }
422
+ }
423
+ function validReport(value) {
424
+ if (!isRecord(value))
425
+ return false;
426
+ return (typeof value.run_id === "string" &&
427
+ num(value.original_input_tokens) !== null &&
428
+ num(value.compacted_input_tokens) !== null &&
429
+ num(value.tokens_saved) !== null &&
430
+ num(value.cost_before_per_run) !== null &&
431
+ num(value.cost_after_per_run) !== null &&
432
+ num(value.saving_per_run) !== null);
433
+ }
434
+ /**
435
+ * Read every run directory under `runsDirectory`, assembling one RunRecord per directory that
436
+ * carries a valid report.json. The ONLY IO path. Reads exclusively local files; makes NO
437
+ * network call. Directories without a valid report.json are recorded as skipped, never dropped
438
+ * silently.
439
+ */
440
+ export async function collectRunRecords(runsDirectory = ".compaction/runs") {
441
+ let runDirs;
442
+ try {
443
+ const entries = await readdir(runsDirectory, { withFileTypes: true });
444
+ runDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
445
+ }
446
+ catch (error) {
447
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "unknown";
448
+ if (code === "ENOENT")
449
+ return { runs: [], skipped: [] };
450
+ throw error;
451
+ }
452
+ const runs = [];
453
+ const skipped = [];
454
+ for (const dirName of runDirs) {
455
+ const runDirectory = join(runsDirectory, dirName);
456
+ const reportPath = join(runDirectory, "report.json");
457
+ const reportRead = await readJsonIfPresent(reportPath);
458
+ if (reportRead.value === undefined) {
459
+ skipped.push({ run_directory: runDirectory, reason: reportRead.reason ?? "report.json not found" });
460
+ continue;
461
+ }
462
+ if (!validReport(reportRead.value)) {
463
+ skipped.push({ run_directory: runDirectory, reason: "report.json missing required fields" });
464
+ continue;
465
+ }
466
+ const report = reportRead.value;
467
+ const strongRead = await readJsonIfPresent(join(runDirectory, "strong-eval.json"));
468
+ const labelsRead = await readJsonIfPresent(join(runDirectory, "run-labels.json"));
469
+ const labels = parseRunLabels(labelsRead.value);
470
+ runs.push(assembleRunRecord({
471
+ runId: report.run_id,
472
+ runDirectory,
473
+ reportPath,
474
+ report,
475
+ strongEval: strongRead.value,
476
+ labels
477
+ }));
478
+ }
479
+ return { runs, skipped };
480
+ }
481
+ /** Read records + build the report in one call (local IO + pure rollup). */
482
+ export async function createAggregateReport(runsDirectory = ".compaction/runs", options = {}) {
483
+ const { runs, skipped } = await collectRunRecords(runsDirectory);
484
+ return buildAggregateReport(runs, {
485
+ ...options,
486
+ sourceGlob: options.sourceGlob ?? `${runsDirectory}/*/report.json`,
487
+ skippedRunDirectories: skipped
488
+ });
489
+ }
490
+ //# sourceMappingURL=session-aggregate.js.map
@@ -0,0 +1,51 @@
1
+ import type { AgentTrace, TraceMessage, WasteFinding } from "./types.js";
2
+ export interface SkillInjectionFinding extends WasteFinding {
3
+ category: "repeated_skill_injection";
4
+ skillName: string;
5
+ /** The first (retained) copy's message id. Never compacted. */
6
+ firstCopyMessageId: string;
7
+ /** The later byte-identical copy's message id (the redundant, reportable volume). */
8
+ redundantCopyMessageId: string;
9
+ }
10
+ export interface SkillInjectionSkillAttribution {
11
+ skill_name: string;
12
+ /** Number of redundant byte-identical copies (second-and-later occurrences). */
13
+ redundant_byte_identical_copies: number;
14
+ /** Estimated (chars/4) trace tokens carried by the redundant copies — NOT realized savings. */
15
+ estimated_tokens: number;
16
+ redundant_copy_message_ids: string[];
17
+ }
18
+ export interface SkillInjectionAdvisory {
19
+ /** Report-only advisory. Nothing was compacted; these are detection-only figures. */
20
+ report_only: true;
21
+ /** Distinct skills that have at least one redundant byte-identical copy. */
22
+ skills: SkillInjectionSkillAttribution[];
23
+ /** Total redundant byte-identical copies across all skills. */
24
+ total_redundant_byte_identical_copies: number;
25
+ /** Total addressable est trace-token volume (chars/4) the policy COULD unlock if applied later. */
26
+ total_addressable_estimated_tokens: number;
27
+ /** Number of first/unique injection copies that are retained (never candidates). */
28
+ first_copy_count: number;
29
+ estimate_label: string;
30
+ recommendation: string;
31
+ }
32
+ export declare function parseSkillName(content: string): string | null;
33
+ export declare function isSkillInjection(message: TraceMessage): boolean;
34
+ /**
35
+ * Detect repeated byte-identical, same-skill skill injections.
36
+ *
37
+ * Key = `${skillName}:${normalizeExactContent(content)}` — exact, NOT a prefix, NOT fuzzy.
38
+ * First occurrence of each key is retained; each later exact-key copy is a redundant
39
+ * byte-identical candidate (the reportable volume). Different normalized content (e.g.
40
+ * divergent ARGUMENTS) ⇒ different key ⇒ never grouped (near-identical excluded by
41
+ * construction). Two different skills can never share a key because the key is prefixed
42
+ * with the parsed skill name.
43
+ *
44
+ * REPORT-ONLY: returns findings; never mutates the trace.
45
+ */
46
+ export declare function detectSkillInjectionRepetition(trace: AgentTrace): SkillInjectionFinding[];
47
+ /**
48
+ * Build the report-only advisory (per-skill spend attribution + total addressable est).
49
+ * Pure: derived from the detector findings; nothing is compacted.
50
+ */
51
+ export declare function buildSkillInjectionAdvisory(trace: AgentTrace): SkillInjectionAdvisory;