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,132 @@
1
+ /**
2
+ * schema.ts — PMA-1 DDL for analytics.db.
3
+ *
4
+ * Idempotent (CREATE IF NOT EXISTS). One statement per exec so a failure names
5
+ * the exact table. Matches the schema defined in
6
+ * docs/specs/provider-model-analytics-program.md §6.
7
+ *
8
+ * PREVENT-PI-004: no network. PREVENT-002: no interpolated identifiers.
9
+ */
10
+ import type { DatabaseSync } from "node:sqlite";
11
+
12
+ const SCHEMA_VERSION = 1;
13
+
14
+ /**
15
+ * Initialize the analytics.db schema. Safe to call on every open — every
16
+ * statement is CREATE IF NOT EXISTS.
17
+ */
18
+ export function initAnalyticsSchema(db: DatabaseSync): void {
19
+ // ── Lifecycle tables ──────────────────────────────────────────────
20
+
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS analytics_meta (
23
+ key TEXT PRIMARY KEY,
24
+ value TEXT NOT NULL
25
+ )
26
+ `);
27
+
28
+ db.exec(`
29
+ CREATE TABLE IF NOT EXISTS analytics_schema (
30
+ version INTEGER PRIMARY KEY,
31
+ applied_at INTEGER NOT NULL,
32
+ digest TEXT NOT NULL
33
+ )
34
+ `);
35
+
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS analytics_migrations (
38
+ id TEXT PRIMARY KEY,
39
+ started_at INTEGER NOT NULL,
40
+ completed_at INTEGER,
41
+ source_fingerprint TEXT,
42
+ rows_copied INTEGER,
43
+ status TEXT NOT NULL,
44
+ detail TEXT
45
+ )
46
+ `);
47
+
48
+ // ── Append-only fact tables ───────────────────────────────────────
49
+
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS request_events (
52
+ id TEXT PRIMARY KEY,
53
+ correlation_id TEXT,
54
+ session_id TEXT,
55
+ repo_id TEXT,
56
+ turn_id TEXT,
57
+ event_kind TEXT NOT NULL CHECK(event_kind IN (
58
+ 'request_started',
59
+ 'provider_selected',
60
+ 'first_token',
61
+ 'request_completed',
62
+ 'request_failed'
63
+ )),
64
+ observed_at INTEGER NOT NULL,
65
+ provider TEXT,
66
+ model TEXT,
67
+ status TEXT,
68
+ input_tokens INTEGER,
69
+ output_tokens INTEGER,
70
+ cache_read_tokens INTEGER,
71
+ cache_write_tokens INTEGER,
72
+ duration_ms REAL,
73
+ ttft_ms REAL,
74
+ source TEXT NOT NULL,
75
+ quality_json TEXT NOT NULL
76
+ )
77
+ `);
78
+
79
+ db.exec(`
80
+ CREATE TABLE IF NOT EXISTS measurement_samples (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ observed_at INTEGER NOT NULL,
83
+ sample_kind TEXT NOT NULL,
84
+ provider TEXT,
85
+ model TEXT,
86
+ value REAL NOT NULL,
87
+ unit TEXT NOT NULL,
88
+ correlation_id TEXT,
89
+ source TEXT NOT NULL,
90
+ quality_json TEXT NOT NULL
91
+ )
92
+ `);
93
+
94
+ db.exec(`
95
+ CREATE TABLE IF NOT EXISTS identity_observations (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ observed_at INTEGER NOT NULL,
98
+ provider TEXT,
99
+ model TEXT,
100
+ source TEXT NOT NULL,
101
+ metadata_json TEXT NOT NULL
102
+ )
103
+ `);
104
+
105
+ // ── Indexes (spec §6) ─────────────────────────────────────────────
106
+
107
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_observed_at ON request_events(observed_at)`);
108
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_provider_observed ON request_events(provider, observed_at)`);
109
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_model_observed ON request_events(model, observed_at)`);
110
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_kind_observed ON request_events(event_kind, observed_at)`);
111
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_request_events_correlation ON request_events(correlation_id, observed_at)`);
112
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_measurement_samples_kind_observed ON measurement_samples(sample_kind, observed_at)`);
113
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_measurement_samples_provider_model ON measurement_samples(provider, model, observed_at)`);
114
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_identity_obs_provider_model ON identity_observations(provider, model, observed_at)`);
115
+
116
+ // ── Schema version stamp (insert-only, never overwrites) ─────────
117
+
118
+ const existing = db
119
+ .prepare("SELECT value FROM analytics_meta WHERE key = 'schema_version'")
120
+ .get() as { value: string } | undefined;
121
+ if (!existing) {
122
+ db.prepare("INSERT INTO analytics_meta (key, value) VALUES ('schema_version', ?)")
123
+ .run(String(SCHEMA_VERSION));
124
+ db.prepare("INSERT INTO analytics_schema (version, applied_at, digest) VALUES (?, ?, ?)")
125
+ .run(SCHEMA_VERSION, Date.now(), "pma-1-initial");
126
+ }
127
+ }
128
+
129
+ /** The current analytics schema version. */
130
+ export function getAnalyticsSchemaVersion(): number {
131
+ return SCHEMA_VERSION;
132
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * admin.ts — AnalyticsAdmin delegate bodies for the SQLite backend.
3
+ * Destructive/maintenance operations: prune, vacuum, integrityCheck, backup, clear, close.
4
+ */
5
+ import { copyFileSync, existsSync } from "node:fs";
6
+ import type {
7
+ RetentionPolicy,
8
+ PruneReport,
9
+ IntegrityReport,
10
+ MaintenanceReport,
11
+ BackupReport,
12
+ } from "../types.js";
13
+ import type { AnalyticsStoreCtx } from "./ctx.js";
14
+ import { withTx } from "../connection.js";
15
+ import { analyticsDbPath } from "../connection.js";
16
+
17
+ export function prune(ctx: AnalyticsStoreCtx, policy: RetentionPolicy): PruneReport {
18
+ const now = Date.now();
19
+ const eventCutoff = now - policy.maxEventAgeMs;
20
+ const measCutoff = now - policy.maxMeasurementAgeMs;
21
+
22
+ const delEvents = ctx.db
23
+ .prepare("DELETE FROM request_events WHERE observed_at < ?")
24
+ .run(eventCutoff) as { changes?: number };
25
+ const delMeas = ctx.db
26
+ .prepare("DELETE FROM measurement_samples WHERE observed_at < ?")
27
+ .run(measCutoff) as { changes?: number };
28
+ // Identity observations are not age-pruned (they're low-volume snapshots).
29
+
30
+ if (policy.vacuumAfterPrune) {
31
+ try {
32
+ ctx.db.exec("VACUUM");
33
+ } catch {
34
+ /* non-fatal */
35
+ }
36
+ }
37
+
38
+ return {
39
+ eventsRemoved: delEvents.changes ?? 0,
40
+ measurementsRemoved: delMeas.changes ?? 0,
41
+ identitiesRemoved: 0,
42
+ freedBytes: 0,
43
+ };
44
+ }
45
+
46
+ export function vacuum(ctx: AnalyticsStoreCtx): MaintenanceReport {
47
+ try {
48
+ ctx.db.exec("VACUUM");
49
+ return { ok: true, detail: "VACUUM completed" };
50
+ } catch (e) {
51
+ return { ok: false, detail: String(e) };
52
+ }
53
+ }
54
+
55
+ export function integrityCheck(ctx: AnalyticsStoreCtx): IntegrityReport {
56
+ try {
57
+ const row = ctx.db.prepare("PRAGMA integrity_check").get() as
58
+ | { integrity_check: string }
59
+ | undefined;
60
+ const result = row?.integrity_check ?? "ok";
61
+ return { ok: result === "ok", detail: result };
62
+ } catch (e) {
63
+ return { ok: false, detail: String(e) };
64
+ }
65
+ }
66
+
67
+ export function backup(ctx: AnalyticsStoreCtx): BackupReport {
68
+ if (ctx.opts.inMemory) {
69
+ return { ok: false, detail: "Cannot backup an in-memory database" };
70
+ }
71
+ try {
72
+ // WAL-safe checkpoint before copy.
73
+ ctx.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
74
+ const src = analyticsDbPath(ctx.stateDir);
75
+ if (!existsSync(src)) return { ok: false, detail: "analytics.db not found" };
76
+ const dest = `${src}.bak-${Date.now()}`;
77
+ copyFileSync(src, dest);
78
+ return { ok: true, path: dest, detail: `Backup written to ${dest}` };
79
+ } catch (e) {
80
+ return { ok: false, detail: String(e) };
81
+ }
82
+ }
83
+
84
+ export function clear(ctx: AnalyticsStoreCtx): void {
85
+ withTx(ctx.db, () => {
86
+ ctx.db.exec("DELETE FROM request_events");
87
+ ctx.db.exec("DELETE FROM measurement_samples");
88
+ ctx.db.exec("DELETE FROM identity_observations");
89
+ // Reset AUTOINCREMENT sequences for measurement_samples + identity_observations.
90
+ try {
91
+ ctx.db.exec("DELETE FROM sqlite_sequence WHERE name IN ('measurement_samples', 'identity_observations')");
92
+ } catch {
93
+ /* non-fatal: sqlite_sequence may not exist yet */
94
+ }
95
+ });
96
+ }
@@ -0,0 +1,9 @@
1
+ /** The shared context passed to every SQLite delegate function. */
2
+ import type { DatabaseSync } from "node:sqlite";
3
+ import type { AnalyticsStoreOptions } from "../types.js";
4
+
5
+ export interface AnalyticsStoreCtx {
6
+ db: DatabaseSync;
7
+ stateDir: string;
8
+ opts: AnalyticsStoreOptions;
9
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * read.ts — AnalyticsReader delegate bodies for the SQLite backend.
3
+ * PMA-1: minimal (status only). Full queries (providers/models/live/detailed)
4
+ * arrive in PMA-3.
5
+ */
6
+ import type { AnalyticsStatus } from "../types.js";
7
+ import type { AnalyticsStoreCtx } from "./ctx.js";
8
+ import { getAnalyticsSchemaVersion } from "../schema.js";
9
+
10
+ export function status(ctx: AnalyticsStoreCtx): AnalyticsStatus {
11
+ const eventCount = (
12
+ ctx.db.prepare("SELECT COUNT(*) AS c FROM request_events").get() as { c: number }
13
+ ).c;
14
+ const measurementCount = (
15
+ ctx.db.prepare("SELECT COUNT(*) AS c FROM measurement_samples").get() as { c: number }
16
+ ).c;
17
+ const identityCount = (
18
+ ctx.db.prepare("SELECT COUNT(*) AS c FROM identity_observations").get() as { c: number }
19
+ ).c;
20
+ // Freshness: the most recent observed_at across all three fact tables.
21
+ const lastEvent = (
22
+ ctx.db
23
+ .prepare("SELECT MAX(observed_at) AS mx FROM request_events")
24
+ .get() as { mx: number | null }
25
+ ).mx;
26
+ const lastMeas = (
27
+ ctx.db
28
+ .prepare("SELECT MAX(observed_at) AS mx FROM measurement_samples")
29
+ .get() as { mx: number | null }
30
+ ).mx;
31
+ const lastId = (
32
+ ctx.db
33
+ .prepare("SELECT MAX(observed_at) AS mx FROM identity_observations")
34
+ .get() as { mx: number | null }
35
+ ).mx;
36
+ const freshThrough = Math.max(lastEvent ?? 0, lastMeas ?? 0, lastId ?? 0) || null;
37
+
38
+ return {
39
+ enabled: true,
40
+ schemaVersion: getAnalyticsSchemaVersion(),
41
+ requestEventCount: eventCount,
42
+ measurementCount,
43
+ identityCount,
44
+ freshThrough,
45
+ };
46
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * rows.ts — row ↔ fact mappers for the SQLite analytics store.
3
+ * Maps snake_case DB columns to camelCase contract fields.
4
+ */
5
+ import type {
6
+ RequestEventFact,
7
+ MeasurementFact,
8
+ IdentityObservation,
9
+ RequestEventKind,
10
+ QualityNote,
11
+ } from "../types.js";
12
+
13
+ export function rowToRequestEvent(r: Record<string, unknown>): RequestEventFact {
14
+ return {
15
+ id: r.id as string,
16
+ correlationId: (r.correlation_id as string) ?? undefined,
17
+ sessionId: (r.session_id as string) ?? undefined,
18
+ repoId: (r.repo_id as string) ?? undefined,
19
+ turnId: (r.turn_id as string) ?? undefined,
20
+ eventKind: r.event_kind as RequestEventKind,
21
+ observedAt: r.observed_at as number,
22
+ provider: (r.provider as string) ?? undefined,
23
+ model: (r.model as string) ?? undefined,
24
+ status: (r.status as string) ?? undefined,
25
+ inputTokens: (r.input_tokens as number) ?? undefined,
26
+ outputTokens: (r.output_tokens as number) ?? undefined,
27
+ cacheReadTokens: (r.cache_read_tokens as number) ?? undefined,
28
+ cacheWriteTokens: (r.cache_write_tokens as number) ?? undefined,
29
+ durationMs: (r.duration_ms as number) ?? undefined,
30
+ ttftMs: (r.ttft_ms as number) ?? undefined,
31
+ source: r.source as RequestEventFact["source"],
32
+ quality: JSON.parse((r.quality_json as string) ?? "{}") as QualityNote,
33
+ };
34
+ }
35
+
36
+ export function rowToMeasurement(r: Record<string, unknown>): MeasurementFact {
37
+ return {
38
+ observedAt: r.observed_at as number,
39
+ sampleKind: r.sample_kind as string,
40
+ provider: (r.provider as string) ?? undefined,
41
+ model: (r.model as string) ?? undefined,
42
+ value: r.value as number,
43
+ unit: r.unit as string,
44
+ correlationId: (r.correlation_id as string) ?? undefined,
45
+ source: r.source as MeasurementFact["source"],
46
+ quality: JSON.parse((r.quality_json as string) ?? "{}") as QualityNote,
47
+ };
48
+ }
49
+
50
+ export function rowToIdentity(r: Record<string, unknown>): IdentityObservation {
51
+ return {
52
+ observedAt: r.observed_at as number,
53
+ provider: (r.provider as string) ?? undefined,
54
+ model: (r.model as string) ?? undefined,
55
+ source: r.source as IdentityObservation["source"],
56
+ metadata: JSON.parse((r.metadata_json as string) ?? "{}") as Record<string, unknown>,
57
+ };
58
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * write.ts — AnalyticsWriter delegate bodies for the SQLite backend.
3
+ *
4
+ * All methods are append-only (INSERT only, never UPDATE). They return
5
+ * AppendResult — never throw into the host. INSERT OR IGNORE handles duplicate
6
+ * ids gracefully (PMA spec §5: "AppendResult identifies accepted/duplicate/
7
+ * failed without throwing").
8
+ */
9
+ import type {
10
+ RequestEventFact,
11
+ MeasurementFact,
12
+ IdentityObservation,
13
+ AppendResult,
14
+ } from "../types.js";
15
+ import type { AnalyticsStoreCtx } from "./ctx.js";
16
+
17
+ export function appendRequestEvent(ctx: AnalyticsStoreCtx, fact: RequestEventFact): AppendResult {
18
+ try {
19
+ const res = ctx.db
20
+ .prepare(
21
+ `INSERT OR IGNORE INTO request_events (
22
+ id, correlation_id, session_id, repo_id, turn_id, event_kind,
23
+ observed_at, provider, model, status,
24
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
25
+ duration_ms, ttft_ms, source, quality_json
26
+ ) VALUES (
27
+ @id, @correlation_id, @session_id, @repo_id, @turn_id, @event_kind,
28
+ @observed_at, @provider, @model, @status,
29
+ @input_tokens, @output_tokens, @cache_read_tokens, @cache_write_tokens,
30
+ @duration_ms, @ttft_ms, @source, @quality_json
31
+ )`,
32
+ )
33
+ .run({
34
+ "@id": fact.id,
35
+ "@correlation_id": fact.correlationId ?? null,
36
+ "@session_id": fact.sessionId ?? null,
37
+ "@repo_id": fact.repoId ?? null,
38
+ "@turn_id": fact.turnId ?? null,
39
+ "@event_kind": fact.eventKind,
40
+ "@observed_at": fact.observedAt,
41
+ "@provider": fact.provider ?? null,
42
+ "@model": fact.model ?? null,
43
+ "@status": fact.status ?? null,
44
+ "@input_tokens": fact.inputTokens ?? null,
45
+ "@output_tokens": fact.outputTokens ?? null,
46
+ "@cache_read_tokens": fact.cacheReadTokens ?? null,
47
+ "@cache_write_tokens": fact.cacheWriteTokens ?? null,
48
+ "@duration_ms": fact.durationMs ?? null,
49
+ "@ttft_ms": fact.ttftMs ?? null,
50
+ "@source": fact.source,
51
+ "@quality_json": JSON.stringify(fact.quality ?? {}),
52
+ }) as { changes?: number };
53
+ return (res.changes ?? 0) > 0
54
+ ? { status: "accepted", id: fact.id }
55
+ : { status: "duplicate", id: fact.id };
56
+ } catch (e) {
57
+ return { status: "failed", error: String(e) };
58
+ }
59
+ }
60
+
61
+ export function appendMeasurement(ctx: AnalyticsStoreCtx, fact: MeasurementFact): AppendResult {
62
+ try {
63
+ const res = ctx.db
64
+ .prepare(
65
+ `INSERT INTO measurement_samples (
66
+ observed_at, sample_kind, provider, model, value, unit,
67
+ correlation_id, source, quality_json
68
+ ) VALUES (
69
+ @observed_at, @sample_kind, @provider, @model, @value, @unit,
70
+ @correlation_id, @source, @quality_json
71
+ )`,
72
+ )
73
+ .run({
74
+ "@observed_at": fact.observedAt,
75
+ "@sample_kind": fact.sampleKind,
76
+ "@provider": fact.provider ?? null,
77
+ "@model": fact.model ?? null,
78
+ "@value": fact.value,
79
+ "@unit": fact.unit,
80
+ "@correlation_id": fact.correlationId ?? null,
81
+ "@source": fact.source,
82
+ "@quality_json": JSON.stringify(fact.quality ?? {}),
83
+ }) as { lastInsertRowid?: number | bigint };
84
+ return { status: "accepted", id: Number(res.lastInsertRowid ?? 0) };
85
+ } catch (e) {
86
+ return { status: "failed", error: String(e) };
87
+ }
88
+ }
89
+
90
+ export function appendIdentity(ctx: AnalyticsStoreCtx, obs: IdentityObservation): AppendResult {
91
+ try {
92
+ const res = ctx.db
93
+ .prepare(
94
+ `INSERT INTO identity_observations (
95
+ observed_at, provider, model, source, metadata_json
96
+ ) VALUES (
97
+ @observed_at, @provider, @model, @source, @metadata_json
98
+ )`,
99
+ )
100
+ .run({
101
+ "@observed_at": obs.observedAt,
102
+ "@provider": obs.provider ?? null,
103
+ "@model": obs.model ?? null,
104
+ "@source": obs.source,
105
+ "@metadata_json": JSON.stringify(obs.metadata ?? {}),
106
+ }) as { lastInsertRowid?: number | bigint };
107
+ return { status: "accepted", id: Number(res.lastInsertRowid ?? 0) };
108
+ } catch (e) {
109
+ return { status: "failed", error: String(e) };
110
+ }
111
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * sqlite-store.ts — SqliteAnalyticsStore: thin delegate shell for the SQLite
3
+ * analytics backend. Mirrors the turns store pattern (src/store/turns/sqlite-store.ts).
4
+ *
5
+ * Every method delegates to a function in sqlite-store/{read,write,admin}.ts.
6
+ * Capability gating: asReader/asWriter/asAdmin return fresh object literals
7
+ * exposing ONLY that interface's methods — never `this`.
8
+ */
9
+ import type { DatabaseSync } from "node:sqlite";
10
+ import type {
11
+ AnalyticsStore,
12
+ AnalyticsReader,
13
+ AnalyticsWriter,
14
+ AnalyticsAdmin,
15
+ AnalyticsStoreOptions,
16
+ RequestEventFact,
17
+ MeasurementFact,
18
+ IdentityObservation,
19
+ AppendResult,
20
+ AnalyticsStatus,
21
+ RetentionPolicy,
22
+ PruneReport,
23
+ IntegrityReport,
24
+ MaintenanceReport,
25
+ BackupReport,
26
+ } from "./types.js";
27
+ import { openAnalyticsDb, closeAnalyticsDb } from "./connection.js";
28
+ import type { AnalyticsStoreCtx } from "./sqlite-store/ctx.js";
29
+ import * as read from "./sqlite-store/read.js";
30
+ import * as write from "./sqlite-store/write.js";
31
+ import * as admin from "./sqlite-store/admin.js";
32
+
33
+ export class SqliteAnalyticsStore implements AnalyticsStore {
34
+ private db: DatabaseSync;
35
+ private readonly stateDir: string;
36
+ private readonly opts: AnalyticsStoreOptions;
37
+
38
+ constructor(options: AnalyticsStoreOptions) {
39
+ this.opts = options;
40
+ this.stateDir = options.stateDir;
41
+ this.db = openAnalyticsDb(options.stateDir, {
42
+ dbPath: options.dbPath,
43
+ inMemory: options.inMemory,
44
+ });
45
+ }
46
+
47
+ private ctx(): AnalyticsStoreCtx {
48
+ return { db: this.db, stateDir: this.stateDir, opts: this.opts };
49
+ }
50
+
51
+ // ── AnalyticsReader ───────────────────────────────────────────────
52
+
53
+ status(): AnalyticsStatus {
54
+ return read.status(this.ctx());
55
+ }
56
+
57
+ // ── AnalyticsWriter ───────────────────────────────────────────────
58
+
59
+ appendRequestEvent(fact: RequestEventFact): AppendResult {
60
+ return write.appendRequestEvent(this.ctx(), fact);
61
+ }
62
+ appendMeasurement(sample: MeasurementFact): AppendResult {
63
+ return write.appendMeasurement(this.ctx(), sample);
64
+ }
65
+ appendIdentity(observation: IdentityObservation): AppendResult {
66
+ return write.appendIdentity(this.ctx(), observation);
67
+ }
68
+
69
+ // ── AnalyticsAdmin ────────────────────────────────────────────────
70
+
71
+ prune(policy: RetentionPolicy): PruneReport {
72
+ return admin.prune(this.ctx(), policy);
73
+ }
74
+ vacuum(): MaintenanceReport {
75
+ return admin.vacuum(this.ctx());
76
+ }
77
+ integrityCheck(): IntegrityReport {
78
+ return admin.integrityCheck(this.ctx());
79
+ }
80
+ backup(): BackupReport {
81
+ return admin.backup(this.ctx());
82
+ }
83
+ clear(): void {
84
+ admin.clear(this.ctx());
85
+ }
86
+ close(): void {
87
+ closeAnalyticsDb(this.stateDir, {
88
+ dbPath: this.opts.dbPath,
89
+ inMemory: this.opts.inMemory,
90
+ });
91
+ }
92
+
93
+ // ── Capability views ──────────────────────────────────────────────
94
+
95
+ asReader(): AnalyticsReader {
96
+ return {
97
+ status: () => this.status(),
98
+ };
99
+ }
100
+
101
+ asWriter(): AnalyticsWriter {
102
+ return {
103
+ appendRequestEvent: (f) => this.appendRequestEvent(f),
104
+ appendMeasurement: (s) => this.appendMeasurement(s),
105
+ appendIdentity: (o) => this.appendIdentity(o),
106
+ };
107
+ }
108
+
109
+ asAdmin(): AnalyticsAdmin {
110
+ return {
111
+ prune: (p) => this.prune(p),
112
+ vacuum: () => this.vacuum(),
113
+ integrityCheck: () => this.integrityCheck(),
114
+ backup: () => this.backup(),
115
+ clear: () => this.clear(),
116
+ close: () => this.close(),
117
+ };
118
+ }
119
+ }