@payconductor/houndsman 0.2.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/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +40 -0
- package/src/cli/commands/doctor-command.ts +60 -0
- package/src/cli/commands/findings-command.ts +38 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +21 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +47 -0
- package/src/cli/index.ts +60 -268
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +185 -260
- package/src/core/commands/review.ts +21 -39
- package/src/core/commands/scaffold.ts +189 -80
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +24 -25
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +65 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +79 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +193 -100
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +247 -421
- package/src/core/paths.ts +26 -11
- package/src/core/pr-context.ts +39 -51
- package/src/core/project-config.ts +56 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +123 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +8 -0
- package/src/core/run-status.ts +29 -59
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +139 -0
- package/src/core/services/project-files.ts +91 -0
- package/src/core/services/verifier.ts +58 -0
- 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/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +57 -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 +155 -245
- package/src/server/run-repository.ts +299 -0
- 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 +825 -0
- package/src/tests/baseline.test.ts +555 -493
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +328 -0
- 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 +205 -0
- 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
package/src/core/orchestrator.ts
CHANGED
|
@@ -5,68 +5,65 @@
|
|
|
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, relative } from "node:path";
|
|
19
|
-
import { renderBrief } from "./brief";
|
|
20
19
|
import {
|
|
21
20
|
type HoundsmanConfig,
|
|
22
21
|
resolveJevConcurrency,
|
|
23
22
|
resolveMaxFilesPerBrief,
|
|
24
23
|
} from "./config";
|
|
25
|
-
import {
|
|
24
|
+
import type { DiffSource } from "./diff-source";
|
|
26
25
|
import { discoverDocs } from "./doc-discoverer";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "./
|
|
34
|
-
import {
|
|
35
|
-
buildStateFromFindings,
|
|
36
|
-
type FindingsStateFile,
|
|
37
|
-
FindingsStateStore,
|
|
38
|
-
fileReader,
|
|
39
|
-
} from "./findings-state";
|
|
40
|
-
import { collectDiffSince, diffEntries, isAncestor, resolveSha } from "./git-revision";
|
|
26
|
+
import { Classification } from "./domain/classification";
|
|
27
|
+
import { Watchdog } from "./domain/watchdog";
|
|
28
|
+
import { mergeCarryover } from "./engine/carryover";
|
|
29
|
+
import { ReviewScopeResolver, type RunClassification } from "./engine/review-scope";
|
|
30
|
+
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
31
|
+
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
32
|
+
import { resolveSha } from "./git-revision";
|
|
41
33
|
import {
|
|
42
34
|
type JevCandidate,
|
|
43
35
|
type JevCandidatesFile,
|
|
44
36
|
JevCandidatesStore,
|
|
45
37
|
JevClient,
|
|
46
38
|
JevDetector,
|
|
39
|
+
parseCandidates,
|
|
40
|
+
serializeCandidates,
|
|
47
41
|
} from "./jev";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
42
|
+
import { displayPath, OUTPUT_FORMAT_PATH, REVIEW_FILE } from "./paths";
|
|
43
|
+
import { currentBranch } from "./pr-context";
|
|
44
|
+
import { ProjectConfig } from "./project-config";
|
|
45
|
+
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
46
|
+
import { ManifestWriter } from "./renderers/manifest-writer";
|
|
47
|
+
import { ReportRenderer } from "./renderers/report-renderer";
|
|
50
48
|
import {
|
|
51
49
|
type ContextSummary,
|
|
52
50
|
type DomainReport,
|
|
53
51
|
dedupeFindings,
|
|
54
|
-
|
|
55
|
-
parseWatchdogReport,
|
|
56
|
-
renderDomainReport,
|
|
57
|
-
renderReport,
|
|
52
|
+
parseContextSummary,
|
|
58
53
|
sanitizeBranch,
|
|
59
54
|
type WatchdogReport,
|
|
60
55
|
} from "./report-builder";
|
|
61
56
|
import { writeRunStatus } from "./run-status";
|
|
62
|
-
import {
|
|
57
|
+
import { systemClock } from "./services/clock";
|
|
58
|
+
import type { ReviewRecord, StorageProvider } from "./storage/types";
|
|
59
|
+
import { ReviewWorkArea } from "./storage/work-area";
|
|
60
|
+
import type { WatchdogMeta } from "./types";
|
|
63
61
|
import { findWatchdog } from "./watchdog-registry";
|
|
64
62
|
|
|
65
63
|
export type ReviewOptions = {
|
|
66
64
|
base: string;
|
|
67
65
|
head: string;
|
|
68
66
|
branch?: string;
|
|
69
|
-
dir: string;
|
|
70
67
|
finalize: boolean;
|
|
71
68
|
failOnBlocking: boolean;
|
|
72
69
|
clean: boolean;
|
|
@@ -76,11 +73,10 @@ export type ReviewOptions = {
|
|
|
76
73
|
};
|
|
77
74
|
|
|
78
75
|
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
79
|
-
export function defaultOptions(
|
|
76
|
+
export function defaultOptions(): ReviewOptions {
|
|
80
77
|
return {
|
|
81
78
|
base: "main",
|
|
82
79
|
head: "HEAD",
|
|
83
|
-
dir: artifactDir,
|
|
84
80
|
finalize: false,
|
|
85
81
|
failOnBlocking: false,
|
|
86
82
|
clean: false,
|
|
@@ -89,64 +85,30 @@ export function defaultOptions(artifactDir: string = ARTIFACT_DIR): ReviewOption
|
|
|
89
85
|
};
|
|
90
86
|
}
|
|
91
87
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
type Paths = {
|
|
98
|
-
slug: string;
|
|
99
|
-
outDir: string;
|
|
100
|
-
briefsDir: string;
|
|
101
|
-
findingsDir: string;
|
|
88
|
+
/** Contexto de uma run: storage + review + staging. */
|
|
89
|
+
export type ReviewContext = {
|
|
90
|
+
storage: StorageProvider;
|
|
91
|
+
review: ReviewRecord;
|
|
92
|
+
work: ReviewWorkArea;
|
|
102
93
|
};
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
briefsDir: join(outDir, "briefs"),
|
|
111
|
-
findingsDir: join(outDir, "findings"),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function prepareDirs(dirs: Paths, clean: boolean): void {
|
|
116
|
-
mkdirSync(dirs.briefsDir, { recursive: true });
|
|
117
|
-
mkdirSync(dirs.findingsDir, { recursive: true });
|
|
118
|
-
if (clean) {
|
|
119
|
-
rmSync(dirs.findingsDir, { recursive: true, force: true });
|
|
120
|
-
mkdirSync(dirs.findingsDir, { recursive: true });
|
|
121
|
-
}
|
|
95
|
+
/** Cria o contexto de uma review existente. */
|
|
96
|
+
export function reviewContext(
|
|
97
|
+
storage: StorageProvider,
|
|
98
|
+
review: ReviewRecord,
|
|
99
|
+
): ReviewContext {
|
|
100
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
122
101
|
}
|
|
123
102
|
|
|
124
103
|
// ---------------------------------------------------------------------------
|
|
125
104
|
// Escopo da revisão (completo vs incremental)
|
|
126
105
|
// ---------------------------------------------------------------------------
|
|
127
106
|
|
|
128
|
-
/** Classificação enriquecida gravada no `classification.json` (modo + revisão + carry-over). */
|
|
129
|
-
type RunClassification = ClassificationResult & {
|
|
130
|
-
/** Commit revisado (head do diff). */
|
|
131
|
-
headSha?: string;
|
|
132
|
-
/** Modo do prepare: completo ou incremental (delta desde a última revisão). */
|
|
133
|
-
mode: "full" | "incremental";
|
|
134
|
-
/** Commit da última revisão (só no incremental). */
|
|
135
|
-
reviewedFrom?: string;
|
|
136
|
-
/** Findings preservados do review anterior (arquivos inalterados). */
|
|
137
|
-
carryover?: MergedFinding[];
|
|
138
|
-
};
|
|
139
|
-
|
|
140
107
|
/** SHA curto para logs (7 chars, como o git). */
|
|
141
108
|
function shortSha(sha: string): string {
|
|
142
109
|
return sha.slice(0, 7);
|
|
143
110
|
}
|
|
144
111
|
|
|
145
|
-
/** Chave de dedupe de finding: `(file, line, rule)` — igual ao `dedupeFindings`. */
|
|
146
|
-
function findingKey(finding: Pick<MergedFinding, "file" | "line" | "rule">): string {
|
|
147
|
-
return `${finding.file}|${finding.line ?? ""}|${finding.rule}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
112
|
/** Aviso no topo do `review.md` quando a revisão foi incremental (senão vazio). */
|
|
151
113
|
function incrementalNote(classification: RunClassification): string {
|
|
152
114
|
if (classification.mode !== "incremental" || !classification.reviewedFrom) return "";
|
|
@@ -158,215 +120,6 @@ function incrementalNote(classification: RunClassification): string {
|
|
|
158
120
|
);
|
|
159
121
|
}
|
|
160
122
|
|
|
161
|
-
/**
|
|
162
|
-
* Findings do review anterior em arquivos inalterados — carregados sem
|
|
163
|
-
* re-revisão (carry-over). Preserva o `foundAtSha` para o diff focado do verify.
|
|
164
|
-
*/
|
|
165
|
-
function buildCarryover(
|
|
166
|
-
previous: FindingsStateFile | null,
|
|
167
|
-
changedPaths: Set<string>,
|
|
168
|
-
): MergedFinding[] {
|
|
169
|
-
if (!previous) return [];
|
|
170
|
-
return Object.values(previous.findings)
|
|
171
|
-
.filter((finding) => finding.present && !changedPaths.has(finding.file))
|
|
172
|
-
.map((finding) => ({
|
|
173
|
-
id: finding.id,
|
|
174
|
-
rule: finding.rule,
|
|
175
|
-
severity: finding.severity,
|
|
176
|
-
file: finding.file,
|
|
177
|
-
line: finding.line ?? undefined,
|
|
178
|
-
message: finding.message,
|
|
179
|
-
action: finding.action,
|
|
180
|
-
evidence: finding.evidence,
|
|
181
|
-
foundAtSha: finding.foundAtSha,
|
|
182
|
-
watchdogs: [...finding.watchdogIds],
|
|
183
|
-
}));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Une os findings re-reportados com os carry-over (preserva ids do estado anterior). */
|
|
187
|
-
function mergeCarryover(
|
|
188
|
-
merged: MergedFinding[],
|
|
189
|
-
carryover: readonly MergedFinding[],
|
|
190
|
-
): MergedFinding[] {
|
|
191
|
-
const byKey = new Map(merged.map((finding) => [findingKey(finding), finding]));
|
|
192
|
-
for (const carried of carryover) {
|
|
193
|
-
const existing = byKey.get(findingKey(carried));
|
|
194
|
-
if (existing) {
|
|
195
|
-
for (const watchdog of carried.watchdogs) {
|
|
196
|
-
if (!existing.watchdogs.includes(watchdog))
|
|
197
|
-
existing.watchdogs.push(watchdog);
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
byKey.set(findingKey(carried), carried);
|
|
202
|
-
merged.push(carried);
|
|
203
|
-
}
|
|
204
|
-
merged.sort((a, b) => {
|
|
205
|
-
const bySeverity = severityRank(a.severity) - severityRank(b.severity);
|
|
206
|
-
if (bySeverity !== 0) return bySeverity;
|
|
207
|
-
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
208
|
-
return (a.line ?? 0) - (b.line ?? 0);
|
|
209
|
-
});
|
|
210
|
-
return merged;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Decide entre review completo e incremental. Incremental só quando há um
|
|
215
|
-
* `reviewed.headSha` anterior, o `HEAD` atual o sucede (sem rebase) e não veio
|
|
216
|
-
* `--full`/`--clean`. Reconstrói só o delta `prevSha..headSha` e carrega os
|
|
217
|
-
* findings de arquivos inalterados do estado anterior.
|
|
218
|
-
*/
|
|
219
|
-
function resolveReviewScope(
|
|
220
|
-
opts: ReviewOptions,
|
|
221
|
-
previous: FindingsStateFile | null,
|
|
222
|
-
headSha: string,
|
|
223
|
-
config: HoundsmanConfig,
|
|
224
|
-
): { classification: RunClassification; diff: DiffSource; reviewedFrom?: string } {
|
|
225
|
-
const prevSha = previous?.reviewed?.headSha;
|
|
226
|
-
if (
|
|
227
|
-
opts.full ||
|
|
228
|
-
opts.clean ||
|
|
229
|
-
!prevSha ||
|
|
230
|
-
prevSha === headSha ||
|
|
231
|
-
!isAncestor(prevSha, headSha)
|
|
232
|
-
) {
|
|
233
|
-
const diff = collectDiff(opts.base, opts.head);
|
|
234
|
-
return {
|
|
235
|
-
diff,
|
|
236
|
-
classification: {
|
|
237
|
-
...classifyRange(opts.base, opts.head, config, diff),
|
|
238
|
-
headSha,
|
|
239
|
-
mode: "full",
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const range = `${prevSha}..${headSha}`;
|
|
245
|
-
const diff = collectDiffSince(prevSha, headSha);
|
|
246
|
-
const entries = diffEntries(range);
|
|
247
|
-
const changedPaths = new Set(entries.map((entry) => entry.path));
|
|
248
|
-
const classification: RunClassification = {
|
|
249
|
-
...classifyEntries(entries, config, churnToRecord(diff.churn)),
|
|
250
|
-
base: prevSha,
|
|
251
|
-
head: opts.head,
|
|
252
|
-
headSha,
|
|
253
|
-
mode: "incremental",
|
|
254
|
-
reviewedFrom: prevSha,
|
|
255
|
-
carryover: buildCarryover(previous, changedPaths),
|
|
256
|
-
};
|
|
257
|
-
return { classification, diff, reviewedFrom: prevSha };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Findings
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
|
|
264
|
-
function readFindings(
|
|
265
|
-
findingsDir: string,
|
|
266
|
-
activeIds: string[],
|
|
267
|
-
): { reports: WatchdogReport[]; pending: string[] } {
|
|
268
|
-
const reports: WatchdogReport[] = [];
|
|
269
|
-
const pending: string[] = [];
|
|
270
|
-
for (const id of activeIds) {
|
|
271
|
-
const path = join(findingsDir, `${id}.json`);
|
|
272
|
-
if (!existsSync(path)) {
|
|
273
|
-
pending.push(id);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
278
|
-
const report = parseWatchdogReport(raw, path);
|
|
279
|
-
if (report) reports.push(report);
|
|
280
|
-
else pending.push(id);
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error(
|
|
283
|
-
`[houndsman] ${relFromRoot(path)} inválido: ${err instanceof Error ? err.message : err}`,
|
|
284
|
-
);
|
|
285
|
-
pending.push(id);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return { reports, pending };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// Manifesto
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
function contextManifest(
|
|
296
|
-
opts: ReviewOptions,
|
|
297
|
-
branch: string,
|
|
298
|
-
): { summarized: boolean; source: ContextSummary["source"] | null } {
|
|
299
|
-
const summary = readContextSummary(contextPaths(opts.dir, branch).summary);
|
|
300
|
-
if (!summary) return { summarized: false, source: null };
|
|
301
|
-
return { summarized: true, source: summary.source ?? null };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function writeManifest(
|
|
305
|
-
outDir: string,
|
|
306
|
-
opts: ReviewOptions,
|
|
307
|
-
branch: string,
|
|
308
|
-
classification: RunClassification,
|
|
309
|
-
activeIds: string[],
|
|
310
|
-
pending: string[],
|
|
311
|
-
jev: JevCandidatesFile,
|
|
312
|
-
config: HoundsmanConfig,
|
|
313
|
-
): void {
|
|
314
|
-
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
315
|
-
let filesOverBudget = 0;
|
|
316
|
-
const watchdogs = activeIds.map((id) => {
|
|
317
|
-
const selection = budgetFor(classification, id, maxFilesPerBrief);
|
|
318
|
-
filesOverBudget += selection.overflow.length;
|
|
319
|
-
return {
|
|
320
|
-
id,
|
|
321
|
-
name: findWatchdog(config, id)?.name ?? id,
|
|
322
|
-
files: selection.selected,
|
|
323
|
-
overflow: selection.overflow.length,
|
|
324
|
-
brief: relFromRoot(join(outDir, "briefs", `${id}.md`)),
|
|
325
|
-
findings: relFromRoot(join(outDir, "findings", `${id}.json`)),
|
|
326
|
-
status: pending.includes(id) ? "pending" : "done",
|
|
327
|
-
};
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const manifest = {
|
|
331
|
-
branch,
|
|
332
|
-
slug: sanitizeBranch(branch),
|
|
333
|
-
base: opts.base,
|
|
334
|
-
head: opts.head,
|
|
335
|
-
headSha: classification.headSha,
|
|
336
|
-
generatedAt: new Date().toISOString(),
|
|
337
|
-
outputFormat: OUTPUT_FORMAT_PATH,
|
|
338
|
-
budget: { maxFilesPerBrief, filesOverBudget },
|
|
339
|
-
churn: churnTotals(classification),
|
|
340
|
-
incremental:
|
|
341
|
-
classification.mode === "incremental"
|
|
342
|
-
? {
|
|
343
|
-
from: classification.reviewedFrom,
|
|
344
|
-
changedFiles: Object.keys(classification.files).length,
|
|
345
|
-
carried: classification.carryover?.length ?? 0,
|
|
346
|
-
}
|
|
347
|
-
: undefined,
|
|
348
|
-
watchdogs,
|
|
349
|
-
context: contextManifest(opts, branch),
|
|
350
|
-
jev: jev.available
|
|
351
|
-
? {
|
|
352
|
-
available: true,
|
|
353
|
-
model: jev.model,
|
|
354
|
-
threshold: jev.threshold,
|
|
355
|
-
analyzedFiles: jev.analyzedFiles,
|
|
356
|
-
candidates: jev.candidates.length,
|
|
357
|
-
skipped: jev.skipped?.length ?? 0,
|
|
358
|
-
}
|
|
359
|
-
: { available: false, reason: jev.reason },
|
|
360
|
-
deleted: classification.deleted,
|
|
361
|
-
unclassified: classification.unclassified,
|
|
362
|
-
excludedCount: classification.excludedCount,
|
|
363
|
-
};
|
|
364
|
-
writeFileSync(
|
|
365
|
-
join(outDir, "manifest.json"),
|
|
366
|
-
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
123
|
// ---------------------------------------------------------------------------
|
|
371
124
|
// Orçamento (triage de PRs grandes)
|
|
372
125
|
// ---------------------------------------------------------------------------
|
|
@@ -377,25 +130,26 @@ function budgetFor(
|
|
|
377
130
|
id: string,
|
|
378
131
|
maxFiles: number,
|
|
379
132
|
): BudgetSelection {
|
|
380
|
-
return
|
|
381
|
-
classification.watchdogs[id] ?? [],
|
|
382
|
-
classification.churn,
|
|
383
|
-
maxFiles,
|
|
384
|
-
);
|
|
133
|
+
return Classification.from(classification).budgetFor(id, maxFiles);
|
|
385
134
|
}
|
|
386
135
|
|
|
387
|
-
/**
|
|
388
|
-
function
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
136
|
+
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
137
|
+
function writeManifest(
|
|
138
|
+
ctx: ReviewContext,
|
|
139
|
+
config: HoundsmanConfig,
|
|
140
|
+
opts: ReviewOptions,
|
|
141
|
+
classification: RunClassification,
|
|
142
|
+
activeIds: string[],
|
|
143
|
+
pending: string[],
|
|
144
|
+
jev: JevCandidatesFile,
|
|
145
|
+
): void {
|
|
146
|
+
new ManifestWriter(
|
|
147
|
+
ctx.storage,
|
|
148
|
+
ctx.review,
|
|
149
|
+
ctx.work,
|
|
150
|
+
new ProjectConfig(config),
|
|
151
|
+
opts,
|
|
152
|
+
).write(classification, activeIds, pending, jev);
|
|
399
153
|
}
|
|
400
154
|
|
|
401
155
|
// ---------------------------------------------------------------------------
|
|
@@ -405,19 +159,21 @@ function churnTotals(classification: ClassificationResult): {
|
|
|
405
159
|
function writeBriefs(
|
|
406
160
|
activeIds: string[],
|
|
407
161
|
classification: ClassificationResult,
|
|
408
|
-
|
|
162
|
+
ctx: ReviewContext,
|
|
409
163
|
branch: string,
|
|
410
164
|
context: ContextSummary | null,
|
|
411
165
|
candidates: JevCandidate[],
|
|
412
166
|
config: HoundsmanConfig,
|
|
413
167
|
): void {
|
|
168
|
+
const work = ctx.work;
|
|
414
169
|
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
415
170
|
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
171
|
+
const renderer = new BriefRenderer();
|
|
416
172
|
for (const id of activeIds) {
|
|
417
173
|
const def = findWatchdog(config, id);
|
|
418
174
|
if (!def) continue;
|
|
419
175
|
const { selected, overflow } = budgetFor(classification, id, maxFilesPerBrief);
|
|
420
|
-
const brief =
|
|
176
|
+
const brief = renderer.render(new Watchdog(def), {
|
|
421
177
|
watchdogId: id,
|
|
422
178
|
files: selected,
|
|
423
179
|
statuses: classification.files,
|
|
@@ -425,13 +181,20 @@ function writeBriefs(
|
|
|
425
181
|
overflow,
|
|
426
182
|
deleted: classification.deleted,
|
|
427
183
|
branch,
|
|
428
|
-
outputPath:
|
|
184
|
+
outputPath: work.findingFile(id),
|
|
429
185
|
outputFormatPath: OUTPUT_FORMAT_PATH,
|
|
430
186
|
docs,
|
|
431
187
|
context,
|
|
432
188
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
433
189
|
});
|
|
434
|
-
|
|
190
|
+
const content = `${brief}\n`;
|
|
191
|
+
work.writeBrief(id, content);
|
|
192
|
+
// Auditoria: o brief também fica no storage (o staging é limpo no finalize).
|
|
193
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
194
|
+
name: `briefs/${id}.md`,
|
|
195
|
+
kind: "brief",
|
|
196
|
+
content,
|
|
197
|
+
});
|
|
435
198
|
}
|
|
436
199
|
}
|
|
437
200
|
|
|
@@ -446,16 +209,16 @@ function writeBriefs(
|
|
|
446
209
|
async function resolveCandidates(
|
|
447
210
|
opts: ReviewOptions,
|
|
448
211
|
classification: ClassificationResult,
|
|
449
|
-
|
|
212
|
+
ctx: ReviewContext,
|
|
450
213
|
config: HoundsmanConfig,
|
|
451
214
|
diff: DiffSource,
|
|
452
215
|
): Promise<JevCandidatesFile> {
|
|
453
|
-
const store = new JevCandidatesStore(
|
|
454
|
-
join(dirs.outDir, "candidates.json"),
|
|
455
|
-
config.watchdogIds,
|
|
456
|
-
);
|
|
457
216
|
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
458
|
-
|
|
217
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
218
|
+
name: "candidates.json",
|
|
219
|
+
kind: "candidates",
|
|
220
|
+
content: serializeCandidates(data),
|
|
221
|
+
});
|
|
459
222
|
return data;
|
|
460
223
|
};
|
|
461
224
|
|
|
@@ -494,138 +257,198 @@ async function resolveCandidates(
|
|
|
494
257
|
}
|
|
495
258
|
}
|
|
496
259
|
|
|
497
|
-
function
|
|
498
|
-
if (!jev.available) {
|
|
499
|
-
console.log(`Jev: pulado (${jev.reason}).`);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
260
|
+
function jevSummary(jev: JevCandidatesFile): string[] {
|
|
261
|
+
if (!jev.available) return [`Jev: pulado (${jev.reason}).`];
|
|
502
262
|
const skipped = jev.skipped?.length ?? 0;
|
|
503
|
-
|
|
263
|
+
return [
|
|
504
264
|
`Jev: ${jev.candidates.length} candidato(s) em ${jev.analyzedFiles ?? 0} arquivo(s)` +
|
|
505
265
|
(skipped > 0 ? `, ${skipped} com falha.` : "."),
|
|
506
|
-
|
|
266
|
+
];
|
|
507
267
|
}
|
|
508
268
|
|
|
509
|
-
function
|
|
269
|
+
function discoveryMessages(
|
|
510
270
|
activeIds: string[],
|
|
511
271
|
classification: ClassificationResult,
|
|
512
272
|
briefsDir: string,
|
|
513
|
-
):
|
|
514
|
-
|
|
273
|
+
): string[] {
|
|
274
|
+
const messages = [
|
|
515
275
|
`Discovery: ${activeIds.length} watchdog(s) ativado(s), ${Object.keys(classification.files).length} arquivo(s), ${classification.deleted.length} deletado(s).`,
|
|
516
|
-
|
|
517
|
-
|
|
276
|
+
`Briefs: ${displayPath(briefsDir)}/`,
|
|
277
|
+
];
|
|
518
278
|
for (const file of classification.deleted) {
|
|
519
|
-
|
|
279
|
+
messages.push(`Deletado (breaking change): ${file}`);
|
|
520
280
|
}
|
|
281
|
+
return messages;
|
|
521
282
|
}
|
|
522
283
|
|
|
523
284
|
/** Resumo do orçamento — só quando algum arquivo ficou de fora (PR grande). */
|
|
524
|
-
function
|
|
285
|
+
function budgetMessage(
|
|
525
286
|
classification: ClassificationResult,
|
|
526
287
|
config: HoundsmanConfig,
|
|
527
|
-
):
|
|
288
|
+
): string[] {
|
|
528
289
|
const maxFiles = resolveMaxFilesPerBrief(config);
|
|
529
290
|
let overflow = 0;
|
|
530
291
|
for (const id of Object.keys(classification.watchdogs)) {
|
|
531
292
|
overflow += budgetFor(classification, id, maxFiles).overflow.length;
|
|
532
293
|
}
|
|
533
|
-
if (overflow === 0) return;
|
|
534
|
-
|
|
294
|
+
if (overflow === 0) return [];
|
|
295
|
+
return [
|
|
535
296
|
`Orçamento: ${maxFiles} arquivo(s)/brief — ${overflow} fora do orçamento (menor churn; registrar em skipped).`,
|
|
536
|
-
|
|
297
|
+
];
|
|
537
298
|
}
|
|
538
299
|
|
|
300
|
+
/** Resultado do prepare — mensagens para o CLI imprimir. */
|
|
301
|
+
export type PrepareResult = { messages: string[] };
|
|
302
|
+
|
|
539
303
|
/** Passo 1 — descobre os arquivos e grava briefs + manifesto. */
|
|
540
304
|
export async function prepare(
|
|
541
305
|
opts: ReviewOptions,
|
|
542
306
|
branch: string,
|
|
543
307
|
config: HoundsmanConfig,
|
|
544
|
-
|
|
545
|
-
): Promise<
|
|
546
|
-
|
|
308
|
+
ctx: ReviewContext,
|
|
309
|
+
): Promise<PrepareResult> {
|
|
310
|
+
const messages: string[] = [];
|
|
311
|
+
ctx.work.ensure();
|
|
312
|
+
if (opts.clean) ctx.work.cleanFindings();
|
|
313
|
+
ctx.storage.updateReview(ctx.review.id, { base: opts.base, head: opts.head });
|
|
547
314
|
|
|
548
315
|
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
549
|
-
writeRunStatus(
|
|
316
|
+
writeRunStatus(ctx.storage, ctx.review.id, {
|
|
317
|
+
status: "reviewing",
|
|
318
|
+
resetStartedAt: true,
|
|
319
|
+
});
|
|
550
320
|
|
|
551
321
|
// Marca o commit revisado e decide entre review completo e incremental.
|
|
552
322
|
const headSha = resolveSha(opts.head);
|
|
553
|
-
const previous =
|
|
554
|
-
const { classification, diff } =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
`${JSON.stringify(classification, null, 2)}\n`,
|
|
323
|
+
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
324
|
+
const { classification, diff } = new ReviewScopeResolver(opts, config).resolve(
|
|
325
|
+
previous,
|
|
326
|
+
headSha,
|
|
558
327
|
);
|
|
328
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
329
|
+
name: "classification.json",
|
|
330
|
+
kind: "classification",
|
|
331
|
+
content: `${JSON.stringify(classification, null, 2)}\n`,
|
|
332
|
+
});
|
|
559
333
|
|
|
560
|
-
const jev = await resolveCandidates(opts, classification,
|
|
334
|
+
const jev = await resolveCandidates(opts, classification, ctx, config, diff);
|
|
561
335
|
|
|
562
336
|
const activeIds = Object.keys(classification.watchdogs);
|
|
563
337
|
if (Object.keys(classification.files).length === 0) {
|
|
564
|
-
writeRunStatus(
|
|
565
|
-
|
|
338
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
339
|
+
messages.push(
|
|
566
340
|
classification.mode === "incremental"
|
|
567
341
|
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
568
342
|
: `Nenhuma alteração entre ${opts.base}...${opts.head}.`,
|
|
569
343
|
);
|
|
570
|
-
return;
|
|
344
|
+
return { messages };
|
|
571
345
|
}
|
|
572
346
|
if (activeIds.length === 0) {
|
|
573
|
-
writeRunStatus(
|
|
574
|
-
|
|
575
|
-
|
|
347
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
348
|
+
messages.push(
|
|
349
|
+
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
350
|
+
);
|
|
351
|
+
return { messages };
|
|
576
352
|
}
|
|
577
353
|
if (classification.mode === "incremental") {
|
|
578
|
-
|
|
354
|
+
messages.push(
|
|
579
355
|
`Modo incremental: ${Object.keys(classification.files).length} arquivo(s) alterado(s) desde ${shortSha(classification.reviewedFrom ?? "")}; ${classification.carryover?.length ?? 0} finding(s) carregado(s) do review anterior.`,
|
|
580
356
|
);
|
|
581
357
|
}
|
|
582
358
|
|
|
583
|
-
const context = readContextSummary(
|
|
359
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
360
|
+
persistContext(ctx);
|
|
584
361
|
|
|
585
|
-
writeBriefs(activeIds, classification,
|
|
586
|
-
writeManifest(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
classification,
|
|
591
|
-
|
|
592
|
-
activeIds,
|
|
593
|
-
jev,
|
|
594
|
-
config,
|
|
362
|
+
writeBriefs(activeIds, classification, ctx, branch, context, jev.candidates, config);
|
|
363
|
+
writeManifest(ctx, config, opts, classification, activeIds, activeIds, jev);
|
|
364
|
+
|
|
365
|
+
messages.push(
|
|
366
|
+
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
367
|
+
...budgetMessage(classification, config),
|
|
368
|
+
...jevSummary(jev),
|
|
595
369
|
);
|
|
596
|
-
printDiscovery(activeIds, classification, dirs.briefsDir);
|
|
597
|
-
printBudget(classification, config);
|
|
598
|
-
printJevSummary(jev);
|
|
599
370
|
|
|
600
371
|
if (context) {
|
|
601
|
-
|
|
372
|
+
messages.push("Contexto da PR resumido e injetado nos briefs.");
|
|
602
373
|
} else {
|
|
603
|
-
|
|
374
|
+
messages.push(
|
|
604
375
|
"Sem contexto da PR (rode `bun run houndsman context` antes, se houver PR).",
|
|
605
376
|
);
|
|
606
377
|
}
|
|
607
378
|
|
|
608
|
-
|
|
609
|
-
`\nPróximo passo: despachar os sub-agentes (um por brief em ${
|
|
379
|
+
messages.push(
|
|
380
|
+
`\nPróximo passo: despachar os sub-agentes (um por brief em ${displayPath(ctx.work.briefsDir)}/).`,
|
|
381
|
+
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
610
382
|
);
|
|
611
|
-
|
|
383
|
+
|
|
384
|
+
return { messages };
|
|
385
|
+
}
|
|
386
|
+
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
387
|
+
function persistContext(ctx: ReviewContext): void {
|
|
388
|
+
const raw = ctx.work.readContextRaw();
|
|
389
|
+
if (raw !== null) {
|
|
390
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
391
|
+
name: "context.json",
|
|
392
|
+
kind: "context",
|
|
393
|
+
content: raw,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const summary = ctx.work.readContextSummaryRaw();
|
|
397
|
+
if (summary !== null) {
|
|
398
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
399
|
+
name: "context-summary.json",
|
|
400
|
+
kind: "context-summary",
|
|
401
|
+
content: summary,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const brief = ctx.work.readContextBriefRaw();
|
|
405
|
+
if (brief !== null) {
|
|
406
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
407
|
+
name: "context-brief.md",
|
|
408
|
+
kind: "context-brief",
|
|
409
|
+
content: brief,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
612
412
|
}
|
|
613
413
|
|
|
614
414
|
// ---------------------------------------------------------------------------
|
|
615
415
|
// Passo 2 — finalizar
|
|
616
416
|
// ---------------------------------------------------------------------------
|
|
617
417
|
|
|
618
|
-
/** Lê o classification.json
|
|
619
|
-
function readClassification(
|
|
620
|
-
|
|
418
|
+
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
419
|
+
function readClassification(ctx: ReviewContext): RunClassification {
|
|
420
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "classification.json");
|
|
421
|
+
if (!artifact) {
|
|
621
422
|
throw new Error(
|
|
622
|
-
`Não encontrei
|
|
423
|
+
`Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
|
|
623
424
|
);
|
|
624
425
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
426
|
+
return JSON.parse(artifact.content) as RunClassification;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
|
|
430
|
+
function readCandidates(ctx: ReviewContext, config: HoundsmanConfig): JevCandidatesFile {
|
|
431
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "candidates.json");
|
|
432
|
+
if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
|
|
433
|
+
try {
|
|
434
|
+
return (
|
|
435
|
+
parseCandidates(JSON.parse(artifact.content), config.watchdogIds) ??
|
|
436
|
+
JevCandidatesStore.unavailable("candidates.json inválido")
|
|
437
|
+
);
|
|
438
|
+
} catch {
|
|
439
|
+
return JevCandidatesStore.unavailable("candidates.json inválido");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Resumo do contexto persistido (usado quando o staging já foi limpo). */
|
|
444
|
+
function readPersistedContextSummary(ctx: ReviewContext): ContextSummary | null {
|
|
445
|
+
const artifact = ctx.storage.readArtifact(ctx.review.id, "context-summary.json");
|
|
446
|
+
if (!artifact) return null;
|
|
447
|
+
try {
|
|
448
|
+
return parseContextSummary(JSON.parse(artifact.content));
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
629
452
|
}
|
|
630
453
|
|
|
631
454
|
/**
|
|
@@ -633,7 +456,7 @@ function readClassification(classificationPath: string): RunClassification {
|
|
|
633
456
|
* Mantém o `review.md` consolidado navegável em PRs grandes (centenas de achados).
|
|
634
457
|
*/
|
|
635
458
|
function writeDomainReports(
|
|
636
|
-
|
|
459
|
+
ctx: ReviewContext,
|
|
637
460
|
branch: string,
|
|
638
461
|
reports: WatchdogReport[],
|
|
639
462
|
classification: ClassificationResult,
|
|
@@ -641,22 +464,23 @@ function writeDomainReports(
|
|
|
641
464
|
nameOf: (id: string) => string,
|
|
642
465
|
): DomainReport[] {
|
|
643
466
|
const domains: DomainReport[] = [];
|
|
467
|
+
const renderer = new ReportRenderer(nameOf);
|
|
644
468
|
for (const report of reports) {
|
|
645
469
|
const findings = dedupeFindings([report]);
|
|
646
470
|
if (findings.length === 0) continue;
|
|
647
471
|
const file = `review.${sanitizeBranch(report.watchdogId)}.md`;
|
|
648
472
|
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
473
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
474
|
+
name: file,
|
|
475
|
+
kind: "domain-review",
|
|
476
|
+
content: `${renderer.renderDomain(
|
|
652
477
|
branch,
|
|
653
478
|
report,
|
|
654
479
|
findings,
|
|
655
480
|
expected.length,
|
|
656
481
|
context,
|
|
657
|
-
nameOf,
|
|
658
482
|
)}\n`,
|
|
659
|
-
);
|
|
483
|
+
});
|
|
660
484
|
domains.push({
|
|
661
485
|
id: report.watchdogId,
|
|
662
486
|
name: nameOf(report.watchdogId),
|
|
@@ -673,6 +497,7 @@ export type FinalizeResult = {
|
|
|
673
497
|
reportPath: string;
|
|
674
498
|
blockers: number;
|
|
675
499
|
attention: number;
|
|
500
|
+
messages: string[];
|
|
676
501
|
};
|
|
677
502
|
|
|
678
503
|
/**
|
|
@@ -683,43 +508,32 @@ export function finalize(
|
|
|
683
508
|
opts: ReviewOptions,
|
|
684
509
|
branch: string,
|
|
685
510
|
config: HoundsmanConfig,
|
|
686
|
-
|
|
511
|
+
ctx: ReviewContext,
|
|
687
512
|
): FinalizeResult {
|
|
688
|
-
const classification = readClassification(
|
|
513
|
+
const classification = readClassification(ctx);
|
|
689
514
|
const activeIds = Object.keys(classification.watchdogs);
|
|
690
515
|
|
|
691
|
-
const
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
).read() ?? JevCandidatesStore.unavailable("candidates.json ausente");
|
|
697
|
-
writeManifest(
|
|
698
|
-
dirs.outDir,
|
|
699
|
-
opts,
|
|
700
|
-
branch,
|
|
701
|
-
classification,
|
|
702
|
-
activeIds,
|
|
703
|
-
pending,
|
|
704
|
-
jev,
|
|
705
|
-
config,
|
|
706
|
-
);
|
|
516
|
+
const messages: string[] = [];
|
|
517
|
+
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
518
|
+
messages.push(...warnings);
|
|
519
|
+
const jev = readCandidates(ctx, config);
|
|
520
|
+
writeManifest(ctx, config, opts, classification, activeIds, pending, jev);
|
|
707
521
|
|
|
708
522
|
if (pending.length > 0) {
|
|
709
523
|
throw new Error(
|
|
710
524
|
`Faltam ${pending.length}/${activeIds.length} findings: ${pending.join(", ")}.\n` +
|
|
711
|
-
`Cada watchdog deve gravar ${
|
|
525
|
+
`Cada watchdog deve gravar ${displayPath(ctx.work.findingsDir)}/<id>.json (schema ${OUTPUT_FORMAT_PATH}).`,
|
|
712
526
|
);
|
|
713
527
|
}
|
|
714
528
|
|
|
715
|
-
const now =
|
|
529
|
+
const now = systemClock();
|
|
716
530
|
// Findings re-reportados neste ciclo + carry-over (arquivos inalterados).
|
|
717
531
|
const merged = mergeCarryover(
|
|
718
532
|
dedupeFindings(reports),
|
|
719
533
|
classification.carryover ?? [],
|
|
720
534
|
);
|
|
721
535
|
if (classification.mode === "incremental") {
|
|
722
|
-
|
|
536
|
+
messages.push(
|
|
723
537
|
`Carry-over: ${classification.carryover?.length ?? 0} finding(s) preservado(s) de ${shortSha(classification.reviewedFrom ?? "")}.`,
|
|
724
538
|
);
|
|
725
539
|
}
|
|
@@ -728,53 +542,65 @@ export function finalize(
|
|
|
728
542
|
// anexa o `id` de cada finding aos itens do relatório. A linha é cache —
|
|
729
543
|
// recalculada por `houndsman findings sync` após edições. O commit revisado
|
|
730
544
|
// (`reviewed.headSha`) habilita o review incremental e o diff focado do verify.
|
|
731
|
-
const store = FindingsStateStore.at(opts.dir, branch);
|
|
732
545
|
const state = buildStateFromFindings(merged, {
|
|
733
546
|
branch,
|
|
734
547
|
readFile: fileReader(process.cwd()),
|
|
735
|
-
previous:
|
|
548
|
+
previous: ctx.storage.readFindings(ctx.review.id),
|
|
736
549
|
now,
|
|
737
550
|
headSha: classification.headSha,
|
|
738
551
|
reviewed: classification.headSha
|
|
739
552
|
? { headSha: classification.headSha, at: now }
|
|
740
553
|
: undefined,
|
|
741
554
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
555
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
556
|
+
ctx.storage.updateReview(ctx.review.id, {
|
|
557
|
+
base: opts.base,
|
|
558
|
+
head: opts.head,
|
|
559
|
+
headSha: classification.headSha,
|
|
560
|
+
});
|
|
561
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
747
562
|
|
|
748
|
-
|
|
563
|
+
persistContext(ctx);
|
|
564
|
+
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
749
565
|
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
750
566
|
const domains = writeDomainReports(
|
|
751
|
-
|
|
567
|
+
ctx,
|
|
752
568
|
branch,
|
|
753
569
|
reports,
|
|
754
570
|
classification,
|
|
755
571
|
context,
|
|
756
572
|
nameOf,
|
|
757
573
|
);
|
|
758
|
-
const markdown =
|
|
574
|
+
const markdown = new ReportRenderer(nameOf).render(
|
|
759
575
|
branch,
|
|
760
576
|
reports,
|
|
761
577
|
merged,
|
|
762
578
|
{ classified: classification.watchdogs },
|
|
763
579
|
context,
|
|
764
580
|
jev.available ? jev.candidates : undefined,
|
|
765
|
-
nameOf,
|
|
766
581
|
domains,
|
|
767
582
|
);
|
|
768
|
-
|
|
769
|
-
|
|
583
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
584
|
+
name: REVIEW_FILE,
|
|
585
|
+
kind: "review",
|
|
586
|
+
content: `${incrementalNote(classification)}${markdown}\n`,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
590
|
+
messages.push(
|
|
591
|
+
`Triagem: ${location} — ${Object.keys(state.findings).length} finding(s).`,
|
|
592
|
+
);
|
|
770
593
|
|
|
771
594
|
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
772
595
|
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
773
|
-
|
|
774
|
-
`Relatório: ${
|
|
596
|
+
messages.push(
|
|
597
|
+
`Relatório: ${location} — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
775
598
|
);
|
|
776
599
|
|
|
777
|
-
|
|
600
|
+
// O staging já cumpriu o papel (briefs/findings/contexto estão no storage).
|
|
601
|
+
ctx.work.clean();
|
|
602
|
+
|
|
603
|
+
return { reportPath: location, blockers, attention, messages };
|
|
778
604
|
}
|
|
779
605
|
|
|
780
606
|
/** Resolve a branch (opção explícita ou a atual do git). */
|