@payconductor/houndsman 0.2.0 → 0.3.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/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 +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -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 +22 -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 +43 -0
- package/src/cli/index.ts +58 -266
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +34 -144
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- 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 +9 -19
- 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 +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +170 -38
- 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/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -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 +112 -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 +108 -0
- package/src/core/run-status.ts +6 -11
- 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 +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/index.ts +64 -234
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -493
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/infrastructure.test.ts +210 -0
package/src/core/orchestrator.ts
CHANGED
|
@@ -15,29 +15,21 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { join
|
|
19
|
-
import { renderBrief } from "./brief";
|
|
18
|
+
import { join } from "node:path";
|
|
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, FindingsStateStore, fileReader } from "./findings-state";
|
|
32
|
+
import { resolveSha } from "./git-revision";
|
|
41
33
|
import {
|
|
42
34
|
type JevCandidate,
|
|
43
35
|
type JevCandidatesFile,
|
|
@@ -45,21 +37,24 @@ import {
|
|
|
45
37
|
JevClient,
|
|
46
38
|
JevDetector,
|
|
47
39
|
} from "./jev";
|
|
48
|
-
import { ARTIFACT_DIR, OUTPUT_FORMAT_PATH } from "./paths";
|
|
49
|
-
import {
|
|
40
|
+
import { ARTIFACT_DIR, OUTPUT_FORMAT_PATH, relFromRoot } from "./paths";
|
|
41
|
+
import { currentBranch, readContextSummary } from "./pr-context";
|
|
42
|
+
import { ProjectConfig } from "./project-config";
|
|
43
|
+
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
44
|
+
import { ManifestWriter } from "./renderers/manifest-writer";
|
|
45
|
+
import { ReportRenderer } from "./renderers/report-renderer";
|
|
50
46
|
import {
|
|
51
47
|
type ContextSummary,
|
|
52
48
|
type DomainReport,
|
|
53
49
|
dedupeFindings,
|
|
54
|
-
type MergedFinding,
|
|
55
50
|
parseWatchdogReport,
|
|
56
|
-
renderDomainReport,
|
|
57
|
-
renderReport,
|
|
58
51
|
sanitizeBranch,
|
|
59
52
|
type WatchdogReport,
|
|
60
53
|
} from "./report-builder";
|
|
54
|
+
import { RunLayout } from "./run-layout";
|
|
61
55
|
import { writeRunStatus } from "./run-status";
|
|
62
|
-
import {
|
|
56
|
+
import { systemClock } from "./services/clock";
|
|
57
|
+
import type { WatchdogMeta } from "./types";
|
|
63
58
|
import { findWatchdog } from "./watchdog-registry";
|
|
64
59
|
|
|
65
60
|
export type ReviewOptions = {
|
|
@@ -89,27 +84,11 @@ export function defaultOptions(artifactDir: string = ARTIFACT_DIR): ReviewOption
|
|
|
89
84
|
};
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
return relative(process.cwd(), absPath).replace(/\\/g, "/");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
type Paths = {
|
|
98
|
-
slug: string;
|
|
99
|
-
outDir: string;
|
|
100
|
-
briefsDir: string;
|
|
101
|
-
findingsDir: string;
|
|
102
|
-
};
|
|
87
|
+
/** Layout de uma run (paths de briefs/findings/artefatos). */
|
|
88
|
+
type Paths = RunLayout;
|
|
103
89
|
|
|
104
90
|
function paths(opts: ReviewOptions, branch: string): Paths {
|
|
105
|
-
|
|
106
|
-
const outDir = join(opts.dir, slug);
|
|
107
|
-
return {
|
|
108
|
-
slug,
|
|
109
|
-
outDir,
|
|
110
|
-
briefsDir: join(outDir, "briefs"),
|
|
111
|
-
findingsDir: join(outDir, "findings"),
|
|
112
|
-
};
|
|
91
|
+
return new RunLayout(opts.dir, branch);
|
|
113
92
|
}
|
|
114
93
|
|
|
115
94
|
function prepareDirs(dirs: Paths, clean: boolean): void {
|
|
@@ -125,28 +104,11 @@ function prepareDirs(dirs: Paths, clean: boolean): void {
|
|
|
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,105 +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
123
|
// ---------------------------------------------------------------------------
|
|
261
124
|
// Findings
|
|
262
125
|
// ---------------------------------------------------------------------------
|
|
@@ -264,9 +127,10 @@ function resolveReviewScope(
|
|
|
264
127
|
function readFindings(
|
|
265
128
|
findingsDir: string,
|
|
266
129
|
activeIds: string[],
|
|
267
|
-
): { reports: WatchdogReport[]; pending: string[] } {
|
|
130
|
+
): { reports: WatchdogReport[]; pending: string[]; warnings: string[] } {
|
|
268
131
|
const reports: WatchdogReport[] = [];
|
|
269
132
|
const pending: string[] = [];
|
|
133
|
+
const warnings: string[] = [];
|
|
270
134
|
for (const id of activeIds) {
|
|
271
135
|
const path = join(findingsDir, `${id}.json`);
|
|
272
136
|
if (!existsSync(path)) {
|
|
@@ -275,129 +139,52 @@ function readFindings(
|
|
|
275
139
|
}
|
|
276
140
|
try {
|
|
277
141
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
278
|
-
const report = parseWatchdogReport(raw, path)
|
|
142
|
+
const report = parseWatchdogReport(raw, path, (message) =>
|
|
143
|
+
warnings.push(message),
|
|
144
|
+
);
|
|
279
145
|
if (report) reports.push(report);
|
|
280
146
|
else pending.push(id);
|
|
281
147
|
} catch (err) {
|
|
282
|
-
|
|
148
|
+
warnings.push(
|
|
283
149
|
`[houndsman] ${relFromRoot(path)} inválido: ${err instanceof Error ? err.message : err}`,
|
|
284
150
|
);
|
|
285
151
|
pending.push(id);
|
|
286
152
|
}
|
|
287
153
|
}
|
|
288
|
-
return { reports, pending };
|
|
154
|
+
return { reports, pending, warnings };
|
|
289
155
|
}
|
|
290
156
|
|
|
291
157
|
// ---------------------------------------------------------------------------
|
|
292
|
-
//
|
|
158
|
+
// Orçamento (triage de PRs grandes)
|
|
293
159
|
// ---------------------------------------------------------------------------
|
|
294
160
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return
|
|
161
|
+
/** Aplica o orçamento aos arquivos de um watchdog. */
|
|
162
|
+
function budgetFor(
|
|
163
|
+
classification: ClassificationResult,
|
|
164
|
+
id: string,
|
|
165
|
+
maxFiles: number,
|
|
166
|
+
): BudgetSelection {
|
|
167
|
+
return Classification.from(classification).budgetFor(id, maxFiles);
|
|
302
168
|
}
|
|
303
169
|
|
|
170
|
+
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
304
171
|
function writeManifest(
|
|
305
|
-
|
|
172
|
+
layout: RunLayout,
|
|
173
|
+
config: HoundsmanConfig,
|
|
306
174
|
opts: ReviewOptions,
|
|
307
|
-
branch: string,
|
|
308
175
|
classification: RunClassification,
|
|
309
176
|
activeIds: string[],
|
|
310
177
|
pending: string[],
|
|
311
178
|
jev: JevCandidatesFile,
|
|
312
|
-
config: HoundsmanConfig,
|
|
313
179
|
): void {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
371
|
-
// Orçamento (triage de PRs grandes)
|
|
372
|
-
// ---------------------------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
/** Aplica o orçamento aos arquivos de um watchdog. */
|
|
375
|
-
function budgetFor(
|
|
376
|
-
classification: ClassificationResult,
|
|
377
|
-
id: string,
|
|
378
|
-
maxFiles: number,
|
|
379
|
-
): BudgetSelection {
|
|
380
|
-
return selectByBudget(
|
|
381
|
-
classification.watchdogs[id] ?? [],
|
|
382
|
-
classification.churn,
|
|
383
|
-
maxFiles,
|
|
180
|
+
new ManifestWriter(layout, new ProjectConfig(config), opts).write(
|
|
181
|
+
classification,
|
|
182
|
+
activeIds,
|
|
183
|
+
pending,
|
|
184
|
+
jev,
|
|
384
185
|
);
|
|
385
186
|
}
|
|
386
187
|
|
|
387
|
-
/** Totais de churn do diff (métricas do manifesto). */
|
|
388
|
-
function churnTotals(classification: ClassificationResult): {
|
|
389
|
-
additions: number;
|
|
390
|
-
deletions: number;
|
|
391
|
-
} {
|
|
392
|
-
let additions = 0;
|
|
393
|
-
let deletions = 0;
|
|
394
|
-
for (const entry of Object.values(classification.churn ?? {})) {
|
|
395
|
-
additions += entry.additions;
|
|
396
|
-
deletions += entry.deletions;
|
|
397
|
-
}
|
|
398
|
-
return { additions, deletions };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
188
|
// ---------------------------------------------------------------------------
|
|
402
189
|
// Briefs
|
|
403
190
|
// ---------------------------------------------------------------------------
|
|
@@ -413,11 +200,12 @@ function writeBriefs(
|
|
|
413
200
|
): void {
|
|
414
201
|
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
415
202
|
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
203
|
+
const renderer = new BriefRenderer();
|
|
416
204
|
for (const id of activeIds) {
|
|
417
205
|
const def = findWatchdog(config, id);
|
|
418
206
|
if (!def) continue;
|
|
419
207
|
const { selected, overflow } = budgetFor(classification, id, maxFilesPerBrief);
|
|
420
|
-
const brief =
|
|
208
|
+
const brief = renderer.render(new Watchdog(def), {
|
|
421
209
|
watchdogId: id,
|
|
422
210
|
files: selected,
|
|
423
211
|
statuses: classification.files,
|
|
@@ -425,13 +213,13 @@ function writeBriefs(
|
|
|
425
213
|
overflow,
|
|
426
214
|
deleted: classification.deleted,
|
|
427
215
|
branch,
|
|
428
|
-
outputPath: relFromRoot(
|
|
216
|
+
outputPath: relFromRoot(dirs.findingFile(id)),
|
|
429
217
|
outputFormatPath: OUTPUT_FORMAT_PATH,
|
|
430
218
|
docs,
|
|
431
219
|
context,
|
|
432
220
|
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
433
221
|
});
|
|
434
|
-
writeFileSync(
|
|
222
|
+
writeFileSync(dirs.briefFile(id), `${brief}\n`);
|
|
435
223
|
}
|
|
436
224
|
}
|
|
437
225
|
|
|
@@ -450,10 +238,7 @@ async function resolveCandidates(
|
|
|
450
238
|
config: HoundsmanConfig,
|
|
451
239
|
diff: DiffSource,
|
|
452
240
|
): Promise<JevCandidatesFile> {
|
|
453
|
-
const store = new JevCandidatesStore(
|
|
454
|
-
join(dirs.outDir, "candidates.json"),
|
|
455
|
-
config.watchdogIds,
|
|
456
|
-
);
|
|
241
|
+
const store = new JevCandidatesStore(dirs.candidatesFile, config.watchdogIds);
|
|
457
242
|
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
458
243
|
store.write(data);
|
|
459
244
|
return data;
|
|
@@ -494,55 +279,57 @@ async function resolveCandidates(
|
|
|
494
279
|
}
|
|
495
280
|
}
|
|
496
281
|
|
|
497
|
-
function
|
|
498
|
-
if (!jev.available) {
|
|
499
|
-
console.log(`Jev: pulado (${jev.reason}).`);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
282
|
+
function jevSummary(jev: JevCandidatesFile): string[] {
|
|
283
|
+
if (!jev.available) return [`Jev: pulado (${jev.reason}).`];
|
|
502
284
|
const skipped = jev.skipped?.length ?? 0;
|
|
503
|
-
|
|
285
|
+
return [
|
|
504
286
|
`Jev: ${jev.candidates.length} candidato(s) em ${jev.analyzedFiles ?? 0} arquivo(s)` +
|
|
505
287
|
(skipped > 0 ? `, ${skipped} com falha.` : "."),
|
|
506
|
-
|
|
288
|
+
];
|
|
507
289
|
}
|
|
508
290
|
|
|
509
|
-
function
|
|
291
|
+
function discoveryMessages(
|
|
510
292
|
activeIds: string[],
|
|
511
293
|
classification: ClassificationResult,
|
|
512
294
|
briefsDir: string,
|
|
513
|
-
):
|
|
514
|
-
|
|
295
|
+
): string[] {
|
|
296
|
+
const messages = [
|
|
515
297
|
`Discovery: ${activeIds.length} watchdog(s) ativado(s), ${Object.keys(classification.files).length} arquivo(s), ${classification.deleted.length} deletado(s).`,
|
|
516
|
-
|
|
517
|
-
|
|
298
|
+
`Briefs: ${relFromRoot(briefsDir)}/`,
|
|
299
|
+
];
|
|
518
300
|
for (const file of classification.deleted) {
|
|
519
|
-
|
|
301
|
+
messages.push(`Deletado (breaking change): ${file}`);
|
|
520
302
|
}
|
|
303
|
+
return messages;
|
|
521
304
|
}
|
|
522
305
|
|
|
523
306
|
/** Resumo do orçamento — só quando algum arquivo ficou de fora (PR grande). */
|
|
524
|
-
function
|
|
307
|
+
function budgetMessage(
|
|
525
308
|
classification: ClassificationResult,
|
|
526
309
|
config: HoundsmanConfig,
|
|
527
|
-
):
|
|
310
|
+
): string[] {
|
|
528
311
|
const maxFiles = resolveMaxFilesPerBrief(config);
|
|
529
312
|
let overflow = 0;
|
|
530
313
|
for (const id of Object.keys(classification.watchdogs)) {
|
|
531
314
|
overflow += budgetFor(classification, id, maxFiles).overflow.length;
|
|
532
315
|
}
|
|
533
|
-
if (overflow === 0) return;
|
|
534
|
-
|
|
316
|
+
if (overflow === 0) return [];
|
|
317
|
+
return [
|
|
535
318
|
`Orçamento: ${maxFiles} arquivo(s)/brief — ${overflow} fora do orçamento (menor churn; registrar em skipped).`,
|
|
536
|
-
|
|
319
|
+
];
|
|
537
320
|
}
|
|
538
321
|
|
|
322
|
+
/** Resultado do prepare — mensagens para o CLI imprimir. */
|
|
323
|
+
export type PrepareResult = { messages: string[] };
|
|
324
|
+
|
|
539
325
|
/** Passo 1 — descobre os arquivos e grava briefs + manifesto. */
|
|
540
326
|
export async function prepare(
|
|
541
327
|
opts: ReviewOptions,
|
|
542
328
|
branch: string,
|
|
543
329
|
config: HoundsmanConfig,
|
|
544
330
|
dirs: Paths = paths(opts, branch),
|
|
545
|
-
): Promise<
|
|
331
|
+
): Promise<PrepareResult> {
|
|
332
|
+
const messages: string[] = [];
|
|
546
333
|
prepareDirs(dirs, opts.clean);
|
|
547
334
|
|
|
548
335
|
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
@@ -551,9 +338,12 @@ export async function prepare(
|
|
|
551
338
|
// Marca o commit revisado e decide entre review completo e incremental.
|
|
552
339
|
const headSha = resolveSha(opts.head);
|
|
553
340
|
const previous = FindingsStateStore.at(opts.dir, branch).read();
|
|
554
|
-
const { classification, diff } =
|
|
341
|
+
const { classification, diff } = new ReviewScopeResolver(opts, config).resolve(
|
|
342
|
+
previous,
|
|
343
|
+
headSha,
|
|
344
|
+
);
|
|
555
345
|
writeFileSync(
|
|
556
|
-
|
|
346
|
+
dirs.classificationFile,
|
|
557
347
|
`${JSON.stringify(classification, null, 2)}\n`,
|
|
558
348
|
);
|
|
559
349
|
|
|
@@ -562,53 +352,51 @@ export async function prepare(
|
|
|
562
352
|
const activeIds = Object.keys(classification.watchdogs);
|
|
563
353
|
if (Object.keys(classification.files).length === 0) {
|
|
564
354
|
writeRunStatus(dirs.outDir, { branch, status: "review_done" });
|
|
565
|
-
|
|
355
|
+
messages.push(
|
|
566
356
|
classification.mode === "incremental"
|
|
567
357
|
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
568
358
|
: `Nenhuma alteração entre ${opts.base}...${opts.head}.`,
|
|
569
359
|
);
|
|
570
|
-
return;
|
|
360
|
+
return { messages };
|
|
571
361
|
}
|
|
572
362
|
if (activeIds.length === 0) {
|
|
573
363
|
writeRunStatus(dirs.outDir, { branch, status: "review_done" });
|
|
574
|
-
|
|
575
|
-
|
|
364
|
+
messages.push(
|
|
365
|
+
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
366
|
+
);
|
|
367
|
+
return { messages };
|
|
576
368
|
}
|
|
577
369
|
if (classification.mode === "incremental") {
|
|
578
|
-
|
|
370
|
+
messages.push(
|
|
579
371
|
`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
372
|
);
|
|
581
373
|
}
|
|
582
374
|
|
|
583
|
-
const context = readContextSummary(contextPaths
|
|
375
|
+
const context = readContextSummary(dirs.contextPaths.summary);
|
|
584
376
|
|
|
585
377
|
writeBriefs(activeIds, classification, dirs, branch, context, jev.candidates, config);
|
|
586
|
-
writeManifest(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
classification,
|
|
591
|
-
|
|
592
|
-
activeIds,
|
|
593
|
-
jev,
|
|
594
|
-
config,
|
|
378
|
+
writeManifest(dirs, config, opts, classification, activeIds, activeIds, jev);
|
|
379
|
+
|
|
380
|
+
messages.push(
|
|
381
|
+
...discoveryMessages(activeIds, classification, dirs.briefsDir),
|
|
382
|
+
...budgetMessage(classification, config),
|
|
383
|
+
...jevSummary(jev),
|
|
595
384
|
);
|
|
596
|
-
printDiscovery(activeIds, classification, dirs.briefsDir);
|
|
597
|
-
printBudget(classification, config);
|
|
598
|
-
printJevSummary(jev);
|
|
599
385
|
|
|
600
386
|
if (context) {
|
|
601
|
-
|
|
387
|
+
messages.push("Contexto da PR resumido e injetado nos briefs.");
|
|
602
388
|
} else {
|
|
603
|
-
|
|
389
|
+
messages.push(
|
|
604
390
|
"Sem contexto da PR (rode `bun run houndsman context` antes, se houver PR).",
|
|
605
391
|
);
|
|
606
392
|
}
|
|
607
393
|
|
|
608
|
-
|
|
394
|
+
messages.push(
|
|
609
395
|
`\nPróximo passo: despachar os sub-agentes (um por brief em ${relFromRoot(dirs.briefsDir)}/).`,
|
|
396
|
+
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
610
397
|
);
|
|
611
|
-
|
|
398
|
+
|
|
399
|
+
return { messages };
|
|
612
400
|
}
|
|
613
401
|
|
|
614
402
|
// ---------------------------------------------------------------------------
|
|
@@ -633,7 +421,7 @@ function readClassification(classificationPath: string): RunClassification {
|
|
|
633
421
|
* Mantém o `review.md` consolidado navegável em PRs grandes (centenas de achados).
|
|
634
422
|
*/
|
|
635
423
|
function writeDomainReports(
|
|
636
|
-
|
|
424
|
+
layout: RunLayout,
|
|
637
425
|
branch: string,
|
|
638
426
|
reports: WatchdogReport[],
|
|
639
427
|
classification: ClassificationResult,
|
|
@@ -641,20 +429,20 @@ function writeDomainReports(
|
|
|
641
429
|
nameOf: (id: string) => string,
|
|
642
430
|
): DomainReport[] {
|
|
643
431
|
const domains: DomainReport[] = [];
|
|
432
|
+
const renderer = new ReportRenderer(nameOf);
|
|
644
433
|
for (const report of reports) {
|
|
645
434
|
const findings = dedupeFindings([report]);
|
|
646
435
|
if (findings.length === 0) continue;
|
|
647
436
|
const file = `review.${sanitizeBranch(report.watchdogId)}.md`;
|
|
648
437
|
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
649
438
|
writeFileSync(
|
|
650
|
-
|
|
651
|
-
`${
|
|
439
|
+
layout.file(file),
|
|
440
|
+
`${renderer.renderDomain(
|
|
652
441
|
branch,
|
|
653
442
|
report,
|
|
654
443
|
findings,
|
|
655
444
|
expected.length,
|
|
656
445
|
context,
|
|
657
|
-
nameOf,
|
|
658
446
|
)}\n`,
|
|
659
447
|
);
|
|
660
448
|
domains.push({
|
|
@@ -673,6 +461,7 @@ export type FinalizeResult = {
|
|
|
673
461
|
reportPath: string;
|
|
674
462
|
blockers: number;
|
|
675
463
|
attention: number;
|
|
464
|
+
messages: string[];
|
|
676
465
|
};
|
|
677
466
|
|
|
678
467
|
/**
|
|
@@ -685,25 +474,16 @@ export function finalize(
|
|
|
685
474
|
config: HoundsmanConfig,
|
|
686
475
|
dirs: Paths = paths(opts, branch),
|
|
687
476
|
): FinalizeResult {
|
|
688
|
-
const classification = readClassification(
|
|
477
|
+
const classification = readClassification(dirs.classificationFile);
|
|
689
478
|
const activeIds = Object.keys(classification.watchdogs);
|
|
690
479
|
|
|
691
|
-
const
|
|
480
|
+
const messages: string[] = [];
|
|
481
|
+
const { reports, pending, warnings } = readFindings(dirs.findingsDir, activeIds);
|
|
482
|
+
messages.push(...warnings);
|
|
692
483
|
const jev =
|
|
693
|
-
new JevCandidatesStore(
|
|
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
|
-
);
|
|
484
|
+
new JevCandidatesStore(dirs.candidatesFile, config.watchdogIds).read() ??
|
|
485
|
+
JevCandidatesStore.unavailable("candidates.json ausente");
|
|
486
|
+
writeManifest(dirs, config, opts, classification, activeIds, pending, jev);
|
|
707
487
|
|
|
708
488
|
if (pending.length > 0) {
|
|
709
489
|
throw new Error(
|
|
@@ -712,14 +492,14 @@ export function finalize(
|
|
|
712
492
|
);
|
|
713
493
|
}
|
|
714
494
|
|
|
715
|
-
const now =
|
|
495
|
+
const now = systemClock();
|
|
716
496
|
// Findings re-reportados neste ciclo + carry-over (arquivos inalterados).
|
|
717
497
|
const merged = mergeCarryover(
|
|
718
498
|
dedupeFindings(reports),
|
|
719
499
|
classification.carryover ?? [],
|
|
720
500
|
);
|
|
721
501
|
if (classification.mode === "incremental") {
|
|
722
|
-
|
|
502
|
+
messages.push(
|
|
723
503
|
`Carry-over: ${classification.carryover?.length ?? 0} finding(s) preservado(s) de ${shortSha(classification.reviewedFrom ?? "")}.`,
|
|
724
504
|
);
|
|
725
505
|
}
|
|
@@ -740,41 +520,40 @@ export function finalize(
|
|
|
740
520
|
: undefined,
|
|
741
521
|
});
|
|
742
522
|
store.write(state);
|
|
743
|
-
|
|
523
|
+
messages.push(
|
|
744
524
|
`Triagem: ${relFromRoot(store.filePath)} — ${Object.keys(state.findings).length} finding(s).`,
|
|
745
525
|
);
|
|
746
526
|
writeRunStatus(dirs.outDir, { branch, status: "review_done" });
|
|
747
527
|
|
|
748
|
-
const context = readContextSummary(contextPaths
|
|
528
|
+
const context = readContextSummary(dirs.contextPaths.summary);
|
|
749
529
|
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
750
530
|
const domains = writeDomainReports(
|
|
751
|
-
dirs
|
|
531
|
+
dirs,
|
|
752
532
|
branch,
|
|
753
533
|
reports,
|
|
754
534
|
classification,
|
|
755
535
|
context,
|
|
756
536
|
nameOf,
|
|
757
537
|
);
|
|
758
|
-
const markdown =
|
|
538
|
+
const markdown = new ReportRenderer(nameOf).render(
|
|
759
539
|
branch,
|
|
760
540
|
reports,
|
|
761
541
|
merged,
|
|
762
542
|
{ classified: classification.watchdogs },
|
|
763
543
|
context,
|
|
764
544
|
jev.available ? jev.candidates : undefined,
|
|
765
|
-
nameOf,
|
|
766
545
|
domains,
|
|
767
546
|
);
|
|
768
|
-
const reportPath =
|
|
547
|
+
const reportPath = dirs.reviewFile;
|
|
769
548
|
writeFileSync(reportPath, `${incrementalNote(classification)}${markdown}\n`);
|
|
770
549
|
|
|
771
550
|
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
772
551
|
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
773
|
-
|
|
552
|
+
messages.push(
|
|
774
553
|
`Relatório: ${relFromRoot(reportPath)} — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
775
554
|
);
|
|
776
555
|
|
|
777
|
-
return { reportPath, blockers, attention };
|
|
556
|
+
return { reportPath, blockers, attention, messages };
|
|
778
557
|
}
|
|
779
558
|
|
|
780
559
|
/** Resolve a branch (opção explícita ou a atual do git). */
|