altimate-receipts 0.3.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.
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ deriveFindings,
4
+ deriveSpans,
5
+ gradeLetter,
6
+ listSessions,
7
+ loadSession,
8
+ selectSummary,
9
+ upsertSection
10
+ } from "./chunk-UHI6BGLE.js";
11
+
12
+ // src/report/sessions.ts
13
+ async function deriveTargets(opts) {
14
+ const sessions2 = await listSessions(opts.agent);
15
+ let targets;
16
+ if (opts.last && opts.last > 0) {
17
+ targets = sessions2.slice(0, opts.last);
18
+ } else {
19
+ const one = selectSummary(sessions2, opts.selector);
20
+ targets = one ? [one] : [];
21
+ }
22
+ const out = [];
23
+ for (const summary of targets) {
24
+ const session = await loadSession(summary);
25
+ if (!session) {
26
+ continue;
27
+ }
28
+ const derived = deriveSpans(session);
29
+ out.push({ summary, derived, findings: deriveFindings(derived) });
30
+ }
31
+ return out;
32
+ }
33
+
34
+ // src/report/trends.ts
35
+ var TRENDS_START = "<!-- receipts:trends:start -->";
36
+ var TRENDS_END = "<!-- receipts:trends:end -->";
37
+ var SEV_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
38
+ var SEV_SHORT = {
39
+ critical: "crit",
40
+ high: "high",
41
+ medium: "med",
42
+ low: "low"
43
+ };
44
+ var TEST_CMD = /\b(pytest|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\b/i;
45
+ function ranTests(derived) {
46
+ return derived.spans.some((s) => {
47
+ if (s.kind !== "tool" || !/bash|shell|exec|run|terminal/i.test(s.name)) {
48
+ return false;
49
+ }
50
+ const cmd = s.input && typeof s.input === "object" ? String(s.input.command ?? "") : "";
51
+ return TEST_CMD.test(cmd);
52
+ });
53
+ }
54
+ function kindOf(id) {
55
+ if (id.startsWith("errcluster-")) {
56
+ return "errcluster";
57
+ }
58
+ return id.replace(/-(?:tool-\d+-\d+|gen-\d+)$/, "");
59
+ }
60
+ function computeTrends(inputs, requested) {
61
+ const counts = { A: 0, B: 0, C: 0, F: 0 };
62
+ const sequence = [];
63
+ const costSequence = [];
64
+ let totalCostUsd = 0;
65
+ let destructiveOps = 0;
66
+ let filesChanged = 0;
67
+ let ran = 0;
68
+ const rec = /* @__PURE__ */ new Map();
69
+ inputs.forEach((inp, i) => {
70
+ const grade = gradeLetter(inp.findings.main);
71
+ counts[grade]++;
72
+ sequence.push(grade);
73
+ const cost = inp.derived.totalCost || 0;
74
+ costSequence.push(Number(cost.toFixed(4)));
75
+ totalCostUsd += cost;
76
+ destructiveOps += inp.derived.destructiveCount;
77
+ filesChanged += inp.derived.filesChanged.length;
78
+ if (ranTests(inp.derived)) {
79
+ ran++;
80
+ }
81
+ for (const f of [...inp.findings.main, ...inp.findings.minor]) {
82
+ const kind = kindOf(f.id);
83
+ let e = rec.get(kind);
84
+ if (!e) {
85
+ e = {
86
+ kind,
87
+ sessions: /* @__PURE__ */ new Set(),
88
+ worst: f.severity,
89
+ title: f.title,
90
+ example: f.filePath,
91
+ bestScore: f.score
92
+ };
93
+ rec.set(kind, e);
94
+ }
95
+ e.sessions.add(i);
96
+ if (SEV_ORDER[f.severity] < SEV_ORDER[e.worst]) {
97
+ e.worst = f.severity;
98
+ }
99
+ if (f.score > e.bestScore) {
100
+ e.bestScore = f.score;
101
+ e.title = f.title;
102
+ e.example = f.filePath ?? e.example;
103
+ } else if (!e.example && f.filePath) {
104
+ e.example = f.filePath;
105
+ }
106
+ }
107
+ });
108
+ const recurring = [...rec.values()].map((e) => ({
109
+ kind: e.kind,
110
+ sessions: e.sessions.size,
111
+ worst: e.worst,
112
+ title: e.title,
113
+ example: e.example
114
+ })).sort(
115
+ (a, b) => b.sessions - a.sessions || SEV_ORDER[a.worst] - SEV_ORDER[b.worst] || a.kind.localeCompare(b.kind)
116
+ );
117
+ return {
118
+ window: { requested, used: inputs.length },
119
+ grades: { counts, sequence },
120
+ recurring,
121
+ evidence: {
122
+ totalCostUsd: Number(totalCostUsd.toFixed(2)),
123
+ destructiveOps,
124
+ filesChanged,
125
+ testsRanRate: { ran, of: inputs.length },
126
+ costSequence
127
+ }
128
+ };
129
+ }
130
+ var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
131
+ function sparkline(values) {
132
+ if (values.length === 0) {
133
+ return "\u2014";
134
+ }
135
+ const max = Math.max(...values, 0);
136
+ if (max <= 0) {
137
+ return BARS[0].repeat(values.length);
138
+ }
139
+ return values.map((v) => BARS[Math.min(BARS.length - 1, Math.round(v / max * (BARS.length - 1)))]).join("");
140
+ }
141
+ var money = (n) => `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
142
+ var sessions = (n) => `${n} session${n === 1 ? "" : "s"}`;
143
+ var ANSI = {
144
+ reset: "\x1B[0m",
145
+ bold: "\x1B[1m",
146
+ dim: "\x1B[2m",
147
+ red: "\x1B[31m",
148
+ grn: "\x1B[32m",
149
+ yel: "\x1B[33m",
150
+ blu: "\x1B[34m",
151
+ mag: "\x1B[35m",
152
+ cyn: "\x1B[36m",
153
+ gray: "\x1B[90m"
154
+ };
155
+ var PLAIN = Object.fromEntries(Object.keys(ANSI).map((k) => [k, ""]));
156
+ var gradeCol = (c, g) => ({ A: c.grn, B: c.grn, C: c.yel, F: c.red })[g];
157
+ var sevCol = (c, s) => ({ critical: c.red, high: c.yel, medium: c.cyn, low: c.gray })[s];
158
+ var sevIcon = (s) => ({ critical: "\u26D4", high: "\u26A0\uFE0F ", medium: "\u{1F50D}", low: "\xB7" })[s];
159
+ function renderTrends(trends, format = "card", opts = {}) {
160
+ if (format === "json") {
161
+ return JSON.stringify(trends, null, 2);
162
+ }
163
+ return format === "md" ? renderMd(trends) : renderCard(trends, opts.color !== false);
164
+ }
165
+ function renderCard(trends, color) {
166
+ const c = color ? ANSI : PLAIN;
167
+ const W = 74;
168
+ const line = (s = "") => ` ${s}`;
169
+ const out = [];
170
+ const title = "\u{1F9FE} RECEIPTS \u2014 Trends";
171
+ const right = `last ${sessions(trends.window.used)} \xB7 proof`;
172
+ const gap = Math.max(1, W - 2 - title.length - right.length);
173
+ out.push(`${c.cyn} \u2554${"\u2550".repeat(W)}\u2557${c.reset}`);
174
+ out.push(
175
+ ` ${c.cyn}\u2551${c.reset} ${c.bold}\u{1F9FE} RECEIPTS${c.reset}${c.dim} \u2014 Trends${c.reset}${" ".repeat(gap)}${c.mag}${c.bold}${right}${c.reset} ${c.cyn}\u2551${c.reset}`
176
+ );
177
+ out.push(`${c.cyn} \u255A${"\u2550".repeat(W)}\u255D${c.reset}`);
178
+ out.push("");
179
+ const seq = trends.grades.sequence.map((g) => `${gradeCol(c, g)}${g}${c.reset}`).join(" ");
180
+ const counts = ["A", "B", "C", "F"].filter((g) => trends.grades.counts[g] > 0).map((g) => `${gradeCol(c, g)}${g}\xD7${trends.grades.counts[g]}${c.reset}`).join(" ") || `${c.gray}none${c.reset}`;
181
+ out.push(line(`${c.gray}Grades ${c.reset}${seq} ${c.gray}(oldest \u2192 newest)${c.reset}`));
182
+ out.push(line(` ${counts}`));
183
+ out.push("");
184
+ out.push(line(`${c.gray}RECURS MOST ${c.reset}${c.dim}(across sessions)${c.reset}`));
185
+ if (trends.recurring.length === 0) {
186
+ out.push(line(`${c.grn} \u2705 nothing recurred \u2014 no findings across the window${c.reset}`));
187
+ }
188
+ for (const r of trends.recurring.slice(0, 8)) {
189
+ const kn = `${r.sessions}/${trends.window.used}`;
190
+ const file = r.example ? `${c.blu}${r.example.split("/").pop()}${c.reset}` : `${c.gray}\u2014${c.reset}`;
191
+ out.push(
192
+ line(
193
+ ` ${c.bold}${kn.padStart(5)}${c.reset} ${sevIcon(r.worst)} ${r.title.slice(0, 44).padEnd(44)} ${sevCol(c, r.worst)}${SEV_SHORT[r.worst].padEnd(4)}${c.reset} ${c.gray}\xB7${c.reset} ${file}`
194
+ )
195
+ );
196
+ }
197
+ out.push("");
198
+ const ev = trends.evidence;
199
+ out.push(line(`${c.gray}EVIDENCE ${c.reset}${c.dim}(${sessions(trends.window.used)})${c.reset}`));
200
+ out.push(
201
+ line(
202
+ ` ${c.grn}${money(ev.totalCostUsd)}${c.reset} total${c.gray} \xB7 ${c.reset}${ev.destructiveOps ? c.red : c.grn}${ev.destructiveOps} destructive${c.reset}${c.gray} \xB7 ${c.reset}tests ran ${ev.testsRanRate.ran}/${ev.testsRanRate.of}${c.gray} \xB7 ${c.reset}${ev.filesChanged} files changed`
203
+ )
204
+ );
205
+ const spark = color ? sparkline(ev.costSequence) : ev.costSequence.map((n) => n.toFixed(2)).join(" ");
206
+ out.push(line(` ${c.gray}cost ${c.reset}${spark}`));
207
+ out.push("");
208
+ out.push(` ${c.gray}${"\u2500".repeat(W)}${c.reset}`);
209
+ out.push(
210
+ line(
211
+ `${c.grn}Verified by Receipts${c.reset}${c.gray} \xB7 deterministic \xB7 0 model calls \xB7 evidence, not judgement${c.reset}`
212
+ )
213
+ );
214
+ if (trends.window.used < trends.window.requested) {
215
+ out.push(
216
+ line(
217
+ `${c.dim}(asked for ${trends.window.requested}; only ${trends.window.used} session${trends.window.used === 1 ? "" : "s"} available)${c.reset}`
218
+ )
219
+ );
220
+ }
221
+ return `
222
+ ${out.join("\n")}
223
+ `;
224
+ }
225
+ function renderMd(trends) {
226
+ const lines = [TRENDS_START];
227
+ lines.push(`## Receipts trends \u2014 last ${sessions(trends.window.used)}`);
228
+ lines.push("<!-- generated by `receipts trends` \u2014 what your agent does over time -->");
229
+ lines.push("");
230
+ const seq = trends.grades.sequence.join(" ");
231
+ const counts = ["A", "B", "C", "F"].filter((g) => trends.grades.counts[g] > 0).map((g) => `${g}\xD7${trends.grades.counts[g]}`).join(" ");
232
+ lines.push(`**Grades** (oldest \u2192 newest): \`${seq || "\u2014"}\` \u2014 ${counts || "none"}`);
233
+ lines.push("");
234
+ lines.push("**Recurs most** (across sessions):");
235
+ if (trends.recurring.length === 0) {
236
+ lines.push("- _nothing recurred \u2014 no findings across the window_");
237
+ }
238
+ for (const r of trends.recurring.slice(0, 8)) {
239
+ const file = r.example ? ` _\u2014 ${r.example.split("/").pop()}_` : "";
240
+ lines.push(
241
+ `- \`${r.sessions}/${trends.window.used}\` **${r.title}** (${SEV_SHORT[r.worst]})${file}`
242
+ );
243
+ }
244
+ lines.push("");
245
+ const ev = trends.evidence;
246
+ lines.push(
247
+ `**Evidence** (${sessions(trends.window.used)}): ${money(ev.totalCostUsd)} total \xB7 ${ev.destructiveOps} destructive ops \xB7 tests ran ${ev.testsRanRate.ran}/${ev.testsRanRate.of} \xB7 ${ev.filesChanged} files changed`
248
+ );
249
+ lines.push(TRENDS_END);
250
+ return lines.join("\n");
251
+ }
252
+ function upsertTrendsSection(existing, block) {
253
+ return upsertSection(existing, block, TRENDS_START, TRENDS_END);
254
+ }
255
+
256
+ export {
257
+ deriveTargets,
258
+ computeTrends,
259
+ renderTrends,
260
+ upsertTrendsSection
261
+ };
262
+ //# sourceMappingURL=chunk-RQLUZ6FQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/report/sessions.ts","../src/report/trends.ts"],"sourcesContent":["import { type FindingSet, deriveFindings } from \"../findings/findings.js\";\nimport { type DerivedSummary, deriveSpans } from \"../findings/spans.js\";\nimport { listSessions, loadSession, selectSummary } from \"../trace/load.js\";\nimport type { AgentSource, SessionSummary } from \"../trace/types.js\";\n\n/** One session, loaded and run through the same engine the Report Card uses. */\nexport interface SessionDerivation {\n summary: SessionSummary;\n derived: DerivedSummary;\n findings: FindingSet;\n}\n\n/**\n * Resolve the target sessions and derive each (spans + findings). Most-recent-first.\n * - `last N` → the most recent N sessions\n * - otherwise → the single session matching `selector` (or the most recent)\n * Per-session read failures are skipped so one broken session never sinks the batch.\n * Shared by `guardrails` and `trends` (CLI + MCP) — one cross-session walk.\n */\nexport async function deriveTargets(opts: {\n agent?: AgentSource;\n last?: number;\n selector?: string;\n}): Promise<SessionDerivation[]> {\n const sessions = await listSessions(opts.agent);\n let targets: SessionSummary[];\n if (opts.last && opts.last > 0) {\n targets = sessions.slice(0, opts.last);\n } else {\n const one = selectSummary(sessions, opts.selector);\n targets = one ? [one] : [];\n }\n\n const out: SessionDerivation[] = [];\n for (const summary of targets) {\n const session = await loadSession(summary);\n if (!session) {\n continue;\n }\n const derived = deriveSpans(session);\n out.push({ summary, derived, findings: deriveFindings(derived) });\n }\n return out;\n}\n","import type { FindingSet, Severity } from \"../findings/findings.js\";\nimport { type GradeLetter, gradeLetter } from \"../findings/grade.js\";\nimport type { DerivedSummary } from \"../findings/spans.js\";\nimport { upsertSection } from \"./section.js\";\n\n/**\n * Cross-session trend digest. Pure aggregation over per-session findings + evidence\n * the Report Card already derives — no new detectors, no clock, no random, zero\n * model calls. Evidence, not judgement: this reports the *frequency and severity of\n * mechanically-detected findings over time*, never whether the agent is \"improving.\"\n */\n\nexport const TRENDS_START = \"<!-- receipts:trends:start -->\";\nexport const TRENDS_END = \"<!-- receipts:trends:end -->\";\n\nconst SEV_ORDER: Record<Severity, number> = { critical: 0, high: 1, medium: 2, low: 3 };\nconst SEV_SHORT: Record<Severity, string> = {\n critical: \"crit\",\n high: \"high\",\n medium: \"med\",\n low: \"low\",\n};\n\n/** A finding kind that recurred across sessions, with the count and a citation. */\nexport interface RecurringTrend {\n /** stable Finding.id (dynamic families like `errcluster-<tool>` collapsed) */\n kind: string;\n /** number of sessions in the window in which this kind appeared */\n sessions: number;\n /** worst severity seen for this kind across the window */\n worst: Severity;\n /** representative headline (the highest-scoring occurrence) */\n title: string;\n /** an example file the finding was about (findings carry a file, not a line) */\n example?: string;\n}\n\nexport interface Trends {\n window: { requested: number; used: number };\n grades: {\n counts: Record<GradeLetter, number>;\n /** oldest → newest */\n sequence: GradeLetter[];\n };\n /** ranked by sessions-affected, then worst severity */\n recurring: RecurringTrend[];\n evidence: {\n totalCostUsd: number;\n destructiveOps: number;\n filesChanged: number;\n testsRanRate: { ran: number; of: number };\n /** per-session, oldest → newest */\n costSequence: number[];\n };\n}\n\n/** One session's derived evidence + findings, the input to a trend. */\nexport interface TrendsInput {\n derived: DerivedSummary;\n findings: FindingSet;\n}\n\n// tests-ran detection — mirror the Report Card's command scan (card.ts).\nconst TEST_CMD =\n /\\b(pytest|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\\b/i;\n\nfunction ranTests(derived: DerivedSummary): boolean {\n return derived.spans.some((s) => {\n if (s.kind !== \"tool\" || !/bash|shell|exec|run|terminal/i.test(s.name)) {\n return false;\n }\n const cmd =\n s.input && typeof s.input === \"object\"\n ? String((s.input as Record<string, unknown>).command ?? \"\")\n : \"\";\n return TEST_CMD.test(cmd);\n });\n}\n\n/**\n * Reduce a finding id to its stable detector kind so the same kind aggregates\n * across sessions. Many detectors suffix a per-occurrence span id\n * (`destructive-tool-2-5`, `grader-edit-tool-3-1`, `force-push-gen-4`); strip it.\n * `errcluster-<tool>` suffixes a tool name, so collapse that family explicitly.\n */\nfunction kindOf(id: string): string {\n if (id.startsWith(\"errcluster-\")) {\n return \"errcluster\";\n }\n return id.replace(/-(?:tool-\\d+-\\d+|gen-\\d+)$/, \"\");\n}\n\ninterface RecAccum {\n kind: string;\n sessions: Set<number>;\n worst: Severity;\n title: string;\n example?: string;\n bestScore: number;\n}\n\n/**\n * Compute the cross-session digest. `inputs` are in **chronological order**\n * (oldest → newest); `requested` is the N the user asked for (for the no-silent-cap\n * note). Deterministic: same inputs → same Trends.\n */\nexport function computeTrends(inputs: TrendsInput[], requested: number): Trends {\n const counts: Record<GradeLetter, number> = { A: 0, B: 0, C: 0, F: 0 };\n const sequence: GradeLetter[] = [];\n const costSequence: number[] = [];\n let totalCostUsd = 0;\n let destructiveOps = 0;\n let filesChanged = 0;\n let ran = 0;\n\n const rec = new Map<string, RecAccum>();\n\n inputs.forEach((inp, i) => {\n const grade = gradeLetter(inp.findings.main);\n counts[grade]++;\n sequence.push(grade);\n\n const cost = inp.derived.totalCost || 0;\n costSequence.push(Number(cost.toFixed(4)));\n totalCostUsd += cost;\n destructiveOps += inp.derived.destructiveCount;\n filesChanged += inp.derived.filesChanged.length;\n if (ranTests(inp.derived)) {\n ran++;\n }\n\n // Recurring kinds: main + minor (so low-confidence-but-useful kinds like\n // out-of-scope still surface), counted once per session via the Set.\n for (const f of [...inp.findings.main, ...inp.findings.minor]) {\n const kind = kindOf(f.id);\n let e = rec.get(kind);\n if (!e) {\n e = {\n kind,\n sessions: new Set(),\n worst: f.severity,\n title: f.title,\n example: f.filePath,\n bestScore: f.score,\n };\n rec.set(kind, e);\n }\n e.sessions.add(i);\n if (SEV_ORDER[f.severity] < SEV_ORDER[e.worst]) {\n e.worst = f.severity;\n }\n if (f.score > e.bestScore) {\n e.bestScore = f.score;\n e.title = f.title;\n e.example = f.filePath ?? e.example;\n } else if (!e.example && f.filePath) {\n e.example = f.filePath;\n }\n }\n });\n\n const recurring: RecurringTrend[] = [...rec.values()]\n .map((e) => ({\n kind: e.kind,\n sessions: e.sessions.size,\n worst: e.worst,\n title: e.title,\n example: e.example,\n }))\n .sort(\n (a, b) =>\n b.sessions - a.sessions ||\n SEV_ORDER[a.worst] - SEV_ORDER[b.worst] ||\n a.kind.localeCompare(b.kind),\n );\n\n return {\n window: { requested, used: inputs.length },\n grades: { counts, sequence },\n recurring,\n evidence: {\n totalCostUsd: Number(totalCostUsd.toFixed(2)),\n destructiveOps,\n filesChanged,\n testsRanRate: { ran, of: inputs.length },\n costSequence,\n },\n };\n}\n\n// ---- rendering ----------------------------------------------------------------\n\nconst BARS = \"▁▂▃▄▅▆▇█\";\n\n/** A deterministic unicode sparkline scaled to the window's max. */\nfunction sparkline(values: number[]): string {\n if (values.length === 0) {\n return \"—\";\n }\n const max = Math.max(...values, 0);\n if (max <= 0) {\n return BARS[0].repeat(values.length);\n }\n return values\n .map((v) => BARS[Math.min(BARS.length - 1, Math.round((v / max) * (BARS.length - 1)))])\n .join(\"\");\n}\n\nconst money = (n: number): string =>\n `$${n.toLocaleString(\"en-US\", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;\n\nconst sessions = (n: number): string => `${n} session${n === 1 ? \"\" : \"s\"}`;\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n grn: \"\\x1b[32m\",\n yel: \"\\x1b[33m\",\n blu: \"\\x1b[34m\",\n mag: \"\\x1b[35m\",\n cyn: \"\\x1b[36m\",\n gray: \"\\x1b[90m\",\n};\nconst PLAIN = Object.fromEntries(Object.keys(ANSI).map((k) => [k, \"\"])) as typeof ANSI;\n\nconst gradeCol = (c: typeof ANSI, g: GradeLetter): string =>\n ({ A: c.grn, B: c.grn, C: c.yel, F: c.red })[g];\nconst sevCol = (c: typeof ANSI, s: Severity): string =>\n ({ critical: c.red, high: c.yel, medium: c.cyn, low: c.gray })[s];\nconst sevIcon = (s: Severity): string =>\n ({ critical: \"⛔\", high: \"⚠️ \", medium: \"🔍\", low: \"·\" })[s];\n\nexport type TrendsFormat = \"card\" | \"md\" | \"json\";\n\n/**\n * Render the digest. `card` is the ANSI default for stdout; `md` is a marker-\n * delimited section for `--out`; `json` is the structured `Trends` object.\n */\nexport function renderTrends(\n trends: Trends,\n format: TrendsFormat = \"card\",\n opts: { color?: boolean } = {},\n): string {\n if (format === \"json\") {\n return JSON.stringify(trends, null, 2);\n }\n return format === \"md\" ? renderMd(trends) : renderCard(trends, opts.color !== false);\n}\n\nfunction renderCard(trends: Trends, color: boolean): string {\n const c = color ? ANSI : PLAIN;\n const W = 74;\n const line = (s = \"\"): string => ` ${s}`;\n const out: string[] = [];\n\n const title = \"🧾 RECEIPTS — Trends\";\n const right = `last ${sessions(trends.window.used)} · proof`;\n // pad the header to W cols: lead space + title + gap + right + trail space = W.\n // title.length counts the 🧾 surrogate pair as 2, matching its 2-col render width.\n const gap = Math.max(1, W - 2 - title.length - right.length);\n out.push(`${c.cyn} ╔${\"═\".repeat(W)}╗${c.reset}`);\n out.push(\n ` ${c.cyn}║${c.reset} ${c.bold}🧾 RECEIPTS${c.reset}${c.dim} — Trends${c.reset}${\" \".repeat(gap)}${c.mag}${c.bold}${right}${c.reset} ${c.cyn}║${c.reset}`,\n );\n out.push(`${c.cyn} ╚${\"═\".repeat(W)}╝${c.reset}`);\n out.push(\"\");\n\n // grades\n const seq = trends.grades.sequence.map((g) => `${gradeCol(c, g)}${g}${c.reset}`).join(\" \");\n const counts =\n ([\"A\", \"B\", \"C\", \"F\"] as const)\n .filter((g) => trends.grades.counts[g] > 0)\n .map((g) => `${gradeCol(c, g)}${g}×${trends.grades.counts[g]}${c.reset}`)\n .join(\" \") || `${c.gray}none${c.reset}`;\n out.push(line(`${c.gray}Grades ${c.reset}${seq} ${c.gray}(oldest → newest)${c.reset}`));\n out.push(line(` ${counts}`));\n out.push(\"\");\n\n // recurring\n out.push(line(`${c.gray}RECURS MOST ${c.reset}${c.dim}(across sessions)${c.reset}`));\n if (trends.recurring.length === 0) {\n out.push(line(`${c.grn} ✅ nothing recurred — no findings across the window${c.reset}`));\n }\n for (const r of trends.recurring.slice(0, 8)) {\n const kn = `${r.sessions}/${trends.window.used}`;\n const file = r.example\n ? `${c.blu}${r.example.split(\"/\").pop()}${c.reset}`\n : `${c.gray}—${c.reset}`;\n out.push(\n line(\n ` ${c.bold}${kn.padStart(5)}${c.reset} ${sevIcon(r.worst)} ${r.title.slice(0, 44).padEnd(44)} ${sevCol(c, r.worst)}${SEV_SHORT[r.worst].padEnd(4)}${c.reset} ${c.gray}·${c.reset} ${file}`,\n ),\n );\n }\n out.push(\"\");\n\n // evidence\n const ev = trends.evidence;\n out.push(line(`${c.gray}EVIDENCE ${c.reset}${c.dim}(${sessions(trends.window.used)})${c.reset}`));\n out.push(\n line(\n ` ${c.grn}${money(ev.totalCostUsd)}${c.reset} total${c.gray} · ${c.reset}` +\n `${ev.destructiveOps ? c.red : c.grn}${ev.destructiveOps} destructive${c.reset}${c.gray} · ${c.reset}` +\n `tests ran ${ev.testsRanRate.ran}/${ev.testsRanRate.of}${c.gray} · ${c.reset}` +\n `${ev.filesChanged} files changed`,\n ),\n );\n const spark = color\n ? sparkline(ev.costSequence)\n : ev.costSequence.map((n) => n.toFixed(2)).join(\" \");\n out.push(line(` ${c.gray}cost ${c.reset}${spark}`));\n out.push(\"\");\n\n out.push(` ${c.gray}${\"─\".repeat(W)}${c.reset}`);\n out.push(\n line(\n `${c.grn}Verified by Receipts${c.reset}${c.gray} · deterministic · 0 model calls · evidence, not judgement${c.reset}`,\n ),\n );\n if (trends.window.used < trends.window.requested) {\n out.push(\n line(\n `${c.dim}(asked for ${trends.window.requested}; only ${trends.window.used} session${trends.window.used === 1 ? \"\" : \"s\"} available)${c.reset}`,\n ),\n );\n }\n return `\\n${out.join(\"\\n\")}\\n`;\n}\n\nfunction renderMd(trends: Trends): string {\n const lines: string[] = [TRENDS_START];\n lines.push(`## Receipts trends — last ${sessions(trends.window.used)}`);\n lines.push(\"<!-- generated by `receipts trends` — what your agent does over time -->\");\n lines.push(\"\");\n const seq = trends.grades.sequence.join(\" \");\n const counts = ([\"A\", \"B\", \"C\", \"F\"] as const)\n .filter((g) => trends.grades.counts[g] > 0)\n .map((g) => `${g}×${trends.grades.counts[g]}`)\n .join(\" \");\n lines.push(`**Grades** (oldest → newest): \\`${seq || \"—\"}\\` — ${counts || \"none\"}`);\n lines.push(\"\");\n lines.push(\"**Recurs most** (across sessions):\");\n if (trends.recurring.length === 0) {\n lines.push(\"- _nothing recurred — no findings across the window_\");\n }\n for (const r of trends.recurring.slice(0, 8)) {\n // basename only — keep absolute local paths out of a committed AGENTS.md\n const file = r.example ? ` _— ${r.example.split(\"/\").pop()}_` : \"\";\n lines.push(\n `- \\`${r.sessions}/${trends.window.used}\\` **${r.title}** (${SEV_SHORT[r.worst]})${file}`,\n );\n }\n lines.push(\"\");\n const ev = trends.evidence;\n lines.push(\n `**Evidence** (${sessions(trends.window.used)}): ${money(ev.totalCostUsd)} total · ${ev.destructiveOps} destructive ops · tests ran ${ev.testsRanRate.ran}/${ev.testsRanRate.of} · ${ev.filesChanged} files changed`,\n );\n lines.push(TRENDS_END);\n return lines.join(\"\\n\");\n}\n\n/** Insert/replace the delimited trends section in an existing document. Idempotent. */\nexport function upsertTrendsSection(existing: string, block: string): string {\n return upsertSection(existing, block, TRENDS_START, TRENDS_END);\n}\n"],"mappings":"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","names":["sessions"]}