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.
- package/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
package/dist/store/allocation.js
CHANGED
|
@@ -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,
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
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);
|