@agent-finops/core 0.5.8 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { usageRecordSchema } from "./schema.js";
4
+ import { downgradeSampleUsageEvidence, parseUsageRecord } from "./schema.js";
5
5
  // One level up from dist/ = the package root, where samples/ ships (see
6
6
  // "files" in package.json). Must survive npm installation — never resolve
7
7
  // relative to the repo.
@@ -12,7 +12,8 @@ export const sampleFiles = [
12
12
  ];
13
13
  export async function loadSampleUsageData(rootDir = packageRoot) {
14
14
  const records = await Promise.all(sampleFiles.map((file) => loadUsageCsv(resolve(rootDir, file))));
15
- return records.flat().sort((left, right) => left.timestamp.localeCompare(right.timestamp));
15
+ return downgradeSampleUsageEvidence(records.flat())
16
+ .sort((left, right) => left.timestamp.localeCompare(right.timestamp));
16
17
  }
17
18
  async function loadUsageCsv(path) {
18
19
  const contents = await readFile(path, "utf8");
@@ -28,7 +29,7 @@ export function parseUsageCsv(contents) {
28
29
  return recordLines.map((line) => {
29
30
  const values = line.split(",");
30
31
  const row = Object.fromEntries(headers.map((header, index) => [header, values[index]?.trim() ?? ""]));
31
- return usageRecordSchema.parse({
32
+ return parseUsageRecord({
32
33
  id: row.id,
33
34
  timestamp: row.timestamp,
34
35
  source: {
@@ -49,11 +50,38 @@ export function parseUsageCsv(contents) {
49
50
  userId: optionalValue(row.user_id),
50
51
  workspaceId: optionalValue(row.workspace_id),
51
52
  apiKeyId: optionalValue(row.api_key_id),
52
- operation: optionalValue(row.operation)
53
+ providerCostType: optionalValue(row.provider_cost_type),
54
+ operation: optionalValue(row.operation),
55
+ usageGranularity: optionalValue(row.usage_granularity),
56
+ workloadSemantics: workloadSemantics(row)
53
57
  });
54
58
  });
55
59
  }
56
60
  function optionalValue(value) {
57
61
  return value === "" ? undefined : value;
58
62
  }
63
+ function workloadSemantics(row) {
64
+ const stableInputFingerprint = optionalValue(row.stable_input_fingerprint);
65
+ const batchEligible = optionalBoolean(row.batch_eligible);
66
+ const downgradeSafe = optionalBoolean(row.downgrade_safe);
67
+ if (stableInputFingerprint === undefined &&
68
+ batchEligible === undefined &&
69
+ downgradeSafe === undefined) {
70
+ return undefined;
71
+ }
72
+ return {
73
+ ...(stableInputFingerprint ? { stableInputFingerprint } : {}),
74
+ ...(batchEligible !== undefined ? { batchEligible } : {}),
75
+ ...(downgradeSafe !== undefined ? { downgradeSafe } : {})
76
+ };
77
+ }
78
+ function optionalBoolean(value) {
79
+ if (value === undefined || value === "")
80
+ return undefined;
81
+ if (value === "true")
82
+ return true;
83
+ if (value === "false")
84
+ return false;
85
+ throw new Error(`Expected true/false CSV value, received ${JSON.stringify(value)}.`);
86
+ }
59
87
  //# sourceMappingURL=sampleData.js.map
package/dist/schema.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export declare const costConfidenceValues: readonly ["verified", "estimated", "detected_unverified", "missing"];
3
3
  export declare const costConfidenceSchema: z.ZodEnum<{
4
- verified: "verified";
5
4
  estimated: "estimated";
5
+ verified: "verified";
6
6
  detected_unverified: "detected_unverified";
7
7
  missing: "missing";
8
8
  }>;
@@ -12,14 +12,45 @@ export declare const spendSourceSchema: z.ZodObject<{
12
12
  name: z.ZodString;
13
13
  provider: z.ZodString;
14
14
  confidence: z.ZodEnum<{
15
- verified: "verified";
16
15
  estimated: "estimated";
16
+ verified: "verified";
17
17
  detected_unverified: "detected_unverified";
18
18
  missing: "missing";
19
19
  }>;
20
20
  observedFrom: z.ZodString;
21
21
  }, z.core.$strip>;
22
22
  export type SpendSource = z.infer<typeof spendSourceSchema>;
23
+ /**
24
+ * What one normalized usage record represents.
25
+ *
26
+ * Only `call` and `invocation` are precise enough to support per-workload
27
+ * counterfactuals such as model routing, result caching, Batch API moves, or
28
+ * prompt trimming. The remaining values are still useful financial evidence,
29
+ * but must never be silently treated as individual calls.
30
+ */
31
+ export declare const usageGranularityValues: readonly ["call", "invocation", "session", "daily_aggregate", "usage_bucket", "billing_bucket", "seat", "user_aggregate"];
32
+ export declare const usageGranularitySchema: z.ZodEnum<{
33
+ call: "call";
34
+ invocation: "invocation";
35
+ session: "session";
36
+ daily_aggregate: "daily_aggregate";
37
+ usage_bucket: "usage_bucket";
38
+ billing_bucket: "billing_bucket";
39
+ seat: "seat";
40
+ user_aggregate: "user_aggregate";
41
+ }>;
42
+ export type UsageGranularity = z.infer<typeof usageGranularitySchema>;
43
+ /**
44
+ * Explicit adapter attestations for workload-specific optimization advice.
45
+ * These fields are intentionally absent by default: an operation label alone
46
+ * does not prove identical inputs, latency tolerance, or downgrade safety.
47
+ */
48
+ export declare const workloadSemanticsSchema: z.ZodObject<{
49
+ stableInputFingerprint: z.ZodOptional<z.ZodString>;
50
+ batchEligible: z.ZodOptional<z.ZodBoolean>;
51
+ downgradeSafe: z.ZodOptional<z.ZodBoolean>;
52
+ }, z.core.$strict>;
53
+ export type WorkloadSemantics = z.infer<typeof workloadSemanticsSchema>;
23
54
  export declare const usageRecordSchema: z.ZodObject<{
24
55
  id: z.ZodString;
25
56
  timestamp: z.ZodString;
@@ -28,8 +59,8 @@ export declare const usageRecordSchema: z.ZodObject<{
28
59
  name: z.ZodString;
29
60
  provider: z.ZodString;
30
61
  confidence: z.ZodEnum<{
31
- verified: "verified";
32
62
  estimated: "estimated";
63
+ verified: "verified";
33
64
  detected_unverified: "detected_unverified";
34
65
  missing: "missing";
35
66
  }>;
@@ -40,8 +71,8 @@ export declare const usageRecordSchema: z.ZodObject<{
40
71
  outputTokens: z.ZodNumber;
41
72
  amountUsd: z.ZodNullable<z.ZodNumber>;
42
73
  costConfidence: z.ZodEnum<{
43
- verified: "verified";
44
74
  estimated: "estimated";
75
+ verified: "verified";
45
76
  detected_unverified: "detected_unverified";
46
77
  missing: "missing";
47
78
  }>;
@@ -54,14 +85,59 @@ export declare const usageRecordSchema: z.ZodObject<{
54
85
  quantity: z.ZodOptional<z.ZodNumber>;
55
86
  agentId: z.ZodOptional<z.ZodString>;
56
87
  operation: z.ZodOptional<z.ZodString>;
88
+ usageGranularity: z.ZodOptional<z.ZodEnum<{
89
+ call: "call";
90
+ invocation: "invocation";
91
+ session: "session";
92
+ daily_aggregate: "daily_aggregate";
93
+ usage_bucket: "usage_bucket";
94
+ billing_bucket: "billing_bucket";
95
+ seat: "seat";
96
+ user_aggregate: "user_aggregate";
97
+ }>>;
98
+ workloadSemantics: z.ZodOptional<z.ZodObject<{
99
+ stableInputFingerprint: z.ZodOptional<z.ZodString>;
100
+ batchEligible: z.ZodOptional<z.ZodBoolean>;
101
+ downgradeSafe: z.ZodOptional<z.ZodBoolean>;
102
+ }, z.core.$strict>>;
57
103
  }, z.core.$strip>;
58
104
  export type UsageRecord = z.infer<typeof usageRecordSchema>;
105
+ /**
106
+ * True only when a record can honestly ground a modeled workload change.
107
+ *
108
+ * A provider billing row, usage bucket, seat, user total, or unlabelled legacy
109
+ * row may describe real spend, but it does not prove that one row was one
110
+ * optimizable call. Requiring both explicit call/invocation provenance and a
111
+ * named operation keeps aggregate connector data in accounting views without
112
+ * manufacturing per-call savings advice from it.
113
+ */
114
+ export declare function hasModeledWorkloadEvidence(record: UsageRecord): boolean;
115
+ /** Explicit one-call / one-model-invocation provenance, never inferred. */
116
+ export declare function hasCallLevelProvenance(record: UsageRecord): boolean;
117
+ /** A positive priced observation from which a dollar counterfactual can be modeled. */
118
+ export declare function hasPricedEvidence(record: UsageRecord): boolean;
119
+ /**
120
+ * Recognize the bundled demo records written by releases that predate the
121
+ * persisted `mode` field. The marker is deliberately narrow: every record
122
+ * must come from the shipped sample CSV and use a sample source identifier.
123
+ * This lets newer clients recover the demo boundary without guessing that an
124
+ * arbitrary unlabeled state is real local or connected evidence.
125
+ */
126
+ export declare function isBundledSampleUsage(records: UsageRecord[]): boolean;
127
+ /** A declared demo/sample mode can never carry proof-level financial labels. */
128
+ export declare function downgradeSampleUsageEvidence(records: UsageRecord[]): UsageRecord[];
129
+ /**
130
+ * Stable financial cohort for period-over-period comparisons. Missing shape
131
+ * provenance returns `undefined`; unknown rows must not be pooled into a fake
132
+ * comparable series.
133
+ */
134
+ export declare function spendComparisonKey(record: UsageRecord): string | undefined;
59
135
  export declare const attributionCandidateSchema: z.ZodObject<{
60
136
  entityType: z.ZodEnum<{
61
137
  user: "user";
62
138
  project: "project";
63
- client: "client";
64
139
  agent: "agent";
140
+ client: "client";
65
141
  workspace: "workspace";
66
142
  api_key: "api_key";
67
143
  }>;
@@ -76,8 +152,8 @@ export declare const attributionMappingSchema: z.ZodObject<{
76
152
  entityType: z.ZodEnum<{
77
153
  user: "user";
78
154
  project: "project";
79
- client: "client";
80
155
  agent: "agent";
156
+ client: "client";
81
157
  workspace: "workspace";
82
158
  api_key: "api_key";
83
159
  }>;
@@ -89,8 +165,8 @@ export declare const attributionMappingSchema: z.ZodObject<{
89
165
  entityType: z.ZodEnum<{
90
166
  user: "user";
91
167
  project: "project";
92
- client: "client";
93
168
  agent: "agent";
169
+ client: "client";
94
170
  workspace: "workspace";
95
171
  api_key: "api_key";
96
172
  }>;
@@ -99,10 +175,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
99
175
  evidence: z.ZodArray<z.ZodString>;
100
176
  }, z.core.$strip>>;
101
177
  status: z.ZodEnum<{
102
- unmapped: "unmapped";
103
178
  auto_mapped: "auto_mapped";
104
179
  needs_confirmation: "needs_confirmation";
105
180
  needs_question: "needs_question";
181
+ unmapped: "unmapped";
106
182
  }>;
107
183
  evidence: z.ZodArray<z.ZodString>;
108
184
  }, z.core.$strip>;
@@ -112,8 +188,8 @@ export declare const spendBreakdownEntrySchema: z.ZodObject<{
112
188
  amountUsd: z.ZodNumber;
113
189
  recordCount: z.ZodNumber;
114
190
  confidence: z.ZodEnum<{
115
- verified: "verified";
116
191
  estimated: "estimated";
192
+ verified: "verified";
117
193
  detected_unverified: "detected_unverified";
118
194
  missing: "missing";
119
195
  }>;
@@ -125,12 +201,13 @@ export declare const spendAnomalySchema: z.ZodObject<{
125
201
  week_over_week_spike: "week_over_week_spike";
126
202
  }>;
127
203
  key: z.ZodString;
204
+ comparisonKey: z.ZodOptional<z.ZodString>;
128
205
  previousAmountUsd: z.ZodNumber;
129
206
  currentAmountUsd: z.ZodNumber;
130
207
  multiplier: z.ZodNumber;
131
208
  confidence: z.ZodEnum<{
132
- verified: "verified";
133
209
  estimated: "estimated";
210
+ verified: "verified";
134
211
  detected_unverified: "detected_unverified";
135
212
  missing: "missing";
136
213
  }>;
@@ -146,8 +223,8 @@ export declare const workflowWatchEntrySchema: z.ZodObject<{
146
223
  shareOfSpend: z.ZodNumber;
147
224
  recordCount: z.ZodNumber;
148
225
  confidence: z.ZodEnum<{
149
- verified: "verified";
150
226
  estimated: "estimated";
227
+ verified: "verified";
151
228
  detected_unverified: "detected_unverified";
152
229
  missing: "missing";
153
230
  }>;
@@ -171,8 +248,8 @@ export declare const recommendationSchema: z.ZodObject<{
171
248
  }>;
172
249
  estimatedImpactUsd: z.ZodNumber;
173
250
  confidence: z.ZodEnum<{
174
- verified: "verified";
175
251
  estimated: "estimated";
252
+ verified: "verified";
176
253
  detected_unverified: "detected_unverified";
177
254
  missing: "missing";
178
255
  }>;
@@ -216,8 +293,8 @@ export declare const spendInsightSchema: z.ZodObject<{
216
293
  affectedModels: z.ZodArray<z.ZodString>;
217
294
  estimatedImpactUsd: z.ZodNumber;
218
295
  confidence: z.ZodEnum<{
219
- verified: "verified";
220
296
  estimated: "estimated";
297
+ verified: "verified";
221
298
  detected_unverified: "detected_unverified";
222
299
  missing: "missing";
223
300
  }>;
@@ -229,14 +306,14 @@ export declare const spendSummarySchema: z.ZodObject<{
229
306
  totalUsd: z.ZodNumber;
230
307
  recordCount: z.ZodNumber;
231
308
  confidence: z.ZodEnum<{
232
- verified: "verified";
233
309
  estimated: "estimated";
310
+ verified: "verified";
234
311
  detected_unverified: "detected_unverified";
235
312
  missing: "missing";
236
313
  }>;
237
314
  confidenceBreakdown: z.ZodRecord<z.ZodEnum<{
238
- verified: "verified";
239
315
  estimated: "estimated";
316
+ verified: "verified";
240
317
  detected_unverified: "detected_unverified";
241
318
  missing: "missing";
242
319
  }>, z.ZodNumber>;
@@ -245,8 +322,8 @@ export declare const spendSummarySchema: z.ZodObject<{
245
322
  amountUsd: z.ZodNumber;
246
323
  recordCount: z.ZodNumber;
247
324
  confidence: z.ZodEnum<{
248
- verified: "verified";
249
325
  estimated: "estimated";
326
+ verified: "verified";
250
327
  detected_unverified: "detected_unverified";
251
328
  missing: "missing";
252
329
  }>;
@@ -256,8 +333,8 @@ export declare const spendSummarySchema: z.ZodObject<{
256
333
  amountUsd: z.ZodNumber;
257
334
  recordCount: z.ZodNumber;
258
335
  confidence: z.ZodEnum<{
259
- verified: "verified";
260
336
  estimated: "estimated";
337
+ verified: "verified";
261
338
  detected_unverified: "detected_unverified";
262
339
  missing: "missing";
263
340
  }>;
@@ -267,8 +344,8 @@ export declare const spendSummarySchema: z.ZodObject<{
267
344
  amountUsd: z.ZodNumber;
268
345
  recordCount: z.ZodNumber;
269
346
  confidence: z.ZodEnum<{
270
- verified: "verified";
271
347
  estimated: "estimated";
348
+ verified: "verified";
272
349
  detected_unverified: "detected_unverified";
273
350
  missing: "missing";
274
351
  }>;
@@ -278,8 +355,8 @@ export declare const spendSummarySchema: z.ZodObject<{
278
355
  amountUsd: z.ZodNumber;
279
356
  recordCount: z.ZodNumber;
280
357
  confidence: z.ZodEnum<{
281
- verified: "verified";
282
358
  estimated: "estimated";
359
+ verified: "verified";
283
360
  detected_unverified: "detected_unverified";
284
361
  missing: "missing";
285
362
  }>;
@@ -289,8 +366,8 @@ export declare const spendSummarySchema: z.ZodObject<{
289
366
  amountUsd: z.ZodNumber;
290
367
  recordCount: z.ZodNumber;
291
368
  confidence: z.ZodEnum<{
292
- verified: "verified";
293
369
  estimated: "estimated";
370
+ verified: "verified";
294
371
  detected_unverified: "detected_unverified";
295
372
  missing: "missing";
296
373
  }>;
@@ -300,8 +377,8 @@ export declare const spendSummarySchema: z.ZodObject<{
300
377
  amountUsd: z.ZodNumber;
301
378
  recordCount: z.ZodNumber;
302
379
  confidence: z.ZodEnum<{
303
- verified: "verified";
304
380
  estimated: "estimated";
381
+ verified: "verified";
305
382
  detected_unverified: "detected_unverified";
306
383
  missing: "missing";
307
384
  }>;
@@ -311,8 +388,8 @@ export declare const spendSummarySchema: z.ZodObject<{
311
388
  amountUsd: z.ZodNumber;
312
389
  recordCount: z.ZodNumber;
313
390
  confidence: z.ZodEnum<{
314
- verified: "verified";
315
391
  estimated: "estimated";
392
+ verified: "verified";
316
393
  detected_unverified: "detected_unverified";
317
394
  missing: "missing";
318
395
  }>;
@@ -322,8 +399,8 @@ export declare const spendSummarySchema: z.ZodObject<{
322
399
  amountUsd: z.ZodNumber;
323
400
  recordCount: z.ZodNumber;
324
401
  confidence: z.ZodEnum<{
325
- verified: "verified";
326
402
  estimated: "estimated";
403
+ verified: "verified";
327
404
  detected_unverified: "detected_unverified";
328
405
  missing: "missing";
329
406
  }>;
@@ -338,8 +415,8 @@ export declare const spendSummarySchema: z.ZodObject<{
338
415
  shareOfSpend: z.ZodNumber;
339
416
  recordCount: z.ZodNumber;
340
417
  confidence: z.ZodEnum<{
341
- verified: "verified";
342
418
  estimated: "estimated";
419
+ verified: "verified";
343
420
  detected_unverified: "detected_unverified";
344
421
  missing: "missing";
345
422
  }>;
@@ -355,12 +432,13 @@ export declare const spendSummarySchema: z.ZodObject<{
355
432
  week_over_week_spike: "week_over_week_spike";
356
433
  }>;
357
434
  key: z.ZodString;
435
+ comparisonKey: z.ZodOptional<z.ZodString>;
358
436
  previousAmountUsd: z.ZodNumber;
359
437
  currentAmountUsd: z.ZodNumber;
360
438
  multiplier: z.ZodNumber;
361
439
  confidence: z.ZodEnum<{
362
- verified: "verified";
363
440
  estimated: "estimated";
441
+ verified: "verified";
364
442
  detected_unverified: "detected_unverified";
365
443
  missing: "missing";
366
444
  }>;
@@ -378,8 +456,8 @@ export declare const spendSummarySchema: z.ZodObject<{
378
456
  }>;
379
457
  estimatedImpactUsd: z.ZodNumber;
380
458
  confidence: z.ZodEnum<{
381
- verified: "verified";
382
459
  estimated: "estimated";
460
+ verified: "verified";
383
461
  detected_unverified: "detected_unverified";
384
462
  missing: "missing";
385
463
  }>;
@@ -416,8 +494,8 @@ export declare const spendSummarySchema: z.ZodObject<{
416
494
  affectedModels: z.ZodArray<z.ZodString>;
417
495
  estimatedImpactUsd: z.ZodNumber;
418
496
  confidence: z.ZodEnum<{
419
- verified: "verified";
420
497
  estimated: "estimated";
498
+ verified: "verified";
421
499
  detected_unverified: "detected_unverified";
422
500
  missing: "missing";
423
501
  }>;
package/dist/schema.js CHANGED
@@ -13,6 +13,35 @@ export const spendSourceSchema = z.object({
13
13
  confidence: costConfidenceSchema,
14
14
  observedFrom: z.string().min(1)
15
15
  });
16
+ /**
17
+ * What one normalized usage record represents.
18
+ *
19
+ * Only `call` and `invocation` are precise enough to support per-workload
20
+ * counterfactuals such as model routing, result caching, Batch API moves, or
21
+ * prompt trimming. The remaining values are still useful financial evidence,
22
+ * but must never be silently treated as individual calls.
23
+ */
24
+ export const usageGranularityValues = [
25
+ "call",
26
+ "invocation",
27
+ "session",
28
+ "daily_aggregate",
29
+ "usage_bucket",
30
+ "billing_bucket",
31
+ "seat",
32
+ "user_aggregate"
33
+ ];
34
+ export const usageGranularitySchema = z.enum(usageGranularityValues);
35
+ /**
36
+ * Explicit adapter attestations for workload-specific optimization advice.
37
+ * These fields are intentionally absent by default: an operation label alone
38
+ * does not prove identical inputs, latency tolerance, or downgrade safety.
39
+ */
40
+ export const workloadSemanticsSchema = z.object({
41
+ stableInputFingerprint: z.string().min(8).max(128).regex(/^[a-zA-Z0-9:_-]+$/).optional(),
42
+ batchEligible: z.boolean().optional(),
43
+ downgradeSafe: z.boolean().optional()
44
+ }).strict();
16
45
  export const usageRecordSchema = z.object({
17
46
  id: z.string().min(1),
18
47
  timestamp: z.string().datetime({ offset: true }),
@@ -30,7 +59,9 @@ export const usageRecordSchema = z.object({
30
59
  providerCostType: z.string().min(1).optional(),
31
60
  quantity: z.number().nonnegative().optional(),
32
61
  agentId: z.string().min(1).optional(),
33
- operation: z.string().min(1).optional()
62
+ operation: z.string().min(1).optional(),
63
+ usageGranularity: usageGranularitySchema.optional(),
64
+ workloadSemantics: workloadSemanticsSchema.optional()
34
65
  }).superRefine((record, context) => {
35
66
  if (record.costConfidence === "missing" && record.amountUsd !== null) {
36
67
  context.addIssue({
@@ -47,6 +78,79 @@ export const usageRecordSchema = z.object({
47
78
  });
48
79
  }
49
80
  });
81
+ /**
82
+ * True only when a record can honestly ground a modeled workload change.
83
+ *
84
+ * A provider billing row, usage bucket, seat, user total, or unlabelled legacy
85
+ * row may describe real spend, but it does not prove that one row was one
86
+ * optimizable call. Requiring both explicit call/invocation provenance and a
87
+ * named operation keeps aggregate connector data in accounting views without
88
+ * manufacturing per-call savings advice from it.
89
+ */
90
+ export function hasModeledWorkloadEvidence(record) {
91
+ return (hasCallLevelProvenance(record) &&
92
+ typeof record.operation === "string" &&
93
+ record.operation.trim().length > 0 &&
94
+ hasPricedEvidence(record));
95
+ }
96
+ /** Explicit one-call / one-model-invocation provenance, never inferred. */
97
+ export function hasCallLevelProvenance(record) {
98
+ return record.usageGranularity === "call" || record.usageGranularity === "invocation";
99
+ }
100
+ /** A positive priced observation from which a dollar counterfactual can be modeled. */
101
+ export function hasPricedEvidence(record) {
102
+ return (typeof record.amountUsd === "number" &&
103
+ record.amountUsd > 0 &&
104
+ record.costConfidence !== "missing");
105
+ }
106
+ /**
107
+ * Recognize the bundled demo records written by releases that predate the
108
+ * persisted `mode` field. The marker is deliberately narrow: every record
109
+ * must come from the shipped sample CSV and use a sample source identifier.
110
+ * This lets newer clients recover the demo boundary without guessing that an
111
+ * arbitrary unlabeled state is real local or connected evidence.
112
+ */
113
+ export function isBundledSampleUsage(records) {
114
+ return records.length > 0 && records.every(isBundledSampleRecord);
115
+ }
116
+ function isBundledSampleRecord(record) {
117
+ return record.source.observedFrom === "sample_csv" &&
118
+ /(?:^|-)sample$/i.test(record.source.id);
119
+ }
120
+ /** A declared demo/sample mode can never carry proof-level financial labels. */
121
+ export function downgradeSampleUsageEvidence(records) {
122
+ return records.map(downgradeSampleRecordEvidence);
123
+ }
124
+ function downgradeSampleRecordEvidence(record) {
125
+ return {
126
+ ...record,
127
+ source: {
128
+ ...record.source,
129
+ confidence: record.source.confidence === "verified"
130
+ ? "estimated"
131
+ : record.source.confidence
132
+ },
133
+ costConfidence: record.costConfidence === "verified"
134
+ ? "estimated"
135
+ : record.costConfidence
136
+ };
137
+ }
138
+ /**
139
+ * Stable financial cohort for period-over-period comparisons. Missing shape
140
+ * provenance returns `undefined`; unknown rows must not be pooled into a fake
141
+ * comparable series.
142
+ */
143
+ export function spendComparisonKey(record) {
144
+ if (!record.providerCostType || !record.usageGranularity) {
145
+ return undefined;
146
+ }
147
+ return [
148
+ record.source.id,
149
+ record.source.provider,
150
+ record.providerCostType,
151
+ record.usageGranularity
152
+ ].map((part) => encodeURIComponent(part)).join("|");
153
+ }
50
154
  export const attributionCandidateSchema = z.object({
51
155
  entityType: z.enum(["client", "project", "agent", "user", "workspace", "api_key"]),
52
156
  entityId: z.string().min(1),
@@ -69,6 +173,7 @@ export const spendBreakdownEntrySchema = z.object({
69
173
  export const spendAnomalySchema = z.object({
70
174
  kind: z.enum(["day_over_day_spike", "week_over_week_spike"]),
71
175
  key: z.string().min(1),
176
+ comparisonKey: z.string().min(1).optional(),
72
177
  previousAmountUsd: z.number().nonnegative(),
73
178
  currentAmountUsd: z.number().nonnegative(),
74
179
  multiplier: z.number().nonnegative(),
@@ -150,7 +255,10 @@ export const spendSummarySchema = z.object({
150
255
  insights: z.array(spendInsightSchema).default([])
151
256
  });
152
257
  export function parseUsageRecord(value) {
153
- return usageRecordSchema.parse(value);
258
+ const record = usageRecordSchema.parse(value);
259
+ if (!isBundledSampleRecord(record))
260
+ return record;
261
+ return downgradeSampleRecordEvidence(record);
154
262
  }
155
263
  export function parseSpendSummary(value) {
156
264
  return spendSummarySchema.parse(value);
@@ -1,15 +1,18 @@
1
1
  import type { UsageSignal } from "./discovery.js";
2
+ import type { FinancialEvidenceStatus, SourceValidationCoverage } from "./sourceStatus.js";
2
3
  export type SourceType = "local_folder" | "provider_export" | "provider_api" | "browser_account" | "local_tool_detection" | "mcp_tool" | "internal_system";
3
4
  export type SourceAccessMethod = "file" | "api" | "browser" | "cli_detection" | "mcp" | "internal" | "manual";
4
5
  export type ConnectorAuthMode = "oauth" | "api_token_ref" | "browser_session" | "mcp_auth" | "manual_export" | "none";
5
6
  export type ConnectorTokenStorage = "local_reference_only" | "keychain_reference" | "none";
6
- export type SourceVerificationStatus = "verified" | "estimated" | "detected_unverified" | "missing";
7
+ /** @deprecated Use FinancialEvidenceStatus. Kept only for persisted v1 migration. */
8
+ export type SourceVerificationStatus = FinancialEvidenceStatus;
9
+ export type SourceBoundaryApproval = "approved";
7
10
  export type IngestionLaneId = "local_files_exports" | "provider_apis" | "browser_account_ui" | "local_cli_tool_detection" | "mcp_internal_systems";
8
11
  export type IngestionLane = {
9
12
  id: IngestionLaneId;
10
13
  label: string;
11
14
  sourceTypes: SourceType[];
12
- defaultVerification: SourceVerificationStatus;
15
+ defaultFinancialEvidence: FinancialEvidenceStatus;
13
16
  };
14
17
  export type ApprovedSource = {
15
18
  id: string;
@@ -22,7 +25,12 @@ export type ApprovedSource = {
22
25
  scope: string;
23
26
  lane: IngestionLaneId;
24
27
  accessMethod: SourceAccessMethod;
25
- verification: SourceVerificationStatus;
28
+ /** Permission to read this exact boundary. This is never financial proof. */
29
+ boundaryApproval: SourceBoundaryApproval;
30
+ /** How thoroughly the connector/parser itself has been exercised. */
31
+ validationCoverage: SourceValidationCoverage;
32
+ /** Quality of the financial numbers currently emitted by this source. */
33
+ financialEvidence: FinancialEvidenceStatus;
26
34
  fieldsVerified: string[];
27
35
  fieldsEstimated: string[];
28
36
  fieldsMissing: string[];
@@ -73,7 +81,7 @@ export type ProviderConnectorCatalogEntry = {
73
81
  };
74
82
  export type MissingSourcePrompt = {
75
83
  provider: string;
76
- status: Extract<SourceVerificationStatus, "detected_unverified" | "missing">;
84
+ status: Extract<FinancialEvidenceStatus, "detected_unverified" | "missing">;
77
85
  reason: string;
78
86
  detectedEvidence: string[];
79
87
  suggestedConnector: string;
@@ -100,8 +108,25 @@ export declare const providerCatalog: ProviderCatalogEntry[];
100
108
  export declare const providerConnectorCatalog: ProviderConnectorCatalogEntry[];
101
109
  export declare const defaultDeniedGlobs: string[];
102
110
  export declare function createLocalFolderSourceRegistry(rootPath: string, now?: Date): SourceRegistry;
103
- export declare function addApprovedSource(registry: SourceRegistry, source: Omit<ApprovedSource, "approvedAt" | "readOnly" | "scope" | "lane" | "accessMethod" | "verification" | "fieldsVerified" | "fieldsEstimated" | "fieldsMissing"> & Partial<Pick<ApprovedSource, "readOnly" | "scope" | "lane" | "accessMethod" | "verification" | "fieldsVerified" | "fieldsEstimated" | "fieldsMissing" | "authMode" | "authScopes" | "tokenStorage" | "authReference">>, now?: Date): SourceRegistry;
111
+ export declare function addApprovedSource(registry: SourceRegistry, source: Omit<ApprovedSource, "approvedAt" | "readOnly" | "scope" | "lane" | "accessMethod" | "boundaryApproval" | "validationCoverage" | "financialEvidence" | "fieldsVerified" | "fieldsEstimated" | "fieldsMissing"> & Partial<Pick<ApprovedSource, "readOnly" | "scope" | "lane" | "accessMethod" | "boundaryApproval" | "validationCoverage" | "financialEvidence" | "fieldsVerified" | "fieldsEstimated" | "fieldsMissing" | "authMode" | "authScopes" | "tokenStorage" | "authReference">>, now?: Date): SourceRegistry;
104
112
  export declare function createProviderConnectorStub(provider: string, type?: SourceType, now?: Date): ApprovedSource;
113
+ /**
114
+ * Read a persisted source registry into the canonical three-axis contract.
115
+ *
116
+ * Version 1 registries used `verification` for several unrelated meanings.
117
+ * It is accepted here only as a migration input for financial evidence and is
118
+ * deliberately omitted from the returned object. A local-folder approval is
119
+ * permission metadata, so even a legacy `verification: "verified"` migrates
120
+ * to `financialEvidence: "missing"`.
121
+ */
122
+ export declare function normalizeSourceRegistry(value: unknown): SourceRegistry;
123
+ /**
124
+ * Persisted source registries are repository-controlled configuration. Until
125
+ * an external provider-sync receipt binds their exact bytes, keep only the
126
+ * approved read-only boundary and remove any self-asserted validation or
127
+ * financial-evidence claims.
128
+ */
129
+ export declare function downgradeUntrustedSourceRegistryClaims(registry: SourceRegistry): SourceRegistry;
105
130
  export declare function buildMissingSourcePrompts(signals: UsageSignal[], registry: SourceRegistry): MissingSourcePrompt[];
106
131
  export declare function confirmMapping(input: Omit<ConfirmedMapping, "id" | "status" | "confirmedAt">, now?: Date): ConfirmedMapping;
107
132
  export declare function createScanAuditLog(events?: ScanAuditEvent[]): ScanAuditLog;