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
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { execFile } from 'node:child_process';
|
|
17
17
|
import { promisify } from 'node:util';
|
|
18
|
-
import { attributeCommits, isGitRepo, projectName } from '../git/correlate.js';
|
|
18
|
+
import { attributeCommits, commitCountSince, isGitRepo, projectName } from '../git/correlate.js';
|
|
19
19
|
import { isDemo } from '../config.js';
|
|
20
|
-
import { survivingLines, revertScan, prefetchSurvival } from '../git/quality.js';
|
|
20
|
+
import { survivingLines, revertScan, prefetchSurvival, survivalHead } from '../git/quality.js';
|
|
21
|
+
import { repoIdentity, repoSpendScope } from '../git/repoScope.js';
|
|
22
|
+
import { mapLimit } from '../util/pool.js';
|
|
23
|
+
import { modelSpendFromRows } from '../store/scopeEvidence.js';
|
|
21
24
|
import { revertCompletenessWitness } from '../git/completeness.js';
|
|
22
25
|
import { acceptanceForCommit } from './proposals.js';
|
|
23
26
|
import { GATE_LADDER, terminalRealizationBounds, serialRealization, aggregatePolarity, polarityFromVerdict, verdictFromPolarity, evaluateCodingOutcome, } from './gates.js';
|
|
@@ -93,6 +96,77 @@ function signalPolarity(signals, kind) {
|
|
|
93
96
|
return aggregatePolarity(observations);
|
|
94
97
|
}
|
|
95
98
|
export const CLEAN_COMPLETENESS_EVENT_TYPES = CODING_CLEAN_COMPLETENESS_EVENT_TYPES;
|
|
99
|
+
/** `Store.modelPricingBasis`, over rows already in scope (same rule: null cards are not revisions). */
|
|
100
|
+
function pricingLineageFromRows(rows, model, provider) {
|
|
101
|
+
const bases = new Set();
|
|
102
|
+
const cards = new Set();
|
|
103
|
+
for (const r of rows) {
|
|
104
|
+
if (r.model !== model || r.provider !== provider)
|
|
105
|
+
continue;
|
|
106
|
+
if (r.costBasis != null)
|
|
107
|
+
bases.add(r.costBasis);
|
|
108
|
+
if (r.rateCardSha256)
|
|
109
|
+
cards.add(r.rateCardSha256);
|
|
110
|
+
}
|
|
111
|
+
return { costBases: [...bases].sort(), rateCardShas: [...cards].sort() };
|
|
112
|
+
}
|
|
113
|
+
export function keptBucketOf(u) {
|
|
114
|
+
if (u.maturing)
|
|
115
|
+
return 'maturing';
|
|
116
|
+
const survived = u.funnel.results.find((r) => r.gate === 'survived')?.verdict;
|
|
117
|
+
// A fail projected from contradictory evidence is not a refutation: the
|
|
118
|
+
// waste rollup already files it under 'conflicted', and calling it "not
|
|
119
|
+
// kept" here settled what the evidence left open.
|
|
120
|
+
const definiteFail = u.funnel.results.some((r) => r.verdict === 'fail' && r.polarity !== 'conflicted');
|
|
121
|
+
const contested = (u.funnel.conflicts?.length ?? 0) > 0;
|
|
122
|
+
return u.reverted || definiteFail ? 'notKept' : contested ? 'contested' : survived === 'pass' ? 'kept' : 'unknown';
|
|
123
|
+
}
|
|
124
|
+
export function keptSummary(report) {
|
|
125
|
+
const out = {
|
|
126
|
+
kept: { units: 0, costUsd: 0 },
|
|
127
|
+
notKept: { units: 0, costUsd: 0 },
|
|
128
|
+
contested: { units: 0, costUsd: 0 },
|
|
129
|
+
unknown: { units: 0, costUsd: 0 },
|
|
130
|
+
maturing: { units: 0, costUsd: 0, nextVerdictMs: null },
|
|
131
|
+
};
|
|
132
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
133
|
+
for (const u of report.units) {
|
|
134
|
+
const cost = Number.isFinite(u.attributedCostUsd) ? u.attributedCostUsd : 0;
|
|
135
|
+
if (u.maturing) {
|
|
136
|
+
out.maturing.units += 1;
|
|
137
|
+
out.maturing.costUsd += cost;
|
|
138
|
+
// The unit's own window when it recorded one, as the Statement places it.
|
|
139
|
+
const due = u.tsEpochMs + (u.checkWindowDays ?? report.windowDays) * DAY_MS;
|
|
140
|
+
out.maturing.nextVerdictMs = out.maturing.nextVerdictMs === null ? due : Math.min(out.maturing.nextVerdictMs, due);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const bucket = out[keptBucketOf(u)];
|
|
144
|
+
bucket.units += 1;
|
|
145
|
+
bucket.costUsd += cost;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** The most commits one period scan measures (the newest first). */
|
|
150
|
+
export const MAX_PERIOD_COMMITS = 1500;
|
|
151
|
+
/** The value period the terminal commands cover by default. */
|
|
152
|
+
export const CLI_PERIOD_DAYS = 90;
|
|
153
|
+
/**
|
|
154
|
+
* The git budget for a terminal command. The 20 s default exists so a
|
|
155
|
+
* dashboard route answers instead of hanging (it once took 416 s); a command
|
|
156
|
+
* someone runs and watches, with a progress line, can spend longer to measure
|
|
157
|
+
* a real period, and what the budget does not reach is still reported unknown.
|
|
158
|
+
*/
|
|
159
|
+
export const CLI_GIT_BUDGET_MS = 180_000;
|
|
160
|
+
/**
|
|
161
|
+
* The time `scan` and `discover` give the value pass across ALL repositories
|
|
162
|
+
* together. Per-repository budgets alone stacked: a machine with dozens of
|
|
163
|
+
* repositories (agent worktrees count) spent 14 minutes and more. Largest spend
|
|
164
|
+
* goes first; a repository the stage does not reach is reported as not
|
|
165
|
+
* measured, with the command that measures it.
|
|
166
|
+
*/
|
|
167
|
+
export const SCAN_VALUE_BUDGET_MS = 120_000;
|
|
168
|
+
/** Time kept back from the stage deadline for scoring and writing each repository after its git work. */
|
|
169
|
+
export const STAGE_FINISH_RESERVE_MS = 15_000;
|
|
96
170
|
/**
|
|
97
171
|
* Build a gate result from its four-valued polarity, deriving the legacy
|
|
98
172
|
* verdict rather than accepting one. The projection lives in exactly one place
|
|
@@ -132,7 +206,10 @@ function cleanCompleteness(project, commitHash, commitTsEpochMs, observedAtMs, w
|
|
|
132
206
|
};
|
|
133
207
|
}
|
|
134
208
|
export async function computeRealization(store, repoPath, opts = {}) {
|
|
135
|
-
const limit = opts.limit
|
|
209
|
+
const limit = opts.limit
|
|
210
|
+
?? (opts.sinceDays !== undefined
|
|
211
|
+
? Math.max(1, Math.min(MAX_PERIOD_COMMITS, await commitCountSince(repoPath, opts.sinceDays)))
|
|
212
|
+
: 30);
|
|
136
213
|
const windowDays = opts.windowDays ?? 14;
|
|
137
214
|
// ONE budget across every commit's git work, not one per commit.
|
|
138
215
|
//
|
|
@@ -171,11 +248,16 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
171
248
|
// x-segreant-project); otherwise fall back to the project-blind window sum so a
|
|
172
249
|
// classic 'default'-tagged proxy store is unchanged. This is the bridge that
|
|
173
250
|
// makes native, no-proxy imported spend produce correct per-project RoI.
|
|
174
|
-
|
|
251
|
+
// Extended by evidence: folders and sessions that verifiably made commits in
|
|
252
|
+
// this repository (git/repoScope.ts). Without any such evidence the scope is
|
|
253
|
+
// the label alone, exactly as before.
|
|
254
|
+
const spendScope = opts.spendScope ?? await repoSpendScope(store, repoPath);
|
|
255
|
+
const projectScoped = store.hasProjectSpend(project) || spendScope.extended;
|
|
175
256
|
const attributions = await attributeCommits(store, repoPath, {
|
|
176
257
|
limit,
|
|
177
258
|
persist: opts.persist,
|
|
178
|
-
scopeProject: projectScoped ? project : undefined,
|
|
259
|
+
scopeProject: projectScoped && !spendScope.extended ? project : undefined,
|
|
260
|
+
scope: projectScoped && spendScope.extended ? spendScope : undefined,
|
|
179
261
|
});
|
|
180
262
|
const scan = await revertScan(repoPath, limit);
|
|
181
263
|
const reverted = scan.reverted;
|
|
@@ -197,11 +279,56 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
197
279
|
// the whole allowance before measuring anything — turning a scheduling
|
|
198
280
|
// accident into forty unknown gates.
|
|
199
281
|
gitDeadlineMs = Number.isFinite(gitBudgetMs) ? Date.now() + gitBudgetMs : undefined;
|
|
282
|
+
if (opts.gitDeadlineAtMs !== undefined) {
|
|
283
|
+
gitDeadlineMs = gitDeadlineMs === undefined ? opts.gitDeadlineAtMs : Math.min(gitDeadlineMs, opts.gitDeadlineAtMs);
|
|
284
|
+
}
|
|
200
285
|
// Warm the git caches for every unit in parallel; the loop below still
|
|
201
286
|
// measures (and decides measured/unmeasured) one unit at a time.
|
|
202
|
-
|
|
287
|
+
// Blame need not trace history older than the oldest commit measured here: a
|
|
288
|
+
// day before it keeps every measured commit's line count exact (blameAt).
|
|
289
|
+
const blameSinceSec = attributions.length > 0
|
|
290
|
+
? Math.floor(Math.min(...attributions.map((a) => a.tsEpochMs)) / 1000) - 24 * 60 * 60
|
|
291
|
+
: undefined;
|
|
292
|
+
// A budget that runs out leaves the rest unmeasured, so spend it where the
|
|
293
|
+
// dollars are: matured commits (the only ones survival can judge) by
|
|
294
|
+
// attributed cost, largest first, then the maturing ones. The report keeps
|
|
295
|
+
// the attribution order.
|
|
296
|
+
const position = new Map(attributions.map((a, i) => [a.hash, i]));
|
|
297
|
+
// With an evidence scope, every unit's model attribution reads the SAME
|
|
298
|
+
// scoped rows its dollars came from. Read them once for the span of all
|
|
299
|
+
// windows and slice per unit: two queries per commit, one of them mapping
|
|
300
|
+
// every row of the window (scoped or not) through the economic ledger, took
|
|
301
|
+
// most of a repository's measurement time on a real ledger.
|
|
302
|
+
let scopedAll = [];
|
|
303
|
+
let scopedAllTs = [];
|
|
304
|
+
if (spendScope.extended && attributions.length > 0) {
|
|
305
|
+
const lo = Math.min(...attributions.map((a) => a.windowStartMs));
|
|
306
|
+
const hi = Math.max(...attributions.map((a) => a.windowEndMs));
|
|
307
|
+
scopedAll = store.requestsInRange(lo, hi).filter((r) => spendScope.matches(r));
|
|
308
|
+
scopedAllTs = scopedAll.map((r) => r.tsEpochMs);
|
|
309
|
+
}
|
|
310
|
+
const atOrAfter = (t) => {
|
|
311
|
+
let lo = 0;
|
|
312
|
+
let hi = scopedAllTs.length;
|
|
313
|
+
while (lo < hi) {
|
|
314
|
+
const m = (lo + hi) >> 1;
|
|
315
|
+
if (scopedAllTs[m] < t)
|
|
316
|
+
lo = m + 1;
|
|
317
|
+
else
|
|
318
|
+
hi = m;
|
|
319
|
+
}
|
|
320
|
+
return lo;
|
|
321
|
+
};
|
|
322
|
+
const measureOrder = [...attributions].sort((a, b) => {
|
|
323
|
+
const am = now - a.tsEpochMs < windowMs ? 1 : 0;
|
|
324
|
+
const bm = now - b.tsEpochMs < windowMs ? 1 : 0;
|
|
325
|
+
return am - bm || b.attributedCostUsd - a.attributedCostUsd;
|
|
326
|
+
});
|
|
327
|
+
// One HEAD for the whole pass: every unit below is measured against it.
|
|
328
|
+
const passHead = await survivalHead(repoPath);
|
|
329
|
+
await prefetchSurvival(repoPath, measureOrder.map((a) => a.hash), gitDeadlineMs, 8, blameSinceSec, passHead);
|
|
203
330
|
const units = [];
|
|
204
|
-
for (const a of
|
|
331
|
+
for (const a of measureOrder) {
|
|
205
332
|
const ageDays = (now - a.tsEpochMs) / (24 * 60 * 60 * 1000);
|
|
206
333
|
const maturing = now - a.tsEpochMs < windowMs;
|
|
207
334
|
// Checked ONCE per unit and reused, so a unit is measured or unmeasured as a
|
|
@@ -211,17 +338,23 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
211
338
|
// were gathered.
|
|
212
339
|
const scanned = !gitBudgetSpent();
|
|
213
340
|
const survival = scanned
|
|
214
|
-
? await survivingLines(repoPath, a.hash, gitDeadlineMs)
|
|
215
|
-
: { added: 0, surviving: 0, measured: false };
|
|
341
|
+
? await survivingLines(repoPath, a.hash, gitDeadlineMs, blameSinceSec, passHead)
|
|
342
|
+
: { added: 0, surviving: 0, measured: false, filesWithAdditions: 0 };
|
|
216
343
|
const { added, surviving } = survival;
|
|
217
344
|
const survivalRatio = added > 0 ? Math.min(1, surviving / added) : 0;
|
|
218
345
|
if (!survival.measured)
|
|
219
346
|
unmeasuredSurvival += 1;
|
|
220
347
|
const isReverted = reverted.has(a.hash) || reverted.has(a.hash.slice(0, 7));
|
|
221
|
-
// proxy gates: proposals captured in this commit's attribution window
|
|
222
|
-
|
|
223
|
-
|
|
348
|
+
// proxy gates: proposals captured in this commit's attribution window.
|
|
349
|
+
// The full diff is read only when there is a proposal to compare it with:
|
|
350
|
+
// every use below is gated on one. Reading it for every commit cost one
|
|
351
|
+
// serial `git show` per unit (1,126 of them, about 56 s on Windows, for a
|
|
352
|
+
// ledger with no captured proposals at all).
|
|
224
353
|
const winProposals = store.proposalsInWindow(project, a.windowStartMs, a.windowEndMs);
|
|
354
|
+
const addedByFile = scanned && winProposals.length > 0
|
|
355
|
+
? await addedLinesByFile(repoPath, a.hash)
|
|
356
|
+
: new Map();
|
|
357
|
+
const committedPaths = [...addedByFile.keys()];
|
|
225
358
|
const proposalCaptureCoverage = !scanned || winProposals.length === 0
|
|
226
359
|
// Without this unit's committed paths there is nothing to match a proposal
|
|
227
360
|
// against, so capture coverage is unknown for the same reason an empty
|
|
@@ -315,7 +448,7 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
315
448
|
// this. Only the line and file counts come from the skipped calls, so the
|
|
316
449
|
// reason falls back to what the attribution already knew.
|
|
317
450
|
committed: gate('committed', 'supported', scanned
|
|
318
|
-
? `${added} lines in ${
|
|
451
|
+
? `${added} lines in ${survival.filesWithAdditions || a.filesChanged} files`
|
|
319
452
|
: `${a.filesChanged} files; line counts unmeasured (scan budget exhausted)`),
|
|
320
453
|
tested: gate('tested', signalPolarity(signals, 'tested'), 'CI/test signal'),
|
|
321
454
|
merged: gate('merged', signalPolarity(signals, 'merged'), 'merge signal'),
|
|
@@ -347,10 +480,20 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
347
480
|
// from another project's concurrent traffic. A partial exact window has no
|
|
348
481
|
// winner; a wholly legacy window retains a display-only compatibility label
|
|
349
482
|
// with null cost/share so the frontier cannot treat it as priceable evidence.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
483
|
+
// With an evidence-extended scope the model read takes the SAME rows the
|
|
484
|
+
// dollars came from (label, linked sessions, linked folders).
|
|
485
|
+
const scopedRows = spendScope.extended
|
|
486
|
+
? (a.windowEndMs > a.windowStartMs ? scopedAll.slice(atOrAfter(a.windowStartMs), atOrAfter(a.windowEndMs)) : [])
|
|
487
|
+
: null;
|
|
488
|
+
const unitSpendScope = scopedRows === null ? undefined : spendScope.evidenceFor(scopedRows);
|
|
489
|
+
const modelSpend = scopedRows !== null
|
|
490
|
+
? modelSpendFromRows(scopedRows)
|
|
491
|
+
: store.byModel(a.windowStartMs, a.windowEndMs, projectScoped ? project : undefined);
|
|
492
|
+
const economicModelRows = scopedRows !== null
|
|
493
|
+
? store.economicRowsOf(scopedRows)
|
|
494
|
+
: store.economicRequestRowsInRange(a.windowStartMs, a.windowEndMs, {
|
|
495
|
+
project: projectScoped ? project : undefined,
|
|
496
|
+
});
|
|
354
497
|
const modelAuthority = canonicalModelAttribution(economicModelRows);
|
|
355
498
|
const dominantProvider = modelAuthority.coverage === 'exact'
|
|
356
499
|
? modelAuthority.dominant?.provider ?? null
|
|
@@ -378,7 +521,9 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
378
521
|
let dominantModelCostBasis = null;
|
|
379
522
|
let dominantModelRateCard = null;
|
|
380
523
|
if (dominantProvider !== null && dominantModel !== null) {
|
|
381
|
-
const lineage =
|
|
524
|
+
const lineage = scopedRows !== null
|
|
525
|
+
? pricingLineageFromRows(scopedRows, dominantModel, dominantProvider)
|
|
526
|
+
: store.modelPricingBasis(a.windowStartMs, a.windowEndMs, dominantModel, projectScoped ? project : undefined, dominantProvider);
|
|
382
527
|
dominantModelCostBasis =
|
|
383
528
|
lineage.costBases.length === 1 ? lineage.costBases[0] : lineage.costBases.length > 1 ? 'mixed' : null;
|
|
384
529
|
dominantModelRateCard =
|
|
@@ -388,6 +533,7 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
388
533
|
...a,
|
|
389
534
|
ageDays,
|
|
390
535
|
maturing,
|
|
536
|
+
checkWindowDays: windowDays,
|
|
391
537
|
survivalRatio,
|
|
392
538
|
reverted: isReverted,
|
|
393
539
|
hadProposal,
|
|
@@ -405,10 +551,12 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
405
551
|
cleanCompleteness: completeness,
|
|
406
552
|
proposalCaptureCoverage,
|
|
407
553
|
...(contributionEvidence === undefined ? {} : { contributionEvidence }),
|
|
554
|
+
...(unitSpendScope === undefined ? {} : { spendScope: unitSpendScope }),
|
|
408
555
|
});
|
|
409
556
|
}
|
|
410
557
|
// Persist the snapshot so this realized-value picture can be served later
|
|
411
558
|
// without the repo (e.g. to a manager's dashboard). Keyed by commit hash.
|
|
559
|
+
units.sort((x, y) => (position.get(x.hash) ?? 0) - (position.get(y.hash) ?? 0));
|
|
412
560
|
if (opts.persist) {
|
|
413
561
|
store.saveRealizationUnits(units.map((u) => ({
|
|
414
562
|
commitHash: u.hash,
|
|
@@ -425,7 +573,7 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
425
573
|
costScope: projectScoped ? 'project' : 'window',
|
|
426
574
|
})));
|
|
427
575
|
}
|
|
428
|
-
|
|
576
|
+
const report = rollupRealization(units, {
|
|
429
577
|
generatedAt: now,
|
|
430
578
|
windowDays,
|
|
431
579
|
acceptanceThreshold,
|
|
@@ -433,6 +581,39 @@ export async function computeRealization(store, repoPath, opts = {}) {
|
|
|
433
581
|
projectScoped,
|
|
434
582
|
survivalUnmeasuredUnits: unmeasuredSurvival,
|
|
435
583
|
});
|
|
584
|
+
if (spendScope.extended) {
|
|
585
|
+
report.spendScope = {
|
|
586
|
+
linkedSessions: spendScope.linkedSessions.size,
|
|
587
|
+
linkedFolders: [...spendScope.linkedFolders],
|
|
588
|
+
verifiedObservations: spendScope.verifiedObservations,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
if (opts.sinceDays !== undefined && projectScoped) {
|
|
592
|
+
report.periodCoverage = periodCoverage(store.requestsInRange(now - opts.sinceDays * 24 * 60 * 60 * 1000, now + 1).filter((r) => spendScope.matches(r)), units, now - opts.sinceDays * 24 * 60 * 60 * 1000, now);
|
|
593
|
+
}
|
|
594
|
+
return report;
|
|
595
|
+
}
|
|
596
|
+
/** Split the period's in-scope spend by whether, and why not, a commit window covers it. */
|
|
597
|
+
export function periodCoverage(rows, units, periodStartMs, periodEndMs) {
|
|
598
|
+
const windows = units.map((u) => [u.windowStartMs, u.windowEndMs]).sort((a, b) => a[0] - b[0]);
|
|
599
|
+
const oldest = windows.length > 0 ? windows[0][0] : Infinity;
|
|
600
|
+
const newest = windows.length > 0 ? Math.max(...windows.map((w) => w[1])) : -Infinity;
|
|
601
|
+
const out = {
|
|
602
|
+
periodStartMs, periodEndMs, scopedCostUsd: 0, onCommitsUsd: 0,
|
|
603
|
+
beforeOldestUsd: 0, noCommitFollowedUsd: 0, notCommittedYetUsd: 0,
|
|
604
|
+
};
|
|
605
|
+
for (const r of rows) {
|
|
606
|
+
out.scopedCostUsd += r.costUsd;
|
|
607
|
+
if (r.tsEpochMs < oldest)
|
|
608
|
+
out.beforeOldestUsd += r.costUsd;
|
|
609
|
+
else if (r.tsEpochMs >= newest)
|
|
610
|
+
out.notCommittedYetUsd += r.costUsd;
|
|
611
|
+
else if (windows.some(([a, b]) => r.tsEpochMs >= a && r.tsEpochMs < b))
|
|
612
|
+
out.onCommitsUsd += r.costUsd;
|
|
613
|
+
else
|
|
614
|
+
out.noCommitFollowedUsd += r.costUsd;
|
|
615
|
+
}
|
|
616
|
+
return out;
|
|
436
617
|
}
|
|
437
618
|
/**
|
|
438
619
|
* Reconstruct a realization report from persisted snapshots — no git, no repo.
|
|
@@ -517,11 +698,23 @@ export function realizationFromStore(store, opts = {}) {
|
|
|
517
698
|
* the subject, not whatever repository the process happens to be sitting in.
|
|
518
699
|
*/
|
|
519
700
|
export async function loadRealization(store, repo, opts = {}) {
|
|
701
|
+
if (opts.preferStored && store.countRealizationUnits() > 0) {
|
|
702
|
+
return { source: 'store', report: realizationFromStore(store, { windowDays: opts.windowDays }) };
|
|
703
|
+
}
|
|
704
|
+
// With no request recorded there is no spend for git work to attribute, and
|
|
705
|
+
// the dashboard's value read spent 17-22 s per load proving that on an empty
|
|
706
|
+
// ledger. The Realized claim and the budget drawer's recommendation waited
|
|
707
|
+
// on it. (Other reads do not queue behind it: settings answered in 6-16 ms
|
|
708
|
+
// while a 25 s value read ran.)
|
|
709
|
+
if (opts.preferStored && store.earliestRequestMs() === null)
|
|
710
|
+
return null;
|
|
520
711
|
if (!isDemo() && repo && (await isGitRepo(repo))) {
|
|
521
712
|
return {
|
|
522
713
|
source: 'git',
|
|
523
714
|
report: await computeRealization(store, repo, {
|
|
524
|
-
limit: opts.limit ?? 40,
|
|
715
|
+
limit: opts.limit ?? (opts.sinceDays !== undefined ? undefined : 40),
|
|
716
|
+
sinceDays: opts.sinceDays,
|
|
717
|
+
gitScanBudgetMs: opts.gitScanBudgetMs,
|
|
525
718
|
windowDays: opts.windowDays ?? 14,
|
|
526
719
|
persist: opts.persist ?? false,
|
|
527
720
|
}),
|
|
@@ -657,6 +850,51 @@ export function projectTaskStrata(store, opts = {}) {
|
|
|
657
850
|
}
|
|
658
851
|
return out.sort((a, b) => b.costUsd - a.costUsd);
|
|
659
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Clones, worktrees and copies of one repository (same root commit) are one
|
|
855
|
+
* repository: measured once, on the checkout whose HEAD is newest (the one the
|
|
856
|
+
* others' work was merged into), with the others' spend in its scope through
|
|
857
|
+
* git/repoScope.ts. Measuring each separately counted the same spend once per
|
|
858
|
+
* checkout.
|
|
859
|
+
*/
|
|
860
|
+
async function oneCheckoutPerRepository(repos) {
|
|
861
|
+
const groups = new Map();
|
|
862
|
+
const out = [];
|
|
863
|
+
for (const r of repos) {
|
|
864
|
+
const id = await repoIdentity(r.repoPath);
|
|
865
|
+
if (id === null) {
|
|
866
|
+
out.push(r);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const head = await headCommitTimeMs(r.repoPath);
|
|
870
|
+
const list = groups.get(id) ?? [];
|
|
871
|
+
list.push({ ...r, headMs: head });
|
|
872
|
+
groups.set(id, list);
|
|
873
|
+
}
|
|
874
|
+
for (const list of groups.values()) {
|
|
875
|
+
list.sort((a, b) => b.headMs - a.headMs || b.costUsd - a.costUsd);
|
|
876
|
+
const [best, ...rest] = list;
|
|
877
|
+
const { headMs: _h, ...keep } = best;
|
|
878
|
+
out.push({
|
|
879
|
+
...keep,
|
|
880
|
+
sources: [...new Set(list.flatMap((r) => r.sources))].sort(),
|
|
881
|
+
costUsd: list.reduce((s, r) => s + r.costUsd, 0),
|
|
882
|
+
requests: list.reduce((s, r) => s + r.requests, 0),
|
|
883
|
+
...(rest.length > 0 ? { otherCheckouts: rest.map((r) => r.repoPath) } : {}),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return out.sort((a, b) => b.costUsd - a.costUsd);
|
|
887
|
+
}
|
|
888
|
+
async function headCommitTimeMs(repoPath) {
|
|
889
|
+
try {
|
|
890
|
+
const { stdout } = await run('git', ['-C', repoPath, 'log', '-1', '--format=%ct', 'HEAD']);
|
|
891
|
+
const n = Number(stdout.trim());
|
|
892
|
+
return Number.isFinite(n) ? n * 1000 : 0;
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return 0;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
660
898
|
/**
|
|
661
899
|
* Discover the git repos behind the ledger's projects — the interconnectedness that
|
|
662
900
|
* makes native per-project RoI possible with NO --repo and NO wiring. For each
|
|
@@ -666,13 +904,13 @@ export function projectTaskStrata(store, opts = {}) {
|
|
|
666
904
|
* a cwd that isn't a repo (or has been deleted), is simply skipped — never guessed.
|
|
667
905
|
*/
|
|
668
906
|
export async function discoverProjectRepos(store) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
907
|
+
// One git process per folder, several at once: one after another this took
|
|
908
|
+
// 5.7 s on a real ledger before any repository was measured.
|
|
909
|
+
const paths = store.projectPaths();
|
|
910
|
+
const isRepo = await mapLimit(paths, 8, (p) => isGitRepo(p.cwd));
|
|
911
|
+
return paths
|
|
912
|
+
.filter((_, i) => isRepo[i])
|
|
913
|
+
.map((p) => ({ project: p.project, repoPath: p.cwd, sources: p.sources, costUsd: p.costUsd, requests: p.requests }));
|
|
676
914
|
}
|
|
677
915
|
/**
|
|
678
916
|
* Auto-correlate every discovered project repo: run the REAL realization scorer on
|
|
@@ -684,7 +922,31 @@ export async function discoverProjectRepos(store) {
|
|
|
684
922
|
* e.g. in demo mode, where seeded rows carry no cwd.
|
|
685
923
|
*/
|
|
686
924
|
export async function realizeDiscoveredProjects(store, opts = {}) {
|
|
687
|
-
|
|
925
|
+
// The deadline starts HERE, before discovery and scoping. It used to start
|
|
926
|
+
// after them, so about 19 s of setup on a real ledger ran outside the budget
|
|
927
|
+
// and the stage that promised 120 s took 162 s (H010).
|
|
928
|
+
const stageDeadline = opts.stageBudgetMs === undefined ? undefined : Date.now() + opts.stageBudgetMs;
|
|
929
|
+
const found = await oneCheckoutPerRepository(await discoverProjectRepos(store));
|
|
930
|
+
// With a period and a shared deadline, rank by what each repository really
|
|
931
|
+
// spent in the period (label plus commit evidence: a moved checkout's label
|
|
932
|
+
// alone can hold a sliver of it), and skip one with no priced spend at all.
|
|
933
|
+
// Every scope is built once, several at once, from one read of the commit
|
|
934
|
+
// observations, and handed to the measurement below instead of rebuilt there.
|
|
935
|
+
const periodCost = new Map();
|
|
936
|
+
const scopes = new Map();
|
|
937
|
+
if (opts.stageBudgetMs !== undefined && opts.sinceDays !== undefined) {
|
|
938
|
+
const now = Date.now();
|
|
939
|
+
const rows = store.requestsInRange(now - opts.sinceDays * 24 * 60 * 60 * 1000, now + 1);
|
|
940
|
+
const observations = store.observedCommits();
|
|
941
|
+
const built = await mapLimit(found, 6, (r) => repoSpendScope(store, r.repoPath, { observations }));
|
|
942
|
+
found.forEach((r, i) => {
|
|
943
|
+
const scope = built[i];
|
|
944
|
+
scopes.set(r.repoPath, scope);
|
|
945
|
+
periodCost.set(r.repoPath, rows.reduce((s, row) => (scope.matches(row) ? s + row.costUsd : s), 0));
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
const rank = (r) => periodCost.get(r.repoPath) ?? r.costUsd;
|
|
949
|
+
const repos = opts.stageBudgetMs === undefined ? found : [...found].sort((a, b) => rank(b) - rank(a));
|
|
688
950
|
const results = new Array(repos.length);
|
|
689
951
|
// Repositories are independent and almost all of their time is spent waiting
|
|
690
952
|
// on git, so a few run at once. Safe on one DatabaseSync handle because every
|
|
@@ -696,13 +958,38 @@ export async function realizeDiscoveredProjects(store, opts = {}) {
|
|
|
696
958
|
while (next < repos.length) {
|
|
697
959
|
const i = next++;
|
|
698
960
|
const r = repos[i];
|
|
961
|
+
const left = stageDeadline === undefined ? undefined : stageDeadline - Date.now();
|
|
962
|
+
const spent = periodCost.get(r.repoPath);
|
|
963
|
+
if (spent !== undefined && spent <= 0) {
|
|
964
|
+
results[i] = { ...r, units: 0, realizedUnits: 0, measured: false, skipped: 'no-spend', periodCostUsd: 0 };
|
|
965
|
+
done += 1;
|
|
966
|
+
opts.onProgress?.(done, repos.length, r.project);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (left !== undefined && left <= 0) {
|
|
970
|
+
results[i] = { ...r, units: 0, realizedUnits: 0, measured: false, skipped: 'deadline', ...(spent === undefined ? {} : { periodCostUsd: spent }) };
|
|
971
|
+
done += 1;
|
|
972
|
+
opts.onProgress?.(done, repos.length, r.project);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
699
975
|
const rep = await computeRealization(store, r.repoPath, {
|
|
700
|
-
limit: opts.limit ?? 40,
|
|
976
|
+
limit: opts.limit ?? (opts.sinceDays !== undefined ? undefined : 40),
|
|
977
|
+
sinceDays: opts.sinceDays,
|
|
978
|
+
gitScanBudgetMs: left === undefined ? opts.gitScanBudgetMs : Math.min(left, opts.gitScanBudgetMs ?? left),
|
|
701
979
|
windowDays: opts.windowDays,
|
|
702
980
|
persist: true,
|
|
981
|
+
...(scopes.has(r.repoPath) ? { spendScope: scopes.get(r.repoPath) } : {}),
|
|
982
|
+
// Stop git a little before the stage deadline: the scoring and the
|
|
983
|
+
// snapshot write after it still have to fit inside the promise.
|
|
984
|
+
...(stageDeadline === undefined ? {} : { gitDeadlineAtMs: stageDeadline - STAGE_FINISH_RESERVE_MS }),
|
|
703
985
|
});
|
|
704
986
|
const realizedUnits = rep.units.filter((u) => !u.maturing && u.funnel.realized).length;
|
|
705
|
-
|
|
987
|
+
const periodCostUsd = rep.periodCoverage?.scopedCostUsd ?? spent;
|
|
988
|
+
results[i] = {
|
|
989
|
+
...r, units: rep.units.length, realizedUnits, measured: true, kept: keptSummary(rep),
|
|
990
|
+
...(periodCostUsd === undefined ? {} : { periodCostUsd }),
|
|
991
|
+
...(rep.periodCoverage === undefined ? {} : { coverage: rep.periodCoverage }),
|
|
992
|
+
};
|
|
706
993
|
done += 1;
|
|
707
994
|
opts.onProgress?.(done, repos.length, r.project);
|
|
708
995
|
}
|
|
@@ -752,11 +1039,14 @@ export function rollupRealization(units, opts) {
|
|
|
752
1039
|
wasteMap.set(stage, b);
|
|
753
1040
|
}
|
|
754
1041
|
const instrumentation = Object.fromEntries(GATE_LADDER.map((g) => [g, 0]));
|
|
1042
|
+
const gatePasses = Object.fromEntries(GATE_LADDER.map((g) => [g, 0]));
|
|
755
1043
|
const gateConflicts = Object.fromEntries(GATE_LADDER.map((g) => [g, 0]));
|
|
756
1044
|
for (const u of mature) {
|
|
757
1045
|
for (const r of u.funnel.results) {
|
|
758
1046
|
if (r.verdict !== 'unknown')
|
|
759
1047
|
instrumentation[r.gate] += 1;
|
|
1048
|
+
if (r.verdict === 'pass' && r.polarity !== 'conflicted')
|
|
1049
|
+
gatePasses[r.gate] += 1;
|
|
760
1050
|
if (r.polarity === 'conflicted')
|
|
761
1051
|
gateConflicts[r.gate] += 1;
|
|
762
1052
|
}
|
|
@@ -796,6 +1086,7 @@ export function rollupRealization(units, opts) {
|
|
|
796
1086
|
realizedSpendShare: totalCostUsd > 0 ? spendOnRealizedUnitsUsd / totalCostUsd : null,
|
|
797
1087
|
wasteByStage: [...wasteMap.values()].sort((a, b) => b.costUsd - a.costUsd),
|
|
798
1088
|
instrumentation,
|
|
1089
|
+
gatePasses,
|
|
799
1090
|
gateConflicts,
|
|
800
1091
|
realizationBounds: terminalRealizationBounds(mature.map((u) => u.funnel)),
|
|
801
1092
|
serial: serialRealization(mature.map((u) => u.funnel)),
|
package/dist/value/report.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* 5. RoI, drift, time reclaimed, frontier — all read (1)–(4), never the store
|
|
43
43
|
* directly, so no surface can re-derive an input its own way.
|
|
44
44
|
*/
|
|
45
|
+
import { capBasisSpend } from '../budget/guard.js';
|
|
45
46
|
import { isDemo, DEFAULT_CONFIG } from '../config.js';
|
|
46
47
|
import { demoLiftOptions } from '../demo/seed.js';
|
|
47
48
|
import { projectName } from '../git/correlate.js';
|
|
@@ -76,9 +77,12 @@ const DEFAULT_SPEND_WINDOW_DAYS = 30;
|
|
|
76
77
|
export async function valueSpine(store, config, opts) {
|
|
77
78
|
const windowDays = opts.windowDays ?? 14;
|
|
78
79
|
const loaded = await loadRealization(store, opts.repo, {
|
|
79
|
-
limit: opts.limit ?? 40,
|
|
80
|
+
limit: opts.limit ?? (opts.sinceDays !== undefined ? undefined : 40),
|
|
80
81
|
windowDays,
|
|
81
82
|
persist: opts.persist ?? false,
|
|
83
|
+
sinceDays: opts.sinceDays,
|
|
84
|
+
gitScanBudgetMs: opts.gitScanBudgetMs,
|
|
85
|
+
preferStored: opts.preferStored,
|
|
82
86
|
});
|
|
83
87
|
if (!loaded)
|
|
84
88
|
return null;
|
|
@@ -267,14 +271,30 @@ export function budgetAdvice(store, config, opts = {}) {
|
|
|
267
271
|
const series = store.economicSeries(startMs, endMs, DAY_MS, liveOnly);
|
|
268
272
|
const realizedSpendShare = opts.realizedSpendShare ?? null;
|
|
269
273
|
const coverage = exactRows.length === 0 ? 'legacy_unknown' : exact.complete ? 'exact' : 'partial';
|
|
274
|
+
const rec = recommendBudget({
|
|
275
|
+
dailySpends: series.map((s) => s.costUsd),
|
|
276
|
+
realizedSpendShare,
|
|
277
|
+
frontier: opts.frontier ?? [],
|
|
278
|
+
currentDailyCapUsd: config.budget.dailyUsd,
|
|
279
|
+
decisionInputs: budgetCapDecisionInputs(coverage, realizedSpendShare),
|
|
280
|
+
});
|
|
281
|
+
// A user whose spend so far was all imported from agent logs has history,
|
|
282
|
+
// just none the cap governs. "Not enough spend history" would contradict
|
|
283
|
+
// every other page, so name the basis and the two ways forward instead.
|
|
284
|
+
if (liveOnly && rec.status === 'cold_start') {
|
|
285
|
+
// "No requests went through the proxy" is a claim about requests, so it
|
|
286
|
+
// is decided by counting them, on the guard's own projection (review 4, F3).
|
|
287
|
+
const live = capBasisSpend(store, config.budget, startMs, endMs);
|
|
288
|
+
const liveUsd = series.reduce((a, s) => a + s.costUsd, 0);
|
|
289
|
+
const importedUsd = store.economicSeries(startMs, endMs, DAY_MS, false).reduce((a, s) => a + s.costUsd, 0) - liveUsd;
|
|
290
|
+
if (live.requests === 0 && importedUsd >= 0.005) {
|
|
291
|
+
rec.rationale = [
|
|
292
|
+
`No requests went through Segreant's proxy in the last ${days} days. The $${importedUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} at list price in that time was imported from agent logs: it is observed, but a cap only governs proxied requests, so it does not count toward one. Route a tool through the proxy for a few active days, or count imported spend toward the cap with: segreant budget --include-imported on`,
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
270
296
|
return {
|
|
271
|
-
...
|
|
272
|
-
dailySpends: series.map((s) => s.costUsd),
|
|
273
|
-
realizedSpendShare,
|
|
274
|
-
frontier: opts.frontier ?? [],
|
|
275
|
-
currentDailyCapUsd: config.budget.dailyUsd,
|
|
276
|
-
decisionInputs: budgetCapDecisionInputs(coverage, realizedSpendShare),
|
|
277
|
-
}),
|
|
297
|
+
...rec,
|
|
278
298
|
spendBasis: liveOnly ? 'live_proxy' : 'all_observed',
|
|
279
299
|
windowDays: days,
|
|
280
300
|
economic: {
|
|
@@ -131,12 +131,14 @@ function events(store, startMs, endMs) {
|
|
|
131
131
|
return event;
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The requests before `endMs` that a link names, read per link rather than by
|
|
136
|
+
* filtering the whole ledger: the report ran on every /api/value load and held
|
|
137
|
+
* every request ever recorded, outcomes or not (540 MiB of heap on a
|
|
138
|
+
* 300,000-request ledger).
|
|
139
|
+
*/
|
|
140
|
+
function matched(store, link, endMs) {
|
|
141
|
+
return store.requestsForSpendLink(link, endMs);
|
|
140
142
|
}
|
|
141
143
|
/** Preview is read-only; apply appends one signal to the existing ledger. */
|
|
142
144
|
export function recordReportedOutcome(store, raw, apply = false, enabled = true, nowMs = Date.now()) {
|
|
@@ -146,7 +148,7 @@ export function recordReportedOutcome(store, raw, apply = false, enabled = true,
|
|
|
146
148
|
const prior = events(store, 0, Number.MAX_SAFE_INTEGER).filter((item) => item.outcomeId === event.outcomeId);
|
|
147
149
|
if (prior.some((item) => !sameIdentity(item, event)))
|
|
148
150
|
throw new Error('outcomeId already has a different kind or spend link');
|
|
149
|
-
const rows = matched(store.
|
|
151
|
+
const rows = matched(store, event.link, Number.MAX_SAFE_INTEGER);
|
|
150
152
|
const preview = {
|
|
151
153
|
apply,
|
|
152
154
|
basis: SELF_REPORTED_BASIS,
|
|
@@ -180,8 +182,7 @@ export function selfReportedValueReport(store, startMs, endMs, enabled = true) {
|
|
|
180
182
|
// A linked session or request may have started before the report window, so
|
|
181
183
|
// spend is matched over all history up to the window end, exactly as the
|
|
182
184
|
// record preview matches it. The window selects outcomes, not their cost.
|
|
183
|
-
const
|
|
184
|
-
const claims = selected.map((group) => matched(requestRows, group[0].link));
|
|
185
|
+
const claims = selected.map((group) => matched(store, group[0].link, endMs));
|
|
185
186
|
const claimedBy = new Map();
|
|
186
187
|
for (const rows of claims)
|
|
187
188
|
for (const row of rows)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -263,9 +263,12 @@ selected eligible address is pinned into the one socket request; the OS resolver
|
|
|
263
263
|
is not asked to choose a different address afterward.
|
|
264
264
|
Only a genuinely absent receipt path may establish a genesis predecessor, and a
|
|
265
265
|
present invalid path is never silently reset. Loaded JSON egress configuration
|
|
266
|
-
also accepts only the two declared modes and exact rule field types
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
also accepts only the two declared modes and exact rule field types. A present
|
|
267
|
+
but malformed egress section (ambiguous mode or enabled values, an invalid rule,
|
|
268
|
+
unexpected fields) loads as `invalid`, which refuses every non-loopback target,
|
|
269
|
+
the built-in provider and price-list routes included, until it is repaired; only
|
|
270
|
+
an absent section means local-locked. Falling back to local-locked would now let
|
|
271
|
+
built-in provider traffic through a typo. Loaded rules use the same exact semantic validator as CLI-authored
|
|
269
272
|
rules: canonical HTTPS origin, non-empty safe path prefix, valid identifiers,
|
|
270
273
|
and no unexpected fields. A bounded stale-lock refusal is
|
|
271
274
|
operator-repairable only after confirming no Segreant writer is active; the lock
|
|
@@ -444,5 +447,6 @@ genuinely open:
|
|
|
444
447
|
|
|
445
448
|
- Sees only traffic routed through it (D1).
|
|
446
449
|
- Cost accuracy depends on the pricing table; unknown models are flagged `estimated` and use a conservative fallback rather than failing.
|
|
447
|
-
- Budget blocking is pre-flight on
|
|
450
|
+
- Budget blocking is pre-flight on recorded spend. Every request already admitted when the cap is crossed still completes, so the overshoot is bounded by how many requests are in flight times their cost: a stream cannot be un-sent. Measured 2026-10-17 against a local stub provider: with 50 concurrent requests at $0.00072 each, 42 completed past a $1.66 cap ($0.031 over) and the other 752 were refused with 429. One agent working serially overshoots by at most one request.
|
|
451
|
+
- A cap saved from the CLI, the dashboard or an editor governs the running proxy within a second: the proxy re-reads the budget when `config.json` changes. While the file is invalid, every request is refused (503) without being sent; deleting the file keeps the cap in force.
|
|
448
452
|
- "Cost reduction %" is a function of baseline waste, not a guarantee the tool makes. Segreant provides visibility and controls; the savings are the user's to realize.
|