pi-mega-compact 0.11.2 → 0.11.4

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.
Files changed (112) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +9 -1
  2. package/dist/extensions/dashboard-server/api-contracts/provider-cache.js +7 -0
  3. package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
  4. package/dist/extensions/dashboard-server/index-reader.js +22 -0
  5. package/dist/extensions/dashboard-server/routes-cache.js +80 -0
  6. package/dist/extensions/dashboard-server/routes-cache.test.js +248 -0
  7. package/dist/extensions/dashboard-server/routes-maintenance.js +7 -7
  8. package/dist/extensions/dashboard-server/routes-repo.js +24 -0
  9. package/dist/extensions/dashboard-server/routes.js +1 -0
  10. package/dist/extensions/dashboard-server/server.js +3 -1
  11. package/dist/extensions/mega-cache-replay.test.js +233 -24
  12. package/dist/extensions/mega-compact-s38.test.js +778 -20
  13. package/dist/extensions/mega-config.js +4 -0
  14. package/dist/extensions/mega-events/agent-handlers.js +44 -8
  15. package/dist/extensions/mega-events/context-handler.js +37 -19
  16. package/dist/extensions/mega-events/error-classifier.js +125 -135
  17. package/dist/extensions/mega-events/outage-advisor.js +53 -0
  18. package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
  19. package/dist/extensions/mega-runtime/runtime-snapshot.js +14 -4
  20. package/dist/extensions/mega-runtime/runtime.js +1 -0
  21. package/dist/extensions/mega-runtime/snapshot.js +1 -1
  22. package/dist/extensions/mega-runtime/snapshot.test.js +127 -0
  23. package/dist/extensions/mega-runtime/widget.js +4 -4
  24. package/dist/extensions/mega-runtime/widget.test.js +103 -12
  25. package/dist/src/pricing.js +78 -0
  26. package/dist/src/store/sqlite/perf-samples.js +104 -0
  27. package/dist/src/store/sqlite/perf-samples.test.js +93 -1
  28. package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js → AchievementTiles-vEo2MtE2.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{AchievementTiles-Cml6YC8w.js.map → AchievementTiles-vEo2MtE2.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/AchievementsTab-DjxfYrDc.js +2 -0
  31. package/extensions/dashboard-client/dist/assets/{AchievementsTab-B8NqDt7I.js.map → AchievementsTab-DjxfYrDc.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js +2 -0
  33. package/extensions/dashboard-client/dist/assets/CacheTab-Dnpv94wq.js.map +1 -0
  34. package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js → ConfigTab-Cm6ymbmx.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{ConfigTab-CBYvp2mV.js.map → ConfigTab-Cm6ymbmx.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js → EventsTab-DXWl_YzI.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{EventsTab-DlTJqiDm.js.map → EventsTab-DXWl_YzI.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js → GameTab-BLDudcO5.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{GameTab-DkGTqKJW.js.map → GameTab-BLDudcO5.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js +2 -0
  41. package/extensions/dashboard-client/dist/assets/MaintenanceTab-DLkKCztn.js.map +1 -0
  42. package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js → MetricsTab-Cq4u-rZF.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{MetricsTab-BFo7pKM-.js.map → MetricsTab-Cq4u-rZF.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js → OverviewTab-D4kGvsiQ.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{OverviewTab-CClCUd3-.js.map → OverviewTab-D4kGvsiQ.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js +2 -0
  47. package/extensions/dashboard-client/dist/assets/ReposTab-C0Wnrf2u.js.map +1 -0
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js → SessionsTab-DZC2-GCn.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{SessionsTab-CCYd-kSz.js.map → SessionsTab-DZC2-GCn.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js +2 -0
  51. package/extensions/dashboard-client/dist/assets/TimeSavedCard-UqljSyP-.js.map +1 -0
  52. package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js → TopicsTab-01G71hv1.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{TopicsTab-DRLJmVv0.js.map → TopicsTab-01G71hv1.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js → TurnsTab-DLRg0LZT.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{TurnsTab-CLNM0T5R.js.map → TurnsTab-DLRg0LZT.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +1 -0
  57. package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js → index-BX0bcgLu.js} +4 -4
  58. package/extensions/dashboard-client/dist/assets/{index-BcoSz-L4.js.map → index-BX0bcgLu.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js → useSSE-BBYt5UPB.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{useSSE-Cezlih5V.js.map → useSSE-BBYt5UPB.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/index.html +2 -2
  62. package/extensions/dashboard-client/src/api/client.ts +6 -0
  63. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +13 -1
  64. package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +141 -0
  65. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +31 -3
  66. package/extensions/dashboard-client/src/main.tsx +1 -0
  67. package/extensions/dashboard-client/src/styles/maintenance.css +267 -0
  68. package/extensions/dashboard-client/src/styles/overview-extra.css +9 -0
  69. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +83 -29
  70. package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +301 -136
  71. package/extensions/dashboard-client/src/utils/format.ts +42 -7
  72. package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -8
  73. package/extensions/dashboard-server/api-contracts/index.ts +4 -0
  74. package/extensions/dashboard-server/api-contracts/multi-repo.ts +6 -0
  75. package/extensions/dashboard-server/api-contracts/provider-cache.ts +30 -0
  76. package/extensions/dashboard-server/api-contracts.test.ts +3 -2
  77. package/extensions/dashboard-server/index-reader.ts +23 -0
  78. package/extensions/dashboard-server/routes-cache.test.ts +302 -0
  79. package/extensions/dashboard-server/routes-cache.ts +123 -0
  80. package/extensions/dashboard-server/routes-maintenance.ts +18 -11
  81. package/extensions/dashboard-server/routes-repo.ts +31 -4
  82. package/extensions/dashboard-server/routes.ts +1 -0
  83. package/extensions/dashboard-server/server.ts +2 -0
  84. package/extensions/mega-cache-replay.test.ts +481 -166
  85. package/extensions/mega-compact-s38.test.ts +785 -20
  86. package/extensions/mega-config.ts +14 -0
  87. package/extensions/mega-events/agent-handlers.ts +48 -7
  88. package/extensions/mega-events/context-handler.ts +59 -24
  89. package/extensions/mega-events/error-classifier.ts +167 -129
  90. package/extensions/mega-events/outage-advisor.ts +73 -0
  91. package/extensions/mega-runtime/helpers.ts +2 -0
  92. package/extensions/mega-runtime/reset-runtime.ts +1 -0
  93. package/extensions/mega-runtime/runtime-snapshot.ts +31 -9
  94. package/extensions/mega-runtime/runtime.ts +1 -0
  95. package/extensions/mega-runtime/snapshot.test.ts +139 -0
  96. package/extensions/mega-runtime/snapshot.ts +6 -4
  97. package/extensions/mega-runtime/widget.test.ts +290 -105
  98. package/extensions/mega-runtime/widget.ts +29 -6
  99. package/package.json +1 -1
  100. package/src/pricing.ts +95 -0
  101. package/src/store/sqlite/perf-samples.test.ts +107 -3
  102. package/src/store/sqlite/perf-samples.ts +142 -6
  103. package/extensions/dashboard-client/dist/assets/AchievementsTab-B8NqDt7I.js +0 -2
  104. package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js +0 -2
  105. package/extensions/dashboard-client/dist/assets/CacheTab-BCLzVL4S.js.map +0 -1
  106. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js +0 -2
  107. package/extensions/dashboard-client/dist/assets/MaintenanceTab-Cw43zmUx.js.map +0 -1
  108. package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js +0 -2
  109. package/extensions/dashboard-client/dist/assets/ReposTab-_3R4RSLZ.js.map +0 -1
  110. package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js +0 -2
  111. package/extensions/dashboard-client/dist/assets/TimeSavedCard-0TqsrY6y.js.map +0 -1
  112. package/extensions/dashboard-client/dist/assets/index-gn66lYD2.css +0 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * snapshot.test.ts — C.4 computeMegaSnapshot tests for providerCachePct
3
+ * and megaCacheFlare fields.
4
+ *
5
+ * Uses MEGACOMPACT_STATE_DIR + mkdtemp (G7). No pi runtime.
6
+ */
7
+ import { describe, it, before, after } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { mkdtempSync, rmSync } from "node:fs";
12
+ import { computeMegaSnapshot, type SnapshotInput } from "./snapshot.js";
13
+
14
+ function baseInput(overrides: Partial<SnapshotInput> = {}): SnapshotInput {
15
+ return {
16
+ rtTokensSaved: 1000,
17
+ lastCtxPercent: null,
18
+ lastCtxTokens: 0,
19
+ lastCtxWindow: 0,
20
+ activeAgents: 0,
21
+ currentTurn: 0,
22
+ statusKey: undefined,
23
+ ready: false,
24
+ armed: false,
25
+ st: {
26
+ totalTokenEstimate: 800,
27
+ originalTokens: 1200,
28
+ storageDedupRate: 0.12,
29
+ checkpointCount: 3,
30
+ lastCheckpointId: undefined,
31
+ lastSummary: undefined,
32
+ injectedCount: 0,
33
+ dedupHitRate: 0.05,
34
+ tokensSaved: 200,
35
+ dedupAttempts: 40,
36
+ dedupCollapsed: 5,
37
+ },
38
+ repo: {
39
+ tokensSaved: 5000,
40
+ totalTokenEstimate: 4000,
41
+ checkpointCount: 9,
42
+ sessionCount: 2,
43
+ originalTokens: 6000,
44
+ dedupAttempts: 100,
45
+ dedupCollapsed: 30,
46
+ storageDedupRate: 0.3,
47
+ },
48
+ pressureBand: "low",
49
+ configTier: "default",
50
+ modelSnap: undefined,
51
+ lastCompactAt: null,
52
+ embedderName: () => "Trigram",
53
+ driftStatus: () => "ok" as const,
54
+ getCachedGameState: () => ({
55
+ game_mode_on: false,
56
+ theme: "transparent",
57
+ tui_display_mode: "full",
58
+ }),
59
+ getTurnLevel: () => 1,
60
+ providerCachePct: 56.2,
61
+ megaCacheFlare: false,
62
+ megaCacheFlarePct: 0,
63
+ levelUpFlare: false,
64
+ achievementFlare: false,
65
+ achievementFlareTitles: [],
66
+ activeEffect: null,
67
+ lastActivityAt: Date.now(),
68
+ ticker: [],
69
+ lastWhy: undefined,
70
+ tierTrace: undefined,
71
+ pulsing: false,
72
+ ...overrides,
73
+ };
74
+ }
75
+
76
+ describe("computeMegaSnapshot (C.4)", () => {
77
+ let dir: string;
78
+ before(() => {
79
+ dir = mkdtempSync(join(tmpdir(), "mc-snap-"));
80
+ process.env.MEGACOMPACT_STATE_DIR = dir;
81
+ });
82
+ after(() => {
83
+ delete process.env.MEGACOMPACT_STATE_DIR;
84
+ rmSync(dir, { recursive: true, force: true });
85
+ });
86
+
87
+ // ── C.1: providerCachePct flows into cachePct ─────────────────────────
88
+ it("cachePct equals providerCachePct (not dedup hit rate)", () => {
89
+ const res = computeMegaSnapshot(baseInput({ providerCachePct: 56.2 }));
90
+ assert.equal(res.widgetData.cachePct, 56.2, "cachePct is providerCachePct");
91
+ });
92
+
93
+ it("cachePct is 0 when providerCachePct is 0", () => {
94
+ const res = computeMegaSnapshot(baseInput({ providerCachePct: 0 }));
95
+ assert.equal(res.widgetData.cachePct, 0, "cachePct is 0");
96
+ });
97
+
98
+ it("cachePct is independent of dedup hit rate", () => {
99
+ const res = computeMegaSnapshot(
100
+ baseInput({
101
+ providerCachePct: 12.5,
102
+ st: {
103
+ totalTokenEstimate: 99999,
104
+ originalTokens: 99999,
105
+ storageDedupRate: 0.99,
106
+ checkpointCount: 1,
107
+ lastCheckpointId: undefined,
108
+ lastSummary: undefined,
109
+ injectedCount: 0,
110
+ dedupHitRate: 0.05,
111
+ tokensSaved: 200,
112
+ dedupAttempts: 40,
113
+ dedupCollapsed: 5,
114
+ },
115
+ }),
116
+ );
117
+ assert.equal(res.widgetData.cachePct, 12.5, "cachePct ignores dedup");
118
+ });
119
+
120
+ // ── megaCacheFlare fields pass through ────────────────────────────────
121
+ it("megaCacheFlare + megaCacheFlarePct pass through to widgetData", () => {
122
+ const res = computeMegaSnapshot(
123
+ baseInput({ megaCacheFlare: true, megaCacheFlarePct: 287 }),
124
+ );
125
+ assert.equal(res.widgetData.megaCacheFlare, true);
126
+ assert.equal(res.widgetData.megaCacheFlarePct, 287);
127
+ });
128
+
129
+ it("megaCacheFlare is false by default", () => {
130
+ const res = computeMegaSnapshot(baseInput());
131
+ assert.equal(res.widgetData.megaCacheFlare, false);
132
+ });
133
+
134
+ // ── level pass-through ────────────────────────────────────────────────
135
+ it("level flows from getTurnLevel", () => {
136
+ const res = computeMegaSnapshot(baseInput({ getTurnLevel: () => 42 }));
137
+ assert.equal(res.curLevel, 42);
138
+ });
139
+ });
@@ -81,6 +81,9 @@ export interface SnapshotInput {
81
81
  // Model snapshot (pre-fetched)
82
82
  modelSnap: ModelSnapshot | undefined;
83
83
 
84
+ // Provider prompt cache hit rate (lifetime avg, required — callers compute once).
85
+ providerCachePct: number;
86
+
84
87
  // Game-mode state (callers that need real-time values pass lambdas)
85
88
  getCachedGameState: () => GameState;
86
89
  getTurnLevel: () => number;
@@ -150,8 +153,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
150
153
  const ctxPct = p.lastCtxPercent != null ? p.lastCtxPercent / 100 : 0;
151
154
 
152
155
  // ── model + provider (S26 capture) for the header ─────────────────────
153
- const modelName =
154
- p.modelSnap?.modelName ?? p.modelSnap?.modelId ?? "?";
156
+ const modelName = p.modelSnap?.modelName ?? p.modelSnap?.modelId ?? "?";
155
157
  const modelStr = p.modelSnap?.provider
156
158
  ? `${modelName}·${p.modelSnap.provider}`
157
159
  : modelName;
@@ -177,7 +179,7 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
177
179
  // ── S31: game-mode state ──────────────────────────────────────────────
178
180
  const gs = p.getCachedGameState();
179
181
  const curLevel = p.getTurnLevel();
180
- const cachePct = st.dedupHitRate * 100;
182
+ const cachePct = p.providerCachePct;
181
183
 
182
184
  const widgetData: WidgetData = {
183
185
  version: ownVersion(),
@@ -227,4 +229,4 @@ export function computeMegaSnapshot(p: SnapshotInput): SnapshotResult {
227
229
  };
228
230
 
229
231
  return { widgetData, curLevel };
230
- }
232
+ }
@@ -99,6 +99,8 @@ describe("buildWidgetLines (S31)", () => {
99
99
  level: 1,
100
100
  cachePct,
101
101
  megaCacheFlare: flare,
102
+ // C.3: flare gate reads megaCacheFlarePct, not cachePct
103
+ ...(flare ? { megaCacheFlarePct: cachePct } : {}),
102
104
  });
103
105
  const lines = buildWidgetLines(wd, WIDTH, 0);
104
106
  const body = contentLines(lines);
@@ -161,123 +163,306 @@ describe("buildWidgetLines (S31)", () => {
161
163
  }
162
164
  });
163
165
 
164
-
165
166
  describe("buildWidgetLines ambient border effect (v0.8.3)", () => {
166
- const effBase = (overrides: Partial<WidgetData> = {}): WidgetData => baseWd({
167
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: true, level: 1, cachePct: 42, ...overrides,
168
- });
169
- const isBorder = (l: string): boolean => l.includes("");
167
+ const effBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
168
+ baseWd({
169
+ theme: DEFAULT_THEME,
170
+ tuiMode: "full",
171
+ gameMode: true,
172
+ level: 1,
173
+ cachePct: 42,
174
+ ...overrides,
175
+ });
176
+ const isBorder = (l: string): boolean => l.includes("─");
177
+
178
+ it("activeEffect (pulse, mid-window) -> border lines carry a 256-color fg SGR", () => {
179
+ const ae = {
180
+ type: "pulse" as const,
181
+ role: "accent" as const,
182
+ startedAt: Date.now() - 250,
183
+ durationMs: 2000,
184
+ };
185
+ const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
186
+ const borders = lines.filter(isBorder);
187
+ assert.ok(borders.length >= 2, "has top + bottom borders");
188
+ for (const b of borders) {
189
+ assert.ok(
190
+ b.includes("\x1b[38;5;"),
191
+ `border carries 256-color fg: ${JSON.stringify(b)}`,
192
+ );
193
+ }
194
+ });
195
+
196
+ it("activeEffect null -> plain borders, no 38;5 fg SGR on border lines", () => {
197
+ const lines = buildWidgetLines(effBase({ activeEffect: null }), WIDTH, 0);
198
+ const borders = lines.filter(isBorder);
199
+ for (const b of borders) {
200
+ assert.ok(
201
+ !b.includes("\x1b[38;5;"),
202
+ `no effect SGR on plain border: ${JSON.stringify(b)}`,
203
+ );
204
+ }
205
+ });
206
+
207
+ it("expired activeEffect -> plain borders (per-frame expiry enforced)", () => {
208
+ const ae = {
209
+ type: "pulse" as const,
210
+ role: "accent" as const,
211
+ startedAt: Date.now() - 5000,
212
+ durationMs: 1000,
213
+ };
214
+ const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
215
+ const borders = lines.filter(isBorder);
216
+ for (const b of borders) {
217
+ assert.ok(
218
+ !b.includes("\x1b[38;5;"),
219
+ `expired effect -> plain border: ${JSON.stringify(b)}`,
220
+ );
221
+ }
222
+ });
223
+
224
+ it("activeEffect border lines are width-safe (pulse, minimal + full)", () => {
225
+ for (const tuiMode of ["minimal", "full"] as const) {
226
+ const ae = {
227
+ type: "pulse" as const,
228
+ role: "mega" as const,
229
+ startedAt: Date.now() - 100,
230
+ durationMs: 2000,
231
+ };
232
+ const lines = buildWidgetLines(
233
+ effBase({ activeEffect: ae, tuiMode }),
234
+ 60,
235
+ 0,
236
+ );
237
+ for (const l of lines)
238
+ assert.ok(
239
+ visibleWidth(l) <= 60,
240
+ `width safe (${tuiMode}): ${visibleWidth(l)}`,
241
+ );
242
+ }
243
+ });
170
244
 
171
- it("activeEffect (pulse, mid-window) -> border lines carry a 256-color fg SGR", () => {
172
- const ae = { type: "pulse" as const, role: "accent" as const, startedAt: Date.now() - 250, durationMs: 2000 };
173
- const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
174
- const borders = lines.filter(isBorder);
175
- assert.ok(borders.length >= 2, "has top + bottom borders");
176
- for (const b of borders) {
177
- assert.ok(b.includes("\x1b[38;5;"), `border carries 256-color fg: ${JSON.stringify(b)}`);
178
- }
179
- });
245
+ it("flash effect mid-window border carries the full base index SGR", () => {
246
+ // Force an 'on' phase of the 120ms alternate by starting just now.
247
+ const ae = {
248
+ type: "flash" as const,
249
+ role: "red" as const,
250
+ startedAt: Date.now(),
251
+ durationMs: 1200,
252
+ };
253
+ const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
254
+ const borders = lines.filter(isBorder);
255
+ assert.ok(
256
+ borders.some((b) => b.includes("\x1b[38;5;203m")),
257
+ `flash-on phase uses red base 203`,
258
+ );
259
+ });
260
+ });
180
261
 
181
- it("activeEffect null -> plain borders, no 38;5 fg SGR on border lines", () => {
182
- const lines = buildWidgetLines(effBase({ activeEffect: null }), WIDTH, 0);
183
- const borders = lines.filter(isBorder);
184
- for (const b of borders) {
185
- assert.ok(!b.includes("\x1b[38;5;"), `no effect SGR on plain border: ${JSON.stringify(b)}`);
186
- }
187
- });
262
+ describe("buildWidgetLines C.3 flare gate regression", () => {
263
+ const flareBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
264
+ baseWd({
265
+ theme: DEFAULT_THEME,
266
+ tuiMode: "full",
267
+ gameMode: true,
268
+ level: 1,
269
+ cachePct: 42,
270
+ ...overrides,
271
+ });
188
272
 
189
- it("expired activeEffect -> plain borders (per-frame expiry enforced)", () => {
190
- const ae = { type: "pulse" as const, role: "accent" as const, startedAt: Date.now() - 5000, durationMs: 1000 };
191
- const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
192
- const borders = lines.filter(isBorder);
193
- for (const b of borders) {
194
- assert.ok(!b.includes("\x1b[38;5;"), `expired effect -> plain border: ${JSON.stringify(b)}`);
195
- }
196
- });
273
+ it("flare fires when megaCacheFlarePct >= 100 even when cachePct < 100", () => {
274
+ // Regression: the old code read (wd.cachePct ?? 0) >= 100, so when
275
+ // cachePct was 56 (provider hit rate) the flare silently broke.
276
+ const lines = buildWidgetLines(
277
+ flareBase({
278
+ cachePct: 56,
279
+ megaCacheFlare: true,
280
+ megaCacheFlarePct: 287,
281
+ }),
282
+ WIDTH,
283
+ 0,
284
+ );
285
+ assert.ok(
286
+ lines.join("\n").includes("MEGA CACHE"),
287
+ "flare fires from megaCacheFlarePct",
288
+ );
289
+ });
197
290
 
198
- it("activeEffect border lines are width-safe (pulse, minimal + full)", () => {
199
- for (const tuiMode of ["minimal", "full"] as const) {
200
- const ae = { type: "pulse" as const, role: "mega" as const, startedAt: Date.now() - 100, durationMs: 2000 };
201
- const lines = buildWidgetLines(effBase({ activeEffect: ae, tuiMode }), 60, 0);
202
- for (const l of lines) assert.ok(visibleWidth(l) <= 60, `width safe (${tuiMode}): ${visibleWidth(l)}`);
203
- }
204
- });
291
+ it("flare suppressed when megaCacheFlarePct < 100 even when cachePct >= 100", () => {
292
+ // Symmetric regression: high cachePct should NOT alone trigger flare.
293
+ const lines = buildWidgetLines(
294
+ flareBase({
295
+ cachePct: 150,
296
+ megaCacheFlare: true,
297
+ megaCacheFlarePct: 42,
298
+ }),
299
+ WIDTH,
300
+ 0,
301
+ );
302
+ assert.ok(
303
+ !lines.join("\n").includes("MEGA CACHE"),
304
+ "no flare when megaCacheFlarePct below threshold",
305
+ );
306
+ });
205
307
 
206
- it("flash effect mid-window border carries the full base index SGR", () => {
207
- // Force an 'on' phase of the 120ms alternate by starting just now.
208
- const ae = { type: "flash" as const, role: "red" as const, startedAt: Date.now(), durationMs: 1200 };
209
- const lines = buildWidgetLines(effBase({ activeEffect: ae }), WIDTH, 0);
210
- const borders = lines.filter(isBorder);
211
- assert.ok(borders.some((b) => b.includes("\x1b[38;5;203m")), `flash-on phase uses red base 203`);
212
- });
308
+ it("flare suppressed when megaCacheFlarePct is missing (undefined)", () => {
309
+ const lines = buildWidgetLines(
310
+ flareBase({
311
+ cachePct: 150,
312
+ megaCacheFlare: true,
313
+ }),
314
+ WIDTH,
315
+ 0,
316
+ );
317
+ assert.ok(
318
+ !lines.join("\n").includes("MEGA CACHE"),
319
+ "no flare without megaCacheFlarePct",
320
+ );
321
+ });
213
322
  });
214
323
 
215
324
  describe("buildWidgetLines footer stability (P1 — no 250ms rotation)", () => {
216
- // P1: the L5 ticker branch used to re-pick the head text every 250ms via
217
- // `step = floor(Date.now()/250)`, flipping the footer line on a 250·N ms
218
- // cycle. After the fix it pins to the most-recent entry, so two renders of
219
- // the SAME WidgetData at t and t+500ms (well past one rotation slot) must
220
- // produce byte-identical footer lines. Proves the metronome is gone.
221
- const stabBase = (overrides: Partial<WidgetData> = {}): WidgetData => baseWd({
222
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: false, level: 1, cachePct: 42, ...overrides,
223
- });
325
+ // P1: the L5 ticker branch used to re-pick the head text every 250ms via
326
+ // `step = floor(Date.now()/250)`, flipping the footer line on a 250·N ms
327
+ // cycle. After the fix it pins to the most-recent entry, so two renders of
328
+ // the SAME WidgetData at t and t+500ms (well past one rotation slot) must
329
+ // produce byte-identical footer lines. Proves the metronome is gone.
330
+ const stabBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
331
+ baseWd({
332
+ theme: DEFAULT_THEME,
333
+ tuiMode: "full",
334
+ gameMode: false,
335
+ level: 1,
336
+ cachePct: 42,
337
+ ...overrides,
338
+ });
224
339
 
225
- it("ticker footer is byte-identical across simulated frames 500ms apart", () => {
226
- const wd = stabBase({
227
- ticker: [
228
- { text: "first", at: 1000 },
229
- { text: "second", at: 2000 },
230
- { text: "third", at: 3000 },
231
- ],
232
- lastWhy: "because",
233
- });
234
- const realNow = Date.now;
235
- try {
236
- Date.now = () => 1_000_000; // t
237
- const a = buildWidgetLines(wd, WIDTH, 0);
238
- Date.now = () => 1_000_500; // t+500ms (would have rotated twice under the old code)
239
- const b = buildWidgetLines(wd, WIDTH, 0);
240
- assert.deepEqual(a, b, "footer byte-identical across 500ms with no state change");
241
- // And the pinned head is the most-recent entry (third), not a rotation:
242
- assert.ok(a.some((l) => l.includes("third")), "shows most-recent ticker entry");
243
- assert.ok(a.some((l) => l.includes("(+2 more)")), "(+N more) suffix preserved");
244
- assert.ok(a.some((l) => l.includes("because")), "lastWhy preserved");
245
- } finally {
246
- Date.now = realNow;
247
- }
248
- });
340
+ it("ticker footer is byte-identical across simulated frames 500ms apart", () => {
341
+ const wd = stabBase({
342
+ ticker: [
343
+ { text: "first", at: 1000 },
344
+ { text: "second", at: 2000 },
345
+ { text: "third", at: 3000 },
346
+ ],
347
+ lastWhy: "because",
348
+ });
349
+ const realNow = Date.now;
350
+ try {
351
+ Date.now = () => 1_000_000; // t
352
+ const a = buildWidgetLines(wd, WIDTH, 0);
353
+ Date.now = () => 1_000_500; // t+500ms (would have rotated twice under the old code)
354
+ const b = buildWidgetLines(wd, WIDTH, 0);
355
+ assert.deepEqual(
356
+ a,
357
+ b,
358
+ "footer byte-identical across 500ms with no state change",
359
+ );
360
+ // And the pinned head is the most-recent entry (third), not a rotation:
361
+ assert.ok(
362
+ a.some((l) => l.includes("third")),
363
+ "shows most-recent ticker entry",
364
+ );
365
+ assert.ok(
366
+ a.some((l) => l.includes("(+2 more)")),
367
+ "(+N more) suffix preserved",
368
+ );
369
+ assert.ok(
370
+ a.some((l) => l.includes("because")),
371
+ "lastWhy preserved",
372
+ );
373
+ } finally {
374
+ Date.now = realNow;
375
+ }
376
+ });
249
377
 
250
- it("footer still flips when the ticker actually changes (new pushTicker)", () => {
251
- const realNow = Date.now;
252
- try {
253
- Date.now = () => 1_000_000;
254
- const before = buildWidgetLines(stabBase({ ticker: [{ text: "a", at: 1 }] }), WIDTH, 0);
255
- const after = buildWidgetLines(stabBase({ ticker: [{ text: "a", at: 1 }, { text: "b", at: 2 }] }), WIDTH, 0);
256
- assert.notDeepEqual(before, after, "footer changes when ticker grows");
257
- } finally {
258
- Date.now = realNow;
259
- }
260
- });
378
+ it("footer still flips when the ticker actually changes (new pushTicker)", () => {
379
+ const realNow = Date.now;
380
+ try {
381
+ Date.now = () => 1_000_000;
382
+ const before = buildWidgetLines(
383
+ stabBase({ ticker: [{ text: "a", at: 1 }] }),
384
+ WIDTH,
385
+ 0,
386
+ );
387
+ const after = buildWidgetLines(
388
+ stabBase({
389
+ ticker: [
390
+ { text: "a", at: 1 },
391
+ { text: "b", at: 2 },
392
+ ],
393
+ }),
394
+ WIDTH,
395
+ 0,
396
+ );
397
+ assert.notDeepEqual(before, after, "footer changes when ticker grows");
398
+ } finally {
399
+ Date.now = realNow;
400
+ }
401
+ });
261
402
  });
262
403
 
263
404
  describe("buildWidgetLines achievement flare (S35)", () => {
264
- const achBase = (overrides: Partial<WidgetData> = {}): WidgetData => baseWd({
265
- theme: DEFAULT_THEME, tuiMode: "full", gameMode: true, level: 1, cachePct: 42, ...overrides,
266
- });
267
- it("achievementFlare + titles -> renders the unlock toast line", () => {
268
- const lines = buildWidgetLines(achBase({ achievementFlare: true, achievementFlareTitles: ["First Compact"] }), WIDTH, 0);
269
- assert.ok(lines.some((l) => l.includes("Achievement unlocked: First Compact")), "toast line present");
270
- });
271
- it("achievementFlare off -> no toast line", () => {
272
- const lines = buildWidgetLines(achBase({ achievementFlare: false, achievementFlareTitles: ["First Compact"] }), WIDTH, 0);
273
- assert.ok(!lines.some((l) => l.includes("Achievement unlocked")), "no toast when flare off");
274
- });
275
- it("gameMode off -> no toast even if flare set", () => {
276
- const lines = buildWidgetLines(achBase({ gameMode: false, achievementFlare: true, achievementFlareTitles: ["X"] }), WIDTH, 0);
277
- assert.ok(!lines.some((l) => l.includes("Achievement unlocked")), "no toast when game off");
278
- });
279
- it("achievement toast is width-safe", () => {
280
- const lines = buildWidgetLines(achBase({ achievementFlare: true, achievementFlareTitles: ["First Compact", "Turn Veteran"] }), 60, 0);
281
- for (const l of lines) assert.ok(visibleWidth(l) <= 60, "width safe");
282
- });
405
+ const achBase = (overrides: Partial<WidgetData> = {}): WidgetData =>
406
+ baseWd({
407
+ theme: DEFAULT_THEME,
408
+ tuiMode: "full",
409
+ gameMode: true,
410
+ level: 1,
411
+ cachePct: 42,
412
+ ...overrides,
413
+ });
414
+ it("achievementFlare + titles -> renders the unlock toast line", () => {
415
+ const lines = buildWidgetLines(
416
+ achBase({
417
+ achievementFlare: true,
418
+ achievementFlareTitles: ["First Compact"],
419
+ }),
420
+ WIDTH,
421
+ 0,
422
+ );
423
+ assert.ok(
424
+ lines.some((l) => l.includes("Achievement unlocked: First Compact")),
425
+ "toast line present",
426
+ );
427
+ });
428
+ it("achievementFlare off -> no toast line", () => {
429
+ const lines = buildWidgetLines(
430
+ achBase({
431
+ achievementFlare: false,
432
+ achievementFlareTitles: ["First Compact"],
433
+ }),
434
+ WIDTH,
435
+ 0,
436
+ );
437
+ assert.ok(
438
+ !lines.some((l) => l.includes("Achievement unlocked")),
439
+ "no toast when flare off",
440
+ );
441
+ });
442
+ it("gameMode off -> no toast even if flare set", () => {
443
+ const lines = buildWidgetLines(
444
+ achBase({
445
+ gameMode: false,
446
+ achievementFlare: true,
447
+ achievementFlareTitles: ["X"],
448
+ }),
449
+ WIDTH,
450
+ 0,
451
+ );
452
+ assert.ok(
453
+ !lines.some((l) => l.includes("Achievement unlocked")),
454
+ "no toast when game off",
455
+ );
456
+ });
457
+ it("achievement toast is width-safe", () => {
458
+ const lines = buildWidgetLines(
459
+ achBase({
460
+ achievementFlare: true,
461
+ achievementFlareTitles: ["First Compact", "Turn Veteran"],
462
+ }),
463
+ 60,
464
+ 0,
465
+ );
466
+ for (const l of lines) assert.ok(visibleWidth(l) <= 60, "width safe");
467
+ });
283
468
  });
@@ -33,7 +33,22 @@ export {
33
33
  } from "./widget-ansi.js";
34
34
  export type { TickerEntry, WidgetData } from "./widget-types.js";
35
35
 
36
- import { C, PULSE, DEFAULT_PANEL_BG, panelBgFor, themeAnsi, sgrReset, wrapLine, panelLine, panelBar, effectBorderSgr, effectBar, fmtTokens, ramp, sinceCompactStr } from "./widget-ansi.js";
36
+ import {
37
+ C,
38
+ PULSE,
39
+ DEFAULT_PANEL_BG,
40
+ panelBgFor,
41
+ themeAnsi,
42
+ sgrReset,
43
+ wrapLine,
44
+ panelLine,
45
+ panelBar,
46
+ effectBorderSgr,
47
+ effectBar,
48
+ fmtTokens,
49
+ ramp,
50
+ sinceCompactStr,
51
+ } from "./widget-ansi.js";
37
52
  import type { WidgetData } from "./widget-types.js";
38
53
 
39
54
  // ── buildWidgetLines ───────────────────────────────────────────────────────
@@ -70,13 +85,13 @@ export function buildWidgetLines(
70
85
  // width guard + theme bg apply identically to the full panel. Level is shown
71
86
  // only when game mode is on (otherwise just the cache %).
72
87
  if (wd.tuiMode === "minimal") {
73
- const lvl = wd.gameMode ? wd.level ?? 1 : undefined;
88
+ const lvl = wd.gameMode ? (wd.level ?? 1) : undefined;
74
89
  const cachePct = wd.cachePct ?? 0;
75
90
  const cacheStr = `${Math.round(cachePct * 10) / 10}%`;
76
91
  const accent = themeAnsi(wd.theme, "accent");
77
92
  const mega = themeAnsi(wd.theme, "mega");
78
93
  const megaFlare =
79
- wd.gameMode && wd.megaCacheFlare && cachePct >= 100
94
+ wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
80
95
  ? ` ${mega}MEGA CACHE${sgrReset(mega)}`
81
96
  : "";
82
97
  const body =
@@ -100,7 +115,7 @@ export function buildWidgetLines(
100
115
  ? `${themeAnsi(wd.theme, "accent")}${wd.gameMode && wd.levelUpFlare ? "\x1b[5m" : ""}LVL ${wd.level ?? 1}${wd.gameMode && wd.levelUpFlare ? "\x1b[0m" : ""}${sgrReset(themeAnsi(wd.theme, "accent"))} `
101
116
  : "";
102
117
  const megaFlareSuffix =
103
- wd.gameMode && wd.megaCacheFlare && (wd.cachePct ?? 0) >= 100
118
+ wd.gameMode && wd.megaCacheFlare && (wd.megaCacheFlarePct ?? 0) >= 100
104
119
  ? `${sep}${themeAnsi(wd.theme, "mega")}MEGA CACHE! (oops, you cached so hard the dedup caught fire)${sgrReset(themeAnsi(wd.theme, "mega"))}`
105
120
  : "";
106
121
  // Build one long content line — let terminal wrap it naturally
@@ -157,13 +172,21 @@ export function buildWidgetLines(
157
172
  ),
158
173
  );
159
174
  } else if (wd.pulsing) {
160
- lines.push(panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width, panelBg));
175
+ lines.push(
176
+ panelLine(` ${pulse}${C.teal}compacting…${C.reset}`, width, panelBg),
177
+ );
161
178
  }
162
179
  // S35: achievement-unlock toast (one-line, accent) -- fires for one render cycle.
163
180
  if (wd.gameMode && wd.achievementFlare && wd.achievementFlareTitles?.length) {
164
181
  const accentSgr = themeAnsi(wd.theme, "accent");
165
182
  const titlesStr = wd.achievementFlareTitles.join(", ");
166
- lines.push(panelLine(` ${accentSgr}🏆 Achievement unlocked: ${titlesStr}${sgrReset(accentSgr)}`, width, panelBg));
183
+ lines.push(
184
+ panelLine(
185
+ ` ${accentSgr}🏆 Achievement unlocked: ${titlesStr}${sgrReset(accentSgr)}`,
186
+ width,
187
+ panelBg,
188
+ ),
189
+ );
167
190
  }
168
191
  // bottom border
169
192
  lines.push(effectBar(panelBar(width, "─", panelBg), borderSgr));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",