@agent-finops/report 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1082 @@
1
+ export { generatePlainEnglishSummary, groupByDimensions } from "./terminal.js";
2
+ export { generateReportCardSvg, generateReportCardCaption } from "./reportCard.js";
3
+ export function generateMarkdownReport(input) {
4
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
5
+ const mappingQuestions = (input.mappings ?? []).filter((mapping) => mapping.status !== "auto_mapped");
6
+ const recommendations = [...input.summary.recommendations].sort(compareRecommendations);
7
+ const insights = [...(input.summary.insights ?? [])].sort(compareInsights);
8
+ const totalEstimatedImpactUsd = recommendations.reduce((total, recommendation) => total + recommendation.estimatedImpactUsd, 0);
9
+ const lines = [
10
+ "# AI Spend Analyst Report",
11
+ "",
12
+ `Generated: ${generatedAt}`,
13
+ "",
14
+ "> Local-first report. No files, credentials, invoices, or raw spend data were uploaded. Costs are confidence-labeled.",
15
+ "",
16
+ "## Executive summary",
17
+ "",
18
+ `- Total tracked spend: ${formatUsd(input.summary.totalUsd)}`,
19
+ `- Records analyzed: ${input.summary.recordCount}`,
20
+ `- Overall confidence: ${input.summary.confidence}`,
21
+ `- Discovery signals: ${input.discovery?.signals.length ?? 0}`,
22
+ `- Mapping questions: ${mappingQuestions.length}`,
23
+ `- Estimated optimization impact: ${formatUsd(totalEstimatedImpactUsd)}`,
24
+ "",
25
+ "## Diagnose → Recommend → Apply → Verify",
26
+ "",
27
+ ...operatingLoopMarkdownLines(input.summary, recommendations, insights),
28
+ "",
29
+ "## Board brief",
30
+ "",
31
+ "- Decision needed: approve the top local optimization actions before connecting more sources.",
32
+ `- Current readout: ${formatUsd(input.summary.totalUsd)} tracked across ${input.summary.recordCount} local records with ${input.summary.confidence} confidence.`,
33
+ `- Biggest cost driver: ${topDriverLine(input.summary.byModel)}`,
34
+ `- Attribution risk: ${mappingQuestions.length} mapping question${mappingQuestions.length === 1 ? "" : "s"} need confirmation before this becomes finance-grade.`,
35
+ `- Savings thesis: ${formatUsd(totalEstimatedImpactUsd)} in near-term estimated impact from ${recommendations.length} local recommendations.`,
36
+ "",
37
+ "## Confidence breakdown",
38
+ "",
39
+ ...confidenceBreakdownLines(input.summary),
40
+ "",
41
+ "## Evidence quality ledger",
42
+ "",
43
+ ...evidenceLedgerMarkdownLines(input.providerRecords ?? []),
44
+ "",
45
+ "## Provider-by-provider live QA",
46
+ "",
47
+ ...providerQaMarkdownLines(input.providerQa ?? []),
48
+ "",
49
+ "## Spend by source",
50
+ ...breakdownLines(input.summary.bySource),
51
+ "",
52
+ "## Spend by model",
53
+ "",
54
+ ...breakdownLines(input.summary.byModel),
55
+ "",
56
+ "## Spend by client",
57
+ "",
58
+ ...breakdownLines(input.summary.byClient),
59
+ "",
60
+ "## Spend by project",
61
+ "",
62
+ ...breakdownLines(input.summary.byProject),
63
+ "",
64
+ "## Spend by agent",
65
+ "",
66
+ ...breakdownLines(input.summary.byAgent),
67
+ "",
68
+ "## Enterprise entity spend",
69
+ "",
70
+ "### Spend by user",
71
+ "",
72
+ ...breakdownLines(input.summary.byUser),
73
+ "",
74
+ "### Spend by workspace / team",
75
+ "",
76
+ ...breakdownLines(input.summary.byWorkspace),
77
+ "",
78
+ "### Spend by API key",
79
+ "",
80
+ ...breakdownLines(input.summary.byApiKey),
81
+ "",
82
+ "## Agency margin and workflow watch",
83
+ "",
84
+ ...workflowWatchMarkdownLines(input.summary.workflowWatch),
85
+ "",
86
+ "## Source coverage and connection gaps",
87
+ "",
88
+ ...sourceCoverageMarkdownLines(input),
89
+ "",
90
+ "## Confirmed mappings",
91
+ "",
92
+ ...confirmedMappingMarkdownLines(input.confirmedMappings ?? []),
93
+ "",
94
+ "## Anomalies and likely causes",
95
+ "",
96
+ ...(input.summary.anomalies.length === 0
97
+ ? ["No deterministic anomaly detected in this sample window."]
98
+ : input.summary.anomalies.map((anomaly) => `- ${anomaly.key}: ${formatUsd(anomaly.previousAmountUsd)} → ${formatUsd(anomaly.currentAmountUsd)} (${anomaly.multiplier.toFixed(1)}x, ${anomaly.confidence})`)),
99
+ "",
100
+ "## Mapping questions",
101
+ "",
102
+ ...(mappingQuestions.length === 0
103
+ ? ["No mapping questions. Current records were auto-mapped by deterministic sample metadata."]
104
+ : mappingQuestions.map((mapping) => `- ${mapping.usageRecordId}: ${mapping.status}. Evidence: ${mapping.evidence.join("; ")}`)),
105
+ "",
106
+ "## Analyst insights",
107
+ "",
108
+ ...insightMarkdownLines(insights),
109
+ "",
110
+ "## Priority recommendations",
111
+ "",
112
+ ...(recommendations.length === 0
113
+ ? ["No recommendations generated from the current sample."]
114
+ : recommendations.flatMap((recommendation) => [
115
+ `- **${recommendation.title}** (${recommendation.confidence})`,
116
+ ` - Priority: ${recommendation.priority}`,
117
+ ` - Estimated impact: ${formatUsd(recommendation.estimatedImpactUsd)}`,
118
+ ` - Rationale: ${recommendation.rationale}`,
119
+ ` - Why it matters: ${recommendation.whyItMatters}`,
120
+ ` - Next action: ${recommendation.nextAction}`
121
+ ])),
122
+ "",
123
+ "## Board action plan",
124
+ "",
125
+ ...boardActionPlanLines(recommendations, mappingQuestions.length),
126
+ "",
127
+ "## Next source to connect",
128
+ "",
129
+ nextSourceLine(input),
130
+ ""
131
+ ];
132
+ return lines.join("\n");
133
+ }
134
+ export function generateApplyArtifactMarkdown(input) {
135
+ const watch = input.summary.workflowWatch;
136
+ const lines = [
137
+ "# AI Spend Apply Artifact",
138
+ "",
139
+ "> Low-risk Apply artifact. Copy this into your coding agent to cut cost, then verify before rollout.",
140
+ "",
141
+ "## Target workflow",
142
+ ""
143
+ ];
144
+ if (watch.length === 0) {
145
+ lines.push("No workflow watch entries were generated. Add client, project, agent, and operation metadata first.");
146
+ }
147
+ else {
148
+ const top = watch[0];
149
+ lines.push(`- Workflow: ${top.clientId} / ${top.projectId} / ${top.workflowKey}`, `- Agent: ${top.agentId}`, `- Current spend: ${formatUsd(top.amountUsd)} (${formatPercent(top.shareOfSpend)} of tracked spend)`, `- Estimated savings: ${formatUsd(top.estimatedSavingsUsd)}`, `- Margin at risk: ${formatUsd(top.estimatedMarginRiskUsd)}`, "", "## Copy this into your coding agent", "", "```text", `You are optimizing the ${top.workflowKey} workflow for client ${top.clientId} / project ${top.projectId}.`, `Goal: reduce AI spend by about ${formatUsd(top.estimatedSavingsUsd)} without lowering delivery quality.`, `Change request: ${top.suggestedOptimization}`, "Constraints: keep outputs functionally equivalent, preserve tests, do not add cloud uploads, keep the workflow local-first unless explicitly approved. Do not change user-visible quality thresholds without approval.", `Verification: ${top.verificationPlan}`, "Return a small diff, explain expected savings, and include rollback steps.", "```", "", "## Verification plan", "", `- ${top.verificationPlan}`, "- Compare cost, latency, and output acceptance against the pre-change baseline.", "- Roll back if quality drops or costs move in the wrong direction.");
150
+ }
151
+ lines.push("", "## Full watchlist", "", ...workflowWatchMarkdownLines(watch), "");
152
+ return lines.join("\n");
153
+ }
154
+ export function generateActionPlanMarkdown(input) {
155
+ const recommendations = [...input.summary.recommendations].sort(compareRecommendations);
156
+ const watch = input.summary.workflowWatch[0];
157
+ return [
158
+ "# AI Spend Action Plan",
159
+ "",
160
+ "> Human-approved next actions generated from the local report. Do not apply changes automatically.",
161
+ "",
162
+ "## Immediate actions",
163
+ "",
164
+ ...(recommendations.length === 0 ? ["- No optimization recommendations generated yet. Connect richer source data or confirm mappings first."] : recommendations.slice(0, 3).flatMap((recommendation, index) => [
165
+ `${index + 1}. **${recommendation.title}**`,
166
+ ` - Estimated impact: ${formatUsd(recommendation.estimatedImpactUsd)} (${recommendation.confidence})`,
167
+ ` - Do next: ${recommendation.nextAction}`,
168
+ ` - Evidence: ${recommendation.rationale}`
169
+ ])),
170
+ "",
171
+ "## Owner handoff",
172
+ "",
173
+ `- Primary workflow: ${watch ? `${watch.clientId} / ${watch.projectId} / ${watch.workflowKey}` : "not enough mapped workflow data yet"}`,
174
+ "- Approval needed: owner confirms quality bar, acceptable latency, and rollback trigger before any change ships.",
175
+ "- Output expected: one small diff or config/policy change plus before/after measurements.",
176
+ ""
177
+ ].join("\n");
178
+ }
179
+ export function generatePolicyConfigDraftMarkdown(input) {
180
+ const watch = input.summary.workflowWatch[0];
181
+ const topRecommendation = [...input.summary.recommendations].sort(compareRecommendations)[0];
182
+ return [
183
+ "# AI Spend Policy / Config Draft",
184
+ "",
185
+ "> Low-risk draft for a human to copy into a repo, MCP config, or team policy. It is not applied automatically.",
186
+ "",
187
+ "```yaml",
188
+ "aiSpendPolicy:",
189
+ " cloudUpload: false",
190
+ " humanApproved: true",
191
+ ` targetWorkflow: ${yamlString(watch ? `${watch.clientId}/${watch.projectId}/${watch.workflowKey}` : "unmapped")}`,
192
+ ` targetAgent: ${yamlString(watch?.agentId ?? "unmapped")}`,
193
+ ` currentTrackedSpendUsd: ${input.summary.totalUsd.toFixed(2)}`,
194
+ ` expectedSavingsUsd: ${(watch?.estimatedSavingsUsd ?? topRecommendation?.estimatedImpactUsd ?? 0).toFixed(2)}`,
195
+ " allowedApplyModes:",
196
+ " - coding_agent_prompt",
197
+ " - policy_draft",
198
+ " - config_draft",
199
+ " blockedApplyModes:",
200
+ " - automatic_live_routing",
201
+ " - gateway_proxy_changes",
202
+ " - hard_budget_kill_switches",
203
+ " verification:",
204
+ " compareBeforeAfterSpend: true",
205
+ " compareLatency: true",
206
+ " compareOutputAcceptance: true",
207
+ " rollbackOnQualityDrop: true",
208
+ "```",
209
+ "",
210
+ "## Policy notes",
211
+ "",
212
+ "- Treat verified spend, estimated spend, usage evidence, and missing cost data separately.",
213
+ "- Keep source connectors read-only until an owner explicitly approves write-capable changes.",
214
+ "- Use the verification plan before expanding beyond the first workflow.",
215
+ ""
216
+ ].join("\n");
217
+ }
218
+ export function generateVerificationPlanMarkdown(input) {
219
+ const watch = input.summary.workflowWatch[0];
220
+ return [
221
+ "# AI Spend Verification Plan",
222
+ "",
223
+ "> Prove savings before rollout. This is the controller checklist for the Apply step.",
224
+ "",
225
+ "## Before baseline",
226
+ "",
227
+ `- Tracked spend: ${formatUsd(input.summary.totalUsd)}`,
228
+ `- Target workflow spend: ${watch ? formatUsd(watch.amountUsd) : "not available"}`,
229
+ `- Target workflow records: ${watch?.recordCount ?? 0}`,
230
+ `- Confidence: ${watch?.confidence ?? input.summary.confidence}`,
231
+ "- Capture latency, acceptance/QA result, and any human override notes before changing anything.",
232
+ "",
233
+ "## After-change check",
234
+ "",
235
+ "- Rerun the same workflow/sample window.",
236
+ "- Compare spend, latency, error rate, and output acceptance side by side.",
237
+ `- Expected savings: ${watch ? formatUsd(watch.estimatedSavingsUsd) : "unknown until a workflow is mapped"}`,
238
+ "- Mark result as verified only if cost decreases and quality remains acceptable.",
239
+ "",
240
+ "## Rollback triggers",
241
+ "",
242
+ "- Output quality drops or requires extra human repair.",
243
+ "- Latency worsens enough to affect delivery.",
244
+ "- Cost does not improve on the same sample window.",
245
+ "- Source confidence is still missing for the cost being optimized.",
246
+ ""
247
+ ].join("\n");
248
+ }
249
+ export function generateDemoPackageMarkdown(input) {
250
+ return [
251
+ "# AI Spend Analyst Demo Package",
252
+ "",
253
+ "## Demo command flow",
254
+ "",
255
+ "```bash",
256
+ "ai-spend-agent init --path ./demo-workspace",
257
+ "ai-spend-agent doctor --path ./demo-workspace",
258
+ "ai-spend-agent scan --sample --path ./demo-workspace",
259
+ "ai-spend-agent report --path ./demo-workspace",
260
+ "ai-spend-agent apply-artifact --path ./demo-workspace",
261
+ "```",
262
+ "",
263
+ "## What the buyer should understand in 10 seconds",
264
+ "",
265
+ `- The agent found ${formatUsd(input.summary.totalUsd)} of tracked AI spend across ${input.summary.recordCount} records.`,
266
+ `- It generated ${input.summary.recommendations.length} ranked optimization recommendation(s).`,
267
+ `- It labels confidence as ${input.summary.confidence} and separates verified, estimated, usage-only, and missing cost evidence.`,
268
+ "- It outputs local reports and human-approved Apply/Verify artifacts before any automation.",
269
+ "",
270
+ "## Demo artifacts",
271
+ "",
272
+ "- `report.md` and `report.html`: board-ready readout.",
273
+ "- `ai-spend-coding-agent-prompt.md`: copyable coding-agent task.",
274
+ "- `ai-spend-action-plan.md`: operator action list.",
275
+ "- `ai-spend-policy-config-draft.md`: low-risk policy/config draft.",
276
+ "- `ai-spend-verify-plan.md`: before/after savings and quality check.",
277
+ "",
278
+ "## QA controller checklist",
279
+ "",
280
+ "- [ ] No arbitrary home scan was used.",
281
+ "- [ ] No raw secrets appear in stdout or generated artifacts.",
282
+ "- [ ] Report and artifacts use confidence language, not overclaims.",
283
+ "- [ ] Apply steps are human-approved and low-risk only.",
284
+ "- [ ] Demo flow completes from init to Apply/Verify artifacts in under 15 minutes.",
285
+ ""
286
+ ].join("\n");
287
+ }
288
+ function yamlString(value) {
289
+ return JSON.stringify(value);
290
+ }
291
+ export function generateHtmlReport(input) {
292
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
293
+ const mappingQuestions = (input.mappings ?? []).filter((mapping) => mapping.status !== "auto_mapped");
294
+ const recommendations = [...input.summary.recommendations].sort(compareRecommendations);
295
+ const insights = [...(input.summary.insights ?? [])].sort(compareInsights);
296
+ const totalEstimatedImpactUsd = recommendations.reduce((total, recommendation) => total + recommendation.estimatedImpactUsd, 0);
297
+ const topRecommendation = recommendations[0];
298
+ return `<!doctype html>
299
+ <html lang="en">
300
+ <head>
301
+ <meta charset="utf-8" />
302
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
303
+ <title>AI Spend Analyst Report</title>
304
+ <style>${premiumReportCss()}</style>
305
+ </head>
306
+ <body>
307
+ <main class="report-shell" aria-labelledby="report-title">
308
+ <section class="hero-panel">
309
+ <div class="report-kicker">AI Spend Analyst · Local report</div>
310
+ <div class="hero-grid">
311
+ <div>
312
+ <h1 id="report-title">Board-ready spend readout</h1>
313
+ <p class="hero-copy">A client-facing artifact for deciding which AI costs to verify, optimize, and assign owners to next.</p>
314
+ </div>
315
+ <div class="hero-meta" aria-label="Report metadata">
316
+ <span>Generated</span>
317
+ <strong>${escapeHtml(generatedAt)}</strong>
318
+ <span>Confidence status</span>
319
+ <strong>${escapeHtml(formatConfidenceLabel(input.summary.confidence))}</strong>
320
+ </div>
321
+ </div>
322
+ <aside class="privacy-banner" aria-label="Privacy posture">
323
+ <span class="privacy-dot" aria-hidden="true"></span>
324
+ <strong>Local files only. No cloud upload.</strong>
325
+ <span>No credentials, invoices, or raw spend data leave the machine.</span>
326
+ </aside>
327
+ </section>
328
+
329
+ <section class="metric-grid" aria-label="Executive metrics">
330
+ ${metricCard("Tracked spend", formatUsd(input.summary.totalUsd), `${input.summary.recordCount} local records`, "primary")}
331
+ ${metricCard("Optimization impact", formatUsd(totalEstimatedImpactUsd), `${recommendations.length} ranked recommendations`)}
332
+ ${metricCard("Mapping questions", String(mappingQuestions.length), "Need confirmation for finance-grade attribution")}
333
+ ${metricCard("Discovery signals", String(input.discovery?.signals.length ?? 0), "Local source hints found during scan")}
334
+ </section>
335
+
336
+ <section class="operating-loop" aria-label="Diagnose recommend apply verify operating loop">
337
+ <div class="section-heading">
338
+ <div>
339
+ <div class="section-label">Operating loop</div>
340
+ <h2>Diagnose → Recommend → Apply → Verify</h2>
341
+ </div>
342
+ <span class="impact-pill">Human-approved before rollout</span>
343
+ </div>
344
+ <div class="loop-grid">
345
+ ${operatingLoopCards(input.summary, recommendations, insights).join("\n")}
346
+ </div>
347
+ </section>
348
+
349
+ <section class="artifact-grid">
350
+ <article class="artifact-card artifact-card--wide">
351
+ <div class="section-label">Board brief</div>
352
+ <h2>Decision needed before adding more sources</h2>
353
+ <ul class="brief-list">
354
+ <li><span>Current readout</span><strong>${formatUsd(input.summary.totalUsd)} across ${input.summary.recordCount} records</strong></li>
355
+ <li><span>Biggest cost driver</span><strong>${escapeHtml(topDriverLine(input.summary.byModel))}</strong></li>
356
+ <li><span>Attribution risk</span><strong>${mappingQuestions.length} mapping question${mappingQuestions.length === 1 ? "" : "s"}</strong></li>
357
+ <li><span>Savings thesis</span><strong>${formatUsd(totalEstimatedImpactUsd)} near-term estimated impact</strong></li>
358
+ </ul>
359
+ </article>
360
+
361
+ <article class="artifact-card">
362
+ <div class="section-label">Confidence</div>
363
+ <h2>Cost confidence mix</h2>
364
+ <div class="stacked-bars" aria-label="Confidence breakdown">
365
+ ${confidenceBarSegments(input.summary)}
366
+ </div>
367
+ <div class="mini-breakdown">
368
+ ${confidenceBreakdownHtml(input.summary)}
369
+ </div>
370
+ </article>
371
+ </section>
372
+
373
+ <section class="evidence-quality" aria-label="Evidence quality ledger">
374
+ <div class="section-heading">
375
+ <div>
376
+ <div class="section-label">Evidence quality ledger</div>
377
+ <h2>Verified spend, estimates, usage evidence, and missing costs stay separate</h2>
378
+ </div>
379
+ <span class="impact-pill">No silent allocation</span>
380
+ </div>
381
+ <div class="evidence-quality-grid">
382
+ ${evidenceLedgerHtml(input.providerRecords ?? [])}
383
+ </div>
384
+ </section>
385
+
386
+ <section class="provider-qa" aria-label="Provider-by-provider live QA">
387
+ <div class="section-heading">
388
+ <div>
389
+ <div class="section-label">Provider-by-provider live QA</div>
390
+ <h2>API response drift, pagination, rate limits, and source-specific instructions</h2>
391
+ </div>
392
+ <span class="impact-pill">${input.providerQa?.length ?? 0} provider${(input.providerQa?.length ?? 0) === 1 ? "" : "s"}</span>
393
+ </div>
394
+ <div class="provider-qa-grid">
395
+ ${providerQaHtml(input.providerQa ?? [])}
396
+ </div>
397
+ </section>
398
+
399
+ <section class="analyst-insights" aria-label="Analyst insights">
400
+ <div class="section-heading">
401
+ <div>
402
+ <div class="section-label">Analyst insights</div>
403
+ <h2>What the agent thinks is happening</h2>
404
+ </div>
405
+ <span class="impact-pill">${insights.length} ranked finding${insights.length === 1 ? "" : "s"}</span>
406
+ </div>
407
+ <div class="insight-grid">
408
+ ${insights.length === 0 ? emptyState("No analyst insights generated yet. Run a scan with enough local spend history to surface ranked findings.") : insights.map(insightCard).join("\n")}
409
+ </div>
410
+ </section>
411
+
412
+ <section class="workflow-watch" aria-label="Agency margin and workflow watch">
413
+ <div class="section-heading">
414
+ <div>
415
+ <div class="section-label">Agency margin + workflow optimization</div>
416
+ <h2>Which clients, projects, agents, and workflows are eating margin</h2>
417
+ </div>
418
+ <span class="impact-pill">${input.summary.workflowWatch.length} watched workflow${input.summary.workflowWatch.length === 1 ? "" : "s"}</span>
419
+ </div>
420
+ <div class="workflow-chart">
421
+ ${input.summary.workflowWatch.length === 0 ? emptyState("No workflow watch entries yet. Add client, project, agent, and operation metadata to surface margin risk.") : input.summary.workflowWatch.map(workflowWatchCard).join("\n")}
422
+ </div>
423
+ </section>
424
+
425
+ <section class="entity-spend" aria-label="Enterprise entity spend">
426
+ <div class="section-heading">
427
+ <div>
428
+ <div class="section-label">Enterprise entity spend</div>
429
+ <h2>User, workspace/team, and API-key attribution</h2>
430
+ </div>
431
+ <span class="impact-pill">Auditable source signals</span>
432
+ </div>
433
+ <div class="source-detail-grid">
434
+ <article class="source-detail-card">
435
+ <h3>By user</h3>
436
+ <div class="entity-breakdown-list">
437
+ ${entityBreakdownHtml(input.summary.byUser)}
438
+ </div>
439
+ </article>
440
+ <article class="source-detail-card">
441
+ <h3>By workspace / team</h3>
442
+ <div class="entity-breakdown-list">
443
+ ${entityBreakdownHtml(input.summary.byWorkspace)}
444
+ </div>
445
+ </article>
446
+ <article class="source-detail-card">
447
+ <h3>By API key</h3>
448
+ <div class="entity-breakdown-list">
449
+ ${entityBreakdownHtml(input.summary.byApiKey)}
450
+ </div>
451
+ </article>
452
+ </div>
453
+ </section>
454
+
455
+ <section class="source-coverage" aria-label="Source coverage and connection gaps">
456
+ <div class="section-heading">
457
+ <div>
458
+ <div class="section-label">Source coverage</div>
459
+ <h2>What is connected, what is detected, and what is still missing</h2>
460
+ </div>
461
+ <span class="impact-pill">${input.sourceRegistry?.approvedSources.length ?? 0} approved source${(input.sourceRegistry?.approvedSources.length ?? 0) === 1 ? "" : "s"}</span>
462
+ </div>
463
+ <div class="source-lane-grid">
464
+ ${sourceLaneCards(input.sourceRegistry).join("\n")}
465
+ </div>
466
+ <div class="source-detail-grid">
467
+ <article class="source-detail-card">
468
+ <h3>Connection gaps</h3>
469
+ <div class="missing-source-list">
470
+ ${missingSourcePromptHtml(input.missingSourcePrompts ?? [])}
471
+ </div>
472
+ </article>
473
+ <article class="source-detail-card">
474
+ <h3>Confirmed mappings</h3>
475
+ <div class="confirmed-mapping-list">
476
+ ${confirmedMappingHtml(input.confirmedMappings ?? [])}
477
+ </div>
478
+ </article>
479
+ </div>
480
+ </section>
481
+
482
+ <section class="recommendations-section" aria-label="Priority recommendations">
483
+ <div class="section-heading">
484
+ <div>
485
+ <div class="section-label">Priority recommendations</div>
486
+ <h2>What to do next</h2>
487
+ </div>
488
+ <span class="impact-pill">${formatUsd(totalEstimatedImpactUsd)} estimated impact</span>
489
+ </div>
490
+ <div class="recommendation-grid">
491
+ ${recommendations.length === 0 ? emptyState("No recommendations generated from the current sample.") : recommendations.map(recommendationCard).join("\n")}
492
+ </div>
493
+ </section>
494
+
495
+ <section class="artifact-grid artifact-grid--bottom">
496
+ <article class="artifact-card">
497
+ <div class="section-label">Board action plan</div>
498
+ <h2>Owner-ready next moves</h2>
499
+ <ol class="board-action-list">
500
+ ${boardActionPlanLines(recommendations, mappingQuestions.length).map((line) => `<li>${escapeHtml(stripOrderedPrefix(line))}</li>`).join("\n")}
501
+ </ol>
502
+ </article>
503
+ <article class="artifact-card">
504
+ <div class="section-label">Next source</div>
505
+ <h2>Connect only after the baseline is useful</h2>
506
+ <p>${escapeHtml(nextSourceLine(input))}</p>
507
+ ${topRecommendation ? `<div class="callout"><span>First action</span><strong>${escapeHtml(topRecommendation.nextAction)}</strong></div>` : ""}
508
+ </article>
509
+ </section>
510
+ </main>
511
+ </body>
512
+ </html>`;
513
+ }
514
+ function compareRecommendations(left, right) {
515
+ const priorityRank = { high: 0, medium: 1, low: 2 };
516
+ return (priorityRank[left.priority] - priorityRank[right.priority] ||
517
+ right.estimatedImpactUsd - left.estimatedImpactUsd ||
518
+ left.title.localeCompare(right.title));
519
+ }
520
+ function compareInsights(left, right) {
521
+ const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
522
+ return (severityRank[left.severity] - severityRank[right.severity] ||
523
+ right.estimatedImpactUsd - left.estimatedImpactUsd ||
524
+ right.evidence.length - left.evidence.length ||
525
+ left.title.localeCompare(right.title));
526
+ }
527
+ function operatingLoopMarkdownLines(summary, recommendations, insights) {
528
+ const topWorkflow = summary.workflowWatch[0];
529
+ const topRecommendation = recommendations[0];
530
+ const topInsight = insights[0];
531
+ return [
532
+ `1. **Diagnose the leak:** ${topInsight ? topInsight.title : topWorkflow ? `${topWorkflow.clientId} / ${topWorkflow.projectId} / ${topWorkflow.workflowKey}` : `${formatUsd(summary.totalUsd)} tracked spend baseline`}.`,
533
+ `2. **Recommend a change:** ${topRecommendation ? topRecommendation.nextAction : "Collect more usage evidence before changing the workflow."}`,
534
+ `3. **Apply safely:** ${topWorkflow ? workflowApplyArtifact(topWorkflow) : "Generate a human-approved Apply artifact once a workflow watch entry exists."}`,
535
+ `4. **Verify savings:** ${topWorkflow ? workflowVerificationPlan(topWorkflow) : "Compare before/after cost, latency, and accepted output quality before rollout."}`
536
+ ];
537
+ }
538
+ function operatingLoopCards(summary, recommendations, insights) {
539
+ const topWorkflow = summary.workflowWatch[0];
540
+ const topRecommendation = recommendations[0];
541
+ const topInsight = insights[0];
542
+ return [
543
+ loopCard("01", "Diagnose the leak", topInsight ? topInsight.title : topWorkflow ? `${topWorkflow.clientId} / ${topWorkflow.workflowKey}` : `${formatUsd(summary.totalUsd)} tracked baseline`, topInsight?.summary ?? "Locate the client, project, agent, model, or workflow where spend is leaking margin."),
544
+ loopCard("02", "Recommend a change", topRecommendation ? formatUsd(topRecommendation.estimatedImpactUsd) : "Evidence first", topRecommendation?.nextAction ?? "Wait for enough local evidence before recommending workflow changes."),
545
+ loopCard("03", "Apply safely", topWorkflow ? workflowApplyArtifact(topWorkflow) : "Human-approved artifact", topWorkflow?.suggestedOptimization ?? "Generate a copy/paste implementation prompt, policy draft, or config task before touching production."),
546
+ loopCard("04", "Verify savings", topWorkflow ? `${formatUsd(topWorkflow.estimatedSavingsUsd)} target` : "Before/after proof", topWorkflow ? workflowVerificationPlan(topWorkflow) : "Compare cost, latency, and accepted output quality against the baseline.")
547
+ ];
548
+ }
549
+ function loopCard(step, title, value, body) {
550
+ return `<article class="loop-card">
551
+ <span class="loop-step">${escapeHtml(step)}</span>
552
+ <h3>${escapeHtml(title)}</h3>
553
+ <strong>${escapeHtml(value)}</strong>
554
+ <p>${escapeHtml(body)}</p>
555
+ </article>`;
556
+ }
557
+ function insightMarkdownLines(insights) {
558
+ if (insights.length === 0) {
559
+ return ["No analyst insights generated yet. Run a scan with enough local spend history to surface ranked findings."];
560
+ }
561
+ return insights.flatMap((insight) => [
562
+ `- **${insight.title}** (${insight.severity}, ${insight.confidence})`,
563
+ ` - Estimated impact: ${formatUsd(insight.estimatedImpactUsd)}`,
564
+ ` - Summary: ${insight.summary}`,
565
+ ` - Affected: ${affectedEntitiesLine(insight)}`,
566
+ ` - Evidence: ${insight.evidence.map((item) => `${item.label}: ${item.value}${item.detail ? ` (${item.detail})` : ""}`).join("; ")}`,
567
+ ` - Recommended action: ${insight.recommendedAction}`,
568
+ ...(insight.verificationNeeded ? [` - Verification needed: ${insight.verificationNeeded}`] : [])
569
+ ]);
570
+ }
571
+ function affectedEntitiesLine(insight) {
572
+ const parts = [
573
+ insight.affectedClients.length > 0 ? `clients ${insight.affectedClients.join(", ")}` : undefined,
574
+ insight.affectedProjects.length > 0 ? `projects ${insight.affectedProjects.join(", ")}` : undefined,
575
+ insight.affectedAgents.length > 0 ? `agents ${insight.affectedAgents.join(", ")}` : undefined,
576
+ insight.affectedModels.length > 0 ? `models ${insight.affectedModels.join(", ")}` : undefined
577
+ ].filter((part) => part !== undefined);
578
+ return parts.length > 0 ? parts.join("; ") : "global spend baseline";
579
+ }
580
+ function insightCard(insight) {
581
+ return `<article class="insight-card insight-card--${escapeHtml(insight.severity)}">
582
+ <div class="insight-topline">
583
+ <span class="severity-badge severity-badge--${escapeHtml(insight.severity)}">${escapeHtml(insight.severity)}</span>
584
+ <span class="confidence-chip">${escapeHtml(insight.confidence)}</span>
585
+ </div>
586
+ <h3>${escapeHtml(insight.title)}</h3>
587
+ <p>${escapeHtml(insight.summary)}</p>
588
+ <dl class="insight-facts">
589
+ <div><dt>Impact</dt><dd>${formatUsd(insight.estimatedImpactUsd)}</dd></div>
590
+ <div><dt>Affected</dt><dd>${escapeHtml(affectedEntitiesLine(insight))}</dd></div>
591
+ </dl>
592
+ <div class="evidence-list"><strong>Evidence</strong>${insight.evidence.map((item) => `<span>${escapeHtml(item.label)}: ${escapeHtml(item.value)}${item.detail ? ` · ${escapeHtml(item.detail)}` : ""}</span>`).join("")}</div>
593
+ <div class="next-action"><strong>Recommended action:</strong> ${escapeHtml(insight.recommendedAction)}</div>
594
+ ${insight.verificationNeeded ? `<div class="verification-note"><strong>Verification needed:</strong> ${escapeHtml(insight.verificationNeeded)}</div>` : ""}
595
+ </article>`;
596
+ }
597
+ function boardActionPlanLines(recommendations, mappingQuestionCount) {
598
+ const topThree = recommendations.slice(0, 3);
599
+ if (topThree.length === 0) {
600
+ return ["No board actions yet. Import or scan more usage data, then rerun the local report."];
601
+ }
602
+ return [
603
+ ...topThree.map((recommendation, index) => `${index + 1}. ${recommendation.nextAction} (${recommendation.priority}, ${formatUsd(recommendation.estimatedImpactUsd)} estimated impact)`),
604
+ mappingQuestionCount > 0
605
+ ? `4. Confirm ${mappingQuestionCount} attribution mapping question${mappingQuestionCount === 1 ? "" : "s"} so the next report can separate verified spend from estimates.`
606
+ : "4. Keep the local-only report as the baseline, then connect the next source only after the action owners are assigned."
607
+ ];
608
+ }
609
+ function topDriverLine(entries) {
610
+ const topEntry = entries[0];
611
+ if (!topEntry) {
612
+ return "none detected yet";
613
+ }
614
+ return `${topEntry.key} at ${formatUsd(topEntry.amountUsd)} across ${topEntry.recordCount} records`;
615
+ }
616
+ function confidenceBreakdownLines(summary) {
617
+ return Object.entries(summary.confidenceBreakdown).map(([confidence, amount]) => `- ${confidence}: ${formatUsd(amount)}`);
618
+ }
619
+ function evidenceLedgerMarkdownLines(records) {
620
+ const ledger = buildEvidenceLedger(records);
621
+ return [
622
+ `- Verified spend: ${formatUsd(ledger.verifiedSpendUsd)} across ${ledger.verifiedSpendCount} record${ledger.verifiedSpendCount === 1 ? "" : "s"}`,
623
+ `- Estimated spend: ${formatUsd(ledger.estimatedSpendUsd)} across ${ledger.estimatedSpendCount} record${ledger.estimatedSpendCount === 1 ? "" : "s"}`,
624
+ `- Verified usage evidence: ${ledger.usageEvidenceTokens.toLocaleString("en-US")} tokens across ${ledger.usageEvidenceCount} record${ledger.usageEvidenceCount === 1 ? "" : "s"}`,
625
+ `- Missing cost data: ${ledger.missingCostCount} record${ledger.missingCostCount === 1 ? "" : "s"} need${ledger.missingCostCount === 1 ? "s" : ""} billing/source reconciliation`
626
+ ];
627
+ }
628
+ function evidenceLedgerHtml(records) {
629
+ const ledger = buildEvidenceLedger(records);
630
+ return [
631
+ evidenceLedgerCard("verified", "Verified spend", formatUsd(ledger.verifiedSpendUsd), `${ledger.verifiedSpendCount} billing-backed record${ledger.verifiedSpendCount === 1 ? "" : "s"}`),
632
+ evidenceLedgerCard("estimated", "Estimated spend", formatUsd(ledger.estimatedSpendUsd), `${ledger.estimatedSpendCount} estimate-backed record${ledger.estimatedSpendCount === 1 ? "" : "s"}`),
633
+ evidenceLedgerCard("usage", "Verified usage evidence", `${ledger.usageEvidenceTokens.toLocaleString("en-US")} tokens`, `${ledger.usageEvidenceCount} usage record${ledger.usageEvidenceCount === 1 ? "" : "s"} without silent dollar allocation`),
634
+ evidenceLedgerCard("missing", "Missing cost data", `${ledger.missingCostCount} record${ledger.missingCostCount === 1 ? "" : "s"}`, "Needs billing/export/source reconciliation before finance-grade reporting")
635
+ ].join("\n");
636
+ }
637
+ function evidenceLedgerCard(tone, label, value, context) {
638
+ return `<article class="evidence-quality-card evidence-quality-card--${escapeHtml(tone)}">
639
+ <span>${escapeHtml(label)}</span>
640
+ <strong>${escapeHtml(value)}</strong>
641
+ <p>${escapeHtml(context)}</p>
642
+ </article>`;
643
+ }
644
+ function buildEvidenceLedger(records) {
645
+ return records.reduce((ledger, record) => {
646
+ if (record.costConfidence === "verified" && typeof record.amountUsd === "number") {
647
+ ledger.verifiedSpendUsd += record.amountUsd;
648
+ ledger.verifiedSpendCount += 1;
649
+ }
650
+ if (record.costConfidence === "estimated" && typeof record.amountUsd === "number") {
651
+ ledger.estimatedSpendUsd += record.amountUsd;
652
+ ledger.estimatedSpendCount += 1;
653
+ }
654
+ const tokenCount = record.inputTokens + record.outputTokens;
655
+ if (tokenCount > 0) {
656
+ ledger.usageEvidenceTokens += tokenCount;
657
+ ledger.usageEvidenceCount += 1;
658
+ }
659
+ if (record.costConfidence === "missing" || record.amountUsd === null) {
660
+ ledger.missingCostCount += 1;
661
+ }
662
+ return ledger;
663
+ }, { verifiedSpendUsd: 0, verifiedSpendCount: 0, estimatedSpendUsd: 0, estimatedSpendCount: 0, usageEvidenceTokens: 0, usageEvidenceCount: 0, missingCostCount: 0 });
664
+ }
665
+ function providerQaMarkdownLines(providerQa) {
666
+ if (providerQa.length === 0) {
667
+ return ["No live-provider QA captured yet. Run provider sync with API access to record pagination, rate-limit, and response-shape evidence."];
668
+ }
669
+ return providerQa.flatMap((qa) => [
670
+ `- **${qa.provider}** endpoints checked: ${qa.requestedEndpoints.join(", ") || "none"}`,
671
+ ...qa.pagination.map((page) => ` - Pagination: ${providerPaginationExplanation(page)}`),
672
+ providerRateLimitExplanation(qa),
673
+ providerResponseDriftExplanation(qa),
674
+ ...qa.instructions.map((instruction) => ` - Instruction: ${instruction}`)
675
+ ]);
676
+ }
677
+ function providerQaHtml(providerQa) {
678
+ if (providerQa.length === 0) {
679
+ return emptyState("No live-provider QA captured yet. Run provider sync with API access to record pagination, rate-limit, and response-shape evidence.");
680
+ }
681
+ return providerQa.map((qa) => `<article class="provider-qa-card">
682
+ <span>${escapeHtml(qa.provider)}</span>
683
+ <h3>${escapeHtml(qa.requestedEndpoints.join(", ") || "No endpoints checked")}</h3>
684
+ <ul>
685
+ ${qa.pagination.map((page) => `<li>${escapeHtml(providerPaginationExplanation(page))}</li>`).join("\n")}
686
+ <li>${escapeHtml(stripListPrefix(providerRateLimitExplanation(qa)))}</li>
687
+ <li>${escapeHtml(stripListPrefix(providerResponseDriftExplanation(qa)))}</li>
688
+ ${qa.instructions.map((instruction) => `<li>Instruction: ${escapeHtml(instruction)}</li>`).join("\n")}
689
+ </ul>
690
+ </article>`).join("\n");
691
+ }
692
+ function providerPaginationExplanation(page) {
693
+ return `${page.label}: ${page.pagesFetched} page(s), stopped because ${page.stoppedBecause}${typeof page.limitPerPage === "number" ? `, provider limit ${page.limitPerPage} per page` : ""}`;
694
+ }
695
+ function providerRateLimitExplanation(qa) {
696
+ if (qa.rateLimits.length === 0)
697
+ return " - Rate limits: no rate-limit headers observed";
698
+ return ` - Rate limits: ${qa.rateLimits.map((limit) => `${limit.label}${typeof limit.remainingRequests === "number" ? ` remaining ${limit.remainingRequests} requests` : ""}${typeof limit.retryAfterSeconds === "number" ? `; retry after ${limit.retryAfterSeconds}s` : ""}`).join("; ")}`;
699
+ }
700
+ function providerResponseDriftExplanation(qa) {
701
+ if (qa.responseDrift.length === 0)
702
+ return " - Response drift: no unknown fields or pagination anomalies observed";
703
+ return ` - Response drift: ${qa.responseDrift.map((issue) => `${issue.label} ${issue.field} - ${issue.issue}`).join("; ")}`;
704
+ }
705
+ function stripListPrefix(value) {
706
+ return value.replace(/^\s*-\s*/, "");
707
+ }
708
+ function breakdownLines(entries) {
709
+ if (entries.length === 0) {
710
+ return ["No spend in this dimension."];
711
+ }
712
+ return entries.map((entry) => `- ${entry.key}: ${formatUsd(entry.amountUsd)} across ${entry.recordCount} records (${entry.confidence})`);
713
+ }
714
+ function entityBreakdownHtml(entries) {
715
+ if (entries.length === 0) {
716
+ return emptyState("No source signal for this entity yet. Connect provider admin data or confirm mappings to make this first-class.");
717
+ }
718
+ return entries.slice(0, 5).map((entry) => `<div class="mapping-row">
719
+ <span>${escapeHtml(formatConfidenceLabel(entry.confidence))}</span>
720
+ <strong>${escapeHtml(entry.key)}</strong>
721
+ <p>${formatUsd(entry.amountUsd)} across ${entry.recordCount} record${entry.recordCount === 1 ? "" : "s"}</p>
722
+ </div>`).join("\n");
723
+ }
724
+ function workflowWatchMarkdownLines(entries) {
725
+ if (entries.length === 0) {
726
+ return ["No workflow watch entries yet. Add client, project, agent, and operation metadata to surface margin risk."];
727
+ }
728
+ return entries.flatMap((entry) => [
729
+ `- **${entry.clientId} / ${entry.projectId} / ${entry.workflowKey}** (${entry.confidence})`,
730
+ ` - Tracked spend: ${formatUsd(entry.amountUsd)} across ${entry.recordCount} records`,
731
+ ` - Margin risk: ${formatUsd(entry.estimatedMarginRiskUsd)}`,
732
+ ` - Estimated savings: ${formatUsd(entry.estimatedSavingsUsd)}`,
733
+ ` - Suggested optimization: ${entry.suggestedOptimization}`,
734
+ ` - Apply artifact: ${workflowApplyArtifact(entry)}`,
735
+ ` - Verification plan: ${workflowVerificationPlan(entry)}`
736
+ ]);
737
+ }
738
+ function workflowWatchCard(entry) {
739
+ const width = Math.max(6, Math.min(100, Math.round(entry.shareOfSpend * 100)));
740
+ return `<article class="workflow-card">
741
+ <div class="workflow-card-main">
742
+ <div>
743
+ <h3>${escapeHtml(entry.clientId)} / ${escapeHtml(entry.projectId)} / ${escapeHtml(entry.workflowKey)}</h3>
744
+ <p>${escapeHtml(entry.agentId)} · ${escapeHtml(entry.confidence)}</p>
745
+ </div>
746
+ <strong>${formatUsd(entry.amountUsd)}</strong>
747
+ </div>
748
+ <div class="workflow-bar" aria-label="${escapeHtml(formatPercent(entry.shareOfSpend))} of tracked spend"><span style="width: ${width}%"></span></div>
749
+ <div class="workflow-facts">
750
+ <span>Margin risk <strong>${formatUsd(entry.estimatedMarginRiskUsd)}</strong></span>
751
+ <span>Est. savings <strong>${formatUsd(entry.estimatedSavingsUsd)}</strong></span>
752
+ <span>Share <strong>${formatPercent(entry.shareOfSpend)}</strong></span>
753
+ </div>
754
+ <div class="apply-prompt"><strong>Apply artifact:</strong> ${escapeHtml(workflowApplyArtifact(entry))}</div>
755
+ <div class="verification-note"><strong>Verify:</strong> ${escapeHtml(workflowVerificationPlan(entry))}</div>
756
+ </article>`;
757
+ }
758
+ function sourceCoverageMarkdownLines(input) {
759
+ const registry = input.sourceRegistry;
760
+ if (!registry) {
761
+ return ["No source registry attached to this report yet. Run scan/connect before generating a source coverage report."];
762
+ }
763
+ const laneLines = registry.ingestionLanes.map((lane) => {
764
+ const count = registry.approvedSources.filter((source) => source.lane === lane.id).length;
765
+ return `- ${lane.label}: ${count} approved source${count === 1 ? "" : "s"}`;
766
+ });
767
+ const promptLines = (input.missingSourcePrompts ?? []).length === 0
768
+ ? ["- No detected-but-missing connector prompts."]
769
+ : (input.missingSourcePrompts ?? []).map((prompt) => `- ${prompt.reason} Suggested: ${prompt.suggestedConnector}`);
770
+ return [...laneLines, "", "### Detected but missing", "", ...promptLines];
771
+ }
772
+ function confirmedMappingMarkdownLines(mappings) {
773
+ if (mappings.length === 0) {
774
+ return ["No confirmed mappings yet. Use `confirm-mapping` to pin source spend to a team, project, workflow, or agent."];
775
+ }
776
+ return mappings.map((mapping) => {
777
+ const target = [mapping.team, mapping.person, mapping.client, mapping.project, mapping.agent, mapping.workflow].filter(Boolean).join(" / ");
778
+ return `- ${mapping.provider}: ${target} (${Math.round(mapping.confidence * 100)}% confidence). Evidence: ${mapping.evidence.join("; ")}`;
779
+ });
780
+ }
781
+ function sourceLaneCards(registry) {
782
+ const lanes = registry?.ingestionLanes ?? [];
783
+ if (lanes.length === 0) {
784
+ return [emptyState("No source registry attached yet.")];
785
+ }
786
+ return lanes.map((lane) => {
787
+ const sources = registry?.approvedSources.filter((source) => source.lane === lane.id) ?? [];
788
+ const verification = sources[0]?.verification ?? lane.defaultVerification;
789
+ return `<article class="source-lane-card source-lane-card--${escapeHtml(lane.id)}">
790
+ <span class="source-lane-status">${escapeHtml(formatConfidenceLabel(verification))}</span>
791
+ <h3>${escapeHtml(lane.label)}</h3>
792
+ <strong>${sources.length} approved source${sources.length === 1 ? "" : "s"}</strong>
793
+ <p>${sources.length === 0 ? "Not connected yet." : sources.map((source) => source.label).join("; ")}</p>
794
+ </article>`;
795
+ });
796
+ }
797
+ function missingSourcePromptHtml(prompts) {
798
+ if (prompts.length === 0) {
799
+ return emptyState("No detected-but-missing connector prompts yet.");
800
+ }
801
+ return prompts.map((prompt) => `<div class="source-gap-row">
802
+ <span>${escapeHtml(formatConfidenceLabel(prompt.status))}</span>
803
+ <strong>${escapeHtml(prompt.provider)}</strong>
804
+ <p>${escapeHtml(prompt.reason)}</p>
805
+ <code>${escapeHtml(prompt.suggestedConnector)}</code>
806
+ </div>`).join("\n");
807
+ }
808
+ function confirmedMappingHtml(mappings) {
809
+ if (mappings.length === 0) {
810
+ return emptyState("No confirmed mappings yet.");
811
+ }
812
+ return mappings.map((mapping) => {
813
+ const target = [mapping.team, mapping.person, mapping.client, mapping.project, mapping.agent, mapping.workflow].filter(Boolean).join(" / ");
814
+ return `<div class="mapping-row">
815
+ <span>${escapeHtml(mapping.provider)}</span>
816
+ <strong>${escapeHtml(target || mapping.sourceId)}</strong>
817
+ <p>${Math.round(mapping.confidence * 100)}% confidence · ${escapeHtml(mapping.evidence.join("; "))}</p>
818
+ </div>`;
819
+ }).join("\n");
820
+ }
821
+ function workflowApplyArtifact(entry) {
822
+ const legacyEntry = entry;
823
+ return entry.applyArtifact ?? legacyEntry.applyArtifactId ?? `apply-${entry.id}`;
824
+ }
825
+ function workflowVerificationPlan(entry) {
826
+ return entry.verificationPlan ?? "Do not change user-visible quality thresholds without approval; compare cost, latency, and accepted output quality against the current baseline.";
827
+ }
828
+ function nextSourceLine(input) {
829
+ const providers = new Set(input.discovery?.signals.map((signal) => signal.provider) ?? []);
830
+ if (!providers.has("openai")) {
831
+ return "Connect or import OpenAI billing/export data first, then label costs as verified only after source confirmation.";
832
+ }
833
+ if (!providers.has("anthropic")) {
834
+ return "Connect or import Anthropic usage/cost exports next, then compare source totals against local detected usage.";
835
+ }
836
+ return "Review unmatched local usage signals and confirm client/project mappings before expanding to another provider.";
837
+ }
838
+ function formatUsd(amount) {
839
+ return `$${amount.toFixed(2)}`;
840
+ }
841
+ function formatPercent(ratio) {
842
+ return `${Math.round(ratio * 100)}%`;
843
+ }
844
+ function premiumReportCss() {
845
+ return `
846
+ :root {
847
+ color-scheme: dark;
848
+ --bg: #08090a;
849
+ --panel: #0f1011;
850
+ --surface: rgba(255, 255, 255, 0.035);
851
+ --surface-strong: rgba(255, 255, 255, 0.055);
852
+ --border: rgba(255, 255, 255, 0.08);
853
+ --border-soft: rgba(255, 255, 255, 0.05);
854
+ --text: #f7f8f8;
855
+ --muted: #8a8f98;
856
+ --soft: #d0d6e0;
857
+ --accent: #7170ff;
858
+ --accent-bg: #5e6ad2;
859
+ --success: #10b981;
860
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
861
+ background: var(--bg);
862
+ color: var(--text);
863
+ font-feature-settings: "cv01", "ss03";
864
+ }
865
+ * { box-sizing: border-box; }
866
+ body {
867
+ margin: 0;
868
+ min-height: 100vh;
869
+ background:
870
+ radial-gradient(circle at 18% -8%, rgba(113, 112, 255, 0.24), transparent 30rem),
871
+ radial-gradient(circle at 90% 6%, rgba(16, 185, 129, 0.10), transparent 26rem),
872
+ #08090a;
873
+ }
874
+ .report-shell { width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 48px 0 64px; }
875
+ .hero-panel, .artifact-card, .analyst-insights, .workflow-watch, .source-coverage, .provider-qa, .operating-loop, .recommendations-section, .metric-card, .evidence-quality {
876
+ border: 1px solid var(--border);
877
+ background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.022));
878
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.045), 0 24px 80px rgba(0,0,0,0.26);
879
+ }
880
+ .hero-panel { border-radius: 24px; padding: 34px; overflow: hidden; position: relative; }
881
+ .hero-panel::after { content: ""; position: absolute; inset: auto -16% -42% 45%; height: 280px; background: radial-gradient(circle, rgba(113,112,255,0.18), transparent 70%); pointer-events: none; }
882
+ .report-kicker, .section-label { color: var(--accent); font-size: 12px; font-weight: 590; letter-spacing: 0.08em; text-transform: uppercase; }
883
+ .hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 28px; align-items: end; position: relative; z-index: 1; }
884
+ h1, h2, p { margin: 0; }
885
+ h1 { max-width: 760px; font-size: clamp(42px, 7vw, 76px); line-height: 0.96; letter-spacing: -0.07em; font-weight: 510; color: var(--text); }
886
+ h2 { margin-top: 10px; font-size: 22px; line-height: 1.18; letter-spacing: -0.03em; font-weight: 510; color: var(--text); }
887
+ .hero-copy { max-width: 620px; margin-top: 18px; color: var(--muted); font-size: 18px; line-height: 1.62; letter-spacing: -0.01em; }
888
+ .hero-meta { display: grid; grid-template-columns: 1fr; gap: 8px; padding: 18px; border: 1px solid var(--border-soft); border-radius: 16px; background: rgba(255,255,255,0.025); }
889
+ .hero-meta span { color: var(--muted); font-size: 12px; }
890
+ .hero-meta strong { color: var(--soft); font-size: 13px; font-weight: 510; overflow-wrap: anywhere; }
891
+ .privacy-banner { position: relative; z-index: 1; display: flex; gap: 10px; align-items: center; margin-top: 28px; padding: 14px 16px; border: 1px solid rgba(16,185,129,0.22); border-radius: 999px; background: rgba(16,185,129,0.075); color: var(--soft); font-size: 14px; }
892
+ .privacy-banner strong { color: var(--text); font-weight: 590; }
893
+ .privacy-dot { width: 8px; height: 8px; border-radius: 999px; background: var(--success); box-shadow: 0 0 18px rgba(16,185,129,0.75); flex: 0 0 auto; }
894
+ .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-top: 16px; }
895
+ .metric-card { border-radius: 18px; padding: 18px; min-height: 132px; }
896
+ .metric-card--primary { background: linear-gradient(180deg, rgba(94,106,210,0.25), rgba(255,255,255,0.03)); border-color: rgba(130,143,255,0.34); }
897
+ .metric-label { color: var(--muted); font-size: 12px; font-weight: 510; }
898
+ .metric-value { display: block; margin-top: 18px; color: var(--text); font-size: 31px; line-height: 1; letter-spacing: -0.05em; font-weight: 510; }
899
+ .metric-context { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.45; }
900
+ .operating-loop { margin-top: 16px; border-radius: 22px; padding: 24px; }
901
+ .loop-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
902
+ .loop-card { position: relative; min-height: 220px; padding: 18px; border: 1px solid var(--border-soft); border-radius: 18px; background: rgba(255,255,255,0.025); overflow: hidden; }
903
+ .loop-card::after { content: ""; position: absolute; inset: auto 12px 12px auto; width: 44px; height: 44px; border-radius: 999px; background: rgba(113,112,255,0.10); }
904
+ .loop-step { color: var(--accent); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
905
+ .loop-card h3 { margin: 28px 0 12px; color: var(--text); font-size: 18px; line-height: 1.18; letter-spacing: -0.03em; font-weight: 590; }
906
+ .loop-card strong { display: block; color: var(--soft); font-size: 14px; line-height: 1.45; font-weight: 590; }
907
+ .loop-card p { margin-top: 12px; color: var(--muted); font-size: 13px; line-height: 1.55; }
908
+ .artifact-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr); gap: 16px; margin-top: 16px; }
909
+ .artifact-grid--bottom { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
910
+ .artifact-card, .recommendations-section { border-radius: 22px; padding: 24px; }
911
+ .brief-list { list-style: none; padding: 0; margin: 22px 0 0; display: grid; gap: 12px; }
912
+ .brief-list li, .mini-breakdown div { display: flex; justify-content: space-between; gap: 18px; padding-top: 12px; border-top: 1px solid var(--border-soft); color: var(--muted); }
913
+ .brief-list strong, .mini-breakdown strong { color: var(--soft); font-weight: 510; text-align: right; }
914
+ .stacked-bars { display: flex; width: 100%; height: 12px; margin: 22px 0 12px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.05); }
915
+ .bar-segment { min-width: 2px; }
916
+ .bar-segment--verified { background: #10b981; }
917
+ .bar-segment--estimated { background: #7170ff; }
918
+ .bar-segment--detected-unverified { background: #d97706; }
919
+ .bar-segment--missing { background: #62666d; }
920
+ .mini-breakdown { display: grid; gap: 0; }
921
+ .analyst-insights { margin-top: 16px; border-radius: 22px; padding: 24px; }
922
+ .evidence-quality { margin-top: 16px; border-radius: 22px; padding: 24px; }
923
+ .evidence-quality-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
924
+ .evidence-quality-card { border: 1px solid var(--border-soft); border-radius: 18px; padding: 18px; background: rgba(255,255,255,0.025); }
925
+ .evidence-quality-card span { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
926
+ .evidence-quality-card strong { display: block; margin-top: 12px; color: var(--text); font-size: 26px; line-height: 1; letter-spacing: -0.04em; font-weight: 510; }
927
+ .evidence-quality-card p { margin-top: 10px; color: var(--muted); font-size: 13px; line-height: 1.5; }
928
+ .evidence-quality-card--verified { border-color: rgba(16,185,129,0.28); }
929
+ .evidence-quality-card--estimated { border-color: rgba(113,112,255,0.30); }
930
+ .evidence-quality-card--usage { border-color: rgba(59,130,246,0.30); }
931
+ .evidence-quality-card--missing { border-color: rgba(217,119,6,0.32); }
932
+ .insight-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
933
+ .insight-card { border: 1px solid var(--border-soft); border-radius: 18px; padding: 18px; background: rgba(255,255,255,0.025); }
934
+ .insight-card--critical, .insight-card--high { border-color: rgba(217,119,6,0.36); background: linear-gradient(180deg, rgba(217,119,6,0.12), rgba(255,255,255,0.025)); }
935
+ .insight-topline { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
936
+ .severity-badge, .confidence-chip { border: 1px solid var(--border); border-radius: 999px; padding: 5px 8px; color: var(--soft); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
937
+ .severity-badge--critical, .severity-badge--high { border-color: rgba(217,119,6,0.42); color: #fbbf24; background: rgba(217,119,6,0.12); }
938
+ .confidence-chip { color: var(--muted); text-transform: none; letter-spacing: 0; }
939
+ .insight-card h3 { margin: 16px 0 10px; color: var(--text); font-size: 18px; line-height: 1.25; letter-spacing: -0.02em; font-weight: 590; }
940
+ .insight-card p { color: var(--muted); line-height: 1.62; font-size: 14px; }
941
+ .insight-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 16px 0 0; }
942
+ .insight-facts div { padding: 12px; border: 1px solid var(--border-soft); border-radius: 12px; background: rgba(255,255,255,0.025); }
943
+ .insight-facts dt { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
944
+ .insight-facts dd { margin: 6px 0 0; color: var(--soft); font-size: 13px; line-height: 1.45; }
945
+ .evidence-list { display: grid; gap: 8px; margin-top: 14px; color: var(--muted); font-size: 13px; line-height: 1.45; }
946
+ .evidence-list strong { color: var(--soft); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
947
+ .evidence-list span { padding-left: 10px; border-left: 1px solid var(--border-soft); }
948
+ .verification-note { margin-top: 12px; padding: 12px; border: 1px solid rgba(113,112,255,0.24); border-radius: 12px; background: rgba(113,112,255,0.08); color: var(--soft); font-size: 13px; line-height: 1.5; }
949
+ .workflow-watch { margin-top: 16px; border-radius: 22px; padding: 24px; }
950
+ .source-coverage { margin-top: 16px; border-radius: 22px; padding: 24px; }
951
+ .provider-qa { margin-top: 16px; border-radius: 22px; padding: 24px; }
952
+ .provider-qa-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
953
+ .provider-qa-card { border: 1px solid var(--border-soft); border-radius: 18px; padding: 18px; background: rgba(255,255,255,0.025); }
954
+ .provider-qa-card span { color: var(--accent); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
955
+ .provider-qa-card h3 { margin: 12px 0; color: var(--text); font-size: 16px; line-height: 1.25; letter-spacing: -0.02em; font-weight: 590; }
956
+ .provider-qa-card ul { margin: 0; padding-left: 18px; color: var(--muted); font-size: 13px; line-height: 1.55; }
957
+ .provider-qa-card li { margin: 7px 0; }
958
+ .source-lane-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
959
+ .source-lane-card, .source-detail-card { border: 1px solid var(--border-soft); border-radius: 16px; padding: 16px; background: rgba(255,255,255,0.025); }
960
+ .source-lane-status { color: var(--accent); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
961
+ .source-lane-card h3, .source-detail-card h3 { margin: 12px 0 10px; color: var(--text); font-size: 16px; line-height: 1.25; letter-spacing: -0.02em; font-weight: 590; }
962
+ .source-lane-card strong { display: block; color: var(--soft); font-size: 14px; margin-bottom: 8px; }
963
+ .source-lane-card p, .source-detail-card p { color: var(--muted); font-size: 13px; line-height: 1.5; }
964
+ .source-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; margin-top: 14px; }
965
+ .missing-source-list, .confirmed-mapping-list { display: grid; gap: 10px; }
966
+ .source-gap-row, .mapping-row { padding: 12px; border: 1px solid var(--border-soft); border-radius: 12px; background: rgba(255,255,255,0.022); }
967
+ .source-gap-row span, .mapping-row span { display: block; color: var(--accent); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
968
+ .source-gap-row strong, .mapping-row strong { display: block; margin-top: 6px; color: var(--soft); font-size: 14px; }
969
+ .source-gap-row code { display: inline-block; margin-top: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(113,112,255,0.12); color: var(--soft); font-size: 12px; }
970
+ .workflow-chart { display: grid; gap: 14px; }
971
+ .workflow-card { border: 1px solid var(--border-soft); border-radius: 18px; padding: 18px; background: rgba(255,255,255,0.025); }
972
+ .workflow-card-main { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; }
973
+ .workflow-card h3 { margin: 0; color: var(--text); font-size: 17px; line-height: 1.25; letter-spacing: -0.02em; font-weight: 590; }
974
+ .workflow-card p { margin-top: 7px; color: var(--muted); line-height: 1.45; font-size: 13px; }
975
+ .workflow-card-main > strong { color: var(--text); font-size: 24px; line-height: 1; letter-spacing: -0.04em; font-weight: 510; white-space: nowrap; }
976
+ .workflow-bar { height: 10px; margin-top: 16px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.055); }
977
+ .workflow-bar span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #7170ff, #10b981); }
978
+ .workflow-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
979
+ .workflow-facts span { padding: 11px; border: 1px solid var(--border-soft); border-radius: 12px; color: var(--muted); background: rgba(255,255,255,0.022); font-size: 12px; }
980
+ .workflow-facts strong { display: block; margin-top: 4px; color: var(--soft); font-size: 13px; }
981
+ .apply-prompt { margin-top: 14px; padding: 12px; border-radius: 12px; background: rgba(16,185,129,0.075); color: var(--soft); font-size: 13px; line-height: 1.5; }
982
+ .recommendations-section { margin-top: 16px; }
983
+ .section-heading { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
984
+ .impact-pill { border: 1px solid rgba(113,112,255,0.32); border-radius: 999px; padding: 8px 12px; color: var(--soft); background: rgba(113,112,255,0.10); font-size: 13px; font-weight: 510; }
985
+ .recommendation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
986
+ .recommendation-card { border: 1px solid var(--border-soft); border-radius: 18px; padding: 18px; background: rgba(255,255,255,0.025); }
987
+ .recommendation-card--high { border-color: rgba(113,112,255,0.36); background: linear-gradient(180deg, rgba(113,112,255,0.14), rgba(255,255,255,0.025)); }
988
+ .recommendation-topline { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
989
+ .priority-badge { border: 1px solid var(--border); border-radius: 999px; padding: 5px 8px; color: var(--soft); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
990
+ .recommendation-card h3 { margin: 16px 0 10px; color: var(--text); font-size: 18px; line-height: 1.25; letter-spacing: -0.02em; font-weight: 590; }
991
+ .recommendation-card p, .artifact-card p { color: var(--muted); line-height: 1.62; font-size: 14px; }
992
+ .impact-line { display: block; color: var(--text); font-size: 28px; letter-spacing: -0.05em; font-weight: 510; }
993
+ .next-action { margin-top: 14px; padding: 12px; border-radius: 12px; background: rgba(255,255,255,0.035); color: var(--soft); font-size: 13px; line-height: 1.5; }
994
+ .board-action-list { margin: 20px 0 0; padding-left: 20px; color: var(--soft); }
995
+ .board-action-list li { margin: 10px 0; padding-left: 8px; line-height: 1.58; }
996
+ .callout { margin-top: 18px; padding: 14px; border: 1px solid var(--border-soft); border-radius: 14px; background: rgba(255,255,255,0.025); }
997
+ .callout span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 6px; }
998
+ .callout strong { color: var(--soft); font-size: 13px; line-height: 1.5; }
999
+ .empty-state { color: var(--muted); border: 1px dashed var(--border); border-radius: 16px; padding: 22px; }
1000
+ @media (max-width: 960px) { .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hero-grid, .artifact-grid, .artifact-grid--bottom { grid-template-columns: 1fr; } .loop-grid, .recommendation-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
1001
+ @media (max-width: 760px) { .report-shell { width: min(100% - 28px, 1180px); padding: 24px 0 40px; } .hero-panel, .artifact-card, .analyst-insights, .workflow-watch, .provider-qa, .operating-loop, .recommendations-section { padding: 20px; border-radius: 18px; } .metric-grid, .loop-grid, .insight-grid, .provider-qa-grid, .workflow-facts, .recommendation-grid { grid-template-columns: 1fr; } .workflow-card-main { flex-direction: column; } .privacy-banner { align-items: flex-start; border-radius: 16px; flex-wrap: wrap; } .section-heading { align-items: flex-start; flex-direction: column; } }
1002
+ `;
1003
+ }
1004
+ function formatConfidenceLabel(confidence) {
1005
+ switch (confidence) {
1006
+ case "verified":
1007
+ return "Verified from source data";
1008
+ case "estimated":
1009
+ return "Estimated from local records";
1010
+ case "detected_unverified":
1011
+ return "Detected, not yet verified";
1012
+ case "missing":
1013
+ return "Source data needed";
1014
+ }
1015
+ }
1016
+ function metricCard(label, value, context, tone) {
1017
+ return `<article class="metric-card${tone === "primary" ? " metric-card--primary" : ""}">
1018
+ <span class="metric-label">${escapeHtml(label)}</span>
1019
+ <strong class="metric-value">${escapeHtml(value)}</strong>
1020
+ <p class="metric-context">${escapeHtml(context)}</p>
1021
+ </article>`;
1022
+ }
1023
+ function recommendationCard(recommendation) {
1024
+ return `<article class="recommendation-card recommendation-card--${escapeHtml(recommendation.priority)}">
1025
+ <div class="recommendation-topline">
1026
+ <span class="impact-line">${formatUsd(recommendation.estimatedImpactUsd)}</span>
1027
+ <span class="priority-badge">${escapeHtml(recommendation.priority)}</span>
1028
+ </div>
1029
+ <h3>${escapeHtml(recommendation.title)}</h3>
1030
+ <p>${escapeHtml(recommendation.whyItMatters)}</p>
1031
+ <div class="next-action"><strong>Next action:</strong> ${escapeHtml(recommendation.nextAction)}</div>
1032
+ </article>`;
1033
+ }
1034
+ function confidenceBarSegments(summary) {
1035
+ const total = Math.max(summary.totalUsd, 1);
1036
+ return Object.entries(summary.confidenceBreakdown)
1037
+ .map(([confidence, amount]) => {
1038
+ const width = Math.max((amount / total) * 100, amount > 0 ? 3 : 0);
1039
+ return `<span class="bar-segment bar-segment--${escapeHtml(confidence.replace(/_/g, "-"))}" style="width: ${width.toFixed(1)}%" title="${escapeHtml(confidence)} ${formatUsd(amount)}"></span>`;
1040
+ })
1041
+ .join("\n");
1042
+ }
1043
+ function confidenceBreakdownHtml(summary) {
1044
+ return Object.entries(summary.confidenceBreakdown)
1045
+ .map(([confidence, amount]) => `<div><span>${escapeHtml(confidence.replace(/_/g, " "))}</span><strong>${formatUsd(amount)}</strong></div>`)
1046
+ .join("\n");
1047
+ }
1048
+ function emptyState(message) {
1049
+ return `<div class="empty-state">${escapeHtml(message)}</div>`;
1050
+ }
1051
+ function stripOrderedPrefix(line) {
1052
+ return line.replace(/^\d+\.\s*/, "");
1053
+ }
1054
+ function markdownToSimpleHtml(markdown) {
1055
+ return markdown
1056
+ .split("\n")
1057
+ .map((line) => {
1058
+ if (line.startsWith("# "))
1059
+ return `<h1>${escapeHtml(line.slice(2))}</h1>`;
1060
+ if (line.startsWith("## "))
1061
+ return `<h2>${escapeHtml(line.slice(3))}</h2>`;
1062
+ if (line.startsWith("> "))
1063
+ return `<blockquote>${escapeHtml(line.slice(2))}</blockquote>`;
1064
+ if (line.startsWith("- "))
1065
+ return `<p>• ${formatInline(line.slice(2))}</p>`;
1066
+ if (line.trim() === "")
1067
+ return "";
1068
+ return `<p>${formatInline(line)}</p>`;
1069
+ })
1070
+ .join("\n");
1071
+ }
1072
+ function formatInline(text) {
1073
+ return escapeHtml(text).replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
1074
+ }
1075
+ function escapeHtml(text) {
1076
+ return text
1077
+ .replace(/&/g, "&amp;")
1078
+ .replace(/</g, "&lt;")
1079
+ .replace(/>/g, "&gt;")
1080
+ .replace(/"/g, "&quot;");
1081
+ }
1082
+ //# sourceMappingURL=index.js.map