@mainahq/core 0.2.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 (156) hide show
  1. package/README.md +31 -0
  2. package/package.json +37 -0
  3. package/src/ai/__tests__/ai.test.ts +207 -0
  4. package/src/ai/__tests__/design-approaches.test.ts +192 -0
  5. package/src/ai/__tests__/spec-questions.test.ts +191 -0
  6. package/src/ai/__tests__/tiers.test.ts +110 -0
  7. package/src/ai/commit-msg.ts +28 -0
  8. package/src/ai/design-approaches.ts +76 -0
  9. package/src/ai/index.ts +205 -0
  10. package/src/ai/pr-summary.ts +60 -0
  11. package/src/ai/spec-questions.ts +74 -0
  12. package/src/ai/tiers.ts +52 -0
  13. package/src/ai/try-generate.ts +89 -0
  14. package/src/ai/validate.ts +66 -0
  15. package/src/benchmark/__tests__/reporter.test.ts +525 -0
  16. package/src/benchmark/__tests__/runner.test.ts +113 -0
  17. package/src/benchmark/__tests__/story-loader.test.ts +152 -0
  18. package/src/benchmark/reporter.ts +332 -0
  19. package/src/benchmark/runner.ts +91 -0
  20. package/src/benchmark/story-loader.ts +88 -0
  21. package/src/benchmark/types.ts +95 -0
  22. package/src/cache/__tests__/keys.test.ts +97 -0
  23. package/src/cache/__tests__/manager.test.ts +312 -0
  24. package/src/cache/__tests__/ttl.test.ts +94 -0
  25. package/src/cache/keys.ts +44 -0
  26. package/src/cache/manager.ts +231 -0
  27. package/src/cache/ttl.ts +77 -0
  28. package/src/config/__tests__/config.test.ts +376 -0
  29. package/src/config/index.ts +198 -0
  30. package/src/context/__tests__/budget.test.ts +179 -0
  31. package/src/context/__tests__/engine.test.ts +163 -0
  32. package/src/context/__tests__/episodic.test.ts +291 -0
  33. package/src/context/__tests__/relevance.test.ts +323 -0
  34. package/src/context/__tests__/retrieval.test.ts +143 -0
  35. package/src/context/__tests__/selector.test.ts +174 -0
  36. package/src/context/__tests__/semantic.test.ts +252 -0
  37. package/src/context/__tests__/treesitter.test.ts +229 -0
  38. package/src/context/__tests__/working.test.ts +236 -0
  39. package/src/context/budget.ts +130 -0
  40. package/src/context/engine.ts +394 -0
  41. package/src/context/episodic.ts +251 -0
  42. package/src/context/relevance.ts +325 -0
  43. package/src/context/retrieval.ts +325 -0
  44. package/src/context/selector.ts +93 -0
  45. package/src/context/semantic.ts +331 -0
  46. package/src/context/treesitter.ts +216 -0
  47. package/src/context/working.ts +192 -0
  48. package/src/db/__tests__/db.test.ts +151 -0
  49. package/src/db/index.ts +211 -0
  50. package/src/db/schema.ts +84 -0
  51. package/src/design/__tests__/design.test.ts +310 -0
  52. package/src/design/__tests__/generate-hld-lld.test.ts +109 -0
  53. package/src/design/__tests__/review.test.ts +561 -0
  54. package/src/design/index.ts +297 -0
  55. package/src/design/review.ts +327 -0
  56. package/src/explain/__tests__/explain.test.ts +173 -0
  57. package/src/explain/index.ts +181 -0
  58. package/src/features/__tests__/analyzer.test.ts +358 -0
  59. package/src/features/__tests__/checklist.test.ts +454 -0
  60. package/src/features/__tests__/numbering.test.ts +319 -0
  61. package/src/features/__tests__/quality.test.ts +295 -0
  62. package/src/features/__tests__/traceability.test.ts +147 -0
  63. package/src/features/analyzer.ts +445 -0
  64. package/src/features/checklist.ts +366 -0
  65. package/src/features/index.ts +18 -0
  66. package/src/features/numbering.ts +404 -0
  67. package/src/features/quality.ts +349 -0
  68. package/src/features/test-stubs.ts +157 -0
  69. package/src/features/traceability.ts +260 -0
  70. package/src/feedback/__tests__/async-feedback.test.ts +52 -0
  71. package/src/feedback/__tests__/collector.test.ts +219 -0
  72. package/src/feedback/__tests__/compress.test.ts +150 -0
  73. package/src/feedback/__tests__/preferences.test.ts +169 -0
  74. package/src/feedback/collector.ts +135 -0
  75. package/src/feedback/compress.ts +92 -0
  76. package/src/feedback/preferences.ts +108 -0
  77. package/src/git/__tests__/git.test.ts +62 -0
  78. package/src/git/index.ts +110 -0
  79. package/src/hooks/__tests__/runner.test.ts +266 -0
  80. package/src/hooks/index.ts +8 -0
  81. package/src/hooks/runner.ts +130 -0
  82. package/src/index.ts +356 -0
  83. package/src/init/__tests__/init.test.ts +228 -0
  84. package/src/init/index.ts +364 -0
  85. package/src/language/__tests__/detect.test.ts +77 -0
  86. package/src/language/__tests__/profile.test.ts +51 -0
  87. package/src/language/detect.ts +70 -0
  88. package/src/language/profile.ts +110 -0
  89. package/src/prompts/__tests__/defaults.test.ts +52 -0
  90. package/src/prompts/__tests__/engine.test.ts +183 -0
  91. package/src/prompts/__tests__/evolution-resolve.test.ts +169 -0
  92. package/src/prompts/__tests__/evolution.test.ts +187 -0
  93. package/src/prompts/__tests__/loader.test.ts +105 -0
  94. package/src/prompts/candidates/review-v2.md +55 -0
  95. package/src/prompts/defaults/ai-review.md +49 -0
  96. package/src/prompts/defaults/commit.md +30 -0
  97. package/src/prompts/defaults/context.md +26 -0
  98. package/src/prompts/defaults/design-approaches.md +57 -0
  99. package/src/prompts/defaults/design-hld-lld.md +55 -0
  100. package/src/prompts/defaults/design.md +53 -0
  101. package/src/prompts/defaults/explain.md +31 -0
  102. package/src/prompts/defaults/fix.md +32 -0
  103. package/src/prompts/defaults/index.ts +38 -0
  104. package/src/prompts/defaults/review.md +41 -0
  105. package/src/prompts/defaults/spec-questions.md +59 -0
  106. package/src/prompts/defaults/tests.md +72 -0
  107. package/src/prompts/engine.ts +137 -0
  108. package/src/prompts/evolution.ts +409 -0
  109. package/src/prompts/loader.ts +71 -0
  110. package/src/review/__tests__/review.test.ts +288 -0
  111. package/src/review/comprehensive.ts +362 -0
  112. package/src/review/index.ts +417 -0
  113. package/src/stats/__tests__/tracker.test.ts +323 -0
  114. package/src/stats/index.ts +11 -0
  115. package/src/stats/tracker.ts +492 -0
  116. package/src/ticket/__tests__/ticket.test.ts +273 -0
  117. package/src/ticket/index.ts +185 -0
  118. package/src/utils.ts +87 -0
  119. package/src/verify/__tests__/ai-review.test.ts +242 -0
  120. package/src/verify/__tests__/coverage.test.ts +83 -0
  121. package/src/verify/__tests__/detect.test.ts +175 -0
  122. package/src/verify/__tests__/diff-filter.test.ts +338 -0
  123. package/src/verify/__tests__/fix.test.ts +478 -0
  124. package/src/verify/__tests__/linters/clippy.test.ts +45 -0
  125. package/src/verify/__tests__/linters/go-vet.test.ts +27 -0
  126. package/src/verify/__tests__/linters/ruff.test.ts +64 -0
  127. package/src/verify/__tests__/mutation.test.ts +141 -0
  128. package/src/verify/__tests__/pipeline.test.ts +553 -0
  129. package/src/verify/__tests__/proof.test.ts +97 -0
  130. package/src/verify/__tests__/secretlint.test.ts +190 -0
  131. package/src/verify/__tests__/semgrep.test.ts +217 -0
  132. package/src/verify/__tests__/slop.test.ts +366 -0
  133. package/src/verify/__tests__/sonar.test.ts +113 -0
  134. package/src/verify/__tests__/syntax-guard.test.ts +227 -0
  135. package/src/verify/__tests__/trivy.test.ts +191 -0
  136. package/src/verify/__tests__/visual.test.ts +139 -0
  137. package/src/verify/ai-review.ts +276 -0
  138. package/src/verify/coverage.ts +134 -0
  139. package/src/verify/detect.ts +171 -0
  140. package/src/verify/diff-filter.ts +183 -0
  141. package/src/verify/fix.ts +317 -0
  142. package/src/verify/linters/clippy.ts +52 -0
  143. package/src/verify/linters/go-vet.ts +32 -0
  144. package/src/verify/linters/ruff.ts +47 -0
  145. package/src/verify/mutation.ts +143 -0
  146. package/src/verify/pipeline.ts +328 -0
  147. package/src/verify/proof.ts +277 -0
  148. package/src/verify/secretlint.ts +168 -0
  149. package/src/verify/semgrep.ts +170 -0
  150. package/src/verify/slop.ts +493 -0
  151. package/src/verify/sonar.ts +146 -0
  152. package/src/verify/syntax-guard.ts +251 -0
  153. package/src/verify/trivy.ts +161 -0
  154. package/src/verify/visual.ts +460 -0
  155. package/src/workflow/__tests__/context.test.ts +110 -0
  156. package/src/workflow/context.ts +81 -0
@@ -0,0 +1,492 @@
1
+ import type { Result } from "../db/index.ts";
2
+ import { getContextDb, getStatsDb } from "../db/index.ts";
3
+
4
+ export interface SnapshotInput {
5
+ branch: string;
6
+ commitHash: string;
7
+ verifyDurationMs: number;
8
+ totalDurationMs: number;
9
+ contextTokens: number;
10
+ contextBudget: number;
11
+ cacheHits: number;
12
+ cacheMisses: number;
13
+ findingsTotal: number;
14
+ findingsErrors: number;
15
+ findingsWarnings: number;
16
+ toolsRun: number;
17
+ syntaxPassed: boolean;
18
+ pipelinePassed: boolean;
19
+ skipped?: boolean;
20
+ }
21
+
22
+ export interface CommitSnapshot extends SnapshotInput {
23
+ id: string;
24
+ timestamp: string;
25
+ contextUtilization: number;
26
+ skipped: boolean;
27
+ }
28
+
29
+ export interface StatsReport {
30
+ totalCommits: number;
31
+ latest: CommitSnapshot | null;
32
+ averages: {
33
+ verifyDurationMs: number;
34
+ contextTokens: number;
35
+ cacheHitRate: number;
36
+ findingsPerCommit: number;
37
+ };
38
+ }
39
+
40
+ export type TrendDirection = "up" | "down" | "stable";
41
+
42
+ export interface TrendsReport {
43
+ verifyDuration: TrendDirection;
44
+ contextTokens: TrendDirection;
45
+ cacheHitRate: TrendDirection;
46
+ findingsPerCommit: TrendDirection;
47
+ window: number;
48
+ }
49
+
50
+ export interface ComparisonReport {
51
+ totalCommits: number;
52
+ /** Total findings caught by maina that would have shipped without it */
53
+ findingsCaught: number;
54
+ /** Total verification time invested */
55
+ totalVerifyTimeMs: number;
56
+ /** Average verify time per commit */
57
+ avgVerifyTimeMs: number;
58
+ /** Context tokens assembled (shows context engine doing useful work) */
59
+ totalContextTokens: number;
60
+ /** Episodic entries (shows memory growing) */
61
+ episodicEntries: number;
62
+ /** Semantic entities indexed */
63
+ semanticEntities: number;
64
+ /** Dependency edges mapped */
65
+ dependencyEdges: number;
66
+ /** Cache hits (tokens saved) */
67
+ cacheHits: number;
68
+ /** What raw git gives you: none of the above */
69
+ withoutMaina: {
70
+ findingsCaught: 0;
71
+ contextTokens: 0;
72
+ episodicMemory: 0;
73
+ verificationTools: 0;
74
+ cacheHits: 0;
75
+ };
76
+ }
77
+
78
+ interface RawSnapshotRow {
79
+ id: string;
80
+ timestamp: string;
81
+ branch: string;
82
+ commit_hash: string;
83
+ verify_duration_ms: number;
84
+ total_duration_ms: number;
85
+ context_tokens: number;
86
+ context_budget: number;
87
+ context_utilization: number;
88
+ cache_hits: number;
89
+ cache_misses: number;
90
+ findings_total: number;
91
+ findings_errors: number;
92
+ findings_warnings: number;
93
+ tools_run: number;
94
+ syntax_passed: number;
95
+ pipeline_passed: number;
96
+ skipped: number;
97
+ }
98
+
99
+ function rowToSnapshot(row: RawSnapshotRow): CommitSnapshot {
100
+ return {
101
+ id: row.id,
102
+ timestamp: row.timestamp,
103
+ branch: row.branch,
104
+ commitHash: row.commit_hash,
105
+ verifyDurationMs: row.verify_duration_ms,
106
+ totalDurationMs: row.total_duration_ms,
107
+ contextTokens: row.context_tokens,
108
+ contextBudget: row.context_budget,
109
+ contextUtilization: row.context_utilization,
110
+ cacheHits: row.cache_hits,
111
+ cacheMisses: row.cache_misses,
112
+ findingsTotal: row.findings_total,
113
+ findingsErrors: row.findings_errors,
114
+ findingsWarnings: row.findings_warnings,
115
+ toolsRun: row.tools_run,
116
+ syntaxPassed: row.syntax_passed === 1,
117
+ pipelinePassed: row.pipeline_passed === 1,
118
+ skipped: row.skipped === 1,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Records a commit snapshot to the stats database.
124
+ * Generates a UUID id and ISO timestamp automatically.
125
+ * Computes contextUtilization from contextTokens / contextBudget.
126
+ */
127
+ export function recordSnapshot(
128
+ mainaDir: string,
129
+ snapshot: SnapshotInput,
130
+ ): Result<void> {
131
+ try {
132
+ const dbResult = getStatsDb(mainaDir);
133
+ if (!dbResult.ok) return dbResult;
134
+
135
+ const { db } = dbResult.value;
136
+ const id = crypto.randomUUID();
137
+ const timestamp = new Date().toISOString();
138
+ const contextUtilization =
139
+ snapshot.contextBudget > 0
140
+ ? snapshot.contextTokens / snapshot.contextBudget
141
+ : 0;
142
+
143
+ db.prepare(
144
+ `INSERT INTO commit_snapshots (
145
+ id, timestamp, branch, commit_hash,
146
+ verify_duration_ms, total_duration_ms,
147
+ context_tokens, context_budget, context_utilization,
148
+ cache_hits, cache_misses,
149
+ findings_total, findings_errors, findings_warnings,
150
+ tools_run, syntax_passed, pipeline_passed, skipped
151
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
152
+ ).run(
153
+ id,
154
+ timestamp,
155
+ snapshot.branch,
156
+ snapshot.commitHash,
157
+ snapshot.verifyDurationMs,
158
+ snapshot.totalDurationMs,
159
+ snapshot.contextTokens,
160
+ snapshot.contextBudget,
161
+ contextUtilization,
162
+ snapshot.cacheHits,
163
+ snapshot.cacheMisses,
164
+ snapshot.findingsTotal,
165
+ snapshot.findingsErrors,
166
+ snapshot.findingsWarnings,
167
+ snapshot.toolsRun,
168
+ snapshot.syntaxPassed ? 1 : 0,
169
+ snapshot.pipelinePassed ? 1 : 0,
170
+ snapshot.skipped ? 1 : 0,
171
+ );
172
+
173
+ return { ok: true, value: undefined };
174
+ } catch (e) {
175
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Returns the most recent commit snapshot, or null if none exist.
181
+ */
182
+ export function getLatest(mainaDir: string): Result<CommitSnapshot | null> {
183
+ try {
184
+ const dbResult = getStatsDb(mainaDir);
185
+ if (!dbResult.ok) return dbResult;
186
+
187
+ const { db } = dbResult.value;
188
+ const row = db
189
+ .prepare("SELECT * FROM commit_snapshots ORDER BY rowid DESC LIMIT 1")
190
+ .get() as RawSnapshotRow | null;
191
+
192
+ if (!row) return { ok: true, value: null };
193
+ return { ok: true, value: rowToSnapshot(row) };
194
+ } catch (e) {
195
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Computes aggregate statistics over the last N snapshots.
201
+ * Returns total commit count, latest snapshot, and averages.
202
+ */
203
+ export function getStats(
204
+ mainaDir: string,
205
+ options?: { last?: number },
206
+ ): Result<StatsReport> {
207
+ try {
208
+ const dbResult = getStatsDb(mainaDir);
209
+ if (!dbResult.ok) return dbResult;
210
+
211
+ const { db } = dbResult.value;
212
+ const limit = options?.last ?? 10;
213
+
214
+ // Total count of all snapshots
215
+ const countRow = db
216
+ .prepare("SELECT COUNT(*) as cnt FROM commit_snapshots")
217
+ .get() as { cnt: number };
218
+ const totalCommits = countRow.cnt;
219
+
220
+ // Latest snapshot
221
+ const latestRow = db
222
+ .prepare("SELECT * FROM commit_snapshots ORDER BY rowid DESC LIMIT 1")
223
+ .get() as RawSnapshotRow | null;
224
+ const latest = latestRow ? rowToSnapshot(latestRow) : null;
225
+
226
+ // Last N snapshots for averages
227
+ const rows = db
228
+ .prepare("SELECT * FROM commit_snapshots ORDER BY rowid DESC LIMIT ?")
229
+ .all(limit) as RawSnapshotRow[];
230
+
231
+ let avgVerifyDurationMs = 0;
232
+ let avgContextTokens = 0;
233
+ let avgCacheHitRate = 0;
234
+ let avgFindingsPerCommit = 0;
235
+
236
+ if (rows.length > 0) {
237
+ let totalVerify = 0;
238
+ let totalTokens = 0;
239
+ let totalCacheHits = 0;
240
+ let totalCacheTotal = 0;
241
+ let totalFindings = 0;
242
+
243
+ for (const row of rows) {
244
+ totalVerify += row.verify_duration_ms;
245
+ totalTokens += row.context_tokens;
246
+ totalCacheHits += row.cache_hits;
247
+ totalCacheTotal += row.cache_hits + row.cache_misses;
248
+ totalFindings += row.findings_total;
249
+ }
250
+
251
+ avgVerifyDurationMs = totalVerify / rows.length;
252
+ avgContextTokens = totalTokens / rows.length;
253
+ avgCacheHitRate =
254
+ totalCacheTotal > 0 ? totalCacheHits / totalCacheTotal : 0;
255
+ avgFindingsPerCommit = totalFindings / rows.length;
256
+ }
257
+
258
+ return {
259
+ ok: true,
260
+ value: {
261
+ totalCommits,
262
+ latest,
263
+ averages: {
264
+ verifyDurationMs: avgVerifyDurationMs,
265
+ contextTokens: avgContextTokens,
266
+ cacheHitRate: avgCacheHitRate,
267
+ findingsPerCommit: avgFindingsPerCommit,
268
+ },
269
+ },
270
+ };
271
+ } catch (e) {
272
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
273
+ }
274
+ }
275
+
276
+ function computeTrend(recent: number, previous: number): TrendDirection {
277
+ if (previous === 0 && recent === 0) return "stable";
278
+ if (previous === 0) return "up";
279
+
280
+ const change = (recent - previous) / Math.abs(previous);
281
+ if (Math.abs(change) < 0.05) return "stable";
282
+ return change > 0 ? "up" : "down";
283
+ }
284
+
285
+ /**
286
+ * Compares averages of recent N snapshots vs previous N snapshots.
287
+ * Returns trend direction for each metric with a 5% threshold.
288
+ */
289
+ export function getTrends(
290
+ mainaDir: string,
291
+ options?: { window?: number },
292
+ ): Result<TrendsReport> {
293
+ try {
294
+ const dbResult = getStatsDb(mainaDir);
295
+ if (!dbResult.ok) return dbResult;
296
+
297
+ const { db } = dbResult.value;
298
+ const window = options?.window ?? 5;
299
+
300
+ const allStable: TrendsReport = {
301
+ verifyDuration: "stable",
302
+ contextTokens: "stable",
303
+ cacheHitRate: "stable",
304
+ findingsPerCommit: "stable",
305
+ window,
306
+ };
307
+
308
+ // Need at least 2*window snapshots
309
+ const countRow = db
310
+ .prepare("SELECT COUNT(*) as cnt FROM commit_snapshots")
311
+ .get() as { cnt: number };
312
+
313
+ if (countRow.cnt < 2 * window) {
314
+ return { ok: true, value: allStable };
315
+ }
316
+
317
+ // Fetch 2*window most recent snapshots
318
+ const rows = db
319
+ .prepare("SELECT * FROM commit_snapshots ORDER BY rowid DESC LIMIT ?")
320
+ .all(2 * window) as RawSnapshotRow[];
321
+
322
+ // Recent = first `window` rows, Previous = next `window` rows
323
+ const recentRows = rows.slice(0, window);
324
+ const previousRows = rows.slice(window, 2 * window);
325
+
326
+ function avgMetrics(subset: RawSnapshotRow[]) {
327
+ let totalVerify = 0;
328
+ let totalTokens = 0;
329
+ let totalCacheHits = 0;
330
+ let totalCacheTotal = 0;
331
+ let totalFindings = 0;
332
+
333
+ for (const row of subset) {
334
+ totalVerify += row.verify_duration_ms;
335
+ totalTokens += row.context_tokens;
336
+ totalCacheHits += row.cache_hits;
337
+ totalCacheTotal += row.cache_hits + row.cache_misses;
338
+ totalFindings += row.findings_total;
339
+ }
340
+
341
+ const n = subset.length;
342
+ return {
343
+ verifyDuration: n > 0 ? totalVerify / n : 0,
344
+ contextTokens: n > 0 ? totalTokens / n : 0,
345
+ cacheHitRate:
346
+ totalCacheTotal > 0 ? totalCacheHits / totalCacheTotal : 0,
347
+ findingsPerCommit: n > 0 ? totalFindings / n : 0,
348
+ };
349
+ }
350
+
351
+ const recent = avgMetrics(recentRows);
352
+ const previous = avgMetrics(previousRows);
353
+
354
+ return {
355
+ ok: true,
356
+ value: {
357
+ verifyDuration: computeTrend(
358
+ recent.verifyDuration,
359
+ previous.verifyDuration,
360
+ ),
361
+ contextTokens: computeTrend(
362
+ recent.contextTokens,
363
+ previous.contextTokens,
364
+ ),
365
+ cacheHitRate: computeTrend(recent.cacheHitRate, previous.cacheHitRate),
366
+ findingsPerCommit: computeTrend(
367
+ recent.findingsPerCommit,
368
+ previous.findingsPerCommit,
369
+ ),
370
+ window,
371
+ },
372
+ };
373
+ } catch (e) {
374
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Generate a comparison report: what maina provides vs raw git commit.
380
+ * Queries stats DB + context DB for comprehensive metrics.
381
+ */
382
+ export function getComparison(mainaDir: string): Result<ComparisonReport> {
383
+ try {
384
+ const dbResult = getStatsDb(mainaDir);
385
+ if (!dbResult.ok) {
386
+ return { ok: false, error: dbResult.error };
387
+ }
388
+
389
+ const db = dbResult.value.db;
390
+
391
+ const aggRow = db
392
+ .prepare(
393
+ `SELECT
394
+ COUNT(*) as total_commits,
395
+ SUM(findings_total) as total_findings,
396
+ SUM(verify_duration_ms) as total_verify_ms,
397
+ AVG(verify_duration_ms) as avg_verify_ms,
398
+ SUM(context_tokens) as total_context_tokens,
399
+ SUM(cache_hits) as total_cache_hits
400
+ FROM commit_snapshots`,
401
+ )
402
+ .get() as {
403
+ total_commits: number;
404
+ total_findings: number;
405
+ total_verify_ms: number;
406
+ avg_verify_ms: number;
407
+ total_context_tokens: number;
408
+ total_cache_hits: number;
409
+ } | null;
410
+
411
+ let episodicEntries = 0;
412
+ let semanticEntities = 0;
413
+ let dependencyEdges = 0;
414
+ try {
415
+ const ctxDb = getContextDb(mainaDir);
416
+ if (ctxDb.ok) {
417
+ const epCount = ctxDb.value.db
418
+ .prepare("SELECT COUNT(*) as c FROM episodic_entries")
419
+ .get() as { c: number } | null;
420
+ episodicEntries = epCount?.c ?? 0;
421
+
422
+ const seCount = ctxDb.value.db
423
+ .prepare("SELECT COUNT(*) as c FROM semantic_entities")
424
+ .get() as { c: number } | null;
425
+ semanticEntities = seCount?.c ?? 0;
426
+
427
+ const deCount = ctxDb.value.db
428
+ .prepare("SELECT COUNT(*) as c FROM dependency_edges")
429
+ .get() as { c: number } | null;
430
+ dependencyEdges = deCount?.c ?? 0;
431
+ }
432
+ } catch {
433
+ // Context DB not available
434
+ }
435
+
436
+ return {
437
+ ok: true,
438
+ value: {
439
+ totalCommits: aggRow?.total_commits ?? 0,
440
+ findingsCaught: aggRow?.total_findings ?? 0,
441
+ totalVerifyTimeMs: aggRow?.total_verify_ms ?? 0,
442
+ avgVerifyTimeMs: Math.round(aggRow?.avg_verify_ms ?? 0),
443
+ totalContextTokens: aggRow?.total_context_tokens ?? 0,
444
+ episodicEntries,
445
+ semanticEntities,
446
+ dependencyEdges,
447
+ cacheHits: aggRow?.total_cache_hits ?? 0,
448
+ withoutMaina: {
449
+ findingsCaught: 0,
450
+ contextTokens: 0,
451
+ episodicMemory: 0,
452
+ verificationTools: 0,
453
+ cacheHits: 0,
454
+ },
455
+ },
456
+ };
457
+ } catch (e) {
458
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Compute the skip rate from commit_snapshots.
464
+ * Returns total commits, skipped count, and rate (0-1).
465
+ */
466
+ export function getSkipRate(
467
+ mainaDir: string,
468
+ ): Result<{ total: number; skipped: number; rate: number }> {
469
+ try {
470
+ const dbResult = getStatsDb(mainaDir);
471
+ if (!dbResult.ok) return dbResult;
472
+
473
+ const { db } = dbResult.value;
474
+
475
+ const row = db
476
+ .prepare(
477
+ `SELECT
478
+ COUNT(*) as total,
479
+ SUM(CASE WHEN skipped = 1 THEN 1 ELSE 0 END) as skipped
480
+ FROM commit_snapshots`,
481
+ )
482
+ .get() as { total: number; skipped: number } | null;
483
+
484
+ const total = row?.total ?? 0;
485
+ const skipped = row?.skipped ?? 0;
486
+ const rate = total > 0 ? skipped / total : 0;
487
+
488
+ return { ok: true, value: { total, skipped, rate } };
489
+ } catch (e) {
490
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
491
+ }
492
+ }