@compaction/cli 0.2.0 → 0.3.1

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 (153) hide show
  1. package/README.md +142 -64
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +9 -5
  10. package/dist/cli/commands/dev.d.ts +17 -0
  11. package/dist/cli/commands/dev.js +131 -0
  12. package/dist/cli/commands/gateway.d.ts +31 -0
  13. package/dist/cli/commands/gateway.js +312 -0
  14. package/dist/cli/commands/hooks.d.ts +5 -0
  15. package/dist/cli/commands/hooks.js +181 -0
  16. package/dist/cli/commands/init.js +422 -17
  17. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  18. package/dist/cli/commands/input-compaction-ab.js +125 -0
  19. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  20. package/dist/cli/commands/optimize-hosted.js +123 -0
  21. package/dist/cli/commands/optimize.js +8 -0
  22. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  23. package/dist/cli/commands/output-shaping-ab.js +132 -0
  24. package/dist/cli/commands/output-shaping.d.ts +7 -0
  25. package/dist/cli/commands/output-shaping.js +43 -0
  26. package/dist/cli/commands/policies.d.ts +2 -0
  27. package/dist/cli/commands/policies.js +80 -0
  28. package/dist/cli/commands/precall.d.ts +19 -0
  29. package/dist/cli/commands/precall.js +436 -0
  30. package/dist/cli/commands/recommend.js +1 -1
  31. package/dist/cli/commands/run.js +297 -12
  32. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  33. package/dist/cli/commands/upgrade-status.js +152 -0
  34. package/dist/cli/cursor-export-read.d.ts +13 -0
  35. package/dist/cli/cursor-export-read.js +53 -0
  36. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  37. package/dist/cli/cursor-live-preflight.js +46 -0
  38. package/dist/cli/index.js +36 -2
  39. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  40. package/dist/cli/onboarding/GatewayTui.js +113 -0
  41. package/dist/cli/onboarding/model.d.ts +32 -1
  42. package/dist/cli/onboarding/model.js +27 -2
  43. package/dist/cli/terminal-logo.d.ts +27 -0
  44. package/dist/cli/terminal-logo.js +49 -0
  45. package/dist/core/activity-event.d.ts +107 -0
  46. package/dist/core/activity-event.js +182 -0
  47. package/dist/core/activity-store.d.ts +63 -0
  48. package/dist/core/activity-store.js +254 -0
  49. package/dist/core/activity-view.d.ts +76 -0
  50. package/dist/core/activity-view.js +120 -0
  51. package/dist/core/api-client/index.d.ts +3 -1
  52. package/dist/core/api-client/index.js +4 -0
  53. package/dist/core/api-client/payload.d.ts +5 -1
  54. package/dist/core/api-client/payload.js +8 -6
  55. package/dist/core/api-client/persisted-config.d.ts +57 -0
  56. package/dist/core/api-client/persisted-config.js +156 -0
  57. package/dist/core/api-client/tool.d.ts +29 -0
  58. package/dist/core/api-client/tool.js +50 -0
  59. package/dist/core/api-client/types.d.ts +15 -4
  60. package/dist/core/auto-apply-ask.d.ts +58 -0
  61. package/dist/core/auto-apply-ask.js +105 -0
  62. package/dist/core/auto-apply-gates.d.ts +76 -0
  63. package/dist/core/auto-apply-gates.js +113 -0
  64. package/dist/core/before-call-activity.d.ts +35 -0
  65. package/dist/core/before-call-activity.js +103 -0
  66. package/dist/core/before-call-recovery.d.ts +19 -0
  67. package/dist/core/before-call-recovery.js +46 -0
  68. package/dist/core/before-call-stdin.d.ts +59 -0
  69. package/dist/core/before-call-stdin.js +78 -0
  70. package/dist/core/before-call.d.ts +149 -0
  71. package/dist/core/before-call.js +358 -0
  72. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  73. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  74. package/dist/core/capture-record.d.ts +40 -0
  75. package/dist/core/capture-record.js +55 -0
  76. package/dist/core/claude-code-before-call.d.ts +37 -0
  77. package/dist/core/claude-code-before-call.js +120 -0
  78. package/dist/core/claude-code-connect.d.ts +31 -0
  79. package/dist/core/claude-code-connect.js +87 -0
  80. package/dist/core/claude-code-hook-record.d.ts +71 -0
  81. package/dist/core/claude-code-hook-record.js +0 -0
  82. package/dist/core/claude-code-hooks.d.ts +77 -0
  83. package/dist/core/claude-code-hooks.js +141 -0
  84. package/dist/core/codex-capture.d.ts +45 -0
  85. package/dist/core/codex-capture.js +204 -0
  86. package/dist/core/command-runner.js +5 -1
  87. package/dist/core/compaction-artifacts.d.ts +51 -0
  88. package/dist/core/compaction-artifacts.js +242 -0
  89. package/dist/core/compactor.d.ts +9 -0
  90. package/dist/core/compactor.js +95 -0
  91. package/dist/core/cross-surface-event.d.ts +306 -0
  92. package/dist/core/cross-surface-event.js +330 -0
  93. package/dist/core/cursor-capture.d.ts +54 -0
  94. package/dist/core/cursor-capture.js +215 -0
  95. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  96. package/dist/core/cursor-preflight-probe.js +88 -0
  97. package/dist/core/cursor-preflight.d.ts +86 -0
  98. package/dist/core/cursor-preflight.js +126 -0
  99. package/dist/core/gateway/apply-activation.d.ts +39 -0
  100. package/dist/core/gateway/apply-activation.js +84 -0
  101. package/dist/core/gateway/apply-policy.d.ts +64 -0
  102. package/dist/core/gateway/apply-policy.js +221 -0
  103. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  104. package/dist/core/gateway/apply-receipt.js +75 -0
  105. package/dist/core/gateway/cache-proof.d.ts +45 -0
  106. package/dist/core/gateway/cache-proof.js +65 -0
  107. package/dist/core/gateway/configure.d.ts +50 -0
  108. package/dist/core/gateway/configure.js +169 -0
  109. package/dist/core/gateway/openai-usage.d.ts +56 -0
  110. package/dist/core/gateway/openai-usage.js +128 -0
  111. package/dist/core/gateway/receipt.d.ts +138 -0
  112. package/dist/core/gateway/receipt.js +120 -0
  113. package/dist/core/gateway/recovery.d.ts +23 -0
  114. package/dist/core/gateway/recovery.js +68 -0
  115. package/dist/core/gateway/server.d.ts +51 -0
  116. package/dist/core/gateway/server.js +276 -0
  117. package/dist/core/gateway/status.d.ts +45 -0
  118. package/dist/core/gateway/status.js +109 -0
  119. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  120. package/dist/core/hook-usage-aggregate.js +161 -0
  121. package/dist/core/input-compaction-ab.d.ts +111 -0
  122. package/dist/core/input-compaction-ab.js +158 -0
  123. package/dist/core/local-run-record.d.ts +109 -0
  124. package/dist/core/local-run-record.js +223 -0
  125. package/dist/core/output-shaping-ab.d.ts +140 -0
  126. package/dist/core/output-shaping-ab.js +146 -0
  127. package/dist/core/output-shaping-attach.d.ts +31 -0
  128. package/dist/core/output-shaping-attach.js +57 -0
  129. package/dist/core/output-shaping.d.ts +56 -0
  130. package/dist/core/output-shaping.js +89 -0
  131. package/dist/core/policy-middleware.d.ts +121 -0
  132. package/dist/core/policy-middleware.js +919 -0
  133. package/dist/core/policy-preferences.d.ts +99 -0
  134. package/dist/core/policy-preferences.js +232 -0
  135. package/dist/core/run-aggregator.d.ts +11 -1
  136. package/dist/core/run-aggregator.js +29 -2
  137. package/dist/core/run-flow-report.d.ts +82 -0
  138. package/dist/core/run-flow-report.js +71 -0
  139. package/dist/core/safety-report.js +8 -1
  140. package/dist/core/shim-capture-bridge.d.ts +32 -0
  141. package/dist/core/shim-capture-bridge.js +88 -0
  142. package/dist/core/skill-injection-policy.d.ts +72 -0
  143. package/dist/core/skill-injection-policy.js +183 -0
  144. package/dist/core/spend-attribution.js +1 -1
  145. package/dist/core/token-accounting.d.ts +1 -1
  146. package/dist/core/tool-shim.d.ts +129 -0
  147. package/dist/core/tool-shim.js +447 -0
  148. package/dist/core/trace-parser.d.ts +13 -13
  149. package/dist/core/trace-parser.js +6 -6
  150. package/dist/core/types.d.ts +12 -2
  151. package/dist/core/waste-detector.d.ts +20 -0
  152. package/dist/core/waste-detector.js +160 -6
  153. package/package.json +1 -1
@@ -0,0 +1,306 @@
1
+ /**
2
+ * ADDITIVE typed representation of the cross-surface evidence & reporting contract —
3
+ * `docs/ops/cross-surface-evidence-contract.md` (authority doc; founder decisions D1–D6, 2026-07-03).
4
+ *
5
+ * One event model, many surfaces: every Compaction surface reports the SAME kind of event with
6
+ * different honest evidence tiers. This module is the *contract/type level* of that model:
7
+ *
8
+ * - It is ADDITIVE. It does NOT change the existing `usage_event` wire shape
9
+ * (`src/core/capture-record.ts`), the local run record (`src/core/local-run-record.ts`), or any
10
+ * current writer/reader. No writer is REQUIRED to populate these fields (named follow-ups in
11
+ * the contract §7 stay follow-ups); the Cursor `run cursor` local run record is the first
12
+ * writer that does (D3 alignment, charter 2026-07-03-run-5), via
13
+ * `buildCursorRunCrossSurfaceEvent` below.
14
+ * - It makes the D5 HARD GUARDRAIL structural where TypeScript allows: a fixed-plan /
15
+ * plan-efficiency event can NEVER be billing-confirmed. `claim_scope:
16
+ * "billing-confirmed-workflow-scoped"` is only constructible on `surface: "api_metered"` with
17
+ * `cost_source: "provider-reported" | "operator-entered"` (contract §5 / D6), and a
18
+ * plan-efficiency payload structurally excludes it (`plan_efficiency?: never` on that arm;
19
+ * `billing_confirmed: false` is a literal type on the payload itself).
20
+ * - Where the type system cannot enforce a rule on parsed/unknown data, the REPORT-ONLY validator
21
+ * `validateCrossSurfaceEvent` catches it: it returns problems and never throws.
22
+ * - Honesty rails (contract §2): per-axis `token_source` with `unavailable_reason` required when
23
+ * unavailable; never a silent zero (an unavailable axis never carries a numeric count); input
24
+ * and output are independent axes; "generalized" is not a representable claim scope.
25
+ *
26
+ * NOTE on naming: contract `surface` values use underscores (`claude_code`, `browser_extension`)
27
+ * per the founder-verbatim field contract; the existing wire `ToolName` enum uses dashes
28
+ * (`claude-code`). `surfaceForToolName` maps between them without changing either.
29
+ */
30
+ import type { ToolName } from "./api-client/index.js";
31
+ import type { RunFlowTokenReport } from "./run-flow-report.js";
32
+ /** Contract §2: which Compaction surface produced the event (exact values, verbatim). */
33
+ export declare const CROSS_SURFACE_SURFACES: readonly ["cli", "claude_code", "codex", "cursor", "browser_extension", "api_metered"];
34
+ export type CrossSurfaceSurface = (typeof CROSS_SURFACE_SURFACES)[number];
35
+ /** Contract §2: whose model/service was used — distinct from `surface`. */
36
+ export declare const CROSS_SURFACE_PROVIDERS: readonly ["anthropic", "openai", "cursor", "chatgpt", "other"];
37
+ export type CrossSurfaceProvider = (typeof CROSS_SURFACE_PROVIDERS)[number];
38
+ /** Contract §2: per-axis token source. Input and output are INDEPENDENT axes. */
39
+ export declare const CROSS_SURFACE_TOKEN_SOURCES: readonly ["provider-reported", "local-estimate", "unavailable"];
40
+ export type CrossSurfaceTokenSource = (typeof CROSS_SURFACE_TOKEN_SOURCES)[number];
41
+ /** Contract §2: how the cost figure (if any) is known. `operator-entered` is the billing-delta operator path. */
42
+ export declare const CROSS_SURFACE_COST_SOURCES: readonly ["provider-reported", "operator-entered", "local-estimate", "unavailable"];
43
+ export type CrossSurfaceCostSource = (typeof CROSS_SURFACE_COST_SOURCES)[number];
44
+ /** Contract §5 (D6): the ONLY cost sources that can underwrite a billing-confirmed figure. */
45
+ export type BillingConfirmedCostSource = "provider-reported" | "operator-entered";
46
+ /**
47
+ * Contract §2 honesty rule: what a figure may be scoped to — this run / this workflow / these
48
+ * conditions. A GENERALIZED claim is intentionally NOT representable (D6: "Generalized
49
+ * billing-confirmed = future only. Public generalized 'saves X%' is forbidden.").
50
+ * `billing-confirmed-workflow-scoped` exists only on the `BillingConfirmedCrossSurfaceEvent` arm.
51
+ */
52
+ export declare const CROSS_SURFACE_CLAIM_SCOPES: readonly ["run-scoped", "workflow-scoped", "conditions-scoped", "billing-confirmed-workflow-scoped"];
53
+ export type CrossSurfaceClaimScope = (typeof CROSS_SURFACE_CLAIM_SCOPES)[number];
54
+ /** Every claim scope EXCEPT billing-confirmed — the only scopes a standard event may carry. */
55
+ export type NonBillingClaimScope = Exclude<CrossSurfaceClaimScope, "billing-confirmed-workflow-scoped">;
56
+ /** Contract §2: accepted/reverted — reverts are honest signal, never hidden. */
57
+ export type CrossSurfaceAcceptance = "accepted" | "reverted";
58
+ /** Contract §2: result of the applicable eval gate, or `not_evaluated` (never auto-certified). */
59
+ export type CrossSurfaceEvalStatus = "passed" | "failed" | "not_computed" | "not_evaluated";
60
+ /** How the underlying product is billed. `fixed-plan` marks quota/subscription billing (D5). */
61
+ export type CrossSurfaceBillingModel = "fixed-plan" | "metered" | "unknown";
62
+ /**
63
+ * One token axis: its honest source, plus the EXACT reason (and optional rerun guidance) when the
64
+ * axis is `unavailable`. Contract §2: "field-level unavailable WITH the exact reason".
65
+ */
66
+ export interface CrossSurfaceTokenAxis {
67
+ source: CrossSurfaceTokenSource;
68
+ /** REQUIRED (validator-enforced) when `source` is "unavailable" — the exact honest reason. */
69
+ unavailable_reason?: string;
70
+ }
71
+ /** Per-axis token sources. Neither axis ever inherits the other's tier (contract §2). */
72
+ export interface CrossSurfaceTokenSources {
73
+ input: CrossSurfaceTokenAxis;
74
+ output: CrossSurfaceTokenAxis;
75
+ }
76
+ /**
77
+ * Contract §4 (D5): plan-efficiency — a NEW evidence type for fixed-plan products
78
+ * (quota-extension). DEFINED only; ZERO evidence exists today and no claim may be made.
79
+ * `billing_confirmed` is the LITERAL `false`: a plan-efficiency figure is structurally incapable
80
+ * of being billing-confirmed, however strong the measurement.
81
+ */
82
+ export interface PlanEfficiencySignals {
83
+ evidence_type: "plan-efficiency";
84
+ /** HARD GUARDRAIL (D5): literal `false` — a fixed-plan figure can NEVER be billing-confirmed. */
85
+ billing_confirmed: false;
86
+ /** Identity of the quota window the signals were observed in (content-free). */
87
+ quota_window_id?: string;
88
+ /** More useful work per quota window (contract §4). */
89
+ tasks_completed_in_window?: number | null;
90
+ /** Fewer / later cap events (contract §4). */
91
+ cap_events_in_window?: number | null;
92
+ /** Fewer visible tokens per task — LOCAL-ESTIMATE per the tier table (contract §4). */
93
+ visible_tokens_per_task_estimate?: number | null;
94
+ /** More successful tasks before reset (contract §4). */
95
+ successful_tasks_before_reset?: number | null;
96
+ }
97
+ /**
98
+ * Fields shared by every event arm. ALL fields are optional/additive (contract §2: "a surface
99
+ * omits what it genuinely cannot observe, with a reason") — the discriminating fields live on the
100
+ * arms below. Counts may be `null` to mean "explicitly not counted" (never rendered as 0).
101
+ */
102
+ export interface CrossSurfaceEventCommon {
103
+ /** Existing user/team model (paid reporting aggregates by these). Free/local may omit (no sync). */
104
+ user_id?: string;
105
+ team_id?: string;
106
+ /** Whose model/service was used — e.g. surface `browser_extension` + provider `chatgpt`. */
107
+ provider?: CrossSurfaceProvider;
108
+ /** The model as honestly known — `unknown` stays `unknown`, never inferred. */
109
+ model_label?: string;
110
+ /** Identity for rollup and A/B comparability (billing-delta requires pinned workflow identity). */
111
+ workflow_id?: string;
112
+ session_id?: string;
113
+ run_id?: string;
114
+ /** Input tokens before/after Compaction acted, counted per `token_source.input`. */
115
+ input_before?: number | null;
116
+ input_after?: number | null;
117
+ /** Output tokens before/after (or estimate), counted per `token_source.output`. */
118
+ output_before?: number | null;
119
+ output_after?: number | null;
120
+ /** An estimate is an estimate and says so — it never upgrades the axis tier. */
121
+ output_estimate?: number | null;
122
+ /** Per-axis honest sources. A numeric count without a labeled axis is a validator problem. */
123
+ token_source?: CrossSurfaceTokenSources;
124
+ /** Which deterministic policy acted (content-free identifier). */
125
+ policy_used?: string;
126
+ /** Whether the user kept or reverted the optimization. */
127
+ acceptance?: CrossSurfaceAcceptance;
128
+ /** Result of the applicable recoverability eval gate, or `not_evaluated`. */
129
+ recoverability?: CrossSurfaceEvalStatus;
130
+ /** Result of the applicable output eval gate (short-but-sufficient), or `not_evaluated`. */
131
+ eval_status?: CrossSurfaceEvalStatus;
132
+ /** The exact honest label for this event's figures — never stronger than the sources support. */
133
+ evidence_level?: string;
134
+ /** Per-surface honesty caveats carried WITH the event, never stripped in rollup. */
135
+ caveats?: string[];
136
+ /** Exact reason when `cost_source` is "unavailable". */
137
+ cost_unavailable_reason?: string;
138
+ }
139
+ /**
140
+ * The standard event arm: any surface, any honest cost source, any NON-billing-confirmed claim
141
+ * scope. Fixed-plan surfaces and plan-efficiency events live here — and ONLY here.
142
+ */
143
+ export interface StandardCrossSurfaceEvent extends CrossSurfaceEventCommon {
144
+ surface: CrossSurfaceSurface;
145
+ billing_model?: CrossSurfaceBillingModel;
146
+ cost_source?: CrossSurfaceCostSource;
147
+ /** Structurally excludes "billing-confirmed-workflow-scoped" (D5/D6). */
148
+ claim_scope?: NonBillingClaimScope;
149
+ /** Plan-efficiency signals (fixed-plan quota-extension) — never on the billing-confirmed arm. */
150
+ plan_efficiency?: PlanEfficiencySignals;
151
+ }
152
+ /**
153
+ * The ONLY arm that can carry a billing-confirmed claim (contract §5 / D6): `api_metered`
154
+ * surface, provider-billed or operator-entered cost, workflow-scoped only. A plan-efficiency
155
+ * payload is structurally unrepresentable here (`never`), and `billing_model` can never be
156
+ * "fixed-plan".
157
+ */
158
+ export interface BillingConfirmedCrossSurfaceEvent extends CrossSurfaceEventCommon {
159
+ surface: "api_metered";
160
+ claim_scope: "billing-confirmed-workflow-scoped";
161
+ cost_source: BillingConfirmedCostSource;
162
+ billing_model?: "metered";
163
+ plan_efficiency?: never;
164
+ }
165
+ /**
166
+ * The cross-surface event. A discriminated union: constructing a fixed-plan / non-metered /
167
+ * plan-efficiency event with a billing-confirmed claim scope is a TYPE ERROR, not just a
168
+ * validator problem.
169
+ */
170
+ export type CrossSurfaceEvent = StandardCrossSurfaceEvent | BillingConfirmedCrossSurfaceEvent;
171
+ type Assert<T extends true> = T;
172
+ type BillingConfirmedArm = Extract<CrossSurfaceEvent, {
173
+ claim_scope: "billing-confirmed-workflow-scoped";
174
+ }>;
175
+ /** The billing-confirmed arm exists (so the guards below can never pass vacuously via `never`). */
176
+ export type StructuralGuard_BillingConfirmedArmExists = Assert<[BillingConfirmedArm] extends [never] ? false : true>;
177
+ /** A billing-confirmed claim can ONLY exist on the api_metered surface. */
178
+ export type StructuralGuard_BillingConfirmedRequiresMeteredSurface = Assert<BillingConfirmedArm["surface"] extends "api_metered" ? true : false>;
179
+ /** A billing-confirmed claim can ONLY carry provider-reported | operator-entered cost. */
180
+ export type StructuralGuard_BillingConfirmedCostSource = Assert<BillingConfirmedArm["cost_source"] extends BillingConfirmedCostSource ? true : false>;
181
+ /** A billing-confirmed event can NEVER carry a plan-efficiency payload. */
182
+ export type StructuralGuard_BillingConfirmedExcludesPlanEfficiency = Assert<Required<BillingConfirmedArm>["plan_efficiency"] extends never ? true : false>;
183
+ /** A plan-efficiency payload is structurally incapable of claiming billing_confirmed. */
184
+ export type StructuralGuard_PlanEfficiencyNeverBillingConfirmed = Assert<PlanEfficiencySignals["billing_confirmed"] extends false ? true : false>;
185
+ /** A billing-confirmed event can never be marked fixed-plan. */
186
+ export type StructuralGuard_BillingConfirmedNeverFixedPlan = Assert<Required<BillingConfirmedArm>["billing_model"] extends "metered" ? true : false>;
187
+ /**
188
+ * Map the EXISTING wire `ToolName` to the contract `surface` value, honestly:
189
+ * - `claude-code` → `claude_code`, `codex` → `codex`, `cursor` → `cursor`;
190
+ * - `other` (the bare `compaction run -- <cmd>` wrapper) → `cli` per the tier table ("bare
191
+ * run/trace analysis without provider usage");
192
+ * - `openai-agents` → `undefined`: the contract names no surface for it, and unknown stays
193
+ * unknown — this helper never guesses (named residual, like the control-plane enum alignment).
194
+ */
195
+ export declare function surfaceForToolName(tool: ToolName): CrossSurfaceSurface | undefined;
196
+ /**
197
+ * Contract tier table (§3, D3/D4): surfaces whose token tier can NEVER be provider-reported —
198
+ * Cursor emits no usage (vendor gap; "never provider-reported"), and the browser's visible
199
+ * surface exposes no provider usage ("may NOT claim provider-reported web tokens").
200
+ */
201
+ export declare const NEVER_PROVIDER_REPORTED_SURFACES: readonly CrossSurfaceSurface[];
202
+ export interface CrossSurfaceEventValidation {
203
+ /** Empty when the event honors the contract. Report-only: nothing throws, nothing exits. */
204
+ problems: string[];
205
+ }
206
+ /**
207
+ * Validate one parsed/unknown cross-surface event against the contract. REPORT-ONLY: returns the
208
+ * list of problems (empty = contract-honest) and NEVER throws — mirroring the repo's
209
+ * skipped-with-reason validator style. Rules enforced here are exactly the ones the type system
210
+ * cannot enforce on unknown data:
211
+ *
212
+ * 1. exact enums for surface / provider / token sources / cost source / claim scope;
213
+ * 2. D5 hard guardrail: a fixed-plan or plan-efficiency event is NEVER billing-confirmed;
214
+ * 3. D6: billing-confirmed requires surface api_metered + provider-reported|operator-entered cost;
215
+ * 4. tier table (D3/D4): cursor and browser_extension events never carry provider-reported tokens;
216
+ * 5. no silent zero: an unavailable axis never carries a numeric count;
217
+ * 6. per-axis unavailable_reason required when an axis is unavailable;
218
+ * 7. a numeric count requires its axis to be labeled (a count without a token_source is unlabeled).
219
+ */
220
+ export declare function validateCrossSurfaceEvent(value: unknown): CrossSurfaceEventValidation;
221
+ /**
222
+ * Per-surface constraints for the `run` local-run-record writers (contract tier table §3):
223
+ * - `codex` — `run codex`: provider `openai`; token axes are provider-reported where a
224
+ * `turn.completed.usage` block existed (the report says which). Codex reports token usage but
225
+ * never a cost/billing figure, so cost is UNAVAILABLE with that exact reason.
226
+ * - `cursor` — `run cursor`: provider `cursor`; LOCAL-ESTIMATE/unavailable only (D3 ceiling —
227
+ * Cursor emits no usage), and no cost data exists at all.
228
+ * - `cli` — bare `run -- <cmd>`: an arbitrary wrapped command, provider `other` (no provider is
229
+ * involved that Compaction can observe); input is a local chars/4 estimate, output is
230
+ * unavailable with the command-surface reason, and no billing surface exists.
231
+ * - `claude_code` — the `capture claude-code --from-hook` always-on path (charter 2026-07-04-run-8):
232
+ * provider `anthropic`; token axes are PROVIDER-REPORTED where the Claude Code session usage
233
+ * fields carried them (the report says which — a missing axis stays unavailable-with-reason,
234
+ * never a silent zero). Claude Code reports tokens but no cost/billing figure, so cost is
235
+ * UNAVAILABLE with that exact reason. This is NOT invented evidence: the Stop-hook capture reads
236
+ * the session's OWN provider usage metadata — the same provider-reported tokens `capture
237
+ * claude-code` already surfaces — it simply also records a metrics-only activity event so a
238
+ * subsequent session appears in `compaction activity` with no manual import.
239
+ */
240
+ export declare const RUN_RECORD_EVENT_SURFACES: {
241
+ readonly codex: {
242
+ readonly surface: "codex";
243
+ readonly provider: "openai";
244
+ readonly cost_unavailable_reason: "codex exec --json reports token usage (turn.completed.usage) but no cost or billing figure; no cost data exists on this path";
245
+ };
246
+ readonly claude_code: {
247
+ readonly surface: "claude_code";
248
+ readonly provider: "anthropic";
249
+ readonly cost_unavailable_reason: "Claude Code session usage reports provider tokens (input/output) but no cost or billing figure; no cost data exists on this path";
250
+ };
251
+ readonly cursor: {
252
+ readonly surface: "cursor";
253
+ readonly provider: "cursor";
254
+ readonly cost_unavailable_reason: "Cursor emits no usage or cost data; no cost figure exists for this run";
255
+ };
256
+ readonly cli: {
257
+ readonly surface: "cli";
258
+ readonly provider: "other";
259
+ readonly cost_unavailable_reason: "a bare wrapped command has no provider usage or billing surface; no cost figure exists for this run";
260
+ };
261
+ };
262
+ export type RunRecordEventSurface = keyof typeof RUN_RECORD_EVENT_SURFACES;
263
+ export interface RunCrossSurfaceEventParams {
264
+ runId: string;
265
+ tokenReport: RunFlowTokenReport;
266
+ /** TRUE per-axis unavailability reasons from the capture (required content for unavailable axes). */
267
+ reasons?: {
268
+ input?: string;
269
+ output?: string;
270
+ };
271
+ /** The model as the capture HONESTLY knows it. Omitted → "unknown" (unknown stays unknown, never inferred). */
272
+ modelLabel?: string;
273
+ }
274
+ /**
275
+ * Build the honest cross-surface event for one `run` local run record (any of the three `run`
276
+ * surfaces above):
277
+ * - `surface`/`provider` fixed per surface; `model_label` only as honestly known, else `"unknown"`;
278
+ * - per-axis `token_source` copied VERBATIM from the token report (input and output INDEPENDENT),
279
+ * with the capture's TRUE per-run reason on any unavailable axis (never a bare "unavailable");
280
+ * - counts only on available axes, under the count field that matches the axis tier:
281
+ * `input_before` for the counted/estimated input; a provider-reported output is a COUNT
282
+ * (`output_before`), a local-estimate output is an ESTIMATE and says so (`output_estimate`) —
283
+ * an unavailable axis carries NO count (never a silent zero);
284
+ * - `cost_source: "unavailable"` with the exact per-surface reason (no `run` path has billing data);
285
+ * - `claim_scope: "run-scoped"` (never generalized); caveats = the report's honest notes.
286
+ *
287
+ * The tier ceilings stay intact: this builder never SETS provider-reported (it only copies the
288
+ * report's label), and if a never-provider-reported surface (Cursor) is handed one it passes it
289
+ * through unchanged so the report-only validator rejects it (rule 4) instead of hiding it.
290
+ */
291
+ export declare function buildRunCrossSurfaceEvent(surfaceKey: RunRecordEventSurface, params: RunCrossSurfaceEventParams): StandardCrossSurfaceEvent;
292
+ /**
293
+ * The Cursor writer (D3 alignment, #583) — kept as the named entry point its call/test sites use;
294
+ * now a thin delegate to the generalized builder with the `cursor` constraints (`surface:
295
+ * "cursor"`, `provider: "cursor"`, `model_label: "unknown"` — never inferred, D3 ceiling intact).
296
+ */
297
+ export declare function buildCursorRunCrossSurfaceEvent(params: {
298
+ runId: string;
299
+ tokenReport: RunFlowTokenReport;
300
+ /** TRUE per-axis unavailability reasons from the capture (required content for unavailable axes). */
301
+ reasons?: {
302
+ input?: string;
303
+ output?: string;
304
+ };
305
+ }): StandardCrossSurfaceEvent;
306
+ export {};
@@ -0,0 +1,330 @@
1
+ /** Contract §2: which Compaction surface produced the event (exact values, verbatim). */
2
+ export const CROSS_SURFACE_SURFACES = [
3
+ "cli",
4
+ "claude_code",
5
+ "codex",
6
+ "cursor",
7
+ "browser_extension",
8
+ "api_metered"
9
+ ];
10
+ /** Contract §2: whose model/service was used — distinct from `surface`. */
11
+ export const CROSS_SURFACE_PROVIDERS = ["anthropic", "openai", "cursor", "chatgpt", "other"];
12
+ /** Contract §2: per-axis token source. Input and output are INDEPENDENT axes. */
13
+ export const CROSS_SURFACE_TOKEN_SOURCES = ["provider-reported", "local-estimate", "unavailable"];
14
+ /** Contract §2: how the cost figure (if any) is known. `operator-entered` is the billing-delta operator path. */
15
+ export const CROSS_SURFACE_COST_SOURCES = [
16
+ "provider-reported",
17
+ "operator-entered",
18
+ "local-estimate",
19
+ "unavailable"
20
+ ];
21
+ /**
22
+ * Contract §2 honesty rule: what a figure may be scoped to — this run / this workflow / these
23
+ * conditions. A GENERALIZED claim is intentionally NOT representable (D6: "Generalized
24
+ * billing-confirmed = future only. Public generalized 'saves X%' is forbidden.").
25
+ * `billing-confirmed-workflow-scoped` exists only on the `BillingConfirmedCrossSurfaceEvent` arm.
26
+ */
27
+ export const CROSS_SURFACE_CLAIM_SCOPES = [
28
+ "run-scoped",
29
+ "workflow-scoped",
30
+ "conditions-scoped",
31
+ "billing-confirmed-workflow-scoped"
32
+ ];
33
+ /* ------------------------------------------------------------------------------------------------
34
+ * Surfaces ↔ existing wire ToolName mapping (additive helper; changes neither enum).
35
+ * ---------------------------------------------------------------------------------------------- */
36
+ /**
37
+ * Map the EXISTING wire `ToolName` to the contract `surface` value, honestly:
38
+ * - `claude-code` → `claude_code`, `codex` → `codex`, `cursor` → `cursor`;
39
+ * - `other` (the bare `compaction run -- <cmd>` wrapper) → `cli` per the tier table ("bare
40
+ * run/trace analysis without provider usage");
41
+ * - `openai-agents` → `undefined`: the contract names no surface for it, and unknown stays
42
+ * unknown — this helper never guesses (named residual, like the control-plane enum alignment).
43
+ */
44
+ export function surfaceForToolName(tool) {
45
+ switch (tool) {
46
+ case "claude-code":
47
+ return "claude_code";
48
+ case "codex":
49
+ return "codex";
50
+ case "cursor":
51
+ return "cursor";
52
+ case "other":
53
+ return "cli";
54
+ case "openai-agents":
55
+ return undefined;
56
+ }
57
+ }
58
+ /* ------------------------------------------------------------------------------------------------
59
+ * REPORT-ONLY validator. Returns problems; never throws. It exists for the states TypeScript
60
+ * cannot police (parsed JSON, widened objects, wire data) and encodes the contract's honesty
61
+ * rules as label-correctness checks.
62
+ * ---------------------------------------------------------------------------------------------- */
63
+ /**
64
+ * Contract tier table (§3, D3/D4): surfaces whose token tier can NEVER be provider-reported —
65
+ * Cursor emits no usage (vendor gap; "never provider-reported"), and the browser's visible
66
+ * surface exposes no provider usage ("may NOT claim provider-reported web tokens").
67
+ */
68
+ export const NEVER_PROVIDER_REPORTED_SURFACES = ["cursor", "browser_extension"];
69
+ function isPlainObject(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+ function isOneOf(value, allowed) {
73
+ return typeof value === "string" && allowed.includes(value);
74
+ }
75
+ function isNonEmptyString(value) {
76
+ return typeof value === "string" && value.trim() !== "";
77
+ }
78
+ /** A count field may be absent, `null` (explicitly not counted), or a finite number ≥ 0. */
79
+ function countProblem(field, value) {
80
+ if (value === undefined || value === null)
81
+ return undefined;
82
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0)
83
+ return undefined;
84
+ return `${field}: must be a finite number >= 0, null, or absent`;
85
+ }
86
+ const INPUT_COUNT_FIELDS = ["input_before", "input_after"];
87
+ const OUTPUT_COUNT_FIELDS = ["output_before", "output_after", "output_estimate"];
88
+ function validateAxis(axisName, axis, event, problems) {
89
+ const countFields = axisName === "input" ? INPUT_COUNT_FIELDS : OUTPUT_COUNT_FIELDS;
90
+ if (!isPlainObject(axis)) {
91
+ problems.push(`token_source.${axisName}: must be an object with a per-axis source`);
92
+ return;
93
+ }
94
+ if (!isOneOf(axis.source, CROSS_SURFACE_TOKEN_SOURCES)) {
95
+ problems.push(`token_source.${axisName}.source: must be one of ${CROSS_SURFACE_TOKEN_SOURCES.join(" | ")} (exact)`);
96
+ return;
97
+ }
98
+ if (axis.source === "unavailable") {
99
+ // Contract §2: field-level unavailable WITH the exact reason — required per axis.
100
+ if (!isNonEmptyString(axis.unavailable_reason)) {
101
+ problems.push(`token_source.${axisName}.unavailable_reason: required when the ${axisName} axis is unavailable (the exact honest reason)`);
102
+ }
103
+ // Contract §2: never a silent zero — an unavailable axis never carries a numeric count
104
+ // (a 0 here would dress a missing count as a real one).
105
+ for (const field of countFields) {
106
+ if (typeof event[field] === "number") {
107
+ problems.push(`${field}: no silent zero — the ${axisName} axis is unavailable, so a numeric count (including 0) must not be present`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Validate one parsed/unknown cross-surface event against the contract. REPORT-ONLY: returns the
114
+ * list of problems (empty = contract-honest) and NEVER throws — mirroring the repo's
115
+ * skipped-with-reason validator style. Rules enforced here are exactly the ones the type system
116
+ * cannot enforce on unknown data:
117
+ *
118
+ * 1. exact enums for surface / provider / token sources / cost source / claim scope;
119
+ * 2. D5 hard guardrail: a fixed-plan or plan-efficiency event is NEVER billing-confirmed;
120
+ * 3. D6: billing-confirmed requires surface api_metered + provider-reported|operator-entered cost;
121
+ * 4. tier table (D3/D4): cursor and browser_extension events never carry provider-reported tokens;
122
+ * 5. no silent zero: an unavailable axis never carries a numeric count;
123
+ * 6. per-axis unavailable_reason required when an axis is unavailable;
124
+ * 7. a numeric count requires its axis to be labeled (a count without a token_source is unlabeled).
125
+ */
126
+ export function validateCrossSurfaceEvent(value) {
127
+ const problems = [];
128
+ if (!isPlainObject(value)) {
129
+ return { problems: ["event: must be a JSON object"] };
130
+ }
131
+ const event = value;
132
+ // Rule 1 — exact enums.
133
+ if (!isOneOf(event.surface, CROSS_SURFACE_SURFACES)) {
134
+ problems.push(`surface: must be one of ${CROSS_SURFACE_SURFACES.join(" | ")} (exact)`);
135
+ }
136
+ if (event.provider !== undefined && !isOneOf(event.provider, CROSS_SURFACE_PROVIDERS)) {
137
+ problems.push(`provider: must be one of ${CROSS_SURFACE_PROVIDERS.join(" | ")} (exact)`);
138
+ }
139
+ if (event.cost_source !== undefined && !isOneOf(event.cost_source, CROSS_SURFACE_COST_SOURCES)) {
140
+ problems.push(`cost_source: must be one of ${CROSS_SURFACE_COST_SOURCES.join(" | ")} (exact)`);
141
+ }
142
+ if (event.claim_scope !== undefined && !isOneOf(event.claim_scope, CROSS_SURFACE_CLAIM_SCOPES)) {
143
+ problems.push(`claim_scope: must be one of ${CROSS_SURFACE_CLAIM_SCOPES.join(" | ")} (exact; generalized claims are not representable)`);
144
+ }
145
+ if (event.cost_source === "unavailable" && !isNonEmptyString(event.cost_unavailable_reason)) {
146
+ problems.push("cost_unavailable_reason: required when cost_source is unavailable (the exact honest reason)");
147
+ }
148
+ // Count fields are well-formed (absent | null | finite >= 0).
149
+ for (const field of [...INPUT_COUNT_FIELDS, ...OUTPUT_COUNT_FIELDS]) {
150
+ const problem = countProblem(field, event[field]);
151
+ if (problem !== undefined)
152
+ problems.push(problem);
153
+ }
154
+ // Rules 5–6 — per-axis honesty; rule 7 — counts require a labeled axis.
155
+ const tokenSource = event.token_source;
156
+ if (tokenSource !== undefined) {
157
+ if (!isPlainObject(tokenSource)) {
158
+ problems.push("token_source: must be an object with input and output axes");
159
+ }
160
+ else {
161
+ validateAxis("input", tokenSource.input, event, problems);
162
+ validateAxis("output", tokenSource.output, event, problems);
163
+ }
164
+ }
165
+ else {
166
+ for (const field of [...INPUT_COUNT_FIELDS, ...OUTPUT_COUNT_FIELDS]) {
167
+ if (typeof event[field] === "number") {
168
+ problems.push(`${field}: a numeric count requires token_source for its axis — an unlabeled count is not honest`);
169
+ }
170
+ }
171
+ }
172
+ // Rule 4 — tier ceilings (D3/D4): cursor + browser events never carry provider-reported tokens.
173
+ if (isOneOf(event.surface, NEVER_PROVIDER_REPORTED_SURFACES) &&
174
+ isPlainObject(tokenSource)) {
175
+ for (const axisName of ["input", "output"]) {
176
+ const axis = tokenSource[axisName];
177
+ if (isPlainObject(axis) && axis.source === "provider-reported") {
178
+ problems.push(`token_source.${axisName}.source: surface "${String(event.surface)}" can never be provider-reported (contract tier table — vendor exposes no usage)`);
179
+ }
180
+ }
181
+ }
182
+ // Rules 2–3 — the D5/D6 guardrail on unknown data.
183
+ const planEfficiency = event.plan_efficiency;
184
+ if (planEfficiency !== undefined) {
185
+ if (!isPlainObject(planEfficiency)) {
186
+ problems.push("plan_efficiency: must be an object");
187
+ }
188
+ else {
189
+ if (planEfficiency.evidence_type !== "plan-efficiency") {
190
+ problems.push('plan_efficiency.evidence_type: must be the literal "plan-efficiency"');
191
+ }
192
+ if (planEfficiency.billing_confirmed !== false) {
193
+ problems.push("plan_efficiency.billing_confirmed: must be the literal false — a fixed-plan figure can NEVER be billing-confirmed (contract §4, D5)");
194
+ }
195
+ }
196
+ }
197
+ const claimsBillingConfirmed = event.claim_scope === "billing-confirmed-workflow-scoped";
198
+ const isFixedPlanEvent = event.billing_model === "fixed-plan" ||
199
+ planEfficiency !== undefined ||
200
+ isOneOf(event.surface, NEVER_PROVIDER_REPORTED_SURFACES);
201
+ if (claimsBillingConfirmed) {
202
+ if (event.surface !== "api_metered") {
203
+ problems.push(`claim_scope: billing-confirmed is only possible on surface "api_metered" (contract §5, D6) — surface "${String(event.surface)}" can never be billing-confirmed`);
204
+ }
205
+ if (event.cost_source !== "provider-reported" && event.cost_source !== "operator-entered") {
206
+ problems.push("cost_source: a billing-confirmed claim requires provider-reported or operator-entered cost (contract §5, D6) — never a price-table/local estimate relabeled");
207
+ }
208
+ if (event.billing_model === "fixed-plan" || planEfficiency !== undefined) {
209
+ problems.push("claim_scope: a fixed-plan / plan-efficiency event can NEVER be billing-confirmed (contract §4, D5 — hard guardrail, no exceptions)");
210
+ }
211
+ }
212
+ // Defensive label rule: an evidence label that SAYS billing-confirmed on a fixed-plan event is
213
+ // the exact overclaim D5 forbids, whatever the claim_scope field says.
214
+ if (typeof event.evidence_level === "string" &&
215
+ event.evidence_level.toLowerCase().includes("billing-confirmed") &&
216
+ isFixedPlanEvent) {
217
+ problems.push("evidence_level: mentions billing-confirmed on a fixed-plan / plan-efficiency / never-provider-reported event — labels must make this impossible (contract §4, D5)");
218
+ }
219
+ return { problems };
220
+ }
221
+ /* ------------------------------------------------------------------------------------------------
222
+ * Run-record event builders — the writers of the additive `cross_surface_event` field on `run`
223
+ * local run records. Cursor shipped first (D3 alignment, charter 2026-07-03-run-5, #583); the
224
+ * codex + cli writers complete the `run` surfaces (#582 residual #5, same charter). Every writer
225
+ * populates the event from the SAME `RunFlowTokenReport` the record already embeds, so the two can
226
+ * never diverge — and per-axis sources are copied VERBATIM (the builder never launders labels).
227
+ * ---------------------------------------------------------------------------------------------- */
228
+ /** Generic honest fallback when an unavailable axis arrives without its TRUE per-run reason. */
229
+ const AXIS_UNAVAILABLE_FALLBACK = "not safely separable / not reported";
230
+ /** One axis derived from the token report. NEVER relabels: if an upstream source ever wrongly said
231
+ * provider-reported for a never-provider-reported surface (e.g. Cursor), this passes it through
232
+ * and `validateCrossSurfaceEvent` flags it (rule 4) — the builder does not launder labels. */
233
+ function axisFromReport(source, reason) {
234
+ if (source === "unavailable") {
235
+ return { source, unavailable_reason: reason ?? AXIS_UNAVAILABLE_FALLBACK };
236
+ }
237
+ return { source };
238
+ }
239
+ /**
240
+ * Per-surface constraints for the `run` local-run-record writers (contract tier table §3):
241
+ * - `codex` — `run codex`: provider `openai`; token axes are provider-reported where a
242
+ * `turn.completed.usage` block existed (the report says which). Codex reports token usage but
243
+ * never a cost/billing figure, so cost is UNAVAILABLE with that exact reason.
244
+ * - `cursor` — `run cursor`: provider `cursor`; LOCAL-ESTIMATE/unavailable only (D3 ceiling —
245
+ * Cursor emits no usage), and no cost data exists at all.
246
+ * - `cli` — bare `run -- <cmd>`: an arbitrary wrapped command, provider `other` (no provider is
247
+ * involved that Compaction can observe); input is a local chars/4 estimate, output is
248
+ * unavailable with the command-surface reason, and no billing surface exists.
249
+ * - `claude_code` — the `capture claude-code --from-hook` always-on path (charter 2026-07-04-run-8):
250
+ * provider `anthropic`; token axes are PROVIDER-REPORTED where the Claude Code session usage
251
+ * fields carried them (the report says which — a missing axis stays unavailable-with-reason,
252
+ * never a silent zero). Claude Code reports tokens but no cost/billing figure, so cost is
253
+ * UNAVAILABLE with that exact reason. This is NOT invented evidence: the Stop-hook capture reads
254
+ * the session's OWN provider usage metadata — the same provider-reported tokens `capture
255
+ * claude-code` already surfaces — it simply also records a metrics-only activity event so a
256
+ * subsequent session appears in `compaction activity` with no manual import.
257
+ */
258
+ export const RUN_RECORD_EVENT_SURFACES = {
259
+ codex: {
260
+ surface: "codex",
261
+ provider: "openai",
262
+ cost_unavailable_reason: "codex exec --json reports token usage (turn.completed.usage) but no cost or billing figure; no cost data exists on this path"
263
+ },
264
+ claude_code: {
265
+ surface: "claude_code",
266
+ provider: "anthropic",
267
+ cost_unavailable_reason: "Claude Code session usage reports provider tokens (input/output) but no cost or billing figure; no cost data exists on this path"
268
+ },
269
+ cursor: {
270
+ surface: "cursor",
271
+ provider: "cursor",
272
+ cost_unavailable_reason: "Cursor emits no usage or cost data; no cost figure exists for this run"
273
+ },
274
+ cli: {
275
+ surface: "cli",
276
+ provider: "other",
277
+ cost_unavailable_reason: "a bare wrapped command has no provider usage or billing surface; no cost figure exists for this run"
278
+ }
279
+ };
280
+ /**
281
+ * Build the honest cross-surface event for one `run` local run record (any of the three `run`
282
+ * surfaces above):
283
+ * - `surface`/`provider` fixed per surface; `model_label` only as honestly known, else `"unknown"`;
284
+ * - per-axis `token_source` copied VERBATIM from the token report (input and output INDEPENDENT),
285
+ * with the capture's TRUE per-run reason on any unavailable axis (never a bare "unavailable");
286
+ * - counts only on available axes, under the count field that matches the axis tier:
287
+ * `input_before` for the counted/estimated input; a provider-reported output is a COUNT
288
+ * (`output_before`), a local-estimate output is an ESTIMATE and says so (`output_estimate`) —
289
+ * an unavailable axis carries NO count (never a silent zero);
290
+ * - `cost_source: "unavailable"` with the exact per-surface reason (no `run` path has billing data);
291
+ * - `claim_scope: "run-scoped"` (never generalized); caveats = the report's honest notes.
292
+ *
293
+ * The tier ceilings stay intact: this builder never SETS provider-reported (it only copies the
294
+ * report's label), and if a never-provider-reported surface (Cursor) is handed one it passes it
295
+ * through unchanged so the report-only validator rejects it (rule 4) instead of hiding it.
296
+ */
297
+ export function buildRunCrossSurfaceEvent(surfaceKey, params) {
298
+ const spec = RUN_RECORD_EVENT_SURFACES[surfaceKey];
299
+ const report = params.tokenReport;
300
+ const input = axisFromReport(report.input_token_source, params.reasons?.input);
301
+ const output = axisFromReport(report.output_token_source, params.reasons?.output);
302
+ return {
303
+ surface: spec.surface,
304
+ provider: spec.provider,
305
+ model_label: params.modelLabel ?? "unknown",
306
+ run_id: params.runId,
307
+ token_source: { input, output },
308
+ // Counts ride ONLY on an available axis (never a silent zero on an unavailable one).
309
+ ...(input.source !== "unavailable" && report.input_tokens !== undefined ? { input_before: report.input_tokens } : {}),
310
+ ...(output.source === "provider-reported" && report.output_tokens !== undefined
311
+ ? { output_before: report.output_tokens }
312
+ : {}),
313
+ ...(output.source === "local-estimate" && report.output_tokens !== undefined
314
+ ? { output_estimate: report.output_tokens }
315
+ : {}),
316
+ cost_source: "unavailable",
317
+ cost_unavailable_reason: spec.cost_unavailable_reason,
318
+ claim_scope: "run-scoped",
319
+ caveats: [...report.notes]
320
+ };
321
+ }
322
+ /**
323
+ * The Cursor writer (D3 alignment, #583) — kept as the named entry point its call/test sites use;
324
+ * now a thin delegate to the generalized builder with the `cursor` constraints (`surface:
325
+ * "cursor"`, `provider: "cursor"`, `model_label: "unknown"` — never inferred, D3 ceiling intact).
326
+ */
327
+ export function buildCursorRunCrossSurfaceEvent(params) {
328
+ return buildRunCrossSurfaceEvent("cursor", params);
329
+ }
330
+ //# sourceMappingURL=cross-surface-event.js.map