@payconductor/houndsman 0.3.0 → 0.5.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 +25 -39
- 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/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -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/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- 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 +271 -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 +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -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/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -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/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -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 +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
package/src/core/orchestrator.ts
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* Genérico: recebe o `HoundsmanConfig` por parâmetro (injeção explícita).
|
|
6
6
|
* Entrypoints: `cli/index.ts` (`houndsman`). Aqui não há `process.argv`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Persistência:
|
|
9
|
+
* storage reviews/findings/artefatos (SQLite por default)
|
|
10
|
+
* <home>/work/<review> staging dos subagentes
|
|
11
|
+
* briefs/<id>.md instruções prontas para cada sub-agente
|
|
12
|
+
* findings/<id>.json saída dos sub-agentes (schema skill/OUTPUT-FORMAT.md)
|
|
13
|
+
* context*.json/md contexto da PR (Passo 0)
|
|
14
|
+
*
|
|
15
|
+
* Artefatos persistidos: classification, candidates, manifest, review.md,
|
|
16
|
+
* review.<watchdog>.md, context e context-summary.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
19
19
|
import {
|
|
20
20
|
type HoundsmanConfig,
|
|
21
21
|
resolveJevConcurrency,
|
|
@@ -28,32 +28,42 @@ import { Watchdog } from "./domain/watchdog";
|
|
|
28
28
|
import { mergeCarryover } from "./engine/carryover";
|
|
29
29
|
import { ReviewScopeResolver, type RunClassification } from "./engine/review-scope";
|
|
30
30
|
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
31
|
-
import { buildStateFromFindings,
|
|
31
|
+
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
32
32
|
import { resolveSha } from "./git-revision";
|
|
33
|
+
import { syncResolvedThreads } from "./github-review";
|
|
33
34
|
import {
|
|
34
35
|
type JevCandidate,
|
|
35
36
|
type JevCandidatesFile,
|
|
36
37
|
JevCandidatesStore,
|
|
37
38
|
JevClient,
|
|
38
39
|
JevDetector,
|
|
40
|
+
parseCandidates,
|
|
41
|
+
serializeCandidates,
|
|
39
42
|
} from "./jev";
|
|
40
|
-
import {
|
|
41
|
-
|
|
43
|
+
import {
|
|
44
|
+
displayPath,
|
|
45
|
+
OUTPUT_FORMAT_PATH,
|
|
46
|
+
REVIEW_FILE,
|
|
47
|
+
REVIEW_RESUME_FILE,
|
|
48
|
+
} from "./paths";
|
|
49
|
+
import { currentBranch } from "./pr-context";
|
|
42
50
|
import { ProjectConfig } from "./project-config";
|
|
43
51
|
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
44
52
|
import { ManifestWriter } from "./renderers/manifest-writer";
|
|
45
53
|
import { ReportRenderer } from "./renderers/report-renderer";
|
|
54
|
+
import { renderReviewResume } from "./renderers/resume-renderer";
|
|
46
55
|
import {
|
|
47
56
|
type ContextSummary,
|
|
48
57
|
type DomainReport,
|
|
49
58
|
dedupeFindings,
|
|
50
|
-
|
|
59
|
+
parseContextSummary,
|
|
51
60
|
sanitizeBranch,
|
|
52
61
|
type WatchdogReport,
|
|
53
62
|
} from "./report-builder";
|
|
54
|
-
import { RunLayout } from "./run-layout";
|
|
55
63
|
import { writeRunStatus } from "./run-status";
|
|
56
64
|
import { systemClock } from "./services/clock";
|
|
65
|
+
import type { ReviewRecord, StorageProvider } from "./storage/types";
|
|
66
|
+
import { ReviewWorkArea } from "./storage/work-area";
|
|
57
67
|
import type { WatchdogMeta } from "./types";
|
|
58
68
|
import { findWatchdog } from "./watchdog-registry";
|
|
59
69
|
|
|
@@ -61,7 +71,6 @@ export type ReviewOptions = {
|
|
|
61
71
|
base: string;
|
|
62
72
|
head: string;
|
|
63
73
|
branch?: string;
|
|
64
|
-
dir: string;
|
|
65
74
|
finalize: boolean;
|
|
66
75
|
failOnBlocking: boolean;
|
|
67
76
|
clean: boolean;
|
|
@@ -71,11 +80,10 @@ export type ReviewOptions = {
|
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
74
|
-
export function defaultOptions(
|
|
83
|
+
export function defaultOptions(): ReviewOptions {
|
|
75
84
|
return {
|
|
76
85
|
base: "main",
|
|
77
86
|
head: "HEAD",
|
|
78
|
-
dir: artifactDir,
|
|
79
87
|
finalize: false,
|
|
80
88
|
failOnBlocking: false,
|
|
81
89
|
clean: false,
|
|
@@ -84,20 +92,19 @@ export function defaultOptions(artifactDir: string = ARTIFACT_DIR): ReviewOption
|
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
/**
|
|
88
|
-
type
|
|
95
|
+
/** Contexto de uma run: storage + review + staging. */
|
|
96
|
+
export type ReviewContext = {
|
|
97
|
+
storage: StorageProvider;
|
|
98
|
+
review: ReviewRecord;
|
|
99
|
+
work: ReviewWorkArea;
|
|
100
|
+
};
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
mkdirSync(dirs.findingsDir, { recursive: true });
|
|
97
|
-
if (clean) {
|
|
98
|
-
rmSync(dirs.findingsDir, { recursive: true, force: true });
|
|
99
|
-
mkdirSync(dirs.findingsDir, { recursive: true });
|
|
100
|
-
}
|
|
102
|
+
/** Cria o contexto de uma review existente. */
|
|
103
|
+
export function reviewContext(
|
|
104
|
+
storage: StorageProvider,
|
|
105
|
+
review: ReviewRecord,
|
|
106
|
+
): ReviewContext {
|
|
107
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// ---------------------------------------------------------------------------
|
|
@@ -120,40 +127,6 @@ function incrementalNote(classification: RunClassification): string {
|
|
|
120
127
|
);
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Findings
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
function readFindings(
|
|
128
|
-
findingsDir: string,
|
|
129
|
-
activeIds: string[],
|
|
130
|
-
): { reports: WatchdogReport[]; pending: string[]; warnings: string[] } {
|
|
131
|
-
const reports: WatchdogReport[] = [];
|
|
132
|
-
const pending: string[] = [];
|
|
133
|
-
const warnings: string[] = [];
|
|
134
|
-
for (const id of activeIds) {
|
|
135
|
-
const path = join(findingsDir, `${id}.json`);
|
|
136
|
-
if (!existsSync(path)) {
|
|
137
|
-
pending.push(id);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
142
|
-
const report = parseWatchdogReport(raw, path, (message) =>
|
|
143
|
-
warnings.push(message),
|
|
144
|
-
);
|
|
145
|
-
if (report) reports.push(report);
|
|
146
|
-
else pending.push(id);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
warnings.push(
|
|
149
|
-
`[houndsman] ${relFromRoot(path)} inválido: ${err instanceof Error ? err.message : err}`,
|
|
150
|
-
);
|
|
151
|
-
pending.push(id);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { reports, pending, warnings };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
130
|
// ---------------------------------------------------------------------------
|
|
158
131
|
// Orçamento (triage de PRs grandes)
|
|
159
132
|
// ---------------------------------------------------------------------------
|
|
@@ -169,7 +142,7 @@ function budgetFor(
|
|
|
169
142
|
|
|
170
143
|
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
171
144
|
function writeManifest(
|
|
172
|
-
|
|
145
|
+
ctx: ReviewContext,
|
|
173
146
|
config: HoundsmanConfig,
|
|
174
147
|
opts: ReviewOptions,
|
|
175
148
|
classification: RunClassification,
|
|
@@ -177,12 +150,13 @@ function writeManifest(
|
|
|
177
150
|
pending: string[],
|
|
178
151
|
jev: JevCandidatesFile,
|
|
179
152
|
): void {
|
|
180
|
-
new ManifestWriter(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
153
|
+
new ManifestWriter(
|
|
154
|
+
ctx.storage,
|
|
155
|
+
ctx.review,
|
|
156
|
+
ctx.work,
|
|
157
|
+
new ProjectConfig(config),
|
|
158
|
+
opts,
|
|
159
|
+
).write(classification, activeIds, pending, jev);
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
// ---------------------------------------------------------------------------
|
|
@@ -192,12 +166,13 @@ function writeManifest(
|
|
|
192
166
|
function writeBriefs(
|
|
193
167
|
activeIds: string[],
|
|
194
168
|
classification: ClassificationResult,
|
|
195
|
-
|
|
169
|
+
ctx: ReviewContext,
|
|
196
170
|
branch: string,
|
|
197
171
|
context: ContextSummary | null,
|
|
198
172
|
candidates: JevCandidate[],
|
|
199
173
|
config: HoundsmanConfig,
|
|
200
174
|
): void {
|
|
175
|
+
const work = ctx.work;
|
|
201
176
|
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
202
177
|
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
203
178
|
const renderer = new BriefRenderer();
|
|
@@ -213,13 +188,20 @@ function writeBriefs(
|
|
|
213
188
|
overflow,
|
|
214
189
|
deleted: classification.deleted,
|
|
215
190
|
branch,
|
|
216
|
-
outputPath:
|
|
191
|
+
outputPath: work.findingFile(id),
|
|
217
192
|
outputFormatPath: OUTPUT_FORMAT_PATH,
|
|
218
193
|
docs,
|
|
219
194
|
context,
|
|
220
195
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
221
196
|
});
|
|
222
|
-
|
|
197
|
+
const content = `${brief}\n`;
|
|
198
|
+
work.writeBrief(id, content);
|
|
199
|
+
// Auditoria: o brief também fica no storage (o staging é limpo no finalize).
|
|
200
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
201
|
+
name: `briefs/${id}.md`,
|
|
202
|
+
kind: "brief",
|
|
203
|
+
content,
|
|
204
|
+
});
|
|
223
205
|
}
|
|
224
206
|
}
|
|
225
207
|
|
|
@@ -234,13 +216,16 @@ function writeBriefs(
|
|
|
234
216
|
async function resolveCandidates(
|
|
235
217
|
opts: ReviewOptions,
|
|
236
218
|
classification: ClassificationResult,
|
|
237
|
-
|
|
219
|
+
ctx: ReviewContext,
|
|
238
220
|
config: HoundsmanConfig,
|
|
239
221
|
diff: DiffSource,
|
|
240
222
|
): Promise<JevCandidatesFile> {
|
|
241
|
-
const store = new JevCandidatesStore(dirs.candidatesFile, config.watchdogIds);
|
|
242
223
|
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
243
|
-
|
|
224
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
225
|
+
name: "candidates.json",
|
|
226
|
+
kind: "candidates",
|
|
227
|
+
content: serializeCandidates(data),
|
|
228
|
+
});
|
|
244
229
|
return data;
|
|
245
230
|
};
|
|
246
231
|
|
|
@@ -295,7 +280,7 @@ function discoveryMessages(
|
|
|
295
280
|
): string[] {
|
|
296
281
|
const messages = [
|
|
297
282
|
`Discovery: ${activeIds.length} watchdog(s) ativado(s), ${Object.keys(classification.files).length} arquivo(s), ${classification.deleted.length} deletado(s).`,
|
|
298
|
-
`Briefs: ${
|
|
283
|
+
`Briefs: ${displayPath(briefsDir)}/`,
|
|
299
284
|
];
|
|
300
285
|
for (const file of classification.deleted) {
|
|
301
286
|
messages.push(`Deletado (breaking change): ${file}`);
|
|
@@ -327,31 +312,54 @@ export async function prepare(
|
|
|
327
312
|
opts: ReviewOptions,
|
|
328
313
|
branch: string,
|
|
329
314
|
config: HoundsmanConfig,
|
|
330
|
-
|
|
315
|
+
ctx: ReviewContext,
|
|
331
316
|
): Promise<PrepareResult> {
|
|
332
317
|
const messages: string[] = [];
|
|
333
|
-
|
|
318
|
+
ctx.work.ensure();
|
|
319
|
+
if (opts.clean) ctx.work.cleanFindings();
|
|
320
|
+
ctx.storage.updateReview(ctx.review.id, { base: opts.base, head: opts.head });
|
|
321
|
+
|
|
322
|
+
// Marca o commit revisado e decide entre completo, incremental ou no-op.
|
|
323
|
+
const headSha = resolveSha(opts.head);
|
|
324
|
+
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
325
|
+
const scope = new ReviewScopeResolver(opts, config).resolve(previous, headSha);
|
|
326
|
+
|
|
327
|
+
// Mesmo commit já revisado: preserva artefatos e triagem, sem re-revisar.
|
|
328
|
+
if (scope.kind === "noop") {
|
|
329
|
+
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
330
|
+
// Ciclo em andamento continua válido (os findings já despachados devem
|
|
331
|
+
// ser finalizados); fora isso, o relatório anterior é a resposta final.
|
|
332
|
+
const pendingCycle = status === "reviewing";
|
|
333
|
+
if (!pendingCycle) {
|
|
334
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
335
|
+
}
|
|
336
|
+
messages.push(
|
|
337
|
+
`Nenhuma alteração desde a última revisão (${shortSha(scope.reviewedFrom)}).`,
|
|
338
|
+
pendingCycle
|
|
339
|
+
? "Nada novo a preparar — o ciclo em andamento continua; rode `--finalize` quando os findings estiverem gravados."
|
|
340
|
+
: "Nada a revisar — o relatório anterior foi mantido. Use `--full` para re-revisar.",
|
|
341
|
+
);
|
|
342
|
+
return { messages };
|
|
343
|
+
}
|
|
334
344
|
|
|
335
345
|
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
336
|
-
writeRunStatus(
|
|
346
|
+
writeRunStatus(ctx.storage, ctx.review.id, {
|
|
347
|
+
status: "reviewing",
|
|
348
|
+
resetStartedAt: true,
|
|
349
|
+
});
|
|
337
350
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
);
|
|
345
|
-
writeFileSync(
|
|
346
|
-
dirs.classificationFile,
|
|
347
|
-
`${JSON.stringify(classification, null, 2)}\n`,
|
|
348
|
-
);
|
|
351
|
+
const { classification, diff } = scope;
|
|
352
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
353
|
+
name: "classification.json",
|
|
354
|
+
kind: "classification",
|
|
355
|
+
content: `${JSON.stringify(classification, null, 2)}\n`,
|
|
356
|
+
});
|
|
349
357
|
|
|
350
|
-
const jev = await resolveCandidates(opts, classification,
|
|
358
|
+
const jev = await resolveCandidates(opts, classification, ctx, config, diff);
|
|
351
359
|
|
|
352
360
|
const activeIds = Object.keys(classification.watchdogs);
|
|
353
361
|
if (Object.keys(classification.files).length === 0) {
|
|
354
|
-
writeRunStatus(
|
|
362
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
355
363
|
messages.push(
|
|
356
364
|
classification.mode === "incremental"
|
|
357
365
|
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
@@ -360,7 +368,7 @@ export async function prepare(
|
|
|
360
368
|
return { messages };
|
|
361
369
|
}
|
|
362
370
|
if (activeIds.length === 0) {
|
|
363
|
-
writeRunStatus(
|
|
371
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
364
372
|
messages.push(
|
|
365
373
|
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
366
374
|
);
|
|
@@ -372,13 +380,14 @@ export async function prepare(
|
|
|
372
380
|
);
|
|
373
381
|
}
|
|
374
382
|
|
|
375
|
-
const context = readContextSummary(
|
|
383
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
384
|
+
persistContext(ctx);
|
|
376
385
|
|
|
377
|
-
writeBriefs(activeIds, classification,
|
|
378
|
-
writeManifest(
|
|
386
|
+
writeBriefs(activeIds, classification, ctx, branch, context, jev.candidates, config);
|
|
387
|
+
writeManifest(ctx, config, opts, classification, activeIds, activeIds, jev);
|
|
379
388
|
|
|
380
389
|
messages.push(
|
|
381
|
-
...discoveryMessages(activeIds, classification,
|
|
390
|
+
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
382
391
|
...budgetMessage(classification, config),
|
|
383
392
|
...jevSummary(jev),
|
|
384
393
|
);
|
|
@@ -392,28 +401,78 @@ export async function prepare(
|
|
|
392
401
|
}
|
|
393
402
|
|
|
394
403
|
messages.push(
|
|
395
|
-
`\nPróximo passo: despachar os sub-agentes (um por brief em ${
|
|
404
|
+
`\nPróximo passo: despachar os sub-agentes (um por brief em ${displayPath(ctx.work.briefsDir)}/).`,
|
|
396
405
|
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
397
406
|
);
|
|
398
407
|
|
|
399
408
|
return { messages };
|
|
400
409
|
}
|
|
410
|
+
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
411
|
+
function persistContext(ctx: ReviewContext): void {
|
|
412
|
+
const raw = ctx.work.readContextRaw();
|
|
413
|
+
if (raw !== null) {
|
|
414
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
415
|
+
name: "context.json",
|
|
416
|
+
kind: "context",
|
|
417
|
+
content: raw,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const summary = ctx.work.readContextSummaryRaw();
|
|
421
|
+
if (summary !== null) {
|
|
422
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
423
|
+
name: "context-summary.json",
|
|
424
|
+
kind: "context-summary",
|
|
425
|
+
content: summary,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const brief = ctx.work.readContextBriefRaw();
|
|
429
|
+
if (brief !== null) {
|
|
430
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
431
|
+
name: "context-brief.md",
|
|
432
|
+
kind: "context-brief",
|
|
433
|
+
content: brief,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
401
437
|
|
|
402
438
|
// ---------------------------------------------------------------------------
|
|
403
439
|
// Passo 2 — finalizar
|
|
404
440
|
// ---------------------------------------------------------------------------
|
|
405
441
|
|
|
406
|
-
/** Lê o classification.json
|
|
407
|
-
function readClassification(
|
|
408
|
-
|
|
442
|
+
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
443
|
+
function readClassification(ctx: ReviewContext): RunClassification {
|
|
444
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "classification.json");
|
|
445
|
+
if (!artifact) {
|
|
409
446
|
throw new Error(
|
|
410
|
-
`Não encontrei
|
|
447
|
+
`Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return JSON.parse(artifact.content) as RunClassification;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
|
|
454
|
+
function readCandidates(ctx: ReviewContext, config: HoundsmanConfig): JevCandidatesFile {
|
|
455
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "candidates.json");
|
|
456
|
+
if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
|
|
457
|
+
try {
|
|
458
|
+
return (
|
|
459
|
+
parseCandidates(JSON.parse(artifact.content), config.watchdogIds) ??
|
|
460
|
+
JevCandidatesStore.unavailable("candidates.json inválido")
|
|
411
461
|
);
|
|
462
|
+
} catch {
|
|
463
|
+
return JevCandidatesStore.unavailable("candidates.json inválido");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Resumo do contexto persistido (usado quando o staging já foi limpo). */
|
|
468
|
+
function readPersistedContextSummary(ctx: ReviewContext): ContextSummary | null {
|
|
469
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "context-summary.json");
|
|
470
|
+
if (!artifact) return null;
|
|
471
|
+
try {
|
|
472
|
+
return parseContextSummary(JSON.parse(artifact.content));
|
|
473
|
+
} catch {
|
|
474
|
+
return null;
|
|
412
475
|
}
|
|
413
|
-
const parsed = JSON.parse(
|
|
414
|
-
readFileSync(classificationPath, "utf8"),
|
|
415
|
-
) as RunClassification;
|
|
416
|
-
return parsed;
|
|
417
476
|
}
|
|
418
477
|
|
|
419
478
|
/**
|
|
@@ -421,7 +480,7 @@ function readClassification(classificationPath: string): RunClassification {
|
|
|
421
480
|
* Mantém o `review.md` consolidado navegável em PRs grandes (centenas de achados).
|
|
422
481
|
*/
|
|
423
482
|
function writeDomainReports(
|
|
424
|
-
|
|
483
|
+
ctx: ReviewContext,
|
|
425
484
|
branch: string,
|
|
426
485
|
reports: WatchdogReport[],
|
|
427
486
|
classification: ClassificationResult,
|
|
@@ -435,16 +494,17 @@ function writeDomainReports(
|
|
|
435
494
|
if (findings.length === 0) continue;
|
|
436
495
|
const file = `review.${sanitizeBranch(report.watchdogId)}.md`;
|
|
437
496
|
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
497
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
498
|
+
name: file,
|
|
499
|
+
kind: "domain-review",
|
|
500
|
+
content: `${renderer.renderDomain(
|
|
441
501
|
branch,
|
|
442
502
|
report,
|
|
443
503
|
findings,
|
|
444
504
|
expected.length,
|
|
445
505
|
context,
|
|
446
506
|
)}\n`,
|
|
447
|
-
);
|
|
507
|
+
});
|
|
448
508
|
domains.push({
|
|
449
509
|
id: report.watchdogId,
|
|
450
510
|
name: nameOf(report.watchdogId),
|
|
@@ -472,23 +532,42 @@ export function finalize(
|
|
|
472
532
|
opts: ReviewOptions,
|
|
473
533
|
branch: string,
|
|
474
534
|
config: HoundsmanConfig,
|
|
475
|
-
|
|
535
|
+
ctx: ReviewContext,
|
|
476
536
|
): FinalizeResult {
|
|
477
|
-
|
|
537
|
+
// `--finalize` só consolida um ciclo preparado (status `reviewing`). Sem esta
|
|
538
|
+
// guarda, um prepare no-op seguido de `--finalize` reescreveria o relatório
|
|
539
|
+
// anterior com a classificação antiga (perdendo os relatórios por domínio).
|
|
540
|
+
// O `ctx.review` é um snapshot: relê o status atual do storage.
|
|
541
|
+
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
542
|
+
if (status !== "reviewing") {
|
|
543
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
544
|
+
const present = Object.values(state?.findings ?? {}).filter(
|
|
545
|
+
(finding) => finding.present,
|
|
546
|
+
);
|
|
547
|
+
return {
|
|
548
|
+
reportPath: ctx.storage.location(ctx.review.id),
|
|
549
|
+
blockers: present.filter((f) => f.severity === "blocking").length,
|
|
550
|
+
attention: present.filter((f) => f.severity === "attention").length,
|
|
551
|
+
messages: [
|
|
552
|
+
`Nada a finalizar: nenhum ciclo de revisão pendente (status: ${status ?? "sem status"}).`,
|
|
553
|
+
"Se nada mudou desde a última revisão, o relatório anterior foi mantido; use `--full` para re-revisar.",
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const classification = readClassification(ctx);
|
|
478
559
|
const activeIds = Object.keys(classification.watchdogs);
|
|
479
560
|
|
|
480
561
|
const messages: string[] = [];
|
|
481
|
-
const { reports, pending, warnings } = readFindings(
|
|
562
|
+
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
482
563
|
messages.push(...warnings);
|
|
483
|
-
const jev =
|
|
484
|
-
|
|
485
|
-
JevCandidatesStore.unavailable("candidates.json ausente");
|
|
486
|
-
writeManifest(dirs, config, opts, classification, activeIds, pending, jev);
|
|
564
|
+
const jev = readCandidates(ctx, config);
|
|
565
|
+
writeManifest(ctx, config, opts, classification, activeIds, pending, jev);
|
|
487
566
|
|
|
488
567
|
if (pending.length > 0) {
|
|
489
568
|
throw new Error(
|
|
490
569
|
`Faltam ${pending.length}/${activeIds.length} findings: ${pending.join(", ")}.\n` +
|
|
491
|
-
`Cada watchdog deve gravar ${
|
|
570
|
+
`Cada watchdog deve gravar ${displayPath(ctx.work.findingsDir)}/<id>.json (schema ${OUTPUT_FORMAT_PATH}).`,
|
|
492
571
|
);
|
|
493
572
|
}
|
|
494
573
|
|
|
@@ -508,27 +587,61 @@ export function finalize(
|
|
|
508
587
|
// anexa o `id` de cada finding aos itens do relatório. A linha é cache —
|
|
509
588
|
// recalculada por `houndsman findings sync` após edições. O commit revisado
|
|
510
589
|
// (`reviewed.headSha`) habilita o review incremental e o diff focado do verify.
|
|
511
|
-
const store = FindingsStateStore.at(opts.dir, branch);
|
|
512
590
|
const state = buildStateFromFindings(merged, {
|
|
513
591
|
branch,
|
|
514
592
|
readFile: fileReader(process.cwd()),
|
|
515
|
-
previous:
|
|
593
|
+
previous: ctx.storage.readFindings(ctx.review.id),
|
|
594
|
+
reviewedPaths: new Set([
|
|
595
|
+
...reports.flatMap((report) => report.reviewedFiles),
|
|
596
|
+
// Arquivo deletado: o achado deixou de existir junto com ele.
|
|
597
|
+
...classification.deleted,
|
|
598
|
+
]),
|
|
516
599
|
now,
|
|
517
600
|
headSha: classification.headSha,
|
|
518
601
|
reviewed: classification.headSha
|
|
519
602
|
? { headSha: classification.headSha, at: now }
|
|
520
603
|
: undefined,
|
|
521
604
|
});
|
|
522
|
-
store.write(state);
|
|
523
|
-
messages.push(
|
|
524
|
-
`Triagem: ${relFromRoot(store.filePath)} — ${Object.keys(state.findings).length} finding(s).`,
|
|
525
|
-
);
|
|
526
|
-
writeRunStatus(dirs.outDir, { branch, status: "review_done" });
|
|
527
605
|
|
|
528
|
-
|
|
606
|
+
// Findings corrigidos entre revisões (ausentes + done) têm a conversa do PR
|
|
607
|
+
// resolvida automaticamente; as já resolvidas no GitHub são puladas. Só
|
|
608
|
+
// toca o `gh` quando há vínculo remoto pendente (fail-open).
|
|
609
|
+
const threadIds = Object.values(state.findings)
|
|
610
|
+
.filter(
|
|
611
|
+
(finding) =>
|
|
612
|
+
!finding.present &&
|
|
613
|
+
finding.execState === "done" &&
|
|
614
|
+
finding.github &&
|
|
615
|
+
finding.github.kind !== "comment" &&
|
|
616
|
+
!finding.github.resolvedAt,
|
|
617
|
+
)
|
|
618
|
+
.map((finding) => finding.id);
|
|
619
|
+
if (threadIds.length > 0) {
|
|
620
|
+
const sync = syncResolvedThreads({ state, ids: threadIds, now });
|
|
621
|
+
if (!sync.available) {
|
|
622
|
+
messages.push(`GitHub: threads não sincronizadas (${sync.reason}).`);
|
|
623
|
+
} else if (sync.checked > 0) {
|
|
624
|
+
const failures =
|
|
625
|
+
sync.failed.length > 0 ? `, ${sync.failed.length} falha(s)` : "";
|
|
626
|
+
messages.push(
|
|
627
|
+
`GitHub: ${sync.resolved.length} thread(s) resolvida(s), ${sync.skipped.length} já resolvida(s)/pulada(s)${failures}.`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
633
|
+
ctx.storage.updateReview(ctx.review.id, {
|
|
634
|
+
base: opts.base,
|
|
635
|
+
head: opts.head,
|
|
636
|
+
headSha: classification.headSha,
|
|
637
|
+
});
|
|
638
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
639
|
+
|
|
640
|
+
persistContext(ctx);
|
|
641
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
529
642
|
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
530
643
|
const domains = writeDomainReports(
|
|
531
|
-
|
|
644
|
+
ctx,
|
|
532
645
|
branch,
|
|
533
646
|
reports,
|
|
534
647
|
classification,
|
|
@@ -544,16 +657,40 @@ export function finalize(
|
|
|
544
657
|
jev.available ? jev.candidates : undefined,
|
|
545
658
|
domains,
|
|
546
659
|
);
|
|
547
|
-
|
|
548
|
-
|
|
660
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
661
|
+
name: REVIEW_FILE,
|
|
662
|
+
kind: "review",
|
|
663
|
+
content: `${incrementalNote(classification)}${markdown}\n`,
|
|
664
|
+
});
|
|
665
|
+
// Resumo final para leitura humana — não participa de nenhuma etapa seguinte.
|
|
666
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
667
|
+
name: REVIEW_RESUME_FILE,
|
|
668
|
+
kind: "resume",
|
|
669
|
+
content: renderReviewResume({
|
|
670
|
+
branch,
|
|
671
|
+
date: now.slice(0, 10),
|
|
672
|
+
context,
|
|
673
|
+
classification,
|
|
674
|
+
findings: merged,
|
|
675
|
+
nameOf,
|
|
676
|
+
}),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
680
|
+
messages.push(
|
|
681
|
+
`Triagem: ${location} — ${Object.keys(state.findings).length} finding(s).`,
|
|
682
|
+
);
|
|
549
683
|
|
|
550
684
|
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
551
685
|
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
552
686
|
messages.push(
|
|
553
|
-
`Relatório: ${
|
|
687
|
+
`Relatório: ${location} — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
554
688
|
);
|
|
555
689
|
|
|
556
|
-
|
|
690
|
+
// O staging já cumpriu o papel (briefs/findings/contexto estão no storage).
|
|
691
|
+
ctx.work.clean();
|
|
692
|
+
|
|
693
|
+
return { reportPath: location, blockers, attention, messages };
|
|
557
694
|
}
|
|
558
695
|
|
|
559
696
|
/** Resolve a branch (opção explícita ou a atual do git). */
|