@vitest-agent/sdk 1.0.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 (121) hide show
  1. package/DataReader-2kJ9I9Hh.d.ts +1809 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -0
  4. package/dispatch.d.ts +48 -0
  5. package/dispatch.js +5 -0
  6. package/errors/AgentErrors.js +65 -0
  7. package/errors/DataStoreError.js +44 -0
  8. package/errors/DiscoveryError.js +21 -0
  9. package/errors/PathResolutionError.js +27 -0
  10. package/errors/ProjectIdentityError.js +29 -0
  11. package/errors/RunContextError.js +35 -0
  12. package/errors/TddErrors.js +66 -0
  13. package/exit-code-for-tag.js +18 -0
  14. package/formatters/ci-annotations.js +61 -0
  15. package/formatters/gfm.js +20 -0
  16. package/formatters/json.js +18 -0
  17. package/formatters/markdown.js +39 -0
  18. package/formatters/silent.js +9 -0
  19. package/formatters/terminal.js +56 -0
  20. package/index.d.ts +3334 -0
  21. package/index.js +120 -0
  22. package/internal-inject-env.js +42 -0
  23. package/layers/ConfigLive.js +47 -0
  24. package/layers/DataReaderLive.js +1666 -0
  25. package/layers/DataStoreLive.js +1185 -0
  26. package/layers/DetailResolverLive.js +9 -0
  27. package/layers/DiscoveryRegistryLive.js +70 -0
  28. package/layers/EnvironmentDetectorLive.js +21 -0
  29. package/layers/EnvironmentDetectorTest.js +13 -0
  30. package/layers/ExecutorResolverLive.js +9 -0
  31. package/layers/FormatSelectorLive.js +20 -0
  32. package/layers/HistoryTrackerLive.js +47 -0
  33. package/layers/HistoryTrackerTest.js +25 -0
  34. package/layers/LoggerLive.js +64 -0
  35. package/layers/OutputPipelineLive.js +13 -0
  36. package/layers/OutputRendererLive.js +28 -0
  37. package/layers/PathResolutionLive.js +36 -0
  38. package/layers/PerClientSessionMapLive.js +145 -0
  39. package/layers/ProjectDiscoveryLive.js +70 -0
  40. package/layers/ProjectDiscoveryTest.js +12 -0
  41. package/layers/ProjectIdentityLive.js +92 -0
  42. package/layers/RunContextLive.js +81 -0
  43. package/lib/format-triage.js +103 -0
  44. package/lib/format-wrapup.js +67 -0
  45. package/migrations/0001_initial.js +849 -0
  46. package/migrations/registry_0001_initial.js +39 -0
  47. package/migrations/session_map_0001_initial.js +61 -0
  48. package/package.json +64 -0
  49. package/schemas/Agent.js +42 -0
  50. package/schemas/AgentReport.js +71 -0
  51. package/schemas/Baselines.js +16 -0
  52. package/schemas/CacheManifest.js +27 -0
  53. package/schemas/ChannelEvent.js +138 -0
  54. package/schemas/Common.js +105 -0
  55. package/schemas/Config.js +28 -0
  56. package/schemas/Coverage.js +51 -0
  57. package/schemas/CoverageLevel.js +117 -0
  58. package/schemas/CoverageTargets.js +39 -0
  59. package/schemas/History.js +31 -0
  60. package/schemas/Identity.js +64 -0
  61. package/schemas/Options.js +57 -0
  62. package/schemas/RenderState.js +161 -0
  63. package/schemas/RunEvent.js +163 -0
  64. package/schemas/Tdd.js +44 -0
  65. package/schemas/Thresholds.js +30 -0
  66. package/schemas/Transport.js +16 -0
  67. package/schemas/Trends.js +23 -0
  68. package/schemas/turns/FileEditPayload.js +15 -0
  69. package/schemas/turns/HookFirePayload.js +13 -0
  70. package/schemas/turns/HypothesisPayload.js +13 -0
  71. package/schemas/turns/NotePayload.js +13 -0
  72. package/schemas/turns/ToolCallPayload.js +13 -0
  73. package/schemas/turns/ToolResultPayload.js +15 -0
  74. package/schemas/turns/UserPromptPayload.js +12 -0
  75. package/schemas/turns/index.js +15 -0
  76. package/services/Config.js +15 -0
  77. package/services/DataReader.js +8 -0
  78. package/services/DataStore.js +8 -0
  79. package/services/DetailResolver.js +8 -0
  80. package/services/DiscoveryRegistry.js +8 -0
  81. package/services/EnvironmentDetector.js +8 -0
  82. package/services/ExecutorResolver.js +8 -0
  83. package/services/FormatSelector.js +8 -0
  84. package/services/HistoryTracker.js +8 -0
  85. package/services/OutputRenderer.js +8 -0
  86. package/services/PerClientSessionMap.js +22 -0
  87. package/services/ProjectDiscovery.js +8 -0
  88. package/services/ProjectIdentity.js +63 -0
  89. package/services/RunContext.js +53 -0
  90. package/services/idempotency.js +57 -0
  91. package/sidecar-dispatch.js +84 -0
  92. package/sql/assemblers.js +59 -0
  93. package/testing/layers.js +21 -0
  94. package/testing.d.ts +25 -0
  95. package/testing.js +243 -0
  96. package/tsdoc-metadata.json +11 -0
  97. package/utils/ansi.js +49 -0
  98. package/utils/build-report.js +130 -0
  99. package/utils/canonicalize-git-url.js +66 -0
  100. package/utils/classify-test.js +22 -0
  101. package/utils/compress-lines.js +45 -0
  102. package/utils/compute-trend.js +88 -0
  103. package/utils/detect-pm.js +58 -0
  104. package/utils/detect-timeout.js +17 -0
  105. package/utils/ensure-migrated.js +52 -0
  106. package/utils/failure-signature.js +33 -0
  107. package/utils/format-console.js +219 -0
  108. package/utils/format-fatal-error.js +35 -0
  109. package/utils/format-gfm.js +161 -0
  110. package/utils/format-terminal.js +480 -0
  111. package/utils/function-boundary.js +75 -0
  112. package/utils/hyperlink.js +11 -0
  113. package/utils/match-vitest-command.js +88 -0
  114. package/utils/normalize-workspace-key.js +34 -0
  115. package/utils/probe-host-metadata.js +99 -0
  116. package/utils/resolve-data-path.js +66 -0
  117. package/utils/resolve-project-key-from-cwd.js +52 -0
  118. package/utils/resolve-workspace-key.js +33 -0
  119. package/utils/safe-filename.js +34 -0
  120. package/utils/validate-coverage-targets-shape.js +84 -0
  121. package/utils/validate-phase-transition.js +113 -0
@@ -0,0 +1,1666 @@
1
+ import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
2
+ import { DataReader } from "../services/DataReader.js";
3
+ import { Effect, Layer, Option } from "effect";
4
+ import { SqlClient } from "@effect/sql/SqlClient";
5
+
6
+ //#region src/layers/DataReaderLive.ts
7
+ /** @public */
8
+ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
9
+ const sql = yield* SqlClient;
10
+ const getLatestRun = (project) => Effect.gen(function* () {
11
+ yield* Effect.logDebug("getLatestRun").pipe(Effect.annotateLogs({ project }));
12
+ const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project
13
+ FROM test_runs
14
+ WHERE project = ${project}
15
+ ORDER BY timestamp DESC LIMIT 1`;
16
+ if (runs.length === 0) return Option.none();
17
+ const run = runs[0];
18
+ const failedModules = yield* sql`SELECT f.path as file_path, tm.state as module_state, tm.duration as module_duration, tm.id as module_id
19
+ FROM test_modules tm
20
+ JOIN files f ON f.id = tm.file_id
21
+ WHERE tm.run_id = ${run.id} AND tm.state = 'failed'`;
22
+ const moduleIds = failedModules.map((m) => m.module_id);
23
+ const allTestCases = moduleIds.length > 0 ? yield* sql`SELECT tc.module_id, tc.id as test_case_id, tc.name, tc.full_name, tc.state, tc.duration, tc.flaky, tc.slow, tc.classification
24
+ FROM test_cases tc WHERE tc.module_id IN ${sql.in(moduleIds)}` : [];
25
+ const allErrors = moduleIds.length > 0 ? yield* sql`SELECT te.test_case_id, te.module_id, te.scope, te.message, te.stack, te.diff
26
+ FROM test_errors te
27
+ WHERE te.run_id = ${run.id}
28
+ AND te.module_id IN ${sql.in(moduleIds)}` : [];
29
+ const testsByModule = /* @__PURE__ */ new Map();
30
+ for (const tc of allTestCases) {
31
+ const arr = testsByModule.get(tc.module_id);
32
+ if (arr) arr.push(tc);
33
+ else testsByModule.set(tc.module_id, [tc]);
34
+ }
35
+ const errorsByTestCase = /* @__PURE__ */ new Map();
36
+ const errorsByModule = /* @__PURE__ */ new Map();
37
+ for (const e of allErrors) if (e.scope === "test" && e.test_case_id != null) {
38
+ const arr = errorsByTestCase.get(e.test_case_id);
39
+ if (arr) arr.push(e);
40
+ else errorsByTestCase.set(e.test_case_id, [e]);
41
+ } else if (e.scope === "module" && e.module_id != null) {
42
+ const arr = errorsByModule.get(e.module_id);
43
+ if (arr) arr.push(e);
44
+ else errorsByModule.set(e.module_id, [e]);
45
+ }
46
+ const failedModuleReports = failedModules.map((mod) => {
47
+ const tests = testsByModule.get(mod.module_id) ?? [];
48
+ const modErrors = errorsByModule.get(mod.module_id) ?? [];
49
+ const testReports = tests.map((tc) => {
50
+ const tcErrors = errorsByTestCase.get(tc.test_case_id) ?? [];
51
+ return {
52
+ name: tc.name,
53
+ fullName: tc.full_name,
54
+ state: tc.state,
55
+ ...tc.duration != null ? { duration: tc.duration } : {},
56
+ ...tc.flaky === 1 ? { flaky: true } : {},
57
+ ...tc.slow === 1 ? { slow: true } : {},
58
+ ...tcErrors.length > 0 ? { errors: tcErrors.map((e) => ({
59
+ message: e.message,
60
+ ...e.stack != null ? { stack: e.stack } : {},
61
+ ...e.diff != null ? { diff: e.diff } : {}
62
+ })) } : {},
63
+ ...tc.classification != null ? { classification: tc.classification } : {}
64
+ };
65
+ });
66
+ return {
67
+ file: mod.file_path,
68
+ state: mod.module_state,
69
+ ...mod.module_duration != null ? { duration: mod.module_duration } : {},
70
+ ...modErrors.length > 0 ? { errors: modErrors.map((e) => ({
71
+ message: e.message,
72
+ ...e.stack != null ? { stack: e.stack } : {},
73
+ ...e.diff != null ? { diff: e.diff } : {}
74
+ })) } : {},
75
+ tests: testReports
76
+ };
77
+ });
78
+ const unhandledErrors = yield* sql`SELECT message, stack, diff FROM test_errors
79
+ WHERE run_id = ${run.id} AND scope = 'unhandled'`;
80
+ const failedFiles = failedModules.map((m) => m.file_path);
81
+ const projectName = run.project;
82
+ const fileCovRows = yield* sql`SELECT f.path AS file_path, fc.statements, fc.branches, fc.functions, fc.lines,
83
+ fc.uncovered_lines, fc.tier
84
+ FROM file_coverage fc
85
+ JOIN files f ON f.id = fc.file_id
86
+ WHERE fc.run_id = ${run.id}`;
87
+ const lowCoverage = fileCovRows.filter((r) => r.tier === "below_threshold").map((r) => ({
88
+ file: r.file_path,
89
+ summary: {
90
+ statements: r.statements,
91
+ branches: r.branches,
92
+ functions: r.functions,
93
+ lines: r.lines
94
+ },
95
+ uncoveredLines: r.uncovered_lines ?? ""
96
+ }));
97
+ const belowTarget = fileCovRows.filter((r) => r.tier === "below_target").map((r) => ({
98
+ file: r.file_path,
99
+ summary: {
100
+ statements: r.statements,
101
+ branches: r.branches,
102
+ functions: r.functions,
103
+ lines: r.lines
104
+ },
105
+ uncoveredLines: r.uncovered_lines ?? ""
106
+ }));
107
+ const trendRows = yield* sql`SELECT statements, branches, functions, lines FROM coverage_trends WHERE run_id = ${run.id} LIMIT 1`;
108
+ const totals = trendRows[0] ? {
109
+ statements: trendRows[0].statements,
110
+ branches: trendRows[0].branches,
111
+ functions: trendRows[0].functions,
112
+ lines: trendRows[0].lines
113
+ } : {
114
+ statements: 0,
115
+ branches: 0,
116
+ functions: 0,
117
+ lines: 0
118
+ };
119
+ const baselineRows = yield* sql`SELECT metric, value FROM coverage_baselines
120
+ WHERE project = '__global__' AND pattern = ''`;
121
+ const thresholds = {};
122
+ for (const b of baselineRows) if (b.metric === "lines") thresholds.lines = b.value;
123
+ else if (b.metric === "functions") thresholds.functions = b.value;
124
+ else if (b.metric === "branches") thresholds.branches = b.value;
125
+ else if (b.metric === "statements") thresholds.statements = b.value;
126
+ const coverage = fileCovRows.length > 0 || trendRows.length > 0 ? {
127
+ totals,
128
+ thresholds: {
129
+ global: thresholds,
130
+ patterns: []
131
+ },
132
+ scoped: false,
133
+ lowCoverage,
134
+ lowCoverageFiles: lowCoverage.map((f) => f.file),
135
+ ...belowTarget.length > 0 ? {
136
+ belowTarget,
137
+ belowTargetFiles: belowTarget.map((f) => f.file)
138
+ } : {}
139
+ } : void 0;
140
+ const report = {
141
+ timestamp: run.timestamp,
142
+ project: projectName,
143
+ reason: run.reason,
144
+ summary: {
145
+ total: run.total,
146
+ passed: run.passed,
147
+ failed: run.failed,
148
+ skipped: run.skipped,
149
+ duration: run.duration
150
+ },
151
+ failed: failedModuleReports,
152
+ unhandledErrors: unhandledErrors.map((e) => ({
153
+ message: e.message,
154
+ ...e.stack != null ? { stack: e.stack } : {},
155
+ ...e.diff != null ? { diff: e.diff } : {}
156
+ })),
157
+ failedFiles,
158
+ ...coverage ? { coverage } : {}
159
+ };
160
+ return Option.some(report);
161
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
162
+ operation: "read",
163
+ table: "test_runs",
164
+ reason: extractSqlReason(e)
165
+ })));
166
+ const getRunsByProject = () => Effect.gen(function* () {
167
+ yield* Effect.logDebug("getRunsByProject");
168
+ return (yield* sql`SELECT
169
+ t1.project,
170
+ t1.timestamp as last_run,
171
+ t1.reason as last_result,
172
+ t1.total,
173
+ t1.passed,
174
+ t1.failed,
175
+ t1.skipped
176
+ FROM test_runs t1
177
+ INNER JOIN (
178
+ SELECT project, MAX(timestamp) as max_ts
179
+ FROM test_runs
180
+ GROUP BY project
181
+ ) t2 ON t1.project = t2.project
182
+ AND t1.timestamp = t2.max_ts`).map((r) => ({
183
+ project: r.project,
184
+ lastRun: r.last_run,
185
+ lastResult: r.last_result,
186
+ total: r.total,
187
+ passed: r.passed,
188
+ failed: r.failed,
189
+ skipped: r.skipped
190
+ }));
191
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
192
+ operation: "read",
193
+ table: "test_runs",
194
+ reason: extractSqlReason(e)
195
+ })));
196
+ const getHistory = (project) => Effect.gen(function* () {
197
+ yield* Effect.logDebug("getHistory").pipe(Effect.annotateLogs({ project }));
198
+ const rows = yield* sql`SELECT full_name, timestamp, state
199
+ FROM test_history
200
+ WHERE project = ${project}
201
+ ORDER BY full_name, timestamp DESC`;
202
+ const testsMap = /* @__PURE__ */ new Map();
203
+ for (const row of rows) {
204
+ if (row.state !== "passed" && row.state !== "failed") continue;
205
+ const existing = testsMap.get(row.full_name);
206
+ if (existing) existing.push({
207
+ timestamp: row.timestamp,
208
+ state: row.state
209
+ });
210
+ else testsMap.set(row.full_name, [{
211
+ timestamp: row.timestamp,
212
+ state: row.state
213
+ }]);
214
+ }
215
+ const tests = Array.from(testsMap.entries()).map(([fullName, runs]) => ({
216
+ fullName,
217
+ runs
218
+ }));
219
+ return {
220
+ project,
221
+ updatedAt: tests.reduce((latest, t) => {
222
+ const ts = t.runs[0]?.timestamp ?? "";
223
+ return ts > latest ? ts : latest;
224
+ }, "") || (/* @__PURE__ */ new Date()).toISOString(),
225
+ tests
226
+ };
227
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
228
+ operation: "read",
229
+ table: "test_history",
230
+ reason: extractSqlReason(e)
231
+ })));
232
+ const getBaselines = (project) => Effect.gen(function* () {
233
+ yield* Effect.logDebug("getBaselines").pipe(Effect.annotateLogs({ project }));
234
+ const rows = yield* sql`SELECT metric, value, pattern, updated_at
235
+ FROM coverage_baselines
236
+ WHERE project = ${project}
237
+ ORDER BY pattern, metric`;
238
+ if (rows.length === 0) return Option.none();
239
+ const global = {};
240
+ const patternsMap = /* @__PURE__ */ new Map();
241
+ let updatedAt = rows[0].updated_at;
242
+ for (const row of rows) {
243
+ if (row.updated_at > updatedAt) updatedAt = row.updated_at;
244
+ if (row.pattern === "") global[row.metric] = row.value;
245
+ else {
246
+ const existing = patternsMap.get(row.pattern);
247
+ if (existing) existing[row.metric] = row.value;
248
+ else patternsMap.set(row.pattern, { [row.metric]: row.value });
249
+ }
250
+ }
251
+ const patterns = Array.from(patternsMap.entries());
252
+ const baselines = {
253
+ updatedAt,
254
+ global,
255
+ patterns
256
+ };
257
+ return Option.some(baselines);
258
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
259
+ operation: "read",
260
+ table: "coverage_baselines",
261
+ reason: extractSqlReason(e)
262
+ })));
263
+ const getTrends = (project, limit) => Effect.gen(function* () {
264
+ yield* Effect.logDebug("getTrends").pipe(Effect.annotateLogs({
265
+ project,
266
+ limit: limit ?? 50
267
+ }));
268
+ const rows = yield* sql`SELECT timestamp, lines, functions, branches, statements, direction, targets_hash
269
+ FROM coverage_trends
270
+ WHERE project = ${project}
271
+ ORDER BY timestamp DESC
272
+ LIMIT ${limit ?? 50}`;
273
+ if (rows.length === 0) return Option.none();
274
+ const chronological = [...rows].reverse();
275
+ const record = { entries: chronological.map((row, i) => {
276
+ const prev = i > 0 ? chronological[i - 1] : null;
277
+ return {
278
+ timestamp: row.timestamp,
279
+ coverage: {
280
+ lines: row.lines,
281
+ functions: row.functions,
282
+ branches: row.branches,
283
+ statements: row.statements
284
+ },
285
+ delta: prev ? {
286
+ lines: row.lines - prev.lines,
287
+ functions: row.functions - prev.functions,
288
+ branches: row.branches - prev.branches,
289
+ statements: row.statements - prev.statements
290
+ } : {
291
+ lines: 0,
292
+ functions: 0,
293
+ branches: 0,
294
+ statements: 0
295
+ },
296
+ direction: row.direction,
297
+ ...row.targets_hash != null ? { targetsHash: row.targets_hash } : {}
298
+ };
299
+ }) };
300
+ return Option.some(record);
301
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
302
+ operation: "read",
303
+ table: "coverage_trends",
304
+ reason: extractSqlReason(e)
305
+ })));
306
+ const getFlaky = (project) => Effect.gen(function* () {
307
+ yield* Effect.logDebug("getFlaky").pipe(Effect.annotateLogs({ project }));
308
+ return (yield* sql`SELECT
309
+ th1.full_name,
310
+ th1.project,
311
+ SUM(CASE WHEN th1.state = 'passed' THEN 1 ELSE 0 END) as pass_count,
312
+ SUM(CASE WHEN th1.state = 'failed' THEN 1 ELSE 0 END) as fail_count,
313
+ (SELECT state FROM test_history th2
314
+ WHERE th2.full_name = th1.full_name
315
+ AND th2.project = th1.project
316
+ ORDER BY timestamp DESC LIMIT 1) as last_state,
317
+ MAX(th1.timestamp) as last_timestamp
318
+ FROM test_history th1
319
+ WHERE th1.project = ${project}
320
+ GROUP BY th1.full_name, th1.project
321
+ -- Flaky means oscillation, not a clean recovery. A test with both
322
+ -- passes and fails is only flaky when at least one failure occurs at
323
+ -- or after the earliest pass (a fail-after-pass regression). A
324
+ -- monotonic red->green cycle — every failure precedes every pass — is
325
+ -- a recovery, not flakiness, so it is excluded here. ISO-8601
326
+ -- timestamps compare lexicographically.
327
+ HAVING pass_count > 0 AND fail_count > 0
328
+ AND MAX(CASE WHEN th1.state = 'failed' THEN th1.timestamp END)
329
+ >= MIN(CASE WHEN th1.state = 'passed' THEN th1.timestamp END)`).map((r) => ({
330
+ fullName: r.full_name,
331
+ project: r.project,
332
+ passCount: r.pass_count,
333
+ failCount: r.fail_count,
334
+ lastState: r.last_state,
335
+ lastTimestamp: r.last_timestamp
336
+ }));
337
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
338
+ operation: "read",
339
+ table: "test_history",
340
+ reason: extractSqlReason(e)
341
+ })));
342
+ const getPersistentFailures = (project) => Effect.gen(function* () {
343
+ yield* Effect.logDebug("getPersistentFailures").pipe(Effect.annotateLogs({ project }));
344
+ return (yield* sql`WITH ranked AS (
345
+ SELECT full_name, project, state, timestamp, error_message,
346
+ ROW_NUMBER() OVER (
347
+ PARTITION BY project, full_name
348
+ ORDER BY timestamp DESC
349
+ ) as rn
350
+ FROM test_history
351
+ WHERE project = ${project}
352
+ ),
353
+ streak AS (
354
+ SELECT full_name, project, state, timestamp, error_message, rn,
355
+ MIN(CASE WHEN state != 'failed' THEN rn END) OVER (
356
+ PARTITION BY project, full_name
357
+ ) as first_pass_rn
358
+ FROM ranked
359
+ )
360
+ SELECT
361
+ full_name, project,
362
+ COUNT(*) as consecutive_failures,
363
+ MIN(timestamp) as first_failed_at,
364
+ MAX(timestamp) as last_failed_at,
365
+ (SELECT error_message FROM streak s2
366
+ WHERE s2.full_name = streak.full_name
367
+ AND s2.project = streak.project
368
+ AND s2.rn = 1) as last_error_message
369
+ FROM streak
370
+ WHERE state = 'failed'
371
+ AND (first_pass_rn IS NULL OR rn < first_pass_rn)
372
+ GROUP BY full_name, project
373
+ HAVING consecutive_failures >= 2`).map((row) => ({
374
+ fullName: row.full_name,
375
+ project: row.project,
376
+ consecutiveFailures: row.consecutive_failures,
377
+ firstFailedAt: row.first_failed_at,
378
+ lastFailedAt: row.last_failed_at,
379
+ lastErrorMessage: row.last_error_message
380
+ }));
381
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
382
+ operation: "read",
383
+ table: "test_history",
384
+ reason: extractSqlReason(e)
385
+ })));
386
+ const getFileCoverage = (runId) => Effect.gen(function* () {
387
+ yield* Effect.logDebug("getFileCoverage").pipe(Effect.annotateLogs({ runId }));
388
+ return (yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
389
+ FROM file_coverage fc
390
+ JOIN files f ON f.id = fc.file_id
391
+ WHERE fc.run_id = ${runId}`).map((r) => ({
392
+ file: r.file_path,
393
+ summary: {
394
+ statements: r.statements,
395
+ branches: r.branches,
396
+ functions: r.functions,
397
+ lines: r.lines
398
+ },
399
+ uncoveredLines: r.uncovered_lines ?? ""
400
+ }));
401
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
402
+ operation: "read",
403
+ table: "file_coverage",
404
+ reason: extractSqlReason(e)
405
+ })));
406
+ const getCoverage = (project) => Effect.gen(function* () {
407
+ yield* Effect.logDebug("getCoverage").pipe(Effect.annotateLogs({ project }));
408
+ const runs = yield* sql`SELECT id, timestamp FROM test_runs
409
+ WHERE project = ${project}
410
+ ORDER BY timestamp DESC LIMIT 1`;
411
+ if (runs.length === 0) return Option.none();
412
+ const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
413
+ FROM file_coverage fc JOIN files f ON f.id = fc.file_id
414
+ WHERE fc.run_id = ${runs[0].id}`;
415
+ const trendRows = yield* sql`SELECT statements, branches, functions, lines
416
+ FROM coverage_trends
417
+ WHERE project = ${project}
418
+ ORDER BY timestamp DESC LIMIT 1`;
419
+ if (fileCoverageRows.length === 0 && trendRows.length === 0) return Option.none();
420
+ let totals;
421
+ if (trendRows.length > 0) totals = {
422
+ statements: trendRows[0].statements,
423
+ branches: trendRows[0].branches,
424
+ functions: trendRows[0].functions,
425
+ lines: trendRows[0].lines
426
+ };
427
+ else {
428
+ const count = fileCoverageRows.length;
429
+ totals = {
430
+ statements: fileCoverageRows.reduce((sum, r) => sum + r.statements, 0) / count,
431
+ branches: fileCoverageRows.reduce((sum, r) => sum + r.branches, 0) / count,
432
+ functions: fileCoverageRows.reduce((sum, r) => sum + r.functions, 0) / count,
433
+ lines: fileCoverageRows.reduce((sum, r) => sum + r.lines, 0) / count
434
+ };
435
+ }
436
+ const baselinesOpt = yield* getBaselines("__global__");
437
+ const baselines = Option.getOrElse(baselinesOpt, () => ({
438
+ updatedAt: "",
439
+ global: {},
440
+ patterns: []
441
+ }));
442
+ const lowCoverage = fileCoverageRows.map((r) => ({
443
+ file: r.file_path,
444
+ summary: {
445
+ statements: r.statements,
446
+ branches: r.branches,
447
+ functions: r.functions,
448
+ lines: r.lines
449
+ },
450
+ uncoveredLines: r.uncovered_lines ?? ""
451
+ }));
452
+ const report = {
453
+ totals,
454
+ thresholds: {
455
+ global: baselines.global,
456
+ patterns: baselines.patterns
457
+ },
458
+ scoped: false,
459
+ lowCoverage,
460
+ lowCoverageFiles: lowCoverage.map((f) => f.file)
461
+ };
462
+ return Option.some(report);
463
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
464
+ operation: "read",
465
+ table: "file_coverage",
466
+ reason: extractSqlReason(e)
467
+ })));
468
+ const getTestsForFile = (filePath) => Effect.gen(function* () {
469
+ yield* Effect.logDebug("getTestsForFile").pipe(Effect.annotateLogs({ filePath }));
470
+ return (yield* sql`SELECT DISTINCT f.path
471
+ FROM source_test_map stm
472
+ JOIN files sf ON sf.id = stm.source_file_id
473
+ JOIN test_modules tm ON tm.id = stm.test_module_id
474
+ JOIN files f ON f.id = tm.file_id
475
+ WHERE sf.path = ${filePath}
476
+ ORDER BY f.path`).map((r) => r.path);
477
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
478
+ operation: "read",
479
+ table: "source_test_map",
480
+ reason: extractSqlReason(e)
481
+ })));
482
+ const getErrors = (project, errorName) => Effect.gen(function* () {
483
+ yield* Effect.logDebug("getErrors").pipe(Effect.annotateLogs({
484
+ project,
485
+ errorName: errorName ?? null
486
+ }));
487
+ const runs = yield* sql`SELECT id FROM test_runs
488
+ WHERE project = ${project}
489
+ ORDER BY timestamp DESC LIMIT 1`;
490
+ if (runs.length === 0) return [];
491
+ const runId = runs[0].id;
492
+ return (yield* errorName ? sql`SELECT te.id, sf.id AS top_stack_frame_id,
493
+ te.name, te.message, te.diff, te.actual, te.expected, te.stack, te.scope,
494
+ tc.full_name as test_full_name,
495
+ f.path as module_file
496
+ FROM test_errors te
497
+ LEFT JOIN test_cases tc ON tc.id = te.test_case_id
498
+ LEFT JOIN test_modules tm ON tm.id = te.module_id
499
+ LEFT JOIN files f ON f.id = tm.file_id
500
+ LEFT JOIN stack_frames sf ON sf.error_id = te.id AND sf.ordinal = 0
501
+ WHERE te.run_id = ${runId} AND te.name = ${errorName}` : sql`SELECT te.id, sf.id AS top_stack_frame_id,
502
+ te.name, te.message, te.diff, te.actual, te.expected, te.stack, te.scope,
503
+ tc.full_name as test_full_name,
504
+ f.path as module_file
505
+ FROM test_errors te
506
+ LEFT JOIN test_cases tc ON tc.id = te.test_case_id
507
+ LEFT JOIN test_modules tm ON tm.id = te.module_id
508
+ LEFT JOIN files f ON f.id = tm.file_id
509
+ LEFT JOIN stack_frames sf ON sf.error_id = te.id AND sf.ordinal = 0
510
+ WHERE te.run_id = ${runId}`).map((r) => ({
511
+ id: r.id,
512
+ topStackFrameId: r.top_stack_frame_id,
513
+ name: r.name,
514
+ message: r.message,
515
+ diff: r.diff,
516
+ actual: r.actual,
517
+ expected: r.expected,
518
+ stack: r.stack,
519
+ scope: r.scope,
520
+ testFullName: r.test_full_name,
521
+ moduleFile: r.module_file
522
+ }));
523
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
524
+ operation: "read",
525
+ table: "test_errors",
526
+ reason: extractSqlReason(e)
527
+ })));
528
+ const getNotes = (scope, project, testFullName) => Effect.gen(function* () {
529
+ yield* Effect.logDebug("getNotes").pipe(Effect.annotateLogs({
530
+ scope: scope ?? null,
531
+ project: project ?? null
532
+ }));
533
+ if (scope && project && testFullName) return (yield* sql`SELECT * FROM notes
534
+ WHERE scope = ${scope} AND project = ${project} AND test_full_name = ${testFullName}
535
+ ORDER BY created_at DESC`).map(mapNoteRow);
536
+ if (scope && project) return (yield* sql`SELECT * FROM notes
537
+ WHERE scope = ${scope} AND project = ${project}
538
+ ORDER BY created_at DESC`).map(mapNoteRow);
539
+ if (scope) return (yield* sql`SELECT * FROM notes
540
+ WHERE scope = ${scope}
541
+ ORDER BY created_at DESC`).map(mapNoteRow);
542
+ if (project) return (yield* sql`SELECT * FROM notes
543
+ WHERE project = ${project}
544
+ ORDER BY created_at DESC`).map(mapNoteRow);
545
+ return (yield* sql`SELECT * FROM notes ORDER BY created_at DESC`).map(mapNoteRow);
546
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
547
+ operation: "read",
548
+ table: "notes",
549
+ reason: extractSqlReason(e)
550
+ })));
551
+ const getNoteById = (id) => Effect.gen(function* () {
552
+ yield* Effect.logDebug("getNoteById").pipe(Effect.annotateLogs({ id }));
553
+ const rows = yield* sql`SELECT * FROM notes WHERE id = ${id}`;
554
+ if (rows.length === 0) return Option.none();
555
+ return Option.some(mapNoteRow(rows[0]));
556
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
557
+ operation: "read",
558
+ table: "notes",
559
+ reason: extractSqlReason(e)
560
+ })));
561
+ const searchNotes = (query) => Effect.gen(function* () {
562
+ yield* Effect.logDebug("searchNotes").pipe(Effect.annotateLogs({ query }));
563
+ return (yield* sql`SELECT n.*
564
+ FROM notes n
565
+ JOIN notes_fts ON n.id = notes_fts.rowid
566
+ WHERE notes_fts MATCH ${query}
567
+ ORDER BY rank`).map(mapNoteRow);
568
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
569
+ operation: "read",
570
+ table: "notes",
571
+ reason: extractSqlReason(e)
572
+ })));
573
+ const getManifest = () => Effect.gen(function* () {
574
+ yield* Effect.logDebug("getManifest");
575
+ const rows = yield* sql`SELECT
576
+ t1.project,
577
+ t1.timestamp as last_run,
578
+ t1.reason as last_result
579
+ FROM test_runs t1
580
+ INNER JOIN (
581
+ SELECT project, MAX(timestamp) as max_ts
582
+ FROM test_runs
583
+ GROUP BY project
584
+ ) t2 ON t1.project = t2.project
585
+ AND t1.timestamp = t2.max_ts`;
586
+ if (rows.length === 0) return Option.none();
587
+ const dbPath = (yield* sql`PRAGMA database_list`.pipe(Effect.catchAll(() => Effect.succeed([])))).find((d) => d.name === "main")?.file ?? "";
588
+ const projects = rows.map((r) => ({
589
+ project: r.project,
590
+ reportFile: dbPath,
591
+ historyFile: dbPath,
592
+ lastRun: r.last_run,
593
+ lastResult: r.last_result
594
+ }));
595
+ const manifest = {
596
+ updatedAt: rows.reduce((latest, r) => {
597
+ if (!r.last_run) return latest;
598
+ if (!latest) return r.last_run;
599
+ return r.last_run > latest ? r.last_run : latest;
600
+ }, null) ?? (/* @__PURE__ */ new Date()).toISOString(),
601
+ cacheDir: dbPath,
602
+ projects
603
+ };
604
+ return Option.some(manifest);
605
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
606
+ operation: "read",
607
+ table: "test_runs",
608
+ reason: extractSqlReason(e)
609
+ })));
610
+ const getSettings = (hash) => Effect.gen(function* () {
611
+ yield* Effect.logDebug("getSettings").pipe(Effect.annotateLogs({ hash }));
612
+ const rows = yield* sql`SELECT hash, vitest_version, pool, environment, coverage_provider, created_at
613
+ FROM settings WHERE hash = ${hash}`;
614
+ if (rows.length === 0) return Option.none();
615
+ const row = rows[0];
616
+ const envRows = yield* sql`SELECT key, value FROM settings_env_vars WHERE settings_hash = ${hash}`;
617
+ const envVars = {};
618
+ for (const ev of envRows) envVars[ev.key] = ev.value;
619
+ const settings = {
620
+ hash: row.hash,
621
+ reporters: null,
622
+ coverageEnabled: row.coverage_provider != null,
623
+ coverageProvider: row.coverage_provider,
624
+ coverageThresholds: null,
625
+ coverageTargets: null,
626
+ pool: row.pool,
627
+ shard: null,
628
+ project: null,
629
+ environment: row.environment,
630
+ envVars,
631
+ capturedAt: row.created_at
632
+ };
633
+ return Option.some(settings);
634
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
635
+ operation: "read",
636
+ table: "settings",
637
+ reason: extractSqlReason(e)
638
+ })));
639
+ const getLatestSettings = () => Effect.gen(function* () {
640
+ yield* Effect.logDebug("getLatestSettings");
641
+ const rows = yield* sql`SELECT hash, pool, environment, coverage_provider, created_at
642
+ FROM settings ORDER BY rowid DESC LIMIT 1`;
643
+ if (rows.length === 0) return Option.none();
644
+ const row = rows[0];
645
+ const envRows = yield* sql`SELECT key, value FROM settings_env_vars WHERE settings_hash = ${row.hash}`;
646
+ const envVars = {};
647
+ for (const ev of envRows) envVars[ev.key] = ev.value;
648
+ const settings = {
649
+ hash: row.hash,
650
+ reporters: null,
651
+ coverageEnabled: row.coverage_provider != null,
652
+ coverageProvider: row.coverage_provider,
653
+ coverageThresholds: null,
654
+ coverageTargets: null,
655
+ pool: row.pool,
656
+ shard: null,
657
+ project: null,
658
+ environment: row.environment,
659
+ envVars,
660
+ capturedAt: row.created_at
661
+ };
662
+ return Option.some(settings);
663
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
664
+ operation: "read",
665
+ table: "settings",
666
+ reason: extractSqlReason(e)
667
+ })));
668
+ const getTestByFullName = (project, fullName) => Effect.gen(function* () {
669
+ yield* Effect.logDebug("getTestByFullName").pipe(Effect.annotateLogs({
670
+ project,
671
+ fullName
672
+ }));
673
+ const runs = yield* sql`SELECT id FROM test_runs
674
+ WHERE project = ${project}
675
+ ORDER BY timestamp DESC LIMIT 1`;
676
+ if (runs.length === 0) return Option.none();
677
+ const rows = yield* sql`SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path as relative_module_id, tc.classification
678
+ FROM test_cases tc
679
+ JOIN test_modules tm ON tm.id = tc.module_id
680
+ JOIN files f ON f.id = tm.file_id
681
+ WHERE tm.run_id = ${runs[0].id} AND tc.full_name = ${fullName}
682
+ LIMIT 1`;
683
+ if (rows.length === 0) return Option.none();
684
+ const r = rows[0];
685
+ return Option.some({
686
+ id: r.id,
687
+ fullName: r.full_name,
688
+ state: r.state,
689
+ duration: r.duration,
690
+ module: r.relative_module_id,
691
+ classification: r.classification
692
+ });
693
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
694
+ operation: "read",
695
+ table: "test_cases",
696
+ reason: extractSqlReason(e)
697
+ })));
698
+ const listTests = (project, options) => Effect.gen(function* () {
699
+ yield* Effect.logDebug("listTests").pipe(Effect.annotateLogs({ project }));
700
+ const runs = yield* sql`SELECT id FROM test_runs
701
+ WHERE project = ${project}
702
+ ORDER BY timestamp DESC LIMIT 1`;
703
+ if (runs.length === 0) return [];
704
+ const runId = runs[0].id;
705
+ const effectiveLimit = Math.min(options?.limit ?? 100, 500);
706
+ const state = options?.state;
707
+ const mod = options?.module;
708
+ if (state && mod) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
709
+ FROM test_cases tc
710
+ JOIN test_modules tm ON tm.id = tc.module_id
711
+ JOIN files f ON f.id = tm.file_id
712
+ WHERE tm.run_id = ${runId} AND tc.state = ${state} AND f.path = ${mod}
713
+ ORDER BY tc.full_name
714
+ LIMIT ${effectiveLimit}`).map(mapTestListRow);
715
+ if (state) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
716
+ FROM test_cases tc
717
+ JOIN test_modules tm ON tm.id = tc.module_id
718
+ JOIN files f ON f.id = tm.file_id
719
+ WHERE tm.run_id = ${runId} AND tc.state = ${state}
720
+ ORDER BY tc.full_name
721
+ LIMIT ${effectiveLimit}`).map(mapTestListRow);
722
+ if (mod) return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
723
+ FROM test_cases tc
724
+ JOIN test_modules tm ON tm.id = tc.module_id
725
+ JOIN files f ON f.id = tm.file_id
726
+ WHERE tm.run_id = ${runId} AND f.path = ${mod}
727
+ ORDER BY tc.full_name
728
+ LIMIT ${effectiveLimit}`).map(mapTestListRow);
729
+ return (yield* sql`SELECT tc.id, tc.full_name as "full_name", tc.state, tc.duration, f.path as file_path, tc.classification
730
+ FROM test_cases tc
731
+ JOIN test_modules tm ON tm.id = tc.module_id
732
+ JOIN files f ON f.id = tm.file_id
733
+ WHERE tm.run_id = ${runId}
734
+ ORDER BY tc.full_name
735
+ LIMIT ${effectiveLimit}`).map(mapTestListRow);
736
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
737
+ operation: "read",
738
+ table: "test_cases",
739
+ reason: extractSqlReason(e)
740
+ })));
741
+ const listModules = (project) => Effect.gen(function* () {
742
+ yield* Effect.logDebug("listModules").pipe(Effect.annotateLogs({ project }));
743
+ const runs = yield* sql`SELECT id FROM test_runs
744
+ WHERE project = ${project}
745
+ ORDER BY timestamp DESC LIMIT 1`;
746
+ if (runs.length === 0) return [];
747
+ return (yield* sql`SELECT tm.id, f.path as file_path, tm.state,
748
+ (SELECT COUNT(*) FROM test_cases tc WHERE tc.module_id = tm.id) as test_count,
749
+ tm.duration
750
+ FROM test_modules tm
751
+ JOIN files f ON f.id = tm.file_id
752
+ WHERE tm.run_id = ${runs[0].id}
753
+ ORDER BY f.path`).map((r) => ({
754
+ id: r.id,
755
+ file: r.file_path,
756
+ state: r.state,
757
+ testCount: r.test_count,
758
+ duration: r.duration
759
+ }));
760
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
761
+ operation: "read",
762
+ table: "test_modules",
763
+ reason: extractSqlReason(e)
764
+ })));
765
+ const listSuites = (project, options) => Effect.gen(function* () {
766
+ yield* Effect.logDebug("listSuites").pipe(Effect.annotateLogs({ project }));
767
+ const runs = yield* sql`SELECT id FROM test_runs
768
+ WHERE project = ${project}
769
+ ORDER BY timestamp DESC LIMIT 1`;
770
+ if (runs.length === 0) return [];
771
+ const runId = runs[0].id;
772
+ const mod = options?.module;
773
+ if (mod) return (yield* sql`SELECT ts.id, ts.name, f.path as file_path, ts.state,
774
+ (SELECT COUNT(*) FROM test_cases tc WHERE tc.suite_id = ts.id) as test_count
775
+ FROM test_suites ts
776
+ JOIN test_modules tm ON tm.id = ts.module_id
777
+ JOIN files f ON f.id = tm.file_id
778
+ WHERE tm.run_id = ${runId} AND f.path = ${mod}
779
+ ORDER BY ts.name`).map((r) => ({
780
+ id: r.id,
781
+ name: r.name,
782
+ module: r.file_path,
783
+ state: r.state,
784
+ testCount: r.test_count
785
+ }));
786
+ return (yield* sql`SELECT ts.id, ts.name, f.path as file_path, ts.state,
787
+ (SELECT COUNT(*) FROM test_cases tc WHERE tc.suite_id = ts.id) as test_count
788
+ FROM test_suites ts
789
+ JOIN test_modules tm ON tm.id = ts.module_id
790
+ JOIN files f ON f.id = tm.file_id
791
+ WHERE tm.run_id = ${runId}
792
+ ORDER BY ts.name`).map((r) => ({
793
+ id: r.id,
794
+ name: r.name,
795
+ module: r.file_path,
796
+ state: r.state,
797
+ testCount: r.test_count
798
+ }));
799
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
800
+ operation: "read",
801
+ table: "test_suites",
802
+ reason: extractSqlReason(e)
803
+ })));
804
+ const listSettings = () => Effect.gen(function* () {
805
+ yield* Effect.logDebug("listSettings");
806
+ return (yield* sql`SELECT hash, created_at FROM settings ORDER BY rowid DESC`).map((r) => ({
807
+ hash: r.hash,
808
+ capturedAt: r.created_at
809
+ }));
810
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
811
+ operation: "read",
812
+ table: "settings",
813
+ reason: extractSqlReason(e)
814
+ })));
815
+ const sessionRowToDetail = (r) => ({
816
+ id: r.id,
817
+ chatId: r.chat_id,
818
+ project: r.project,
819
+ cwd: r.cwd,
820
+ agentKind: r.agent_kind,
821
+ agentType: r.agent_type,
822
+ parentSessionId: r.parent_session_id,
823
+ triageWasNonEmpty: r.triage_was_non_empty === 1,
824
+ startedAt: r.started_at,
825
+ endedAt: r.ended_at,
826
+ endReason: r.end_reason
827
+ });
828
+ const getSessionById = (id) => Effect.gen(function* () {
829
+ yield* Effect.logDebug("getSessionById").pipe(Effect.annotateLogs({ id }));
830
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE id = ${id} LIMIT 1`;
831
+ if (rows.length === 0) return Option.none();
832
+ return Option.some(sessionRowToDetail(rows[0]));
833
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
834
+ operation: "read",
835
+ table: "sessions",
836
+ reason: extractSqlReason(e)
837
+ })));
838
+ const getSessionByChatId = (chatId) => Effect.gen(function* () {
839
+ yield* Effect.logDebug("getSessionByChatId").pipe(Effect.annotateLogs({ chatId }));
840
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE chat_id = ${chatId} LIMIT 1`;
841
+ if (rows.length === 0) return Option.none();
842
+ return Option.some(sessionRowToDetail(rows[0]));
843
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
844
+ operation: "read",
845
+ table: "sessions",
846
+ reason: extractSqlReason(e)
847
+ })));
848
+ const findSessionsByChatPrefix = (prefix) => Effect.gen(function* () {
849
+ yield* Effect.logDebug("findSessionsByChatPrefix").pipe(Effect.annotateLogs({ prefix }));
850
+ return (yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE chat_id LIKE ${`${prefix}%`} ESCAPE '\\' ORDER BY started_at DESC`).map(sessionRowToDetail);
851
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
852
+ operation: "read",
853
+ table: "sessions",
854
+ reason: extractSqlReason(e)
855
+ })));
856
+ const findActiveSubagentSession = (parentSessionId) => Effect.gen(function* () {
857
+ yield* Effect.logDebug("findActiveSubagentSession").pipe(Effect.annotateLogs({ parentSessionId }));
858
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE parent_session_id = ${parentSessionId} AND agent_kind = 'subagent' AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`;
859
+ if (rows.length === 0) return Option.none();
860
+ return Option.some(sessionRowToDetail(rows[0]));
861
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
862
+ operation: "read",
863
+ table: "sessions",
864
+ reason: extractSqlReason(e)
865
+ })));
866
+ const searchTurns = (options) => Effect.gen(function* () {
867
+ yield* Effect.logDebug("searchTurns").pipe(Effect.annotateLogs({ ...options }));
868
+ const limit = options.limit ?? 100;
869
+ return (yield* sql`SELECT id, session_id, turn_no, type, payload, occurred_at FROM turns WHERE 1=1${options.sessionId !== void 0 ? sql` AND session_id = ${options.sessionId}` : sql``}${options.type !== void 0 ? sql` AND type = ${options.type}` : sql``}${options.since !== void 0 ? sql` AND occurred_at >= ${options.since}` : sql``} ORDER BY occurred_at DESC, turn_no DESC LIMIT ${limit}`).map((r) => ({
870
+ id: r.id,
871
+ sessionId: r.session_id,
872
+ turnNo: r.turn_no,
873
+ type: r.type,
874
+ payload: r.payload,
875
+ occurredAt: r.occurred_at
876
+ }));
877
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
878
+ operation: "read",
879
+ table: "turns",
880
+ reason: extractSqlReason(e)
881
+ })));
882
+ const computeAcceptanceMetrics = () => Effect.gen(function* () {
883
+ yield* Effect.logDebug("computeAcceptanceMetrics");
884
+ const m1 = yield* sql`
885
+ WITH task_orderings AS (
886
+ SELECT
887
+ p.tdd_task_id,
888
+ MIN(CASE WHEN a.artifact_kind = 'test_failed_run' THEN a.id END) AS first_failed_run,
889
+ MIN(CASE WHEN a.artifact_kind = 'code_written' THEN a.id END) AS first_code_written
890
+ FROM tdd_artifacts a
891
+ JOIN tdd_phases p ON a.phase_id = p.id
892
+ WHERE a.artifact_kind IN ('test_failed_run', 'code_written')
893
+ GROUP BY p.tdd_task_id
894
+ )
895
+ SELECT
896
+ COUNT(*) AS total,
897
+ COALESCE(SUM(CASE WHEN first_failed_run < first_code_written THEN 1 ELSE 0 END), 0) AS compliant
898
+ FROM task_orderings
899
+ WHERE first_code_written IS NOT NULL
900
+ `;
901
+ const m2 = yield* sql`
902
+ WITH wrap_up_fires AS (
903
+ SELECT t.session_id
904
+ FROM turns t
905
+ WHERE t.type = 'hook_fire'
906
+ AND json_extract(t.payload, '$.hook_kind') IN ('SessionEnd', 'PreCompact', 'Stop')
907
+ ),
908
+ followups AS (
909
+ SELECT DISTINCT t.session_id
910
+ FROM turns t
911
+ JOIN tool_invocations ti ON ti.turn_id = t.id
912
+ WHERE ti.tool_name IN ('note', 'hypothesis', 'tdd_task', 'note_create', 'hypothesis_validate', 'tdd_session_end')
913
+ )
914
+ SELECT
915
+ (SELECT COUNT(DISTINCT session_id) FROM wrap_up_fires) AS total,
916
+ (SELECT COUNT(DISTINCT wf.session_id)
917
+ FROM wrap_up_fires wf JOIN followups f ON f.session_id = wf.session_id) AS with_followup
918
+ `;
919
+ const m3 = yield* sql`
920
+ WITH triaged_sessions AS (
921
+ SELECT id AS session_id FROM sessions WHERE triage_was_non_empty = 1
922
+ ),
923
+ ranked_tool_results AS (
924
+ SELECT session_id, id AS turn_id,
925
+ ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY turn_no) AS result_no
926
+ FROM turns
927
+ WHERE type = 'tool_result'
928
+ ),
929
+ first_three_tool_calls AS (
930
+ SELECT r.session_id, ti.tool_name
931
+ FROM ranked_tool_results r
932
+ JOIN tool_invocations ti ON ti.turn_id = r.turn_id
933
+ WHERE r.result_no <= 3
934
+ ),
935
+ referenced AS (
936
+ SELECT DISTINCT ts.session_id
937
+ FROM triaged_sessions ts
938
+ JOIN first_three_tool_calls ftc ON ftc.session_id = ts.session_id
939
+ WHERE ftc.tool_name IN ('tdd_session_resume', 'run_tests', 'test_history',
940
+ 'failure_signature_get', 'tdd_session_start')
941
+ )
942
+ SELECT
943
+ (SELECT COUNT(*) FROM triaged_sessions) AS total,
944
+ (SELECT COUNT(*) FROM referenced) AS referenced_count
945
+ `;
946
+ const m4 = yield* sql`
947
+ SELECT
948
+ COUNT(*) AS total,
949
+ COALESCE(SUM(CASE WHEN weakened_count = 0 THEN 1 ELSE 0 END), 0) AS clean_sessions
950
+ FROM (
951
+ SELECT ts.id AS tdd_task_id,
952
+ COUNT(a.id) AS weakened_count
953
+ FROM tdd_tasks ts
954
+ LEFT JOIN tdd_phases p ON p.tdd_task_id = ts.id
955
+ LEFT JOIN tdd_artifacts a ON a.phase_id = p.id AND a.artifact_kind = 'test_weakened'
956
+ WHERE ts.ended_at IS NOT NULL
957
+ GROUP BY ts.id
958
+ ) tasks_with_counts
959
+ `;
960
+ const ratio = (n, d) => d === 0 ? 0 : n / d;
961
+ return {
962
+ phaseEvidenceIntegrity: {
963
+ total: m1[0].total,
964
+ compliant: m1[0].compliant,
965
+ ratio: ratio(m1[0].compliant, m1[0].total)
966
+ },
967
+ complianceHookResponsiveness: {
968
+ total: m2[0].total,
969
+ withFollowup: m2[0].with_followup,
970
+ ratio: ratio(m2[0].with_followup, m2[0].total)
971
+ },
972
+ orientationUsefulness: {
973
+ total: m3[0].total,
974
+ referencedCount: m3[0].referenced_count,
975
+ ratio: ratio(m3[0].referenced_count, m3[0].total)
976
+ },
977
+ antiPatternDetectionRate: {
978
+ total: m4[0].total,
979
+ cleanSessions: m4[0].clean_sessions,
980
+ ratio: ratio(m4[0].clean_sessions, m4[0].total)
981
+ }
982
+ };
983
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
984
+ operation: "read",
985
+ table: "tdd_tasks",
986
+ reason: extractSqlReason(e)
987
+ })));
988
+ const listSessions = (options) => Effect.gen(function* () {
989
+ yield* Effect.logDebug("listSessions").pipe(Effect.annotateLogs({ ...options }));
990
+ const limit = options.limit ?? 50;
991
+ const project = options.project ?? null;
992
+ const agentKind = options.agentKind ?? null;
993
+ return (yield* sql`
994
+ SELECT id, chat_id, project, cwd, agent_kind, agent_type,
995
+ parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason
996
+ FROM sessions
997
+ WHERE (${project} IS NULL OR project = ${project})
998
+ AND (${agentKind} IS NULL OR agent_kind = ${agentKind})
999
+ ORDER BY started_at DESC
1000
+ LIMIT ${limit}
1001
+ `).map(sessionRowToDetail);
1002
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1003
+ operation: "read",
1004
+ table: "sessions",
1005
+ reason: extractSqlReason(e)
1006
+ })));
1007
+ const getFailureSignatureByHash = (hash) => Effect.gen(function* () {
1008
+ yield* Effect.logDebug("getFailureSignatureByHash").pipe(Effect.annotateLogs({ hash }));
1009
+ const sigRows = yield* sql`
1010
+ SELECT signature_hash, first_seen_run_id, first_seen_at, last_seen_at, occurrence_count
1011
+ FROM failure_signatures WHERE signature_hash = ${hash} LIMIT 1
1012
+ `;
1013
+ if (sigRows.length === 0) return Option.none();
1014
+ const errRows = yield* sql`
1015
+ SELECT run_id, message, name FROM test_errors
1016
+ WHERE signature_hash = ${hash}
1017
+ ORDER BY run_id DESC
1018
+ LIMIT 10
1019
+ `;
1020
+ const sig = sigRows[0];
1021
+ return Option.some({
1022
+ signatureHash: sig.signature_hash,
1023
+ firstSeenRunId: sig.first_seen_run_id,
1024
+ firstSeenAt: sig.first_seen_at,
1025
+ lastSeenAt: sig.last_seen_at,
1026
+ occurrenceCount: sig.occurrence_count,
1027
+ recentErrors: errRows.map((e) => ({
1028
+ runId: e.run_id,
1029
+ message: e.message,
1030
+ errorName: e.name
1031
+ }))
1032
+ });
1033
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1034
+ operation: "read",
1035
+ table: "failure_signatures",
1036
+ reason: extractSqlReason(e)
1037
+ })));
1038
+ const goalRowFromDb = (row) => ({
1039
+ id: row.id,
1040
+ sessionId: row.session_id,
1041
+ ordinal: row.ordinal,
1042
+ goal: row.goal,
1043
+ status: row.status,
1044
+ createdAt: row.created_at
1045
+ });
1046
+ const behaviorRowFromDb = (row) => ({
1047
+ id: row.id,
1048
+ goalId: row.goal_id,
1049
+ ordinal: row.ordinal,
1050
+ behavior: row.behavior,
1051
+ suggestedTestName: row.suggested_test_name,
1052
+ status: row.status,
1053
+ createdAt: row.created_at
1054
+ });
1055
+ const fetchGoalsWithBehaviors = (sessionId) => Effect.gen(function* () {
1056
+ const goals = yield* sql`
1057
+ SELECT id, session_id, ordinal, goal, status, created_at
1058
+ FROM tdd_session_goals
1059
+ WHERE session_id = ${sessionId}
1060
+ ORDER BY ordinal
1061
+ `;
1062
+ if (goals.length === 0) return [];
1063
+ const goalIds = goals.map((g) => g.id);
1064
+ const behaviors = yield* sql`
1065
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1066
+ FROM tdd_session_behaviors
1067
+ WHERE goal_id IN ${sql.in(goalIds)}
1068
+ ORDER BY goal_id, ordinal
1069
+ `;
1070
+ const byGoal = /* @__PURE__ */ new Map();
1071
+ for (const id of goalIds) byGoal.set(id, []);
1072
+ for (const b of behaviors) {
1073
+ const list = byGoal.get(b.goal_id);
1074
+ if (list) list.push(behaviorRowFromDb(b));
1075
+ }
1076
+ return goals.map((g) => ({
1077
+ ...goalRowFromDb(g),
1078
+ behaviors: byGoal.get(g.id) ?? []
1079
+ }));
1080
+ });
1081
+ const getGoalById = (id) => Effect.gen(function* () {
1082
+ const goalRows = yield* sql`
1083
+ SELECT id, session_id, ordinal, goal, status, created_at
1084
+ FROM tdd_session_goals
1085
+ WHERE id = ${id}
1086
+ `;
1087
+ if (goalRows.length === 0) return Option.none();
1088
+ const beh = yield* sql`
1089
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1090
+ FROM tdd_session_behaviors
1091
+ WHERE goal_id = ${id}
1092
+ ORDER BY ordinal
1093
+ `;
1094
+ return Option.some({
1095
+ ...goalRowFromDb(goalRows[0]),
1096
+ behaviors: beh.map(behaviorRowFromDb)
1097
+ });
1098
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1099
+ operation: "read",
1100
+ table: "tdd_session_goals",
1101
+ reason: extractSqlReason(e)
1102
+ })));
1103
+ const getGoalsByTddTask = (tddTaskId) => fetchGoalsWithBehaviors(tddTaskId).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1104
+ operation: "read",
1105
+ table: "tdd_session_goals",
1106
+ reason: extractSqlReason(e)
1107
+ })));
1108
+ const getBehaviorById = (id) => Effect.gen(function* () {
1109
+ const rows = yield* sql`
1110
+ SELECT b.id AS b_id, b.goal_id AS b_goal_id, b.ordinal AS b_ordinal, b.behavior AS b_behavior,
1111
+ b.suggested_test_name AS b_suggested_test_name, b.status AS b_status,
1112
+ b.created_at AS b_created_at,
1113
+ g.id AS g_id, g.goal AS g_goal, g.status AS g_status
1114
+ FROM tdd_session_behaviors b
1115
+ JOIN tdd_session_goals g ON g.id = b.goal_id
1116
+ WHERE b.id = ${id}
1117
+ `;
1118
+ if (rows.length === 0) return Option.none();
1119
+ const r = rows[0];
1120
+ const deps = yield* sql`
1121
+ SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1122
+ FROM tdd_behavior_dependencies d
1123
+ JOIN tdd_session_behaviors b ON b.id = d.depends_on_id
1124
+ WHERE d.behavior_id = ${id}
1125
+ ORDER BY b.ordinal
1126
+ `;
1127
+ return Option.some({
1128
+ id: r.b_id,
1129
+ goalId: r.b_goal_id,
1130
+ ordinal: r.b_ordinal,
1131
+ behavior: r.b_behavior,
1132
+ suggestedTestName: r.b_suggested_test_name,
1133
+ status: r.b_status,
1134
+ createdAt: r.b_created_at,
1135
+ parentGoal: {
1136
+ id: r.g_id,
1137
+ goal: r.g_goal,
1138
+ status: r.g_status
1139
+ },
1140
+ dependencies: deps.map(behaviorRowFromDb)
1141
+ });
1142
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1143
+ operation: "read",
1144
+ table: "tdd_session_behaviors",
1145
+ reason: extractSqlReason(e)
1146
+ })));
1147
+ const getBehaviorsByGoal = (goalId) => Effect.gen(function* () {
1148
+ return (yield* sql`
1149
+ SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
1150
+ FROM tdd_session_behaviors
1151
+ WHERE goal_id = ${goalId}
1152
+ ORDER BY ordinal
1153
+ `).map(behaviorRowFromDb);
1154
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1155
+ operation: "read",
1156
+ table: "tdd_session_behaviors",
1157
+ reason: extractSqlReason(e)
1158
+ })));
1159
+ const getBehaviorsByTddTask = (tddTaskId) => Effect.gen(function* () {
1160
+ return (yield* sql`
1161
+ SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1162
+ FROM tdd_session_behaviors b
1163
+ JOIN tdd_session_goals g ON g.id = b.goal_id
1164
+ WHERE g.session_id = ${tddTaskId}
1165
+ ORDER BY g.ordinal, b.ordinal
1166
+ `).map(behaviorRowFromDb);
1167
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1168
+ operation: "read",
1169
+ table: "tdd_session_behaviors",
1170
+ reason: extractSqlReason(e)
1171
+ })));
1172
+ const getBehaviorDependencies = (behaviorId) => Effect.gen(function* () {
1173
+ return (yield* sql`
1174
+ SELECT b.id, b.goal_id, b.ordinal, b.behavior, b.suggested_test_name, b.status, b.created_at
1175
+ FROM tdd_behavior_dependencies d
1176
+ JOIN tdd_session_behaviors b ON b.id = d.depends_on_id
1177
+ WHERE d.behavior_id = ${behaviorId}
1178
+ ORDER BY b.ordinal
1179
+ `).map(behaviorRowFromDb);
1180
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1181
+ operation: "read",
1182
+ table: "tdd_behavior_dependencies",
1183
+ reason: extractSqlReason(e)
1184
+ })));
1185
+ const resolveGoalIdForBehavior = (behaviorId) => Effect.gen(function* () {
1186
+ const rows = yield* sql`
1187
+ SELECT goal_id FROM tdd_session_behaviors WHERE id = ${behaviorId}
1188
+ `;
1189
+ return rows.length === 0 ? Option.none() : Option.some(rows[0].goal_id);
1190
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1191
+ operation: "read",
1192
+ table: "tdd_session_behaviors",
1193
+ reason: extractSqlReason(e)
1194
+ })));
1195
+ const getTddTaskById = (id) => Effect.gen(function* () {
1196
+ yield* Effect.logDebug("getTddTaskById").pipe(Effect.annotateLogs({ id }));
1197
+ const taskRows = yield* sql`
1198
+ SELECT id, session_id, goal, started_at, ended_at, outcome, run_id
1199
+ FROM tdd_tasks WHERE id = ${id} LIMIT 1
1200
+ `;
1201
+ if (taskRows.length === 0) return Option.none();
1202
+ const tddTask = taskRows[0];
1203
+ const goals = yield* fetchGoalsWithBehaviors(id);
1204
+ const phaseRows = yield* sql`
1205
+ SELECT id, behavior_id, phase, started_at, ended_at, transition_reason
1206
+ FROM tdd_phases WHERE tdd_task_id = ${id}
1207
+ ORDER BY started_at ASC
1208
+ `;
1209
+ const artifactRows = yield* sql`
1210
+ SELECT a.id, a.phase_id, a.artifact_kind, a.test_case_id, a.test_run_id, a.recorded_at
1211
+ FROM tdd_artifacts a
1212
+ JOIN tdd_phases p ON a.phase_id = p.id
1213
+ WHERE p.tdd_task_id = ${id}
1214
+ ORDER BY a.recorded_at ASC
1215
+ `;
1216
+ return Option.some({
1217
+ id: tddTask.id,
1218
+ sessionId: tddTask.session_id,
1219
+ goal: tddTask.goal,
1220
+ startedAt: tddTask.started_at,
1221
+ endedAt: tddTask.ended_at,
1222
+ outcome: tddTask.outcome,
1223
+ runId: tddTask.run_id,
1224
+ goals,
1225
+ phases: phaseRows.map((p) => ({
1226
+ id: p.id,
1227
+ behaviorId: p.behavior_id,
1228
+ phase: p.phase,
1229
+ startedAt: p.started_at,
1230
+ endedAt: p.ended_at,
1231
+ transitionReason: p.transition_reason
1232
+ })),
1233
+ artifacts: artifactRows.map((a) => ({
1234
+ id: a.id,
1235
+ phaseId: a.phase_id,
1236
+ artifactKind: a.artifact_kind,
1237
+ testCaseId: a.test_case_id,
1238
+ testRunId: a.test_run_id,
1239
+ recordedAt: a.recorded_at
1240
+ }))
1241
+ });
1242
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1243
+ operation: "read",
1244
+ table: "tdd_tasks",
1245
+ reason: extractSqlReason(e)
1246
+ })));
1247
+ const getCurrentTddPhase = (tddTaskId) => Effect.gen(function* () {
1248
+ yield* Effect.logDebug("getCurrentTddPhase").pipe(Effect.annotateLogs({ tddTaskId }));
1249
+ const rows = yield* sql`
1250
+ SELECT id, phase, started_at, behavior_id FROM tdd_phases
1251
+ WHERE tdd_task_id = ${tddTaskId} AND ended_at IS NULL
1252
+ ORDER BY started_at DESC LIMIT 1
1253
+ `;
1254
+ if (rows.length === 0) return Option.none();
1255
+ return Option.some({
1256
+ id: rows[0].id,
1257
+ phase: rows[0].phase,
1258
+ startedAt: rows[0].started_at,
1259
+ behaviorId: rows[0].behavior_id
1260
+ });
1261
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1262
+ operation: "read",
1263
+ table: "tdd_phases",
1264
+ reason: extractSqlReason(e)
1265
+ })));
1266
+ const getTddArtifactWithContext = (artifactId) => Effect.gen(function* () {
1267
+ yield* Effect.logDebug("getTddArtifactWithContext").pipe(Effect.annotateLogs({ artifactId }));
1268
+ const rows = yield* sql`
1269
+ SELECT
1270
+ a.id,
1271
+ a.phase_id,
1272
+ a.artifact_kind,
1273
+ a.test_case_id,
1274
+ ttc.occurred_at AS test_case_created_turn_at,
1275
+ CASE
1276
+ WHEN ttc.session_id = sess.id THEN 1
1277
+ ELSE 0
1278
+ END AS test_case_authored_in_session,
1279
+ a.test_run_id,
1280
+ a.test_first_failure_run_id,
1281
+ p.behavior_id
1282
+ FROM tdd_artifacts a
1283
+ JOIN tdd_phases p ON p.id = a.phase_id
1284
+ JOIN tdd_tasks ts ON ts.id = p.tdd_task_id
1285
+ JOIN sessions sess ON sess.id = ts.session_id
1286
+ LEFT JOIN test_cases tc ON tc.id = a.test_case_id
1287
+ LEFT JOIN turns ttc ON ttc.id = tc.created_turn_id
1288
+ WHERE a.id = ${artifactId}
1289
+ `;
1290
+ if (rows.length === 0) return Option.none();
1291
+ const r = rows[0];
1292
+ return Option.some({
1293
+ id: r.id,
1294
+ phase_id: r.phase_id,
1295
+ artifact_kind: r.artifact_kind,
1296
+ test_case_id: r.test_case_id,
1297
+ test_case_created_turn_at: r.test_case_created_turn_at,
1298
+ test_case_authored_in_session: r.test_case_authored_in_session === 1,
1299
+ test_run_id: r.test_run_id,
1300
+ test_first_failure_run_id: r.test_first_failure_run_id,
1301
+ behavior_id: r.behavior_id
1302
+ });
1303
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1304
+ operation: "read",
1305
+ table: "tdd_artifacts",
1306
+ reason: extractSqlReason(e)
1307
+ })));
1308
+ const getCommitChanges = (sha) => Effect.gen(function* () {
1309
+ yield* Effect.logDebug("getCommitChanges").pipe(Effect.annotateLogs({ sha: sha ?? "ALL" }));
1310
+ const commitRows = sha !== void 0 ? yield* sql`SELECT sha, parent_sha, message, author, committed_at, branch FROM commits WHERE sha = ${sha}` : yield* sql`
1311
+ SELECT sha, parent_sha, message, author, committed_at, branch FROM commits
1312
+ ORDER BY committed_at DESC NULLS LAST LIMIT 20
1313
+ `;
1314
+ const out = [];
1315
+ for (const c of commitRows) {
1316
+ const fileRows = yield* sql`
1317
+ SELECT f.path, rcf.change_kind
1318
+ FROM run_changed_files rcf
1319
+ JOIN files f ON f.id = rcf.file_id
1320
+ WHERE rcf.commit_sha = ${c.sha}
1321
+ `;
1322
+ out.push({
1323
+ sha: c.sha,
1324
+ parentSha: c.parent_sha,
1325
+ message: c.message,
1326
+ author: c.author,
1327
+ committedAt: c.committed_at,
1328
+ branch: c.branch,
1329
+ files: fileRows.map((r) => ({
1330
+ filePath: r.path,
1331
+ changeKind: r.change_kind
1332
+ }))
1333
+ });
1334
+ }
1335
+ return out;
1336
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1337
+ operation: "read",
1338
+ table: "commits",
1339
+ reason: extractSqlReason(e)
1340
+ })));
1341
+ const listTddTasksForSession = (sessionId, options) => Effect.gen(function* () {
1342
+ yield* Effect.logDebug("listTddTasksForSession").pipe(Effect.annotateLogs({
1343
+ sessionId,
1344
+ walkParents: options?.walkParents ?? false
1345
+ }));
1346
+ const sessionIds = [sessionId];
1347
+ if (options?.walkParents === true) {
1348
+ let currentId = sessionId;
1349
+ for (let i = 0; i < 64 && currentId !== null; i++) {
1350
+ const next = (yield* sql`
1351
+ SELECT parent_session_id FROM sessions WHERE id = ${currentId} LIMIT 1
1352
+ `)[0]?.parent_session_id ?? null;
1353
+ if (next === null || sessionIds.includes(next)) break;
1354
+ sessionIds.push(next);
1355
+ currentId = next;
1356
+ }
1357
+ }
1358
+ return (yield* sql`
1359
+ SELECT id, session_id, goal, started_at, ended_at, outcome FROM tdd_tasks
1360
+ WHERE session_id IN ${sql.in(sessionIds)} ORDER BY started_at DESC
1361
+ `).map((r) => ({
1362
+ id: r.id,
1363
+ sessionId: r.session_id,
1364
+ goal: r.goal,
1365
+ startedAt: r.started_at,
1366
+ endedAt: r.ended_at,
1367
+ outcome: r.outcome
1368
+ }));
1369
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1370
+ operation: "read",
1371
+ table: "tdd_tasks",
1372
+ reason: extractSqlReason(e)
1373
+ })));
1374
+ const listTddArtifactsForTask = (input) => Effect.gen(function* () {
1375
+ yield* Effect.logDebug("listTddArtifactsForTask").pipe(Effect.annotateLogs(input));
1376
+ const limit = input.limit ?? 50;
1377
+ return (yield* sql`
1378
+ SELECT
1379
+ a.id,
1380
+ p.tdd_task_id AS tdd_task_id,
1381
+ a.phase_id,
1382
+ p.phase AS phase_name,
1383
+ a.artifact_kind,
1384
+ p.behavior_id,
1385
+ a.test_case_id,
1386
+ a.test_run_id,
1387
+ a.test_first_failure_run_id,
1388
+ a.recorded_at
1389
+ FROM tdd_artifacts a
1390
+ JOIN tdd_phases p ON p.id = a.phase_id
1391
+ WHERE p.tdd_task_id = ${input.tddTaskId}
1392
+ AND (${input.artifactKind ?? null} IS NULL OR a.artifact_kind = ${input.artifactKind ?? null})
1393
+ AND (${input.phaseId ?? null} IS NULL OR a.phase_id = ${input.phaseId ?? null})
1394
+ AND (${input.behaviorId ?? null} IS NULL OR p.behavior_id = ${input.behaviorId ?? null})
1395
+ ORDER BY a.recorded_at DESC, a.id DESC
1396
+ LIMIT ${limit}
1397
+ `).map((r) => ({
1398
+ id: r.id,
1399
+ tddTaskId: r.tdd_task_id,
1400
+ phaseId: r.phase_id,
1401
+ phaseName: r.phase_name,
1402
+ artifactKind: r.artifact_kind,
1403
+ behaviorId: r.behavior_id,
1404
+ testCaseId: r.test_case_id,
1405
+ testRunId: r.test_run_id,
1406
+ testFirstFailureRunId: r.test_first_failure_run_id,
1407
+ recordedAt: r.recorded_at
1408
+ }));
1409
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1410
+ operation: "read",
1411
+ table: "tdd_artifacts",
1412
+ reason: extractSqlReason(e)
1413
+ })));
1414
+ const listHypotheses = (options) => Effect.gen(function* () {
1415
+ yield* Effect.logDebug("listHypotheses").pipe(Effect.annotateLogs({ ...options }));
1416
+ const limit = options.limit ?? 50;
1417
+ const sessionId = options.sessionId ?? null;
1418
+ const outcome = options.outcome ?? null;
1419
+ return (yield* sql`
1420
+ SELECT id, session_id, content, cited_test_error_id, cited_stack_frame_id,
1421
+ validation_outcome, validated_at
1422
+ FROM hypotheses
1423
+ WHERE (${sessionId} IS NULL OR session_id = ${sessionId})
1424
+ AND (
1425
+ ${outcome} IS NULL
1426
+ OR (${outcome} = 'open' AND validation_outcome IS NULL)
1427
+ OR (${outcome} != 'open' AND validation_outcome = ${outcome})
1428
+ )
1429
+ ORDER BY id DESC
1430
+ LIMIT ${limit}
1431
+ `).map((r) => ({
1432
+ id: r.id,
1433
+ sessionId: r.session_id,
1434
+ content: r.content,
1435
+ citedTestErrorId: r.cited_test_error_id,
1436
+ citedStackFrameId: r.cited_stack_frame_id,
1437
+ validationOutcome: r.validation_outcome,
1438
+ validatedAt: r.validated_at
1439
+ }));
1440
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1441
+ operation: "read",
1442
+ table: "hypotheses",
1443
+ reason: extractSqlReason(e)
1444
+ })));
1445
+ const findIdempotentResponse = (procedurePath, key) => Effect.gen(function* () {
1446
+ yield* Effect.logDebug("findIdempotentResponse").pipe(Effect.annotateLogs({
1447
+ procedurePath,
1448
+ key
1449
+ }));
1450
+ const rows = yield* sql`
1451
+ SELECT result_json FROM mcp_idempotent_responses
1452
+ WHERE procedure_path = ${procedurePath} AND key = ${key}
1453
+ LIMIT 1
1454
+ `;
1455
+ return rows.length === 0 ? Option.none() : Option.some(rows[0].result_json);
1456
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1457
+ operation: "read",
1458
+ table: "mcp_idempotent_responses",
1459
+ reason: extractSqlReason(e)
1460
+ })));
1461
+ const getLatestTestCaseForSession = (chatId) => Effect.gen(function* () {
1462
+ yield* Effect.logDebug("getLatestTestCaseForSession").pipe(Effect.annotateLogs({ chatId }));
1463
+ const rows = yield* sql`
1464
+ SELECT tc.id
1465
+ FROM test_cases tc
1466
+ JOIN test_modules tm ON tc.module_id = tm.id
1467
+ JOIN files f_mod ON f_mod.id = tm.file_id
1468
+ WHERE tm.run_id = (SELECT id FROM test_runs ORDER BY id DESC LIMIT 1)
1469
+ AND EXISTS (
1470
+ SELECT 1
1471
+ FROM file_edits fe
1472
+ JOIN turns t ON fe.turn_id = t.id
1473
+ JOIN sessions s ON t.session_id = s.id
1474
+ JOIN files f_edit ON fe.file_id = f_edit.id
1475
+ WHERE s.chat_id = ${chatId}
1476
+ AND (
1477
+ f_edit.path = f_mod.path
1478
+ OR f_edit.path LIKE '%/' || f_mod.path
1479
+ OR f_mod.path LIKE '%/' || f_edit.path
1480
+ )
1481
+ )
1482
+ ORDER BY tc.id DESC
1483
+ LIMIT 1
1484
+ `;
1485
+ return rows.length === 0 ? Option.none() : Option.some(rows[0].id);
1486
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1487
+ operation: "read",
1488
+ table: "test_cases",
1489
+ reason: extractSqlReason(e)
1490
+ })));
1491
+ const listTagInventory = (options) => Effect.gen(function* () {
1492
+ yield* Effect.logDebug("listTagInventory").pipe(Effect.annotateLogs({ project: options?.project }));
1493
+ if (options?.project) {
1494
+ const runs = yield* sql`SELECT id FROM test_runs
1495
+ WHERE project = ${options.project}
1496
+ ORDER BY timestamp DESC LIMIT 1`;
1497
+ if (runs.length === 0) return [];
1498
+ const runId = runs[0].id;
1499
+ return (yield* sql`
1500
+ SELECT t.name AS tag, ${options.project} AS project,
1501
+ COUNT(DISTINCT tm.id) AS module_count,
1502
+ COUNT(tc.id) AS test_count
1503
+ FROM test_cases tc
1504
+ JOIN test_modules tm ON tm.id = tc.module_id
1505
+ JOIN test_case_tags tct ON tct.test_case_id = tc.id
1506
+ JOIN tags t ON t.id = tct.tag_id
1507
+ WHERE tm.run_id = ${runId}
1508
+ GROUP BY t.name
1509
+ ORDER BY t.name
1510
+ `).map((r) => ({
1511
+ tag: r.tag,
1512
+ project: r.project,
1513
+ moduleCount: r.module_count,
1514
+ testCount: r.test_count
1515
+ }));
1516
+ }
1517
+ return (yield* sql`
1518
+ SELECT t.name AS tag, tr.project AS project,
1519
+ COUNT(DISTINCT tm.id) AS module_count,
1520
+ COUNT(tc.id) AS test_count
1521
+ FROM test_runs tr
1522
+ JOIN (
1523
+ SELECT project, MAX(timestamp) AS max_ts
1524
+ FROM test_runs
1525
+ GROUP BY project
1526
+ ) latest ON tr.project = latest.project AND tr.timestamp = latest.max_ts
1527
+ JOIN test_modules tm ON tm.run_id = tr.id
1528
+ JOIN test_cases tc ON tc.module_id = tm.id
1529
+ JOIN test_case_tags tct ON tct.test_case_id = tc.id
1530
+ JOIN tags t ON t.id = tct.tag_id
1531
+ GROUP BY tr.project, t.name
1532
+ ORDER BY tr.project, t.name
1533
+ `).map((r) => ({
1534
+ tag: r.tag,
1535
+ project: r.project,
1536
+ moduleCount: r.module_count,
1537
+ testCount: r.test_count
1538
+ }));
1539
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1540
+ operation: "read",
1541
+ table: "tags",
1542
+ reason: extractSqlReason(e)
1543
+ })));
1544
+ const listTestsForTag = (tag, options) => Effect.gen(function* () {
1545
+ yield* Effect.logDebug("listTestsForTag").pipe(Effect.annotateLogs({
1546
+ tag,
1547
+ project: options?.project
1548
+ }));
1549
+ if (options?.project) {
1550
+ const runs = yield* sql`SELECT id FROM test_runs
1551
+ WHERE project = ${options.project}
1552
+ ORDER BY timestamp DESC LIMIT 1`;
1553
+ if (runs.length === 0) return [];
1554
+ return (yield* sql`
1555
+ SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path AS file_path, tc.classification
1556
+ FROM test_cases tc
1557
+ JOIN test_modules tm ON tm.id = tc.module_id
1558
+ JOIN files f ON f.id = tm.file_id
1559
+ JOIN test_case_tags tct ON tct.test_case_id = tc.id
1560
+ JOIN tags t ON t.id = tct.tag_id
1561
+ WHERE tm.run_id = ${runs[0].id} AND t.name = ${tag}
1562
+ ORDER BY tc.full_name
1563
+ `).map(mapTestListRow);
1564
+ }
1565
+ return (yield* sql`
1566
+ SELECT tc.id, tc.full_name, tc.state, tc.duration, f.path AS file_path, tc.classification
1567
+ FROM test_runs tr
1568
+ JOIN (
1569
+ SELECT project, MAX(timestamp) AS max_ts
1570
+ FROM test_runs
1571
+ GROUP BY project
1572
+ ) latest ON tr.project = latest.project AND tr.timestamp = latest.max_ts
1573
+ JOIN test_modules tm ON tm.run_id = tr.id
1574
+ JOIN files f ON f.id = tm.file_id
1575
+ JOIN test_cases tc ON tc.module_id = tm.id
1576
+ JOIN test_case_tags tct ON tct.test_case_id = tc.id
1577
+ JOIN tags t ON t.id = tct.tag_id
1578
+ WHERE t.name = ${tag}
1579
+ ORDER BY tr.project, tc.full_name
1580
+ `).map(mapTestListRow);
1581
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1582
+ operation: "read",
1583
+ table: "tags",
1584
+ reason: extractSqlReason(e)
1585
+ })));
1586
+ return {
1587
+ getLatestRun,
1588
+ getRunsByProject,
1589
+ getHistory,
1590
+ getBaselines,
1591
+ getTrends,
1592
+ getFlaky,
1593
+ getPersistentFailures,
1594
+ getFileCoverage,
1595
+ getCoverage,
1596
+ getTestsForFile,
1597
+ getErrors,
1598
+ getNotes,
1599
+ getNoteById,
1600
+ searchNotes,
1601
+ getManifest,
1602
+ getSettings,
1603
+ getLatestSettings,
1604
+ getTestByFullName,
1605
+ listTests,
1606
+ listModules,
1607
+ listSuites,
1608
+ listSettings,
1609
+ getSessionById,
1610
+ getSessionByChatId,
1611
+ findSessionsByChatPrefix,
1612
+ findActiveSubagentSession,
1613
+ listSessions,
1614
+ searchTurns,
1615
+ computeAcceptanceMetrics,
1616
+ getFailureSignatureByHash,
1617
+ getTddTaskById,
1618
+ getGoalById,
1619
+ getGoalsByTddTask,
1620
+ getBehaviorById,
1621
+ getBehaviorsByGoal,
1622
+ getBehaviorsByTddTask,
1623
+ getBehaviorDependencies,
1624
+ resolveGoalIdForBehavior,
1625
+ getCurrentTddPhase,
1626
+ getTddArtifactWithContext,
1627
+ getCommitChanges,
1628
+ listTddTasksForSession,
1629
+ listTddArtifactsForTask,
1630
+ listHypotheses,
1631
+ findIdempotentResponse,
1632
+ getLatestTestCaseForSession,
1633
+ listTagInventory,
1634
+ listTestsForTag
1635
+ };
1636
+ }));
1637
+ function mapTestListRow(row) {
1638
+ return {
1639
+ id: row.id,
1640
+ fullName: row.full_name,
1641
+ state: row.state,
1642
+ duration: row.duration,
1643
+ module: row.file_path,
1644
+ classification: row.classification
1645
+ };
1646
+ }
1647
+ function mapNoteRow(row) {
1648
+ return {
1649
+ id: row.id,
1650
+ title: row.title,
1651
+ content: row.content,
1652
+ scope: row.scope,
1653
+ project: row.project,
1654
+ testFullName: row.test_full_name,
1655
+ modulePath: row.module_path,
1656
+ parentNoteId: row.parent_note_id,
1657
+ createdBy: row.created_by,
1658
+ expiresAt: row.expires_at,
1659
+ pinned: row.pinned === 1,
1660
+ createdAt: row.created_at,
1661
+ updatedAt: row.updated_at
1662
+ };
1663
+ }
1664
+
1665
+ //#endregion
1666
+ export { DataReaderLive };