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
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { runScript } from './schema.js';
|
|
19
19
|
import { pricingEvidenceFromRecord } from './rows.js';
|
|
20
|
+
import { formatMoneyAmount, money } from '../economics/money.js';
|
|
20
21
|
import { requestEconomicEventId } from '../economics/request.js';
|
|
21
22
|
import { priceCorrectionEvent } from '../economics/corrections.js';
|
|
22
23
|
import { economicAttributionFromRows, economicAttributionNumber } from '../economics/attribution.js';
|
|
23
24
|
import { canonicalModelAttribution } from './economicReadModel.js';
|
|
25
|
+
import { evidenceMatcher, modelSpendFromRows, parseScopeEvidence } from './scopeEvidence.js';
|
|
24
26
|
export function saveReceipt(db, r) {
|
|
25
27
|
db
|
|
26
28
|
.prepare(`INSERT INTO receipts (unit, project, ts_epoch_ms, realized, receipt_json)
|
|
@@ -90,13 +92,25 @@ export function realizationProjects(db) {
|
|
|
90
92
|
}
|
|
91
93
|
/** Every row priced with a fallback/family-match rate — the reprice candidates. */
|
|
92
94
|
export function estimatedRequestRows(db) {
|
|
93
|
-
|
|
95
|
+
const rows = db
|
|
94
96
|
.prepare(`SELECT request_id AS requestId, provider, model,
|
|
95
97
|
input_tokens AS inputTokens, output_tokens AS outputTokens,
|
|
96
98
|
cache_write_tokens AS cacheWriteTokens, cache_read_tokens AS cacheReadTokens,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
CASE WHEN cache_write_ttl IN ('5m', '1h') THEN cache_write_ttl END AS cacheWriteTtl,
|
|
100
|
+
cost_usd AS costUsd,
|
|
101
|
+
-- An unpriced row with no token counts has no captured usage to
|
|
102
|
+
-- price (a response cut off at the resource limit): repricing it
|
|
103
|
+
-- wrote a confirmed $0 list price over an unknown cost
|
|
104
|
+
-- (independent review, 2026-10-18). It stays in the report as an
|
|
105
|
+
-- untouched estimate; only the write plan skips it (PR #150
|
|
106
|
+
-- review). An unpriced row WITH token counts stays repriceable.
|
|
107
|
+
CASE WHEN cost_basis = 'unpriced'
|
|
108
|
+
AND input_tokens + output_tokens + cache_write_tokens + cache_read_tokens = 0
|
|
109
|
+
THEN 0 ELSE 1 END AS usageCaptured
|
|
110
|
+
FROM requests
|
|
111
|
+
WHERE estimated = 1 AND cost_basis <> 'not_sent'`)
|
|
99
112
|
.all();
|
|
113
|
+
return rows.map((r) => ({ ...r, usageCaptured: r.usageCaptured === 1 }));
|
|
100
114
|
}
|
|
101
115
|
/**
|
|
102
116
|
* Re-cost estimated rows in one transaction and retain the previous
|
|
@@ -113,10 +127,15 @@ export function estimatedRequestRows(db) {
|
|
|
113
127
|
* left marked stale for disclosure instead of being recomputed on a guess.
|
|
114
128
|
*/
|
|
115
129
|
export function applyRepricedCosts(db, deps, updates, appliedAtMs) {
|
|
116
|
-
const prior = db.prepare(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
const prior = db.prepare(
|
|
131
|
+
// camelCase, which is what pricingEvidenceFromRecord reads with no prefix.
|
|
132
|
+
// These were aliased CostBasis/RateCardSha256/..., so every reprice event
|
|
133
|
+
// recorded legacy_unknown as the provenance it replaced (independent review,
|
|
134
|
+
// 2026-10-18).
|
|
135
|
+
`SELECT cost_usd AS costUsd, estimated, ts_epoch_ms AS tsEpochMs, project,
|
|
136
|
+
cost_basis AS costBasis, rate_card_sha256 AS rateCardSha256,
|
|
137
|
+
rate_card_source_kind AS rateCardSourceKind, rate_match_kind AS rateMatchKind,
|
|
138
|
+
rate_match_provider AS rateMatchProvider, rate_match_model AS rateMatchModel
|
|
120
139
|
FROM requests WHERE request_id = ?`);
|
|
121
140
|
const update = db.prepare(`UPDATE requests
|
|
122
141
|
SET cost_usd = ?, estimated = 0,
|
|
@@ -131,6 +150,9 @@ export function applyRepricedCosts(db, deps, updates, appliedAtMs) {
|
|
|
131
150
|
new_rate_card_source_kind, new_rate_match_kind, new_rate_match_provider, new_rate_match_model
|
|
132
151
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
133
152
|
const touched = [];
|
|
153
|
+
// Which requests this transaction actually rewrote, so a caller reports
|
|
154
|
+
// those and not its plan (PR #146 review).
|
|
155
|
+
const writtenRequestIds = [];
|
|
134
156
|
runScript(db, 'BEGIN');
|
|
135
157
|
try {
|
|
136
158
|
for (const u of updates) {
|
|
@@ -143,11 +165,22 @@ export function applyRepricedCosts(db, deps, updates, appliedAtMs) {
|
|
|
143
165
|
throw new Error(`exact reprice for request ${u.requestId} requires an exact replacement amount`);
|
|
144
166
|
}
|
|
145
167
|
const recordedAt = new Date(appliedAtMs).toISOString();
|
|
168
|
+
// A correction keeps its source's basis: the economic ledger never
|
|
169
|
+
// moves an amount from one kind of evidence to another. An estimate
|
|
170
|
+
// repriced at a list rate is recorded as that estimate, corrected to the
|
|
171
|
+
// exact figure; the request row itself says which rate priced it. Without
|
|
172
|
+
// this, every fallback row on an exact ledger refused to reprice
|
|
173
|
+
// (PR #146 review). Re-labelling the other way would claim more
|
|
174
|
+
// certainty than the source event carries.
|
|
175
|
+
const source = exactSource.amount;
|
|
176
|
+
const nextAmount = u.economicAmount.basis === source.basis
|
|
177
|
+
? u.economicAmount
|
|
178
|
+
: money(formatMoneyAmount(u.economicAmount), u.economicAmount.currency, source.basis);
|
|
146
179
|
const correction = priceCorrectionEvent({
|
|
147
180
|
id: `economic:request:${u.requestId}:price-corrected`,
|
|
148
181
|
source: exactSource,
|
|
149
|
-
previousAmount:
|
|
150
|
-
nextAmount
|
|
182
|
+
previousAmount: source,
|
|
183
|
+
nextAmount,
|
|
151
184
|
recordedAt,
|
|
152
185
|
});
|
|
153
186
|
deps.economicLedger.appendWithinTransaction(correction);
|
|
@@ -158,10 +191,11 @@ export function applyRepricedCosts(db, deps, updates, appliedAtMs) {
|
|
|
158
191
|
continue;
|
|
159
192
|
event.run(u.requestId, 'reprice', appliedAtMs, Number(old.costUsd), Boolean(old.estimated) ? 1 : 0, oldPricing.costBasis, oldPricing.rateCardSha256, oldPricing.rateCardSourceKind, oldPricing.rateMatchKind, oldPricing.rateMatchProvider, oldPricing.rateMatchModel, u.costUsd, 0, u.pricing.costBasis, u.pricing.rateCardSha256, u.pricing.rateCardSourceKind, u.pricing.rateMatchKind, u.pricing.rateMatchProvider, u.pricing.rateMatchModel);
|
|
160
193
|
touched.push({ tsEpochMs: Number(old.tsEpochMs), project: String(old.project) });
|
|
194
|
+
writtenRequestIds.push(u.requestId);
|
|
161
195
|
}
|
|
162
196
|
const sync = syncRealizationCosts(db, deps, touched);
|
|
163
197
|
runScript(db, 'COMMIT');
|
|
164
|
-
return sync;
|
|
198
|
+
return { ...sync, writtenRequestIds };
|
|
165
199
|
}
|
|
166
200
|
catch (e) {
|
|
167
201
|
runScript(db, 'ROLLBACK');
|
|
@@ -234,7 +268,11 @@ function syncRealizationCosts(db, deps, repriced) {
|
|
|
234
268
|
if (scope === 'synthetic_demo')
|
|
235
269
|
continue;
|
|
236
270
|
const unitProject = canon(row.project);
|
|
237
|
-
const
|
|
271
|
+
const evidence = scope === 'project' ? parseScopeEvidence(unit.spendScope) : undefined;
|
|
272
|
+
// A unit scoped by evidence also absorbed linked sessions' and folders' spend
|
|
273
|
+
// under other labels, so any repriced request in its window may touch it.
|
|
274
|
+
const affected = priced.some((p) => p.tsEpochMs >= startMs && p.tsEpochMs < endMs
|
|
275
|
+
&& (scope !== 'project' || evidence !== undefined || p.project === unitProject));
|
|
238
276
|
if (!affected)
|
|
239
277
|
continue;
|
|
240
278
|
out.markedStale += 1;
|
|
@@ -243,10 +281,36 @@ function syncRealizationCosts(db, deps, repriced) {
|
|
|
243
281
|
out.unresolvable += 1;
|
|
244
282
|
continue;
|
|
245
283
|
}
|
|
284
|
+
if (evidence !== undefined && deps.requestsInRange === undefined) {
|
|
285
|
+
// The rule that produced these dollars cannot be re-run here: disclose.
|
|
286
|
+
markStale.run(row.commitHash);
|
|
287
|
+
out.unresolvable += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
246
290
|
const scoped = scope === 'project' ? row.project : undefined;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
291
|
+
let spend;
|
|
292
|
+
let modelSpend;
|
|
293
|
+
let economicRows;
|
|
294
|
+
if (evidence !== undefined) {
|
|
295
|
+
const linked = evidenceMatcher(evidence);
|
|
296
|
+
const rows = deps.requestsInRange(startMs, endMs)
|
|
297
|
+
.filter((r) => canon(r.projectCanonical ?? r.project) === unitProject || linked(r));
|
|
298
|
+
const ids = new Set(rows.map((r) => r.requestId));
|
|
299
|
+
spend = {
|
|
300
|
+
label: row.project,
|
|
301
|
+
costUsd: rows.reduce((s, r) => s + r.costUsd, 0),
|
|
302
|
+
requests: rows.length,
|
|
303
|
+
inputTokens: rows.reduce((s, r) => s + r.inputTokens, 0),
|
|
304
|
+
outputTokens: rows.reduce((s, r) => s + r.outputTokens, 0),
|
|
305
|
+
};
|
|
306
|
+
modelSpend = modelSpendFromRows(rows);
|
|
307
|
+
economicRows = deps.economicRequestRows?.(startMs, endMs, undefined).filter((r) => ids.has(r.requestId));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
spend = deps.summary(startMs, endMs, scoped);
|
|
311
|
+
modelSpend = deps.byModel(startMs, endMs, scoped);
|
|
312
|
+
economicRows = deps.economicRequestRows?.(startMs, endMs, scoped);
|
|
313
|
+
}
|
|
250
314
|
const economic = economicRows === undefined ? undefined : economicAttributionFromRows(economicRows);
|
|
251
315
|
const modelAuthority = economicRows === undefined ? undefined : canonicalModelAttribution(economicRows);
|
|
252
316
|
const totalLines = Number(unit.linesAdded ?? 0) + Number(unit.linesDeleted ?? 0);
|
package/dist/store/schema.js
CHANGED
|
@@ -59,7 +59,8 @@ CREATE TABLE IF NOT EXISTS requests (
|
|
|
59
59
|
scope_capture_status TEXT NOT NULL DEFAULT 'legacy_unknown',
|
|
60
60
|
provider_scope_declaration_id TEXT,
|
|
61
61
|
attribution_basis TEXT NOT NULL DEFAULT 'legacy_unknown',
|
|
62
|
-
capture_coverage TEXT NOT NULL DEFAULT 'legacy_unknown'
|
|
62
|
+
capture_coverage TEXT NOT NULL DEFAULT 'legacy_unknown',
|
|
63
|
+
cache_write_ttl TEXT NOT NULL DEFAULT 'legacy_unknown'
|
|
63
64
|
);
|
|
64
65
|
|
|
65
66
|
CREATE INDEX IF NOT EXISTS idx_requests_ts ON requests(ts_epoch_ms);
|
|
@@ -236,10 +237,46 @@ CREATE TABLE IF NOT EXISTS import_file_cursors (
|
|
|
236
237
|
mtime_ms INTEGER NOT NULL,
|
|
237
238
|
truncated_lines INTEGER NOT NULL DEFAULT 0,
|
|
238
239
|
truncated_rows INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
reader_version INTEGER NOT NULL DEFAULT 1,
|
|
239
241
|
at_ms INTEGER NOT NULL,
|
|
240
242
|
PRIMARY KEY (source, path)
|
|
241
243
|
);
|
|
242
244
|
|
|
245
|
+
-- A git commit an agent session reported making, read from the session's own
|
|
246
|
+
-- log (git prints "[branch sha] subject" when it creates a commit). This is an
|
|
247
|
+
-- observation, not yet a fact about any repository: realization accepts it only
|
|
248
|
+
-- when the sha exists in the repo, the subject matches and the times agree.
|
|
249
|
+
CREATE TABLE IF NOT EXISTS observed_commits (
|
|
250
|
+
source TEXT NOT NULL,
|
|
251
|
+
session_id TEXT NOT NULL,
|
|
252
|
+
short_sha TEXT NOT NULL,
|
|
253
|
+
branch TEXT NOT NULL,
|
|
254
|
+
subject TEXT NOT NULL,
|
|
255
|
+
ts_epoch_ms INTEGER NOT NULL,
|
|
256
|
+
cwd TEXT,
|
|
257
|
+
-- How the line was recognised. NULL rows came from a reader that matched a
|
|
258
|
+
-- commit line anywhere, code fixtures included. The migration drops them, and
|
|
259
|
+
-- the next import re-reads every log under the strict rule.
|
|
260
|
+
extraction TEXT,
|
|
261
|
+
PRIMARY KEY (source, session_id, short_sha)
|
|
262
|
+
);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_observed_commits_sha ON observed_commits(short_sha);
|
|
264
|
+
|
|
265
|
+
-- The vendors' own usage meters, as their tools log them (src/quota/limits.ts):
|
|
266
|
+
-- Codex's rate-limit percentages when they change, Claude's limit messages.
|
|
267
|
+
-- Evidence of what the vendor said, kept apart from Segreant's own count.
|
|
268
|
+
CREATE TABLE IF NOT EXISTS quota_events (
|
|
269
|
+
source TEXT NOT NULL,
|
|
270
|
+
kind TEXT NOT NULL,
|
|
271
|
+
ts_epoch_ms INTEGER NOT NULL,
|
|
272
|
+
used_percent REAL,
|
|
273
|
+
window_minutes INTEGER,
|
|
274
|
+
resets_at_ms INTEGER,
|
|
275
|
+
detail TEXT,
|
|
276
|
+
PRIMARY KEY (source, kind, ts_epoch_ms)
|
|
277
|
+
);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_quota_events_ts ON quota_events(ts_epoch_ms);
|
|
279
|
+
|
|
243
280
|
-- What retention deleted, so an absence can be told from a deletion.
|
|
244
281
|
--
|
|
245
282
|
-- prune used to run one DELETE, print a count, and leave nothing behind. A
|
|
@@ -1116,10 +1153,25 @@ function validateAppendOnlyTriggerAuthority(db) {
|
|
|
1116
1153
|
}
|
|
1117
1154
|
/** Idempotent schema migrations for DBs created before a column existed. */
|
|
1118
1155
|
function migrate(db) {
|
|
1156
|
+
// Commit observations made by the loose reader cannot be re-judged: the line
|
|
1157
|
+
// around each match was not kept. They are dropped once, and IMPORT_READER_
|
|
1158
|
+
// VERSION 4 makes the next import read every log again under the strict rule.
|
|
1159
|
+
const observedCols = db.prepare('PRAGMA table_info(observed_commits)').all();
|
|
1160
|
+
if (observedCols.length > 0 && !observedCols.some((c) => c.name === 'extraction')) {
|
|
1161
|
+
db.prepare('ALTER TABLE observed_commits ADD COLUMN extraction TEXT').run();
|
|
1162
|
+
db.prepare('DELETE FROM observed_commits WHERE extraction IS NULL').run();
|
|
1163
|
+
}
|
|
1119
1164
|
const cols = db.prepare('PRAGMA table_info(requests)').all();
|
|
1120
1165
|
if (!cols.some((c) => c.name === 'user')) {
|
|
1121
1166
|
db.prepare('ALTER TABLE requests ADD COLUMN user TEXT').run();
|
|
1122
1167
|
}
|
|
1168
|
+
if (!cols.some((c) => c.name === 'cache_write_ttl')) {
|
|
1169
|
+
// The cache-write duration ('5m' or '1h') a request was priced with;
|
|
1170
|
+
// 'not_recorded' when its source did not say. No backfill: an older row
|
|
1171
|
+
// reads 'legacy_unknown', and a reprice says how many rows it priced at the
|
|
1172
|
+
// 5-minute rate for that reason (PR #146 review).
|
|
1173
|
+
db.prepare("ALTER TABLE requests ADD COLUMN cache_write_ttl TEXT NOT NULL DEFAULT 'legacy_unknown'").run();
|
|
1174
|
+
}
|
|
1123
1175
|
if (!cols.some((c) => c.name === 'source')) {
|
|
1124
1176
|
db.prepare('ALTER TABLE requests ADD COLUMN source TEXT').run();
|
|
1125
1177
|
}
|
|
@@ -1219,6 +1271,12 @@ function migrate(db) {
|
|
|
1219
1271
|
// column exists to prevent. Unknown stays unknown.
|
|
1220
1272
|
db.prepare("ALTER TABLE openai_cost_observation_runs ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'legacy_unknown'").run();
|
|
1221
1273
|
}
|
|
1274
|
+
const cursorCols = db.prepare('PRAGMA table_info(import_file_cursors)').all();
|
|
1275
|
+
if (cursorCols.length > 0 && !cursorCols.some((c) => c.name === 'reader_version')) {
|
|
1276
|
+
// Cursors written before readers learned to capture commits: version 1, so
|
|
1277
|
+
// their files are read once more by the current reader.
|
|
1278
|
+
db.prepare('ALTER TABLE import_file_cursors ADD COLUMN reader_version INTEGER NOT NULL DEFAULT 1').run();
|
|
1279
|
+
}
|
|
1222
1280
|
const signalCols = db.prepare('PRAGMA table_info(gate_signals)').all();
|
|
1223
1281
|
if (!signalCols.some((c) => c.name === 'evidence_source')) {
|
|
1224
1282
|
db.prepare("ALTER TABLE gate_signals ADD COLUMN evidence_source TEXT NOT NULL DEFAULT 'manual'").run();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evidence a value snapshot's dollars were scoped by beyond its project
|
|
3
|
+
* label: the agent sessions and the folders that verifiably made commits in the
|
|
4
|
+
* repository (see git/repoScope.ts for how that evidence is accepted).
|
|
5
|
+
*
|
|
6
|
+
* A snapshot records the evidence its own window used (`unit.spendScope`), so a
|
|
7
|
+
* later reprice re-sums exactly the same rows by exactly the same rule instead
|
|
8
|
+
* of falling back to the label and quietly dropping the linked spend. Pure: no
|
|
9
|
+
* git, no I/O, so the store layer can apply it.
|
|
10
|
+
*/
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
/** Normalize a path for prefix comparison (Windows paths compare case-insensitively). */
|
|
13
|
+
export function normPath(p) {
|
|
14
|
+
const r = resolve(p).replace(/[\\/]+/g, '/').replace(/\/$/, '');
|
|
15
|
+
return process.platform === 'win32' ? r.toLowerCase() : r;
|
|
16
|
+
}
|
|
17
|
+
export function pathUnder(path, root) {
|
|
18
|
+
return path === root || path.startsWith(root + '/');
|
|
19
|
+
}
|
|
20
|
+
/** A matcher for rows the evidence covers (the label is checked by the caller). */
|
|
21
|
+
export function evidenceMatcher(ev) {
|
|
22
|
+
const sessions = new Set(ev.sessions);
|
|
23
|
+
const roots = ev.folders.map(normPath);
|
|
24
|
+
return (row) => {
|
|
25
|
+
if (row.sessionId !== null && sessions.has(row.sessionId))
|
|
26
|
+
return true;
|
|
27
|
+
if (row.cwd) {
|
|
28
|
+
const c = normPath(row.cwd);
|
|
29
|
+
if (roots.some((root) => pathUnder(c, root)))
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Read evidence back from a persisted snapshot; anything malformed is absent. */
|
|
36
|
+
export function parseScopeEvidence(value) {
|
|
37
|
+
if (value === null || typeof value !== 'object')
|
|
38
|
+
return undefined;
|
|
39
|
+
const v = value;
|
|
40
|
+
const strings = (x) => Array.isArray(x) && x.every((s) => typeof s === 'string') ? x : null;
|
|
41
|
+
const sessions = strings(v.sessions);
|
|
42
|
+
const folders = strings(v.folders);
|
|
43
|
+
if (sessions === null || folders === null)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (sessions.length === 0 && folders.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return { sessions, folders };
|
|
48
|
+
}
|
|
49
|
+
/** The `byModel` shape, grouped from already-scoped rows (same order: cost, descending). */
|
|
50
|
+
export function modelSpendFromRows(rows) {
|
|
51
|
+
const groups = new Map();
|
|
52
|
+
for (const r of rows) {
|
|
53
|
+
const key = `${r.provider}\0${r.model}`;
|
|
54
|
+
const g = groups.get(key) ?? {
|
|
55
|
+
provider: r.provider, label: r.model, costUsd: 0, requests: 0, inputTokens: 0, outputTokens: 0,
|
|
56
|
+
cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
57
|
+
};
|
|
58
|
+
g.costUsd += r.costUsd;
|
|
59
|
+
g.requests += 1;
|
|
60
|
+
g.inputTokens += r.inputTokens;
|
|
61
|
+
g.outputTokens += r.outputTokens;
|
|
62
|
+
g.cacheReadTokens += r.cacheReadTokens;
|
|
63
|
+
g.cacheWriteTokens += r.cacheWriteTokens;
|
|
64
|
+
groups.set(key, g);
|
|
65
|
+
}
|
|
66
|
+
return [...groups.values()].sort((a, b) => b.costUsd - a.costUsd);
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map over items with at most `limit` calls in flight, keeping input order.
|
|
3
|
+
*
|
|
4
|
+
* Most of what the value stage does per repository is waiting on a git child
|
|
5
|
+
* process, so a few at once finish far sooner than one after another, and a
|
|
6
|
+
* bound keeps a machine with fifty repositories from spawning fifty gits.
|
|
7
|
+
*/
|
|
8
|
+
export async function mapLimit(items, limit, fn) {
|
|
9
|
+
const out = new Array(items.length);
|
|
10
|
+
let next = 0;
|
|
11
|
+
const lane = async () => {
|
|
12
|
+
while (next < items.length) {
|
|
13
|
+
const i = next++;
|
|
14
|
+
out[i] = await fn(items[i], i);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, lane));
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Recorded evidence timestamps use zero as an unknown sentinel, never epoch. */
|
|
2
|
+
export function recordedTime(ms, dayOnly = false) {
|
|
3
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= 0 || ms > 8.64e15)
|
|
4
|
+
return 'unknown';
|
|
5
|
+
const iso = new Date(ms).toISOString();
|
|
6
|
+
return dayOnly ? iso.slice(0, 10) : iso;
|
|
7
|
+
}
|