pi-mega-compact 0.20.78 → 0.20.80

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 (107) hide show
  1. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
  2. package/dist/extensions/mega-config.js +2 -0
  3. package/dist/extensions/mega-events/analytics-handler.js +169 -0
  4. package/dist/extensions/mega-events/register.js +3 -0
  5. package/dist/src/queryExpansion.js +1 -0
  6. package/dist/src/store/analytics/connection.js +114 -0
  7. package/dist/src/store/analytics/contract-compliance.test/_helpers.js +39 -0
  8. package/dist/src/store/analytics/index.js +25 -0
  9. package/dist/src/store/analytics/memory-store.js +111 -0
  10. package/dist/src/store/analytics/schema.js +108 -0
  11. package/dist/src/store/analytics/sqlite-store/admin.js +84 -0
  12. package/dist/src/store/analytics/sqlite-store/ctx.js +1 -0
  13. package/dist/src/store/analytics/sqlite-store/read.js +25 -0
  14. package/dist/src/store/analytics/sqlite-store/rows.js +44 -0
  15. package/dist/src/store/analytics/sqlite-store/write.js +90 -0
  16. package/dist/src/store/analytics/sqlite-store.js +79 -0
  17. package/dist/src/store/analytics/types.js +17 -0
  18. package/dist/src/store/sqlite/utils.js +7 -0
  19. package/dist/src/store/turns/connection.js +1 -1
  20. package/dist/src/vector-cortex/ledger/sqlite.js +1 -0
  21. package/dist/vector-cortex/ledger/sqlite.js +1 -0
  22. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js → AdminTab-C-kTQx7a.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{AdminTab-BsA8-_bX.js.map → AdminTab-C-kTQx7a.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js → AreaChart-BmntvH86.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{AreaChart-BRHEfWhc.js.map → AreaChart-BmntvH86.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js → BarChart-D_AStVsb.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{BarChart-C-M0LaFc.js.map → BarChart-D_AStVsb.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js → CacheTab-VRFk7EPo.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{CacheTab-Cj3n59U3.js.map → CacheTab-VRFk7EPo.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js → ConfigTab-Ci5LOohz.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{ConfigTab-C016XTNC.js.map → ConfigTab-Ci5LOohz.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js → EventsTab-8Y7if14O.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{EventsTab-DE29fuqe.js.map → EventsTab-8Y7if14O.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{HealthTab-D4eVI8ar.js → HealthTab-Cw5eYIYq.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{HealthTab-D4eVI8ar.js.map → HealthTab-Cw5eYIYq.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js → MaintenanceTab-B0o3a57B.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-4fJ7M4S4.js.map → MaintenanceTab-B0o3a57B.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js → MemoryMapTab-JV8mLhsc.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CDbcK9e4.js.map → MemoryMapTab-JV8mLhsc.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js → MetricsCards-Bkcv7Nl8.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{MetricsCards-VUEcgGzo.js.map → MetricsCards-Bkcv7Nl8.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/MetricsTab-CAG4VgbM.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/{MetricsTab-CKqJ0Zl-.js.map → MetricsTab-CAG4VgbM.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/OverviewTab-gxdUR58S.js +16 -0
  45. package/extensions/dashboard-client/dist/assets/OverviewTab-gxdUR58S.js.map +1 -0
  46. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js → ReposTab-2hMRSS76.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{ReposTab-C4iveqrP.js.map → ReposTab-2hMRSS76.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js → SessionsTab-Pjx5TBtW.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{SessionsTab-DSEwkMQm.js.map → SessionsTab-Pjx5TBtW.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js → SetupTab-B6Y3eHXo.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{SetupTab-CCyWgy4O.js.map → SetupTab-B6Y3eHXo.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js → TimeSavedCard-BSo2lwSE.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CyD8e_e_.js.map → TimeSavedCard-BSo2lwSE.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js → TurnMemoryView-CdGCu8Af.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{TurnMemoryView-5tLbWkRv.js.map → TurnMemoryView-CdGCu8Af.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/TurnsTab-Cc4kAoAR.js +2 -0
  57. package/extensions/dashboard-client/dist/assets/{TurnsTab-C7pm92Hg.js.map → TurnsTab-Cc4kAoAR.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js → VcStatusBadge-DcLNLyy6.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{VcStatusBadge-ObhcIyfN.js.map → VcStatusBadge-DcLNLyy6.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js → VectorCortexTab-n-3v91td.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{VectorCortexTab-BtHZb7o7.js.map → VectorCortexTab-n-3v91td.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js → WikiTab-BpwWUleH.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{WikiTab-lQ4l3QIK.js.map → WikiTab-BpwWUleH.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js → button-B38fJDBG.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{button-DhQQAJGm.js.map → button-B38fJDBG.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js → card-COtt-Q5y.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{card-Cfhz9X31.js.map → card-COtt-Q5y.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js → client-extra-D659nPqu.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{client-extra-CKNOGEz_.js.map → client-extra-D659nPqu.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js → generateCategoricalChart-COFUvM7d.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-JKjK9_cB.js.map → generateCategoricalChart-COFUvM7d.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js → index-C3ZUU5vU.js} +3 -3
  73. package/extensions/dashboard-client/dist/assets/{index-iuxgtzEW.js.map → index-C3ZUU5vU.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js → switch-DvYCCZ0E.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{switch-7EHEQMSP.js.map → switch-DvYCCZ0E.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js → toggle-bJGW8eFJ.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{toggle-z1lWY4UM.js.map → toggle-bJGW8eFJ.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js → useSSE-D-OoWxa2.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{useSSE-sctOZuRM.js.map → useSSE-D-OoWxa2.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/index.html +1 -1
  81. package/extensions/dashboard-client/src/components/RepoContextStack.tsx +28 -21
  82. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
  83. package/extensions/mega-config-types.ts +3 -0
  84. package/extensions/mega-config.ts +2 -0
  85. package/extensions/mega-events/analytics-handler.ts +236 -0
  86. package/extensions/mega-events/register.ts +3 -0
  87. package/package.json +1 -1
  88. package/src/queryExpansion.ts +1 -0
  89. package/src/store/analytics/connection.ts +128 -0
  90. package/src/store/analytics/contract-compliance.test/_helpers.ts +47 -0
  91. package/src/store/analytics/index.ts +61 -0
  92. package/src/store/analytics/memory-store.ts +161 -0
  93. package/src/store/analytics/schema.ts +132 -0
  94. package/src/store/analytics/sqlite-store/admin.ts +96 -0
  95. package/src/store/analytics/sqlite-store/ctx.ts +9 -0
  96. package/src/store/analytics/sqlite-store/read.ts +46 -0
  97. package/src/store/analytics/sqlite-store/rows.ts +58 -0
  98. package/src/store/analytics/sqlite-store/write.ts +111 -0
  99. package/src/store/analytics/sqlite-store.ts +119 -0
  100. package/src/store/analytics/types.ts +202 -0
  101. package/src/store/sqlite/utils.ts +7 -0
  102. package/src/store/turns/connection.ts +1 -1
  103. package/src/vector-cortex/ledger/sqlite.ts +1 -0
  104. package/extensions/dashboard-client/dist/assets/MetricsTab-CKqJ0Zl-.js +0 -2
  105. package/extensions/dashboard-client/dist/assets/OverviewTab-9Kxo79mi.js +0 -16
  106. package/extensions/dashboard-client/dist/assets/OverviewTab-9Kxo79mi.js.map +0 -1
  107. package/extensions/dashboard-client/dist/assets/TurnsTab-C7pm92Hg.js +0 -2
@@ -0,0 +1,236 @@
1
+ /**
2
+ * analytics-handler.ts — PMA-2 verified ingestion adapter.
3
+ *
4
+ * Registers event handlers that capture request-lifecycle facts from verified
5
+ * pi event seams (proven in PMA-0) into the isolated analytics.db (built in
6
+ * PMA-1). Mirrors the perf-handler.ts pattern: closure over `runtime`, non-fatal
7
+ * try/catch on every handler, flag-gated.
8
+ *
9
+ * Event-to-fact mapping (all seams verified in docs/specs/pma-0-discovery-findings.md):
10
+ * turn_start → request_started (observedAt, provider/model)
11
+ * before_provider_request → provider_selected (same correlationId)
12
+ * message_update[text_start] → TTFT stamp (attached to terminal fact)
13
+ * turn_end → request_completed/request_failed (tokens, latency, ttft, status)
14
+ *
15
+ * Everything is best-effort: AppendResult carries accepted/duplicate/failed
16
+ * without throwing into the host. When the flag is OFF, no handlers are
17
+ * registered and no DB is opened.
18
+ *
19
+ * PREVENT-PI-004: no network. PREVENT-PI-003: no system-role messages.
20
+ */
21
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
+ import { type MegaRuntime } from "../mega-runtime.js";
23
+ import { type MegaConfig } from "../mega-config.js";
24
+ import {
25
+ createAnalyticsStore,
26
+ closeAllAnalyticsDbs,
27
+ type AnalyticsStore,
28
+ type RequestEventFact,
29
+ type RequestEventKind,
30
+ type QualityNote,
31
+ } from "../../src/store/analytics/index.js";
32
+
33
+ // ── Per-stateDir store cache (mirrors mega-turn-store.ts storeFor) ─────
34
+
35
+ const stores = new Map<string, AnalyticsStore>();
36
+
37
+ function storeFor(stateDir: string): AnalyticsStore {
38
+ let s = stores.get(stateDir);
39
+ if (!s) {
40
+ s = createAnalyticsStore({ stateDir });
41
+ stores.set(stateDir, s);
42
+ }
43
+ return s;
44
+ }
45
+
46
+ /** Close all cached analytics stores (test teardown / shutdown). */
47
+ export function closeAnalyticsStores(): void {
48
+ closeAllAnalyticsDbs();
49
+ stores.clear();
50
+ }
51
+
52
+ // ── Per-runtime PMA-2 timing state ────────────────────────────────────
53
+ // Kept off MegaRuntime (a module-private WeakMap) so runtime.ts stays a
54
+ // pure field-declaration shell under the soft-limit headroom gate. The
55
+ // adapter is the sole reader/writer of this transient per-turn state.
56
+
57
+ interface PmaTiming {
58
+ providerStart: number;
59
+ ttft: number;
60
+ correlationId: string | null;
61
+ }
62
+
63
+ const pmaTimings = new WeakMap<object, PmaTiming>();
64
+
65
+ function getPma(rt: object): PmaTiming {
66
+ let s = pmaTimings.get(rt);
67
+ if (!s) {
68
+ s = { providerStart: 0, ttft: 0, correlationId: null };
69
+ pmaTimings.set(rt, s);
70
+ }
71
+ return s;
72
+ }
73
+
74
+ /** @internal Test-only accessor — production code uses getPma() in-module. */
75
+ export function __pmaTimingForTest(rt: object): PmaTiming {
76
+ return getPma(rt);
77
+ }
78
+
79
+ // ── Usage block narrowing (same as perf-handler) ──────────────────────
80
+
81
+ interface UsageBlock {
82
+ input: number;
83
+ output: number;
84
+ cacheRead: number;
85
+ cacheWrite: number;
86
+ }
87
+
88
+ function usageOf(msg: { role?: string; usage?: UsageBlock }): UsageBlock | null {
89
+ if (msg.role === "assistant" && msg.usage) return msg.usage;
90
+ return null;
91
+ }
92
+
93
+ // ── Helpers ───────────────────────────────────────────────────────────
94
+
95
+ /** Build a QualityNote with provider/model availability. */
96
+ function qualityFor(provider: string | undefined, model: string | undefined): QualityNote {
97
+ if (!provider || !model) return { unavailable: true, note: "provider/model not captured" };
98
+ return {};
99
+ }
100
+
101
+ // ── Registration ──────────────────────────────────────────────────────
102
+
103
+ /** Register the analytics event handlers. Flag-gated: OFF = complete no-op. */
104
+ export function registerAnalyticsHandler(
105
+ pi: ExtensionAPI,
106
+ runtime: MegaRuntime,
107
+ config: MegaConfig,
108
+ ): void {
109
+ if (!config.providerModelAnalytics) return; // flag OFF — no DB, no handlers
110
+
111
+ // ── turn_start: stamp the request start + emit request_started ──────
112
+
113
+ pi.on("turn_start", async (event) => {
114
+ try {
115
+ const now = Date.now();
116
+ const corrId = `req_${runtime.rt.sessionId}_${event.turnIndex}`;
117
+ const pma = getPma(runtime);
118
+ pma.correlationId = corrId;
119
+ pma.providerStart = 0; // reset; stamped at before_provider_request
120
+ pma.ttft = 0;
121
+
122
+ const provider = runtime.currentModel?.provider;
123
+ const model = runtime.currentModel?.modelId;
124
+ const fact: RequestEventFact = {
125
+ id: `${corrId}_started`,
126
+ correlationId: corrId,
127
+ sessionId: runtime.rt.sessionId,
128
+ turnId: String(event.turnIndex),
129
+ eventKind: "request_started",
130
+ observedAt: now,
131
+ provider,
132
+ model,
133
+ source: "host_adapter",
134
+ quality: qualityFor(provider, model),
135
+ };
136
+ storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
137
+ } catch {
138
+ /* non-fatal: analytics never breaks the agent loop */
139
+ }
140
+ });
141
+
142
+ // ── before_provider_request: stamp provider start + emit provider_selected
143
+
144
+ pi.on("before_provider_request", async () => {
145
+ try {
146
+ const pma = getPma(runtime);
147
+ pma.providerStart = Date.now();
148
+ const corrId = pma.correlationId;
149
+ if (!corrId) return;
150
+ const provider = runtime.currentModel?.provider;
151
+ const model = runtime.currentModel?.modelId;
152
+ const fact: RequestEventFact = {
153
+ id: `${corrId}_selected`,
154
+ correlationId: corrId,
155
+ sessionId: runtime.rt.sessionId,
156
+ eventKind: "provider_selected",
157
+ observedAt: pma.providerStart,
158
+ provider,
159
+ model,
160
+ source: "host_adapter",
161
+ quality: qualityFor(provider, model),
162
+ };
163
+ storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
164
+ } catch {
165
+ /* non-fatal */
166
+ }
167
+ });
168
+
169
+ // ── message_update: capture TTFT on first text token ────────────────
170
+ // Net-new seam — no existing handler registers message_update.
171
+ // Proven available in PMA-0 (agent-loop.js:201-227).
172
+
173
+ pi.on("message_update", async (event) => {
174
+ try {
175
+ const pma = getPma(runtime);
176
+ if (pma.ttft > 0) return; // already captured this turn
177
+ if (pma.providerStart <= 0) return; // no provider start stamp
178
+ const chunkType = (event as { assistantMessageEvent?: { type?: string } })
179
+ .assistantMessageEvent?.type;
180
+ if (chunkType !== "text_start" && chunkType !== "text_delta") return;
181
+ pma.ttft = Date.now() - pma.providerStart;
182
+ } catch {
183
+ /* non-fatal */
184
+ }
185
+ });
186
+
187
+ // ── turn_end: emit terminal fact with tokens, latency, ttft, status ─
188
+
189
+ pi.on("turn_end", async (event) => {
190
+ try {
191
+ const pma = getPma(runtime);
192
+ const corrId = pma.correlationId;
193
+ if (!corrId) return;
194
+
195
+ const now = Date.now();
196
+ const durationMs = runtime.perfTurnStart > 0 ? now - runtime.perfTurnStart : undefined;
197
+ const u = usageOf(event.message as { role?: string; usage?: UsageBlock });
198
+ const stopReason = (event.message as { stopReason?: string }).stopReason;
199
+ const provider = runtime.currentModel?.provider;
200
+ const model = runtime.currentModel?.modelId;
201
+
202
+ const isFailed = stopReason === "error" || stopReason === "aborted";
203
+ const eventKind: RequestEventKind = isFailed ? "request_failed" : "request_completed";
204
+
205
+ const fact: RequestEventFact = {
206
+ id: `${corrId}_${eventKind}`,
207
+ correlationId: corrId,
208
+ sessionId: runtime.rt.sessionId,
209
+ turnId: String(event.turnIndex),
210
+ eventKind,
211
+ observedAt: now,
212
+ provider,
213
+ model,
214
+ status: stopReason ?? undefined,
215
+ inputTokens: u?.input,
216
+ outputTokens: u?.output,
217
+ cacheReadTokens: u?.cacheRead,
218
+ cacheWriteTokens: u?.cacheWrite,
219
+ durationMs,
220
+ ttftMs: pma.ttft > 0 ? pma.ttft : undefined,
221
+ source: "host_adapter",
222
+ quality: {
223
+ ...qualityFor(provider, model),
224
+ ...(pma.ttft > 0 ? {} : { note: "TTFT not captured (no first-token event)" }),
225
+ },
226
+ };
227
+ storeFor(runtime.currentStateDir).asWriter().appendRequestEvent(fact);
228
+
229
+ // Reset per-turn state.
230
+ pma.correlationId = null;
231
+ pma.ttft = 0;
232
+ } catch {
233
+ /* non-fatal */
234
+ }
235
+ });
236
+ }
@@ -13,6 +13,7 @@ import { registerAgentHandlers } from "./agent-handlers.js";
13
13
  import { registerContextHandler } from "./context-handler.js";
14
14
  import { registerCompactHandlers } from "./compact-handlers.js";
15
15
  import { registerPerfHandler } from "./perf-handler.js";
16
+ import { registerAnalyticsHandler } from "./analytics-handler.js";
16
17
  import { registerVectorCortexRender } from "../mega-context/vector-cortex.js";
17
18
 
18
19
  /**
@@ -49,4 +50,6 @@ export function registerEventHandlers(
49
50
  registerContextHandler(pi, runtime, config);
50
51
  registerCompactHandlers(pi, runtime, config);
51
52
  registerPerfHandler(pi, runtime);
53
+ // PMA-2: analytics ingestion — flag-gated (OFF = no-op, no DB opened).
54
+ registerAnalyticsHandler(pi, runtime, config);
52
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.78",
3
+ "version": "0.20.80",
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",
@@ -200,6 +200,7 @@ function loadDb(stateDir: string): { db: import("node:sqlite").DatabaseSync } {
200
200
  const { DatabaseSync } = require("node:sqlite") as typeof import("node:sqlite");
201
201
  const { join } = require("node:path") as typeof import("node:path");
202
202
  const db = new DatabaseSync(join(stateDir, "mega-compact.db"));
203
+ db.exec("PRAGMA busy_timeout = 5000"); // tolerate concurrent-opener WAL contention (see store/sqlite/utils.ts)
203
204
  db.exec("PRAGMA journal_mode=WAL");
204
205
  _dbCache.set(stateDir, { db });
205
206
  return { db };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * connection.ts — PMA-1 connection manager for analytics.db.
3
+ *
4
+ * Own module-level connection cache, deliberately SEPARATE from the main
5
+ * memory store (openStore) and the turns store (openTurnDb) so an analytics-DB
6
+ * failure can never touch the authoritative memory DB or the turns DB (PMA §3.1).
7
+ *
8
+ * Mirrors src/store/turns/connection.ts. PREVENT-PI-004: node:sqlite in-process,
9
+ * no network. PREVENT-002: parameterized path binding.
10
+ */
11
+ import { existsSync, mkdirSync, statSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import type { DatabaseSync } from "node:sqlite";
14
+ import { DatabaseSync as Db } from "node:sqlite";
15
+ import { initAnalyticsSchema } from "./schema.js";
16
+
17
+ export const ANALYTICS_DB_FILE = "analytics.db";
18
+
19
+ /** Resolve the analytics DB path, honoring the env override. */
20
+ export function analyticsDbPath(stateDir: string): string {
21
+ const env = process.env.MEGACOMPACT_ANALYTICS_DB_PATH;
22
+ if (env && env.trim() !== "") return env.trim();
23
+ return join(stateDir, ANALYTICS_DB_FILE);
24
+ }
25
+
26
+ // Module-private cache — separate from openStore's cache and turns' cache.
27
+ const cache = new Map<string, DatabaseSync>();
28
+
29
+ /** File size helper (0 for in-memory). */
30
+ function dbFileSize(dbPath: string): number {
31
+ try {
32
+ return statSync(dbPath).size;
33
+ } catch {
34
+ return 0;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Open (or reuse from cache) the analytics.db for `stateDir`.
40
+ * Sets PRAGMAs, initializes the schema, and caches the connection.
41
+ * In-memory mode (`:memory:`) is supported for tests.
42
+ */
43
+ export function openAnalyticsDb(
44
+ stateDir: string,
45
+ options?: { dbPath?: string; inMemory?: boolean },
46
+ ): DatabaseSync {
47
+ const inMemory = options?.inMemory === true;
48
+ const resolvedPath = inMemory ? ":memory:" : (options?.dbPath ?? analyticsDbPath(stateDir));
49
+ const cacheKey = inMemory ? `:memory:${stateDir}` : resolvedPath;
50
+
51
+ const cached = cache.get(cacheKey);
52
+ if (cached) {
53
+ try {
54
+ cached.exec("SELECT 1");
55
+ return cached;
56
+ } catch {
57
+ // Stale handle — evict and re-open.
58
+ cache.delete(cacheKey);
59
+ }
60
+ }
61
+
62
+ // Ensure the state dir exists for file-backed mode.
63
+ if (!inMemory && !existsSync(stateDir)) {
64
+ mkdirSync(stateDir, { recursive: true });
65
+ }
66
+
67
+ const db = inMemory ? new Db(":memory:") : new Db(resolvedPath);
68
+ db.exec("PRAGMA busy_timeout = 5000");
69
+ db.exec("PRAGMA journal_mode = WAL");
70
+ db.exec("PRAGMA foreign_keys = ON");
71
+
72
+ initAnalyticsSchema(db);
73
+ cache.set(cacheKey, db);
74
+ return db;
75
+ }
76
+
77
+ /** Close and evict the analytics DB for `stateDir` (idempotent). */
78
+ export function closeAnalyticsDb(
79
+ stateDir: string,
80
+ options?: { dbPath?: string; inMemory?: boolean },
81
+ ): void {
82
+ const inMemory = options?.inMemory === true;
83
+ const resolvedPath = inMemory ? ":memory:" : (options?.dbPath ?? analyticsDbPath(stateDir));
84
+ const cacheKey = inMemory ? `:memory:${stateDir}` : resolvedPath;
85
+
86
+ const db = cache.get(cacheKey);
87
+ if (!db) return;
88
+ try {
89
+ db.close();
90
+ } catch {
91
+ /* already closed */
92
+ }
93
+ cache.delete(cacheKey);
94
+ }
95
+
96
+ /** Close ALL cached analytics DBs (test teardown / graceful shutdown). */
97
+ export function closeAllAnalyticsDbs(): void {
98
+ for (const db of cache.values()) {
99
+ try {
100
+ db.close();
101
+ } catch {
102
+ /* already closed */
103
+ }
104
+ }
105
+ cache.clear();
106
+ }
107
+
108
+ /** Report the file size of analytics.db for a state dir (0 if absent/in-memory). */
109
+ export function analyticsDbSize(stateDir: string): number {
110
+ return dbFileSize(analyticsDbPath(stateDir));
111
+ }
112
+
113
+ /**
114
+ * SAVEPOINT-based transaction wrapper (nests safely under an outer transaction).
115
+ * Uses a distinct savepoint name from the turns store (`turns_tx`) and the main
116
+ * store (`mc_tx`) to avoid collisions.
117
+ */
118
+ export function withTx(db: DatabaseSync, fn: () => void): void {
119
+ db.exec("SAVEPOINT analytics_tx");
120
+ try {
121
+ fn();
122
+ db.exec("RELEASE analytics_tx");
123
+ } catch (e) {
124
+ db.exec("ROLLBACK TO analytics_tx");
125
+ db.exec("RELEASE analytics_tx");
126
+ throw e;
127
+ }
128
+ }
@@ -0,0 +1,47 @@
1
+ /** Helpers for the analytics compliance suite. */
2
+ import type { AnalyticsStoreOptions, AnalyticsStore, RequestEventFact, MeasurementFact, IdentityObservation } from "../types.js";
3
+
4
+ export type StoreFactory = (options: AnalyticsStoreOptions) => AnalyticsStore;
5
+
6
+ let counter = 0;
7
+
8
+ /** Create a valid RequestEventFact with sensible defaults + overrides. */
9
+ export function makeRequestEvent(overrides: Partial<RequestEventFact> = {}): RequestEventFact {
10
+ return {
11
+ id: `evt_${++counter}`,
12
+ correlationId: "corr_test",
13
+ sessionId: "sess_test",
14
+ eventKind: "request_started",
15
+ observedAt: Date.now(),
16
+ provider: "anthropic",
17
+ model: "claude-sonnet-4",
18
+ source: "test",
19
+ quality: {},
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ /** Create a valid MeasurementFact. */
25
+ export function makeMeasurement(overrides: Partial<MeasurementFact> = {}): MeasurementFact {
26
+ return {
27
+ observedAt: Date.now(),
28
+ sampleKind: "cache_hit_pct",
29
+ value: 72.5,
30
+ unit: "percent",
31
+ source: "test",
32
+ quality: {},
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ /** Create a valid IdentityObservation. */
38
+ export function makeIdentity(overrides: Partial<IdentityObservation> = {}): IdentityObservation {
39
+ return {
40
+ observedAt: Date.now(),
41
+ provider: "anthropic",
42
+ model: "claude-sonnet-4",
43
+ source: "test",
44
+ metadata: { contextWindow: 200000 },
45
+ ...overrides,
46
+ };
47
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * index.ts — Barrel for src/store/analytics/ (PMA-1).
3
+ *
4
+ * Hosts import types + the factory. SQL schemas + internals stay private.
5
+ *
6
+ * PREVENT-PI-004: no network. PREVENT-002: parameterized SQL only.
7
+ */
8
+
9
+ export type {
10
+ RequestEventKind,
11
+ FactSource,
12
+ QualityNote,
13
+ RequestEventFact,
14
+ MeasurementFact,
15
+ IdentityObservation,
16
+ AppendResult,
17
+ AnalyticsStatus,
18
+ PruneReport,
19
+ IntegrityReport,
20
+ MaintenanceReport,
21
+ BackupReport,
22
+ RetentionPolicy,
23
+ AnalyticsReader,
24
+ AnalyticsWriter,
25
+ AnalyticsAdmin,
26
+ AnalyticsStore,
27
+ AnalyticsStoreOptions,
28
+ AnalyticsStoreFactory,
29
+ } from "./types.js";
30
+
31
+ export { SqliteAnalyticsStore } from "./sqlite-store.js";
32
+ export { InMemoryAnalyticsStore } from "./memory-store.js";
33
+
34
+ export {
35
+ openAnalyticsDb,
36
+ closeAnalyticsDb,
37
+ closeAllAnalyticsDbs,
38
+ analyticsDbPath,
39
+ analyticsDbSize,
40
+ ANALYTICS_DB_FILE,
41
+ withTx,
42
+ } from "./connection.js";
43
+
44
+ export { initAnalyticsSchema, getAnalyticsSchemaVersion } from "./schema.js";
45
+
46
+ import { SqliteAnalyticsStore } from "./sqlite-store.js";
47
+ import { InMemoryAnalyticsStore } from "./memory-store.js";
48
+ import type { AnalyticsStore, AnalyticsStoreOptions } from "./types.js";
49
+
50
+ /**
51
+ * Factory: create an AnalyticsStore from options.
52
+ *
53
+ * - Default: SqliteAnalyticsStore backed by analytics.db in stateDir
54
+ * - inMemory: InMemoryAnalyticsStore (no file I/O)
55
+ */
56
+ export function createAnalyticsStore(options: AnalyticsStoreOptions): AnalyticsStore {
57
+ if (options.inMemory) {
58
+ return new InMemoryAnalyticsStore(options);
59
+ }
60
+ return new SqliteAnalyticsStore(options);
61
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * memory-store.ts — InMemoryAnalyticsStore: Map-backed dual backend.
3
+ *
4
+ * Mirrors the turns store pattern (src/store/turns/memory-store.ts).
5
+ * Satisfies the same contract so the compliance suite runs against both
6
+ * backends unchanged.
7
+ */
8
+ import type {
9
+ AnalyticsStore,
10
+ AnalyticsReader,
11
+ AnalyticsWriter,
12
+ AnalyticsAdmin,
13
+ AnalyticsStoreOptions,
14
+ RequestEventFact,
15
+ MeasurementFact,
16
+ IdentityObservation,
17
+ AppendResult,
18
+ AnalyticsStatus,
19
+ RetentionPolicy,
20
+ PruneReport,
21
+ IntegrityReport,
22
+ MaintenanceReport,
23
+ BackupReport,
24
+ } from "./types.js";
25
+ import { getAnalyticsSchemaVersion } from "./schema.js";
26
+
27
+ interface MeasurementRow extends MeasurementFact {
28
+ id: number;
29
+ }
30
+ interface IdentityRow extends IdentityObservation {
31
+ id: number;
32
+ }
33
+
34
+ export class InMemoryAnalyticsStore implements AnalyticsStore {
35
+ private events: Map<string, RequestEventFact> = new Map();
36
+ private measurements: MeasurementRow[] = [];
37
+ private identities: IdentityRow[] = [];
38
+ private nextId = 1;
39
+
40
+ constructor(_options?: AnalyticsStoreOptions) {
41
+ // Options unused — the in-memory store doesn't open a DB.
42
+ }
43
+
44
+ private allocId(): number {
45
+ return this.nextId++;
46
+ }
47
+
48
+ // ── AnalyticsReader ───────────────────────────────────────────────
49
+
50
+ status(): AnalyticsStatus {
51
+ const lastEvent = this.events.size > 0
52
+ ? Math.max(...[...this.events.values()].map((e) => e.observedAt))
53
+ : 0;
54
+ const lastMeas = this.measurements.length > 0
55
+ ? Math.max(...this.measurements.map((m) => m.observedAt))
56
+ : 0;
57
+ const lastId = this.identities.length > 0
58
+ ? Math.max(...this.identities.map((i) => i.observedAt))
59
+ : 0;
60
+ return {
61
+ enabled: true,
62
+ schemaVersion: getAnalyticsSchemaVersion(),
63
+ requestEventCount: this.events.size,
64
+ measurementCount: this.measurements.length,
65
+ identityCount: this.identities.length,
66
+ freshThrough: Math.max(lastEvent, lastMeas, lastId) || null,
67
+ };
68
+ }
69
+
70
+ // ── AnalyticsWriter ───────────────────────────────────────────────
71
+
72
+ appendRequestEvent(fact: RequestEventFact): AppendResult {
73
+ if (this.events.has(fact.id)) {
74
+ return { status: "duplicate", id: fact.id };
75
+ }
76
+ this.events.set(fact.id, { ...fact });
77
+ return { status: "accepted", id: fact.id };
78
+ }
79
+
80
+ appendMeasurement(sample: MeasurementFact): AppendResult {
81
+ const id = this.allocId();
82
+ this.measurements.push({ ...sample, id });
83
+ return { status: "accepted", id };
84
+ }
85
+
86
+ appendIdentity(observation: IdentityObservation): AppendResult {
87
+ const id = this.allocId();
88
+ this.identities.push({ ...observation, id });
89
+ return { status: "accepted", id };
90
+ }
91
+
92
+ // ── AnalyticsAdmin ────────────────────────────────────────────────
93
+
94
+ prune(policy: RetentionPolicy): PruneReport {
95
+ const now = Date.now();
96
+ const eventCutoff = now - policy.maxEventAgeMs;
97
+ const measCutoff = now - policy.maxMeasurementAgeMs;
98
+ const beforeE = this.events.size;
99
+ const beforeM = this.measurements.length;
100
+ for (const [k, e] of this.events) {
101
+ if (e.observedAt < eventCutoff) this.events.delete(k);
102
+ }
103
+ this.measurements = this.measurements.filter((m) => m.observedAt >= measCutoff);
104
+ return {
105
+ eventsRemoved: beforeE - this.events.size,
106
+ measurementsRemoved: beforeM - this.measurements.length,
107
+ identitiesRemoved: 0,
108
+ freedBytes: 0,
109
+ };
110
+ }
111
+
112
+ vacuum(): MaintenanceReport {
113
+ return { ok: true, detail: "no-op for in-memory store" };
114
+ }
115
+
116
+ integrityCheck(): IntegrityReport {
117
+ return { ok: true, detail: "ok" };
118
+ }
119
+
120
+ backup(): BackupReport {
121
+ return { ok: false, detail: "Cannot backup an in-memory database" };
122
+ }
123
+
124
+ clear(): void {
125
+ this.events.clear();
126
+ this.measurements = [];
127
+ this.identities = [];
128
+ this.nextId = 1;
129
+ }
130
+
131
+ close(): void {
132
+ this.clear();
133
+ }
134
+
135
+ // ── Capability views ──────────────────────────────────────────────
136
+
137
+ asReader(): AnalyticsReader {
138
+ return {
139
+ status: () => this.status(),
140
+ };
141
+ }
142
+
143
+ asWriter(): AnalyticsWriter {
144
+ return {
145
+ appendRequestEvent: (f) => this.appendRequestEvent(f),
146
+ appendMeasurement: (s) => this.appendMeasurement(s),
147
+ appendIdentity: (o) => this.appendIdentity(o),
148
+ };
149
+ }
150
+
151
+ asAdmin(): AnalyticsAdmin {
152
+ return {
153
+ prune: (p) => this.prune(p),
154
+ vacuum: () => this.vacuum(),
155
+ integrityCheck: () => this.integrityCheck(),
156
+ backup: () => this.backup(),
157
+ clear: () => this.clear(),
158
+ close: () => this.close(),
159
+ };
160
+ }
161
+ }