@agent-finops/core 0.8.1 → 0.9.1
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/README.md +5 -3
- package/dist/actionPlanner.d.ts +140 -0
- package/dist/actionPlanner.js +938 -0
- package/dist/actionVerification.d.ts +1240 -0
- package/dist/actionVerification.js +1028 -0
- package/dist/activitySnapshot.d.ts +142 -50
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentDraftToken.d.ts +80 -0
- package/dist/agentDraftToken.js +188 -0
- package/dist/agentEconomicsReceipt.d.ts +74 -74
- package/dist/agentLoopContract.d.ts +27 -0
- package/dist/agentLoopContract.js +36 -0
- package/dist/glance.d.ts +27 -1
- package/dist/glance.js +151 -12
- package/dist/guidedAnswer.d.ts +51 -0
- package/dist/guidedAnswer.js +352 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +13 -1
- package/dist/localAgentFormats/gemini.js +2 -2
- package/dist/localAgentFormats/registry.js +6 -2
- package/dist/localAgentFormats/runtimeRegistry.js +5 -2
- package/dist/localAgentFormats/types.d.ts +2 -1
- package/dist/localAgentLogs.d.ts +362 -3
- package/dist/localAgentLogs.js +1964 -165
- package/dist/modelPricing.d.ts +1 -1
- package/dist/modelPricing.js +1 -1
- package/dist/projectEconomics.d.ts +617 -0
- package/dist/projectEconomics.js +620 -0
- package/dist/projectEconomicsBuilder.d.ts +89 -0
- package/dist/projectEconomicsBuilder.js +473 -0
- package/dist/projectIndexStore.d.ts +545 -0
- package/dist/projectIndexStore.js +606 -0
- package/dist/providerConnectors.d.ts +161 -1
- package/dist/providerConnectors.js +406 -11
- package/dist/qualitativeIndexCache.d.ts +494 -0
- package/dist/qualitativeIndexCache.js +930 -0
- package/dist/resultCard.d.ts +350 -0
- package/dist/resultCard.js +604 -0
- package/dist/runtimeCommands.d.ts +36 -0
- package/dist/runtimeCommands.js +50 -0
- package/dist/scanGuard.d.ts +3 -1
- package/dist/scanGuard.js +164 -4
- package/dist/schema.d.ts +33 -31
- package/dist/schema.js +9 -1
- package/dist/sessionVitals.d.ts +145 -0
- package/dist/sessionVitals.js +521 -0
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- package/package.json +1 -1
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { subscriptionPlans } from "./planMath.js";
|
|
3
|
+
/** §1.2 canonical vocabulary — everything outside this set is killed. */
|
|
4
|
+
export const resultCardVocabulary = Object.freeze({
|
|
5
|
+
committed: "committed",
|
|
6
|
+
/** Sanctioned narrow alias for the committed total, statusline <75 col only. */
|
|
7
|
+
committedNarrowAlias: "subs",
|
|
8
|
+
apiEquivalent: "API-equivalent",
|
|
9
|
+
billed: "billed",
|
|
10
|
+
estimatedMarker: "~",
|
|
11
|
+
notReported: "not reported",
|
|
12
|
+
notReportedShort: "n/r",
|
|
13
|
+
notReportedLegend: "n/r = not reported",
|
|
14
|
+
estimatedMarkerLegend: "~ = estimated at API rates",
|
|
15
|
+
everythingElse: "everything else",
|
|
16
|
+
unattributed: "unattributed",
|
|
17
|
+
detectedUnverifiedSuffix: "detected (unverified · beta connector)",
|
|
18
|
+
blendPolicy: "never_blended"
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Killed on sight (§1.2): QA greps new/changed rendered copy for these.
|
|
22
|
+
* "provider-reported" survives ONLY in trust/mode lines explaining where
|
|
23
|
+
* `billed` comes from — never on a figure.
|
|
24
|
+
*/
|
|
25
|
+
export const resultCardKilledTerms = Object.freeze([
|
|
26
|
+
"usage value",
|
|
27
|
+
"observed value",
|
|
28
|
+
"cost/value",
|
|
29
|
+
"API-equivalent/estimated",
|
|
30
|
+
// QA MINOR-5: the pre-C-lane statusline suffix, killed on every new surface.
|
|
31
|
+
"7d value"
|
|
32
|
+
]);
|
|
33
|
+
const usdSchema = z.number().finite().nonnegative();
|
|
34
|
+
const shareSchema = z.number().finite().min(0).max(1);
|
|
35
|
+
export const resultCardRunwaySchema = z.object({
|
|
36
|
+
kind: z.enum(["five-hour", "weekly"]),
|
|
37
|
+
remainingPercent: z.number().finite().min(0).max(100),
|
|
38
|
+
resetsAt: z.string().datetime({ offset: true })
|
|
39
|
+
}).strict();
|
|
40
|
+
export const resultCardSubscriptionRowSchema = z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
agentId: z.enum(["claude-code", "codex"]).nullable(),
|
|
43
|
+
planLabel: z.string().min(1).nullable(),
|
|
44
|
+
connection: z.enum(["local_logs", "connected", "detected_only"]),
|
|
45
|
+
committedUsdPerMonth: usdSchema.nullable(),
|
|
46
|
+
apiEquivalentUsd: usdSchema.nullable(),
|
|
47
|
+
providerBilledUsd: usdSchema.nullable(),
|
|
48
|
+
detectedUnverifiedUsd: usdSchema.nullable(),
|
|
49
|
+
runways: z.array(resultCardRunwaySchema).max(2)
|
|
50
|
+
}).strict();
|
|
51
|
+
export const resultCardTotalsSchema = z.object({
|
|
52
|
+
subscriptionCommitted: z.object({
|
|
53
|
+
amountUsd: usdSchema.nullable(),
|
|
54
|
+
pricedSubs: z.number().int().nonnegative(),
|
|
55
|
+
totalSubs: z.number().int().nonnegative()
|
|
56
|
+
}).strict().superRefine((committed, context) => {
|
|
57
|
+
if (committed.pricedSubs > committed.totalSubs) {
|
|
58
|
+
context.addIssue({
|
|
59
|
+
code: "custom",
|
|
60
|
+
path: ["pricedSubs"],
|
|
61
|
+
message: "Priced subscriptions cannot exceed total subscriptions."
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if ((committed.amountUsd === null) !== (committed.pricedSubs === 0)) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: ["amountUsd"],
|
|
68
|
+
message: "A committed total exists exactly when at least one subscription is priced."
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
apiEquivalent: z.object({
|
|
73
|
+
amountUsd: usdSchema.nullable(),
|
|
74
|
+
financialEvidence: z.enum(["estimated", "missing"])
|
|
75
|
+
}).strict().superRefine((total, context) => {
|
|
76
|
+
if ((total.amountUsd === null) !== (total.financialEvidence === "missing")) {
|
|
77
|
+
context.addIssue({
|
|
78
|
+
code: "custom",
|
|
79
|
+
path: ["financialEvidence"],
|
|
80
|
+
message: "API-equivalent amount and financial evidence must agree (null ⟺ missing)."
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
providerBilled: z.object({
|
|
85
|
+
amountUsd: usdSchema.nullable(),
|
|
86
|
+
financialEvidence: z.enum(["verified", "missing"])
|
|
87
|
+
}).strict().superRefine((total, context) => {
|
|
88
|
+
if ((total.amountUsd === null) !== (total.financialEvidence === "missing")) {
|
|
89
|
+
context.addIssue({
|
|
90
|
+
code: "custom",
|
|
91
|
+
path: ["financialEvidence"],
|
|
92
|
+
message: "Provider-billed amount and financial evidence must agree (null ⟺ missing)."
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
blended: z.null(),
|
|
97
|
+
blendPolicy: z.literal("never_blended")
|
|
98
|
+
}).strict();
|
|
99
|
+
export const resultCardByProjectSchema = z.object({
|
|
100
|
+
basis: z.enum(["api_equivalent", "provider_billed"]),
|
|
101
|
+
rows: z.array(z.object({
|
|
102
|
+
project: z.string().min(1),
|
|
103
|
+
amountUsd: usdSchema,
|
|
104
|
+
share: shareSchema,
|
|
105
|
+
unattributed: z.boolean()
|
|
106
|
+
}).strict()).min(1),
|
|
107
|
+
everythingElse: z.object({
|
|
108
|
+
amountUsd: usdSchema,
|
|
109
|
+
share: shareSchema,
|
|
110
|
+
projectCount: z.number().int().positive()
|
|
111
|
+
}).strict().nullable()
|
|
112
|
+
}).strict().superRefine((block, context) => {
|
|
113
|
+
const shares = [
|
|
114
|
+
...block.rows.map((row) => row.share),
|
|
115
|
+
...(block.everythingElse ? [block.everythingElse.share] : [])
|
|
116
|
+
];
|
|
117
|
+
const shareSum = shares.reduce((total, share) => total + share, 0);
|
|
118
|
+
if (Math.abs(shareSum - 1) > 0.0001) {
|
|
119
|
+
context.addIssue({
|
|
120
|
+
code: "custom",
|
|
121
|
+
path: ["rows"],
|
|
122
|
+
message: "By-project shares must sum to 1.0000."
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
export const resultCardSchema = z.object({
|
|
127
|
+
kind: z.literal("aibill.result_card"),
|
|
128
|
+
schemaVersion: z.literal(1),
|
|
129
|
+
currency: z.literal("USD"),
|
|
130
|
+
windowDays: z.number().int().positive(),
|
|
131
|
+
mode: z.enum(["local-logs", "connected", "mixed", "demo"]),
|
|
132
|
+
subscriptions: z.array(resultCardSubscriptionRowSchema),
|
|
133
|
+
totals: resultCardTotalsSchema,
|
|
134
|
+
byProject: resultCardByProjectSchema.nullable()
|
|
135
|
+
}).strict().superRefine((card, context) => {
|
|
136
|
+
if (card.mode === "demo" && card.subscriptions.length > 0) {
|
|
137
|
+
context.addIssue({
|
|
138
|
+
code: "custom",
|
|
139
|
+
path: ["subscriptions"],
|
|
140
|
+
message: "Sample/demo cards never carry real detected subscriptions."
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (card.totals.subscriptionCommitted.totalSubs !== card.subscriptions.length) {
|
|
144
|
+
context.addIssue({
|
|
145
|
+
code: "custom",
|
|
146
|
+
path: ["totals", "subscriptionCommitted", "totalSubs"],
|
|
147
|
+
message: "totalSubs must equal the number of subscription rows."
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (new Set(card.subscriptions.map((row) => row.id)).size !== card.subscriptions.length) {
|
|
151
|
+
context.addIssue({
|
|
152
|
+
code: "custom",
|
|
153
|
+
path: ["subscriptions"],
|
|
154
|
+
message: "Subscription ids must be unique."
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (card.byProject) {
|
|
158
|
+
// rows + everythingElse reconcile exactly to the basis total (≤$0.01 drift).
|
|
159
|
+
const basisTotal = card.byProject.basis === "api_equivalent"
|
|
160
|
+
? card.totals.apiEquivalent.amountUsd
|
|
161
|
+
: card.totals.providerBilled.amountUsd;
|
|
162
|
+
if (basisTotal === null) {
|
|
163
|
+
context.addIssue({
|
|
164
|
+
code: "custom",
|
|
165
|
+
path: ["byProject", "basis"],
|
|
166
|
+
message: "A by-project block requires its basis total to exist."
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const rowSum = card.byProject.rows.reduce((total, row) => total + row.amountUsd, 0) +
|
|
171
|
+
(card.byProject.everythingElse?.amountUsd ?? 0);
|
|
172
|
+
if (Math.abs(rowSum - basisTotal) > 0.011) {
|
|
173
|
+
context.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["byProject", "rows"],
|
|
176
|
+
message: "By-project rows plus everything-else must reconcile to the basis total."
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
/** Providers whose connected billing is a subscription with no local agent. */
|
|
183
|
+
const providerSubscriptionProviders = new Set(["cursor", "github-copilot"]);
|
|
184
|
+
/**
|
|
185
|
+
* Cost types whose estimated dollars are usage × published API rates — the
|
|
186
|
+
* `api_equivalent` basis. Estimated dollars from any other connected source
|
|
187
|
+
* are provider-reported-but-unverified and stay disclosure-only.
|
|
188
|
+
*/
|
|
189
|
+
const apiEquivalentCostTypes = new Set(["local_agent_logs", "anthropic_claude_code_usage"]);
|
|
190
|
+
const unattributedProjectKeys = new Set(["unmapped", "(home)", "home", "unattributed", "unknown", ""]);
|
|
191
|
+
function isUnattributedProject(projectId) {
|
|
192
|
+
return projectId === undefined || unattributedProjectKeys.has(projectId.trim().toLowerCase());
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The one basis classifier every surface shares (§1.2): verified dollars are
|
|
196
|
+
* provider_billed; estimated dollars are api_equivalent ONLY when they were
|
|
197
|
+
* priced at published API rates; every other priced-but-unverified dollar is
|
|
198
|
+
* detected_unverified (disclosure-only). Exported so renderers can keep their
|
|
199
|
+
* bars/tables same-kind (QA finding M2) instead of re-deriving basis rules.
|
|
200
|
+
*/
|
|
201
|
+
export function classifyResultCardRecordBasis(record, mode) {
|
|
202
|
+
return classifyRecordBasis(record, mode);
|
|
203
|
+
}
|
|
204
|
+
function classifyRecordBasis(record, mode) {
|
|
205
|
+
if (typeof record.amountUsd !== "number")
|
|
206
|
+
return "none";
|
|
207
|
+
if (record.costConfidence === "verified")
|
|
208
|
+
return "provider_billed";
|
|
209
|
+
if (record.costConfidence === "estimated") {
|
|
210
|
+
if (mode === "demo")
|
|
211
|
+
return "api_equivalent";
|
|
212
|
+
return record.providerCostType !== undefined && apiEquivalentCostTypes.has(record.providerCostType)
|
|
213
|
+
? "api_equivalent"
|
|
214
|
+
: "detected_unverified";
|
|
215
|
+
}
|
|
216
|
+
if (record.costConfidence === "detected_unverified")
|
|
217
|
+
return "detected_unverified";
|
|
218
|
+
return "none";
|
|
219
|
+
}
|
|
220
|
+
function roundCents(amount) {
|
|
221
|
+
return Math.round(amount * 100) / 100;
|
|
222
|
+
}
|
|
223
|
+
function sumOrNull(amounts) {
|
|
224
|
+
if (amounts.length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
return roundCents(amounts.reduce((total, amount) => total + amount, 0));
|
|
227
|
+
}
|
|
228
|
+
/** Strip the provider brand from a detected plan label: "Claude Max 5x" → "Max 5x". */
|
|
229
|
+
function displayPlanLabel(plan) {
|
|
230
|
+
const known = plan.planId
|
|
231
|
+
? subscriptionPlans.find((candidate) => candidate.id === plan.planId)
|
|
232
|
+
: undefined;
|
|
233
|
+
const label = known?.name ?? plan.planLabel;
|
|
234
|
+
if (!label)
|
|
235
|
+
return null;
|
|
236
|
+
return label.replace(/^Claude /u, "").replace(/^ChatGPT /u, "").trim() || null;
|
|
237
|
+
}
|
|
238
|
+
function committedPriceFor(plan) {
|
|
239
|
+
const known = plan.planId
|
|
240
|
+
? subscriptionPlans.find((candidate) => candidate.id === plan.planId)
|
|
241
|
+
: undefined;
|
|
242
|
+
return known?.monthlyUsd ?? null;
|
|
243
|
+
}
|
|
244
|
+
function subscriptionDisplayId(agent) {
|
|
245
|
+
return agent === "claude-code" ? "claude" : "chatgpt";
|
|
246
|
+
}
|
|
247
|
+
function subscriptionOrder(id) {
|
|
248
|
+
if (id === "claude")
|
|
249
|
+
return 0;
|
|
250
|
+
if (id === "chatgpt")
|
|
251
|
+
return 1;
|
|
252
|
+
if (id === "cursor")
|
|
253
|
+
return 2;
|
|
254
|
+
return 3;
|
|
255
|
+
}
|
|
256
|
+
function orderRunways(runways) {
|
|
257
|
+
return [...runways]
|
|
258
|
+
.sort((left, right) => {
|
|
259
|
+
const remaining = left.remainingPercent - right.remainingPercent;
|
|
260
|
+
if (remaining !== 0)
|
|
261
|
+
return remaining;
|
|
262
|
+
const reset = Date.parse(left.resetsAt) - Date.parse(right.resetsAt);
|
|
263
|
+
if (reset !== 0)
|
|
264
|
+
return reset;
|
|
265
|
+
return left.kind === right.kind ? 0 : left.kind === "five-hour" ? -1 : 1;
|
|
266
|
+
})
|
|
267
|
+
.slice(0, 2);
|
|
268
|
+
}
|
|
269
|
+
/** Project names clip at 24 chars + `…` on every renderer (§1.1). */
|
|
270
|
+
export function clipResultCardProjectName(name) {
|
|
271
|
+
const characters = [...name];
|
|
272
|
+
if (characters.length <= 24)
|
|
273
|
+
return name;
|
|
274
|
+
return `${characters.slice(0, 24).join("")}…`;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Largest-remainder integer percentages: printed shares sum to exactly 100.
|
|
278
|
+
* Input weights need not be normalized.
|
|
279
|
+
*/
|
|
280
|
+
export function largestRemainderPercents(weights) {
|
|
281
|
+
return largestRemainderUnits(weights, 100);
|
|
282
|
+
}
|
|
283
|
+
function largestRemainderUnits(weights, totalUnits) {
|
|
284
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
285
|
+
if (total <= 0 || weights.length === 0)
|
|
286
|
+
return weights.map(() => 0);
|
|
287
|
+
const exact = weights.map((weight) => (weight / total) * totalUnits);
|
|
288
|
+
const floors = exact.map((value) => Math.floor(value));
|
|
289
|
+
let remaining = totalUnits - floors.reduce((sum, value) => sum + value, 0);
|
|
290
|
+
const byRemainder = exact
|
|
291
|
+
.map((value, index) => ({ index, remainder: value - Math.floor(value) }))
|
|
292
|
+
.sort((left, right) => right.remainder - left.remainder || left.index - right.index);
|
|
293
|
+
const result = [...floors];
|
|
294
|
+
for (const { index } of byRemainder) {
|
|
295
|
+
if (remaining <= 0)
|
|
296
|
+
break;
|
|
297
|
+
result[index] = (result[index] ?? 0) + 1;
|
|
298
|
+
remaining -= 1;
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Build the canonical result card from window-scoped usage records plus
|
|
304
|
+
* locally detected plans. Pure and deterministic; every §1.1 rule lives here,
|
|
305
|
+
* not in the renderers.
|
|
306
|
+
*/
|
|
307
|
+
export function buildResultCard(input) {
|
|
308
|
+
const windowDays = input.windowDays ?? 30;
|
|
309
|
+
const mode = input.mode;
|
|
310
|
+
const records = input.records;
|
|
311
|
+
// --- basis-wide sums (same-kind money is summable; cross-kind never) -----
|
|
312
|
+
const apiEquivalentAmounts = [];
|
|
313
|
+
const providerBilledAmounts = [];
|
|
314
|
+
for (const record of records) {
|
|
315
|
+
const basis = classifyRecordBasis(record, mode);
|
|
316
|
+
if (basis === "api_equivalent")
|
|
317
|
+
apiEquivalentAmounts.push(record.amountUsd ?? 0);
|
|
318
|
+
if (basis === "provider_billed")
|
|
319
|
+
providerBilledAmounts.push(record.amountUsd ?? 0);
|
|
320
|
+
}
|
|
321
|
+
const apiEquivalentTotal = sumOrNull(apiEquivalentAmounts);
|
|
322
|
+
const providerBilledTotal = sumOrNull(providerBilledAmounts);
|
|
323
|
+
// --- subscription rows ---------------------------------------------------
|
|
324
|
+
// Sample/demo mode: real detected plans never mix into demo output (§1.1).
|
|
325
|
+
const subscriptions = [];
|
|
326
|
+
if (mode !== "demo") {
|
|
327
|
+
const detectedSubscriptions = (input.detectedPlans ?? []).filter((plan) => plan.billing === "subscription" && (plan.agent === "claude-code" || plan.agent === "codex"));
|
|
328
|
+
const seenAgents = new Set();
|
|
329
|
+
for (const plan of detectedSubscriptions) {
|
|
330
|
+
if (seenAgents.has(plan.agent))
|
|
331
|
+
continue;
|
|
332
|
+
seenAgents.add(plan.agent);
|
|
333
|
+
const agentRecords = records.filter((record) => record.agentId === plan.agent);
|
|
334
|
+
const agentApiAmounts = agentRecords
|
|
335
|
+
.filter((record) => classifyRecordBasis(record, mode) === "api_equivalent")
|
|
336
|
+
.map((record) => record.amountUsd ?? 0);
|
|
337
|
+
const agentBilledAmounts = agentRecords
|
|
338
|
+
.filter((record) => classifyRecordBasis(record, mode) === "provider_billed")
|
|
339
|
+
.map((record) => record.amountUsd ?? 0);
|
|
340
|
+
subscriptions.push({
|
|
341
|
+
id: subscriptionDisplayId(plan.agent),
|
|
342
|
+
agentId: plan.agent,
|
|
343
|
+
planLabel: displayPlanLabel(plan),
|
|
344
|
+
connection: "local_logs",
|
|
345
|
+
committedUsdPerMonth: committedPriceFor(plan),
|
|
346
|
+
apiEquivalentUsd: sumOrNull(agentApiAmounts),
|
|
347
|
+
providerBilledUsd: sumOrNull(agentBilledAmounts),
|
|
348
|
+
detectedUnverifiedUsd: null,
|
|
349
|
+
runways: orderRunways(input.runways?.[plan.agent] ?? [])
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// Provider-billed subscriptions with no local agent (cursor today). The
|
|
353
|
+
// beta cursor connector hard-codes estimated confidence, so its dollars
|
|
354
|
+
// land in detectedUnverifiedUsd — never providerBilledUsd — until the
|
|
355
|
+
// connector is live-verified.
|
|
356
|
+
const providersSeen = [...new Set(records
|
|
357
|
+
.map((record) => record.source.provider)
|
|
358
|
+
.filter((provider) => providerSubscriptionProviders.has(provider)))].sort();
|
|
359
|
+
for (const provider of providersSeen) {
|
|
360
|
+
const providerRecords = records.filter((record) => record.source.provider === provider);
|
|
361
|
+
const billedAmounts = providerRecords
|
|
362
|
+
.filter((record) => classifyRecordBasis(record, mode) === "provider_billed")
|
|
363
|
+
.map((record) => record.amountUsd ?? 0);
|
|
364
|
+
const detectedAmounts = providerRecords
|
|
365
|
+
.filter((record) => classifyRecordBasis(record, mode) === "detected_unverified")
|
|
366
|
+
.map((record) => record.amountUsd ?? 0);
|
|
367
|
+
const providerPlan = input.providerPlans?.find((plan) => plan.provider === provider);
|
|
368
|
+
subscriptions.push({
|
|
369
|
+
id: provider,
|
|
370
|
+
agentId: null,
|
|
371
|
+
planLabel: providerPlan?.planLabel ?? null,
|
|
372
|
+
connection: "connected",
|
|
373
|
+
committedUsdPerMonth: providerPlan?.committedUsdPerMonth ?? null,
|
|
374
|
+
apiEquivalentUsd: null,
|
|
375
|
+
providerBilledUsd: sumOrNull(billedAmounts),
|
|
376
|
+
detectedUnverifiedUsd: sumOrNull(detectedAmounts),
|
|
377
|
+
runways: []
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
subscriptions.sort((left, right) => subscriptionOrder(left.id) - subscriptionOrder(right.id) || left.id.localeCompare(right.id));
|
|
381
|
+
}
|
|
382
|
+
// --- totals stack (never blended) ----------------------------------------
|
|
383
|
+
const pricedSubs = subscriptions.filter((row) => row.committedUsdPerMonth !== null);
|
|
384
|
+
const totals = {
|
|
385
|
+
subscriptionCommitted: {
|
|
386
|
+
amountUsd: pricedSubs.length > 0
|
|
387
|
+
? roundCents(pricedSubs.reduce((total, row) => total + (row.committedUsdPerMonth ?? 0), 0))
|
|
388
|
+
: null,
|
|
389
|
+
pricedSubs: pricedSubs.length,
|
|
390
|
+
totalSubs: subscriptions.length
|
|
391
|
+
},
|
|
392
|
+
apiEquivalent: {
|
|
393
|
+
amountUsd: apiEquivalentTotal,
|
|
394
|
+
financialEvidence: apiEquivalentTotal === null ? "missing" : "estimated"
|
|
395
|
+
},
|
|
396
|
+
providerBilled: {
|
|
397
|
+
amountUsd: providerBilledTotal,
|
|
398
|
+
financialEvidence: providerBilledTotal === null ? "missing" : "verified"
|
|
399
|
+
},
|
|
400
|
+
blended: null,
|
|
401
|
+
blendPolicy: "never_blended"
|
|
402
|
+
};
|
|
403
|
+
return resultCardSchema.parse({
|
|
404
|
+
kind: "aibill.result_card",
|
|
405
|
+
schemaVersion: 1,
|
|
406
|
+
currency: "USD",
|
|
407
|
+
windowDays,
|
|
408
|
+
mode,
|
|
409
|
+
subscriptions,
|
|
410
|
+
totals,
|
|
411
|
+
byProject: buildByProject(records, mode, totals)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function buildByProject(records, mode, totals) {
|
|
415
|
+
// The card's primary basis follows the MODE (§3: local → api_equivalent,
|
|
416
|
+
// connected → provider_billed), falling back to the other basis only when
|
|
417
|
+
// the preferred one has no money at all. QA finding M3: the first verified
|
|
418
|
+
// billed row must never hijack by-project away from rich local attribution
|
|
419
|
+
// in local/mixed modes.
|
|
420
|
+
const preferred = mode === "connected" ? "provider_billed" : "api_equivalent";
|
|
421
|
+
const preferredTotal = preferred === "provider_billed"
|
|
422
|
+
? totals.providerBilled.amountUsd
|
|
423
|
+
: totals.apiEquivalent.amountUsd;
|
|
424
|
+
const fallback = preferred === "provider_billed" ? "api_equivalent" : "provider_billed";
|
|
425
|
+
const fallbackTotal = fallback === "provider_billed"
|
|
426
|
+
? totals.providerBilled.amountUsd
|
|
427
|
+
: totals.apiEquivalent.amountUsd;
|
|
428
|
+
const basis = preferredTotal !== null ? preferred : fallbackTotal !== null ? fallback : null;
|
|
429
|
+
if (basis === null)
|
|
430
|
+
return null;
|
|
431
|
+
const basisTotal = basis === "provider_billed"
|
|
432
|
+
? totals.providerBilled.amountUsd
|
|
433
|
+
: totals.apiEquivalent.amountUsd;
|
|
434
|
+
if (basisTotal <= 0)
|
|
435
|
+
return null;
|
|
436
|
+
const basisRecords = records.filter((record) => classifyRecordBasis(record, mode) === basis);
|
|
437
|
+
const named = new Map();
|
|
438
|
+
let unattributedAmount = 0;
|
|
439
|
+
for (const record of basisRecords) {
|
|
440
|
+
const amount = record.amountUsd ?? 0;
|
|
441
|
+
if (isUnattributedProject(record.projectId)) {
|
|
442
|
+
unattributedAmount += amount;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const key = record.projectId;
|
|
446
|
+
named.set(key, (named.get(key) ?? 0) + amount);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const namedSorted = [...named.entries()]
|
|
450
|
+
.filter(([, amount]) => amount > 0)
|
|
451
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
452
|
+
if (namedSorted.length === 0 && unattributedAmount <= 0)
|
|
453
|
+
return null;
|
|
454
|
+
// byProject spans the ENTIRE primary basis: top-2 named projects + the
|
|
455
|
+
// unattributed row (never hidden, never renamed) + everything else.
|
|
456
|
+
const topNamed = namedSorted.slice(0, 2);
|
|
457
|
+
const restNamed = namedSorted.slice(2);
|
|
458
|
+
const restAmount = restNamed.reduce((total, [, amount]) => total + amount, 0);
|
|
459
|
+
const buckets = [
|
|
460
|
+
...topNamed.map(([project, amount]) => ({ kind: "named", project, amount })),
|
|
461
|
+
...(unattributedAmount > 0
|
|
462
|
+
? [{ kind: "unattributed", amount: unattributedAmount }]
|
|
463
|
+
: []),
|
|
464
|
+
...(restNamed.length > 0
|
|
465
|
+
? [{ kind: "everything_else", amount: restAmount }]
|
|
466
|
+
: [])
|
|
467
|
+
];
|
|
468
|
+
// Exact reconciliation: cent-round every bucket, then absorb the residual
|
|
469
|
+
// rounding drift into the largest bucket so rows + everythingElse equal the
|
|
470
|
+
// basis total to the cent (≤$0.01 drift rule met by construction).
|
|
471
|
+
const rounded = buckets.map((bucket) => roundCents(bucket.amount));
|
|
472
|
+
const drift = roundCents(basisTotal - rounded.reduce((total, amount) => total + amount, 0));
|
|
473
|
+
if (drift !== 0) {
|
|
474
|
+
const largestIndex = rounded.reduce((best, amount, index) => (amount > (rounded[best] ?? 0) ? index : best), 0);
|
|
475
|
+
rounded[largestIndex] = roundCents((rounded[largestIndex] ?? 0) + drift);
|
|
476
|
+
}
|
|
477
|
+
// Machine shares: 4-decimal largest-remainder fractions summing to 1.0000.
|
|
478
|
+
const shareUnits = largestRemainderUnits(rounded, 10_000);
|
|
479
|
+
const shares = shareUnits.map((units) => units / 10_000);
|
|
480
|
+
const rows = [];
|
|
481
|
+
let everythingElse = null;
|
|
482
|
+
buckets.forEach((bucket, index) => {
|
|
483
|
+
if (bucket.kind === "everything_else") {
|
|
484
|
+
everythingElse = {
|
|
485
|
+
amountUsd: rounded[index] ?? 0,
|
|
486
|
+
share: shares[index] ?? 0,
|
|
487
|
+
projectCount: restNamed.length
|
|
488
|
+
};
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
rows.push({
|
|
492
|
+
project: bucket.kind === "unattributed"
|
|
493
|
+
? resultCardVocabulary.unattributed
|
|
494
|
+
: clipResultCardProjectName(bucket.project ?? ""),
|
|
495
|
+
amountUsd: rounded[index] ?? 0,
|
|
496
|
+
share: shares[index] ?? 0,
|
|
497
|
+
unattributed: bucket.kind === "unattributed"
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
if (rows.length === 0)
|
|
501
|
+
return null;
|
|
502
|
+
return { basis, rows, everythingElse };
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* The improve card's PROJECT line (§3): the CURRENT project's standing on the
|
|
506
|
+
* card's primary basis — one line, N=1 plus context. Returns undefined when
|
|
507
|
+
* no project attribution exists for the current directory (the line is
|
|
508
|
+
* omitted, never fabricated). Rank counts named projects; the unattributed
|
|
509
|
+
* bucket is excluded from ranking but included in the denominator total.
|
|
510
|
+
*/
|
|
511
|
+
export function buildResultCardProjectLine(input) {
|
|
512
|
+
const byProject = input.card.byProject;
|
|
513
|
+
if (!byProject)
|
|
514
|
+
return undefined;
|
|
515
|
+
const basis = byProject.basis;
|
|
516
|
+
const basisTotal = basis === "provider_billed"
|
|
517
|
+
? input.card.totals.providerBilled.amountUsd
|
|
518
|
+
: input.card.totals.apiEquivalent.amountUsd;
|
|
519
|
+
if (basisTotal === null || basisTotal <= 0)
|
|
520
|
+
return undefined;
|
|
521
|
+
const named = new Map();
|
|
522
|
+
let unattributedAmount = 0;
|
|
523
|
+
for (const record of input.records) {
|
|
524
|
+
if (classifyRecordBasis(record, input.card.mode) !== basis)
|
|
525
|
+
continue;
|
|
526
|
+
const amount = record.amountUsd ?? 0;
|
|
527
|
+
if (isUnattributedProject(record.projectId)) {
|
|
528
|
+
unattributedAmount += amount;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
named.set(record.projectId, (named.get(record.projectId) ?? 0) + amount);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const formatAmount = (amount) => basis === "provider_billed"
|
|
535
|
+
? formatBilledUsdExact(amount)
|
|
536
|
+
: formatApproxUsd(amount);
|
|
537
|
+
const basisWord = basis === "provider_billed" ? "billed" : "API-equivalent";
|
|
538
|
+
const windowSuffix = `${input.card.windowDays}d`;
|
|
539
|
+
// QA MINOR-2: one bucket, one percentage. When the current bucket is also
|
|
540
|
+
// a by-project row, reuse the card's largest-remainder display percent so
|
|
541
|
+
// the same money never shows two different shares across surfaces.
|
|
542
|
+
const bucketWeights = [
|
|
543
|
+
...byProject.rows.map((row) => row.amountUsd),
|
|
544
|
+
...(byProject.everythingElse ? [byProject.everythingElse.amountUsd] : [])
|
|
545
|
+
];
|
|
546
|
+
const bucketPercents = largestRemainderPercents(bucketWeights);
|
|
547
|
+
const cardPercentFor = (predicate) => {
|
|
548
|
+
const index = byProject.rows.findIndex(predicate);
|
|
549
|
+
return index >= 0 ? bucketPercents[index] : undefined;
|
|
550
|
+
};
|
|
551
|
+
if (input.currentProjectId === undefined || isUnattributedProject(input.currentProjectId)) {
|
|
552
|
+
if (unattributedAmount <= 0)
|
|
553
|
+
return undefined;
|
|
554
|
+
const amount = roundCents(unattributedAmount);
|
|
555
|
+
const percent = cardPercentFor((row) => row.unattributed) ??
|
|
556
|
+
Math.round((amount / basisTotal) * 100);
|
|
557
|
+
return `${resultCardVocabulary.unattributed} · ${formatAmount(amount)} of ` +
|
|
558
|
+
`${formatAmount(basisTotal)} ${basisWord} (${percent}%, ${windowSuffix})`;
|
|
559
|
+
}
|
|
560
|
+
const currentAmount = named.get(input.currentProjectId);
|
|
561
|
+
if (currentAmount === undefined || currentAmount <= 0)
|
|
562
|
+
return undefined;
|
|
563
|
+
const amount = roundCents(currentAmount);
|
|
564
|
+
const clippedName = clipResultCardProjectName(input.currentProjectId);
|
|
565
|
+
const percent = cardPercentFor((row) => !row.unattributed && row.project === clippedName) ??
|
|
566
|
+
Math.round((amount / basisTotal) * 100);
|
|
567
|
+
const rank = 1 + [...named.values()].filter((value) => value > currentAmount).length;
|
|
568
|
+
const projectCount = named.size;
|
|
569
|
+
return `${clippedName} · ${formatAmount(amount)} of ` +
|
|
570
|
+
`${formatAmount(basisTotal)} ${basisWord} (${percent}%, ${windowSuffix}) · ` +
|
|
571
|
+
`rank ${rank} of ${projectCount} project${projectCount === 1 ? "" : "s"}`;
|
|
572
|
+
}
|
|
573
|
+
// --- shared renderer formatting (one grammar for every surface, §1.2) -------
|
|
574
|
+
/** `committed $320/mo` amount part: "$320/mo" (whole dollars, list prices). */
|
|
575
|
+
export function formatCommittedPerMonth(amountUsd) {
|
|
576
|
+
return `$${formatWholeUsdNumber(amountUsd)}/mo`;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* API-equivalent figures always carry `~` and round to whole dollars.
|
|
580
|
+
* Real-but-tiny usage prints `~<$1` (QA MINOR-4): `~$0` reads as absence.
|
|
581
|
+
*/
|
|
582
|
+
export function formatApproxUsd(amountUsd) {
|
|
583
|
+
if (amountUsd > 0 && Math.round(amountUsd) === 0)
|
|
584
|
+
return "~<$1";
|
|
585
|
+
return `~$${formatWholeUsdNumber(amountUsd)}`;
|
|
586
|
+
}
|
|
587
|
+
function formatWholeUsdNumber(amountUsd) {
|
|
588
|
+
return Math.round(amountUsd).toLocaleString("en-US");
|
|
589
|
+
}
|
|
590
|
+
/** Provider-billed money is never compacted, rounded, or approximated. */
|
|
591
|
+
export function formatBilledUsdExact(amountUsd) {
|
|
592
|
+
const text = String(amountUsd);
|
|
593
|
+
if (/[eE]/u.test(text))
|
|
594
|
+
return `$${text}`;
|
|
595
|
+
const [whole, fraction] = text.split(".");
|
|
596
|
+
const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/gu, ",");
|
|
597
|
+
const decimal = fraction === undefined
|
|
598
|
+
? ".00"
|
|
599
|
+
: fraction.length === 1
|
|
600
|
+
? `.${fraction}0`
|
|
601
|
+
: `.${fraction}`;
|
|
602
|
+
return `$${grouped}${decimal}`;
|
|
603
|
+
}
|
|
604
|
+
//# sourceMappingURL=resultCard.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AibillImproveDeliveryV0 = "source_preview" | "published";
|
|
2
|
+
/**
|
|
3
|
+
* Distribution truth for the guided action loop.
|
|
4
|
+
*
|
|
5
|
+
* Release gate: change this single value to `published` only in the exact,
|
|
6
|
+
* coordinated new-version release candidate. Its packed-install gate must
|
|
7
|
+
* prove every generated handoff uses the new package command before publish;
|
|
8
|
+
* the clean public-registry smoke then verifies that same exact commit. Until
|
|
9
|
+
* that release candidate exists, handoffs execute the already-built checkout
|
|
10
|
+
* and must not let `npx` download the older public package.
|
|
11
|
+
*/
|
|
12
|
+
export declare const AIBILL_IMPROVE_DELIVERY_V0: AibillImproveDeliveryV0;
|
|
13
|
+
/**
|
|
14
|
+
* Build a command for capabilities that exist only in the current source
|
|
15
|
+
* preview. Keep every generated handoff on the checkout until the coordinated
|
|
16
|
+
* npm release containing those capabilities has passed its registry smoke.
|
|
17
|
+
*/
|
|
18
|
+
export declare function aibillCommandV0(args: string, delivery?: AibillImproveDeliveryV0): string;
|
|
19
|
+
/** One privacy-safe command shared by terminal, MCP, and Glance. */
|
|
20
|
+
export declare function aibillImproveCommandV0(delivery?: AibillImproveDeliveryV0): string;
|
|
21
|
+
/**
|
|
22
|
+
* Version-pinned command for machine-composed lines (M4c): a command an AI
|
|
23
|
+
* client relays to a human must be reproducible and must not silently
|
|
24
|
+
* resolve to a different release, so `draft_improve_command` pins to the
|
|
25
|
+
* composing package's own version (`npx aibill@<version> …`). A version
|
|
26
|
+
* that is not a plain semver falls back to the unpinned published command
|
|
27
|
+
* rather than composing an unrunnable line. In source-preview builds the
|
|
28
|
+
* checkout command needs no pin.
|
|
29
|
+
*
|
|
30
|
+
* Release gate (n2): the coordinated release must also prove the pinned
|
|
31
|
+
* version EXISTS on the public registry and supports the composed flags —
|
|
32
|
+
* the packed-install gate described on AIBILL_IMPROVE_DELIVERY_V0 is the
|
|
33
|
+
* natural home for that check; QA 24 asserts only that the pin is present.
|
|
34
|
+
*/
|
|
35
|
+
export declare function aibillPinnedCommandV0(args: string, version: string, delivery?: AibillImproveDeliveryV0): string;
|
|
36
|
+
//# sourceMappingURL=runtimeCommands.d.ts.map
|