@trazum/cli 1.8.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,492 @@
1
+ import { formatSignedUsd, formatUsd, getMessages, getModel } from '@trazum/core';
2
+ /**
3
+ * Markdown for the places a pull request is actually read.
4
+ *
5
+ * One renderer, two destinations. The GitHub step summary and a PR comment want
6
+ * the same numbers with different framing, and the numbers come from the same
7
+ * verdicts the terminal report prints — so a discrepancy between what a
8
+ * reviewer reads on the pull request and what the job log said is impossible by
9
+ * construction rather than by care.
10
+ *
11
+ * Nothing here knows the name `GITHUB_STEP_SUMMARY`. The CLI writes a file; the
12
+ * Action decides what that file is for. That keeps `trazum` a tool you can run
13
+ * on your laptop and read the output of.
14
+ */
15
+ /** GitHub rejects a comment body over 65,536 characters. */
16
+ export const MAX_COMMENT_CHARS = 60_000;
17
+ /** A step summary is capped at 1 MiB. Well under it, and honest when it trims. */
18
+ export const MAX_SUMMARY_CHARS = 900_000;
19
+ /**
20
+ * A value fit to sit in a table cell.
21
+ *
22
+ * Paths come from a repository, and on a pull request that means from whoever
23
+ * opened it. `prompts/a|b``c\|d.txt` is a legal POSIX filename, and each of
24
+ * those characters breaks a markdown table in its own way.
25
+ *
26
+ * **This emits `<code>` with HTML entities rather than a backtick span, and the
27
+ * reason is that the entity version has no failure mode to reason about.** The
28
+ * first version did the obvious thing — wrap in backticks, escape `|` as `\|`,
29
+ * widen the fence past the longest backtick run — and CodeQL was right to flag
30
+ * it: it did not handle a backslash. Given `a\|b.txt` it emitted `` `a\\|b.txt` ``,
31
+ * and whether that survives depends on whether the row splitter reads `\\|` as
32
+ * an escaped pipe or as an escaped backslash followed by a live one. It happens
33
+ * to work in cmark-gfm today. An escaper whose correctness rests on that is not
34
+ * an escaper.
35
+ *
36
+ * With entities there is **no `|` character in the output at all**, so the row
37
+ * cannot split under any scanner; backticks inside `<code>` are literal, so the
38
+ * fence arithmetic disappears; and a backslash needs no treatment. Three hazard
39
+ * classes collapse into one rule: encode `&`, `<`, `>` and `|`.
40
+ *
41
+ * Newlines still have to go — anything vertical ends the row — so they become a
42
+ * single space.
43
+ */
44
+ export function mdCell(value) {
45
+ const flat = value.replace(/[\r\n\t]+/g, ' ').trim();
46
+ if (flat === '')
47
+ return '';
48
+ const encoded = flat
49
+ // `&` first, or it would double-encode the entities added below.
50
+ .replace(/&/g, '&amp;')
51
+ .replace(/</g, '&lt;')
52
+ .replace(/>/g, '&gt;')
53
+ .replace(/\|/g, '&#124;');
54
+ return `<code>${encoded}</code>`;
55
+ }
56
+ /**
57
+ * A value fit to sit inline in prose.
58
+ *
59
+ * For values that are *words* — a model's display name. Paths go through
60
+ * `mdCell`, because a path is code and a code span is both safer and less ugly:
61
+ * escaping every `.` and `-` turned `a.txt` into `a\.txt`, which renders
62
+ * correctly and reads like a bug to anyone who sees the source.
63
+ *
64
+ * So the escaped set is only what can change meaning **mid-line**: emphasis,
65
+ * code spans, links, autolinks and table cells. `#`, `-`, `+` and `.` are
66
+ * block-level constructs that need to start a line to mean anything, and the
67
+ * newline collapse above guarantees this value never does.
68
+ */
69
+ export function mdText(value) {
70
+ return value
71
+ .replace(/[\r\n]+/g, ' ')
72
+ .replace(/([\\`*_~[\]<>|])/g, '\\$1')
73
+ .trim();
74
+ }
75
+ /**
76
+ * Untrusted **prose** in a table cell.
77
+ *
78
+ * `mdCell` is for values that are code — a path, a sha — and it says so by
79
+ * wrapping them in `<code>`. A commit subject and an author's name are neither.
80
+ * Rendering `<code>David Muñoz Rey</code>` in a table typesets somebody's name as
81
+ * a code span, and `<code>fix: the rules only trimmed in two languages</code>`
82
+ * does the same to a sentence. Both were wrong in the first draft of the blame
83
+ * report, and only visible once it was rendered.
84
+ *
85
+ * The safety is `mdCell`'s, unchanged, for the same reason: **entities, so there
86
+ * is no `|` in the output at all** and the row cannot split under any scanner.
87
+ * `mdText`'s backslash escaping is complete and would also survive a cell, but it
88
+ * puts the correctness on a reader's ability to see that `\\\|` is an escaped
89
+ * backslash followed by an escaped pipe. Nothing here should need that.
90
+ *
91
+ * Then the inline-markdown set on top, which `mdCell` does not need because
92
+ * backticks make its content literal. A subject reading `fix *everything*` would
93
+ * otherwise arrive in italics, and two backticks in one would open a code span —
94
+ * cosmetic rather than dangerous, and still not what the author wrote.
95
+ */
96
+ export function mdTextCell(value) {
97
+ const flat = value.replace(/[\r\n\t]+/g, ' ').trim();
98
+ if (flat === '')
99
+ return '';
100
+ return (flat
101
+ // `&` first, or it would double-encode the entities added below.
102
+ .replace(/&/g, '&amp;')
103
+ .replace(/</g, '&lt;')
104
+ .replace(/>/g, '&gt;')
105
+ .replace(/\|/g, '&#124;')
106
+ // Applied last, and deliberately not including `|`, `<` or `>`: those are
107
+ // already entities by this point and have no character left to escape.
108
+ .replace(/([\\`*_~[\]])/g, '\\$1'));
109
+ }
110
+ /** Truncates a body to fit, saying so rather than trailing off. */
111
+ export function fitWithin(body, limit, notice) {
112
+ if (body.length <= limit)
113
+ return body;
114
+ const room = limit - notice.length - 2;
115
+ return `${body.slice(0, Math.max(0, room))}\n\n${notice}`;
116
+ }
117
+ const overBudget = (v) => v.maxTokens !== null && v.tokens > v.maxTokens;
118
+ /**
119
+ * The check report as markdown.
120
+ *
121
+ * The table is the whole point, so it comes first and the prose comes after. A
122
+ * reviewer scanning a comment reads the rows and stops.
123
+ */
124
+ /**
125
+ * The baseline half of a check report.
126
+ *
127
+ * Only the directions that cost money are itemised — a list of every file that
128
+ * shrank buries the two rows a reviewer has to act on. Shrinking still gets its
129
+ * headline, because a branch that made things cheaper deserves to say so.
130
+ */
131
+ function baselineBlock(baseline, t) {
132
+ const n = (value) => value.toLocaleString(t.numberLocale);
133
+ const md = t.markdown;
134
+ const { comparison, breached, money } = baseline;
135
+ const pct = (value) => `${value > 0 ? '+' : ''}${value.toFixed(1)}%`;
136
+ const signed = (value) => `${value > 0 ? '+' : ''}${n(value)}`;
137
+ const lines = [];
138
+ const headline = comparison.delta === 0
139
+ ? md.baselineUnchanged()
140
+ : comparison.delta > 0
141
+ ? md.baselineGrew(n(comparison.delta), pct(comparison.deltaPct))
142
+ : md.baselineShrank(n(-comparison.delta), pct(comparison.deltaPct));
143
+ if (breached.length > 0) {
144
+ const limits = breached
145
+ .map((breach) => breach.kind === 'tokens'
146
+ ? md.baselineLimitTokens(n(breach.limit))
147
+ : md.baselineLimitPct(String(breach.limit)))
148
+ .join(', ');
149
+ lines.push('> [!CAUTION]');
150
+ lines.push(`> **${headline}** — ${md.baselineOverLimit(limits)}.`);
151
+ }
152
+ else {
153
+ lines.push(`**${headline}.**`);
154
+ }
155
+ lines.push('');
156
+ const moved = [...comparison.grown, ...comparison.added, ...comparison.removed];
157
+ if (moved.length > 0) {
158
+ lines.push(`| | ${md.columnFile()} | ${md.baselineColumnBefore()} | ${md.baselineColumnAfter()} | ${md.columnChange()} |`);
159
+ lines.push('|:--:|---|--:|--:|--:|');
160
+ for (const change of comparison.grown) {
161
+ lines.push(`| 📈 | ${mdCell(change.path)} | ${n(change.before)} | ${n(change.after)} | ${signed(change.delta)} |`);
162
+ }
163
+ for (const change of comparison.added) {
164
+ lines.push(`| 🆕 | ${mdCell(change.path)} | – | ${n(change.after)} | ${signed(change.delta)} |`);
165
+ }
166
+ for (const change of comparison.removed) {
167
+ lines.push(`| 🗑️ | ${mdCell(change.path)} | ${n(change.before)} | – | ${signed(change.delta)} |`);
168
+ }
169
+ lines.push('');
170
+ }
171
+ // Money is shown when it means something and explained when it does not. A
172
+ // delta across a reprice is two different measurements subtracted, which is
173
+ // worse than no figure at all in a comment somebody will quote in a meeting.
174
+ lines.push(money.comparable
175
+ ? md.baselineMoney(formatUsd(money.before), formatUsd(money.after), formatSignedUsd(money.after - money.before))
176
+ : `_${md.baselineMoneyIncomparable()}_`);
177
+ lines.push('');
178
+ if (breached.length > 0) {
179
+ lines.push(md.baselineReRecord('trazum baseline', baseline.path));
180
+ lines.push('');
181
+ }
182
+ return lines;
183
+ }
184
+ export function renderCheckMarkdown(input) {
185
+ const { target, verdicts, level, tokenSource, truncated, t } = input;
186
+ const n = (value) => value.toLocaleString(t.numberLocale);
187
+ const md = t.markdown;
188
+ const failures = verdicts.filter(overBudget);
189
+ const unbudgeted = verdicts.filter((v) => v.maxTokens === null);
190
+ // The verdict counts what was *measured*, not what was listed. "All 3 prompts
191
+ // are within budget" over a set where one had no budget claims something about
192
+ // that file which nobody established — and the unbudgeted note below is the
193
+ // honest half of the same sentence.
194
+ const measured = verdicts.length - unbudgeted.length;
195
+ const lines = [];
196
+ lines.push(`### ${md.checkHeading(mdCell(target))}`);
197
+ lines.push('');
198
+ /**
199
+ * The cost diff leads.
200
+ *
201
+ * A reviewer reads the first two lines of a comment and scrolls past the rest.
202
+ * "Does each file fit its ceiling" is the older question and the narrower one;
203
+ * "did this branch make the repository more expensive" is what the pull request
204
+ * is actually proposing, so it goes above the table rather than under it.
205
+ */
206
+ if (input.baseline)
207
+ lines.push(...baselineBlock(input.baseline, t));
208
+ lines.push(failures.length > 0
209
+ ? `**${md.overBudget(failures.length, measured)}**`
210
+ : md.allWithin(measured));
211
+ lines.push('');
212
+ lines.push(`| | ${md.columnFile()} | ${md.columnTokens()} | ${md.columnBudget()} |`);
213
+ lines.push('|:--:|---|--:|--:|');
214
+ for (const v of verdicts) {
215
+ const mark = v.maxTokens === null ? '–' : overBudget(v) ? '❌' : '✅';
216
+ const budget = v.maxTokens === null ? md.noBudget() : n(v.maxTokens);
217
+ lines.push(`| ${mark} | ${mdCell(v.path)} | ${n(v.tokens)} | ${budget} |`);
218
+ }
219
+ lines.push('');
220
+ // Advice belongs under the table, once, rather than inside a cell where it
221
+ // would either be truncated or wreck the column widths.
222
+ const actionable = failures.filter((v) => v.optimizedTokens !== null);
223
+ if (actionable.length > 0) {
224
+ lines.push(`#### ${md.whatWouldHelp()}`);
225
+ lines.push('');
226
+ for (const v of actionable) {
227
+ const fits = v.optimizedTokens <= v.maxTokens;
228
+ lines.push(`- ${mdCell(v.path)} — ${fits
229
+ ? md.wouldFit(level, n(v.optimizedTokens))
230
+ : md.stillTooBig(n(v.optimizedTokens))}`);
231
+ }
232
+ lines.push('');
233
+ }
234
+ if (unbudgeted.length > 0) {
235
+ // Named, not hidden. A prompt outside every pattern is not being watched,
236
+ // and a report that omits that reads as "everything is fine".
237
+ lines.push(md.unbudgetedNote(unbudgeted.length));
238
+ lines.push('');
239
+ }
240
+ if (truncated) {
241
+ lines.push(`> [!WARNING]`);
242
+ lines.push(`> ${md.truncated()}`);
243
+ lines.push('');
244
+ }
245
+ lines.push(`<sub>${md.footer(tokenSource === 'external' ? md.sourceExact() : md.sourceEstimated(), level)}</sub>`);
246
+ return lines.join('\n');
247
+ }
248
+ /**
249
+ * The diff report as markdown.
250
+ *
251
+ * Carries the sign convention into the heading, because this is the one place a
252
+ * reader arrives with no context: every number is `after - before`, and positive
253
+ * means worse. Getting that wrong in a PR comment would be worse than not
254
+ * commenting.
255
+ */
256
+ export function renderDiffMarkdown(input) {
257
+ const { comparison, beforePath, afterPath, optimized, locale, t } = input;
258
+ const n = (value) => value.toLocaleString(t.numberLocale);
259
+ const md = t.markdown;
260
+ const signed = (value) => `${value > 0 ? '+' : ''}${n(value)}`;
261
+ const grew = comparison.tokenDelta > 0;
262
+ const mark = grew ? '⚠️' : comparison.tokenDelta < 0 ? '✅' : '➖';
263
+ const lines = [];
264
+ lines.push(`### ${md.diffHeading(mdCell(beforePath), mdCell(afterPath))}`);
265
+ lines.push('');
266
+ if (optimized) {
267
+ lines.push(`_${md.measuringOptimised()}_`);
268
+ lines.push('');
269
+ }
270
+ lines.push(`| | ${md.columnMetric()} | ${md.columnChange()} |`);
271
+ lines.push('|:--:|---|--:|');
272
+ lines.push(`| ${mark} | ${md.metricTokens(n(comparison.tokensBefore), n(comparison.tokensAfter))} | ${signed(comparison.tokenDelta)} (${signed(Math.round(comparison.deltaPct))}%) |`);
273
+ lines.push(`| 💰 | ${mdText(md.metricMonthly(n(comparison.usage.callsPerMonth), getModel(comparison.usage.model).displayName))} | ${formatSignedUsd(comparison.monthlyDeltaUsd)} |`);
274
+ lines.push('');
275
+ lines.push(`<sub>${md.deltaConvention()}</sub>`);
276
+ lines.push('');
277
+ const copy = getMessages(locale).rules;
278
+ const { rules, advisories } = comparison;
279
+ if (advisories.appeared.length > 0) {
280
+ lines.push(`> [!WARNING]`);
281
+ lines.push(`> **${md.advisoriesAppeared()}**`);
282
+ for (const id of advisories.appeared)
283
+ lines.push(`> - \`${id}\``);
284
+ lines.push('');
285
+ }
286
+ if (advisories.resolved.length > 0) {
287
+ lines.push(`**${md.advisoriesResolved()}**`);
288
+ for (const id of advisories.resolved)
289
+ lines.push(`- \`${id}\``);
290
+ lines.push('');
291
+ }
292
+ if (rules.newlyFiring.length > 0) {
293
+ lines.push(`**${md.rulesNewlyFiring()}**`);
294
+ for (const id of rules.newlyFiring)
295
+ lines.push(`- ${mdText(copy[id].title)}`);
296
+ lines.push('');
297
+ }
298
+ if (rules.noLongerFiring.length > 0) {
299
+ lines.push(`**${md.rulesNoLongerFiring()}**`);
300
+ for (const id of rules.noLongerFiring)
301
+ lines.push(`- ${mdText(copy[id].title)}`);
302
+ lines.push('');
303
+ }
304
+ return lines.join('\n').trimEnd();
305
+ }
306
+ /**
307
+ * Wraps a report for a pull request comment.
308
+ *
309
+ * **Collapsed when there is nothing wrong**, and that is the decision worth
310
+ * defending. A green table that stays green on every push is the thing a
311
+ * maintainer learns to skip — and once they skip it, they skip the red one too.
312
+ * Expanded means something needs reading.
313
+ *
314
+ * The marker is an HTML comment, invisible in the rendered comment and stable
315
+ * across pushes, so the poster can find its own previous comment and replace it
316
+ * rather than adding another. `key` separates two runs that legitimately post
317
+ * about different things in the same pull request.
318
+ */
319
+ export function wrapForComment(body, options) {
320
+ const { marker, ok, title, collapsedNote, trimNotice } = options;
321
+ const inner = fitWithin(body, MAX_COMMENT_CHARS - 400, trimNotice);
322
+ if (!ok)
323
+ return `${marker}\n\n${inner}`;
324
+ return [
325
+ marker,
326
+ '',
327
+ `<details>`,
328
+ `<summary>✅ ${title} — ${collapsedNote}</summary>`,
329
+ '',
330
+ inner,
331
+ '',
332
+ `</details>`,
333
+ ].join('\n');
334
+ }
335
+ /**
336
+ * The invisible anchor a comment is found by on the next push.
337
+ *
338
+ * The key reaches an HTML comment, so it is reduced to alphanumerics and single
339
+ * separators: runs collapse, edges are trimmed, and a key with nothing usable in
340
+ * it falls back to `default`. That leaves no `--` in the output at all, which
341
+ * takes the whole `-->` question off the table rather than reasoning about
342
+ * whether a particular arrangement of dashes happens to be safe.
343
+ */
344
+ export function commentMarker(key) {
345
+ const safe = key
346
+ .replace(/[^A-Za-z0-9]+/g, '-')
347
+ .replace(/^-+|-+$/g, '')
348
+ .slice(0, 64)
349
+ .replace(/-+$/, '');
350
+ const usable = /[A-Za-z0-9]/.test(safe) ? safe : 'default';
351
+ return `<!-- trazum-report:${usable} -->`;
352
+ }
353
+ /**
354
+ * The ranking as markdown.
355
+ *
356
+ * Every string but the heading comes from `t.rank`, the same object the terminal
357
+ * report reads. That is not tidiness — a second copy of "there is no score" is a
358
+ * second thing to keep true, and the first time somebody softens one of these
359
+ * sentences they will soften the copy they happened to be looking at.
360
+ *
361
+ * **Money and tokens stay in adjacent columns**, as in the terminal, and for the
362
+ * reason the terminal has them: four prompts reading `$0.25` looked like four
363
+ * equivalent jobs when three of them recovered a single token. A pull request
364
+ * comment is where that misreading would do the most damage, because nobody
365
+ * reading one has the file open.
366
+ */
367
+ export function renderRankMarkdown(input) {
368
+ const { root, ranked, level, modelDisplayName, callsPerMonth, truncated, skipped, t } = input;
369
+ const n = (value) => value.toLocaleString(t.numberLocale);
370
+ const cols = t.rank.columns;
371
+ const lines = [];
372
+ lines.push(`### ${t.markdown.rankHeading(mdCell(root), ranked.length)}`);
373
+ lines.push('');
374
+ lines.push(t.rank.subheading(mdText(modelDisplayName), n(callsPerMonth)));
375
+ lines.push('');
376
+ lines.push(`| ${cols.recoverable} | ${cols.tokensBack} | ${cols.tokens} | ${cols.density} | ${cols.notes} |`);
377
+ lines.push('|--:|--:|--:|--:|---|');
378
+ for (const entry of ranked) {
379
+ const { profile } = entry;
380
+ const notes = [];
381
+ if (profile.examples > 0) {
382
+ notes.push(t.rank.noteExamples(profile.examples, n(profile.exampleTokens)));
383
+ }
384
+ if (profile.formatTokens > 0)
385
+ notes.push(t.rank.noteFormat(n(profile.formatTokens)));
386
+ const protectedShare = profile.tokens === 0 ? 0 : profile.protectedTokens / profile.tokens;
387
+ if (protectedShare >= 0.25)
388
+ notes.push(t.rank.noteProtected(Math.round(protectedShare * 100)));
389
+ // The path is `<code>`; the notes are prose in the same cell. Two escapers
390
+ // in one cell because they are two kinds of value, and `mdCell` on a whole
391
+ // sentence would entity-encode punctuation nobody needs encoded.
392
+ const note = notes.length > 0 ? ` — ${mdTextCell(notes.join(', '))}` : '';
393
+ lines.push(`| ${formatUsd(entry.recoverableUsd)} | ${n(entry.recoverable)} | ${n(profile.tokens)} | `
394
+ + `${profile.tokensPerSentence.toFixed(1)} | ${mdCell(entry.path)}${note} |`);
395
+ }
396
+ lines.push('');
397
+ if (truncated) {
398
+ lines.push('> [!WARNING]');
399
+ lines.push(`> ${t.check.walkTruncated()}`);
400
+ lines.push('');
401
+ }
402
+ // Named rather than silent, exactly as in the terminal: a repository where
403
+ // most prompts live in code would otherwise show a short list and read as the
404
+ // whole picture.
405
+ if (skipped > 0) {
406
+ lines.push(t.rank.skipped(skipped));
407
+ lines.push('');
408
+ }
409
+ lines.push(`<sub>${mdText(t.rank.densityNote())}</sub>`);
410
+ lines.push('');
411
+ lines.push(`<sub>${mdText(t.rank.recoverableNote())} ${t.markdown.rankLevel(level)}</sub>`);
412
+ return lines.join('\n');
413
+ }
414
+ /**
415
+ * The token history as markdown.
416
+ *
417
+ * A rise is bold and a fall is not, which is the same asymmetry the terminal
418
+ * makes with colour: growth is the thing somebody has to act on, and a report
419
+ * that shouts equally about both trains the reader to ignore it.
420
+ *
421
+ * **Author and subject are the least trusted values this repository renders.**
422
+ * They come from commit metadata, which on a pull request from a fork is written
423
+ * by whoever opened it, and they land in a table on a page maintainers read. Both
424
+ * go through `mdCell`, which emits entities rather than escapes — so there is no
425
+ * `|` in the output to split a row and no backtick arithmetic to get wrong.
426
+ */
427
+ export function renderBlameMarkdown(input) {
428
+ const { repoPath, rows, truncated, netCost, t } = input;
429
+ const n = (value) => value.toLocaleString(t.numberLocale);
430
+ const cols = t.blame.columns;
431
+ const measured = rows.filter((r) => r.tokens !== null);
432
+ const newest = measured[0];
433
+ const oldest = measured[measured.length - 1];
434
+ const lines = [];
435
+ lines.push(`### ${t.markdown.blameHeading(mdCell(repoPath))}`);
436
+ lines.push('');
437
+ lines.push(`| ${cols.when} | ${cols.tokens} | ${cols.change} | ${cols.who} | ${cols.commit} |`);
438
+ lines.push('|---|--:|--:|---|---|');
439
+ for (const row of rows) {
440
+ const tokens = row.tokens === null ? t.blame.goneAt() : n(row.tokens);
441
+ const change = row.delta === null
442
+ ? row.tokens === null
443
+ ? ''
444
+ : t.blame.addedAt()
445
+ : row.delta > 0
446
+ ? `**+${n(row.delta)}**`
447
+ : row.delta < 0
448
+ ? n(row.delta)
449
+ : '·';
450
+ lines.push(`| ${row.revision.date.slice(0, 10)} | ${tokens} | ${change} | `
451
+ + `${mdTextCell(row.revision.author)} | ${mdCell(row.revision.shortSha)} `
452
+ + `${mdTextCell(row.revision.subject)} |`);
453
+ }
454
+ lines.push('');
455
+ if (truncated) {
456
+ lines.push(t.blame.truncated(rows.length));
457
+ lines.push('');
458
+ }
459
+ const renamed = rows.find((row) => row.name !== null);
460
+ if (renamed?.name) {
461
+ lines.push(t.blame.followedRename(mdCell(renamed.name)));
462
+ lines.push('');
463
+ }
464
+ if (newest && oldest && newest !== oldest) {
465
+ const delta = newest.tokens - oldest.tokens;
466
+ const pct = oldest.tokens === 0
467
+ ? '—'
468
+ : `${delta >= 0 ? '+' : ''}${((delta / oldest.tokens) * 100).toFixed(0)}%`;
469
+ lines.push(`**${t.blame.net(n(oldest.tokens), n(newest.tokens), `${delta >= 0 ? '+' : ''}${n(delta)}`, pct)}**`);
470
+ lines.push('');
471
+ // Priced by the caller, which owns the usage profile. Recomputing it here
472
+ // would give a comment and a job log two chances to disagree about the same
473
+ // history.
474
+ if (netCost !== null) {
475
+ lines.push(t.blame.netCost(netCost.amount, mdText(netCost.modelDisplayName), n(netCost.callsPerMonth)));
476
+ lines.push('');
477
+ }
478
+ }
479
+ // The single worst commit, which is the question the command is really for.
480
+ const worst = rows
481
+ .filter((row) => row.delta !== null && row.delta > 0)
482
+ .sort((a, b) => b.delta - a.delta)[0];
483
+ if (worst) {
484
+ lines.push(`#### ${t.blame.biggestRise()}`);
485
+ lines.push('');
486
+ lines.push(`- ${t.blame.biggestRiseDetail(n(worst.delta), mdTextCell(worst.revision.author), mdTextCell(worst.revision.subject), mdCell(worst.revision.shortSha))}`);
487
+ lines.push('');
488
+ }
489
+ lines.push(`<sub>${mdText(t.blame.estimateNote())}</sub>`);
490
+ return lines.join('\n');
491
+ }
492
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import type { LlmProvider } from '@trazum/core';
2
+ /**
3
+ * Not asking the model the same question twice.
4
+ *
5
+ * The roadmap item this answers was "prompt caching for `--suggest`", meaning
6
+ * the API feature: mark a stable prefix with `cache_control` and pay a tenth of
7
+ * the price for it on every later call. **That cannot work here, and the reason
8
+ * is a number rather than an opinion.**
9
+ *
10
+ * Prompt caching has a minimum cacheable prefix — 512 tokens on the newest
11
+ * models, 1,024 on most, 4,096 on some — and a prefix shorter than the minimum
12
+ * is *silently* not cached: no error, no warning, `cache_creation_input_tokens`
13
+ * comes back zero. Trazum's suggest prompt is **291 tokens**. Marking it would
14
+ * have looked like an optimisation, cost a line of code, changed nothing, and
15
+ * been impossible to notice. `suggest-cache.test.js` measures it against the
16
+ * published minima so that stays true, or stops being true loudly.
17
+ *
18
+ * The stable prefix is also the only thing that *could* be cached: the rest of
19
+ * the request is the author's prompt, which is different every time. So there
20
+ * is no arrangement of `cache_control` that helps.
21
+ *
22
+ * What does help is the observation behind the request — running `--suggest`
23
+ * over a directory asks the same questions again on every run, and most of the
24
+ * prompts have not changed since the last one. Answering those from disk is not
25
+ * a 90% saving on the call, it is the whole call. On a re-run after editing two
26
+ * files out of forty, thirty-eight requests do not happen.
27
+ *
28
+ * Three decisions worth arguing with:
29
+ *
30
+ * **The raw response is cached, not the parsed suggestions.** Everything
31
+ * `suggestRewrites` does after the model answers — checking each `before`
32
+ * appears byte for byte, refusing anything that touches protected content,
33
+ * dropping overlaps — is deterministic and lives in the core. Caching the text
34
+ * means a hit is re-validated by *today's* rules rather than replaying a
35
+ * verdict reached by an older version. Same reasoning as recomputing token
36
+ * counts on read instead of storing them.
37
+ *
38
+ * **It is opt-in.** A cache hit returns what the model said last time, and a
39
+ * model is not a pure function — silently answering from a week-old response
40
+ * would be a surprise, in a tool whose other model-touching features
41
+ * (`--suggest`, `--apply-suggestions`, `--reorder`) all require asking twice.
42
+ *
43
+ * **The files are 0600 in a 0700 directory.** The cache holds prompt text, and
44
+ * a prompt is the most sensitive thing this tool ever touches — it is somebody's
45
+ * unreleased product behaviour. A world-readable cache in a shared home
46
+ * directory would publish it to every account on the machine.
47
+ */
48
+ /**
49
+ * Bumped when anything that shapes the answer changes and is not already in the
50
+ * key — the suggest system prompt, the response format, the checking rules.
51
+ * A stale entry answers a question that is no longer the one being asked.
52
+ *
53
+ * Exported so the test can derive a key independently rather than comparing
54
+ * `cacheKey` to itself.
55
+ */
56
+ export declare const SCHEMA = 2;
57
+ /** Seven days. Long enough for a working week, short enough that an alias that started pointing at a new model does not answer forever. */
58
+ export declare const DEFAULT_TTL_DAYS = 7;
59
+ export interface CacheEntry {
60
+ schema: number;
61
+ /** When it was written, so the TTL can be applied by the reader. */
62
+ at: number;
63
+ provider: string;
64
+ model: string;
65
+ /** The model's answer, before any checking. */
66
+ response: string;
67
+ }
68
+ /**
69
+ * Where the cache lives.
70
+ *
71
+ * `XDG_CACHE_HOME` first, because a user who set it meant it. Not the project
72
+ * directory: two checkouts of the same repository ask the same questions, and a
73
+ * per-checkout cache answers neither of them from the other.
74
+ */
75
+ export declare function cacheDir(env?: NodeJS.ProcessEnv): string;
76
+ /**
77
+ * The key: everything that changes the answer, and nothing that does not.
78
+ *
79
+ * `provider` and `model` are in here rather than only in the entry because two
80
+ * models answer differently — a hit from the wrong one is not a hit. The system
81
+ * prompt is in here rather than relying on `SCHEMA` alone, so a caller passing
82
+ * their own system prompt gets their own entries without anybody remembering to
83
+ * bump a constant.
84
+ */
85
+ export declare function cacheKey(input: {
86
+ provider: string;
87
+ model: string;
88
+ system: string;
89
+ user: string;
90
+ }): string;
91
+ export declare function readEntry(dir: string, key: string, now: number, ttlDays: number): CacheEntry | null;
92
+ export declare function writeEntry(dir: string, key: string, entry: CacheEntry): void;
93
+ /** Delete every entry. Returns how many went. */
94
+ export declare function clearCache(dir: string): number;
95
+ /** Entry count and total bytes, so `--clear-suggestion-cache` can say what it emptied. */
96
+ export declare function cacheStats(dir: string): {
97
+ entries: number;
98
+ bytes: number;
99
+ };
100
+ export interface CachedProvider extends LlmProvider {
101
+ /** How many calls this provider answered from disk. */
102
+ readonly hits: number;
103
+ /** How many it had to make. */
104
+ readonly misses: number;
105
+ }
106
+ /**
107
+ * Wraps a provider so identical questions are asked once.
108
+ *
109
+ * A wrapper rather than a change inside `suggestRewrites`, for two reasons: the
110
+ * core stays free of `node:fs` (it is browser-safe, and a test asserts the
111
+ * import graph), and every command that reaches for an LLM gets the cache by
112
+ * passing through one function rather than by each remembering to.
113
+ */
114
+ export declare function cachingProvider(inner: LlmProvider, options: {
115
+ dir: string;
116
+ ttlDays?: number;
117
+ now?: () => number;
118
+ }): CachedProvider;
119
+ //# sourceMappingURL=suggest-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest-cache.d.ts","sourceRoot":"","sources":["../src/suggest-cache.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,IAAI,CAAC;AAExB,2IAA2I;AAC3I,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGrE;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAMT;AAMD,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd,UAAU,GAAG,IAAI,CAwBnB;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAW5E;AAED,iDAAiD;AACjD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqB9C;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAoB1E;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE;IACP,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,GACA,cAAc,CAuChB"}