@agent-finops/core 0.5.9 → 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.
@@ -3,31 +3,31 @@ export const ingestionLanes = [
3
3
  id: "local_files_exports",
4
4
  label: "Local files and provider exports",
5
5
  sourceTypes: ["local_folder", "provider_export"],
6
- defaultVerification: "estimated"
6
+ defaultFinancialEvidence: "estimated"
7
7
  },
8
8
  {
9
9
  id: "provider_apis",
10
10
  label: "Official provider APIs",
11
11
  sourceTypes: ["provider_api"],
12
- defaultVerification: "verified"
12
+ defaultFinancialEvidence: "verified"
13
13
  },
14
14
  {
15
15
  id: "browser_account_ui",
16
16
  label: "Browser Account UI",
17
17
  sourceTypes: ["browser_account"],
18
- defaultVerification: "verified"
18
+ defaultFinancialEvidence: "verified"
19
19
  },
20
20
  {
21
21
  id: "local_cli_tool_detection",
22
22
  label: "Local CLI/tool detection path",
23
23
  sourceTypes: ["local_tool_detection"],
24
- defaultVerification: "detected_unverified"
24
+ defaultFinancialEvidence: "detected_unverified"
25
25
  },
26
26
  {
27
27
  id: "mcp_internal_systems",
28
28
  label: "MCP and internal systems",
29
29
  sourceTypes: ["mcp_tool", "internal_system"],
30
- defaultVerification: "verified"
30
+ defaultFinancialEvidence: "verified"
31
31
  }
32
32
  ];
33
33
  export const supportedSourceTypes = ingestionLanes.flatMap((lane) => lane.sourceTypes);
@@ -195,7 +195,9 @@ export function createLocalFolderSourceRegistry(rootPath, now = new Date()) {
195
195
  scope: "Read-only scan of the explicit --path root. No writes outside .ai-spend-agent. No cloud upload.",
196
196
  lane: "local_files_exports",
197
197
  accessMethod: "file",
198
- verification: "verified",
198
+ boundaryApproval: "approved",
199
+ validationCoverage: "untested",
200
+ financialEvidence: "missing",
199
201
  fieldsVerified: ["approved local folder boundary"],
200
202
  fieldsEstimated: [],
201
203
  fieldsMissing: ["provider account billing data"]
@@ -209,14 +211,24 @@ export function createLocalFolderSourceRegistry(rootPath, now = new Date()) {
209
211
  }
210
212
  export function addApprovedSource(registry, source, now = new Date()) {
211
213
  const timestamp = now.toISOString();
214
+ const canonicalRegistry = normalizeSourceRegistry(registry);
215
+ if (source.readOnly === false) {
216
+ throw new Error("Approved source boundaries must remain read-only.");
217
+ }
212
218
  const nextSource = {
213
- ...source,
214
- readOnly: source.readOnly ?? true,
219
+ id: source.id,
220
+ type: source.type,
221
+ label: source.label,
222
+ ...(source.path ? { path: source.path } : {}),
223
+ ...(source.provider ? { provider: source.provider } : {}),
224
+ readOnly: true,
215
225
  approvedAt: timestamp,
216
226
  scope: source.scope ?? defaultScopeForSource(source.type),
217
227
  lane: source.lane ?? laneForSourceType(source.type),
218
228
  accessMethod: source.accessMethod ?? accessMethodForSourceType(source.type),
219
- verification: source.verification ?? ingestionLanes.find((lane) => lane.sourceTypes.includes(source.type))?.defaultVerification ?? "estimated",
229
+ boundaryApproval: source.boundaryApproval ?? "approved",
230
+ validationCoverage: source.validationCoverage ?? "untested",
231
+ financialEvidence: source.type === "local_folder" ? "missing" : (source.financialEvidence ?? "missing"),
220
232
  fieldsVerified: source.fieldsVerified ?? [],
221
233
  fieldsEstimated: source.fieldsEstimated ?? [],
222
234
  fieldsMissing: source.fieldsMissing ?? [],
@@ -225,11 +237,11 @@ export function addApprovedSource(registry, source, now = new Date()) {
225
237
  tokenStorage: source.tokenStorage,
226
238
  authReference: source.authReference
227
239
  };
228
- const withoutExisting = registry.approvedSources.filter((candidate) => candidate.id !== nextSource.id);
240
+ const withoutExisting = canonicalRegistry.approvedSources.filter((candidate) => candidate.id !== nextSource.id);
229
241
  return {
230
- ...registry,
231
- ingestionLanes: registry.ingestionLanes ?? ingestionLanes,
232
- supportedSourceTypes: registry.supportedSourceTypes ?? supportedSourceTypes,
242
+ ...canonicalRegistry,
243
+ ingestionLanes: canonicalRegistry.ingestionLanes ?? ingestionLanes,
244
+ supportedSourceTypes: canonicalRegistry.supportedSourceTypes ?? supportedSourceTypes,
233
245
  approvedSources: [...withoutExisting, nextSource],
234
246
  updatedAt: timestamp
235
247
  };
@@ -248,7 +260,11 @@ export function createProviderConnectorStub(provider, type = providerCatalog.fin
248
260
  scope: defaultScopeForSource(type),
249
261
  lane: laneForSourceType(type),
250
262
  accessMethod: accessMethodForSourceType(type, catalogEntry),
251
- verification: "missing",
263
+ boundaryApproval: "approved",
264
+ // Registering a read-only boundary is not evidence that a connector ran.
265
+ // A successful provider result promotes this axis explicitly.
266
+ validationCoverage: "untested",
267
+ financialEvidence: "missing",
252
268
  fieldsVerified: catalogEntry?.verifiedFields ?? [],
253
269
  fieldsEstimated: [],
254
270
  fieldsMissing: catalogEntry?.missingFields ?? ["approved account/API/export source"],
@@ -257,6 +273,157 @@ export function createProviderConnectorStub(provider, type = providerCatalog.fin
257
273
  tokenStorage: tokenStorageForConnectorType(type, connectorEntry)
258
274
  };
259
275
  }
276
+ /**
277
+ * Read a persisted source registry into the canonical three-axis contract.
278
+ *
279
+ * Version 1 registries used `verification` for several unrelated meanings.
280
+ * It is accepted here only as a migration input for financial evidence and is
281
+ * deliberately omitted from the returned object. A local-folder approval is
282
+ * permission metadata, so even a legacy `verification: "verified"` migrates
283
+ * to `financialEvidence: "missing"`.
284
+ */
285
+ export function normalizeSourceRegistry(value) {
286
+ if (!isObject(value) || value.version !== 1 || value.localOnly !== true || value.cloudUpload !== false) {
287
+ throw new Error("Invalid local source registry: expected the canonical local-only registry shape.");
288
+ }
289
+ if (!Array.isArray(value.approvedSources) ||
290
+ !Array.isArray(value.deniedGlobs) ||
291
+ !Array.isArray(value.ingestionLanes) ||
292
+ !Array.isArray(value.supportedSourceTypes) ||
293
+ !isValidIso(value.updatedAt) ||
294
+ !isStringList(value.deniedGlobs) ||
295
+ !value.supportedSourceTypes.every(isSourceTypeValue)) {
296
+ throw new Error("Invalid local source registry: expected valid source, lane, and deny lists.");
297
+ }
298
+ const normalizedLanes = value.ingestionLanes.map(normalizeIngestionLane);
299
+ const normalizedSources = value.approvedSources.map(normalizeApprovedSource);
300
+ return {
301
+ version: 1,
302
+ localOnly: true,
303
+ cloudUpload: false,
304
+ approvedSources: normalizedSources,
305
+ deniedGlobs: [...value.deniedGlobs],
306
+ ingestionLanes: normalizedLanes,
307
+ supportedSourceTypes: [...value.supportedSourceTypes],
308
+ updatedAt: value.updatedAt
309
+ };
310
+ }
311
+ function normalizeIngestionLane(value) {
312
+ if (!isObject(value) ||
313
+ !isIngestionLaneId(value.id) ||
314
+ !isNonEmptyString(value.label) ||
315
+ !Array.isArray(value.sourceTypes) ||
316
+ !value.sourceTypes.every(isSourceTypeValue)) {
317
+ throw new Error("Invalid local source registry: an ingestion lane has a malformed shape.");
318
+ }
319
+ if (value.defaultFinancialEvidence !== undefined && !isFinancialEvidence(value.defaultFinancialEvidence)) {
320
+ throw new Error("Invalid local source registry: an ingestion lane has invalid financial evidence.");
321
+ }
322
+ if (value.defaultVerification !== undefined && !isFinancialEvidence(value.defaultVerification)) {
323
+ throw new Error("Invalid local source registry: an ingestion lane has an invalid legacy verification value.");
324
+ }
325
+ const financialEvidence = isFinancialEvidence(value.defaultFinancialEvidence)
326
+ ? value.defaultFinancialEvidence
327
+ : isFinancialEvidence(value.defaultVerification)
328
+ ? value.defaultVerification
329
+ : undefined;
330
+ if (!financialEvidence) {
331
+ throw new Error("Invalid local source registry: an ingestion lane is missing its financial-evidence default.");
332
+ }
333
+ return {
334
+ id: value.id,
335
+ label: value.label,
336
+ sourceTypes: [...value.sourceTypes],
337
+ defaultFinancialEvidence: financialEvidence
338
+ };
339
+ }
340
+ function normalizeApprovedSource(value) {
341
+ if (!isObject(value) ||
342
+ !isNonEmptyString(value.id) ||
343
+ !isSourceTypeValue(value.type) ||
344
+ !isNonEmptyString(value.label) ||
345
+ value.readOnly !== true ||
346
+ !isValidIso(value.approvedAt) ||
347
+ !isNonEmptyString(value.scope) ||
348
+ !isIngestionLaneId(value.lane) ||
349
+ !isAccessMethod(value.accessMethod) ||
350
+ !isStringList(value.fieldsVerified) ||
351
+ !isStringList(value.fieldsEstimated) ||
352
+ !isStringList(value.fieldsMissing)) {
353
+ throw new Error("Invalid local source registry: an approved source has a malformed shape.");
354
+ }
355
+ if (value.boundaryApproval !== undefined && value.boundaryApproval !== "approved") {
356
+ throw new Error("Invalid local source registry: an approved source has an invalid boundary approval.");
357
+ }
358
+ if (value.validationCoverage !== undefined && !isValidationCoverage(value.validationCoverage)) {
359
+ throw new Error("Invalid local source registry: an approved source has an invalid validation coverage.");
360
+ }
361
+ if (value.financialEvidence !== undefined && !isFinancialEvidence(value.financialEvidence)) {
362
+ throw new Error("Invalid local source registry: an approved source has invalid financial evidence.");
363
+ }
364
+ if (value.verification !== undefined && !isFinancialEvidence(value.verification)) {
365
+ throw new Error("Invalid local source registry: an approved source has an invalid legacy verification value.");
366
+ }
367
+ const provider = optionalNonEmptyString(value.provider, "provider");
368
+ const path = optionalNonEmptyString(value.path, "path");
369
+ const authReference = optionalNonEmptyString(value.authReference, "auth reference");
370
+ const authMode = optionalEnum(value.authMode, isAuthMode, "auth mode");
371
+ const tokenStorage = optionalEnum(value.tokenStorage, isTokenStorage, "token storage");
372
+ if (value.authScopes !== undefined && !isStringList(value.authScopes)) {
373
+ throw new Error("Invalid local source registry: an approved source has invalid auth scopes.");
374
+ }
375
+ const migratedEvidence = isFinancialEvidence(value.financialEvidence)
376
+ ? value.financialEvidence
377
+ : isFinancialEvidence(value.verification)
378
+ ? value.verification
379
+ : "missing";
380
+ return {
381
+ id: value.id,
382
+ type: value.type,
383
+ label: value.label,
384
+ ...(path ? { path } : {}),
385
+ ...(provider ? { provider } : {}),
386
+ readOnly: true,
387
+ approvedAt: value.approvedAt,
388
+ scope: value.scope,
389
+ lane: value.lane,
390
+ accessMethod: value.accessMethod,
391
+ boundaryApproval: "approved",
392
+ validationCoverage: isValidationCoverage(value.validationCoverage)
393
+ ? value.validationCoverage
394
+ : "untested",
395
+ financialEvidence: value.type === "local_folder" ? "missing" : migratedEvidence,
396
+ fieldsVerified: [...value.fieldsVerified],
397
+ fieldsEstimated: [...value.fieldsEstimated],
398
+ fieldsMissing: [...value.fieldsMissing],
399
+ ...(authMode ? { authMode } : {}),
400
+ ...(value.authScopes ? { authScopes: [...value.authScopes] } : {}),
401
+ ...(tokenStorage ? { tokenStorage } : {}),
402
+ ...(authReference ? { authReference } : {})
403
+ };
404
+ }
405
+ /**
406
+ * Persisted source registries are repository-controlled configuration. Until
407
+ * an external provider-sync receipt binds their exact bytes, keep only the
408
+ * approved read-only boundary and remove any self-asserted validation or
409
+ * financial-evidence claims.
410
+ */
411
+ export function downgradeUntrustedSourceRegistryClaims(registry) {
412
+ const canonical = normalizeSourceRegistry(registry);
413
+ return {
414
+ ...canonical,
415
+ approvedSources: canonical.approvedSources.map((source) => ({
416
+ ...source,
417
+ validationCoverage: "untested",
418
+ financialEvidence: "missing",
419
+ fieldsVerified: source.fieldsVerified.filter((field) => /approved|boundary|read-only|folder/i.test(field)),
420
+ fieldsMissing: Array.from(new Set([
421
+ ...source.fieldsMissing,
422
+ "machine-bound provider validation and financial evidence"
423
+ ]))
424
+ }))
425
+ };
426
+ }
260
427
  export function buildMissingSourcePrompts(signals, registry) {
261
428
  const providerSignals = new Map();
262
429
  for (const signal of signals) {
@@ -267,7 +434,7 @@ export function buildMissingSourcePrompts(signals, registry) {
267
434
  }
268
435
  const prompts = [];
269
436
  for (const [provider, detectedSignals] of Array.from(providerSignals.entries())) {
270
- if (hasVerifiedProviderSource(registry, provider)) {
437
+ if (hasCurrentProviderFinancialEvidence(registry, provider)) {
271
438
  continue;
272
439
  }
273
440
  const catalogEntry = providerCatalog.find((entry) => entry.id === provider);
@@ -276,7 +443,7 @@ export function buildMissingSourcePrompts(signals, registry) {
276
443
  prompts.push({
277
444
  provider,
278
445
  status: "detected_unverified",
279
- reason: `${provider} was detected locally, but no verified provider/API/browser/export source is connected.`,
446
+ reason: `${provider} was detected locally, but no approved provider/API/browser/export boundary has current financial evidence. Connector validation is reported separately.`,
280
447
  detectedEvidence: detectedSignals.map((signal) => signal.evidence),
281
448
  suggestedConnector: `connect ${provider} --type ${preferredType}`,
282
449
  suggestedSourceTypes
@@ -315,17 +482,26 @@ export function slugifySourceId(label) {
315
482
  .replace(/^-+|-+$/g, "");
316
483
  return slug || "approved-source";
317
484
  }
318
- function hasVerifiedProviderSource(registry, provider) {
485
+ function hasCurrentProviderFinancialEvidence(registry, provider) {
319
486
  return registry.approvedSources.some((source) => {
320
487
  if (source.provider !== provider) {
321
488
  return false;
322
489
  }
323
- if (source.verification === "verified" && source.type !== "local_tool_detection") {
324
- return true;
325
- }
326
- return source.type === "provider_export" || source.type === "provider_api" || source.type === "browser_account" || source.type === "internal_system";
490
+ return source.boundaryApproval === "approved" &&
491
+ source.validationCoverage !== "failed" &&
492
+ source.financialEvidence !== "missing" &&
493
+ source.type !== "local_tool_detection";
327
494
  });
328
495
  }
496
+ function validationCoverageForSource(provider, type) {
497
+ if (type === "provider_api" && (provider === "openai" || provider === "anthropic"))
498
+ return "live_verified";
499
+ if (type === "provider_api" && (provider === "cursor" || provider === "github-copilot" || provider === "copilot"))
500
+ return "fixture_verified";
501
+ if (provider === "local-agent-logs" && type === "local_tool_detection")
502
+ return "live_verified";
503
+ return "untested";
504
+ }
329
505
  function authModeForConnectorType(type, connectorEntry) {
330
506
  if (type === "browser_account")
331
507
  return "browser_session";
@@ -377,4 +553,57 @@ function accessMethodForSourceType(type, catalogEntry) {
377
553
  return "internal";
378
554
  return "file";
379
555
  }
556
+ function isObject(value) {
557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
558
+ }
559
+ function isNonEmptyString(value) {
560
+ return typeof value === "string" && value.trim().length > 0;
561
+ }
562
+ function isStringList(value) {
563
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
564
+ }
565
+ function isValidIso(value) {
566
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
567
+ }
568
+ function isSourceTypeValue(value) {
569
+ return supportedSourceTypes.includes(value);
570
+ }
571
+ function isIngestionLaneId(value) {
572
+ return value === "local_files_exports" ||
573
+ value === "provider_apis" ||
574
+ value === "browser_account_ui" ||
575
+ value === "local_cli_tool_detection" ||
576
+ value === "mcp_internal_systems";
577
+ }
578
+ function isAccessMethod(value) {
579
+ return value === "file" || value === "api" || value === "browser" || value === "cli_detection" || value === "mcp" || value === "internal" || value === "manual";
580
+ }
581
+ function isFinancialEvidence(value) {
582
+ return value === "verified" || value === "estimated" || value === "detected_unverified" || value === "missing";
583
+ }
584
+ function isValidationCoverage(value) {
585
+ return value === "live_verified" || value === "fixture_verified" || value === "untested" || value === "failed";
586
+ }
587
+ function isAuthMode(value) {
588
+ return value === "oauth" || value === "api_token_ref" || value === "browser_session" || value === "mcp_auth" || value === "manual_export" || value === "none";
589
+ }
590
+ function isTokenStorage(value) {
591
+ return value === "local_reference_only" || value === "keychain_reference" || value === "none";
592
+ }
593
+ function optionalNonEmptyString(value, label) {
594
+ if (value === undefined)
595
+ return undefined;
596
+ if (!isNonEmptyString(value)) {
597
+ throw new Error(`Invalid local source registry: an approved source has an invalid ${label}.`);
598
+ }
599
+ return value;
600
+ }
601
+ function optionalEnum(value, predicate, label) {
602
+ if (value === undefined)
603
+ return undefined;
604
+ if (!predicate(value)) {
605
+ throw new Error(`Invalid local source registry: an approved source has an invalid ${label}.`);
606
+ }
607
+ return value;
608
+ }
380
609
  //# sourceMappingURL=sourceRegistry.js.map
@@ -0,0 +1,65 @@
1
+ import type { CostConfidence, UsageRecord } from "./schema.js";
2
+ /**
3
+ * How thoroughly an ingestion path itself has been exercised.
4
+ *
5
+ * This is deliberately separate from financial evidence quality. A connector
6
+ * can be live-verified while a particular record is still only an estimate.
7
+ */
8
+ export declare const sourceValidationCoverageValues: readonly ["live_verified", "fixture_verified", "untested", "failed"];
9
+ export type SourceValidationCoverage = typeof sourceValidationCoverageValues[number];
10
+ /** The existing aibill financial-evidence vocabulary, named for this contract. */
11
+ export type FinancialEvidenceStatus = CostConfidence;
12
+ export declare const sourceFreshnessStatusValues: readonly ["fresh", "stale", "not_checked"];
13
+ export type SourceFreshnessStatus = typeof sourceFreshnessStatusValues[number];
14
+ export type SourceStatusId = "claude-code" | "codex" | "openai" | "anthropic" | "cursor" | "github-copilot";
15
+ export type SourceStatusDefinition = {
16
+ id: SourceStatusId;
17
+ label: string;
18
+ validationCoverage: SourceValidationCoverage;
19
+ validationNote: string;
20
+ staleAfterHours: number;
21
+ };
22
+ export type SourceStatusObservation = {
23
+ id: SourceStatusId;
24
+ financialEvidence: FinancialEvidenceStatus;
25
+ financialEvidenceNote: string;
26
+ /** When this source was last read or synced, even if it returned no records. */
27
+ checkedAt?: string;
28
+ /** Timestamp of the newest evidence row, which may precede the sync time. */
29
+ latestEvidenceAt?: string;
30
+ /** A sanitized error from the latest recorded attempt, if one exists. */
31
+ lastError?: string;
32
+ /** Runtime failures may override the shipped validation baseline. */
33
+ validationCoverage?: SourceValidationCoverage;
34
+ };
35
+ export type SourceStatusFreshness = {
36
+ status: SourceFreshnessStatus;
37
+ checkedAt?: string;
38
+ latestEvidenceAt?: string;
39
+ staleAfterHours: number;
40
+ };
41
+ export type SourceStatus = {
42
+ id: SourceStatusId;
43
+ label: string;
44
+ validationCoverage: SourceValidationCoverage;
45
+ validationNote: string;
46
+ financialEvidence: FinancialEvidenceStatus;
47
+ financialEvidenceNote: string;
48
+ freshness: SourceStatusFreshness;
49
+ lastError?: string;
50
+ };
51
+ /**
52
+ * Shipped validation matrix. Keep these claims proof-conservative: a live
53
+ * authentication check is not the same as a non-empty end-to-end billing
54
+ * reconciliation.
55
+ */
56
+ export declare const sourceStatusDefinitions: readonly SourceStatusDefinition[];
57
+ export declare function buildSourceStatuses(observations?: readonly SourceStatusObservation[], now?: Date): SourceStatus[];
58
+ /**
59
+ * Reduce one source's current rows to the evidence label used for its headline.
60
+ * Verified billed cost wins; otherwise estimates win over an unpriced signal.
61
+ */
62
+ export declare function financialEvidenceForRecords(records: readonly UsageRecord[]): FinancialEvidenceStatus;
63
+ /** Stable, plain-text formatter shared by terminal surfaces. */
64
+ export declare function formatSourceStatuses(statuses: readonly SourceStatus[]): string;
65
+ //# sourceMappingURL=sourceStatus.d.ts.map
@@ -0,0 +1,147 @@
1
+ /**
2
+ * How thoroughly an ingestion path itself has been exercised.
3
+ *
4
+ * This is deliberately separate from financial evidence quality. A connector
5
+ * can be live-verified while a particular record is still only an estimate.
6
+ */
7
+ export const sourceValidationCoverageValues = [
8
+ "live_verified",
9
+ "fixture_verified",
10
+ "untested",
11
+ "failed"
12
+ ];
13
+ export const sourceFreshnessStatusValues = ["fresh", "stale", "not_checked"];
14
+ /**
15
+ * Shipped validation matrix. Keep these claims proof-conservative: a live
16
+ * authentication check is not the same as a non-empty end-to-end billing
17
+ * reconciliation.
18
+ */
19
+ export const sourceStatusDefinitions = [
20
+ {
21
+ id: "claude-code",
22
+ label: "Claude Code local logs",
23
+ validationCoverage: "live_verified",
24
+ validationNote: "Local transcript parsing is exercised against live logs; dollar values remain API-rate estimates.",
25
+ staleAfterHours: 72
26
+ },
27
+ {
28
+ id: "codex",
29
+ label: "Codex local logs",
30
+ validationCoverage: "live_verified",
31
+ validationNote: "Local parsing was replayed against live logs; total-only token shapes and unknown aliases remain missing rather than becoming estimated $0.",
32
+ staleAfterHours: 72
33
+ },
34
+ {
35
+ id: "openai",
36
+ label: "OpenAI Costs and Usage API",
37
+ validationCoverage: "live_verified",
38
+ validationNote: "Product connector QA exercised non-empty Admin cost and usage API paths; the tested Costs total reconciled to invoiced API credits less the provider-UI balance with $0.00 variance. This does not reconcile the current user's account; final invoices, tax, discounts, and later adjustments remain separate.",
39
+ staleAfterHours: 48
40
+ },
41
+ {
42
+ id: "anthropic",
43
+ label: "Anthropic Cost Report and Claude Code Analytics",
44
+ validationCoverage: "live_verified",
45
+ validationNote: "Admin cost and Claude Code usage paths are exercised with non-empty live records.",
46
+ staleAfterHours: 48
47
+ },
48
+ {
49
+ id: "cursor",
50
+ label: "Cursor Admin API",
51
+ validationCoverage: "fixture_verified",
52
+ validationNote: "Canonical teamMemberSpend envelopes, totalPages pagination, completeness failures, and malformed responses pass recorded fixtures; live account QA is pending.",
53
+ staleAfterHours: 48
54
+ },
55
+ {
56
+ id: "github-copilot",
57
+ label: "GitHub Copilot organization APIs",
58
+ validationCoverage: "fixture_verified",
59
+ validationNote: "The 2026-03-10 signed-NDJSON metrics workflow, seat pagination, per-seat plan types, and failure paths pass recorded fixtures; live account QA is pending.",
60
+ staleAfterHours: 48
61
+ }
62
+ ];
63
+ export function buildSourceStatuses(observations = [], now = new Date()) {
64
+ const byId = new Map(observations.map((observation) => [observation.id, observation]));
65
+ return sourceStatusDefinitions.map((definition) => {
66
+ const observation = byId.get(definition.id);
67
+ return {
68
+ id: definition.id,
69
+ label: definition.label,
70
+ validationCoverage: observation?.validationCoverage ?? definition.validationCoverage,
71
+ validationNote: definition.validationNote,
72
+ financialEvidence: observation?.financialEvidence ?? "missing",
73
+ financialEvidenceNote: observation?.financialEvidenceNote ?? "No current financial evidence was observed on this machine.",
74
+ freshness: sourceFreshness(definition, observation, now),
75
+ ...(observation?.lastError ? { lastError: observation.lastError } : {})
76
+ };
77
+ });
78
+ }
79
+ /**
80
+ * Reduce one source's current rows to the evidence label used for its headline.
81
+ * Verified billed cost wins; otherwise estimates win over an unpriced signal.
82
+ */
83
+ export function financialEvidenceForRecords(records) {
84
+ if (records.some((record) => record.costConfidence === "verified" && typeof record.amountUsd === "number")) {
85
+ return "verified";
86
+ }
87
+ if (records.some((record) => record.costConfidence === "estimated" && typeof record.amountUsd === "number")) {
88
+ return "estimated";
89
+ }
90
+ if (records.some((record) => record.costConfidence === "detected_unverified")) {
91
+ return "detected_unverified";
92
+ }
93
+ return "missing";
94
+ }
95
+ /** Stable, plain-text formatter shared by terminal surfaces. */
96
+ export function formatSourceStatuses(statuses) {
97
+ return statuses.map((status) => {
98
+ const freshness = formatFreshness(status.freshness);
99
+ return [
100
+ `${status.label} (${status.id})`,
101
+ ` validation coverage: ${status.validationCoverage}`,
102
+ ` financial evidence: ${status.financialEvidence}`,
103
+ ` freshness: ${freshness}`,
104
+ ` last error: ${status.lastError ? singleLineStatusText(status.lastError) : "none recorded"}`,
105
+ ` validation proof: ${status.validationNote}`,
106
+ ` evidence note: ${status.financialEvidenceNote}`
107
+ ].join("\n");
108
+ }).join("\n\n");
109
+ }
110
+ function sourceFreshness(definition, observation, now) {
111
+ const checkedAt = validIso(observation?.checkedAt);
112
+ const latestEvidenceAt = validIso(observation?.latestEvidenceAt);
113
+ if (!checkedAt) {
114
+ return {
115
+ status: "not_checked",
116
+ staleAfterHours: definition.staleAfterHours,
117
+ ...(latestEvidenceAt ? { latestEvidenceAt } : {})
118
+ };
119
+ }
120
+ const ageHours = (now.getTime() - Date.parse(checkedAt)) / 3_600_000;
121
+ return {
122
+ status: ageHours > definition.staleAfterHours ? "stale" : "fresh",
123
+ checkedAt,
124
+ ...(latestEvidenceAt ? { latestEvidenceAt } : {}),
125
+ staleAfterHours: definition.staleAfterHours
126
+ };
127
+ }
128
+ function validIso(value) {
129
+ return value && Number.isFinite(Date.parse(value)) ? value : undefined;
130
+ }
131
+ function formatFreshness(freshness) {
132
+ if (freshness.status === "not_checked")
133
+ return "not_checked (no local check recorded)";
134
+ const latest = freshness.latestEvidenceAt
135
+ ? `; latest evidence ${freshness.latestEvidenceAt}`
136
+ : "; no evidence rows observed";
137
+ return `${freshness.status} (checked ${freshness.checkedAt}${latest}; stale after ${freshness.staleAfterHours}h)`;
138
+ }
139
+ function singleLineStatusText(value) {
140
+ return value
141
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
142
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
143
+ .replace(/\s+/g, " ")
144
+ .trim()
145
+ .slice(0, 500) || "invalid empty error";
146
+ }
147
+ //# sourceMappingURL=sourceStatus.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A connected-provider spend file lives inside a repository and is therefore
3
+ * untrusted by default. A successful provider sync writes this small receipt
4
+ * outside the repository, binding the exact spend.json bytes to this machine
5
+ * and canonical project root. Successful syncs also bind sources.json so a
6
+ * clone cannot forge validation or financial-evidence axes. Merely cloning
7
+ * committed state cannot create the external receipt.
8
+ */
9
+ export type ConnectedSpendTrustResult = {
10
+ trusted: true;
11
+ trustedAt: string;
12
+ spendSha256: string;
13
+ sourceRegistrySha256?: string;
14
+ } | {
15
+ trusted: false;
16
+ reason: "missing" | "mismatch" | "invalid";
17
+ message: string;
18
+ };
19
+ export type ConnectedSpendTrustOptions = {
20
+ /** Test/embedding override. Production callers normally use user state. */
21
+ trustDirectory?: string;
22
+ /** Exact sources.json bytes written by the same successful provider sync. */
23
+ sourceRegistryContents?: string;
24
+ };
25
+ export declare function writeConnectedSpendTrustReceipt(rootPath: string, exactSpendContents: string, options?: ConnectedSpendTrustOptions): Promise<void>;
26
+ export declare function verifyConnectedSpendTrustReceipt(rootPath: string, exactSpendContents: string, options?: ConnectedSpendTrustOptions): Promise<ConnectedSpendTrustResult>;
27
+ /**
28
+ * Provider/source truth axes are trusted only when the same external receipt
29
+ * binds both the connected spend state and the exact persisted sources.json.
30
+ * A repository clone or edit can therefore register a boundary, but cannot
31
+ * self-assert live validation or verified financial evidence.
32
+ */
33
+ export declare function verifyConnectedSourceRegistryTrustReceipt(rootPath: string, exactSpendContents: string, exactSourceRegistryContents: string, options?: ConnectedSpendTrustOptions): Promise<ConnectedSpendTrustResult>;
34
+ /** Remove stale trust whenever connected state is reset or replaced locally. */
35
+ export declare function invalidateConnectedSpendTrustReceipt(rootPath: string, options?: ConnectedSpendTrustOptions): Promise<void>;
36
+ export declare function connectedTrustFailureMessage(reason: string): string;
37
+ //# sourceMappingURL=stateTrust.d.ts.map