@maxiaochao/pi-toolkit 0.1.3

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,420 @@
1
+ // Auto-extracted assets from huggingface/tau src/tau_coding/session_usage.py.
2
+ // Do not hand-edit; regenerate with scripts/regenerate-assets.py.
3
+ // Backslashes are doubled for the TS template literal so the runtime string
4
+ // keeps the original escapes (e.g. \n stays a 2-char \n for the browser JS).
5
+
6
+ export const USAGE_STYLES = `
7
+ .usage-cards {
8
+ display: grid;
9
+ grid-template-columns: repeat(3, minmax(0, 1fr));
10
+ gap: 1px;
11
+ background: var(--line);
12
+ border: 1px solid var(--line);
13
+ }
14
+ .usage-card {
15
+ min-width: 0;
16
+ padding: 16px 18px;
17
+ background: var(--surface);
18
+ transition: background .16s ease;
19
+ }
20
+ .usage-card:hover { background: var(--surface-2); }
21
+ .usage-card span {
22
+ display: block;
23
+ color: var(--accent);
24
+ font-size: 0.62rem;
25
+ font-weight: 500;
26
+ letter-spacing: 0.1em;
27
+ text-transform: uppercase;
28
+ }
29
+ .usage-card strong {
30
+ display: block;
31
+ margin-top: 6px;
32
+ color: var(--bright);
33
+ font-size: 1.25rem;
34
+ font-weight: 500;
35
+ font-variant-numeric: tabular-nums;
36
+ }
37
+ .usage-note {
38
+ margin: 18px 0 0;
39
+ padding: 2px 0 2px 13px;
40
+ border-left: 2px solid var(--line);
41
+ color: var(--muted);
42
+ font-size: 0.74rem;
43
+ }
44
+ .usage-note::before {
45
+ content: "# ";
46
+ color: var(--accent);
47
+ font-weight: 700;
48
+ }
49
+ .usage-charts {
50
+ display: grid;
51
+ grid-template-columns: 1fr 1fr;
52
+ gap: 16px;
53
+ margin-top: 18px;
54
+ }
55
+ .usage-figure {
56
+ position: relative;
57
+ margin: 0;
58
+ padding: 16px 16px 6px;
59
+ background: var(--surface);
60
+ border: 1px solid var(--line);
61
+ }
62
+ .usage-figure:first-child { grid-column: 1 / -1; }
63
+ .usage-chart {
64
+ display: block;
65
+ width: 100%;
66
+ height: auto;
67
+ overflow: visible;
68
+ cursor: crosshair;
69
+ touch-action: none;
70
+ }
71
+ .png-button {
72
+ position: absolute;
73
+ top: 10px;
74
+ right: 10px;
75
+ padding: 3px 9px;
76
+ color: var(--muted);
77
+ background: var(--surface-2);
78
+ border: 1px solid var(--line-strong);
79
+ font-family: var(--mono);
80
+ font-size: 0.62rem;
81
+ letter-spacing: 0.1em;
82
+ text-transform: uppercase;
83
+ cursor: pointer;
84
+ transition: color .15s, border-color .15s;
85
+ }
86
+ .png-button:hover { color: var(--bright); border-color: var(--accent); }
87
+ .png-button[disabled] { opacity: .5; cursor: progress; }
88
+ .grid { stroke: var(--line-strong); stroke-width: 1; opacity: .75; }
89
+ .chart-title {
90
+ fill: var(--bright);
91
+ font-family: var(--mono);
92
+ font-size: 15px;
93
+ font-weight: 600;
94
+ }
95
+ .tick, .axis-label, .legend {
96
+ fill: var(--muted);
97
+ font-family: var(--mono);
98
+ font-size: 11px;
99
+ }
100
+ .hover-line {
101
+ stroke: var(--accent);
102
+ stroke-width: 1;
103
+ stroke-dasharray: 3 4;
104
+ pointer-events: none;
105
+ }
106
+ .event-line { stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .8; }
107
+ .event-marker { stroke: var(--surface); stroke-width: 2; }
108
+ .usage-event:hover .event-line, .usage-event:hover .event-marker { opacity: 1; }
109
+ .hover-point { stroke: var(--surface); stroke-width: 2; pointer-events: none; }
110
+ .series { transition: opacity .15s ease; }
111
+ .series.is-hidden { opacity: .07; pointer-events: none; }
112
+ .series-toggle { cursor: pointer; outline: none; }
113
+ .series-toggle[aria-pressed="false"] { opacity: .25; }
114
+ .series-toggle:hover text { text-decoration: underline; }
115
+ .usage-tooltip {
116
+ position: fixed;
117
+ z-index: 30;
118
+ display: none;
119
+ min-width: 170px;
120
+ padding: 10px 12px;
121
+ background: var(--bg);
122
+ color: var(--text);
123
+ border: 1px solid var(--line-strong);
124
+ box-shadow: 0 10px 35px rgba(0, 0, 0, .35);
125
+ font-family: var(--mono);
126
+ font-size: 0.74rem;
127
+ line-height: 1.7;
128
+ white-space: pre-line;
129
+ pointer-events: none;
130
+ }
131
+ .usage-details {
132
+ display: grid;
133
+ grid-template-columns: minmax(0, 3fr) minmax(190px, 1fr);
134
+ gap: 16px;
135
+ margin-top: 16px;
136
+ }
137
+ .usage-panel {
138
+ min-width: 0;
139
+ padding: 18px;
140
+ background: var(--surface);
141
+ border: 1px solid var(--line);
142
+ }
143
+ .usage-panel h2 { margin: 0 0 14px; text-transform: lowercase; }
144
+ .usage-panel h2::before { content: "$ "; color: var(--accent); }
145
+ .usage-table-wrap {
146
+ overflow: auto;
147
+ max-height: 560px;
148
+ border: 1px solid var(--line);
149
+ scrollbar-color: var(--line-strong) var(--surface);
150
+ }
151
+ .usage-panel table {
152
+ border-collapse: collapse;
153
+ width: 100%;
154
+ white-space: nowrap;
155
+ font-size: 0.72rem;
156
+ font-variant-numeric: tabular-nums;
157
+ }
158
+ .usage-panel th, .usage-panel td {
159
+ padding: 8px 10px;
160
+ text-align: right;
161
+ border-bottom: 1px solid var(--line);
162
+ }
163
+ .usage-panel tbody tr { transition: background .12s ease; }
164
+ .usage-panel tbody tr:hover { background: var(--surface-2); color: var(--bright); }
165
+ .usage-panel th {
166
+ position: sticky;
167
+ top: 0;
168
+ z-index: 1;
169
+ background: var(--bg);
170
+ color: var(--muted);
171
+ font-size: 0.58rem;
172
+ font-weight: 700;
173
+ letter-spacing: 0.08em;
174
+ text-transform: uppercase;
175
+ border-bottom: 1px solid var(--line-strong);
176
+ }
177
+ .usage-panel th:nth-child(3), .usage-panel th:nth-child(4),
178
+ .usage-panel th:nth-child(5),
179
+ .usage-panel td:nth-child(3), .usage-panel td:nth-child(4),
180
+ .usage-panel td:nth-child(5) { text-align: left; }
181
+ .usage-tool {
182
+ display: flex;
183
+ justify-content: space-between;
184
+ gap: 10px;
185
+ padding: 8px 2px;
186
+ border-bottom: 1px solid var(--line);
187
+ font-size: 0.76rem;
188
+ }
189
+ .usage-tool:last-child { border-bottom: none; }
190
+ .usage-tool strong { color: var(--accent); font-weight: 500; }
191
+ @media (max-width: 820px) {
192
+ .usage-cards { grid-template-columns: 1fr 1fr; }
193
+ .usage-charts { grid-template-columns: 1fr; }
194
+ .usage-figure:first-child { grid-column: auto; }
195
+ .usage-details { grid-template-columns: 1fr; }
196
+ }
197
+ `;
198
+
199
+ export const USAGE_SCRIPT = `
200
+ (function () {
201
+ var tooltip = document.querySelector(".usage-tooltip");
202
+ var charts = Array.prototype.slice.call(document.querySelectorAll(".usage-chart"));
203
+
204
+ // The Tau palette ships both variants; switch series colors with the page theme.
205
+ function syncTheme() {
206
+ var dark = document.documentElement.classList.contains("theme-dark");
207
+ charts.forEach(function (chart) {
208
+ var colored = chart.querySelectorAll("[data-dark][data-light]");
209
+ Array.prototype.forEach.call(colored, function (node) {
210
+ var color = dark ? node.dataset.dark : node.dataset.light;
211
+ var tagName = node.tagName.toLowerCase();
212
+ if (tagName === "polyline" || tagName === "line") {
213
+ node.setAttribute("stroke", color);
214
+ } else {
215
+ node.setAttribute("fill", color);
216
+ }
217
+ });
218
+ });
219
+ }
220
+ window.addEventListener("tau-themechange", syncTheme);
221
+ syncTheme();
222
+
223
+ function hideTooltip(chart) {
224
+ if (!tooltip) {
225
+ return;
226
+ }
227
+ tooltip.style.display = "none";
228
+ var line = chart.querySelector(".hover-line");
229
+ if (line) {
230
+ line.setAttribute("visibility", "hidden");
231
+ }
232
+ chart.querySelectorAll(".hover-point").forEach(function (point) {
233
+ point.setAttribute("visibility", "hidden");
234
+ });
235
+ }
236
+ charts.forEach(function (chart) {
237
+ var count = Number(chart.dataset.count);
238
+ var left = Number(chart.dataset.left);
239
+ var right = Number(chart.dataset.right);
240
+ var viewWidth = chart.viewBox.baseVal.width;
241
+ chart.addEventListener("pointermove", function (event) {
242
+ var rect = chart.getBoundingClientRect();
243
+ var svgX = (event.clientX - rect.left) * viewWidth / rect.width;
244
+ var plotWidth = viewWidth - left - right;
245
+ if (svgX < left || svgX > viewWidth - right || count < 1) {
246
+ hideTooltip(chart);
247
+ return;
248
+ }
249
+ var ratio = (svgX - left) / plotWidth * Math.max(count - 1, 1);
250
+ var index = Math.max(0, Math.min(count - 1, Math.round(ratio)));
251
+ var activeSeries = Array.prototype.filter.call(
252
+ chart.querySelectorAll(".series"),
253
+ function (series) { return !series.classList.contains("is-hidden"); }
254
+ );
255
+ if (!activeSeries.length) {
256
+ return;
257
+ }
258
+ var tooltipLines = [];
259
+ activeSeries.forEach(function (series) {
260
+ var polyline = series.querySelector(".series-line");
261
+ var point = polyline && polyline.points.getItem(index);
262
+ var marker = series.querySelector(".hover-point");
263
+ if (!point || !marker) {
264
+ return;
265
+ }
266
+ marker.setAttribute("cx", point.x);
267
+ marker.setAttribute("cy", point.y);
268
+ marker.setAttribute("visibility", "visible");
269
+ var labels = (series.dataset.labels || "").split("|");
270
+ tooltipLines.push(series.dataset.name + " " + labels[index]);
271
+ });
272
+ chart.querySelectorAll('.usage-event[data-request="' + (index + 1) + '"]')
273
+ .forEach(function (sessionEvent) {
274
+ tooltipLines.push("Event " + sessionEvent.dataset.eventInfo);
275
+ });
276
+ var x = activeSeries[0].querySelector(".hover-point").getAttribute("cx");
277
+ var line = chart.querySelector(".hover-line");
278
+ line.setAttribute("x1", x);
279
+ line.setAttribute("x2", x);
280
+ line.setAttribute("visibility", "visible");
281
+ if (!tooltip) {
282
+ return;
283
+ }
284
+ var ts = chart.dataset.timestamps;
285
+ var tsLabel = ts ? " @ " + ts.split("|")[index] : "";
286
+ tooltip.textContent = "Request " + (index + 1) + tsLabel
287
+ + "\\n" + tooltipLines.join("\\n");
288
+ tooltip.style.display = "block";
289
+ var tooltipRect = tooltip.getBoundingClientRect();
290
+ var maxLeft = window.innerWidth - tooltipRect.width - 10;
291
+ tooltip.style.left = Math.min(maxLeft, event.clientX + 14) + "px";
292
+ tooltip.style.top = Math.max(10, event.clientY - tooltipRect.height - 12) + "px";
293
+ });
294
+ chart.addEventListener("pointerleave", function () {
295
+ hideTooltip(chart);
296
+ });
297
+ function toggleSeries(control) {
298
+ var id = control.dataset.seriesId;
299
+ var series = chart.querySelector('.series[data-series-id="' + id + '"]');
300
+ var visible = control.getAttribute("aria-pressed") === "true";
301
+ control.setAttribute("aria-pressed", String(!visible));
302
+ series.classList.toggle("is-hidden", visible);
303
+ }
304
+ chart.querySelectorAll(".series-toggle").forEach(function (control) {
305
+ control.addEventListener("click", function (event) {
306
+ event.stopPropagation();
307
+ toggleSeries(control);
308
+ });
309
+ control.addEventListener("keydown", function (event) {
310
+ if (event.key === "Enter" || event.key === " ") {
311
+ event.preventDefault();
312
+ toggleSeries(control);
313
+ }
314
+ });
315
+ });
316
+ });
317
+
318
+ var SVG_NS = "http:" + "//www.w3.org/2000/svg";
319
+ // The light-theme variants are designed to remain legible on white.
320
+ var PRINT_CSS = [
321
+ 'text{font-family:"JetBrains Mono",Consolas,Menlo,monospace}',
322
+ ".chart-title{fill:#111827;font-size:15px;font-weight:600}",
323
+ ".tick,.axis-label,.legend{fill:#475569;font-size:11px}",
324
+ ".grid{stroke:#cbd5e1;stroke-width:1}",
325
+ ".point{stroke:#ffffff;stroke-width:1}"
326
+ ].join("");
327
+
328
+ function staticSvg(chart) {
329
+ var width = chart.viewBox.baseVal.width;
330
+ var height = chart.viewBox.baseVal.height;
331
+ var clone = chart.cloneNode(true);
332
+ clone.setAttribute("xmlns", SVG_NS);
333
+ clone.setAttribute("width", width);
334
+ clone.setAttribute("height", height);
335
+ clone.removeAttribute("class");
336
+ clone.querySelectorAll(".hover-line").forEach(function (node) {
337
+ node.remove();
338
+ });
339
+ clone.querySelectorAll(".series.is-hidden").forEach(function (node) {
340
+ var id = node.dataset.seriesId;
341
+ var control = clone.querySelector('.series-toggle[data-series-id="' + id + '"]');
342
+ if (control) {
343
+ control.remove();
344
+ }
345
+ node.remove();
346
+ });
347
+ clone.querySelectorAll(".hover-point").forEach(function (node) {
348
+ node.remove();
349
+ });
350
+ clone.querySelectorAll("[data-light]").forEach(function (node) {
351
+ var tagName = node.tagName.toLowerCase();
352
+ if (tagName === "polyline" || tagName === "line") {
353
+ node.setAttribute("stroke", node.dataset.light);
354
+ } else {
355
+ node.setAttribute("fill", node.dataset.light);
356
+ }
357
+ });
358
+ var style = document.createElementNS(SVG_NS, "style");
359
+ style.textContent = PRINT_CSS;
360
+ var background = document.createElementNS(SVG_NS, "rect");
361
+ background.setAttribute("x", "0");
362
+ background.setAttribute("y", "0");
363
+ background.setAttribute("width", width);
364
+ background.setAttribute("height", height);
365
+ background.setAttribute("fill", "#ffffff");
366
+ clone.insertBefore(background, clone.firstChild);
367
+ clone.insertBefore(style, clone.firstChild);
368
+ return {
369
+ markup: new XMLSerializer().serializeToString(clone),
370
+ width: width,
371
+ height: height
372
+ };
373
+ }
374
+
375
+ function downloadPng(chart, button) {
376
+ var data = staticSvg(chart);
377
+ var scale = 2;
378
+ var image = new Image();
379
+ image.onload = function () {
380
+ var canvas = document.createElement("canvas");
381
+ canvas.width = data.width * scale;
382
+ canvas.height = data.height * scale;
383
+ var context = canvas.getContext("2d");
384
+ context.fillStyle = "#ffffff";
385
+ context.fillRect(0, 0, canvas.width, canvas.height);
386
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
387
+ canvas.toBlob(function (blob) {
388
+ var url = URL.createObjectURL(blob);
389
+ var link = document.createElement("a");
390
+ var title = chart.getAttribute("aria-label") || "chart";
391
+ link.href = url;
392
+ link.download = title.toLowerCase().replace(/[^a-z0-9]+/g, "-")
393
+ .replace(/^-|-$/g, "") + ".png";
394
+ document.body.appendChild(link);
395
+ link.click();
396
+ link.remove();
397
+ URL.revokeObjectURL(url);
398
+ button.disabled = false;
399
+ }, "image/png");
400
+ };
401
+ image.onerror = function () {
402
+ button.disabled = false;
403
+ button.textContent = "failed";
404
+ };
405
+ image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(data.markup);
406
+ }
407
+
408
+ document.querySelectorAll(".png-button").forEach(function (button) {
409
+ button.addEventListener("click", function () {
410
+ var figure = button.closest(".usage-figure");
411
+ var chart = figure ? figure.querySelector(".usage-chart") : null;
412
+ if (!chart) {
413
+ return;
414
+ }
415
+ button.disabled = true;
416
+ downloadPng(chart, button);
417
+ });
418
+ });
419
+ })();
420
+ `;
@@ -0,0 +1,196 @@
1
+ // Deterministic test cases for analyzeStreaks / analyzeMisses / parseEntries.
2
+ // Run: node --experimental-strip-types tests/run-tests.mjs
3
+ import { parseEntries, analyzeStreaks, analyzeMisses } from "../render.ts";
4
+
5
+ let pass = 0, fail = 0;
6
+ function check(name, cond, detail = "") {
7
+ if (cond) { pass++; console.log(` PASS ${name}`); }
8
+ else { fail++; console.log(` FAIL ${name} ${detail}`); }
9
+ }
10
+
11
+ // helpers -------------------------------------------------------------
12
+ const T0 = "2026-01-01T00:00:00Z";
13
+ function req({ cached, fresh, write = 0, out = 100, min = 0, provider = "p", model = "m", content = [] }) {
14
+ return { type: "message", timestamp: `2026-01-01T00:${String(min).padStart(2, "0")}:00Z`,
15
+ message: { role: "assistant", provider, model, stopReason: "stop", content, usage: { input: fresh, cacheRead: cached, cacheWrite: write, output: out } } };
16
+ }
17
+ function toolResult(text, tool = "bash") {
18
+ return { type: "message", message: { role: "toolResult", toolName: tool, content: [{ type: "text", text }] } };
19
+ }
20
+ const hitOf = (r) => r.cached / (r.fresh + r.cached + r.cacheWrite);
21
+
22
+ // A. healthy session: no streaks ---------------------------------------
23
+ {
24
+ const entries = Array.from({ length: 4 }, (_, i) => req({ cached: 99000, fresh: 1000, min: i }));
25
+ const d = parseEntries(entries);
26
+ const s = analyzeStreaks(d.requests);
27
+ check("A healthy 99% session -> no streaks", s.length === 0, `got ${s.length}`);
28
+ }
29
+
30
+ // B. classic streak ----------------------------------------------------
31
+ {
32
+ const entries = Array.from({ length: 5 }, (_, i) => req({ cached: 90000, fresh: 10000, min: i }));
33
+ const d = parseEntries(entries);
34
+ const s = analyzeStreaks(d.requests);
35
+ check("B 5x hit90/fresh10k -> 1 streak of 5", s.length === 1 && s[0].hits.length === 5, JSON.stringify(s.map(x => [x.from, x.to])));
36
+ check("B avgHit ~0.9", s[0] && Math.abs(s[0].avgHit - 0.9) < 0.01);
37
+ }
38
+
39
+ // C. tiny-prefix fake streak must be suppressed ------------------------
40
+ {
41
+ const entries = Array.from({ length: 4 }, (_, i) => req({ cached: 10000, fresh: 500, min: i })); // hit .952, fresh 500
42
+ const d = parseEntries(entries);
43
+ const s = analyzeStreaks(d.requests);
44
+ check("C 4x hit95 but fresh<800 -> no streak (fake suppressed)", s.length === 0, `got ${s.length}`);
45
+ }
46
+
47
+ // D. old blind spot: persistent 60-80% with big fresh must be caught ----
48
+ {
49
+ const entries = Array.from({ length: 4 }, (_, i) => req({ cached: 30000, fresh: 15000, min: i })); // hit .667
50
+ const d = parseEntries(entries);
51
+ const s = analyzeStreaks(d.requests);
52
+ check("D 4x hit67/fresh15k -> 1 streak (blind spot covered)", s.length === 1 && s[0].hits.length === 4);
53
+ }
54
+
55
+ // E. streak with a single healthy turn in the middle (gap tolerance) -------
56
+ {
57
+ const entries = [
58
+ req({ cached: 90000, fresh: 10000, min: 1 }),
59
+ req({ cached: 90000, fresh: 10000, min: 2 }),
60
+ req({ cached: 90000, fresh: 10000, min: 3 }),
61
+ req({ cached: 99000, fresh: 1000, min: 4 }), // single healthy outlier -> tolerated
62
+ req({ cached: 90000, fresh: 10000, min: 5 }),
63
+ req({ cached: 90000, fresh: 10000, min: 6 }),
64
+ ];
65
+ const s = analyzeStreaks(parseEntries(entries).requests);
66
+ check("E single healthy outlier tolerated -> one 5-member streak",
67
+ s.length === 1 && s[0].hits.length === 5, JSON.stringify(s.map(x => x.hits.length)));
68
+ }
69
+
70
+ // E2. two consecutive healthy turns DO end a streak ----------------------
71
+ {
72
+ const entries = [
73
+ req({ cached: 90000, fresh: 10000, min: 1 }),
74
+ req({ cached: 90000, fresh: 10000, min: 2 }),
75
+ req({ cached: 90000, fresh: 10000, min: 3 }),
76
+ req({ cached: 99000, fresh: 1000, min: 4 }),
77
+ req({ cached: 99000, fresh: 1000, min: 5 }), // 2nd healthy -> closes
78
+ req({ cached: 90000, fresh: 10000, min: 6 }),
79
+ req({ cached: 90000, fresh: 10000, min: 7 }),
80
+ ];
81
+ const s = analyzeStreaks(parseEntries(entries).requests);
82
+ check("E2 two healthy turns end streak -> two streaks (3 + 2 dropped)",
83
+ s.length === 1 && s[0].hits.length === 3, JSON.stringify(s.map(x => x.hits.length)));
84
+ }
85
+
86
+ // F. model switch breaks streak + miss attribution ---------------------
87
+ {
88
+ const entries = [
89
+ req({ cached: 90000, fresh: 10000, min: 1, model: "a" }),
90
+ req({ cached: 90000, fresh: 10000, min: 2, model: "a" }),
91
+ req({ cached: 90000, fresh: 10000, min: 3, model: "a" }),
92
+ { type: "model_change", timestamp: "2026-01-01T00:03:30Z", provider: "p", modelId: "b" },
93
+ req({ cached: 0, fresh: 50000, min: 4, model: "b" }), // full miss
94
+ req({ cached: 99000, fresh: 1000, min: 5, model: "b" }),
95
+ ];
96
+ const d = parseEntries(entries);
97
+ const s = analyzeStreaks(d.requests);
98
+ const m = analyzeMisses(d.requests, d.events);
99
+ check("F streak len 3 (broken by switch)", s.length === 1 && s[0].hits.length === 3);
100
+ const sw = m.misses.find((x) => x.number === 4);
101
+ check("F request4 miss attributed to model switch", sw && sw.causes.some(([c]) => c === "model switch"), JSON.stringify(m.misses.map((x) => [x.number, x.causes.map((y) => y[0])])));
102
+ check("F only 1 miss total (request4)", m.misses.length === 1, `got ${m.misses.length}`);
103
+ }
104
+
105
+ // G. band boundaries ----------------------------------------------------
106
+ {
107
+ const mk3 = (cached, fresh) => Array.from({ length: 3 }, (_, i) => req({ cached, fresh, min: i + 1 }));
108
+ check("G hit=0.50 (inclusive) -> streak", analyzeStreaks(parseEntries(mk3(5000, 5000)).requests).length === 1);
109
+ check("G hit=0.98 -> no streak (above band)", analyzeStreaks(parseEntries(mk3(98000, 2000)).requests).length === 0);
110
+ const exact = analyzeStreaks(parseEntries(mk3(39000, 1000)).requests).length; // 39000/40000 = 0.975 boundary
111
+ check("G hit=0.975 boundary excluded (high bound exclusive)", exact === 0, `got ${exact}`);
112
+ check("G fresh=800 exactly -> member (>= threshold)", analyzeStreaks(parseEntries(mk3(7200, 800)).requests).length === 1); // hit .9
113
+ check("G fresh=799 -> not member", analyzeStreaks(parseEntries(mk3(7190, 799)).requests).length === 0);
114
+ }
115
+
116
+ // H. TTL expiry attribution ---------------------------------------------
117
+ {
118
+ const entries = [
119
+ req({ cached: 50000, fresh: 2000, min: 1 }),
120
+ req({ cached: 5000, fresh: 15000, min: 40 }), // 39min gap same model, hit .25
121
+ ];
122
+ const m = analyzeMisses(parseEntries(entries).requests, parseEntries(entries).events);
123
+ check("H 39min gap + low hit -> TTL expiry", m.misses.length === 1 && m.misses[0].causes.some(([c]) => c === "TTL expiry"),
124
+ JSON.stringify(m.misses.map((x) => x.causes)));
125
+ }
126
+
127
+ // I. cache rebuild (prev turn was a miss, same model) -------------------
128
+ {
129
+ const entries = [
130
+ req({ cached: 0, fresh: 100000, min: 1 }),
131
+ req({ cached: 400, fresh: 140000, min: 2 }), // 1min later, prev was total miss
132
+ ];
133
+ const d = parseEntries(entries);
134
+ const m = analyzeMisses(d.requests, d.events);
135
+ const two = m.misses.find((x) => x.number === 2);
136
+ check("I request2 -> cache rebuild (prev lost cache)", two && two.causes.some(([c]) => c === "cache rebuild"), JSON.stringify(m.misses.map((x) => [x.number, x.causes.map((y) => y[0])])));
137
+ }
138
+
139
+ // J. trailing event clamp + tool trace ----------------------------------
140
+ {
141
+ const entries = [
142
+ req({ cached: 90000, fresh: 10000, min: 1, content: [{ type: "toolCall", tool: "bash", name: "bash" }] }),
143
+ toolResult("x".repeat(12000)),
144
+ req({ cached: 90000, fresh: 10000, min: 2 }),
145
+ toolResult("y".repeat(4000), "read"),
146
+ req({ cached: 90000, fresh: 10000, min: 3 }),
147
+ { type: "compaction", timestamp: "2026-01-01T00:59:00Z" }, // after last request
148
+ ];
149
+ const d = parseEntries(entries);
150
+ const lastEv = d.events[d.events.length - 1];
151
+ check("J trailing compaction clamped to last request", lastEv.kind === "compaction" && lastEv.requestNumber === 3, JSON.stringify(lastEv));
152
+ const s = analyzeStreaks(d.requests);
153
+ check("J streak tool trace has bash~3000 + read~1000 tokens",
154
+ s.length === 1 && s[0].tools.get("bash") === 3000 && s[0].tools.get("read") === 1000,
155
+ JSON.stringify(s[0] && [...s[0].tools.entries()]));
156
+ }
157
+
158
+ // L. interleaved churn (alternating bad/healthy) must not escape ----------
159
+ {
160
+ const entries = [];
161
+ for (let i = 0; i < 4; i++) {
162
+ entries.push(req({ cached: 30000, fresh: 15000, min: entries.length + 1 })); // hit .667
163
+ entries.push(req({ cached: 99000, fresh: 1000, min: entries.length + 1 })); // hit .99
164
+ }
165
+ const s = analyzeStreaks(parseEntries(entries).requests);
166
+ check("L alternating churn (4 bad turns interleaved) -> 1 streak with 4 members",
167
+ s.length === 1 && s[0].hits.length === 4, JSON.stringify(s.map(x => x.hits.length)));
168
+ }
169
+
170
+ // M. write-heavy rebuild (Anthropic-style) must not fake a mid-band streak --
171
+ {
172
+ const entries = Array.from({ length: 3 }, (_, i) =>
173
+ req({ cached: 5000, fresh: 2000, write: 50000, min: i + 1 })); // std hit 8.8%, write-excluded hit .714
174
+ const s = analyzeStreaks(parseEntries(entries).requests);
175
+ check("M write-heavy rebuild counts members by write-excluded hit",
176
+ s.length === 1 && s[0].hits.length === 3, JSON.stringify(s.map(x => x.hits.length)));
177
+ }
178
+
179
+ // N. tiny prefix (fresh>=800 but prompt<4000) must not streak ---------------
180
+ {
181
+ const entries = Array.from({ length: 4 }, (_, i) => req({ cached: 2000, fresh: 1000, min: i + 1 })); // hit .667, prompt 3k
182
+ const s = analyzeStreaks(parseEntries(entries).requests);
183
+ check("N tiny prefix (<4k) -> no streak even though fresh>=800", s.length === 0, `got ${s.length}`);
184
+ }
185
+
186
+ // K. degenerate inputs ---------------------------------------------------
187
+ {
188
+ check("K empty session -> no crash, no streaks", analyzeStreaks(parseEntries([]).requests).length === 0);
189
+ const single = parseEntries([req({ cached: 1000, fresh: 9000, min: 1 })]).requests;
190
+ check("K single low-hit request -> miss(first request), no streak", analyzeMisses(single, []).misses[0].causes.some(([c]) => c === "first request") && analyzeStreaks(single).length === 0);
191
+ const zero = parseEntries([{ type: "message", message: { role: "assistant", provider: "p", model: "m", usage: { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 } } }]).requests;
192
+ check("K all-zero usage entry skipped", zero.length === 0);
193
+ }
194
+
195
+ console.log(`\n${pass} passed, ${fail} failed`);
196
+ process.exit(fail ? 1 : 0);