@massu/core 1.10.7 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
package/src/memory-db.ts DELETED
@@ -1,1614 +0,0 @@
1
- // Copyright (c) 2026 Massu. All rights reserved.
2
- // Licensed under BSL 1.1 - see LICENSE file for details.
3
-
4
- import Database from 'better-sqlite3';
5
- import { resolve, dirname, basename } from 'path';
6
- import { existsSync, mkdirSync } from 'fs';
7
- import { getConfig, getResolvedPaths, getProjectRoot } from './config.ts';
8
-
9
- /**
10
- * Sanitize a user-provided query string for use with SQLite FTS5 MATCH.
11
- * Wraps each token in double quotes to treat them as literals,
12
- * preventing FTS5 operator injection (AND, OR, NOT, NEAR, *, etc.).
13
- */
14
- export function sanitizeFts5Query(raw: string): string {
15
- const trimmed = raw.trim();
16
- if (!trimmed) return '""';
17
- // Remove any existing double quotes, then wrap each whitespace-separated token
18
- const tokens = trimmed.replace(/"/g, '').split(/\s+/).filter(Boolean);
19
- return tokens.map(t => `"${t}"`).join(' ');
20
- }
21
-
22
- // ============================================================
23
- // P1-001: Memory Database Schema
24
- // ============================================================
25
-
26
- /**
27
- * Connection to the memory SQLite database.
28
- * Stores session memory, observations, and observability data.
29
- */
30
- export function getMemoryDb(): Database.Database {
31
- const dbPath = getResolvedPaths().memoryDbPath;
32
- const dir = dirname(dbPath);
33
- if (!existsSync(dir)) {
34
- mkdirSync(dir, { recursive: true });
35
- }
36
- const db = new Database(dbPath);
37
- db.pragma('journal_mode = WAL');
38
- db.pragma('foreign_keys = ON');
39
- initMemorySchema(db);
40
- return db;
41
- }
42
-
43
- export function initMemorySchema(db: Database.Database): void {
44
- db.exec(`
45
- -- Sessions table (linked to Claude Code session IDs)
46
- CREATE TABLE IF NOT EXISTS sessions (
47
- id INTEGER PRIMARY KEY AUTOINCREMENT,
48
- session_id TEXT UNIQUE NOT NULL,
49
- project TEXT NOT NULL DEFAULT 'my-project',
50
- git_branch TEXT,
51
- started_at TEXT NOT NULL,
52
- started_at_epoch INTEGER NOT NULL,
53
- ended_at TEXT,
54
- ended_at_epoch INTEGER,
55
- status TEXT CHECK(status IN ('active', 'completed', 'abandoned')) NOT NULL DEFAULT 'active',
56
- plan_file TEXT,
57
- plan_phase TEXT,
58
- task_id TEXT
59
- );
60
-
61
- CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
62
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_epoch DESC);
63
- CREATE INDEX IF NOT EXISTS idx_sessions_task_id ON sessions(task_id);
64
-
65
- -- Observations table (structured knowledge from tool usage)
66
- CREATE TABLE IF NOT EXISTS observations (
67
- id INTEGER PRIMARY KEY AUTOINCREMENT,
68
- session_id TEXT NOT NULL,
69
- type TEXT NOT NULL CHECK(type IN (
70
- 'decision', 'bugfix', 'feature', 'refactor', 'discovery',
71
- 'cr_violation', 'vr_check', 'pattern_compliance', 'failed_attempt',
72
- 'file_change', 'incident_near_miss'
73
- )),
74
- title TEXT NOT NULL,
75
- detail TEXT,
76
- files_involved TEXT DEFAULT '[]',
77
- plan_item TEXT,
78
- cr_rule TEXT,
79
- vr_type TEXT,
80
- evidence TEXT,
81
- importance INTEGER NOT NULL DEFAULT 3 CHECK(importance BETWEEN 1 AND 5),
82
- recurrence_count INTEGER NOT NULL DEFAULT 1,
83
- original_tokens INTEGER DEFAULT 0,
84
- created_at TEXT NOT NULL,
85
- created_at_epoch INTEGER NOT NULL,
86
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
87
- );
88
-
89
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
90
- CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
91
- CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
92
- CREATE INDEX IF NOT EXISTS idx_observations_plan_item ON observations(plan_item);
93
- CREATE INDEX IF NOT EXISTS idx_observations_cr_rule ON observations(cr_rule);
94
- CREATE INDEX IF NOT EXISTS idx_observations_importance ON observations(importance DESC);
95
- `);
96
-
97
- // FTS5 tables - create separately to handle "already exists" gracefully
98
- try {
99
- db.exec(`
100
- CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
101
- title, detail, evidence,
102
- content='observations',
103
- content_rowid='id'
104
- );
105
- `);
106
- } catch (_e) {
107
- // FTS5 table may already exist with different schema - ignore
108
- }
109
-
110
- // FTS5 sync triggers
111
- db.exec(`
112
- CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
113
- INSERT INTO observations_fts(rowid, title, detail, evidence)
114
- VALUES (new.id, new.title, new.detail, new.evidence);
115
- END;
116
-
117
- CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
118
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
119
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
120
- END;
121
-
122
- CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
123
- INSERT INTO observations_fts(observations_fts, rowid, title, detail, evidence)
124
- VALUES ('delete', old.id, old.title, old.detail, old.evidence);
125
- INSERT INTO observations_fts(rowid, title, detail, evidence)
126
- VALUES (new.id, new.title, new.detail, new.evidence);
127
- END;
128
- `);
129
-
130
- // Session summaries
131
- db.exec(`
132
- CREATE TABLE IF NOT EXISTS session_summaries (
133
- id INTEGER PRIMARY KEY AUTOINCREMENT,
134
- session_id TEXT NOT NULL,
135
- request TEXT,
136
- investigated TEXT,
137
- decisions TEXT,
138
- completed TEXT,
139
- failed_attempts TEXT,
140
- next_steps TEXT,
141
- files_created TEXT DEFAULT '[]',
142
- files_modified TEXT DEFAULT '[]',
143
- verification_results TEXT DEFAULT '{}',
144
- plan_progress TEXT DEFAULT '{}',
145
- created_at TEXT NOT NULL,
146
- created_at_epoch INTEGER NOT NULL,
147
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
148
- );
149
-
150
- CREATE INDEX IF NOT EXISTS idx_summaries_session ON session_summaries(session_id);
151
- `);
152
-
153
- // User prompts
154
- db.exec(`
155
- CREATE TABLE IF NOT EXISTS user_prompts (
156
- id INTEGER PRIMARY KEY AUTOINCREMENT,
157
- session_id TEXT NOT NULL,
158
- prompt_text TEXT NOT NULL,
159
- prompt_number INTEGER NOT NULL DEFAULT 1,
160
- created_at TEXT NOT NULL,
161
- created_at_epoch INTEGER NOT NULL,
162
- FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
163
- );
164
- `);
165
-
166
- try {
167
- db.exec(`
168
- CREATE VIRTUAL TABLE IF NOT EXISTS user_prompts_fts USING fts5(
169
- prompt_text,
170
- content='user_prompts',
171
- content_rowid='id'
172
- );
173
- `);
174
- } catch (_e) {
175
- // FTS5 table may already exist
176
- }
177
-
178
- db.exec(`
179
- CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
180
- INSERT INTO user_prompts_fts(rowid, prompt_text) VALUES (new.id, new.prompt_text);
181
- END;
182
-
183
- CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON user_prompts BEGIN
184
- INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
185
- VALUES ('delete', old.id, old.prompt_text);
186
- END;
187
- `);
188
-
189
- // Metadata
190
- db.exec(`
191
- CREATE TABLE IF NOT EXISTS memory_meta (
192
- key TEXT PRIMARY KEY,
193
- value TEXT NOT NULL
194
- );
195
- `);
196
-
197
- // ============================================================
198
- // Observability tables (P1-001, P1-002)
199
- // ============================================================
200
-
201
- // P1-001: Conversation turns (full session replay)
202
- db.exec(`
203
- CREATE TABLE IF NOT EXISTS conversation_turns (
204
- id INTEGER PRIMARY KEY AUTOINCREMENT,
205
- session_id TEXT NOT NULL,
206
- turn_number INTEGER NOT NULL,
207
- user_prompt TEXT NOT NULL,
208
- assistant_response TEXT,
209
- tool_calls_json TEXT,
210
- tool_call_count INTEGER DEFAULT 0,
211
- model_used TEXT,
212
- duration_ms INTEGER,
213
- prompt_tokens INTEGER,
214
- response_tokens INTEGER,
215
- created_at TEXT DEFAULT (datetime('now')),
216
- created_at_epoch INTEGER DEFAULT (unixepoch()),
217
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
218
- );
219
-
220
- CREATE INDEX IF NOT EXISTS idx_ct_session ON conversation_turns(session_id);
221
- CREATE INDEX IF NOT EXISTS idx_ct_created ON conversation_turns(created_at DESC);
222
- CREATE INDEX IF NOT EXISTS idx_ct_turn ON conversation_turns(session_id, turn_number);
223
- `);
224
-
225
- // P1-002: Tool call details (analytics)
226
- db.exec(`
227
- CREATE TABLE IF NOT EXISTS tool_call_details (
228
- id INTEGER PRIMARY KEY AUTOINCREMENT,
229
- session_id TEXT NOT NULL,
230
- turn_number INTEGER NOT NULL,
231
- tool_name TEXT NOT NULL,
232
- tool_input_summary TEXT,
233
- tool_input_size INTEGER,
234
- tool_output_size INTEGER,
235
- tool_success INTEGER DEFAULT 1,
236
- duration_ms INTEGER,
237
- files_involved TEXT,
238
- created_at TEXT DEFAULT (datetime('now')),
239
- created_at_epoch INTEGER DEFAULT (unixepoch()),
240
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
241
- );
242
-
243
- CREATE INDEX IF NOT EXISTS idx_tcd_session ON tool_call_details(session_id);
244
- CREATE INDEX IF NOT EXISTS idx_tcd_tool ON tool_call_details(tool_name);
245
- CREATE INDEX IF NOT EXISTS idx_tcd_created ON tool_call_details(created_at DESC);
246
- `);
247
-
248
- // P1-003: FTS5 index for conversation turns
249
- try {
250
- db.exec(`
251
- CREATE VIRTUAL TABLE IF NOT EXISTS conversation_turns_fts USING fts5(
252
- user_prompt,
253
- assistant_response,
254
- content=conversation_turns,
255
- content_rowid=id
256
- );
257
- `);
258
- } catch (_e) {
259
- // FTS5 table may already exist with different schema
260
- }
261
-
262
- // FTS5 sync triggers for conversation_turns
263
- db.exec(`
264
- CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
265
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
266
- VALUES (new.id, new.user_prompt, new.assistant_response);
267
- END;
268
-
269
- CREATE TRIGGER IF NOT EXISTS ct_fts_delete AFTER DELETE ON conversation_turns BEGIN
270
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
271
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
272
- END;
273
-
274
- CREATE TRIGGER IF NOT EXISTS ct_fts_update AFTER UPDATE ON conversation_turns BEGIN
275
- INSERT INTO conversation_turns_fts(conversation_turns_fts, rowid, user_prompt, assistant_response)
276
- VALUES ('delete', old.id, old.user_prompt, old.assistant_response);
277
- INSERT INTO conversation_turns_fts(rowid, user_prompt, assistant_response)
278
- VALUES (new.id, new.user_prompt, new.assistant_response);
279
- END;
280
- `);
281
-
282
- // ============================================================
283
- // PLAN-02 Enhancement Tables (Analytics, Governance, Security, Team, Regression)
284
- // ============================================================
285
-
286
- // P1-001: Quality scores per session
287
- db.exec(`
288
- CREATE TABLE IF NOT EXISTS session_quality_scores (
289
- id INTEGER PRIMARY KEY AUTOINCREMENT,
290
- session_id TEXT NOT NULL UNIQUE,
291
- project TEXT NOT NULL DEFAULT 'my-project',
292
- score INTEGER NOT NULL DEFAULT 100,
293
- security_score INTEGER NOT NULL DEFAULT 100,
294
- architecture_score INTEGER NOT NULL DEFAULT 100,
295
- coupling_score INTEGER NOT NULL DEFAULT 100,
296
- test_score INTEGER NOT NULL DEFAULT 100,
297
- rule_compliance_score INTEGER NOT NULL DEFAULT 100,
298
- observations_total INTEGER NOT NULL DEFAULT 0,
299
- bugs_found INTEGER NOT NULL DEFAULT 0,
300
- bugs_fixed INTEGER NOT NULL DEFAULT 0,
301
- vr_checks_passed INTEGER NOT NULL DEFAULT 0,
302
- vr_checks_failed INTEGER NOT NULL DEFAULT 0,
303
- incidents_triggered INTEGER NOT NULL DEFAULT 0,
304
- created_at TEXT DEFAULT (datetime('now')),
305
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
306
- );
307
- CREATE INDEX IF NOT EXISTS idx_sqs_session ON session_quality_scores(session_id);
308
- CREATE INDEX IF NOT EXISTS idx_sqs_project ON session_quality_scores(project);
309
- `);
310
-
311
- // P1-002: Cost tracking per session
312
- db.exec(`
313
- CREATE TABLE IF NOT EXISTS session_costs (
314
- id INTEGER PRIMARY KEY AUTOINCREMENT,
315
- session_id TEXT NOT NULL UNIQUE,
316
- project TEXT NOT NULL DEFAULT 'my-project',
317
- input_tokens INTEGER NOT NULL DEFAULT 0,
318
- output_tokens INTEGER NOT NULL DEFAULT 0,
319
- cache_read_tokens INTEGER NOT NULL DEFAULT 0,
320
- cache_write_tokens INTEGER NOT NULL DEFAULT 0,
321
- total_tokens INTEGER NOT NULL DEFAULT 0,
322
- estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
323
- model TEXT,
324
- duration_minutes REAL NOT NULL DEFAULT 0.0,
325
- tool_calls INTEGER NOT NULL DEFAULT 0,
326
- created_at TEXT DEFAULT (datetime('now')),
327
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
328
- );
329
- CREATE INDEX IF NOT EXISTS idx_sc_session ON session_costs(session_id);
330
- `);
331
-
332
- // P1-002: Feature cost attribution
333
- db.exec(`
334
- CREATE TABLE IF NOT EXISTS feature_costs (
335
- id INTEGER PRIMARY KEY AUTOINCREMENT,
336
- feature_key TEXT NOT NULL,
337
- session_id TEXT NOT NULL,
338
- tokens_used INTEGER NOT NULL DEFAULT 0,
339
- estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
340
- commit_hash TEXT,
341
- created_at TEXT DEFAULT (datetime('now')),
342
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
343
- );
344
- CREATE INDEX IF NOT EXISTS idx_fc_feature ON feature_costs(feature_key);
345
- CREATE INDEX IF NOT EXISTS idx_fc_session ON feature_costs(session_id);
346
- `);
347
-
348
- // P1-003: Prompt effectiveness outcomes
349
- db.exec(`
350
- CREATE TABLE IF NOT EXISTS prompt_outcomes (
351
- id INTEGER PRIMARY KEY AUTOINCREMENT,
352
- session_id TEXT NOT NULL,
353
- prompt_hash TEXT NOT NULL,
354
- prompt_text TEXT NOT NULL,
355
- prompt_category TEXT NOT NULL DEFAULT 'feature',
356
- word_count INTEGER NOT NULL DEFAULT 0,
357
- outcome TEXT NOT NULL DEFAULT 'success' CHECK(outcome IN ('success', 'partial', 'failure', 'abandoned')),
358
- corrections_needed INTEGER NOT NULL DEFAULT 0,
359
- follow_up_prompts INTEGER NOT NULL DEFAULT 0,
360
- created_at TEXT DEFAULT (datetime('now')),
361
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
362
- );
363
- CREATE INDEX IF NOT EXISTS idx_po_session ON prompt_outcomes(session_id);
364
- CREATE INDEX IF NOT EXISTS idx_po_category ON prompt_outcomes(prompt_category);
365
- `);
366
-
367
- // P2-001: Compliance audit log
368
- db.exec(`
369
- CREATE TABLE IF NOT EXISTS audit_log (
370
- id INTEGER PRIMARY KEY AUTOINCREMENT,
371
- session_id TEXT NOT NULL,
372
- timestamp TEXT DEFAULT (datetime('now')),
373
- event_type TEXT NOT NULL CHECK(event_type IN ('code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction')),
374
- actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
375
- model_id TEXT,
376
- file_path TEXT,
377
- change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
378
- rules_in_effect TEXT,
379
- approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
380
- evidence TEXT,
381
- metadata TEXT,
382
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
383
- );
384
- CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
385
- CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
386
- CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
387
- CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
388
- `);
389
-
390
- // P2-002: Validation results
391
- db.exec(`
392
- CREATE TABLE IF NOT EXISTS validation_results (
393
- id INTEGER PRIMARY KEY AUTOINCREMENT,
394
- session_id TEXT NOT NULL,
395
- file_path TEXT NOT NULL,
396
- validation_type TEXT NOT NULL,
397
- passed INTEGER NOT NULL DEFAULT 1,
398
- details TEXT,
399
- rules_violated TEXT,
400
- created_at TEXT DEFAULT (datetime('now')),
401
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
402
- );
403
- CREATE INDEX IF NOT EXISTS idx_vr_session ON validation_results(session_id);
404
- CREATE INDEX IF NOT EXISTS idx_vr_file ON validation_results(file_path);
405
- `);
406
-
407
- // P2-003: Architecture decisions
408
- db.exec(`
409
- CREATE TABLE IF NOT EXISTS architecture_decisions (
410
- id INTEGER PRIMARY KEY AUTOINCREMENT,
411
- session_id TEXT NOT NULL,
412
- title TEXT NOT NULL,
413
- context TEXT,
414
- decision TEXT NOT NULL,
415
- status TEXT NOT NULL DEFAULT 'accepted' CHECK(status IN ('accepted', 'superseded', 'deprecated')),
416
- alternatives TEXT,
417
- consequences TEXT,
418
- affected_files TEXT,
419
- commit_hash TEXT,
420
- created_at TEXT DEFAULT (datetime('now')),
421
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
422
- );
423
- CREATE INDEX IF NOT EXISTS idx_ad_session ON architecture_decisions(session_id);
424
- CREATE INDEX IF NOT EXISTS idx_ad_status ON architecture_decisions(status);
425
- `);
426
-
427
- // P3-001: Security scores per file
428
- db.exec(`
429
- CREATE TABLE IF NOT EXISTS security_scores (
430
- id INTEGER PRIMARY KEY AUTOINCREMENT,
431
- session_id TEXT NOT NULL,
432
- file_path TEXT NOT NULL,
433
- risk_score INTEGER NOT NULL DEFAULT 0,
434
- findings TEXT,
435
- created_at TEXT DEFAULT (datetime('now')),
436
- FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
437
- );
438
- CREATE INDEX IF NOT EXISTS idx_ss_session ON security_scores(session_id);
439
- CREATE INDEX IF NOT EXISTS idx_ss_file ON security_scores(file_path);
440
- `);
441
-
442
- // P3-002: Dependency assessments
443
- db.exec(`
444
- CREATE TABLE IF NOT EXISTS dependency_assessments (
445
- id INTEGER PRIMARY KEY AUTOINCREMENT,
446
- package_name TEXT NOT NULL,
447
- version TEXT,
448
- risk_score INTEGER NOT NULL DEFAULT 0,
449
- vulnerabilities INTEGER NOT NULL DEFAULT 0,
450
- last_publish_days INTEGER,
451
- weekly_downloads INTEGER,
452
- license TEXT,
453
- bundle_size_kb INTEGER,
454
- previous_removals INTEGER NOT NULL DEFAULT 0,
455
- assessed_at TEXT DEFAULT (datetime('now'))
456
- );
457
- CREATE INDEX IF NOT EXISTS idx_da_package ON dependency_assessments(package_name);
458
- `);
459
-
460
- // P4-001: Developer expertise
461
- db.exec(`
462
- CREATE TABLE IF NOT EXISTS developer_expertise (
463
- id INTEGER PRIMARY KEY AUTOINCREMENT,
464
- developer_id TEXT NOT NULL,
465
- module TEXT NOT NULL,
466
- session_count INTEGER NOT NULL DEFAULT 0,
467
- observation_count INTEGER NOT NULL DEFAULT 0,
468
- expertise_score INTEGER NOT NULL DEFAULT 0,
469
- last_active TEXT DEFAULT (datetime('now')),
470
- UNIQUE(developer_id, module)
471
- );
472
- CREATE INDEX IF NOT EXISTS idx_de_developer ON developer_expertise(developer_id);
473
- CREATE INDEX IF NOT EXISTS idx_de_module ON developer_expertise(module);
474
- `);
475
-
476
- // P4-001: Shared observations for team knowledge
477
- db.exec(`
478
- CREATE TABLE IF NOT EXISTS shared_observations (
479
- id INTEGER PRIMARY KEY AUTOINCREMENT,
480
- original_id INTEGER,
481
- developer_id TEXT NOT NULL,
482
- project TEXT NOT NULL,
483
- observation_type TEXT NOT NULL,
484
- summary TEXT NOT NULL,
485
- file_path TEXT,
486
- module TEXT,
487
- severity INTEGER NOT NULL DEFAULT 3,
488
- is_shared INTEGER NOT NULL DEFAULT 0,
489
- shared_at TEXT,
490
- created_at TEXT DEFAULT (datetime('now'))
491
- );
492
- CREATE INDEX IF NOT EXISTS idx_so_developer ON shared_observations(developer_id);
493
- CREATE INDEX IF NOT EXISTS idx_so_file ON shared_observations(file_path);
494
- CREATE INDEX IF NOT EXISTS idx_so_module ON shared_observations(module);
495
- `);
496
-
497
- // P4-001: Knowledge conflicts
498
- db.exec(`
499
- CREATE TABLE IF NOT EXISTS knowledge_conflicts (
500
- id INTEGER PRIMARY KEY AUTOINCREMENT,
501
- file_path TEXT NOT NULL,
502
- developer_a TEXT NOT NULL,
503
- developer_b TEXT NOT NULL,
504
- conflict_type TEXT NOT NULL DEFAULT 'concurrent_edit',
505
- resolved INTEGER NOT NULL DEFAULT 0,
506
- detected_at TEXT DEFAULT (datetime('now'))
507
- );
508
- CREATE INDEX IF NOT EXISTS idx_kc_file ON knowledge_conflicts(file_path);
509
- `);
510
-
511
- // P4-002: Feature health tracking
512
- db.exec(`
513
- CREATE TABLE IF NOT EXISTS feature_health (
514
- id INTEGER PRIMARY KEY AUTOINCREMENT,
515
- feature_key TEXT NOT NULL UNIQUE,
516
- health_score INTEGER NOT NULL DEFAULT 100,
517
- tests_passing INTEGER NOT NULL DEFAULT 0,
518
- tests_failing INTEGER NOT NULL DEFAULT 0,
519
- test_coverage_pct REAL,
520
- modifications_since_test INTEGER NOT NULL DEFAULT 0,
521
- last_modified TEXT,
522
- last_tested TEXT,
523
- created_at TEXT DEFAULT (datetime('now'))
524
- );
525
- CREATE INDEX IF NOT EXISTS idx_fh_feature ON feature_health(feature_key);
526
- CREATE INDEX IF NOT EXISTS idx_fh_health ON feature_health(health_score);
527
- `);
528
-
529
- // ============================================================
530
- // Hook Tables (cost-tracker.ts, quality-event.ts)
531
- // ============================================================
532
-
533
- // Tool-level cost events (one row per tool call)
534
- db.exec(`
535
- CREATE TABLE IF NOT EXISTS tool_cost_events (
536
- id INTEGER PRIMARY KEY AUTOINCREMENT,
537
- session_id TEXT NOT NULL,
538
- tool_name TEXT NOT NULL,
539
- estimated_input_tokens INTEGER DEFAULT 0,
540
- estimated_output_tokens INTEGER DEFAULT 0,
541
- model TEXT DEFAULT '',
542
- created_at TEXT DEFAULT (datetime('now'))
543
- );
544
- CREATE INDEX IF NOT EXISTS idx_tce_session ON tool_cost_events(session_id);
545
- CREATE INDEX IF NOT EXISTS idx_tce_tool ON tool_cost_events(tool_name);
546
- CREATE INDEX IF NOT EXISTS idx_tce_created ON tool_cost_events(created_at DESC);
547
- `);
548
-
549
- // Quality signal events (test failures, type errors, build failures)
550
- db.exec(`
551
- CREATE TABLE IF NOT EXISTS quality_events (
552
- id INTEGER PRIMARY KEY AUTOINCREMENT,
553
- session_id TEXT NOT NULL,
554
- event_type TEXT NOT NULL,
555
- tool_name TEXT NOT NULL,
556
- details TEXT DEFAULT '',
557
- created_at TEXT DEFAULT (datetime('now'))
558
- );
559
- CREATE INDEX IF NOT EXISTS idx_qe_session ON quality_events(session_id);
560
- CREATE INDEX IF NOT EXISTS idx_qe_event_type ON quality_events(event_type);
561
- CREATE INDEX IF NOT EXISTS idx_qe_created ON quality_events(created_at DESC);
562
- `);
563
-
564
- // ============================================================
565
- // Cloud Sync: Pending sync queue (offline resilience)
566
- // ============================================================
567
- db.exec(`
568
- CREATE TABLE IF NOT EXISTS pending_sync (
569
- id INTEGER PRIMARY KEY AUTOINCREMENT,
570
- payload TEXT NOT NULL,
571
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
572
- retry_count INTEGER NOT NULL DEFAULT 0,
573
- last_error TEXT
574
- );
575
- CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
576
- `);
577
-
578
- // ============================================================
579
- // P3-005: License cache table
580
- // ============================================================
581
- db.exec(`
582
- CREATE TABLE IF NOT EXISTS license_cache (
583
- api_key_hash TEXT PRIMARY KEY,
584
- tier TEXT NOT NULL,
585
- valid_until TEXT NOT NULL,
586
- last_validated TEXT NOT NULL,
587
- features TEXT DEFAULT '[]'
588
- );
589
- `);
590
-
591
- // ============================================================
592
- // Failure Classification: Taxonomy of known failure patterns
593
- // ============================================================
594
- db.exec(`
595
- CREATE TABLE IF NOT EXISTS failure_classes (
596
- id INTEGER PRIMARY KEY AUTOINCREMENT,
597
- name TEXT NOT NULL UNIQUE,
598
- description TEXT NOT NULL,
599
- diff_patterns TEXT NOT NULL DEFAULT '[]',
600
- file_patterns TEXT NOT NULL DEFAULT '[]',
601
- prompt_keywords TEXT NOT NULL DEFAULT '[]',
602
- incidents TEXT NOT NULL DEFAULT '[]',
603
- rules TEXT NOT NULL DEFAULT '[]',
604
- scanner_checks TEXT NOT NULL DEFAULT '[]',
605
- known_message TEXT NOT NULL DEFAULT '',
606
- needs_review INTEGER NOT NULL DEFAULT 0,
607
- created_at TEXT DEFAULT (datetime('now')),
608
- updated_at TEXT DEFAULT (datetime('now'))
609
- );
610
- CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
611
- CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
612
- `);
613
- }
614
-
615
- // ============================================================
616
- // Cloud Sync: Queue Functions
617
- // ============================================================
618
-
619
- /**
620
- * Enqueue a sync payload for later retry.
621
- */
622
- export function enqueueSyncPayload(db: Database.Database, payload: string): void {
623
- db.prepare('INSERT INTO pending_sync (payload) VALUES (?)').run(payload);
624
- }
625
-
626
- /**
627
- * Dequeue pending sync items (oldest first).
628
- *
629
- * P-H012 (plan-stage-c-high-batch): when items exceed retry_count >= 10
630
- * they are discarded — but the discard now emits a stderr warning AND
631
- * inserts an analytics_events telemetry row so the customer can detect
632
- * silent cloud-sync failure (e.g., invalid API key for >10 sync cycles).
633
- * Previously this was a silent DELETE; customers lost all queued
634
- * observations with no visibility.
635
- */
636
- export function dequeuePendingSync(
637
- db: Database.Database,
638
- limit: number = 10
639
- ): Array<{ id: number; payload: string; retry_count: number }> {
640
- // First, discard items that have exceeded max retries
641
- const stale = db.prepare(
642
- 'SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10'
643
- ).all() as Array<{ id: number; retry_count: number; last_error: string | null }>;
644
- if (stale.length > 0) {
645
- const ids = stale.map(s => s.id);
646
- db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => '?').join(',')})`).run(...ids);
647
- // P-H012: stderr warning so the customer's terminal sees what happened.
648
- const lastErrors = [...new Set(stale.map(s => s.last_error).filter(Boolean))];
649
- process.stderr.write(
650
- `[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. ` +
651
- `Likely cause: invalid API key or unreachable endpoint. ` +
652
- `Recent errors: ${lastErrors.slice(0, 3).join('; ') || '(none recorded)'}\n`,
653
- );
654
- // P-H012: telemetry event for dashboard surfacing. Use the analytics_events
655
- // sink that already exists in memory-db (see addObservation pattern).
656
- try {
657
- db.prepare(`
658
- INSERT INTO analytics_events (event_type, event_data, created_at)
659
- VALUES (?, ?, datetime('now'))
660
- `).run(
661
- 'cloud_sync_giveup',
662
- JSON.stringify({
663
- discarded_count: stale.length,
664
- recent_errors: lastErrors.slice(0, 3),
665
- }),
666
- );
667
- } catch {
668
- // analytics_events may not exist in older schemas — best-effort.
669
- }
670
- }
671
-
672
- return db.prepare(
673
- 'SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?'
674
- ).all(limit) as Array<{ id: number; payload: string; retry_count: number }>;
675
- }
676
-
677
- /**
678
- * Remove a successfully synced item from the queue.
679
- */
680
- export function removePendingSync(db: Database.Database, id: number): void {
681
- db.prepare('DELETE FROM pending_sync WHERE id = ?').run(id);
682
- }
683
-
684
- /**
685
- * Increment retry count and record the last error for a failed sync attempt.
686
- */
687
- export function incrementRetryCount(db: Database.Database, id: number, error: string): void {
688
- db.prepare(
689
- 'UPDATE pending_sync SET retry_count = retry_count + 1, last_error = ? WHERE id = ?'
690
- ).run(error, id);
691
- }
692
-
693
- // ============================================================
694
- // P1-002: Database Access Functions (19 functions)
695
- // ============================================================
696
-
697
- /**
698
- * Auto-assign importance score based on observation type and optional VR result.
699
- * Scale: 5=decision/failed_attempt, 4=cr_violation/vr_check(FAIL),
700
- * 3=feature/bugfix, 2=vr_check(PASS)/refactor, 1=file_change/discovery
701
- */
702
- export function assignImportance(type: string, vrResult?: string): number {
703
- switch (type) {
704
- case 'decision':
705
- case 'failed_attempt':
706
- return 5;
707
- case 'cr_violation':
708
- case 'incident_near_miss':
709
- return 4;
710
- case 'vr_check':
711
- return vrResult === 'PASS' ? 2 : 4;
712
- case 'pattern_compliance':
713
- return vrResult === 'PASS' ? 2 : 4;
714
- case 'feature':
715
- case 'bugfix':
716
- return 3;
717
- case 'refactor':
718
- return 2;
719
- case 'file_change':
720
- case 'discovery':
721
- return 1;
722
- default:
723
- return 3;
724
- }
725
- }
726
-
727
- /**
728
- * Derive task_id from plan file path.
729
- * Sessions working on the same plan file share a task_id.
730
- */
731
- export function autoDetectTaskId(planFile: string | null | undefined): string | null {
732
- if (!planFile) return null;
733
- // Use the plan filename without extension as task_id
734
- // e.g., "/path/to/2026-01-30-massu-memory.md" -> "2026-01-30-massu-memory"
735
- const base = basename(planFile);
736
- return base.replace(/\.md$/, '');
737
- }
738
-
739
- export interface CreateSessionOpts {
740
- branch?: string;
741
- planFile?: string;
742
- }
743
-
744
- /**
745
- * Create a session (INSERT OR IGNORE for idempotency).
746
- */
747
- export function createSession(db: Database.Database, sessionId: string, opts?: CreateSessionOpts): void {
748
- const now = new Date();
749
- const taskId = autoDetectTaskId(opts?.planFile);
750
- db.prepare(`
751
- INSERT OR IGNORE INTO sessions (session_id, git_branch, plan_file, task_id, started_at, started_at_epoch)
752
- VALUES (?, ?, ?, ?, ?, ?)
753
- `).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1000));
754
- }
755
-
756
- /**
757
- * End a session by updating status and ended_at.
758
- */
759
- export function endSession(db: Database.Database, sessionId: string, status: 'completed' | 'abandoned' = 'completed'): void {
760
- const now = new Date();
761
- db.prepare(`
762
- UPDATE sessions SET status = ?, ended_at = ?, ended_at_epoch = ? WHERE session_id = ?
763
- `).run(status, now.toISOString(), Math.floor(now.getTime() / 1000), sessionId);
764
- }
765
-
766
- export interface AddObservationOpts {
767
- filesInvolved?: string[];
768
- planItem?: string;
769
- crRule?: string;
770
- vrType?: string;
771
- evidence?: string;
772
- importance?: number;
773
- originalTokens?: number;
774
- }
775
-
776
- /**
777
- * Insert an observation into the memory DB.
778
- */
779
- export function addObservation(
780
- db: Database.Database,
781
- sessionId: string,
782
- type: string,
783
- title: string,
784
- detail: string | null,
785
- opts?: AddObservationOpts
786
- ): number {
787
- const now = new Date();
788
- const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes('PASS') ? 'PASS' : undefined);
789
- const result = db.prepare(`
790
- INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
791
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
792
- `).run(
793
- sessionId, type, title, detail,
794
- JSON.stringify(opts?.filesInvolved ?? []),
795
- opts?.planItem ?? null,
796
- opts?.crRule ?? null,
797
- opts?.vrType ?? null,
798
- opts?.evidence ?? null,
799
- importance,
800
- opts?.originalTokens ?? 0,
801
- now.toISOString(),
802
- Math.floor(now.getTime() / 1000)
803
- );
804
- return Number(result.lastInsertRowid);
805
- }
806
-
807
- export interface SessionSummary {
808
- request?: string;
809
- investigated?: string;
810
- decisions?: string;
811
- completed?: string;
812
- failedAttempts?: string;
813
- nextSteps?: string;
814
- filesCreated?: string[];
815
- filesModified?: string[];
816
- verificationResults?: Record<string, string>;
817
- planProgress?: Record<string, string>;
818
- }
819
-
820
- /**
821
- * Insert a session summary.
822
- */
823
- export function addSummary(db: Database.Database, sessionId: string, summary: SessionSummary): void {
824
- const now = new Date();
825
- db.prepare(`
826
- INSERT INTO session_summaries (session_id, request, investigated, decisions, completed, failed_attempts, next_steps, files_created, files_modified, verification_results, plan_progress, created_at, created_at_epoch)
827
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
828
- `).run(
829
- sessionId,
830
- summary.request ?? null,
831
- summary.investigated ?? null,
832
- summary.decisions ?? null,
833
- summary.completed ?? null,
834
- summary.failedAttempts ?? null,
835
- summary.nextSteps ?? null,
836
- JSON.stringify(summary.filesCreated ?? []),
837
- JSON.stringify(summary.filesModified ?? []),
838
- JSON.stringify(summary.verificationResults ?? {}),
839
- JSON.stringify(summary.planProgress ?? {}),
840
- now.toISOString(),
841
- Math.floor(now.getTime() / 1000)
842
- );
843
- }
844
-
845
- /**
846
- * Insert a user prompt.
847
- */
848
- export function addUserPrompt(db: Database.Database, sessionId: string, text: string, promptNumber: number): void {
849
- const now = new Date();
850
- db.prepare(`
851
- INSERT INTO user_prompts (session_id, prompt_text, prompt_number, created_at, created_at_epoch)
852
- VALUES (?, ?, ?, ?, ?)
853
- `).run(sessionId, text, promptNumber, now.toISOString(), Math.floor(now.getTime() / 1000));
854
- }
855
-
856
- export interface SearchOpts {
857
- type?: string;
858
- crRule?: string;
859
- dateFrom?: string;
860
- limit?: number;
861
- }
862
-
863
- /**
864
- * FTS5 search on observations + user_prompts.
865
- */
866
- export function searchObservations(db: Database.Database, query: string, opts?: SearchOpts): Array<{
867
- id: number;
868
- type: string;
869
- title: string;
870
- created_at: string;
871
- session_id: string;
872
- importance: number;
873
- rank: number;
874
- }> {
875
- const limit = opts?.limit ?? 20;
876
- let sql = `
877
- SELECT o.id, o.type, o.title, o.created_at, o.session_id, o.importance,
878
- rank
879
- FROM observations_fts
880
- JOIN observations o ON observations_fts.rowid = o.id
881
- WHERE observations_fts MATCH ?
882
- `;
883
- const params: (string | number)[] = [sanitizeFts5Query(query)];
884
-
885
- if (opts?.type) {
886
- sql += ' AND o.type = ?';
887
- params.push(opts.type);
888
- }
889
- if (opts?.crRule) {
890
- sql += ' AND o.cr_rule = ?';
891
- params.push(opts.crRule);
892
- }
893
- if (opts?.dateFrom) {
894
- sql += ' AND o.created_at >= ?';
895
- params.push(opts.dateFrom);
896
- }
897
-
898
- sql += ' ORDER BY rank LIMIT ?';
899
- params.push(limit);
900
-
901
- return db.prepare(sql).all(...params) as Array<{
902
- id: number;
903
- type: string;
904
- title: string;
905
- created_at: string;
906
- session_id: string;
907
- importance: number;
908
- rank: number;
909
- }>;
910
- }
911
-
912
- /**
913
- * Get recent observations, optionally filtered by session.
914
- */
915
- export function getRecentObservations(db: Database.Database, limit: number = 20, sessionId?: string): Array<{
916
- id: number;
917
- type: string;
918
- title: string;
919
- detail: string | null;
920
- importance: number;
921
- created_at: string;
922
- session_id: string;
923
- }> {
924
- if (sessionId) {
925
- return db.prepare(`
926
- SELECT id, type, title, detail, importance, created_at, session_id
927
- FROM observations WHERE session_id = ?
928
- ORDER BY created_at_epoch DESC LIMIT ?
929
- `).all(sessionId, limit) as Array<{
930
- id: number; type: string; title: string; detail: string | null;
931
- importance: number; created_at: string; session_id: string;
932
- }>;
933
- }
934
- return db.prepare(`
935
- SELECT id, type, title, detail, importance, created_at, session_id
936
- FROM observations
937
- ORDER BY created_at_epoch DESC LIMIT ?
938
- `).all(limit) as Array<{
939
- id: number; type: string; title: string; detail: string | null;
940
- importance: number; created_at: string; session_id: string;
941
- }>;
942
- }
943
-
944
- /**
945
- * Get recent session summaries.
946
- */
947
- export function getSessionSummaries(db: Database.Database, limit: number = 10): Array<{
948
- session_id: string;
949
- request: string | null;
950
- completed: string | null;
951
- failed_attempts: string | null;
952
- plan_progress: string;
953
- created_at: string;
954
- }> {
955
- return db.prepare(`
956
- SELECT session_id, request, completed, failed_attempts, plan_progress, created_at
957
- FROM session_summaries
958
- ORDER BY created_at_epoch DESC LIMIT ?
959
- `).all(limit) as Array<{
960
- session_id: string; request: string | null; completed: string | null;
961
- failed_attempts: string | null; plan_progress: string; created_at: string;
962
- }>;
963
- }
964
-
965
- /**
966
- * Get complete timeline for a session.
967
- */
968
- export function getSessionTimeline(db: Database.Database, sessionId: string): {
969
- session: Record<string, unknown> | null;
970
- observations: Array<Record<string, unknown>>;
971
- summary: Record<string, unknown> | null;
972
- prompts: Array<Record<string, unknown>>;
973
- } {
974
- const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as Record<string, unknown> | undefined;
975
- const observations = db.prepare('SELECT * FROM observations WHERE session_id = ? ORDER BY created_at_epoch ASC').all(sessionId) as Array<Record<string, unknown>>;
976
- const summary = db.prepare('SELECT * FROM session_summaries WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1').get(sessionId) as Record<string, unknown> | undefined;
977
- const prompts = db.prepare('SELECT * FROM user_prompts WHERE session_id = ? ORDER BY prompt_number ASC').all(sessionId) as Array<Record<string, unknown>>;
978
-
979
- return {
980
- session: session ?? null,
981
- observations,
982
- summary: summary ?? null,
983
- prompts,
984
- };
985
- }
986
-
987
- /**
988
- * Get failed attempt observations.
989
- */
990
- export function getFailedAttempts(db: Database.Database, query?: string, limit: number = 20): Array<{
991
- id: number;
992
- title: string;
993
- detail: string | null;
994
- session_id: string;
995
- recurrence_count: number;
996
- created_at: string;
997
- }> {
998
- if (query) {
999
- return db.prepare(`
1000
- SELECT o.id, o.title, o.detail, o.session_id, o.recurrence_count, o.created_at
1001
- FROM observations_fts
1002
- JOIN observations o ON observations_fts.rowid = o.id
1003
- WHERE observations_fts MATCH ? AND o.type = 'failed_attempt'
1004
- ORDER BY o.recurrence_count DESC, rank LIMIT ?
1005
- `).all(sanitizeFts5Query(query), limit) as Array<{
1006
- id: number; title: string; detail: string | null; session_id: string;
1007
- recurrence_count: number; created_at: string;
1008
- }>;
1009
- }
1010
- return db.prepare(`
1011
- SELECT id, title, detail, session_id, recurrence_count, created_at
1012
- FROM observations WHERE type = 'failed_attempt'
1013
- ORDER BY recurrence_count DESC, created_at_epoch DESC LIMIT ?
1014
- `).all(limit) as Array<{
1015
- id: number; title: string; detail: string | null; session_id: string;
1016
- recurrence_count: number; created_at: string;
1017
- }>;
1018
- }
1019
-
1020
- /**
1021
- * Search decision observations.
1022
- */
1023
- export function getDecisionsAbout(db: Database.Database, query: string, limit: number = 20): Array<{
1024
- id: number;
1025
- title: string;
1026
- detail: string | null;
1027
- session_id: string;
1028
- created_at: string;
1029
- }> {
1030
- return db.prepare(`
1031
- SELECT o.id, o.title, o.detail, o.session_id, o.created_at
1032
- FROM observations_fts
1033
- JOIN observations o ON observations_fts.rowid = o.id
1034
- WHERE observations_fts MATCH ? AND o.type = 'decision'
1035
- ORDER BY rank LIMIT ?
1036
- `).all(sanitizeFts5Query(query), limit) as Array<{
1037
- id: number; title: string; detail: string | null; session_id: string;
1038
- created_at: string;
1039
- }>;
1040
- }
1041
-
1042
- /**
1043
- * Delete observations older than retention period.
1044
- */
1045
- export function pruneOldObservations(db: Database.Database, retentionDays: number = 90): number {
1046
- const cutoffEpoch = Math.floor(Date.now() / 1000) - (retentionDays * 86400);
1047
- const result = db.prepare('DELETE FROM observations WHERE created_at_epoch < ?').run(cutoffEpoch);
1048
- return result.changes;
1049
- }
1050
-
1051
- /**
1052
- * Deduplicate failed attempts across sessions.
1053
- * If the same failure title exists, increment recurrence_count instead of creating a duplicate.
1054
- */
1055
- export function deduplicateFailedAttempt(
1056
- db: Database.Database,
1057
- sessionId: string,
1058
- title: string,
1059
- detail: string | null,
1060
- opts?: AddObservationOpts
1061
- ): number {
1062
- // Check if a similar failed_attempt already exists (across all sessions)
1063
- const existing = db.prepare(`
1064
- SELECT id, recurrence_count FROM observations
1065
- WHERE type = 'failed_attempt' AND title = ?
1066
- ORDER BY created_at_epoch DESC LIMIT 1
1067
- `).get(title) as { id: number; recurrence_count: number } | undefined;
1068
-
1069
- if (existing) {
1070
- // Increment recurrence count and update detail if newer
1071
- db.prepare('UPDATE observations SET recurrence_count = recurrence_count + 1, detail = COALESCE(?, detail) WHERE id = ?')
1072
- .run(detail, existing.id);
1073
- return existing.id;
1074
- }
1075
-
1076
- // New failed attempt
1077
- return addObservation(db, sessionId, 'failed_attempt', title, detail, {
1078
- ...opts,
1079
- importance: 5,
1080
- });
1081
- }
1082
-
1083
- /**
1084
- * Get all sessions linked to a task/plan.
1085
- */
1086
- export function getSessionsByTask(db: Database.Database, taskId: string): Array<{
1087
- session_id: string;
1088
- status: string;
1089
- started_at: string;
1090
- ended_at: string | null;
1091
- plan_phase: string | null;
1092
- }> {
1093
- return db.prepare(`
1094
- SELECT session_id, status, started_at, ended_at, plan_phase
1095
- FROM sessions WHERE task_id = ?
1096
- ORDER BY started_at_epoch DESC
1097
- `).all(taskId) as Array<{
1098
- session_id: string; status: string; started_at: string;
1099
- ended_at: string | null; plan_phase: string | null;
1100
- }>;
1101
- }
1102
-
1103
- /**
1104
- * Aggregate plan_progress across all sessions for a task.
1105
- */
1106
- export function getCrossTaskProgress(db: Database.Database, taskId: string): Record<string, string> {
1107
- const sessions = db.prepare(`
1108
- SELECT session_id FROM sessions WHERE task_id = ?
1109
- `).all(taskId) as Array<{ session_id: string }>;
1110
-
1111
- const merged: Record<string, string> = {};
1112
- for (const session of sessions) {
1113
- const summaries = db.prepare(`
1114
- SELECT plan_progress FROM session_summaries WHERE session_id = ?
1115
- `).all(session.session_id) as Array<{ plan_progress: string }>;
1116
-
1117
- for (const summary of summaries) {
1118
- try {
1119
- const progress = JSON.parse(summary.plan_progress) as Record<string, string>;
1120
- for (const [key, value] of Object.entries(progress)) {
1121
- // Later status wins (complete > in_progress > pending)
1122
- if (!merged[key] || value === 'complete' || (value === 'in_progress' && merged[key] === 'pending')) {
1123
- merged[key] = value;
1124
- }
1125
- }
1126
- } catch (_e) {
1127
- // Skip invalid JSON
1128
- }
1129
- }
1130
- }
1131
-
1132
- return merged;
1133
- }
1134
-
1135
- /**
1136
- * Set task_id on a session for multi-session task linking.
1137
- */
1138
- export function linkSessionToTask(db: Database.Database, sessionId: string, taskId: string): void {
1139
- db.prepare('UPDATE sessions SET task_id = ? WHERE session_id = ?').run(taskId, sessionId);
1140
- }
1141
-
1142
- // ============================================================
1143
- // Observability Functions (P2-002, P2-003, P4-001)
1144
- // ============================================================
1145
-
1146
- /**
1147
- * Insert a conversation turn into the observability table.
1148
- * Returns the new row ID.
1149
- */
1150
- export function addConversationTurn(
1151
- db: Database.Database,
1152
- sessionId: string,
1153
- turnNumber: number,
1154
- userPrompt: string,
1155
- assistantResponse: string | null,
1156
- toolCallsJson: string | null,
1157
- toolCallCount: number,
1158
- promptTokens: number,
1159
- responseTokens: number
1160
- ): number {
1161
- const result = db.prepare(`
1162
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
1163
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1164
- `).run(
1165
- sessionId, turnNumber, userPrompt,
1166
- assistantResponse ? assistantResponse.slice(0, 10000) : null,
1167
- toolCallsJson, toolCallCount, promptTokens, responseTokens
1168
- );
1169
- return Number(result.lastInsertRowid);
1170
- }
1171
-
1172
- /**
1173
- * Insert a tool call detail record.
1174
- */
1175
- export function addToolCallDetail(
1176
- db: Database.Database,
1177
- sessionId: string,
1178
- turnNumber: number,
1179
- toolName: string,
1180
- inputSummary: string | null,
1181
- inputSize: number,
1182
- outputSize: number,
1183
- success: boolean,
1184
- filesInvolved?: string[]
1185
- ): void {
1186
- db.prepare(`
1187
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
1188
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1189
- `).run(
1190
- sessionId, turnNumber, toolName,
1191
- inputSummary ? inputSummary.slice(0, 500) : null,
1192
- inputSize, outputSize, success ? 1 : 0,
1193
- filesInvolved ? JSON.stringify(filesInvolved) : null
1194
- );
1195
- }
1196
-
1197
- /**
1198
- * Get the last processed line number for incremental transcript parsing.
1199
- */
1200
- export function getLastProcessedLine(db: Database.Database, sessionId: string): number {
1201
- const row = db.prepare('SELECT value FROM memory_meta WHERE key = ?').get(`last_processed_line:${sessionId}`) as { value: string } | undefined;
1202
- return row ? parseInt(row.value, 10) : 0;
1203
- }
1204
-
1205
- /**
1206
- * Set the last processed line number for incremental transcript parsing.
1207
- */
1208
- export function setLastProcessedLine(db: Database.Database, sessionId: string, lineNumber: number): void {
1209
- db.prepare('INSERT OR REPLACE INTO memory_meta (key, value) VALUES (?, ?)').run(`last_processed_line:${sessionId}`, String(lineNumber));
1210
- }
1211
-
1212
- /**
1213
- * Delete conversation turns and tool call details older than retention period.
1214
- */
1215
- export function pruneOldConversationTurns(db: Database.Database, retentionDays: number = 90): { turnsDeleted: number; detailsDeleted: number } {
1216
- const cutoffEpoch = Math.floor(Date.now() / 1000) - (retentionDays * 86400);
1217
- const turnsResult = db.prepare('DELETE FROM conversation_turns WHERE created_at_epoch < ?').run(cutoffEpoch);
1218
- const detailsResult = db.prepare('DELETE FROM tool_call_details WHERE created_at_epoch < ?').run(cutoffEpoch);
1219
- return { turnsDeleted: turnsResult.changes, detailsDeleted: detailsResult.changes };
1220
- }
1221
-
1222
- /**
1223
- * Get conversation turns for a session (for replay).
1224
- */
1225
- export function getConversationTurns(db: Database.Database, sessionId: string, opts?: {
1226
- turnFrom?: number;
1227
- turnTo?: number;
1228
- includeToolCalls?: boolean;
1229
- }): Array<{
1230
- id: number;
1231
- turn_number: number;
1232
- user_prompt: string;
1233
- assistant_response: string | null;
1234
- tool_calls_json: string | null;
1235
- tool_call_count: number;
1236
- prompt_tokens: number | null;
1237
- response_tokens: number | null;
1238
- created_at: string;
1239
- }> {
1240
- let sql = 'SELECT id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at FROM conversation_turns WHERE session_id = ?';
1241
- const params: (string | number)[] = [sessionId];
1242
-
1243
- if (opts?.turnFrom !== undefined) {
1244
- sql += ' AND turn_number >= ?';
1245
- params.push(opts.turnFrom);
1246
- }
1247
- if (opts?.turnTo !== undefined) {
1248
- sql += ' AND turn_number <= ?';
1249
- params.push(opts.turnTo);
1250
- }
1251
-
1252
- sql += ' ORDER BY turn_number ASC';
1253
-
1254
- return db.prepare(sql).all(...params) as Array<{
1255
- id: number; turn_number: number; user_prompt: string;
1256
- assistant_response: string | null; tool_calls_json: string | null;
1257
- tool_call_count: number; prompt_tokens: number | null;
1258
- response_tokens: number | null; created_at: string;
1259
- }>;
1260
- }
1261
-
1262
- /**
1263
- * Search conversation turns using FTS5.
1264
- */
1265
- export function searchConversationTurns(db: Database.Database, query: string, opts?: {
1266
- sessionId?: string;
1267
- dateFrom?: string;
1268
- dateTo?: string;
1269
- minToolCalls?: number;
1270
- limit?: number;
1271
- }): Array<{
1272
- id: number;
1273
- session_id: string;
1274
- turn_number: number;
1275
- user_prompt: string;
1276
- tool_call_count: number;
1277
- response_tokens: number | null;
1278
- created_at: string;
1279
- rank: number;
1280
- }> {
1281
- const limit = opts?.limit ?? 20;
1282
- let sql = `
1283
- SELECT ct.id, ct.session_id, ct.turn_number, ct.user_prompt, ct.tool_call_count, ct.response_tokens, ct.created_at, rank
1284
- FROM conversation_turns_fts
1285
- JOIN conversation_turns ct ON conversation_turns_fts.rowid = ct.id
1286
- WHERE conversation_turns_fts MATCH ?
1287
- `;
1288
- const params: (string | number)[] = [sanitizeFts5Query(query)];
1289
-
1290
- if (opts?.sessionId) {
1291
- sql += ' AND ct.session_id = ?';
1292
- params.push(opts.sessionId);
1293
- }
1294
- if (opts?.dateFrom) {
1295
- sql += ' AND ct.created_at >= ?';
1296
- params.push(opts.dateFrom);
1297
- }
1298
- if (opts?.dateTo) {
1299
- sql += ' AND ct.created_at <= ?';
1300
- params.push(opts.dateTo);
1301
- }
1302
- if (opts?.minToolCalls !== undefined) {
1303
- sql += ' AND ct.tool_call_count >= ?';
1304
- params.push(opts.minToolCalls);
1305
- }
1306
-
1307
- sql += ' ORDER BY rank LIMIT ?';
1308
- params.push(limit);
1309
-
1310
- return db.prepare(sql).all(...params) as Array<{
1311
- id: number; session_id: string; turn_number: number;
1312
- user_prompt: string; tool_call_count: number;
1313
- response_tokens: number | null; created_at: string; rank: number;
1314
- }>;
1315
- }
1316
-
1317
- /**
1318
- * Get tool usage patterns (aggregated stats).
1319
- */
1320
- export function getToolPatterns(db: Database.Database, opts?: {
1321
- sessionId?: string;
1322
- toolName?: string;
1323
- dateFrom?: string;
1324
- groupBy?: 'tool' | 'session' | 'day';
1325
- }): Array<Record<string, unknown>> {
1326
- const groupBy = opts?.groupBy ?? 'tool';
1327
- const params: (string | number)[] = [];
1328
- let whereClause = '';
1329
- const conditions: string[] = [];
1330
-
1331
- if (opts?.sessionId) {
1332
- conditions.push('session_id = ?');
1333
- params.push(opts.sessionId);
1334
- }
1335
- if (opts?.toolName) {
1336
- conditions.push('tool_name = ?');
1337
- params.push(opts.toolName);
1338
- }
1339
- if (opts?.dateFrom) {
1340
- conditions.push('created_at >= ?');
1341
- params.push(opts.dateFrom);
1342
- }
1343
-
1344
- if (conditions.length > 0) {
1345
- whereClause = 'WHERE ' + conditions.join(' AND ');
1346
- }
1347
-
1348
- let sql: string;
1349
- switch (groupBy) {
1350
- case 'session':
1351
- sql = `SELECT session_id, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1352
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1353
- SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1354
- AVG(tool_output_size) as avg_output_size
1355
- FROM tool_call_details ${whereClause}
1356
- GROUP BY session_id ORDER BY call_count DESC`;
1357
- break;
1358
- case 'day':
1359
- sql = `SELECT date(created_at) as day, COUNT(*) as call_count, COUNT(DISTINCT tool_name) as unique_tools,
1360
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes
1361
- FROM tool_call_details ${whereClause}
1362
- GROUP BY date(created_at) ORDER BY day DESC`;
1363
- break;
1364
- default: // 'tool'
1365
- sql = `SELECT tool_name, COUNT(*) as call_count,
1366
- SUM(CASE WHEN tool_success = 1 THEN 1 ELSE 0 END) as successes,
1367
- SUM(CASE WHEN tool_success = 0 THEN 1 ELSE 0 END) as failures,
1368
- AVG(tool_output_size) as avg_output_size,
1369
- AVG(tool_input_size) as avg_input_size
1370
- FROM tool_call_details ${whereClause}
1371
- GROUP BY tool_name ORDER BY call_count DESC`;
1372
- break;
1373
- }
1374
-
1375
- return db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
1376
- }
1377
-
1378
- /**
1379
- * Get session stats for observability.
1380
- */
1381
- export function getSessionStats(db: Database.Database, opts?: {
1382
- sessionId?: string;
1383
- limit?: number;
1384
- }): Array<Record<string, unknown>> {
1385
- if (opts?.sessionId) {
1386
- // Single session stats
1387
- const turns = db.prepare('SELECT COUNT(*) as turn_count, SUM(tool_call_count) as total_tool_calls, SUM(prompt_tokens) as total_prompt_tokens, SUM(response_tokens) as total_response_tokens FROM conversation_turns WHERE session_id = ?').get(opts.sessionId) as Record<string, unknown>;
1388
- const toolBreakdown = db.prepare('SELECT tool_name, COUNT(*) as count FROM tool_call_details WHERE session_id = ? GROUP BY tool_name ORDER BY count DESC').all(opts.sessionId) as Array<Record<string, unknown>>;
1389
- const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(opts.sessionId) as Record<string, unknown> | undefined;
1390
-
1391
- return [{
1392
- session_id: opts.sessionId,
1393
- status: session?.status ?? 'unknown',
1394
- started_at: session?.started_at ?? null,
1395
- ended_at: session?.ended_at ?? null,
1396
- ...turns,
1397
- tool_breakdown: toolBreakdown,
1398
- }];
1399
- }
1400
-
1401
- const limit = opts?.limit ?? 10;
1402
- return db.prepare(`
1403
- SELECT s.session_id, s.status, s.started_at, s.ended_at,
1404
- COUNT(ct.id) as turn_count,
1405
- COALESCE(SUM(ct.tool_call_count), 0) as total_tool_calls,
1406
- COALESCE(SUM(ct.prompt_tokens), 0) as total_prompt_tokens,
1407
- COALESCE(SUM(ct.response_tokens), 0) as total_response_tokens
1408
- FROM sessions s
1409
- LEFT JOIN conversation_turns ct ON s.session_id = ct.session_id
1410
- GROUP BY s.session_id
1411
- ORDER BY s.started_at_epoch DESC
1412
- LIMIT ?
1413
- `).all(limit) as Array<Record<string, unknown>>;
1414
- }
1415
-
1416
- /**
1417
- * Get database size information for observability monitoring.
1418
- */
1419
- export function getObservabilityDbSize(db: Database.Database): {
1420
- conversation_turns_count: number;
1421
- tool_call_details_count: number;
1422
- observations_count: number;
1423
- db_page_count: number;
1424
- db_page_size: number;
1425
- estimated_size_mb: number;
1426
- } {
1427
- const turnsCount = (db.prepare('SELECT COUNT(*) as c FROM conversation_turns').get() as { c: number }).c;
1428
- const detailsCount = (db.prepare('SELECT COUNT(*) as c FROM tool_call_details').get() as { c: number }).c;
1429
- const obsCount = (db.prepare('SELECT COUNT(*) as c FROM observations').get() as { c: number }).c;
1430
- const pageCount = (db.pragma('page_count') as Array<{ page_count: number }>)[0]?.page_count ?? 0;
1431
- const pageSize = (db.pragma('page_size') as Array<{ page_size: number }>)[0]?.page_size ?? 4096;
1432
-
1433
- return {
1434
- conversation_turns_count: turnsCount,
1435
- tool_call_details_count: detailsCount,
1436
- observations_count: obsCount,
1437
- db_page_count: pageCount,
1438
- db_page_size: pageSize,
1439
- estimated_size_mb: Math.round((pageCount * pageSize) / (1024 * 1024) * 100) / 100,
1440
- };
1441
- }
1442
-
1443
- // ============================================================
1444
- // Failure Classification: CRUD functions
1445
- // ============================================================
1446
-
1447
- export interface FailureClass {
1448
- id: number;
1449
- name: string;
1450
- description: string;
1451
- diff_patterns: string[];
1452
- file_patterns: string[];
1453
- prompt_keywords: string[];
1454
- incidents: string[];
1455
- rules: string[];
1456
- scanner_checks: string[];
1457
- known_message: string;
1458
- needs_review: boolean;
1459
- }
1460
-
1461
- export interface AddFailureClassOpts {
1462
- name: string;
1463
- description: string;
1464
- diffPatterns?: string[];
1465
- filePatterns?: string[];
1466
- promptKeywords?: string[];
1467
- incidents?: string[];
1468
- rules?: string[];
1469
- scannerChecks?: string[];
1470
- knownMessage?: string;
1471
- needsReview?: boolean;
1472
- }
1473
-
1474
- /**
1475
- * Add a new failure class to the taxonomy.
1476
- */
1477
- export function addFailureClass(db: Database.Database, opts: AddFailureClassOpts): number {
1478
- const result = db.prepare(`
1479
- INSERT OR IGNORE INTO failure_classes (name, description, diff_patterns, file_patterns, prompt_keywords, incidents, rules, scanner_checks, known_message, needs_review)
1480
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1481
- `).run(
1482
- opts.name,
1483
- opts.description,
1484
- JSON.stringify(opts.diffPatterns ?? []),
1485
- JSON.stringify(opts.filePatterns ?? []),
1486
- JSON.stringify(opts.promptKeywords ?? []),
1487
- JSON.stringify(opts.incidents ?? []),
1488
- JSON.stringify(opts.rules ?? []),
1489
- JSON.stringify(opts.scannerChecks ?? []),
1490
- opts.knownMessage ?? '',
1491
- opts.needsReview ? 1 : 0
1492
- );
1493
- return Number(result.lastInsertRowid);
1494
- }
1495
-
1496
- /**
1497
- * Get all failure classes from the taxonomy.
1498
- */
1499
- export function getFailureClasses(db: Database.Database): FailureClass[] {
1500
- const rows = db.prepare('SELECT * FROM failure_classes ORDER BY name LIMIT 10000').all() as Array<Record<string, unknown>>;
1501
- return rows.map(row => ({
1502
- id: row.id as number,
1503
- name: row.name as string,
1504
- description: row.description as string,
1505
- diff_patterns: JSON.parse((row.diff_patterns as string) || '[]'),
1506
- file_patterns: JSON.parse((row.file_patterns as string) || '[]'),
1507
- prompt_keywords: JSON.parse((row.prompt_keywords as string) || '[]'),
1508
- incidents: JSON.parse((row.incidents as string) || '[]'),
1509
- rules: JSON.parse((row.rules as string) || '[]'),
1510
- scanner_checks: JSON.parse((row.scanner_checks as string) || '[]'),
1511
- known_message: row.known_message as string,
1512
- needs_review: !!(row.needs_review as number),
1513
- }));
1514
- }
1515
-
1516
- /**
1517
- * Append an incident identifier to an existing failure class.
1518
- */
1519
- export function appendIncidentToFailureClass(db: Database.Database, className: string, incidentId: string): void {
1520
- const row = db.prepare('SELECT incidents FROM failure_classes WHERE name = ?').get(className) as { incidents: string } | undefined;
1521
- if (!row) return;
1522
- const incidents: string[] = JSON.parse(row.incidents || '[]');
1523
- if (!incidents.includes(incidentId)) {
1524
- incidents.push(incidentId);
1525
- db.prepare('UPDATE failure_classes SET incidents = ?, updated_at = datetime(\'now\') WHERE name = ?')
1526
- .run(JSON.stringify(incidents), className);
1527
- }
1528
- }
1529
-
1530
- export interface FailureClassMatch {
1531
- name: string;
1532
- score: number;
1533
- incidentCount: number;
1534
- rules: string[];
1535
- knownMessage: string;
1536
- }
1537
-
1538
- /**
1539
- * Score all failure classes against provided match text, file path, and prompt context.
1540
- * Returns the best match with its score.
1541
- */
1542
- export function scoreFailureClasses(
1543
- db: Database.Database,
1544
- matchText: string,
1545
- filePath: string,
1546
- promptContext: string,
1547
- weights?: { diffPatternWeight?: number; filePatternWeight?: number; promptKeywordWeight?: number }
1548
- ): FailureClassMatch | null {
1549
- const classes = getFailureClasses(db);
1550
- if (classes.length === 0) return null;
1551
-
1552
- const diffWeight = weights?.diffPatternWeight ?? 3;
1553
- const fileWeight = weights?.filePatternWeight ?? 2;
1554
- const promptWeight = weights?.promptKeywordWeight ?? 2;
1555
-
1556
- let bestMatch: FailureClassMatch | null = null;
1557
-
1558
- for (const fc of classes) {
1559
- let score = 0;
1560
-
1561
- for (const pattern of fc.diff_patterns) {
1562
- if (!pattern) continue;
1563
- try {
1564
- if (new RegExp(pattern, 'i').test(matchText)) {
1565
- score += diffWeight;
1566
- }
1567
- } catch {
1568
- if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
1569
- score += diffWeight;
1570
- }
1571
- }
1572
- }
1573
-
1574
- for (const pattern of fc.file_patterns) {
1575
- if (!pattern) continue;
1576
- try {
1577
- if (new RegExp(pattern).test(filePath)) {
1578
- score += fileWeight;
1579
- }
1580
- } catch {
1581
- if (filePath.includes(pattern)) {
1582
- score += fileWeight;
1583
- }
1584
- }
1585
- }
1586
-
1587
- if (promptContext) {
1588
- for (const keyword of fc.prompt_keywords) {
1589
- if (!keyword) continue;
1590
- try {
1591
- if (new RegExp(keyword, 'i').test(promptContext)) {
1592
- score += promptWeight;
1593
- }
1594
- } catch {
1595
- if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
1596
- score += promptWeight;
1597
- }
1598
- }
1599
- }
1600
- }
1601
-
1602
- if (!bestMatch || score > bestMatch.score) {
1603
- bestMatch = {
1604
- name: fc.name,
1605
- score,
1606
- incidentCount: fc.incidents.length,
1607
- rules: fc.rules,
1608
- knownMessage: fc.known_message,
1609
- };
1610
- }
1611
- }
1612
-
1613
- return bestMatch;
1614
- }