@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,394 @@
1
+ /**
2
+ * Billing-delta measurement record — operator-run A/B protocol (local-first).
3
+ *
4
+ * Implements Alternative 1 / §3 of docs/design/billing-delta-loop.md (the accepted
5
+ * methodology direction): a LOCAL, OPERATOR-RUN measurement record. The OPERATOR
6
+ * performs the real workflow runs themselves (control arm = original context;
7
+ * treatment arm = apply-context-approved compacted context), reads the billed costs
8
+ * from THEIR OWN provider console / billing export, and ENTERS them. This module
9
+ * records runs with provenance, computes per-arm statistics and the delta of means,
10
+ * applies the accepted methodology's honesty rubric, and emits a caveated verdict.
11
+ *
12
+ * HARD HONESTY RAILS (see the accepted doc §2.4, §1(a)-(e), Risks):
13
+ * - This module makes NO provider call, reads NO credential, has NO network access.
14
+ * - Operator-entered figures are labeled `operator_entered: true`. The tool CANNOT
15
+ * verify them against the provider.
16
+ * - The tool NEVER emits the phrase "billing-confirmed savings" as a claim and NEVER
17
+ * sets `billing_confirmed: true`. This is NOT a Tier-3 claim. `ValueProof` is
18
+ * untouched (its `billing_confirmed_savings.claimed` stays false).
19
+ * - The strongest verdict is a "measured, caveated delta on N runs of ONE workflow
20
+ * under recorded conditions" — explicitly NOT a general savings claim.
21
+ * - Aggregate cost figures only (LOW sensitivity). NO prompt/completion content is
22
+ * stored anywhere in the record.
23
+ */
24
+ import { contentHash } from "../content-hash.js";
25
+ import { getModelPricing, isKnownModel } from "../pricing.js";
26
+ export const BILLING_DELTA_RECORD_ROOT = ".compaction/billing-delta";
27
+ export const BILLING_DELTA_RECORD_SCHEMA = "billing-delta.measurement-record.v1";
28
+ export const BILLING_DELTA_REPORT_SCHEMA = "billing-delta.report.v1";
29
+ export const BILLING_DELTA_PRIVACY_POSTURE = {
30
+ sensitivity: "low",
31
+ aggregate_cost_only: true,
32
+ operator_entered_figures: true,
33
+ local_only: true
34
+ };
35
+ /** The documented decision criterion. Named in every report. */
36
+ export const BILLING_DELTA_CRITERION = "delta-of-means interval = [mean_delta - 2*SE, mean_delta + 2*SE], SE = sqrt(sd_control^2/n_control + sd_treatment^2/n_treatment); " +
37
+ "verdict measured_caveated_delta only when N>=3 per arm AND the interval excludes zero (conservative ~2-standard-error rule, sample SD, no distributional claim beyond it)";
38
+ /** The standard-error multiplier used to form the conservative interval. Documented and fixed (no over-engineered statistics). */
39
+ export const BILLING_DELTA_INTERVAL_K = 2;
40
+ /** Verbatim-ish anecdote note required by the accepted doc (§2.4: "a single A/B run is an anecdote, not proof"). */
41
+ export const BILLING_DELTA_ANECDOTE_NOTE = "A single A/B pair is an anecdote, not proof: N=1 per arm cannot distinguish a compaction effect from run-to-run noise.";
42
+ /**
43
+ * Claim-level labels for the token-delta section. Token-level, provider-reported — never a
44
+ * billing claim. The label is selected by the ACTUAL sign/status of the token delta so it
45
+ * never calls a non-reduction a "reduction" (an overclaim by mislabel): a negative input
46
+ * delta means the treatment context GREW (an INCREASE), zero means NO CHANGE, and the
47
+ * insufficient/suppressed statuses have no computed delta at all. Every variant preserves the
48
+ * "NOT billing-confirmed savings" disclaimer and makes no savings claim.
49
+ */
50
+ export const BILLING_DELTA_TOKEN_DELTA_LABELS = {
51
+ /** Genuine reduction: treatment used fewer input tokens than control (positive input delta). */
52
+ reduction: "provider-reported token reduction (workflow-scoped) — NOT billing-confirmed savings",
53
+ /** Increase: treatment used MORE input tokens than control (negative input delta). Not a saving. */
54
+ increase: "provider-reported token INCREASE (workflow-scoped) — NOT a saving; NOT billing-confirmed savings",
55
+ /** No change: the input token delta is exactly zero. */
56
+ no_change: "provider-reported token delta: no change (workflow-scoped) — NOT billing-confirmed savings",
57
+ /** Not computed: insufficient token data or deltas suppressed by a failed task. */
58
+ not_computed: "provider-reported token delta not computed (workflow-scoped) — NOT billing-confirmed savings"
59
+ };
60
+ /**
61
+ * Select the sign/status-accurate claim label. Reduction is used ONLY when the input delta is
62
+ * genuinely positive (treatment used fewer tokens); a negative delta is an increase, zero is no
63
+ * change, and a null delta (insufficient/suppressed status) is "not computed". This never
64
+ * strengthens a claim — it only makes the existing label honest for the actual sign/status.
65
+ */
66
+ export function selectTokenDeltaLabel(status, inputTokenDelta) {
67
+ if (status !== "computed" || inputTokenDelta === null) {
68
+ return BILLING_DELTA_TOKEN_DELTA_LABELS.not_computed;
69
+ }
70
+ if (inputTokenDelta > 0)
71
+ return BILLING_DELTA_TOKEN_DELTA_LABELS.reduction;
72
+ if (inputTokenDelta < 0)
73
+ return BILLING_DELTA_TOKEN_DELTA_LABELS.increase;
74
+ return BILLING_DELTA_TOKEN_DELTA_LABELS.no_change;
75
+ }
76
+ function deriveMeasurementId(workflowId, generatedAt) {
77
+ const seed = `${workflowId}::${generatedAt}`;
78
+ return `bdm_${contentHash(seed).slice(0, 16)}`;
79
+ }
80
+ export function buildTreatmentApprovalLink(path, fileContent) {
81
+ return {
82
+ apply_approval_record_path: path,
83
+ apply_approval_record_sha256: contentHash(fileContent)
84
+ };
85
+ }
86
+ export function initMeasurementRecord(input) {
87
+ const measurement_id = input.measurementId ?? deriveMeasurementId(input.workflowId, input.generatedAt);
88
+ let apply_approval_link;
89
+ if (input.applyApprovalRecordPath && input.applyApprovalRecordContent !== undefined) {
90
+ apply_approval_link = buildTreatmentApprovalLink(input.applyApprovalRecordPath, input.applyApprovalRecordContent);
91
+ }
92
+ return {
93
+ schema: BILLING_DELTA_RECORD_SCHEMA,
94
+ measurement_id,
95
+ created_at: input.generatedAt,
96
+ workflow_id: input.workflowId,
97
+ workflow_description: input.workflowDescription,
98
+ fixed_conditions: {
99
+ model: input.model,
100
+ model_version: input.modelVersion ?? "unknown",
101
+ input_description: input.inputDescription,
102
+ window: input.window
103
+ },
104
+ ...(apply_approval_link ? { apply_approval_link } : {}),
105
+ runs: [],
106
+ privacy_posture: BILLING_DELTA_PRIVACY_POSTURE,
107
+ operator_entered: true,
108
+ billing_confirmed: false,
109
+ tool_makes_provider_call: false
110
+ };
111
+ }
112
+ function deriveRunId(record, input) {
113
+ const seed = `${record.measurement_id}::${input.arm}::${input.runTimestamp}::${record.runs.length}`;
114
+ return `run_${contentHash(seed).slice(0, 12)}`;
115
+ }
116
+ export function addRun(record, input) {
117
+ if (!Number.isFinite(input.billedCost) || input.billedCost < 0) {
118
+ throw new Error("billed cost must be a finite, non-negative number");
119
+ }
120
+ if (!input.currency || input.currency.trim() === "") {
121
+ throw new Error("currency is required");
122
+ }
123
+ let treatment_approval_link;
124
+ if (input.treatmentApprovalRecordPath && input.treatmentApprovalRecordContent !== undefined) {
125
+ treatment_approval_link = buildTreatmentApprovalLink(input.treatmentApprovalRecordPath, input.treatmentApprovalRecordContent);
126
+ }
127
+ const run = {
128
+ run_id: input.runId ?? deriveRunId(record, input),
129
+ arm: input.arm,
130
+ billed_cost: input.billedCost,
131
+ currency: input.currency,
132
+ operator_entered: true,
133
+ ...(input.providerReportedUsage ? { provider_reported_usage: input.providerReportedUsage } : {}),
134
+ run_timestamp: input.runTimestamp,
135
+ ...(input.notes ? { notes: input.notes } : {}),
136
+ ...(treatment_approval_link ? { treatment_approval_link } : {})
137
+ };
138
+ // Backfill the machine-readable privacy posture when rewriting a record created
139
+ // before this field existed, so absence reliably means "written pre-field" and an
140
+ // operator continuing an old measurement does not keep persisting a posture-less file.
141
+ // The posture is a structural property of the substrate (local, operator-entered,
142
+ // aggregate-only), not a per-record choice, so backfilling it is a restatement.
143
+ return {
144
+ ...record,
145
+ privacy_posture: record.privacy_posture ?? BILLING_DELTA_PRIVACY_POSTURE,
146
+ runs: [...record.runs, run]
147
+ };
148
+ }
149
+ function mean(values) {
150
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
151
+ }
152
+ function sampleStandardDeviation(values) {
153
+ if (values.length < 2)
154
+ return null;
155
+ const m = mean(values);
156
+ const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / (values.length - 1);
157
+ return Math.sqrt(variance);
158
+ }
159
+ function armStatistics(arm, runs) {
160
+ const costs = runs.filter((r) => r.arm === arm).map((r) => r.billed_cost);
161
+ if (costs.length === 0) {
162
+ return { arm, n: 0, mean: null, min: null, max: null, sample_sd: null };
163
+ }
164
+ return {
165
+ arm,
166
+ n: costs.length,
167
+ mean: mean(costs),
168
+ min: Math.min(...costs),
169
+ max: Math.max(...costs),
170
+ sample_sd: sampleStandardDeviation(costs)
171
+ };
172
+ }
173
+ function collectConfounders(runs) {
174
+ return runs
175
+ .filter((r) => typeof r.notes === "string" && r.notes.trim() !== "")
176
+ .map((r) => `[${r.arm} ${r.run_id}] ${r.notes}`);
177
+ }
178
+ function collectTreatmentLinks(runs) {
179
+ return runs
180
+ .filter((r) => r.arm === "treatment" && r.treatment_approval_link)
181
+ .map((r) => r.treatment_approval_link);
182
+ }
183
+ const BASE_CAVEATS = (workflowId, nControl, nTreatment) => [
184
+ `Measured, caveated delta on N runs of ONE workflow (workflow_id=${workflowId}; ` +
185
+ `n_control=${nControl}, n_treatment=${nTreatment}) under the recorded fixed conditions only — ` +
186
+ "NOT a billing-confirmed general savings claim.",
187
+ "Operator-entered billing figures, not provider-verified by this tool. The tool made no provider call, " +
188
+ "read no credential, and has no network access.",
189
+ "Run-to-run nondeterminism, prompt-caching, model/version drift, pricing changes, retries, and per-run " +
190
+ "billing attribution are confounders; control or record them. A delta interval that includes zero is not a saving.",
191
+ "This is NOT 'billing-confirmed savings' (Tier 3) and does NOT flip ValueProof.billing_confirmed_savings.claimed."
192
+ ];
193
+ /** Parse the task-quality flag from a run's notes ("task: ok" | "task: failed"); "unknown" otherwise. */
194
+ function taskFlagFromNotes(notes) {
195
+ const m = /task:\s*(ok|failed)/i.exec(notes ?? "");
196
+ return m ? m[1].toLowerCase() : "unknown";
197
+ }
198
+ /** Resolve a model name against the price table, tolerating dated variants (e.g. -20251001). */
199
+ function resolvePricedModel(model) {
200
+ if (isKnownModel(model))
201
+ return model;
202
+ const undated = model.replace(/-\d{8}$/, "");
203
+ if (undated !== model && isKnownModel(undated))
204
+ return undated;
205
+ return null;
206
+ }
207
+ function tokenArmStatistics(arm, runs) {
208
+ const withTokens = runs.filter((r) => r.arm === arm && typeof r.provider_reported_usage?.input_tokens === "number");
209
+ const inputs = withTokens.map((r) => r.provider_reported_usage.input_tokens);
210
+ const outputs = withTokens
211
+ .map((r) => r.provider_reported_usage?.output_tokens)
212
+ .filter((v) => typeof v === "number");
213
+ const distinct = [...new Set(inputs)].sort((a, b) => a - b);
214
+ return {
215
+ arm,
216
+ n_with_tokens: withTokens.length,
217
+ input_tokens_distinct: distinct,
218
+ input_deterministic: withTokens.length > 0 && distinct.length === 1,
219
+ mean_input_tokens: inputs.length > 0 ? inputs.reduce((a, b) => a + b, 0) / inputs.length : null,
220
+ mean_output_tokens: outputs.length > 0 ? outputs.reduce((a, b) => a + b, 0) / outputs.length : null,
221
+ output_min: outputs.length > 0 ? Math.min(...outputs) : null,
222
+ output_max: outputs.length > 0 ? Math.max(...outputs) : null
223
+ };
224
+ }
225
+ /**
226
+ * Compute the provider-reported token-delta section. Input tokens are deterministic for a
227
+ * fixed context+prompt, so one tokened run per arm pins the input delta exactly; the
228
+ * billed-cost verdict and its N>=3 criterion are unaffected by this section.
229
+ */
230
+ export function computeTokenDelta(record) {
231
+ const control = tokenArmStatistics("control", record.runs);
232
+ const treatment = tokenArmStatistics("treatment", record.runs);
233
+ const task_quality = {
234
+ control: record.runs.filter((r) => r.arm === "control").map((r) => taskFlagFromNotes(r.notes)),
235
+ treatment: record.runs.filter((r) => r.arm === "treatment").map((r) => taskFlagFromNotes(r.notes)),
236
+ any_failed: record.runs.some((r) => taskFlagFromNotes(r.notes) === "failed")
237
+ };
238
+ const notes = [];
239
+ const base = {
240
+ control,
241
+ treatment,
242
+ task_quality,
243
+ billing_confirmed: false
244
+ };
245
+ if (task_quality.any_failed) {
246
+ notes.push("Token deltas SUPPRESSED: at least one run recorded 'task: failed'. A cheaper run that " +
247
+ "failed the task is not a saving; fix task quality before reading token deltas.");
248
+ return {
249
+ ...base,
250
+ claim_label: selectTokenDeltaLabel("suppressed_task_failure", null),
251
+ status: "suppressed_task_failure",
252
+ input_token_delta: null,
253
+ input_token_delta_pct: null,
254
+ output_token_delta: null,
255
+ estimated_cost_delta: null,
256
+ notes
257
+ };
258
+ }
259
+ if (control.n_with_tokens === 0 || treatment.n_with_tokens === 0) {
260
+ notes.push(`Insufficient token data: provider-reported input_tokens present on ` +
261
+ `${control.n_with_tokens} control and ${treatment.n_with_tokens} treatment run(s); ` +
262
+ "need at least one tokened run per arm (input tokens are deterministic per arm, so one pins the input delta exactly).");
263
+ return {
264
+ ...base,
265
+ claim_label: selectTokenDeltaLabel("insufficient_token_data", null),
266
+ status: "insufficient_token_data",
267
+ input_token_delta: null,
268
+ input_token_delta_pct: null,
269
+ output_token_delta: null,
270
+ estimated_cost_delta: null,
271
+ notes
272
+ };
273
+ }
274
+ const input_token_delta = control.mean_input_tokens - treatment.mean_input_tokens;
275
+ const input_token_delta_pct = control.mean_input_tokens > 0
276
+ ? input_token_delta / control.mean_input_tokens
277
+ : null;
278
+ const output_token_delta = control.mean_output_tokens !== null && treatment.mean_output_tokens !== null
279
+ ? control.mean_output_tokens - treatment.mean_output_tokens
280
+ : null;
281
+ if (!control.input_deterministic || !treatment.input_deterministic) {
282
+ notes.push("Input tokens were NOT identical across runs within an arm — fixed conditions may have drifted; " +
283
+ "the input delta below uses arm means and should be read with that caveat.");
284
+ }
285
+ notes.push("Output tokens vary run to run; the output delta is a mean difference — read with the per-arm min/max spread.");
286
+ let estimated_cost_delta = null;
287
+ const pricedModel = resolvePricedModel(record.fixed_conditions.model);
288
+ if (pricedModel !== null) {
289
+ const pricing = getModelPricing(pricedModel);
290
+ const amount = (input_token_delta * pricing.inputPerMillionUsd) / 1_000_000 +
291
+ ((output_token_delta ?? 0) * pricing.outputPerMillionUsd) / 1_000_000;
292
+ // When the output delta is unavailable but the model has material output pricing,
293
+ // the cost estimate prices the output component as zero. Label it input-only/incomplete
294
+ // so a partial figure is not read as a complete cost delta.
295
+ const outputUnpriced = output_token_delta === null && pricing.outputPerMillionUsd > 0;
296
+ estimated_cost_delta = {
297
+ amount_usd: amount,
298
+ cost_source: "price_table_estimate",
299
+ note: outputUnpriced
300
+ ? `Price-table ESTIMATE for ${pricedModel} — INPUT-ONLY/INCOMPLETE: output-token delta is ` +
301
+ "unavailable (one arm lacks provider-reported output tokens), so the output cost component " +
302
+ "is priced as zero. Read as a lower bound on the input side only, never a complete or " +
303
+ "billing-confirmed figure."
304
+ : `Price-table ESTIMATE for ${pricedModel} applied to the token deltas — an estimate, ` +
305
+ "never a billed or billing-confirmed figure."
306
+ };
307
+ }
308
+ else {
309
+ notes.push(`No price-table entry for model '${record.fixed_conditions.model}'; estimated cost delta omitted ` +
310
+ "(token deltas stand on their own).");
311
+ }
312
+ return {
313
+ ...base,
314
+ claim_label: selectTokenDeltaLabel("computed", input_token_delta),
315
+ status: "computed",
316
+ input_token_delta,
317
+ input_token_delta_pct,
318
+ output_token_delta,
319
+ estimated_cost_delta,
320
+ notes
321
+ };
322
+ }
323
+ export function computeReport(record, generatedAt) {
324
+ const control = armStatistics("control", record.runs);
325
+ const treatment = armStatistics("treatment", record.runs);
326
+ const currency = record.runs.length > 0 ? record.runs[0].currency : null;
327
+ let mean_delta = null;
328
+ if (control.mean !== null && treatment.mean !== null) {
329
+ mean_delta = control.mean - treatment.mean;
330
+ }
331
+ let delta_standard_error = null;
332
+ let delta_interval = null;
333
+ let delta_interval_excludes_zero = false;
334
+ if (mean_delta !== null &&
335
+ control.sample_sd !== null &&
336
+ treatment.sample_sd !== null &&
337
+ control.n >= 2 &&
338
+ treatment.n >= 2) {
339
+ delta_standard_error = Math.sqrt(control.sample_sd ** 2 / control.n + treatment.sample_sd ** 2 / treatment.n);
340
+ const margin = BILLING_DELTA_INTERVAL_K * delta_standard_error;
341
+ delta_interval = { lower: mean_delta - margin, upper: mean_delta + margin };
342
+ delta_interval_excludes_zero =
343
+ (delta_interval.lower > 0 && delta_interval.upper > 0) ||
344
+ (delta_interval.lower < 0 && delta_interval.upper < 0);
345
+ }
346
+ const verdict_notes = [];
347
+ let verdict;
348
+ if (control.n < 3 || treatment.n < 3) {
349
+ verdict = "insufficient_data";
350
+ verdict_notes.push(BILLING_DELTA_ANECDOTE_NOTE);
351
+ verdict_notes.push(`Need N>=3 per arm (have n_control=${control.n}, n_treatment=${treatment.n}). ` +
352
+ "A single pair is an anecdote, not proof; fewer than 3 runs per arm is insufficient data.");
353
+ }
354
+ else if (!delta_interval_excludes_zero) {
355
+ verdict = "inconclusive";
356
+ verdict_notes.push("The delta-of-means interval includes zero: the measured difference is not distinguishable from " +
357
+ "run-to-run noise at the conservative ~2-standard-error margin. Not a saving.");
358
+ }
359
+ else {
360
+ verdict = "measured_caveated_delta";
361
+ const direction = mean_delta > 0 ? "lower (a saving)" : "higher (a cost increase)";
362
+ verdict_notes.push(`The delta-of-means interval excludes zero: treatment is measured ${direction} than control under ` +
363
+ "the recorded conditions. This is a measured, caveated delta on this workflow only — not a general claim.");
364
+ }
365
+ return {
366
+ schema: BILLING_DELTA_REPORT_SCHEMA,
367
+ measurement_id: record.measurement_id,
368
+ workflow_id: record.workflow_id,
369
+ generated_at: generatedAt,
370
+ currency,
371
+ fixed_conditions: record.fixed_conditions,
372
+ control,
373
+ treatment,
374
+ mean_delta,
375
+ delta_standard_error,
376
+ delta_interval,
377
+ delta_interval_excludes_zero,
378
+ criterion: BILLING_DELTA_CRITERION,
379
+ verdict,
380
+ verdict_notes,
381
+ claim_scope: `workflow_id=${record.workflow_id} under fixed conditions ` +
382
+ `(model=${record.fixed_conditions.model}, model_version=${record.fixed_conditions.model_version}, ` +
383
+ `window=${record.fixed_conditions.window}); n_control=${control.n}, n_treatment=${treatment.n}. ` +
384
+ "The measured workflow and conditions ONLY.",
385
+ not_a_general_claim: true,
386
+ billing_confirmed: false,
387
+ operator_entered: true,
388
+ confounders: collectConfounders(record.runs),
389
+ caveats: BASE_CAVEATS(record.workflow_id, control.n, treatment.n),
390
+ treatment_provenance_links: collectTreatmentLinks(record.runs),
391
+ token_delta: computeTokenDelta(record)
392
+ };
393
+ }
394
+ //# sourceMappingURL=billing-delta-record.js.map
@@ -0,0 +1,74 @@
1
+ import type { AgentTrace } from "./types.js";
2
+ import type { UsageMetadata } from "./usage-metadata.js";
3
+ import type { TraceFingerprint } from "./trace-fingerprint.js";
4
+ export interface CaptureAdapter {
5
+ /** Stable identifier for this adapter. Used in provenance and logging. */
6
+ readonly id: string;
7
+ /**
8
+ * Normalize a raw input (session file path, event stream, export file, etc.)
9
+ * into a CapturedRun containing a normalized AgentTrace, UsageMetadata,
10
+ * and provenance information.
11
+ *
12
+ * The adapter is responsible for:
13
+ * - Setting source: "real_captured" only when the trace comes from an agent
14
+ * runtime session with identifiable workflow context (see real_captured semantics).
15
+ * - Attaching UsageMetadata at normalization time, not re-deriving it downstream.
16
+ * - Setting provenance.captureAdapter to this adapter's id.
17
+ */
18
+ normalize(input: CaptureAdapterInput): Promise<CapturedRun>;
19
+ }
20
+ export interface CaptureAdapterInput {
21
+ /** Path to the source artifact (session JSONL file, export file, stdout dump). */
22
+ sourcePath: string;
23
+ /** Human-readable label for CLI output. */
24
+ label?: string;
25
+ /** Additional adapter-specific options. */
26
+ options?: Record<string, unknown>;
27
+ }
28
+ export interface CapturedRun {
29
+ /** The normalized agent trace. */
30
+ trace: AgentTrace;
31
+ /** Usage metadata attached at capture time. Never re-derived downstream. */
32
+ usage: UsageMetadata;
33
+ /** Provenance record for the capture. */
34
+ provenance: CaptureProvenance;
35
+ }
36
+ export interface CaptureSubagentProvenance {
37
+ /** Unique agent identifier from the subagent JSONL entries. */
38
+ agentId: string;
39
+ /** Total user + assistant entries processed from this subagent. */
40
+ entryCount: number;
41
+ /** Agent type from meta.json, if present. */
42
+ agentType?: string;
43
+ /** Human-readable description from meta.json, if present. */
44
+ description?: string;
45
+ /** Tool use ID linking this subagent to the parent session tool call, from meta.json if present. */
46
+ toolUseId?: string;
47
+ }
48
+ export interface CaptureProvenance {
49
+ /** The adapter that produced this run. */
50
+ captureAdapter: string;
51
+ /** Stable source path or identifier. */
52
+ sourcePath: string;
53
+ /** ISO 8601 timestamp of when the capture was performed. */
54
+ capturedAt: string;
55
+ /** Claude Code session ID, OpenAI Agents run ID, or equivalent. Null if unknown. */
56
+ sessionId: string | null;
57
+ /** Any warnings or limitations the adapter encountered. */
58
+ warnings: string[];
59
+ /** Any limitations that apply to the captured run. */
60
+ limitations: string[];
61
+ /** Provenance records for each included subagent, when --include-subagents is used. */
62
+ subagents?: CaptureSubagentProvenance[];
63
+ /**
64
+ * Non-sensitive per-run distinctness fingerprint (a one-way SHA-256 DIGEST over the
65
+ * canonical normalized trace content — NEVER raw messages/prompts/tool-output).
66
+ *
67
+ * Lets future dogfood records cite a verifiable per-run distinctness proof so that
68
+ * genuinely-different captured sessions are distinguishable from re-captures of the
69
+ * same session (preventing real_captured N over-counting). When an adapter cannot
70
+ * produce a fingerprint, this is omitted and distinctness must be reported as
71
+ * `not_verified` — never counted as distinct. See `src/core/trace-fingerprint.ts`.
72
+ */
73
+ traceFingerprint?: TraceFingerprint;
74
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=capture-adapter.js.map
@@ -0,0 +1,29 @@
1
+ export interface ParsedRunCommand {
2
+ executable: string;
3
+ args: string[];
4
+ }
5
+ export interface LocalCommandRun {
6
+ runId: string;
7
+ command: ParsedRunCommand;
8
+ stdout: string;
9
+ stderr: string;
10
+ rawOutput: string;
11
+ exitCode: number;
12
+ signal: NodeJS.Signals | null;
13
+ pid: number | null;
14
+ startedAt: string;
15
+ endedAt: string;
16
+ durationMs: number;
17
+ }
18
+ export interface PersistedLocalCommandRun {
19
+ run: LocalCommandRun;
20
+ outputDirectory: string;
21
+ stdoutPath: string;
22
+ stderrPath: string;
23
+ rawOutputPath: string;
24
+ metadataPath: string;
25
+ }
26
+ export declare function parseRunCommand(commandParts: string[]): ParsedRunCommand;
27
+ export declare function createRunId(now?: Date): string;
28
+ export declare function executeLocalCommand(command: ParsedRunCommand, runId?: string): Promise<LocalCommandRun>;
29
+ export declare function persistLocalCommandRun(run: LocalCommandRun, runsRoot?: string): Promise<PersistedLocalCommandRun>;
@@ -0,0 +1,96 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ export function parseRunCommand(commandParts) {
6
+ const [executable, ...args] = commandParts;
7
+ if (!executable) {
8
+ throw new Error("Usage: compaction run -- <command> [args...]");
9
+ }
10
+ return { executable, args };
11
+ }
12
+ export function createRunId(now = new Date()) {
13
+ const timestamp = now.toISOString().replace(/[:.]/g, "-");
14
+ return `run-${timestamp}-${randomUUID().slice(0, 8)}`;
15
+ }
16
+ export async function executeLocalCommand(command, runId = createRunId()) {
17
+ const startMs = Date.now();
18
+ const startedAt = new Date(startMs).toISOString();
19
+ let stdout = "";
20
+ let stderr = "";
21
+ let rawOutput = "";
22
+ return new Promise((resolve) => {
23
+ const child = spawn(command.executable, command.args, {
24
+ shell: false,
25
+ windowsHide: true
26
+ });
27
+ const pid = child.pid ?? null;
28
+ let spawnError;
29
+ child.stdout.setEncoding("utf8");
30
+ child.stderr.setEncoding("utf8");
31
+ child.stdout.on("data", (chunk) => {
32
+ stdout += chunk;
33
+ rawOutput += chunk;
34
+ });
35
+ child.stderr.on("data", (chunk) => {
36
+ stderr += chunk;
37
+ rawOutput += chunk;
38
+ });
39
+ child.on("error", (error) => {
40
+ spawnError = error;
41
+ const errorMessage = stderr.length === 0 ? error.message : `\n${error.message}`;
42
+ stderr += errorMessage;
43
+ rawOutput += errorMessage;
44
+ });
45
+ child.on("close", (exitCode, signal) => {
46
+ const endMs = Date.now();
47
+ const resolvedExitCode = spawnError ? 127 : exitCode ?? 1;
48
+ resolve({
49
+ runId,
50
+ command,
51
+ stdout,
52
+ stderr,
53
+ rawOutput,
54
+ exitCode: resolvedExitCode,
55
+ signal,
56
+ pid,
57
+ startedAt,
58
+ endedAt: new Date(endMs).toISOString(),
59
+ durationMs: Math.max(0, endMs - startMs)
60
+ });
61
+ });
62
+ });
63
+ }
64
+ export async function persistLocalCommandRun(run, runsRoot = path.join(".compaction", "runs")) {
65
+ const outputDirectory = path.join(runsRoot, run.runId);
66
+ const stdoutPath = path.join(outputDirectory, "stdout.txt");
67
+ const stderrPath = path.join(outputDirectory, "stderr.txt");
68
+ const rawOutputPath = path.join(outputDirectory, "raw-output.txt");
69
+ const metadataPath = path.join(outputDirectory, "metadata.json");
70
+ await mkdir(outputDirectory, { recursive: true });
71
+ await writeFile(stdoutPath, run.stdout, "utf8");
72
+ await writeFile(stderrPath, run.stderr, "utf8");
73
+ await writeFile(rawOutputPath, run.rawOutput, "utf8");
74
+ await writeFile(metadataPath, `${JSON.stringify({
75
+ runId: run.runId,
76
+ command: run.command,
77
+ exitCode: run.exitCode,
78
+ signal: run.signal,
79
+ pid: run.pid,
80
+ startedAt: run.startedAt,
81
+ endedAt: run.endedAt,
82
+ durationMs: run.durationMs,
83
+ stdoutBytes: Buffer.byteLength(run.stdout, "utf8"),
84
+ stderrBytes: Buffer.byteLength(run.stderr, "utf8"),
85
+ rawOutputBytes: Buffer.byteLength(run.rawOutput, "utf8")
86
+ }, null, 2)}\n`, "utf8");
87
+ return {
88
+ run,
89
+ outputDirectory,
90
+ stdoutPath,
91
+ stderrPath,
92
+ rawOutputPath,
93
+ metadataPath
94
+ };
95
+ }
96
+ //# sourceMappingURL=command-runner.js.map
@@ -0,0 +1 @@
1
+ export declare function contentHash(content: string): string;
@@ -0,0 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ export function contentHash(content) {
3
+ return createHash("sha256").update(content).digest("hex");
4
+ }
5
+ //# sourceMappingURL=content-hash.js.map
@@ -0,0 +1,6 @@
1
+ import type { CostEstimate, TokenEstimate } from "./types.js";
2
+ export interface CacheTokenCounts {
3
+ cacheReadTokens?: number;
4
+ cacheCreationTokens?: number;
5
+ }
6
+ export declare function calculateCost(model: string, estimate: TokenEstimate, cache?: CacheTokenCounts): CostEstimate;
@@ -0,0 +1,20 @@
1
+ import { getCacheCreationPricePerMillion, getCacheReadPricePerMillion, getModelPricing } from "./pricing.js";
2
+ const TOKENS_PER_MILLION = 1_000_000;
3
+ export function calculateCost(model, estimate, cache) {
4
+ const pricing = getModelPricing(model);
5
+ const inputCostUsd = (estimate.inputTokens / TOKENS_PER_MILLION) * pricing.inputPerMillionUsd;
6
+ const outputCostUsd = (estimate.outputTokens / TOKENS_PER_MILLION) * pricing.outputPerMillionUsd;
7
+ const cacheReadTokens = cache?.cacheReadTokens ?? 0;
8
+ const cacheCreationTokens = cache?.cacheCreationTokens ?? 0;
9
+ const cacheReadCostUsd = (cacheReadTokens / TOKENS_PER_MILLION) * getCacheReadPricePerMillion(model);
10
+ const cacheCreationCostUsd = (cacheCreationTokens / TOKENS_PER_MILLION) * getCacheCreationPricePerMillion(model);
11
+ return {
12
+ model,
13
+ inputCostUsd,
14
+ outputCostUsd,
15
+ cacheReadCostUsd: cacheReadTokens > 0 ? cacheReadCostUsd : undefined,
16
+ cacheCreationCostUsd: cacheCreationTokens > 0 ? cacheCreationCostUsd : undefined,
17
+ totalCostUsd: inputCostUsd + outputCostUsd + cacheReadCostUsd + cacheCreationCostUsd
18
+ };
19
+ }
20
+ //# sourceMappingURL=cost-calculator.js.map