@payconductor/houndsman 0.3.0 → 0.4.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 +18 -6
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/help.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +155 -120
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +32 -71
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +175 -128
- package/src/core/paths.ts +23 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +28 -53
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +95 -15
- package/src/server/run-repository.ts +144 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
- package/src/tests/baseline.test.ts +76 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider SQLite do houndsman (biblioteca) — implementação principal.
|
|
3
|
+
*
|
|
4
|
+
* Sync (`bun:sqlite`), WAL e escrita transacional do estado de triagem. O
|
|
5
|
+
* staging de briefs/findings fica fora do banco (`storage/work-area.ts`); aqui
|
|
6
|
+
* vivem reviews, findings e artefatos.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { mkdirSync } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { emptyState, type FindingsStateFile, reorder } from "../../findings-state";
|
|
14
|
+
import { systemClock } from "../../services/clock";
|
|
15
|
+
import type {
|
|
16
|
+
ArtifactRecord,
|
|
17
|
+
CreateReviewInput,
|
|
18
|
+
ReviewPatch,
|
|
19
|
+
ReviewRecord,
|
|
20
|
+
StorageProvider,
|
|
21
|
+
WriteArtifactInput,
|
|
22
|
+
} from "../types";
|
|
23
|
+
import {
|
|
24
|
+
type ArtifactRow,
|
|
25
|
+
artifactFromRow,
|
|
26
|
+
type FindingRow,
|
|
27
|
+
findingFromRow,
|
|
28
|
+
type HistoryRow,
|
|
29
|
+
type ReviewRow,
|
|
30
|
+
reviewFromRow,
|
|
31
|
+
} from "./mappers";
|
|
32
|
+
import { migrate } from "./schema";
|
|
33
|
+
|
|
34
|
+
/** Colunas mutáveis de review (camel → snake). */
|
|
35
|
+
const REVIEW_PATCH_COLUMNS: Record<
|
|
36
|
+
Exclude<keyof ReviewPatch, "resetStartedAt">,
|
|
37
|
+
string
|
|
38
|
+
> = {
|
|
39
|
+
branch: "branch",
|
|
40
|
+
slug: "slug",
|
|
41
|
+
base: "base",
|
|
42
|
+
head: "head",
|
|
43
|
+
headSha: "head_sha",
|
|
44
|
+
reviewedSha: "reviewed_sha",
|
|
45
|
+
reviewedAt: "reviewed_at",
|
|
46
|
+
status: "status",
|
|
47
|
+
startedAt: "started_at",
|
|
48
|
+
updatedAt: "updated_at",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Provider SQLite (arquivo ou `:memory:`). */
|
|
52
|
+
export class SQLiteStorageProvider implements StorageProvider {
|
|
53
|
+
readonly kind = "sqlite" as const;
|
|
54
|
+
private readonly db: Database;
|
|
55
|
+
private readonly path: string;
|
|
56
|
+
|
|
57
|
+
constructor(path: string) {
|
|
58
|
+
this.path = path;
|
|
59
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
60
|
+
this.db = new Database(path);
|
|
61
|
+
migrate(this.db, path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -----------------------------------------------------------------------
|
|
65
|
+
// Reviews
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
getOrCreateReview(input: CreateReviewInput): ReviewRecord {
|
|
69
|
+
const existing = this.findReview(input.repo.key, input.branch);
|
|
70
|
+
if (existing) {
|
|
71
|
+
// O repo pode ter sido movido; a identidade continua a mesma.
|
|
72
|
+
if (
|
|
73
|
+
existing.repoRoot !== input.repo.root ||
|
|
74
|
+
existing.repoRemote !== input.repo.remote
|
|
75
|
+
) {
|
|
76
|
+
this.db.run(
|
|
77
|
+
"UPDATE reviews SET repo_root = ?, repo_remote = ? WHERE id = ?",
|
|
78
|
+
[input.repo.root, input.repo.remote ?? null, existing.id],
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return this.requireReview(existing.id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const now = input.now ?? systemClock();
|
|
85
|
+
const record: ReviewRecord = {
|
|
86
|
+
id: randomUUID(),
|
|
87
|
+
repoKey: input.repo.key,
|
|
88
|
+
repoRoot: input.repo.root,
|
|
89
|
+
repoRemote: input.repo.remote,
|
|
90
|
+
branch: input.branch,
|
|
91
|
+
slug: input.slug,
|
|
92
|
+
base: input.base,
|
|
93
|
+
head: input.head,
|
|
94
|
+
status: null,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
updatedAt: now,
|
|
97
|
+
};
|
|
98
|
+
this.db.run(
|
|
99
|
+
`INSERT INTO reviews
|
|
100
|
+
(id, repo_key, repo_root, repo_remote, branch, slug, base, head,
|
|
101
|
+
created_at, updated_at)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
103
|
+
[
|
|
104
|
+
record.id,
|
|
105
|
+
record.repoKey,
|
|
106
|
+
record.repoRoot,
|
|
107
|
+
record.repoRemote ?? null,
|
|
108
|
+
record.branch,
|
|
109
|
+
record.slug,
|
|
110
|
+
record.base ?? null,
|
|
111
|
+
record.head ?? null,
|
|
112
|
+
record.createdAt,
|
|
113
|
+
record.updatedAt,
|
|
114
|
+
],
|
|
115
|
+
);
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
findReview(repoKey: string, branch: string): ReviewRecord | null {
|
|
120
|
+
const row = this.db
|
|
121
|
+
.query("SELECT * FROM reviews WHERE repo_key = ? AND branch = ?")
|
|
122
|
+
.get(repoKey, branch) as ReviewRow | null;
|
|
123
|
+
return row ? reviewFromRow(row) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getReview(id: string): ReviewRecord | null {
|
|
127
|
+
const row = this.db
|
|
128
|
+
.query("SELECT * FROM reviews WHERE id = ?")
|
|
129
|
+
.get(id) as ReviewRow | null;
|
|
130
|
+
return row ? reviewFromRow(row) : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
listReviews(repoKey?: string): ReviewRecord[] {
|
|
134
|
+
const rows = (
|
|
135
|
+
repoKey
|
|
136
|
+
? this.db
|
|
137
|
+
.query(
|
|
138
|
+
"SELECT * FROM reviews WHERE repo_key = ? ORDER BY updated_at DESC",
|
|
139
|
+
)
|
|
140
|
+
.all(repoKey)
|
|
141
|
+
: this.db.query("SELECT * FROM reviews ORDER BY updated_at DESC").all()
|
|
142
|
+
) as ReviewRow[];
|
|
143
|
+
return rows.map(reviewFromRow);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
updateReview(id: string, patch: ReviewPatch): ReviewRecord | null {
|
|
147
|
+
if (!this.getReview(id)) return null;
|
|
148
|
+
const now = patch.updatedAt ?? systemClock();
|
|
149
|
+
const sets: string[] = [];
|
|
150
|
+
const params: (string | number | null)[] = [];
|
|
151
|
+
|
|
152
|
+
for (const [key, column] of Object.entries(REVIEW_PATCH_COLUMNS)) {
|
|
153
|
+
const value = patch[key as keyof ReviewPatch];
|
|
154
|
+
if (value === undefined) continue;
|
|
155
|
+
sets.push(`${column} = ?`);
|
|
156
|
+
params.push(value as string | number | null);
|
|
157
|
+
}
|
|
158
|
+
if (patch.resetStartedAt) {
|
|
159
|
+
sets.push("started_at = ?");
|
|
160
|
+
params.push(now);
|
|
161
|
+
}
|
|
162
|
+
if (patch.updatedAt === undefined) {
|
|
163
|
+
sets.push("updated_at = ?");
|
|
164
|
+
params.push(now);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (sets.length > 0) {
|
|
168
|
+
params.push(id);
|
|
169
|
+
this.db.run(`UPDATE reviews SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
170
|
+
}
|
|
171
|
+
return this.getReview(id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
deleteReview(id: string): void {
|
|
175
|
+
this.db.run("DELETE FROM reviews WHERE id = ?", [id]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -----------------------------------------------------------------------
|
|
179
|
+
// Findings
|
|
180
|
+
// -----------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
readFindings(reviewId: string): FindingsStateFile | null {
|
|
183
|
+
const review = this.getReview(reviewId);
|
|
184
|
+
if (!review) return null;
|
|
185
|
+
|
|
186
|
+
const rows = this.db
|
|
187
|
+
.query("SELECT * FROM findings WHERE review_id = ? ORDER BY rowid")
|
|
188
|
+
.all(reviewId) as FindingRow[];
|
|
189
|
+
if (
|
|
190
|
+
rows.length === 0 &&
|
|
191
|
+
(review.status === null || review.status === "reviewing")
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const historyStmt = this.db.query(
|
|
197
|
+
"SELECT * FROM finding_history WHERE finding_id = ? ORDER BY rowid",
|
|
198
|
+
);
|
|
199
|
+
const findings: FindingsStateFile["findings"] = {};
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
findings[row.short_id] = findingFromRow(
|
|
202
|
+
row,
|
|
203
|
+
historyStmt.all(row.id) as HistoryRow[],
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const base = emptyState(review.branch);
|
|
208
|
+
const state: FindingsStateFile = {
|
|
209
|
+
...base,
|
|
210
|
+
slug: review.slug,
|
|
211
|
+
updatedAt: review.updatedAt,
|
|
212
|
+
reviewed: review.reviewedSha
|
|
213
|
+
? {
|
|
214
|
+
headSha: review.reviewedSha,
|
|
215
|
+
at: review.reviewedAt ?? review.updatedAt,
|
|
216
|
+
}
|
|
217
|
+
: undefined,
|
|
218
|
+
findings,
|
|
219
|
+
};
|
|
220
|
+
reorder(state);
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
writeFindings(reviewId: string, state: FindingsStateFile): void {
|
|
225
|
+
if (!this.getReview(reviewId)) {
|
|
226
|
+
throw new Error(`Review '${reviewId}' não encontrada.`);
|
|
227
|
+
}
|
|
228
|
+
const now = state.updatedAt || systemClock();
|
|
229
|
+
const upsert = this.db.query(
|
|
230
|
+
`INSERT INTO findings
|
|
231
|
+
(id, review_id, short_id, fingerprint, watchdog_ids, rule, severity,
|
|
232
|
+
file, line, message, action, evidence, anchor, found_at_sha, count,
|
|
233
|
+
present, decision, comment, exec_state, github, created_at, updated_at)
|
|
234
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
235
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
236
|
+
short_id = excluded.short_id,
|
|
237
|
+
fingerprint = excluded.fingerprint,
|
|
238
|
+
watchdog_ids = excluded.watchdog_ids,
|
|
239
|
+
rule = excluded.rule,
|
|
240
|
+
severity = excluded.severity,
|
|
241
|
+
file = excluded.file,
|
|
242
|
+
line = excluded.line,
|
|
243
|
+
message = excluded.message,
|
|
244
|
+
action = excluded.action,
|
|
245
|
+
evidence = excluded.evidence,
|
|
246
|
+
anchor = excluded.anchor,
|
|
247
|
+
found_at_sha = excluded.found_at_sha,
|
|
248
|
+
count = excluded.count,
|
|
249
|
+
present = excluded.present,
|
|
250
|
+
decision = excluded.decision,
|
|
251
|
+
comment = excluded.comment,
|
|
252
|
+
exec_state = excluded.exec_state,
|
|
253
|
+
github = excluded.github,
|
|
254
|
+
updated_at = excluded.updated_at`,
|
|
255
|
+
);
|
|
256
|
+
const deleteHistory = this.db.query(
|
|
257
|
+
"DELETE FROM finding_history WHERE finding_id = ?",
|
|
258
|
+
);
|
|
259
|
+
const insertHistory = this.db.query(
|
|
260
|
+
`INSERT INTO finding_history
|
|
261
|
+
(id, finding_id, at, decision, exec_state, comment, by)
|
|
262
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const transaction = this.db.transaction(() => {
|
|
266
|
+
this.db.run(
|
|
267
|
+
`UPDATE reviews
|
|
268
|
+
SET reviewed_sha = ?, reviewed_at = ?, updated_at = ?
|
|
269
|
+
WHERE id = ?`,
|
|
270
|
+
[
|
|
271
|
+
state.reviewed?.headSha ?? null,
|
|
272
|
+
state.reviewed?.at ?? null,
|
|
273
|
+
now,
|
|
274
|
+
reviewId,
|
|
275
|
+
],
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const existing = this.db
|
|
279
|
+
.query("SELECT id, short_id FROM findings WHERE review_id = ?")
|
|
280
|
+
.all(reviewId) as { id: string; short_id: string }[];
|
|
281
|
+
const idByShort = new Map(existing.map((row) => [row.short_id, row.id]));
|
|
282
|
+
const desired = new Set(Object.keys(state.findings));
|
|
283
|
+
for (const row of existing) {
|
|
284
|
+
if (!desired.has(row.short_id)) {
|
|
285
|
+
this.db.run("DELETE FROM findings WHERE id = ?", [row.id]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const finding of Object.values(state.findings)) {
|
|
290
|
+
const id = idByShort.get(finding.id) ?? randomUUID();
|
|
291
|
+
upsert.run(
|
|
292
|
+
id,
|
|
293
|
+
reviewId,
|
|
294
|
+
finding.id,
|
|
295
|
+
finding.fingerprint,
|
|
296
|
+
JSON.stringify(finding.watchdogIds),
|
|
297
|
+
finding.rule,
|
|
298
|
+
finding.severity,
|
|
299
|
+
finding.file,
|
|
300
|
+
finding.line,
|
|
301
|
+
finding.message,
|
|
302
|
+
finding.action,
|
|
303
|
+
finding.evidence ?? null,
|
|
304
|
+
finding.anchor ? JSON.stringify(finding.anchor) : null,
|
|
305
|
+
finding.foundAtSha ?? null,
|
|
306
|
+
finding.count,
|
|
307
|
+
finding.present ? 1 : 0,
|
|
308
|
+
finding.decision,
|
|
309
|
+
finding.comment ?? null,
|
|
310
|
+
finding.execState,
|
|
311
|
+
finding.github ? JSON.stringify(finding.github) : null,
|
|
312
|
+
finding.createdAt,
|
|
313
|
+
finding.updatedAt,
|
|
314
|
+
);
|
|
315
|
+
deleteHistory.run(id);
|
|
316
|
+
for (const entry of finding.history) {
|
|
317
|
+
insertHistory.run(
|
|
318
|
+
randomUUID(),
|
|
319
|
+
id,
|
|
320
|
+
entry.at,
|
|
321
|
+
entry.decision,
|
|
322
|
+
entry.execState,
|
|
323
|
+
entry.comment ?? null,
|
|
324
|
+
entry.by ?? null,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
transaction();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// -----------------------------------------------------------------------
|
|
333
|
+
// Artefatos
|
|
334
|
+
// -----------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
readArtifact(reviewId: string, name: string): ArtifactRecord | null {
|
|
337
|
+
const row = this.db
|
|
338
|
+
.query("SELECT * FROM artifacts WHERE review_id = ? AND name = ?")
|
|
339
|
+
.get(reviewId, name) as ArtifactRow | null;
|
|
340
|
+
return row ? artifactFromRow(row) : null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
writeArtifact(reviewId: string, input: WriteArtifactInput): ArtifactRecord {
|
|
344
|
+
if (!this.getReview(reviewId)) {
|
|
345
|
+
throw new Error(`Review '${reviewId}' não encontrada.`);
|
|
346
|
+
}
|
|
347
|
+
const now = systemClock();
|
|
348
|
+
const contentType =
|
|
349
|
+
input.contentType ??
|
|
350
|
+
(input.name.toLowerCase().endsWith(".json")
|
|
351
|
+
? "application/json; charset=utf-8"
|
|
352
|
+
: "text/markdown; charset=utf-8");
|
|
353
|
+
const existing = this.readArtifact(reviewId, input.name);
|
|
354
|
+
if (existing) {
|
|
355
|
+
this.db.run(
|
|
356
|
+
`UPDATE artifacts
|
|
357
|
+
SET kind = ?, content = ?, content_type = ?, updated_at = ?
|
|
358
|
+
WHERE id = ?`,
|
|
359
|
+
[input.kind, input.content, contentType, now, existing.id],
|
|
360
|
+
);
|
|
361
|
+
return this.readArtifact(reviewId, input.name) as ArtifactRecord;
|
|
362
|
+
}
|
|
363
|
+
const record: ArtifactRecord = {
|
|
364
|
+
id: randomUUID(),
|
|
365
|
+
reviewId,
|
|
366
|
+
name: input.name,
|
|
367
|
+
kind: input.kind,
|
|
368
|
+
content: input.content,
|
|
369
|
+
contentType,
|
|
370
|
+
createdAt: now,
|
|
371
|
+
updatedAt: now,
|
|
372
|
+
};
|
|
373
|
+
this.db.run(
|
|
374
|
+
`INSERT INTO artifacts
|
|
375
|
+
(id, review_id, name, kind, content, content_type, created_at, updated_at)
|
|
376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
377
|
+
[
|
|
378
|
+
record.id,
|
|
379
|
+
record.reviewId,
|
|
380
|
+
record.name,
|
|
381
|
+
record.kind,
|
|
382
|
+
record.content,
|
|
383
|
+
record.contentType,
|
|
384
|
+
record.createdAt,
|
|
385
|
+
record.updatedAt,
|
|
386
|
+
],
|
|
387
|
+
);
|
|
388
|
+
return record;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
listArtifacts(reviewId: string): ArtifactRecord[] {
|
|
392
|
+
const rows = this.db
|
|
393
|
+
.query("SELECT * FROM artifacts WHERE review_id = ? ORDER BY name")
|
|
394
|
+
.all(reviewId) as ArtifactRow[];
|
|
395
|
+
return rows.map(artifactFromRow);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
deleteArtifact(reviewId: string, name: string): void {
|
|
399
|
+
this.db.run("DELETE FROM artifacts WHERE review_id = ? AND name = ?", [
|
|
400
|
+
reviewId,
|
|
401
|
+
name,
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// -----------------------------------------------------------------------
|
|
406
|
+
// Manutenção / ciclo de vida
|
|
407
|
+
// -----------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
maintain(): void {
|
|
410
|
+
this.db.exec("PRAGMA optimize;");
|
|
411
|
+
this.db.exec("VACUUM;");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
location(reviewId: string): string {
|
|
415
|
+
return `sqlite:${this.path}#${reviewId}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
close(): void {
|
|
419
|
+
this.db.close();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private requireReview(id: string): ReviewRecord {
|
|
423
|
+
const review = this.getReview(id);
|
|
424
|
+
if (!review) throw new Error(`Review '${id}' não encontrada.`);
|
|
425
|
+
return review;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contrato de persistência do houndsman (biblioteca).
|
|
3
|
+
*
|
|
4
|
+
* Tudo que hoje é "run" (reviews, findings, artefatos) passa por aqui. As
|
|
5
|
+
* implementações (`SQLiteStorageProvider` e `FileStorageProvider`) compartilham
|
|
6
|
+
* esta interface — trocar o backend é só trocar a config.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FindingsStateFile } from "../findings-state";
|
|
10
|
+
|
|
11
|
+
/** Estados de execução de uma run. */
|
|
12
|
+
export type ReviewStatus = "reviewing" | "review_done" | "fixing" | "fix_done";
|
|
13
|
+
|
|
14
|
+
/** Identidade estável de um repositório (remote normalizado ou raiz git). */
|
|
15
|
+
export type RepoIdentity = {
|
|
16
|
+
/** Chave usada para escopar as runs (`remote:<...>` ou `path:<...>`). */
|
|
17
|
+
key: string;
|
|
18
|
+
/** Raiz absoluta do repositório no disco. */
|
|
19
|
+
root: string;
|
|
20
|
+
/** Remote `origin` cru, quando existir. */
|
|
21
|
+
remote?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Registro de uma revisão (run de uma branch). */
|
|
25
|
+
export type ReviewRecord = {
|
|
26
|
+
id: string;
|
|
27
|
+
repoKey: string;
|
|
28
|
+
repoRoot: string;
|
|
29
|
+
repoRemote?: string;
|
|
30
|
+
branch: string;
|
|
31
|
+
slug: string;
|
|
32
|
+
base?: string;
|
|
33
|
+
head?: string;
|
|
34
|
+
headSha?: string;
|
|
35
|
+
reviewedSha?: string;
|
|
36
|
+
reviewedAt?: string;
|
|
37
|
+
status: ReviewStatus | null;
|
|
38
|
+
startedAt?: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Campos mutáveis de uma review (os demais são identidade). */
|
|
44
|
+
export type ReviewPatch = Partial<
|
|
45
|
+
Pick<
|
|
46
|
+
ReviewRecord,
|
|
47
|
+
| "branch"
|
|
48
|
+
| "slug"
|
|
49
|
+
| "base"
|
|
50
|
+
| "head"
|
|
51
|
+
| "headSha"
|
|
52
|
+
| "reviewedSha"
|
|
53
|
+
| "reviewedAt"
|
|
54
|
+
| "status"
|
|
55
|
+
| "startedAt"
|
|
56
|
+
| "updatedAt"
|
|
57
|
+
>
|
|
58
|
+
> & {
|
|
59
|
+
/** Reinicia o `startedAt` no início de um novo ciclo de revisão. */
|
|
60
|
+
resetStartedAt?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Entrada de `getOrCreateReview`. */
|
|
64
|
+
export type CreateReviewInput = {
|
|
65
|
+
repo: RepoIdentity;
|
|
66
|
+
branch: string;
|
|
67
|
+
slug: string;
|
|
68
|
+
base?: string;
|
|
69
|
+
head?: string;
|
|
70
|
+
now?: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Espécie de artefato (orienta a UI e o manifesto). */
|
|
74
|
+
export type ArtifactKind =
|
|
75
|
+
| "brief"
|
|
76
|
+
| "classification"
|
|
77
|
+
| "manifest"
|
|
78
|
+
| "candidates"
|
|
79
|
+
| "context"
|
|
80
|
+
| "context-brief"
|
|
81
|
+
| "context-summary"
|
|
82
|
+
| "finding-report"
|
|
83
|
+
| "review"
|
|
84
|
+
| "domain-review"
|
|
85
|
+
| "verify"
|
|
86
|
+
| "other";
|
|
87
|
+
|
|
88
|
+
/** Artefato persistido de uma review. */
|
|
89
|
+
export type ArtifactRecord = {
|
|
90
|
+
id: string;
|
|
91
|
+
reviewId: string;
|
|
92
|
+
name: string;
|
|
93
|
+
kind: ArtifactKind;
|
|
94
|
+
content: string;
|
|
95
|
+
contentType: string;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Entrada de `writeArtifact`. */
|
|
101
|
+
export type WriteArtifactInput = {
|
|
102
|
+
name: string;
|
|
103
|
+
kind: ArtifactKind;
|
|
104
|
+
content: string;
|
|
105
|
+
contentType?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Persistência síncrona de reviews, findings e artefatos.
|
|
110
|
+
*
|
|
111
|
+
* Síncrona de propósito: `bun:sqlite` e o filesystem são síncronos, e o motor
|
|
112
|
+
* inteiro já trabalha nesse modelo.
|
|
113
|
+
*/
|
|
114
|
+
export interface StorageProvider {
|
|
115
|
+
readonly kind: "sqlite" | "file";
|
|
116
|
+
|
|
117
|
+
/** Recupera ou cria a review de uma branch (identidade = repo + branch). */
|
|
118
|
+
getOrCreateReview(input: CreateReviewInput): ReviewRecord;
|
|
119
|
+
findReview(repoKey: string, branch: string): ReviewRecord | null;
|
|
120
|
+
getReview(id: string): ReviewRecord | null;
|
|
121
|
+
/** Reviews do repositório (mais recentes primeiro); sem chave = todas. */
|
|
122
|
+
listReviews(repoKey?: string): ReviewRecord[];
|
|
123
|
+
updateReview(id: string, patch: ReviewPatch): ReviewRecord | null;
|
|
124
|
+
deleteReview(id: string): void;
|
|
125
|
+
|
|
126
|
+
/** Estado de triagem completo; null se a review ainda não tem findings. */
|
|
127
|
+
readFindings(reviewId: string): FindingsStateFile | null;
|
|
128
|
+
/** Substitui o estado de triagem (transacional no SQLite). */
|
|
129
|
+
writeFindings(reviewId: string, state: FindingsStateFile): void;
|
|
130
|
+
|
|
131
|
+
readArtifact(reviewId: string, name: string): ArtifactRecord | null;
|
|
132
|
+
writeArtifact(reviewId: string, input: WriteArtifactInput): ArtifactRecord;
|
|
133
|
+
listArtifacts(reviewId: string): ArtifactRecord[];
|
|
134
|
+
deleteArtifact(reviewId: string, name: string): void;
|
|
135
|
+
|
|
136
|
+
/** Manutenção do backend (SQLite: `PRAGMA optimize` + `VACUUM`). */
|
|
137
|
+
maintain(): void;
|
|
138
|
+
|
|
139
|
+
/** Referência legível do armazenamento de uma review (diagnóstico/logs). */
|
|
140
|
+
location(reviewId: string): string;
|
|
141
|
+
|
|
142
|
+
close(): void;
|
|
143
|
+
}
|