segreant 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSING.md +5 -3
  2. package/README.md +57 -19
  3. package/bin/segreant-statusline.mjs +20 -0
  4. package/bin/segreant.mjs +19 -8
  5. package/dist/alerts/detect.js +13 -0
  6. package/dist/billing/epistemic.js +4 -0
  7. package/dist/billing/importer.js +29 -10
  8. package/dist/billing/reconcile.js +122 -32
  9. package/dist/billing/scope.js +7 -0
  10. package/dist/budget/guard.js +33 -4
  11. package/dist/cli/allocCmd.js +73 -4
  12. package/dist/cli/askModelPrices.js +59 -0
  13. package/dist/cli/backupCmd.js +2 -1
  14. package/dist/cli/basisLines.js +45 -0
  15. package/dist/cli/billingCmd.js +68 -9
  16. package/dist/cli/capitalCmd.js +2 -1
  17. package/dist/cli/connectCmd.js +50 -9
  18. package/dist/cli/egressCmd.js +15 -3
  19. package/dist/cli/evidenceCmd.js +6 -3
  20. package/dist/cli/flags.js +68 -0
  21. package/dist/cli/homeCmd.js +117 -0
  22. package/dist/cli/importCmd.js +210 -54
  23. package/dist/cli/launchCmd.js +2 -1
  24. package/dist/cli/opsCmd.js +49 -2
  25. package/dist/cli/planCmd.js +181 -0
  26. package/dist/cli/pricingDeclareCmd.js +178 -0
  27. package/dist/cli/quotaCmd.js +190 -0
  28. package/dist/cli/runCmd.js +203 -51
  29. package/dist/cli/showCmd.js +37 -11
  30. package/dist/cli/teamCmd.js +5 -5
  31. package/dist/cli/ui.js +24 -8
  32. package/dist/cli/valueCmd.js +189 -14
  33. package/dist/cli.js +102 -21
  34. package/dist/config.js +95 -6
  35. package/dist/connect/antigravity.js +310 -0
  36. package/dist/connect/claudeCode.js +43 -3
  37. package/dist/connect/codex.js +38 -1
  38. package/dist/connect/importShared.js +58 -5
  39. package/dist/connect/opencode.js +37 -9
  40. package/dist/connect/protobuf.js +126 -0
  41. package/dist/connect/registry.js +46 -0
  42. package/dist/connect/repoRoot.js +112 -0
  43. package/dist/cost/basis.js +168 -0
  44. package/dist/cost/declarations.js +37 -0
  45. package/dist/cost/exactPricing.js +4 -1
  46. package/dist/cost/firstSight.js +98 -0
  47. package/dist/cost/overrides.js +230 -0
  48. package/dist/cost/pricing.js +228 -15
  49. package/dist/cost/reprice.js +130 -0
  50. package/dist/dashboard/claim-support.js +11 -8
  51. package/dist/dashboard/contracts.js +23 -3
  52. package/dist/dashboard/routes.js +980 -112
  53. package/dist/dashboard/sessions.js +345 -0
  54. package/dist/dashboard/settings.js +2 -1
  55. package/dist/dashboard/value-cache.js +105 -0
  56. package/dist/dashboard/web/app/components/claimInspector.js +6 -5
  57. package/dist/dashboard/web/app/components/drawer.js +105 -35
  58. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  59. package/dist/dashboard/web/app/components/spine.js +54 -24
  60. package/dist/dashboard/web/app/components/workflow.js +82 -0
  61. package/dist/dashboard/web/app/core/actions.js +734 -23
  62. package/dist/dashboard/web/app/core/api.js +37 -1
  63. package/dist/dashboard/web/app/core/chain.js +11 -4
  64. package/dist/dashboard/web/app/core/claimLayers.js +179 -51
  65. package/dist/dashboard/web/app/core/claimTypes.js +30 -0
  66. package/dist/dashboard/web/app/core/fmt.js +19 -2
  67. package/dist/dashboard/web/app/core/generated-contract.js +23 -3
  68. package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
  69. package/dist/dashboard/web/app/core/generated-types.js +1 -1
  70. package/dist/dashboard/web/app/core/griffin.js +10 -0
  71. package/dist/dashboard/web/app/core/jobs.js +12 -0
  72. package/dist/dashboard/web/app/core/period.js +35 -0
  73. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  74. package/dist/dashboard/web/app/core/periods.js +32 -0
  75. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  76. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  77. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  78. package/dist/dashboard/web/app/core/registry.js +79 -46
  79. package/dist/dashboard/web/app/core/slug.js +9 -0
  80. package/dist/dashboard/web/app/core/theme.js +48 -0
  81. package/dist/dashboard/web/app/core/view.js +69 -0
  82. package/dist/dashboard/web/app/main.js +197 -15
  83. package/dist/dashboard/web/app/views/allocation.js +249 -61
  84. package/dist/dashboard/web/app/views/control.js +57 -16
  85. package/dist/dashboard/web/app/views/data.js +4 -4
  86. package/dist/dashboard/web/app/views/evidence.js +336 -87
  87. package/dist/dashboard/web/app/views/privacy.js +13 -0
  88. package/dist/dashboard/web/app/views/spend.js +30 -15
  89. package/dist/dashboard/web/app/views/statement.js +189 -0
  90. package/dist/dashboard/web/app/views/system.js +44 -4
  91. package/dist/dashboard/web/app/views/value.js +105 -59
  92. package/dist/dashboard/web/app/views/workbench.js +312 -0
  93. package/dist/dashboard/web/classic.html +16 -4
  94. package/dist/dashboard/web/index.html +1 -1
  95. package/dist/dashboard/web/styles/app.css +536 -23
  96. package/dist/demo/seed.js +153 -14
  97. package/dist/economics/ledger.js +1 -1
  98. package/dist/egress/policy.js +129 -5
  99. package/dist/export/allocationCsv.js +70 -0
  100. package/dist/export/billingCsv.js +6 -2
  101. package/dist/export/csv.js +10 -2
  102. package/dist/export/economic.js +6 -2
  103. package/dist/export/statementCsv.js +104 -0
  104. package/dist/git/correlate.js +86 -9
  105. package/dist/git/quality.js +107 -24
  106. package/dist/git/repoScope.js +207 -0
  107. package/dist/git/survivalStore.js +186 -0
  108. package/dist/guide.js +9 -4
  109. package/dist/plans/detect.js +147 -0
  110. package/dist/proxy/server.js +134 -14
  111. package/dist/proxy/usage.js +9 -4
  112. package/dist/quota/limits.js +138 -0
  113. package/dist/quota/statusline.js +68 -0
  114. package/dist/quota/view.js +122 -0
  115. package/dist/scan/knownApps.js +6 -2
  116. package/dist/scan/scan.js +9 -0
  117. package/dist/store/allocation.js +14 -0
  118. package/dist/store/db.js +259 -34
  119. package/dist/store/realization.js +78 -14
  120. package/dist/store/schema.js +59 -1
  121. package/dist/store/scopeEvidence.js +67 -0
  122. package/dist/util/pool.js +19 -0
  123. package/dist/util/time.js +7 -0
  124. package/dist/value/realization.js +321 -30
  125. package/dist/value/report.js +28 -8
  126. package/dist/value/selfReported.js +10 -9
  127. package/docs/ARCHITECTURE.md +8 -4
  128. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
  129. package/docs/CLI-JSON-CONTRACT.md +38 -0
  130. package/docs/DATA-BOUNDARIES.md +66 -6
  131. package/docs/FAQ.md +43 -9
  132. package/docs/GETTING-STARTED.md +19 -12
  133. package/docs/GUIDE.md +81 -18
  134. package/docs/INTEGRATIONS.md +9 -9
  135. package/docs/METHODOLOGY.md +3 -2
  136. package/docs/NEUTRALITY.md +1 -1
  137. package/docs/PRIVACY.md +51 -0
  138. package/docs/README.md +1 -0
  139. package/docs/RELEASE-GATE.md +52 -7
  140. package/docs/THREAT-MODEL.md +2 -1
  141. package/package.json +3 -2
  142. package/pricing/models.json +1366 -60
@@ -179,6 +179,20 @@ export function allocationRuns(db, limit) {
179
179
  result: JSON.parse(String(row.resultJson)),
180
180
  }));
181
181
  }
182
+ /** One recorded run, exactly as stored, or null when no run has that id. */
183
+ export function allocationRunById(db, allocationRunId) {
184
+ const row = db
185
+ .prepare(`SELECT allocation_run_id AS allocationRunId, computed_at_ms AS computedAtMs, result_json AS resultJson
186
+ FROM allocation_runs WHERE allocation_run_id = ?`)
187
+ .get(allocationRunId);
188
+ if (row === undefined)
189
+ return null;
190
+ return {
191
+ allocationRunId: String(row.allocationRunId),
192
+ computedAtMs: Number(row.computedAtMs),
193
+ result: JSON.parse(String(row.resultJson)),
194
+ };
195
+ }
182
196
  function exactRunId(serialized) {
183
197
  return `economic:allocation:${serialized.digest.slice('sha256:'.length)}`;
184
198
  }
package/dist/store/db.js CHANGED
@@ -27,6 +27,7 @@ import { canonicalJson } from '../epistemic/serialization.js';
27
27
  import { RESOURCE_LIMITS } from '../util/resource-limits.js';
28
28
  import { transact, writeBatch } from '../util/transaction.js';
29
29
  import { ATTRIBUTION_BASES } from '../value/characterization.js';
30
+ import { isOnDeclaredRoute, quantizeLocalDays } from '../billing/reconcile.js';
30
31
  import { buildExactAllocationKernelIssuance } from '../alloc/epistemic.js';
31
32
  import { bindCodingRealizationSuccessor, buildCodingRealizationKernelIssuance, codingRealizationKernelEligible } from '../value/epistemic.js';
32
33
  import * as allocation from './allocation.js';
@@ -58,10 +59,31 @@ const EXACT_SPEND_CACHE_KEYS = 64;
58
59
  * "+275760-...", which sorts BEFORE every ordinary timestamp.
59
60
  */
60
61
  const MAX_DATE_MS = Date.UTC(9999, 11, 31, 23, 59, 59, 999);
62
+ /** Every RequestRow column, alias-canonical label included; callers add WHERE and ORDER BY. */
63
+ const REQUEST_ROW_SELECT = `SELECT request_id AS requestId, session_id AS sessionId, ts_epoch_ms AS tsEpochMs,
64
+ provider, model, r.project AS project,
65
+ COALESCE(a.canonical, r.project) AS projectCanonical, task_weight AS taskWeight,
66
+ input_tokens AS inputTokens, output_tokens AS outputTokens,
67
+ cache_write_tokens AS cacheWriteTokens, cache_read_tokens AS cacheReadTokens,
68
+ reasoning_tokens AS reasoningTokens, cost_usd AS costUsd,
69
+ estimated, streamed, status_code AS statusCode, duration_ms AS durationMs, user, source, cwd, via,
70
+ cost_basis AS costBasis, rate_card_sha256 AS rateCardSha256,
71
+ rate_card_source_kind AS rateCardSourceKind, rate_match_kind AS rateMatchKind,
72
+ rate_match_provider AS rateMatchProvider, rate_match_model AS rateMatchModel,
73
+ scope_capture_status AS scopeCaptureStatus,
74
+ provider_scope_declaration_id AS providerScopeDeclarationId,
75
+ attribution_basis AS attributionBasis,
76
+ capture_coverage AS captureCoverage, cache_write_ttl AS cacheWriteTtlRecorded
77
+ FROM requests r LEFT JOIN project_aliases a ON a.alias = r.project`;
61
78
  /**
62
79
  * Merge a few new ids into an already sorted list, in the same order
63
80
  * `Array.prototype.sort()` gives strings. Re-sorting the whole list on every
64
81
  * extension made each proxied request cost O(n log n) in the day's events.
82
+ *
83
+ * Cost note: the merge still copies the whole cached id list on every extension,
84
+ * so the memory copied per append grows with the events a projection retains.
85
+ * That growth is not O(1) and is not asserted as such; it is measured separately.
86
+ * The work counters in exactSpendWorkCounters() count rows read, not bytes copied.
65
87
  */
66
88
  function mergeSortedIds(sorted, added) {
67
89
  if (added.length === 0)
@@ -83,10 +105,15 @@ function mergeSortedIds(sorted, added) {
83
105
  const REQUEST_CHARGE_KINDS = new Set(['charge_estimated', 'provider_charge_observed', 'bill_observed']);
84
106
  const SCOPE_CAPTURE_STATUSES = ['legacy_unknown', 'unscoped', 'declared_unverified', 'not_observed'];
85
107
  const CAPTURE_COVERAGES = ['complete', 'truncated', 'unknown', 'legacy_unknown'];
108
+ const CACHE_WRITE_TTLS = ['5m', '1h', 'not_recorded', 'legacy_unknown'];
86
109
  function requestRowFromRecord(record) {
87
- const { costBasis, rateCardSha256, rateCardSourceKind, rateMatchKind, rateMatchProvider, rateMatchModel, ...row } = record;
110
+ const { costBasis, rateCardSha256, rateCardSourceKind, rateMatchKind, rateMatchProvider, rateMatchModel, cacheWriteTtlRecorded, ...row } = record;
111
+ // 'not_recorded' and 'legacy_unknown' read as absent; any other string is
112
+ // damage (D-251) and refuses rather than passing through.
113
+ const ttl = vocabularyValue(cacheWriteTtlRecorded, CACHE_WRITE_TTLS, 'cacheWriteTtl', 'legacy_unknown');
88
114
  return {
89
115
  ...row,
116
+ ...(ttl === '5m' || ttl === '1h' ? { cacheWriteTtl: ttl } : {}),
90
117
  estimated: Boolean(record.estimated),
91
118
  streamed: Boolean(record.streamed),
92
119
  pricing: pricingEvidenceFromRecord(record),
@@ -149,6 +176,8 @@ export class Store {
149
176
  epistemicLedger;
150
177
  economicLedger;
151
178
  exactSpendCache = new Map();
179
+ /** Diagnostic work counters for the exact-spend readers; see exactSpendWorkCounters(). */
180
+ exactSpendWork = { fullProjections: 0, incrementalRowsVisited: 0 };
152
181
  requestMarks = null;
153
182
  migrationBackupEvidence = null;
154
183
  constructor(path) {
@@ -298,14 +327,47 @@ export class Store {
298
327
  activeBatch = null;
299
328
  /** The size and mtime a tool log file had when it was last imported completely. */
300
329
  importFileCursor(source, path) {
301
- const row = prepared(this.db, 'SELECT size, mtime_ms AS mtimeMs, truncated_lines AS truncatedLines, truncated_rows AS truncatedRows FROM import_file_cursors WHERE source = ? AND path = ?').get(source, path);
330
+ const row = prepared(this.db, 'SELECT size, mtime_ms AS mtimeMs, truncated_lines AS truncatedLines, truncated_rows AS truncatedRows, reader_version AS readerVersion FROM import_file_cursors WHERE source = ? AND path = ?').get(source, path);
302
331
  return row ?? null;
303
332
  }
304
333
  /** Record that every row of this file, at this size and mtime, is in the ledger. */
305
334
  saveImportFileCursor(source, path, cursor) {
306
- prepared(this.db, `INSERT INTO import_file_cursors (source, path, size, mtime_ms, truncated_lines, truncated_rows, at_ms) VALUES (?,?,?,?,?,?,?)
335
+ prepared(this.db, `INSERT INTO import_file_cursors (source, path, size, mtime_ms, truncated_lines, truncated_rows, reader_version, at_ms) VALUES (?,?,?,?,?,?,?,?)
307
336
  ON CONFLICT(source, path) DO UPDATE SET size = excluded.size, mtime_ms = excluded.mtime_ms,
308
- truncated_lines = excluded.truncated_lines, truncated_rows = excluded.truncated_rows, at_ms = excluded.at_ms`).run(source, path, cursor.size, Math.trunc(cursor.mtimeMs), cursor.truncatedLines, cursor.truncatedRows, Date.now());
337
+ truncated_lines = excluded.truncated_lines, truncated_rows = excluded.truncated_rows,
338
+ reader_version = excluded.reader_version, at_ms = excluded.at_ms`).run(source, path, cursor.size, Math.trunc(cursor.mtimeMs), cursor.truncatedLines, cursor.truncatedRows, cursor.readerVersion, Date.now());
339
+ }
340
+ /** Record that a session reported making a commit. The first observation of a sha in a session stands. */
341
+ recordObservedCommit(o) {
342
+ prepared(this.db, `INSERT INTO observed_commits (source, session_id, short_sha, branch, subject, ts_epoch_ms, cwd, extraction) VALUES (?,?,?,?,?,?,?,'output_start')
343
+ ON CONFLICT(source, session_id, short_sha) DO NOTHING`).run(o.source, o.sessionId, o.shortSha, o.branch, o.subject, Math.trunc(o.tsEpochMs), o.cwd);
344
+ this.activeBatch?.tick();
345
+ }
346
+ /** Record what a vendor's meter said at an instant. The first record of an instant stands. */
347
+ recordQuotaEvent(e) {
348
+ prepared(this.db, `INSERT INTO quota_events (source, kind, ts_epoch_ms, used_percent, window_minutes, resets_at_ms, detail)
349
+ VALUES (?,?,?,?,?,?,?) ON CONFLICT(source, kind, ts_epoch_ms) DO NOTHING`).run(e.source, e.kind, Math.trunc(e.tsEpochMs), e.usedPercent, e.windowMinutes, e.resetsAtMs === null ? null : Math.trunc(e.resetsAtMs), e.detail);
350
+ this.activeBatch?.tick();
351
+ }
352
+ /** List cost the ledger holds for one source (tool) in [startMs, endMs). */
353
+ sourceCostBetween(source, startMs, endMs) {
354
+ const row = prepared(this.db, 'SELECT COALESCE(SUM(cost_usd),0) AS c FROM requests WHERE source = ? AND ts_epoch_ms >= ? AND ts_epoch_ms < ?').get(source, Math.trunc(startMs), Math.trunc(endMs));
355
+ return row.c;
356
+ }
357
+ /** The newest reading of one meter, or null. */
358
+ latestQuotaEvent(source, kind) {
359
+ const row = prepared(this.db, `SELECT source, kind, ts_epoch_ms AS tsEpochMs, used_percent AS usedPercent, window_minutes AS windowMinutes,
360
+ resets_at_ms AS resetsAtMs, detail FROM quota_events WHERE source = ? AND kind = ? ORDER BY ts_epoch_ms DESC LIMIT 1`).get(source, kind);
361
+ return row === undefined ? null : { ...row };
362
+ }
363
+ /** Vendor meter events at or after `sinceMs`, oldest first. */
364
+ quotaEvents(sinceMs = 0) {
365
+ return prepared(this.db, `SELECT source, kind, ts_epoch_ms AS tsEpochMs, used_percent AS usedPercent, window_minutes AS windowMinutes,
366
+ resets_at_ms AS resetsAtMs, detail FROM quota_events WHERE ts_epoch_ms >= ? ORDER BY ts_epoch_ms`).all(sinceMs).map((row) => ({ ...row }));
367
+ }
368
+ /** Every commit observation on record (one row per session and sha). */
369
+ observedCommits() {
370
+ return prepared(this.db, 'SELECT source, session_id AS sessionId, short_sha AS shortSha, branch, subject, ts_epoch_ms AS tsEpochMs, cwd FROM observed_commits ORDER BY ts_epoch_ms').all().map((row) => ({ ...row }));
309
371
  }
310
372
  /**
311
373
  * The numeric request column remains a compatibility projection while an
@@ -349,9 +411,9 @@ export class Store {
349
411
  task_weight, input_tokens, output_tokens, cache_write_tokens, cache_read_tokens,
350
412
  reasoning_tokens, cost_usd, estimated, streamed, status_code, duration_ms, user, source, cwd, via,
351
413
  cost_basis, rate_card_sha256, rate_card_source_kind, rate_match_kind, rate_match_provider, rate_match_model,
352
- scope_capture_status, provider_scope_declaration_id, attribution_basis, capture_coverage
353
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)${idempotent ? ' ON CONFLICT(request_id) DO NOTHING' : ''}`;
354
- const info = prepared(this.db, sql).run(row.requestId, row.sessionId, new Date(row.tsEpochMs).toISOString(), row.tsEpochMs, row.provider, row.model, row.project, row.taskWeight, row.inputTokens, row.outputTokens, row.cacheWriteTokens, row.cacheReadTokens, row.reasoningTokens, costUsd, row.estimated ? 1 : 0, row.streamed ? 1 : 0, row.statusCode, row.durationMs, row.user ?? null, row.source ?? null, row.cwd ?? null, row.via ?? 'proxy', pricing.costBasis, pricing.rateCardSha256, pricing.rateCardSourceKind, pricing.rateMatchKind, pricing.rateMatchProvider, pricing.rateMatchModel, scope.status, scope.declarationId, row.attributionBasis ?? 'legacy_unknown', row.captureCoverage ?? 'complete');
414
+ scope_capture_status, provider_scope_declaration_id, attribution_basis, capture_coverage, cache_write_ttl
415
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)${idempotent ? ' ON CONFLICT(request_id) DO NOTHING' : ''}`;
416
+ const info = prepared(this.db, sql).run(row.requestId, row.sessionId, new Date(row.tsEpochMs).toISOString(), row.tsEpochMs, row.provider, row.model, row.project, row.taskWeight, row.inputTokens, row.outputTokens, row.cacheWriteTokens, row.cacheReadTokens, row.reasoningTokens, costUsd, row.estimated ? 1 : 0, row.streamed ? 1 : 0, row.statusCode, row.durationMs, row.user ?? null, row.source ?? null, row.cwd ?? null, row.via ?? 'proxy', pricing.costBasis, pricing.rateCardSha256, pricing.rateCardSourceKind, pricing.rateMatchKind, pricing.rateMatchProvider, pricing.rateMatchModel, scope.status, scope.declarationId, row.attributionBasis ?? 'legacy_unknown', row.captureCoverage ?? 'complete', row.cacheWriteTokens > 0 && (row.cacheWriteTtl === '5m' || row.cacheWriteTtl === '1h') ? row.cacheWriteTtl : 'not_recorded');
355
417
  const inserted = Number(info.changes ?? 0) > 0;
356
418
  if (row.economicAmount !== undefined) {
357
419
  if (inserted) {
@@ -418,6 +480,7 @@ export class Store {
418
480
  byModel: (startMs, endMs, project) => this.byModel(startMs, endMs, project),
419
481
  economicRequestRows: (startMs, endMs, project) => this.economicRequestRowsInRange(startMs, endMs, { project }),
420
482
  economicModelUnits: (startMs, endMs, project) => this.economicModelUnits(startMs, endMs, project),
483
+ requestsInRange: (startMs, endMs) => this.requestsInRange(startMs, endMs),
421
484
  economicLedger: this.economicLedger,
422
485
  };
423
486
  }
@@ -615,6 +678,14 @@ export class Store {
615
678
  // the traffic a cap can actually BLOCK. Imported subscription spend is sunk cost
616
679
  // observed after the fact; counting it toward enforcement froze live traffic in
617
680
  // dogfooding. Legacy NULL via reads as proxy (the conservative direction).
681
+ /**
682
+ * Requests Segreant refused before they reached the provider cost nothing and
683
+ * are not spend, so the exact projection neither counts them nor treats them
684
+ * as unresolved.
685
+ */
686
+ sentClause(alias = '') {
687
+ return ` AND COALESCE(${alias}cost_basis, '') <> 'not_sent'`;
688
+ }
618
689
  viaClause(liveOnly) {
619
690
  return liveOnly ? ` AND COALESCE(via,'proxy') = 'proxy'` : '';
620
691
  }
@@ -744,6 +815,30 @@ export class Store {
744
815
  this.requestMarks ??= new AppendMark(this.db, 'requests');
745
816
  return this.requestMarks.read();
746
817
  }
818
+ /**
819
+ * Change marks for every ledger table the value report reads: the highest
820
+ * rowid and the exact row count. Any append, removal or in-place replacement
821
+ * (a replaced row takes a new rowid) moves a mark, so a cached value report
822
+ * keyed on these marks cannot survive a change to what it was computed from.
823
+ * Exact counts, not AppendMark's bounded estimate, because a cache hit must
824
+ * never be served across a deletion; the cost is a few milliseconds, against
825
+ * the realization work it avoids.
826
+ */
827
+ valueChangeMarks() {
828
+ const mark = (table) => {
829
+ const row = prepared(this.db, `SELECT COALESCE(MAX(rowid), 0) AS rowid, COUNT(*) AS count FROM ${table}`)
830
+ .get();
831
+ return { rowid: Number(row.rowid), count: Number(row.count) };
832
+ };
833
+ return {
834
+ requests: mark('requests'),
835
+ request_price_events: mark('request_price_events'),
836
+ gate_signals: mark('gate_signals'),
837
+ realization_units: mark('realization_units'),
838
+ economic_events: mark('economic_events'),
839
+ project_aliases: mark('project_aliases'),
840
+ };
841
+ }
747
842
  /**
748
843
  * Add the rows and charge events appended since `cached` to its projection,
749
844
  * or return null when that would not equal a full re-projection: rows removed
@@ -761,12 +856,14 @@ export class Store {
761
856
  .all(cached.requestMark.rowid);
762
857
  if (requestMark.count - cached.requestMark.count !== appendedIds.length)
763
858
  return null;
859
+ this.exactSpendWork.incrementalRowsVisited += appendedIds.length;
764
860
  const newRequestIds = new Set(appendedIds.map((row) => row.requestId));
765
861
  // A window projection reads events occurring inside the window; a session
766
862
  // has no fixed window, so every appended event is read. That is at least
767
863
  // as inclusive as the full session projection, and only new events are read.
768
864
  const [startMs, endMs] = scope.kind === 'window' ? [scope.startMs, scope.endMs] : [0, MAX_DATE_MS];
769
865
  const events = this.economicLedger.eventsAppendedAfterInOccurrenceRange(cached.eventMark.rowid, startMs, endMs);
866
+ this.exactSpendWork.incrementalRowsVisited += events.length;
770
867
  for (const event of events) {
771
868
  const metadata = event.metadata;
772
869
  const requestId = metadata !== null && typeof metadata === 'object' && !Array.isArray(metadata)
@@ -780,11 +877,12 @@ export class Store {
780
877
  // NOT INDEXED keeps this a rowid range over the rows appended since the
781
878
  // mark; left to itself the planner walks the whole window's index.
782
879
  `SELECT request_id AS requestId, ts_epoch_ms AS tsEpochMs, via
783
- FROM requests NOT INDEXED WHERE rowid > ? AND ts_epoch_ms >= ? AND ts_epoch_ms < ?` + this.viaClause(liveOnly) + `
880
+ FROM requests NOT INDEXED WHERE rowid > ? AND ts_epoch_ms >= ? AND ts_epoch_ms < ?` + this.viaClause(liveOnly) + this.sentClause() + `
784
881
  ORDER BY ts_epoch_ms ASC, request_id ASC`).all(cached.requestMark.rowid, startMs, endMs)
785
882
  : prepared(this.db, `SELECT request_id AS requestId, ts_epoch_ms AS tsEpochMs, via
786
- FROM requests WHERE rowid > ? AND session_id = ?` + this.viaClause(liveOnly) + `
883
+ FROM requests WHERE rowid > ? AND session_id = ?` + this.viaClause(liveOnly) + this.sentClause() + `
787
884
  ORDER BY ts_epoch_ms ASC, request_id ASC`).all(cached.requestMark.rowid, scope.sessionId));
885
+ this.exactSpendWork.incrementalRowsVisited += rows.length;
788
886
  const delta = this.exactSpendFromRows(rows, startMs, endMs, liveOnly, events);
789
887
  const sourceBases = new Set([...cached.projection.sourceBases, ...delta.sourceBases]);
790
888
  return Object.freeze({
@@ -795,9 +893,25 @@ export class Store {
795
893
  unresolvedRequests: cached.projection.unresolvedRequests + delta.unresolvedRequests,
796
894
  });
797
895
  }
896
+ /**
897
+ * Diagnostic seam for tests, not an input to any figure. `fullProjections`
898
+ * counts whole-window or whole-session re-projections (cache misses, expiry,
899
+ * invalidation, and short sliding windows). `incrementalRowsVisited` counts
900
+ * the request ids, request rows and charge events that cache extensions read.
901
+ * Returns a copy; pair with resetExactSpendWorkCounters().
902
+ */
903
+ exactSpendWorkCounters() {
904
+ return { ...this.exactSpendWork };
905
+ }
906
+ /** Diagnostic seam for tests: zero the exact-spend work counters. */
907
+ resetExactSpendWorkCounters() {
908
+ this.exactSpendWork.fullProjections = 0;
909
+ this.exactSpendWork.incrementalRowsVisited = 0;
910
+ }
798
911
  exactSpendBetweenFull(startMs, endMs, liveOnly) {
912
+ this.exactSpendWork.fullProjections += 1;
799
913
  const rows = prepared(this.db, `SELECT request_id AS requestId, ts_epoch_ms AS tsEpochMs, via
800
- FROM requests WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ?` + this.viaClause(liveOnly) + `
914
+ FROM requests WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ?` + this.viaClause(liveOnly) + this.sentClause() + `
801
915
  ORDER BY ts_epoch_ms ASC, request_id ASC`).all(startMs, endMs);
802
916
  return this.exactSpendFromRows(rows, startMs, endMs, liveOnly);
803
917
  }
@@ -812,7 +926,7 @@ export class Store {
812
926
  const rows = prepared(this.db, `SELECT r.request_id AS requestId, r.ts_epoch_ms AS tsEpochMs, r.via
813
927
  FROM requests r
814
928
  WHERE r.ts_epoch_ms >= ? AND r.ts_epoch_ms < ?
815
- AND ${fam.sql}` + this.viaClause(liveOnly) + `
929
+ AND ${fam.sql}` + this.viaClause(liveOnly) + this.sentClause('r.') + `
816
930
  ORDER BY r.ts_epoch_ms ASC, r.request_id ASC`).all(startMs, endMs, ...fam.args);
817
931
  return this.exactSpendFromRows(rows, startMs, endMs, liveOnly);
818
932
  }
@@ -826,8 +940,9 @@ export class Store {
826
940
  return this.cachedExactSpend(`session:${liveOnly ? 'live' : 'all'}:${sessionId}`, { kind: 'session', sessionId }, liveOnly, () => this.exactSpendForSessionFull(sessionId, liveOnly));
827
941
  }
828
942
  exactSpendForSessionFull(sessionId, liveOnly) {
943
+ this.exactSpendWork.fullProjections += 1;
829
944
  const rows = prepared(this.db, `SELECT request_id AS requestId, ts_epoch_ms AS tsEpochMs, via
830
- FROM requests WHERE session_id = ?` + this.viaClause(liveOnly) + `
945
+ FROM requests WHERE session_id = ?` + this.viaClause(liveOnly) + this.sentClause() + `
831
946
  ORDER BY ts_epoch_ms ASC, request_id ASC`).all(sessionId);
832
947
  if (rows.length === 0) {
833
948
  return Object.freeze({ amount: money('0', 'USD', 'effective'), eventIds: Object.freeze([]), sourceBases: Object.freeze([]), requestCount: 0, unresolvedRequests: 0 });
@@ -852,10 +967,13 @@ export class Store {
852
967
  const row = this.db
853
968
  .prepare(`SELECT COALESCE(SUM(CASE WHEN status_code = 429 THEN 1 ELSE 0 END),0) AS blocked,
854
969
  COALESCE(SUM(CASE WHEN estimated = 1 THEN cost_usd ELSE 0 END),0) AS estCost,
970
+ COALESCE(SUM(CASE WHEN estimated = 0 AND cost_basis = 'user_declared_price' THEN cost_usd ELSE 0 END),0) AS declaredCost,
855
971
  COALESCE(SUM(cost_usd),0) AS total
856
972
  FROM requests WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ?`)
857
973
  .get(startMs, endMs);
858
- return { blocked: row.blocked, estimatedCostUsd: row.estCost, totalCostUsd: row.total };
974
+ // A declared price is resolved (so `estimated` is 0 and a reprice can write
975
+ // it) but it is not a list price; claim support counts it apart (PR #146 review).
976
+ return { blocked: row.blocked, estimatedCostUsd: row.estCost, declaredCostUsd: row.declaredCost, totalCostUsd: row.total };
859
977
  }
860
978
  /**
861
979
  * Local rate-card lineage grouped strictly by the evidence captured when each
@@ -1087,7 +1205,7 @@ export class Store {
1087
1205
  if (fam)
1088
1206
  args.push(...fam.args);
1089
1207
  const rows = this.db
1090
- .prepare(`SELECT DISTINCT cost_basis AS costBasis, rate_card_sha256 AS rateCardSha256
1208
+ .prepare(`SELECT DISTINCT cost_basis AS costBasis, rate_card_sha256 AS rateCardSha256, rate_match_model AS rateMatchModel
1091
1209
  FROM requests
1092
1210
  WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ? AND model = ?` +
1093
1211
  (provider !== undefined ? ` AND provider = ?` : ``) +
@@ -1099,8 +1217,10 @@ export class Store {
1099
1217
  bases.add(r.costBasis);
1100
1218
  // A null card is not a distinct revision — plenty of bases (tool-reported,
1101
1219
  // unpriced) legitimately have none. Only real revisions count as a span.
1220
+ // A declared `like` follows the card under the model it names, so a new
1221
+ // target on the same card is a new price revision (PR #146 review).
1102
1222
  if (r.rateCardSha256)
1103
- cards.add(r.rateCardSha256);
1223
+ cards.add(r.costBasis === 'user_declared_price' && r.rateMatchModel ? `${r.rateCardSha256}:${r.rateMatchModel}` : r.rateCardSha256);
1104
1224
  }
1105
1225
  return { costBases: [...bases].sort(), rateCardShas: [...cards].sort() };
1106
1226
  }
@@ -1241,37 +1361,98 @@ export class Store {
1241
1361
  requestsInRange(startMs, endMs) {
1242
1362
  // Carry the alias-canonical label alongside the raw one so an export totals
1243
1363
  // the same way `byProject` does without rewriting the recorded row.
1244
- const rows = this.db
1245
- .prepare(`SELECT request_id AS requestId, session_id AS sessionId, ts_epoch_ms AS tsEpochMs,
1246
- provider, model, r.project AS project,
1247
- COALESCE(a.canonical, r.project) AS projectCanonical, task_weight AS taskWeight,
1248
- input_tokens AS inputTokens, output_tokens AS outputTokens,
1249
- cache_write_tokens AS cacheWriteTokens, cache_read_tokens AS cacheReadTokens,
1250
- reasoning_tokens AS reasoningTokens, cost_usd AS costUsd,
1251
- estimated, streamed, status_code AS statusCode, duration_ms AS durationMs, user, source, cwd, via,
1252
- cost_basis AS costBasis, rate_card_sha256 AS rateCardSha256,
1253
- rate_card_source_kind AS rateCardSourceKind, rate_match_kind AS rateMatchKind,
1254
- rate_match_provider AS rateMatchProvider, rate_match_model AS rateMatchModel,
1255
- scope_capture_status AS scopeCaptureStatus,
1256
- provider_scope_declaration_id AS providerScopeDeclarationId,
1257
- attribution_basis AS attributionBasis,
1258
- capture_coverage AS captureCoverage
1259
- FROM requests r LEFT JOIN project_aliases a ON a.alias = r.project
1260
- WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ? ORDER BY ts_epoch_ms ASC`)
1261
- .all(startMs, endMs);
1364
+ // Streamed and mapped one record at a time: `.all()` held every raw record
1365
+ // alongside its mapped row, which put a 100,000-row month at 455 MiB RSS.
1366
+ const out = [];
1367
+ const stmt = this.db.prepare(`${REQUEST_ROW_SELECT} WHERE ts_epoch_ms >= ? AND ts_epoch_ms < ? ORDER BY ts_epoch_ms ASC`);
1368
+ for (const record of stmt.iterate(startMs, endMs))
1369
+ out.push(requestRowFromRecord(record));
1370
+ return out;
1371
+ }
1372
+ /**
1373
+ * The rows `requestsInRange(0, endMs)` would return that one self-reported
1374
+ * spend link names: one request, one session, or one tool over [fromMs, toMs).
1375
+ * Same columns, same order, read through an index instead of the whole
1376
+ * ledger. Matching by filtering every request ever recorded took the value
1377
+ * report from 320 MiB of heap to 864 MiB on a 300,000-request ledger.
1378
+ */
1379
+ requestsForSpendLink(link, endMs) {
1380
+ const range = 'ts_epoch_ms >= 0 AND ts_epoch_ms < ?';
1381
+ const order = 'ORDER BY ts_epoch_ms ASC';
1382
+ const rows = (link.type === 'request'
1383
+ ? this.db.prepare(`${REQUEST_ROW_SELECT} WHERE request_id = ? AND ${range} ${order}`).all(link.requestId, endMs)
1384
+ : link.type === 'session'
1385
+ ? this.db.prepare(`${REQUEST_ROW_SELECT} WHERE session_id = ? AND ${range} ${order}`).all(link.sessionId, endMs)
1386
+ : this.db.prepare(`${REQUEST_ROW_SELECT} WHERE source = ? AND ts_epoch_ms >= ? AND ts_epoch_ms < ? AND ${range} ${order}`)
1387
+ .all(link.tool, link.fromMs, link.toMs, endMs));
1262
1388
  return rows.map(requestRowFromRecord);
1263
1389
  }
1390
+ /**
1391
+ * The effective (exact-Money) rows for request rows the caller already holds.
1392
+ * Row by row, so it equals `economicRequestRowsInRange` over the same rows; a
1393
+ * caller slicing one read into many windows uses it instead of re-querying.
1394
+ */
1395
+ economicRowsOf(rows) {
1396
+ return effectiveRequestRows(rows, this.economicLedger);
1397
+ }
1264
1398
  /** Exact-safe request export rows with original/effective Money and lineage. */
1265
1399
  economicRequestsInRange(startMs, endMs, options = {}) {
1266
- return buildEconomicRequestExportRows(this.requestsInRange(startMs, endMs), this.economicLedger, options);
1400
+ // A request refused before it was sent cost nothing and has no economic
1401
+ // event; exporting it would print it as legacy_unknown with
1402
+ // no_exact_economic_event. Same rule as the economic read model (PR #143 review).
1403
+ const rows = this.requestsInRange(startMs, endMs).filter((row) => row.pricing?.costBasis !== 'not_sent');
1404
+ return buildEconomicRequestExportRows(rows, this.economicLedger, options);
1267
1405
  }
1268
1406
  /**
1269
1407
  * Exact request-level economic rows for value consumers. The optional project
1270
1408
  * filter follows the alias family used by `summary()`; `liveOnly` preserves
1271
1409
  * the budget distinction between proxy traffic and imported observations.
1272
1410
  */
1411
+ /**
1412
+ * Run `work` with identical economic row reads served once. The value report
1413
+ * read the same 30-day window three times (usage, cohort, self-reported),
1414
+ * each mapping every row through the economic ledger; on a real ledger that
1415
+ * was most of a 9-12 s dashboard read. The snapshot also makes the three
1416
+ * agree with each other, since a write landing mid-report no longer splits
1417
+ * them. Nested calls share the outer snapshot.
1418
+ */
1419
+ async withReadSnapshot(work) {
1420
+ if (this.readSnapshot !== null)
1421
+ return work();
1422
+ this.readSnapshot = new Map();
1423
+ try {
1424
+ return await work();
1425
+ }
1426
+ finally {
1427
+ this.readSnapshot = null;
1428
+ }
1429
+ }
1430
+ readSnapshot = null;
1273
1431
  economicRequestRowsInRange(startMs, endMs, options = {}) {
1432
+ const key = this.readSnapshot === null ? null : JSON.stringify([
1433
+ startMs, endMs, options.project ?? null, options.liveOnly === true,
1434
+ options.asOf ?? null, options.targetUnit ?? null, options.effectiveAt ?? null,
1435
+ ]);
1436
+ const hit = key === null ? undefined : this.readSnapshot.get(key);
1437
+ if (hit !== undefined)
1438
+ return hit.slice();
1439
+ // Inside a snapshot, proxy-only rows are the full read filtered, so the
1440
+ // full read is done once and both questions are answered from it.
1441
+ const out = key !== null && options.liveOnly === true
1442
+ ? this.economicRequestRowsInRange(startMs, endMs, { ...options, liveOnly: false }).filter((row) => row.via === 'proxy')
1443
+ : this.economicRequestRowsInRangeUncached(startMs, endMs, options);
1444
+ if (key !== null)
1445
+ this.readSnapshot.set(key, out.slice());
1446
+ return out;
1447
+ }
1448
+ economicRequestRowsInRangeUncached(startMs, endMs, options) {
1274
1449
  const rows = this.requestsInRange(startMs, endMs).filter((row) => {
1450
+ // A request refused before it was sent has no economic event because it
1451
+ // cost nothing; reading it here marked it no_exact_economic_event and made
1452
+ // attribution partial (PR #143 review). It is not spend, so it is skipped,
1453
+ // as the exact spend projection skips it.
1454
+ if (row.pricing?.costBasis === 'not_sent')
1455
+ return false;
1275
1456
  if (options.project !== undefined && row.projectCanonical !== this.canonicalProject(options.project))
1276
1457
  return false;
1277
1458
  if (options.liveOnly === true && (row.via ?? 'proxy') !== 'proxy')
@@ -1643,6 +1824,20 @@ export class Store {
1643
1824
  realizationProjects() {
1644
1825
  return realization.realizationProjects(this.db);
1645
1826
  }
1827
+ /**
1828
+ * Recorded guesses, grouped by historical match evidence, costliest first.
1829
+ * Current resolution belongs to the caller: a new card or declaration must
1830
+ * not reinterpret the evidence retained on these requests.
1831
+ */
1832
+ unresolvedModels() {
1833
+ return this.db.prepare(`SELECT provider, model, COUNT(*) AS requests, COALESCE(SUM(cost_usd),0) AS costUsd,
1834
+ rate_match_kind AS rateMatchKind
1835
+ FROM requests
1836
+ WHERE cost_basis != 'not_sent'
1837
+ AND (cost_basis = 'fallback_estimate' OR rate_match_kind IN ('family_provider', 'family_cross_provider'))
1838
+ GROUP BY provider, model, rate_match_kind
1839
+ ORDER BY costUsd DESC, provider, model, rate_match_kind`).all();
1840
+ }
1646
1841
  /** Every row priced with a fallback/family-match rate — the reprice candidates. */
1647
1842
  estimatedRequestRows() {
1648
1843
  return realization.estimatedRequestRows(this.db);
@@ -1998,6 +2193,33 @@ export class Store {
1998
2193
  // (D-173).
1999
2194
  this.retentionFloor().requestsPrunedBeforeMs);
2000
2195
  }
2196
+ /**
2197
+ * Everything Segreant metered in [startMs, endMs), every provider and route,
2198
+ * under the reconciliation's own day quantization, so a comparison's local
2199
+ * figure can be stated as a share of it without the two rounding apart.
2200
+ * Null when an amount cannot be quantized (a non-USD exact amount).
2201
+ */
2202
+ meteredMicrosLikeReconciliation(startMs, endMs, declaredScopeId) {
2203
+ // Both figures from one read of the ledger as it is now. The on-route part
2204
+ // is what the comparison recorded as its local figure; when it no longer
2205
+ // matches the record, the ledger has moved (a prune, a repricing) and a
2206
+ // share of the live total would mix two snapshots (PR #142 review).
2207
+ const rows = this.reconciliationRequestsInRange(startMs, endMs);
2208
+ const inPeriod = (row) => row.tsEpochMs >= startMs && row.tsEpochMs < endMs;
2209
+ const all = quantizeLocalDays(rows, inPeriod);
2210
+ const onRoute = quantizeLocalDays(rows, (row) => inPeriod(row) && isOnDeclaredRoute(row, declaredScopeId));
2211
+ if ('refusal' in all || 'refusal' in onRoute)
2212
+ return null;
2213
+ const sum = (q) => {
2214
+ if ('refusal' in q)
2215
+ return 0;
2216
+ let total = 0;
2217
+ for (const day of q.localByDay.values())
2218
+ total += day.micros;
2219
+ return total;
2220
+ };
2221
+ return { allMicros: sum(all), onRouteMicros: sum(onRoute) };
2222
+ }
2001
2223
  /** Persist a computed reconciliation as an immutable derived record. */
2002
2224
  saveReconciliationRun(result, computedAtMs = Date.now()) {
2003
2225
  return billing.saveReconciliationRun(this.db, result, computedAtMs);
@@ -2118,6 +2340,9 @@ export class Store {
2118
2340
  allocationRuns(limit = 20) {
2119
2341
  return allocation.allocationRuns(this.db, limit);
2120
2342
  }
2343
+ allocationRunById(allocationRunId) {
2344
+ return allocation.allocationRunById(this.db, allocationRunId);
2345
+ }
2121
2346
  /** Recorded reconciliation runs, newest first. */
2122
2347
  reconciliationRuns(limit = 20) {
2123
2348
  return billing.reconciliationRuns(this.db, limit);