@chrysb/alphaclaw 0.4.4 → 0.4.6-beta.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.
- package/README.md +21 -18
- package/lib/public/css/theme.css +29 -0
- package/lib/public/js/app.js +41 -2
- package/lib/public/js/components/badge.js +4 -0
- package/lib/public/js/components/doctor/findings-list.js +191 -0
- package/lib/public/js/components/doctor/fix-card-modal.js +144 -0
- package/lib/public/js/components/doctor/general-warning.js +37 -0
- package/lib/public/js/components/doctor/helpers.js +169 -0
- package/lib/public/js/components/doctor/index.js +536 -0
- package/lib/public/js/components/doctor/summary-cards.js +24 -0
- package/lib/public/js/lib/api.js +79 -0
- package/lib/server/commands.js +8 -4
- package/lib/server/constants.js +22 -26
- package/lib/server/db/doctor/index.js +529 -0
- package/lib/server/db/doctor/schema.js +69 -0
- package/lib/server/doctor/constants.js +43 -0
- package/lib/server/doctor/normalize.js +214 -0
- package/lib/server/doctor/prompt.js +89 -0
- package/lib/server/doctor/service.js +392 -0
- package/lib/server/doctor/workspace-fingerprint.js +126 -0
- package/lib/server/gmail-push.js +102 -6
- package/lib/server/gmail-watch.js +5 -20
- package/lib/server/helpers.js +5 -21
- package/lib/server/routes/doctor.js +123 -0
- package/lib/server/routes/google.js +2 -10
- package/lib/server/routes/system.js +7 -1
- package/lib/server/routes/telegram.js +3 -14
- package/lib/server/routes/usage.js +1 -5
- package/lib/server/routes/webhooks.js +2 -6
- package/lib/server/utils/boolean.js +22 -0
- package/lib/server/utils/json.js +77 -0
- package/lib/server/utils/network.js +5 -0
- package/lib/server/utils/number.js +8 -0
- package/lib/server/utils/shell.js +16 -0
- package/lib/server/webhook-middleware.js +1 -2
- package/lib/server.js +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
4
|
+
const { createSchema } = require("./schema");
|
|
5
|
+
const {
|
|
6
|
+
kDoctorCardStatus,
|
|
7
|
+
kDoctorDefaultRunsLimit,
|
|
8
|
+
kDoctorMaxRunsLimit,
|
|
9
|
+
kDoctorPriority,
|
|
10
|
+
kDoctorRunStatus,
|
|
11
|
+
} = require("../../doctor/constants");
|
|
12
|
+
|
|
13
|
+
let db = null;
|
|
14
|
+
const kDoctorInitialBaselineMetaKey = "initial_workspace_baseline";
|
|
15
|
+
|
|
16
|
+
const ensureDb = () => {
|
|
17
|
+
if (!db) throw new Error("Doctor DB not initialized");
|
|
18
|
+
return db;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const parseJsonText = (value, fallbackValue) => {
|
|
22
|
+
if (typeof value !== "string" || !value) return fallbackValue;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return fallbackValue;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const buildPriorityCounts = (cards = []) => ({
|
|
31
|
+
P0: cards.filter((card) => card.priority === kDoctorPriority.P0).length,
|
|
32
|
+
P1: cards.filter((card) => card.priority === kDoctorPriority.P1).length,
|
|
33
|
+
P2: cards.filter((card) => card.priority === kDoctorPriority.P2).length,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const buildStatusCounts = (cards = []) => ({
|
|
37
|
+
open: cards.filter((card) => card.status === kDoctorCardStatus.open).length,
|
|
38
|
+
dismissed: cards.filter((card) => card.status === kDoctorCardStatus.dismissed).length,
|
|
39
|
+
fixed: cards.filter((card) => card.status === kDoctorCardStatus.fixed).length,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const toCardModel = (row) => {
|
|
43
|
+
if (!row) return null;
|
|
44
|
+
return {
|
|
45
|
+
id: Number(row.id || 0),
|
|
46
|
+
runId: Number(row.run_id || 0),
|
|
47
|
+
createdAt: row.created_at || null,
|
|
48
|
+
updatedAt: row.updated_at || null,
|
|
49
|
+
priority: row.priority || kDoctorPriority.P2,
|
|
50
|
+
category: row.category || "workspace",
|
|
51
|
+
title: row.title || "",
|
|
52
|
+
summary: row.summary || "",
|
|
53
|
+
recommendation: row.recommendation || "",
|
|
54
|
+
evidence: parseJsonText(row.evidence_json, []),
|
|
55
|
+
targetPaths: parseJsonText(row.target_paths_json, []),
|
|
56
|
+
fixPrompt: row.fix_prompt || "",
|
|
57
|
+
status: row.status || kDoctorCardStatus.open,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const attachRunCounts = (run, cards = []) =>
|
|
62
|
+
run
|
|
63
|
+
? {
|
|
64
|
+
...run,
|
|
65
|
+
cardCount: cards.length,
|
|
66
|
+
priorityCounts: buildPriorityCounts(cards),
|
|
67
|
+
statusCounts: buildStatusCounts(cards),
|
|
68
|
+
}
|
|
69
|
+
: null;
|
|
70
|
+
|
|
71
|
+
const getCardsByRunId = (runId) => {
|
|
72
|
+
const database = ensureDb();
|
|
73
|
+
const rows = database
|
|
74
|
+
.prepare(`
|
|
75
|
+
SELECT
|
|
76
|
+
id,
|
|
77
|
+
run_id,
|
|
78
|
+
created_at,
|
|
79
|
+
updated_at,
|
|
80
|
+
priority,
|
|
81
|
+
category,
|
|
82
|
+
title,
|
|
83
|
+
summary,
|
|
84
|
+
recommendation,
|
|
85
|
+
evidence_json,
|
|
86
|
+
target_paths_json,
|
|
87
|
+
fix_prompt,
|
|
88
|
+
status
|
|
89
|
+
FROM doctor_cards
|
|
90
|
+
WHERE run_id = $run_id
|
|
91
|
+
ORDER BY
|
|
92
|
+
CASE priority
|
|
93
|
+
WHEN 'P0' THEN 0
|
|
94
|
+
WHEN 'P1' THEN 1
|
|
95
|
+
ELSE 2
|
|
96
|
+
END ASC,
|
|
97
|
+
created_at DESC
|
|
98
|
+
`)
|
|
99
|
+
.all({ $run_id: Number(runId || 0) });
|
|
100
|
+
return rows.map(toCardModel);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const listDoctorCards = ({ runId } = {}) => {
|
|
104
|
+
const database = ensureDb();
|
|
105
|
+
const hasRunFilter =
|
|
106
|
+
runId !== undefined &&
|
|
107
|
+
runId !== null &&
|
|
108
|
+
String(runId || "").trim() !== "" &&
|
|
109
|
+
String(runId || "").trim().toLowerCase() !== "all";
|
|
110
|
+
const rows = database
|
|
111
|
+
.prepare(`
|
|
112
|
+
SELECT
|
|
113
|
+
c.id,
|
|
114
|
+
c.run_id,
|
|
115
|
+
c.created_at,
|
|
116
|
+
c.updated_at,
|
|
117
|
+
c.priority,
|
|
118
|
+
c.category,
|
|
119
|
+
c.title,
|
|
120
|
+
c.summary,
|
|
121
|
+
c.recommendation,
|
|
122
|
+
c.evidence_json,
|
|
123
|
+
c.target_paths_json,
|
|
124
|
+
c.fix_prompt,
|
|
125
|
+
c.status,
|
|
126
|
+
r.started_at AS run_started_at,
|
|
127
|
+
r.completed_at AS run_completed_at,
|
|
128
|
+
r.status AS run_status
|
|
129
|
+
FROM doctor_cards c
|
|
130
|
+
INNER JOIN doctor_runs r ON r.id = c.run_id
|
|
131
|
+
${hasRunFilter ? "WHERE c.run_id = $run_id" : ""}
|
|
132
|
+
ORDER BY
|
|
133
|
+
CASE c.status
|
|
134
|
+
WHEN 'open' THEN 0
|
|
135
|
+
WHEN 'dismissed' THEN 1
|
|
136
|
+
ELSE 2
|
|
137
|
+
END ASC,
|
|
138
|
+
CASE c.priority
|
|
139
|
+
WHEN 'P0' THEN 0
|
|
140
|
+
WHEN 'P1' THEN 1
|
|
141
|
+
ELSE 2
|
|
142
|
+
END ASC,
|
|
143
|
+
c.created_at DESC
|
|
144
|
+
`)
|
|
145
|
+
.all(hasRunFilter ? { $run_id: Number(runId || 0) } : {});
|
|
146
|
+
return rows.map((row) => ({
|
|
147
|
+
...toCardModel(row),
|
|
148
|
+
runStartedAt: row.run_started_at || null,
|
|
149
|
+
runCompletedAt: row.run_completed_at || null,
|
|
150
|
+
runStatus: row.run_status || kDoctorRunStatus.failed,
|
|
151
|
+
}));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const toRunModel = (row) => {
|
|
155
|
+
if (!row) return null;
|
|
156
|
+
return {
|
|
157
|
+
id: Number(row.id || 0),
|
|
158
|
+
startedAt: row.started_at || null,
|
|
159
|
+
completedAt: row.completed_at || null,
|
|
160
|
+
status: row.status || kDoctorRunStatus.failed,
|
|
161
|
+
engine: row.engine || "",
|
|
162
|
+
workspaceRoot: row.workspace_root || "",
|
|
163
|
+
workspaceFingerprint: row.workspace_fingerprint || "",
|
|
164
|
+
workspaceManifest: parseJsonText(row.workspace_manifest_json, null),
|
|
165
|
+
promptVersion: row.prompt_version || "",
|
|
166
|
+
summary: row.summary || "",
|
|
167
|
+
rawResult: parseJsonText(row.raw_result_json, null),
|
|
168
|
+
error: row.error || "",
|
|
169
|
+
reusedFromRunId: Number(row.reused_from_run_id || 0),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const initDoctorDb = ({ rootDir }) => {
|
|
174
|
+
const dbDir = path.join(rootDir, "db");
|
|
175
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
176
|
+
const dbPath = path.join(dbDir, "doctor.db");
|
|
177
|
+
db = new DatabaseSync(dbPath);
|
|
178
|
+
createSchema(db);
|
|
179
|
+
markIncompleteRunsFailed();
|
|
180
|
+
return { path: dbPath };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getDoctorMeta = (key) => {
|
|
184
|
+
const database = ensureDb();
|
|
185
|
+
const row = database
|
|
186
|
+
.prepare(`
|
|
187
|
+
SELECT
|
|
188
|
+
key,
|
|
189
|
+
value_json,
|
|
190
|
+
updated_at
|
|
191
|
+
FROM doctor_meta
|
|
192
|
+
WHERE key = $key
|
|
193
|
+
LIMIT 1
|
|
194
|
+
`)
|
|
195
|
+
.get({ $key: String(key || "") });
|
|
196
|
+
if (!row) return null;
|
|
197
|
+
return {
|
|
198
|
+
key: row.key || "",
|
|
199
|
+
value: parseJsonText(row.value_json, null),
|
|
200
|
+
updatedAt: row.updated_at || null,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const setDoctorMeta = ({ key, value = null }) => {
|
|
205
|
+
const database = ensureDb();
|
|
206
|
+
database
|
|
207
|
+
.prepare(`
|
|
208
|
+
INSERT INTO doctor_meta (
|
|
209
|
+
key,
|
|
210
|
+
value_json,
|
|
211
|
+
updated_at
|
|
212
|
+
) VALUES (
|
|
213
|
+
$key,
|
|
214
|
+
$value_json,
|
|
215
|
+
strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
216
|
+
)
|
|
217
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
218
|
+
value_json = excluded.value_json,
|
|
219
|
+
updated_at = excluded.updated_at
|
|
220
|
+
`)
|
|
221
|
+
.run({
|
|
222
|
+
$key: String(key || ""),
|
|
223
|
+
$value_json: value == null ? null : JSON.stringify(value),
|
|
224
|
+
});
|
|
225
|
+
return getDoctorMeta(key);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const getInitialWorkspaceBaseline = () => getDoctorMeta(kDoctorInitialBaselineMetaKey)?.value || null;
|
|
229
|
+
|
|
230
|
+
const setInitialWorkspaceBaseline = (baseline) =>
|
|
231
|
+
setDoctorMeta({
|
|
232
|
+
key: kDoctorInitialBaselineMetaKey,
|
|
233
|
+
value: baseline,
|
|
234
|
+
})?.value || null;
|
|
235
|
+
|
|
236
|
+
const markIncompleteRunsFailed = (errorMessage = "Doctor run interrupted before completion") => {
|
|
237
|
+
const database = ensureDb();
|
|
238
|
+
const result = database
|
|
239
|
+
.prepare(`
|
|
240
|
+
UPDATE doctor_runs
|
|
241
|
+
SET
|
|
242
|
+
status = $status,
|
|
243
|
+
completed_at = COALESCE(completed_at, strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
244
|
+
error = COALESCE(NULLIF(error, ''), $error)
|
|
245
|
+
WHERE status = $running_status
|
|
246
|
+
`)
|
|
247
|
+
.run({
|
|
248
|
+
$status: kDoctorRunStatus.failed,
|
|
249
|
+
$running_status: kDoctorRunStatus.running,
|
|
250
|
+
$error: String(errorMessage || ""),
|
|
251
|
+
});
|
|
252
|
+
return Number(result.changes || 0);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const createDoctorRun = ({
|
|
256
|
+
status = kDoctorRunStatus.running,
|
|
257
|
+
engine,
|
|
258
|
+
workspaceRoot,
|
|
259
|
+
workspaceFingerprint = "",
|
|
260
|
+
workspaceManifest = null,
|
|
261
|
+
promptVersion,
|
|
262
|
+
reusedFromRunId = 0,
|
|
263
|
+
}) => {
|
|
264
|
+
const database = ensureDb();
|
|
265
|
+
const result = database
|
|
266
|
+
.prepare(`
|
|
267
|
+
INSERT INTO doctor_runs (
|
|
268
|
+
status,
|
|
269
|
+
engine,
|
|
270
|
+
workspace_root,
|
|
271
|
+
workspace_fingerprint,
|
|
272
|
+
workspace_manifest_json,
|
|
273
|
+
prompt_version,
|
|
274
|
+
reused_from_run_id
|
|
275
|
+
) VALUES (
|
|
276
|
+
$status,
|
|
277
|
+
$engine,
|
|
278
|
+
$workspace_root,
|
|
279
|
+
$workspace_fingerprint,
|
|
280
|
+
$workspace_manifest_json,
|
|
281
|
+
$prompt_version,
|
|
282
|
+
$reused_from_run_id
|
|
283
|
+
)
|
|
284
|
+
`)
|
|
285
|
+
.run({
|
|
286
|
+
$status: String(status || kDoctorRunStatus.running),
|
|
287
|
+
$engine: String(engine || ""),
|
|
288
|
+
$workspace_root: String(workspaceRoot || ""),
|
|
289
|
+
$workspace_fingerprint: String(workspaceFingerprint || ""),
|
|
290
|
+
$workspace_manifest_json: workspaceManifest == null ? null : JSON.stringify(workspaceManifest),
|
|
291
|
+
$prompt_version: String(promptVersion || ""),
|
|
292
|
+
$reused_from_run_id: Number(reusedFromRunId || 0),
|
|
293
|
+
});
|
|
294
|
+
return Number(result.lastInsertRowid || 0);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const completeDoctorRun = ({
|
|
298
|
+
id,
|
|
299
|
+
status,
|
|
300
|
+
summary = "",
|
|
301
|
+
rawResult = null,
|
|
302
|
+
error = "",
|
|
303
|
+
}) => {
|
|
304
|
+
const database = ensureDb();
|
|
305
|
+
const result = database
|
|
306
|
+
.prepare(`
|
|
307
|
+
UPDATE doctor_runs
|
|
308
|
+
SET
|
|
309
|
+
completed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
|
|
310
|
+
status = $status,
|
|
311
|
+
summary = $summary,
|
|
312
|
+
raw_result_json = $raw_result_json,
|
|
313
|
+
error = $error
|
|
314
|
+
WHERE id = $id
|
|
315
|
+
`)
|
|
316
|
+
.run({
|
|
317
|
+
$id: Number(id || 0),
|
|
318
|
+
$status: String(status || kDoctorRunStatus.failed),
|
|
319
|
+
$summary: String(summary || ""),
|
|
320
|
+
$raw_result_json: rawResult == null ? null : JSON.stringify(rawResult),
|
|
321
|
+
$error: String(error || ""),
|
|
322
|
+
});
|
|
323
|
+
return Number(result.changes || 0);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const insertDoctorCards = ({ runId, cards = [] }) => {
|
|
327
|
+
const database = ensureDb();
|
|
328
|
+
database.exec("BEGIN");
|
|
329
|
+
try {
|
|
330
|
+
const stmt = database.prepare(`
|
|
331
|
+
INSERT INTO doctor_cards (
|
|
332
|
+
run_id,
|
|
333
|
+
priority,
|
|
334
|
+
category,
|
|
335
|
+
title,
|
|
336
|
+
summary,
|
|
337
|
+
recommendation,
|
|
338
|
+
evidence_json,
|
|
339
|
+
target_paths_json,
|
|
340
|
+
fix_prompt,
|
|
341
|
+
status
|
|
342
|
+
) VALUES (
|
|
343
|
+
$run_id,
|
|
344
|
+
$priority,
|
|
345
|
+
$category,
|
|
346
|
+
$title,
|
|
347
|
+
$summary,
|
|
348
|
+
$recommendation,
|
|
349
|
+
$evidence_json,
|
|
350
|
+
$target_paths_json,
|
|
351
|
+
$fix_prompt,
|
|
352
|
+
$status
|
|
353
|
+
)
|
|
354
|
+
`);
|
|
355
|
+
for (const card of cards) {
|
|
356
|
+
stmt.run({
|
|
357
|
+
$run_id: Number(runId || 0),
|
|
358
|
+
$priority: String(card?.priority || kDoctorPriority.P2),
|
|
359
|
+
$category: String(card?.category || "workspace"),
|
|
360
|
+
$title: String(card?.title || ""),
|
|
361
|
+
$summary: String(card?.summary || ""),
|
|
362
|
+
$recommendation: String(card?.recommendation || ""),
|
|
363
|
+
$evidence_json: JSON.stringify(card?.evidence || []),
|
|
364
|
+
$target_paths_json: JSON.stringify(card?.targetPaths || []),
|
|
365
|
+
$fix_prompt: String(card?.fixPrompt || ""),
|
|
366
|
+
$status: String(card?.status || kDoctorCardStatus.open),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
database.exec("COMMIT");
|
|
370
|
+
} catch (error) {
|
|
371
|
+
database.exec("ROLLBACK");
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const getDoctorRun = (id) => {
|
|
377
|
+
const database = ensureDb();
|
|
378
|
+
const row = database
|
|
379
|
+
.prepare(`
|
|
380
|
+
SELECT
|
|
381
|
+
id,
|
|
382
|
+
started_at,
|
|
383
|
+
completed_at,
|
|
384
|
+
status,
|
|
385
|
+
engine,
|
|
386
|
+
workspace_root,
|
|
387
|
+
workspace_fingerprint,
|
|
388
|
+
workspace_manifest_json,
|
|
389
|
+
prompt_version,
|
|
390
|
+
summary,
|
|
391
|
+
raw_result_json,
|
|
392
|
+
error,
|
|
393
|
+
reused_from_run_id
|
|
394
|
+
FROM doctor_runs
|
|
395
|
+
WHERE id = $id
|
|
396
|
+
LIMIT 1
|
|
397
|
+
`)
|
|
398
|
+
.get({ $id: Number(id || 0) });
|
|
399
|
+
const run = toRunModel(row);
|
|
400
|
+
if (!run) return null;
|
|
401
|
+
return attachRunCounts(run, getCardsByRunId(run.id));
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const getLatestDoctorRun = () => {
|
|
405
|
+
const database = ensureDb();
|
|
406
|
+
const row = database
|
|
407
|
+
.prepare(`
|
|
408
|
+
SELECT
|
|
409
|
+
id,
|
|
410
|
+
started_at,
|
|
411
|
+
completed_at,
|
|
412
|
+
status,
|
|
413
|
+
engine,
|
|
414
|
+
workspace_root,
|
|
415
|
+
workspace_fingerprint,
|
|
416
|
+
workspace_manifest_json,
|
|
417
|
+
prompt_version,
|
|
418
|
+
summary,
|
|
419
|
+
raw_result_json,
|
|
420
|
+
error,
|
|
421
|
+
reused_from_run_id
|
|
422
|
+
FROM doctor_runs
|
|
423
|
+
ORDER BY started_at DESC
|
|
424
|
+
LIMIT 1
|
|
425
|
+
`)
|
|
426
|
+
.get();
|
|
427
|
+
const run = toRunModel(row);
|
|
428
|
+
if (!run) return null;
|
|
429
|
+
return attachRunCounts(run, getCardsByRunId(run.id));
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const listDoctorRuns = ({ limit = kDoctorDefaultRunsLimit } = {}) => {
|
|
433
|
+
const database = ensureDb();
|
|
434
|
+
const safeLimit = Math.max(
|
|
435
|
+
1,
|
|
436
|
+
Math.min(Number.parseInt(String(limit || kDoctorDefaultRunsLimit), 10) || kDoctorDefaultRunsLimit, kDoctorMaxRunsLimit),
|
|
437
|
+
);
|
|
438
|
+
const rows = database
|
|
439
|
+
.prepare(`
|
|
440
|
+
SELECT
|
|
441
|
+
id,
|
|
442
|
+
started_at,
|
|
443
|
+
completed_at,
|
|
444
|
+
status,
|
|
445
|
+
engine,
|
|
446
|
+
workspace_root,
|
|
447
|
+
workspace_fingerprint,
|
|
448
|
+
workspace_manifest_json,
|
|
449
|
+
prompt_version,
|
|
450
|
+
summary,
|
|
451
|
+
raw_result_json,
|
|
452
|
+
error,
|
|
453
|
+
reused_from_run_id
|
|
454
|
+
FROM doctor_runs
|
|
455
|
+
ORDER BY started_at DESC
|
|
456
|
+
LIMIT $limit
|
|
457
|
+
`)
|
|
458
|
+
.all({ $limit: safeLimit });
|
|
459
|
+
return rows.map((row) => {
|
|
460
|
+
const run = toRunModel(row);
|
|
461
|
+
return attachRunCounts(run, getCardsByRunId(run.id));
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const getDoctorCard = (id) => {
|
|
466
|
+
const database = ensureDb();
|
|
467
|
+
const row = database
|
|
468
|
+
.prepare(`
|
|
469
|
+
SELECT
|
|
470
|
+
id,
|
|
471
|
+
run_id,
|
|
472
|
+
created_at,
|
|
473
|
+
updated_at,
|
|
474
|
+
priority,
|
|
475
|
+
category,
|
|
476
|
+
title,
|
|
477
|
+
summary,
|
|
478
|
+
recommendation,
|
|
479
|
+
evidence_json,
|
|
480
|
+
target_paths_json,
|
|
481
|
+
fix_prompt,
|
|
482
|
+
status
|
|
483
|
+
FROM doctor_cards
|
|
484
|
+
WHERE id = $id
|
|
485
|
+
LIMIT 1
|
|
486
|
+
`)
|
|
487
|
+
.get({ $id: Number(id || 0) });
|
|
488
|
+
return toCardModel(row);
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const updateDoctorCardStatus = ({ id, status }) => {
|
|
492
|
+
const database = ensureDb();
|
|
493
|
+
const nextStatus =
|
|
494
|
+
status === kDoctorCardStatus.fixed || status === kDoctorCardStatus.dismissed
|
|
495
|
+
? status
|
|
496
|
+
: kDoctorCardStatus.open;
|
|
497
|
+
const result = database
|
|
498
|
+
.prepare(`
|
|
499
|
+
UPDATE doctor_cards
|
|
500
|
+
SET
|
|
501
|
+
status = $status,
|
|
502
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
503
|
+
WHERE id = $id
|
|
504
|
+
`)
|
|
505
|
+
.run({
|
|
506
|
+
$id: Number(id || 0),
|
|
507
|
+
$status: nextStatus,
|
|
508
|
+
});
|
|
509
|
+
return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
module.exports = {
|
|
513
|
+
initDoctorDb,
|
|
514
|
+
markIncompleteRunsFailed,
|
|
515
|
+
getDoctorMeta,
|
|
516
|
+
setDoctorMeta,
|
|
517
|
+
getInitialWorkspaceBaseline,
|
|
518
|
+
setInitialWorkspaceBaseline,
|
|
519
|
+
createDoctorRun,
|
|
520
|
+
completeDoctorRun,
|
|
521
|
+
insertDoctorCards,
|
|
522
|
+
getDoctorRun,
|
|
523
|
+
getLatestDoctorRun,
|
|
524
|
+
listDoctorRuns,
|
|
525
|
+
listDoctorCards,
|
|
526
|
+
getDoctorCardsByRunId: getCardsByRunId,
|
|
527
|
+
getDoctorCard,
|
|
528
|
+
updateDoctorCardStatus,
|
|
529
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const hasColumn = (database, tableName, columnName) => {
|
|
2
|
+
const rows = database.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
3
|
+
return rows.some((row) => row.name === columnName);
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const ensureColumn = (database, tableName, columnName, definition) => {
|
|
7
|
+
if (hasColumn(database, tableName, columnName)) return;
|
|
8
|
+
database.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition};`);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const createSchema = (database) => {
|
|
12
|
+
database.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS doctor_runs (
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
16
|
+
completed_at TEXT,
|
|
17
|
+
status TEXT NOT NULL,
|
|
18
|
+
engine TEXT NOT NULL,
|
|
19
|
+
workspace_root TEXT NOT NULL,
|
|
20
|
+
workspace_fingerprint TEXT,
|
|
21
|
+
workspace_manifest_json TEXT,
|
|
22
|
+
prompt_version TEXT NOT NULL,
|
|
23
|
+
summary TEXT,
|
|
24
|
+
raw_result_json TEXT,
|
|
25
|
+
error TEXT,
|
|
26
|
+
reused_from_run_id INTEGER
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
database.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS doctor_meta (
|
|
31
|
+
key TEXT PRIMARY KEY,
|
|
32
|
+
value_json TEXT,
|
|
33
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
34
|
+
);
|
|
35
|
+
`);
|
|
36
|
+
database.exec(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS doctor_cards (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
run_id INTEGER NOT NULL,
|
|
40
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
41
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
42
|
+
priority TEXT NOT NULL,
|
|
43
|
+
category TEXT NOT NULL,
|
|
44
|
+
title TEXT NOT NULL,
|
|
45
|
+
summary TEXT,
|
|
46
|
+
recommendation TEXT NOT NULL,
|
|
47
|
+
evidence_json TEXT,
|
|
48
|
+
target_paths_json TEXT,
|
|
49
|
+
fix_prompt TEXT NOT NULL,
|
|
50
|
+
status TEXT NOT NULL,
|
|
51
|
+
FOREIGN KEY (run_id) REFERENCES doctor_runs(id) ON DELETE CASCADE
|
|
52
|
+
);
|
|
53
|
+
`);
|
|
54
|
+
database.exec(`
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_doctor_runs_started_at
|
|
56
|
+
ON doctor_runs(started_at DESC);
|
|
57
|
+
`);
|
|
58
|
+
ensureColumn(database, "doctor_runs", "workspace_fingerprint", "TEXT");
|
|
59
|
+
ensureColumn(database, "doctor_runs", "workspace_manifest_json", "TEXT");
|
|
60
|
+
ensureColumn(database, "doctor_runs", "reused_from_run_id", "INTEGER");
|
|
61
|
+
database.exec(`
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_doctor_cards_run_id
|
|
63
|
+
ON doctor_cards(run_id, created_at DESC);
|
|
64
|
+
`);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createSchema,
|
|
69
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const kDoctorPromptVersion = "doctor-v1";
|
|
2
|
+
const kDoctorRunStatus = {
|
|
3
|
+
running: "running",
|
|
4
|
+
completed: "completed",
|
|
5
|
+
failed: "failed",
|
|
6
|
+
};
|
|
7
|
+
const kDoctorCardStatus = {
|
|
8
|
+
open: "open",
|
|
9
|
+
dismissed: "dismissed",
|
|
10
|
+
fixed: "fixed",
|
|
11
|
+
};
|
|
12
|
+
const kDoctorPriority = {
|
|
13
|
+
P0: "P0",
|
|
14
|
+
P1: "P1",
|
|
15
|
+
P2: "P2",
|
|
16
|
+
};
|
|
17
|
+
const kDoctorEngine = {
|
|
18
|
+
gatewayAgent: "gateway_agent",
|
|
19
|
+
acpRuntime: "acp_runtime",
|
|
20
|
+
agentMessageFallback: "agent_message_fallback",
|
|
21
|
+
manualImport: "manual_import",
|
|
22
|
+
deterministicReuse: "deterministic_reuse",
|
|
23
|
+
};
|
|
24
|
+
const kDoctorStaleThresholdMs = 7 * 24 * 60 * 60 * 1000;
|
|
25
|
+
const kDoctorMeaningfulChangeScoreThreshold = 4;
|
|
26
|
+
const kDoctorRunTimeoutMs = 10 * 60 * 1000;
|
|
27
|
+
const kDoctorDefaultRunsLimit = 10;
|
|
28
|
+
const kDoctorMaxRunsLimit = 50;
|
|
29
|
+
const kDoctorMaxCardsPerRun = 12;
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
kDoctorPromptVersion,
|
|
33
|
+
kDoctorRunStatus,
|
|
34
|
+
kDoctorCardStatus,
|
|
35
|
+
kDoctorPriority,
|
|
36
|
+
kDoctorEngine,
|
|
37
|
+
kDoctorStaleThresholdMs,
|
|
38
|
+
kDoctorMeaningfulChangeScoreThreshold,
|
|
39
|
+
kDoctorRunTimeoutMs,
|
|
40
|
+
kDoctorDefaultRunsLimit,
|
|
41
|
+
kDoctorMaxRunsLimit,
|
|
42
|
+
kDoctorMaxCardsPerRun,
|
|
43
|
+
};
|