@payconductor/houndsman 0.12.0 → 0.14.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 +23 -2
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +14 -1
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/types.ts +2 -0
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +38 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +57 -1
- package/src/core/defaults.ts +63 -2
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +46 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +17 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +78 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
package/src/core/orchestrator.ts
CHANGED
|
@@ -1,1202 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Fachada da orquestração do houndsman (biblioteca).
|
|
3
|
+
*
|
|
4
|
+
* O trabalho vive nas pipelines (`PreparePipeline`/`FinalizePipeline`), que
|
|
5
|
+
* recebem o `ReviewContext` no construtor. Aqui ficam apenas os tipos, os
|
|
6
|
+
* defaults e as fachadas usadas pelo `ReviewSession` e pelos testes.
|
|
4
7
|
*
|
|
5
8
|
* Genérico: recebe o `HoundsmanConfig` por parâmetro (injeção explícita).
|
|
6
9
|
* Entrypoints: `cli/index.ts` (`houndsman`). Aqui não há `process.argv`.
|
|
7
|
-
*
|
|
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.
|
|
17
10
|
*/
|
|
18
11
|
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import type { AiClient } from "./ai/ai-client";
|
|
22
|
-
import { AiBriefEnricher } from "./ai/features/brief-enricher";
|
|
23
|
-
import { AiClassifier } from "./ai/features/classifier";
|
|
24
|
-
import { AiContextSummarizer } from "./ai/features/context-summarizer";
|
|
25
|
-
import { AiPreAnalyzer } from "./ai/features/pre-analyzer";
|
|
26
|
-
import { AiTechnicalSummarizer } from "./ai/features/technical-summarizer";
|
|
27
|
-
import type { BriefPlan } from "./ai/schemas";
|
|
28
|
-
import { assetPath, OUTPUT_FORMAT_FILE } from "./assets";
|
|
29
|
-
import type { AstService } from "./ast/service";
|
|
30
|
-
import type { AstAnalysis, AstFileBrief, AstImpact } from "./ast/types";
|
|
31
|
-
import { summarizeScope } from "./changed-scope";
|
|
32
|
-
import {
|
|
33
|
-
type HoundsmanConfig,
|
|
34
|
-
resolveJevConcurrency,
|
|
35
|
-
resolveMaxFilesPerBrief,
|
|
36
|
-
} from "./config";
|
|
37
|
-
import type { DiffSource } from "./diff-source";
|
|
38
|
-
import { discoverDocs } from "./doc-discoverer";
|
|
39
|
-
import { Classification } from "./domain/classification";
|
|
40
|
-
import { Watchdog } from "./domain/watchdog";
|
|
41
|
-
import { mergeCarryover } from "./engine/carryover";
|
|
42
|
-
import { ReviewScopeResolver, type RunClassification } from "./engine/review-scope";
|
|
43
|
-
import { ENV_NAMES } from "./env";
|
|
44
|
-
import type { BudgetSelection, ClassificationResult } from "./file-classifier";
|
|
45
|
-
import { buildStateFromFindings, fileReader } from "./findings-state";
|
|
46
|
-
import { syncResolvedThreads } from "./github-review";
|
|
47
|
-
import {
|
|
48
|
-
type JevCandidate,
|
|
49
|
-
type JevCandidatesFile,
|
|
50
|
-
JevCandidatesStore,
|
|
51
|
-
JevClient,
|
|
52
|
-
JevDetector,
|
|
53
|
-
parseCandidates,
|
|
54
|
-
serializeCandidates,
|
|
55
|
-
} from "./jev";
|
|
56
|
-
import {
|
|
57
|
-
displayPath,
|
|
58
|
-
OUTPUT_FORMAT_PATH,
|
|
59
|
-
REVIEW_FILE,
|
|
60
|
-
REVIEW_RESUME_FILE,
|
|
61
|
-
TECHNICAL_SUMMARY_FILE,
|
|
62
|
-
} from "./paths";
|
|
63
|
-
import { ProjectConfig } from "./project-config";
|
|
64
|
-
import { BriefRenderer } from "./renderers/brief-renderer";
|
|
12
|
+
import type { HoundsmanConfig } from "./config";
|
|
13
|
+
import { FinalizePipeline, type FinalizeResult } from "./engine/finalize-pipeline";
|
|
65
14
|
import {
|
|
66
|
-
type
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} from "./
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
73
|
-
type ContextSummary,
|
|
74
|
-
type DomainReport,
|
|
75
|
-
dedupeFindings,
|
|
76
|
-
parseContextSummary,
|
|
77
|
-
sanitizeBranch,
|
|
78
|
-
type WatchdogReport,
|
|
79
|
-
} from "./report-builder";
|
|
80
|
-
import { writeRunStatus } from "./run-status";
|
|
81
|
-
import { systemClock } from "./services/clock";
|
|
15
|
+
type PrepareDeps,
|
|
16
|
+
PreparePipeline,
|
|
17
|
+
type PrepareResult,
|
|
18
|
+
} from "./engine/prepare-pipeline";
|
|
19
|
+
import type { ReviewContext } from "./engine/review-context";
|
|
20
|
+
import type { ReviewOptions } from "./engine/review-options";
|
|
82
21
|
import { GitRepository } from "./services/git-repository";
|
|
83
|
-
import type { ReviewRecord, StorageProvider } from "./storage/types";
|
|
84
|
-
import { ReviewWorkArea } from "./storage/work-area";
|
|
85
|
-
import {
|
|
86
|
-
buildTechnicalSummary,
|
|
87
|
-
parseTechnicalSummary,
|
|
88
|
-
type TechnicalSummary,
|
|
89
|
-
} from "./technical-summary";
|
|
90
|
-
import type { WatchdogMeta } from "./types";
|
|
91
|
-
import { findWatchdog } from "./watchdog-registry";
|
|
92
|
-
|
|
93
|
-
export type ReviewOptions = {
|
|
94
|
-
base: string;
|
|
95
|
-
head: string;
|
|
96
|
-
branch?: string;
|
|
97
|
-
finalize: boolean;
|
|
98
|
-
failOnBlocking: boolean;
|
|
99
|
-
clean: boolean;
|
|
100
|
-
noJev: boolean;
|
|
101
|
-
/** Desativa a análise estrutural (AST) nesta run. */
|
|
102
|
-
noAst: boolean;
|
|
103
|
-
/** Força review completo (ignora o modo incremental). */
|
|
104
|
-
full: boolean;
|
|
105
|
-
/** Desativa as features de IA do fluxo normal. */
|
|
106
|
-
noAi: boolean;
|
|
107
|
-
/** PR de origem (resolvida via `--pr`). */
|
|
108
|
-
pr?: number;
|
|
109
|
-
/** Worktree informado via flag (`--worktree`). */
|
|
110
|
-
worktree?: string;
|
|
111
|
-
/** Worktree resolvido (absoluto; ausente = clone principal). */
|
|
112
|
-
worktreePath?: string;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** Valores default das opções (o CLI sobrepõe com as flags). */
|
|
116
|
-
export function defaultOptions(): ReviewOptions {
|
|
117
|
-
return {
|
|
118
|
-
base: "main",
|
|
119
|
-
head: "HEAD",
|
|
120
|
-
finalize: false,
|
|
121
|
-
failOnBlocking: false,
|
|
122
|
-
clean: false,
|
|
123
|
-
noJev: false,
|
|
124
|
-
noAst: false,
|
|
125
|
-
full: false,
|
|
126
|
-
noAi: false,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Workspace onde os arquivos da review são lidos. */
|
|
131
|
-
export type ReviewWorkspaceContext = {
|
|
132
|
-
/** Raiz operacional (worktree ou clone principal). */
|
|
133
|
-
root?: string;
|
|
134
|
-
/** Raiz do clone principal (identidade/config). */
|
|
135
|
-
mainRoot?: string;
|
|
136
|
-
git?: GitRepository;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/** Contexto de uma run: storage + review + staging + raiz operacional. */
|
|
140
|
-
export type ReviewContext = {
|
|
141
|
-
storage: StorageProvider;
|
|
142
|
-
review: ReviewRecord;
|
|
143
|
-
work: ReviewWorkArea;
|
|
144
|
-
root: string;
|
|
145
|
-
mainRoot: string;
|
|
146
|
-
git: GitRepository;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/** Cria o contexto de uma review existente. */
|
|
150
|
-
export function reviewContext(
|
|
151
|
-
storage: StorageProvider,
|
|
152
|
-
review: ReviewRecord,
|
|
153
|
-
workspace: ReviewWorkspaceContext = {},
|
|
154
|
-
): ReviewContext {
|
|
155
|
-
const root = workspace.root ?? process.cwd();
|
|
156
|
-
return {
|
|
157
|
-
storage,
|
|
158
|
-
review,
|
|
159
|
-
work: new ReviewWorkArea(review),
|
|
160
|
-
root,
|
|
161
|
-
mainRoot: workspace.mainRoot ?? root,
|
|
162
|
-
git: workspace.git ?? new GitRepository(),
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Schema do output: relativo à raiz da review quando a camada existe; no
|
|
168
|
-
* worktree sem camada, cai para o asset embarcado (caminho absoluto legível).
|
|
169
|
-
*/
|
|
170
|
-
function outputFormatPathFor(ctx: ReviewContext): string {
|
|
171
|
-
if (existsSync(join(ctx.root, OUTPUT_FORMAT_PATH))) return OUTPUT_FORMAT_PATH;
|
|
172
|
-
return ctx.review.worktreePath ? assetPath(OUTPUT_FORMAT_FILE) : OUTPUT_FORMAT_PATH;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// Escopo da revisão (completo vs incremental)
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
/** SHA curto para logs (7 chars, como o git). */
|
|
180
|
-
function shortSha(sha: string): string {
|
|
181
|
-
return sha.slice(0, 7);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Aviso no topo do `review.md` quando a revisão foi incremental (senão vazio). */
|
|
185
|
-
function incrementalNote(classification: RunClassification): string {
|
|
186
|
-
if (classification.mode !== "incremental" || !classification.reviewedFrom) return "";
|
|
187
|
-
const carried = classification.carryover?.length ?? 0;
|
|
188
|
-
return (
|
|
189
|
-
`> **Revisão incremental** — desde \`${shortSha(classification.reviewedFrom)}\`; ` +
|
|
190
|
-
`${Object.keys(classification.files).length} arquivo(s) alterado(s), ` +
|
|
191
|
-
`${carried} finding(s) carregado(s) do review anterior.\n\n`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
// Orçamento (triage de PRs grandes)
|
|
197
|
-
// ---------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
/** Aplica o orçamento aos arquivos de um watchdog. */
|
|
200
|
-
function budgetFor(
|
|
201
|
-
classification: ClassificationResult,
|
|
202
|
-
id: string,
|
|
203
|
-
maxFiles: number,
|
|
204
|
-
): BudgetSelection {
|
|
205
|
-
return Classification.from(classification).budgetFor(id, maxFiles);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
209
|
-
function writeManifest(
|
|
210
|
-
ctx: ReviewContext,
|
|
211
|
-
config: HoundsmanConfig,
|
|
212
|
-
opts: ReviewOptions,
|
|
213
|
-
classification: RunClassification,
|
|
214
|
-
activeIds: string[],
|
|
215
|
-
pending: string[],
|
|
216
|
-
jev: JevCandidatesFile,
|
|
217
|
-
ast?: ManifestAst,
|
|
218
|
-
ai?: ManifestAi,
|
|
219
|
-
): void {
|
|
220
|
-
new ManifestWriter(
|
|
221
|
-
ctx.storage,
|
|
222
|
-
ctx.review,
|
|
223
|
-
ctx.work,
|
|
224
|
-
new ProjectConfig(config),
|
|
225
|
-
opts,
|
|
226
|
-
).write(classification, activeIds, pending, jev, ast, ai);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Briefs
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
function writeBriefs(
|
|
234
|
-
activeIds: string[],
|
|
235
|
-
classification: RunClassification,
|
|
236
|
-
ctx: ReviewContext,
|
|
237
|
-
branch: string,
|
|
238
|
-
context: ContextSummary | null,
|
|
239
|
-
candidates: JevCandidate[],
|
|
240
|
-
config: HoundsmanConfig,
|
|
241
|
-
astAnalyses?: ReadonlyMap<string, AstAnalysis>,
|
|
242
|
-
astImpact?: AstImpact,
|
|
243
|
-
aiPlans?: ReadonlyMap<string, BriefPlan>,
|
|
244
|
-
): void {
|
|
245
|
-
const work = ctx.work;
|
|
246
|
-
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
247
|
-
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
248
|
-
const renderer = new BriefRenderer();
|
|
249
|
-
for (const id of activeIds) {
|
|
250
|
-
const def = findWatchdog(config, id);
|
|
251
|
-
if (!def) continue;
|
|
252
|
-
const { selected, overflow } = budgetFor(classification, id, maxFilesPerBrief);
|
|
253
|
-
const astFiles: AstFileBrief[] = selected.flatMap((file) => {
|
|
254
|
-
const analysis = astAnalyses?.get(file);
|
|
255
|
-
if (!analysis?.delta) return [];
|
|
256
|
-
return [
|
|
257
|
-
{
|
|
258
|
-
file,
|
|
259
|
-
delta: analysis.delta,
|
|
260
|
-
importers: astImpact?.importers[file],
|
|
261
|
-
},
|
|
262
|
-
];
|
|
263
|
-
});
|
|
264
|
-
const brief = renderer.render(new Watchdog(def), {
|
|
265
|
-
watchdogId: id,
|
|
266
|
-
files: selected,
|
|
267
|
-
statuses: classification.files,
|
|
268
|
-
churn: classification.churn,
|
|
269
|
-
overflow,
|
|
270
|
-
deleted: classification.deleted,
|
|
271
|
-
branch,
|
|
272
|
-
outputPath: work.findingFile(id),
|
|
273
|
-
outputFormatPath: outputFormatPathFor(ctx),
|
|
274
|
-
worktree: ctx.review.worktreePath,
|
|
275
|
-
docs,
|
|
276
|
-
context,
|
|
277
|
-
candidates: candidates.filter((c) => c.watchdogId === id),
|
|
278
|
-
ast: astFiles,
|
|
279
|
-
aiPlan: aiPlans?.get(id),
|
|
280
|
-
changedScope: classification.changedScope,
|
|
281
|
-
});
|
|
282
|
-
const content = `${brief}\n`;
|
|
283
|
-
work.writeBrief(id, content);
|
|
284
|
-
// Auditoria: o brief também fica no storage (o staging é limpo no finalize).
|
|
285
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
286
|
-
name: `briefs/${id}.md`,
|
|
287
|
-
kind: "brief",
|
|
288
|
-
content,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
// Passo 1 — preparar
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Resolve os candidatos Jev (Passo 1.5). Sempre grava `candidates.json`.
|
|
299
|
-
* Falha-aberto: sem chave / erro / `--no-jev` → artefato `available:false`.
|
|
300
|
-
*/
|
|
301
|
-
async function resolveCandidates(
|
|
302
|
-
opts: ReviewOptions,
|
|
303
|
-
classification: ClassificationResult,
|
|
304
|
-
ctx: ReviewContext,
|
|
305
|
-
config: HoundsmanConfig,
|
|
306
|
-
diff: DiffSource,
|
|
307
|
-
ai?: AiClient,
|
|
308
|
-
): Promise<JevCandidatesFile> {
|
|
309
|
-
const save = (data: JevCandidatesFile): JevCandidatesFile => {
|
|
310
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
311
|
-
name: "candidates.json",
|
|
312
|
-
kind: "candidates",
|
|
313
|
-
content: serializeCandidates(data),
|
|
314
|
-
});
|
|
315
|
-
return data;
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Pré-análise por IA (opcional) tem precedência sobre o Jev.
|
|
319
|
-
if (ai && !opts.noAi) {
|
|
320
|
-
const analyzer = new AiPreAnalyzer(ai);
|
|
321
|
-
if (analyzer.enabled) {
|
|
322
|
-
const candidates: JevCandidate[] = [];
|
|
323
|
-
for (const [id, files] of Object.entries(classification.watchdogs)) {
|
|
324
|
-
const def = findWatchdog(config, id);
|
|
325
|
-
if (!def) continue;
|
|
326
|
-
const inputs = files.slice(0, 5).flatMap((file) => {
|
|
327
|
-
// Só o trecho alterado: candidato fora do diff vira ruído de código
|
|
328
|
-
// pré-existente no brief do watchdog.
|
|
329
|
-
const snippet = diff.byFile.get(file);
|
|
330
|
-
return snippet ? [{ file, snippet }] : [];
|
|
331
|
-
});
|
|
332
|
-
candidates.push(
|
|
333
|
-
...(await analyzer.analyze({
|
|
334
|
-
watchdogId: id,
|
|
335
|
-
name: def.name,
|
|
336
|
-
rules: def.rules,
|
|
337
|
-
files: inputs,
|
|
338
|
-
})),
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
return save({
|
|
342
|
-
available: true,
|
|
343
|
-
model: ai.modelId,
|
|
344
|
-
threshold: JevCandidatesStore.THRESHOLD,
|
|
345
|
-
analyzedFiles: Object.values(classification.watchdogs).flat().length,
|
|
346
|
-
candidates,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (opts.noJev) {
|
|
352
|
-
return save(JevCandidatesStore.unavailable("desativado (--no-jev)"));
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const client = new JevClient();
|
|
356
|
-
if (!client.isConfigured()) {
|
|
357
|
-
return save(
|
|
358
|
-
JevCandidatesStore.unavailable(`${ENV_NAMES.typeSafeApiKey} não configurada`),
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
const detector = new JevDetector(
|
|
364
|
-
client,
|
|
365
|
-
config.watchdogs as Record<string, WatchdogMeta>,
|
|
366
|
-
ctx.root,
|
|
367
|
-
);
|
|
368
|
-
const result = await detector.detect(classification, opts.base, opts.head, {
|
|
369
|
-
diff,
|
|
370
|
-
concurrency: resolveJevConcurrency(config),
|
|
371
|
-
});
|
|
372
|
-
return save({
|
|
373
|
-
available: true,
|
|
374
|
-
model: result.model,
|
|
375
|
-
threshold: JevCandidatesStore.THRESHOLD,
|
|
376
|
-
analyzedFiles: result.analyzedFiles,
|
|
377
|
-
candidates: result.candidates,
|
|
378
|
-
skipped: result.skipped.length > 0 ? result.skipped : undefined,
|
|
379
|
-
});
|
|
380
|
-
} catch (error) {
|
|
381
|
-
return save(
|
|
382
|
-
JevCandidatesStore.unavailable(
|
|
383
|
-
error instanceof Error ? error.message : String(error),
|
|
384
|
-
),
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function jevSummary(jev: JevCandidatesFile): string[] {
|
|
390
|
-
if (!jev.available) return [`Jev: pulado (${jev.reason}).`];
|
|
391
|
-
const skipped = jev.skipped?.length ?? 0;
|
|
392
|
-
return [
|
|
393
|
-
`Jev: ${jev.candidates.length} candidato(s) em ${jev.analyzedFiles ?? 0} arquivo(s)` +
|
|
394
|
-
(skipped > 0 ? `, ${skipped} com falha.` : "."),
|
|
395
|
-
];
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function discoveryMessages(
|
|
399
|
-
activeIds: string[],
|
|
400
|
-
classification: ClassificationResult,
|
|
401
|
-
briefsDir: string,
|
|
402
|
-
): string[] {
|
|
403
|
-
const messages = [
|
|
404
|
-
`Discovery: ${activeIds.length} watchdog(s) ativado(s), ${Object.keys(classification.files).length} arquivo(s), ${classification.deleted.length} deletado(s).`,
|
|
405
|
-
`Briefs: ${displayPath(briefsDir)}/`,
|
|
406
|
-
];
|
|
407
|
-
for (const file of classification.deleted) {
|
|
408
|
-
messages.push(`Deletado (breaking change): ${file}`);
|
|
409
|
-
}
|
|
410
|
-
return messages;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** Resumo do escopo — lembra que só as faixas alteradas são revisáveis. */
|
|
414
|
-
function changedScopeMessage(classification: RunClassification): string[] {
|
|
415
|
-
const scope = classification.changedScope;
|
|
416
|
-
if (!scope) return [];
|
|
417
|
-
const totals = summarizeScope(scope);
|
|
418
|
-
return [
|
|
419
|
-
`Escopo: ${totals.files} arquivo(s), ${totals.symbols} símbolo(s) em ${totals.ranges} faixa(s) alterada(s).`,
|
|
420
|
-
];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** Resumo do orçamento — só quando algum arquivo ficou de fora (PR grande). */
|
|
424
|
-
function budgetMessage(
|
|
425
|
-
classification: ClassificationResult,
|
|
426
|
-
config: HoundsmanConfig,
|
|
427
|
-
): string[] {
|
|
428
|
-
const maxFiles = resolveMaxFilesPerBrief(config);
|
|
429
|
-
let overflow = 0;
|
|
430
|
-
for (const id of Object.keys(classification.watchdogs)) {
|
|
431
|
-
overflow += budgetFor(classification, id, maxFiles).overflow.length;
|
|
432
|
-
}
|
|
433
|
-
if (overflow === 0) return [];
|
|
434
|
-
return [
|
|
435
|
-
`Orçamento: ${maxFiles} arquivo(s)/brief — ${overflow} fora do orçamento (menor churn; registrar em skipped).`,
|
|
436
|
-
];
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Resultado do prepare — mensagens para o CLI imprimir. */
|
|
440
|
-
export type PrepareResult = { messages: string[] };
|
|
441
22
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (!classifier.enabled) return;
|
|
455
|
-
const watchdogs = config.watchdogIds.flatMap((id) => {
|
|
456
|
-
const def = config.watchdogs[id];
|
|
457
|
-
return def ? [{ id, name: def.name, scope: def.scope }] : [];
|
|
458
|
-
});
|
|
459
|
-
const assignments = await classifier.classify(classification.unclassified, watchdogs);
|
|
460
|
-
if (assignments.size === 0) return;
|
|
461
|
-
for (const [file, id] of assignments) {
|
|
462
|
-
const list = classification.watchdogs[id] ?? [];
|
|
463
|
-
if (!list.includes(file)) list.push(file);
|
|
464
|
-
classification.watchdogs[id] = list;
|
|
465
|
-
classification.stats[id] = list.length;
|
|
466
|
-
}
|
|
467
|
-
classification.unclassified = classification.unclassified.filter(
|
|
468
|
-
(file) => !assignments.has(file),
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Resumo do contexto da PR: usa o do subagente ou, na ausência, a IA
|
|
474
|
-
* (`AiContextSummarizer`). Grava o artefato e o staging.
|
|
475
|
-
*/
|
|
476
|
-
async function resolveContextSummary(
|
|
477
|
-
ctx: ReviewContext,
|
|
478
|
-
ai: AiClient | undefined,
|
|
479
|
-
opts: ReviewOptions,
|
|
480
|
-
): Promise<ContextSummary | null> {
|
|
481
|
-
const existing = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
482
|
-
if (existing) return existing;
|
|
483
|
-
if (!ai || opts.noAi) return null;
|
|
484
|
-
const raw = ctx.work.readContextRaw();
|
|
485
|
-
if (!raw) return null;
|
|
486
|
-
const summary = await new AiContextSummarizer(ai).summarize(raw);
|
|
487
|
-
if (!summary) return null;
|
|
488
|
-
ctx.work.writeContextSummary(summary);
|
|
489
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
490
|
-
name: "context-summary.json",
|
|
491
|
-
kind: "context-summary",
|
|
492
|
-
content: `${JSON.stringify(summary, null, 2)}\n`,
|
|
493
|
-
});
|
|
494
|
-
return summary;
|
|
495
|
-
}
|
|
23
|
+
export type { FinalizeResult } from "./engine/finalize-pipeline";
|
|
24
|
+
export type {
|
|
25
|
+
PrepareDeps,
|
|
26
|
+
PrepareProgress,
|
|
27
|
+
PrepareProgressEvent,
|
|
28
|
+
PrepareResult,
|
|
29
|
+
PrepareStage,
|
|
30
|
+
} from "./engine/prepare-pipeline";
|
|
31
|
+
export type { ReviewContext, ReviewWorkspaceContext } from "./engine/review-context";
|
|
32
|
+
export { reviewContext } from "./engine/review-context";
|
|
33
|
+
export type { ReviewOptions } from "./engine/review-options";
|
|
34
|
+
export { defaultOptions } from "./engine/review-options";
|
|
496
35
|
|
|
497
|
-
/**
|
|
498
|
-
|
|
499
|
-
* determinístico a partir da classificação/AST. Persistido para auditoria —
|
|
500
|
-
* o `finalize` apenas o consome.
|
|
501
|
-
*/
|
|
502
|
-
async function resolveTechnicalSummary(
|
|
36
|
+
/** Passo 1 — descobre os arquivos e grava briefs + manifesto. */
|
|
37
|
+
export function prepare(
|
|
503
38
|
ctx: ReviewContext,
|
|
504
|
-
ai: AiClient | undefined,
|
|
505
|
-
opts: ReviewOptions,
|
|
506
|
-
input: {
|
|
507
|
-
branch: string;
|
|
508
|
-
classification: RunClassification;
|
|
509
|
-
context: ContextSummary | null;
|
|
510
|
-
ast?: ReadonlyMap<string, AstAnalysis>;
|
|
511
|
-
impact?: AstImpact;
|
|
512
|
-
domains: readonly string[];
|
|
513
|
-
},
|
|
514
|
-
): Promise<TechnicalSummary> {
|
|
515
|
-
const fallback = buildTechnicalSummary({
|
|
516
|
-
classification: input.classification,
|
|
517
|
-
ast: input.ast,
|
|
518
|
-
impact: input.impact,
|
|
519
|
-
headSha: input.classification.headSha,
|
|
520
|
-
});
|
|
521
|
-
const summary =
|
|
522
|
-
ai && !opts.noAi
|
|
523
|
-
? ((await new AiTechnicalSummarizer(ai).summarize({
|
|
524
|
-
branch: input.branch,
|
|
525
|
-
context: input.context,
|
|
526
|
-
classification: input.classification,
|
|
527
|
-
ast: input.ast,
|
|
528
|
-
impact: input.impact,
|
|
529
|
-
headSha: input.classification.headSha,
|
|
530
|
-
domains: input.domains,
|
|
531
|
-
})) ?? fallback)
|
|
532
|
-
: fallback;
|
|
533
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
534
|
-
name: TECHNICAL_SUMMARY_FILE,
|
|
535
|
-
kind: "technical",
|
|
536
|
-
content: `${JSON.stringify(summary, null, 2)}\n`,
|
|
537
|
-
});
|
|
538
|
-
return summary;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Planos de revisão por watchdog (uma chamada por brief, concorrência
|
|
543
|
-
* limitada). Sem IA/chave/feature, devolve mapa vazio e o brief fica igual.
|
|
544
|
-
*/
|
|
545
|
-
async function buildAiPlans(
|
|
546
|
-
activeIds: string[],
|
|
547
|
-
classification: ClassificationResult,
|
|
548
39
|
config: HoundsmanConfig,
|
|
549
|
-
context: ContextSummary | null,
|
|
550
|
-
astAnalyses: ReadonlyMap<string, AstAnalysis> | undefined,
|
|
551
|
-
astImpact: AstImpact | undefined,
|
|
552
|
-
ai: AiClient | undefined,
|
|
553
|
-
opts: ReviewOptions,
|
|
554
|
-
): Promise<Map<string, BriefPlan>> {
|
|
555
|
-
const plans = new Map<string, BriefPlan>();
|
|
556
|
-
if (!ai || opts.noAi) return plans;
|
|
557
|
-
const enricher = new AiBriefEnricher(ai);
|
|
558
|
-
if (!enricher.enabled) return plans;
|
|
559
|
-
|
|
560
|
-
const docs = discoverDocs(Object.keys(classification.files), config);
|
|
561
|
-
const maxFilesPerBrief = resolveMaxFilesPerBrief(config);
|
|
562
|
-
const inputs = [];
|
|
563
|
-
for (const id of activeIds) {
|
|
564
|
-
const def = findWatchdog(config, id);
|
|
565
|
-
if (!def) continue;
|
|
566
|
-
const { selected } = budgetFor(classification, id, maxFilesPerBrief);
|
|
567
|
-
const astFiles: AstFileBrief[] = selected.flatMap((file) => {
|
|
568
|
-
const analysis = astAnalyses?.get(file);
|
|
569
|
-
if (!analysis?.delta) return [];
|
|
570
|
-
return [
|
|
571
|
-
{ file, delta: analysis.delta, importers: astImpact?.importers[file] },
|
|
572
|
-
];
|
|
573
|
-
});
|
|
574
|
-
inputs.push({
|
|
575
|
-
watchdogId: id,
|
|
576
|
-
name: def.name,
|
|
577
|
-
description: def.description,
|
|
578
|
-
scope: def.scope,
|
|
579
|
-
rules: def.rules,
|
|
580
|
-
files: selected,
|
|
581
|
-
churn: classification.churn,
|
|
582
|
-
context,
|
|
583
|
-
ast: astFiles,
|
|
584
|
-
docs,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
return enricher.enrichAll(inputs);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Passo 1 — descobre os arquivos e grava briefs + manifesto. */
|
|
591
|
-
export async function prepare(
|
|
592
40
|
opts: ReviewOptions,
|
|
593
|
-
|
|
594
|
-
config: HoundsmanConfig,
|
|
595
|
-
ctx: ReviewContext,
|
|
596
|
-
ast?: AstService,
|
|
597
|
-
ai?: AiClient,
|
|
41
|
+
deps: PrepareDeps = {},
|
|
598
42
|
): Promise<PrepareResult> {
|
|
599
|
-
|
|
600
|
-
ctx.work.ensure();
|
|
601
|
-
if (opts.clean) ctx.work.cleanFindings();
|
|
602
|
-
ctx.storage.updateReview(ctx.review.id, { base: opts.base, head: opts.head });
|
|
603
|
-
|
|
604
|
-
// Marca o commit revisado e decide entre completo, incremental ou no-op.
|
|
605
|
-
const headSha = ctx.git.resolveSha(opts.head);
|
|
606
|
-
const previous = ctx.storage.readFindings(ctx.review.id);
|
|
607
|
-
const scope = await new ReviewScopeResolver(
|
|
608
|
-
opts,
|
|
609
|
-
config,
|
|
610
|
-
opts.noAst ? undefined : ast,
|
|
611
|
-
ctx.git,
|
|
612
|
-
).resolve(previous, headSha);
|
|
613
|
-
|
|
614
|
-
// Mesmo commit já revisado: preserva artefatos e triagem, sem re-revisar.
|
|
615
|
-
if (scope.kind === "noop") {
|
|
616
|
-
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
617
|
-
// Ciclo em andamento continua válido (os findings já despachados devem
|
|
618
|
-
// ser finalizados); fora isso, o relatório anterior é a resposta final.
|
|
619
|
-
const pendingCycle = status === "reviewing";
|
|
620
|
-
if (!pendingCycle) {
|
|
621
|
-
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
622
|
-
}
|
|
623
|
-
messages.push(
|
|
624
|
-
`Nenhuma alteração desde a última revisão (${shortSha(scope.reviewedFrom)}).`,
|
|
625
|
-
pendingCycle
|
|
626
|
-
? "Nada novo a preparar — o ciclo em andamento continua; rode `--finalize` quando os findings estiverem gravados."
|
|
627
|
-
: "Nada a revisar — o relatório anterior foi mantido. Use `--full` para re-revisar.",
|
|
628
|
-
);
|
|
629
|
-
return { messages };
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const { classification, diff } = scope;
|
|
633
|
-
|
|
634
|
-
// Run recém-criada e sem nenhum arquivo alterado: não persiste uma "run
|
|
635
|
-
// vazia" na lista. Acontece quando o comando roda no clone principal sem
|
|
636
|
-
// `--pr`/`--worktree` (ex.: `houndsman` na `main` logo após revisar uma PR).
|
|
637
|
-
// Run com artefatos anteriores (contexto, revisão) continua existindo.
|
|
638
|
-
if (Object.keys(classification.files).length === 0) {
|
|
639
|
-
const fresh = ctx.storage.listArtifacts(ctx.review.id).length === 0;
|
|
640
|
-
if (fresh) {
|
|
641
|
-
ctx.work.clean();
|
|
642
|
-
ctx.storage.deleteReview(ctx.review.id);
|
|
643
|
-
messages.push(
|
|
644
|
-
`Nenhuma alteração entre ${opts.base}...${opts.head} — nenhuma run criada.`,
|
|
645
|
-
);
|
|
646
|
-
return { messages };
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Marca o run como "revisando" (reinicia o ciclo) — a UI mostra o status.
|
|
651
|
-
writeRunStatus(ctx.storage, ctx.review.id, {
|
|
652
|
-
status: "reviewing",
|
|
653
|
-
resetStartedAt: true,
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
await applyAiClassification(classification, config, ai, opts);
|
|
657
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
658
|
-
name: "classification.json",
|
|
659
|
-
kind: "classification",
|
|
660
|
-
content: `${JSON.stringify(classification, null, 2)}\n`,
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
const astAnalyses = scope.ast;
|
|
664
|
-
const astImpact = astAnalyses && ast ? await ast.impact(astAnalyses) : undefined;
|
|
665
|
-
if (astAnalyses && ast?.enabled) {
|
|
666
|
-
const summary = {
|
|
667
|
-
enabled: true,
|
|
668
|
-
files: Object.fromEntries(
|
|
669
|
-
[...astAnalyses].map(([path, analysis]) => [
|
|
670
|
-
path,
|
|
671
|
-
{
|
|
672
|
-
facts: analysis.facts,
|
|
673
|
-
delta: analysis.delta,
|
|
674
|
-
deleted: analysis.deleted,
|
|
675
|
-
},
|
|
676
|
-
]),
|
|
677
|
-
),
|
|
678
|
-
};
|
|
679
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
680
|
-
name: "ast-summary.json",
|
|
681
|
-
kind: "ast",
|
|
682
|
-
content: `${JSON.stringify(summary, null, 2)}\n`,
|
|
683
|
-
});
|
|
684
|
-
if (astImpact) {
|
|
685
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
686
|
-
name: "ast-impact.json",
|
|
687
|
-
kind: "ast-impact",
|
|
688
|
-
content: `${JSON.stringify(astImpact, null, 2)}\n`,
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const jev = await resolveCandidates(opts, classification, ctx, config, diff, ai);
|
|
694
|
-
|
|
695
|
-
const activeIds = Object.keys(classification.watchdogs);
|
|
696
|
-
if (Object.keys(classification.files).length === 0) {
|
|
697
|
-
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
698
|
-
messages.push(
|
|
699
|
-
classification.mode === "incremental"
|
|
700
|
-
? `Nenhuma alteração desde a última revisão (${shortSha(classification.reviewedFrom ?? "")}).`
|
|
701
|
-
: `Nenhuma alteração entre ${opts.base}...${opts.head}.`,
|
|
702
|
-
);
|
|
703
|
-
return { messages };
|
|
704
|
-
}
|
|
705
|
-
if (activeIds.length === 0) {
|
|
706
|
-
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
707
|
-
messages.push(
|
|
708
|
-
"Nenhum arquivo revisável (tudo excluído, deletado ou irrelevante).",
|
|
709
|
-
);
|
|
710
|
-
return { messages };
|
|
711
|
-
}
|
|
712
|
-
if (classification.mode === "incremental") {
|
|
713
|
-
messages.push(
|
|
714
|
-
`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.`,
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
persistContext(ctx);
|
|
719
|
-
const context = await resolveContextSummary(ctx, ai, opts);
|
|
720
|
-
await resolveTechnicalSummary(ctx, ai, opts, {
|
|
721
|
-
branch,
|
|
722
|
-
classification,
|
|
723
|
-
context,
|
|
724
|
-
ast: astAnalyses,
|
|
725
|
-
impact: astImpact,
|
|
726
|
-
domains: activeIds.map((id) => findWatchdog(config, id)?.name ?? id),
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
const manifestAst: ManifestAst | undefined = astAnalyses
|
|
730
|
-
? {
|
|
731
|
-
enabled: ast?.enabled ?? false,
|
|
732
|
-
languages: [
|
|
733
|
-
...new Set(
|
|
734
|
-
[...astAnalyses.values()].map(
|
|
735
|
-
(analysis) => analysis.facts.language,
|
|
736
|
-
),
|
|
737
|
-
),
|
|
738
|
-
].sort(),
|
|
739
|
-
files: astAnalyses.size,
|
|
740
|
-
impact: {
|
|
741
|
-
affectedFiles: astImpact?.affectedFiles.length ?? 0,
|
|
742
|
-
importers: Object.keys(astImpact?.importers ?? {}).length,
|
|
743
|
-
exportedChanges: astImpact?.exportedChanges.length ?? 0,
|
|
744
|
-
},
|
|
745
|
-
}
|
|
746
|
-
: undefined;
|
|
747
|
-
|
|
748
|
-
const aiPlans = await buildAiPlans(
|
|
749
|
-
activeIds,
|
|
750
|
-
classification,
|
|
751
|
-
config,
|
|
752
|
-
context,
|
|
753
|
-
astAnalyses,
|
|
754
|
-
astImpact,
|
|
755
|
-
ai,
|
|
756
|
-
opts,
|
|
757
|
-
);
|
|
758
|
-
if (aiPlans.size > 0) {
|
|
759
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
760
|
-
name: "ai-briefs.json",
|
|
761
|
-
kind: "ai-brief",
|
|
762
|
-
content: `${JSON.stringify(
|
|
763
|
-
{ plans: Object.fromEntries(aiPlans) },
|
|
764
|
-
null,
|
|
765
|
-
2,
|
|
766
|
-
)}\n`,
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
const aiUsage = ai?.recorder.total;
|
|
770
|
-
const manifestAi: ManifestAi | undefined =
|
|
771
|
-
ai && !opts.noAi && (aiPlans.size > 0 || (aiUsage?.calls ?? 0) > 0)
|
|
772
|
-
? {
|
|
773
|
-
enabled: true,
|
|
774
|
-
provider: ai.projectConfig.aiProvider,
|
|
775
|
-
model: ai.modelId,
|
|
776
|
-
briefPlans: aiPlans.size,
|
|
777
|
-
usage: aiUsage ?? {
|
|
778
|
-
inputTokens: 0,
|
|
779
|
-
outputTokens: 0,
|
|
780
|
-
totalTokens: 0,
|
|
781
|
-
calls: 0,
|
|
782
|
-
},
|
|
783
|
-
}
|
|
784
|
-
: undefined;
|
|
785
|
-
|
|
786
|
-
writeBriefs(
|
|
787
|
-
activeIds,
|
|
788
|
-
classification,
|
|
789
|
-
ctx,
|
|
790
|
-
branch,
|
|
791
|
-
context,
|
|
792
|
-
jev.candidates,
|
|
793
|
-
config,
|
|
794
|
-
astAnalyses,
|
|
795
|
-
astImpact,
|
|
796
|
-
aiPlans,
|
|
797
|
-
);
|
|
798
|
-
writeManifest(
|
|
799
|
-
ctx,
|
|
800
|
-
config,
|
|
801
|
-
opts,
|
|
802
|
-
classification,
|
|
803
|
-
activeIds,
|
|
804
|
-
activeIds,
|
|
805
|
-
jev,
|
|
806
|
-
manifestAst,
|
|
807
|
-
manifestAi,
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
messages.push(
|
|
811
|
-
...discoveryMessages(activeIds, classification, ctx.work.briefsDir),
|
|
812
|
-
...budgetMessage(classification, config),
|
|
813
|
-
...changedScopeMessage(classification),
|
|
814
|
-
...jevSummary(jev),
|
|
815
|
-
);
|
|
816
|
-
if (astAnalyses) {
|
|
817
|
-
messages.push(
|
|
818
|
-
`AST: ${astAnalyses.size} arquivo(s) analisado(s), ` +
|
|
819
|
-
`${astImpact?.affectedFiles.length ?? 0} impactado(s).`,
|
|
820
|
-
);
|
|
821
|
-
} else {
|
|
822
|
-
messages.push("AST: desabilitado ou sem grammar compatível.");
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (context) {
|
|
826
|
-
messages.push("Contexto da PR resumido e injetado nos briefs.");
|
|
827
|
-
} else {
|
|
828
|
-
messages.push(
|
|
829
|
-
"Sem contexto da PR (rode `bun run houndsman context` antes, se houver PR).",
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
if (aiPlans.size > 0) {
|
|
833
|
-
messages.push(
|
|
834
|
-
`IA: plano de revisão em ${aiPlans.size} brief(s) (sugestão, não substitui regras).`,
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
if (aiUsage && aiUsage.calls > 0) {
|
|
838
|
-
messages.push(
|
|
839
|
-
`IA: ${aiUsage.calls} chamada(s), ${aiUsage.totalTokens} token(s).`,
|
|
840
|
-
);
|
|
841
|
-
ai?.recorder.write(ctx.storage, ctx.review.id);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
messages.push(
|
|
845
|
-
`\nPróximo passo: despachar os sub-agentes (um por brief em ${displayPath(ctx.work.briefsDir)}/).`,
|
|
846
|
-
"Quando os findings estiverem gravados: bun run houndsman --finalize",
|
|
847
|
-
);
|
|
848
|
-
|
|
849
|
-
return { messages };
|
|
850
|
-
}
|
|
851
|
-
/** Persiste o contexto da PR (bruto + resumo) como artefatos, quando existirem. */
|
|
852
|
-
function persistContext(ctx: ReviewContext): void {
|
|
853
|
-
const raw = ctx.work.readContextRaw();
|
|
854
|
-
if (raw !== null) {
|
|
855
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
856
|
-
name: "context.json",
|
|
857
|
-
kind: "context",
|
|
858
|
-
content: raw,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
861
|
-
const summary = ctx.work.readContextSummaryRaw();
|
|
862
|
-
if (summary !== null) {
|
|
863
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
864
|
-
name: "context-summary.json",
|
|
865
|
-
kind: "context-summary",
|
|
866
|
-
content: summary,
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const brief = ctx.work.readContextBriefRaw();
|
|
870
|
-
if (brief !== null) {
|
|
871
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
872
|
-
name: "context-brief.md",
|
|
873
|
-
kind: "context-brief",
|
|
874
|
-
content: brief,
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ---------------------------------------------------------------------------
|
|
880
|
-
// Passo 2 — finalizar
|
|
881
|
-
// ---------------------------------------------------------------------------
|
|
882
|
-
|
|
883
|
-
/** Reaproveita `ast`/`ai` do manifesto anterior (o finalize não os recalcula). */
|
|
884
|
-
function readManifestExtras(ctx: ReviewContext): {
|
|
885
|
-
ast?: ManifestAst;
|
|
886
|
-
ai?: ManifestAi;
|
|
887
|
-
} {
|
|
888
|
-
const artifact = ctx.storage.readArtifact(ctx.review.id, "manifest.json");
|
|
889
|
-
if (!artifact) return {};
|
|
890
|
-
try {
|
|
891
|
-
const parsed = JSON.parse(artifact.content) as {
|
|
892
|
-
ast?: ManifestAst;
|
|
893
|
-
ai?: ManifestAi;
|
|
894
|
-
};
|
|
895
|
-
return { ast: parsed.ast, ai: parsed.ai };
|
|
896
|
-
} catch {
|
|
897
|
-
return {};
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/** Lê o `classification.json` persistido pelo prepare (lança se ausente). */
|
|
902
|
-
function readClassification(ctx: ReviewContext): RunClassification {
|
|
903
|
-
const artifact = ctx.storage.readArtifact(ctx.review.id, "classification.json");
|
|
904
|
-
if (!artifact) {
|
|
905
|
-
throw new Error(
|
|
906
|
-
`Não encontrei a classificação da review. Rode \`bun run houndsman\` (preparar) primeiro.`,
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
return JSON.parse(artifact.content) as RunClassification;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/** Lê os candidatos Jev persistidos (unavailable se ausente/inválido). */
|
|
913
|
-
function readCandidates(ctx: ReviewContext, config: HoundsmanConfig): JevCandidatesFile {
|
|
914
|
-
const artifact = ctx.storage.readArtifact(ctx.review.id, "candidates.json");
|
|
915
|
-
if (!artifact) return JevCandidatesStore.unavailable("candidates.json ausente");
|
|
916
|
-
try {
|
|
917
|
-
return (
|
|
918
|
-
parseCandidates(JSON.parse(artifact.content), config.watchdogIds) ??
|
|
919
|
-
JevCandidatesStore.unavailable("candidates.json inválido")
|
|
920
|
-
);
|
|
921
|
-
} catch {
|
|
922
|
-
return JevCandidatesStore.unavailable("candidates.json inválido");
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/** Resumo do contexto persistido (usado quando o staging já foi limpo). */
|
|
927
|
-
function readPersistedContextSummary(ctx: ReviewContext): ContextSummary | null {
|
|
928
|
-
const artifact = ctx.storage.readArtifact(ctx.review.id, "context-summary.json");
|
|
929
|
-
if (!artifact) return null;
|
|
930
|
-
try {
|
|
931
|
-
return parseContextSummary(JSON.parse(artifact.content));
|
|
932
|
-
} catch {
|
|
933
|
-
return null;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
/** JSON persistido, ou `null` quando ausente/inválido. */
|
|
938
|
-
function readArtifactJson<T>(ctx: ReviewContext, name: string): T | null {
|
|
939
|
-
const artifact = ctx.storage.readArtifact(ctx.review.id, name);
|
|
940
|
-
if (!artifact) return null;
|
|
941
|
-
try {
|
|
942
|
-
return JSON.parse(artifact.content) as T;
|
|
943
|
-
} catch {
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** Pontos técnicos persistidos pelo `prepare`; `null` quando ausentes. */
|
|
949
|
-
function readPersistedTechnicalSummary(ctx: ReviewContext): TechnicalSummary | null {
|
|
950
|
-
const raw = readArtifactJson<unknown>(ctx, TECHNICAL_SUMMARY_FILE);
|
|
951
|
-
return raw ? parseTechnicalSummary(raw) : null;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
/** Fallback para runs antigas: reconstrói os destaques dos artefatos de AST. */
|
|
955
|
-
function buildTechnicalFallback(
|
|
956
|
-
ctx: ReviewContext,
|
|
957
|
-
classification: RunClassification,
|
|
958
|
-
): TechnicalSummary {
|
|
959
|
-
const ast = readArtifactJson<{ files?: Record<string, AstAnalysis> }>(
|
|
960
|
-
ctx,
|
|
961
|
-
"ast-summary.json",
|
|
962
|
-
);
|
|
963
|
-
return buildTechnicalSummary({
|
|
964
|
-
classification,
|
|
965
|
-
ast: ast?.files ? new Map(Object.entries(ast.files)) : undefined,
|
|
966
|
-
impact: readArtifactJson<AstImpact>(ctx, "ast-impact.json") ?? undefined,
|
|
967
|
-
headSha: classification.headSha,
|
|
968
|
-
});
|
|
43
|
+
return new PreparePipeline(ctx, config, opts, deps).run();
|
|
969
44
|
}
|
|
970
45
|
|
|
971
|
-
/**
|
|
972
|
-
* Grava um `review.<watchdog>.md` por watchdog com achados e devolve o índice.
|
|
973
|
-
* Mantém o `review.md` consolidado navegável em PRs grandes (centenas de achados).
|
|
974
|
-
*/
|
|
975
|
-
function writeDomainReports(
|
|
976
|
-
ctx: ReviewContext,
|
|
977
|
-
branch: string,
|
|
978
|
-
reports: WatchdogReport[],
|
|
979
|
-
classification: ClassificationResult,
|
|
980
|
-
context: ContextSummary | null,
|
|
981
|
-
nameOf: (id: string) => string,
|
|
982
|
-
): DomainReport[] {
|
|
983
|
-
const domains: DomainReport[] = [];
|
|
984
|
-
const renderer = new ReportRenderer(nameOf);
|
|
985
|
-
for (const report of reports) {
|
|
986
|
-
const findings = dedupeFindings([report]);
|
|
987
|
-
if (findings.length === 0) continue;
|
|
988
|
-
const file = `review.${sanitizeBranch(report.watchdogId)}.md`;
|
|
989
|
-
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
990
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
991
|
-
name: file,
|
|
992
|
-
kind: "domain-review",
|
|
993
|
-
content: `${renderer.renderDomain(
|
|
994
|
-
branch,
|
|
995
|
-
report,
|
|
996
|
-
findings,
|
|
997
|
-
expected.length,
|
|
998
|
-
context,
|
|
999
|
-
)}\n`,
|
|
1000
|
-
});
|
|
1001
|
-
domains.push({
|
|
1002
|
-
id: report.watchdogId,
|
|
1003
|
-
name: nameOf(report.watchdogId),
|
|
1004
|
-
file,
|
|
1005
|
-
findings: findings.length,
|
|
1006
|
-
reviewedFiles: report.reviewedFiles.length,
|
|
1007
|
-
expectedFiles: expected.length,
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
return domains;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
export type FinalizeResult = {
|
|
1014
|
-
reportPath: string;
|
|
1015
|
-
blockers: number;
|
|
1016
|
-
attention: number;
|
|
1017
|
-
messages: string[];
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Passo 2 — consolida os findings em review.md.
|
|
1022
|
-
* Lança se houver findings pendentes (o CLI decide o exit code).
|
|
1023
|
-
*/
|
|
46
|
+
/** Passo 2 — consolida os findings no resumo final. */
|
|
1024
47
|
export function finalize(
|
|
1025
|
-
opts: ReviewOptions,
|
|
1026
|
-
branch: string,
|
|
1027
|
-
config: HoundsmanConfig,
|
|
1028
48
|
ctx: ReviewContext,
|
|
49
|
+
config: HoundsmanConfig,
|
|
50
|
+
opts: ReviewOptions,
|
|
1029
51
|
): FinalizeResult {
|
|
1030
|
-
|
|
1031
|
-
// guarda, um prepare no-op seguido de `--finalize` reescreveria o relatório
|
|
1032
|
-
// anterior com a classificação antiga (perdendo os relatórios por domínio).
|
|
1033
|
-
// O `ctx.review` é um snapshot: relê o status atual do storage.
|
|
1034
|
-
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
1035
|
-
if (status !== "reviewing") {
|
|
1036
|
-
const state = ctx.storage.readFindings(ctx.review.id);
|
|
1037
|
-
const present = Object.values(state?.findings ?? {}).filter(
|
|
1038
|
-
(finding) => finding.present,
|
|
1039
|
-
);
|
|
1040
|
-
return {
|
|
1041
|
-
reportPath: ctx.storage.location(ctx.review.id),
|
|
1042
|
-
blockers: present.filter((f) => f.severity === "blocking").length,
|
|
1043
|
-
attention: present.filter((f) => f.severity === "attention").length,
|
|
1044
|
-
messages: [
|
|
1045
|
-
`Nada a finalizar: nenhum ciclo de revisão pendente (status: ${status ?? "sem status"}).`,
|
|
1046
|
-
"Se nada mudou desde a última revisão, o relatório anterior foi mantido; use `--full` para re-revisar.",
|
|
1047
|
-
],
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
const classification = readClassification(ctx);
|
|
1052
|
-
const activeIds = Object.keys(classification.watchdogs);
|
|
1053
|
-
|
|
1054
|
-
const messages: string[] = [];
|
|
1055
|
-
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
1056
|
-
messages.push(...warnings);
|
|
1057
|
-
const jev = readCandidates(ctx, config);
|
|
1058
|
-
const extras = readManifestExtras(ctx);
|
|
1059
|
-
writeManifest(
|
|
1060
|
-
ctx,
|
|
1061
|
-
config,
|
|
1062
|
-
opts,
|
|
1063
|
-
classification,
|
|
1064
|
-
activeIds,
|
|
1065
|
-
pending,
|
|
1066
|
-
jev,
|
|
1067
|
-
extras.ast,
|
|
1068
|
-
extras.ai,
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
|
-
if (pending.length > 0) {
|
|
1072
|
-
throw new Error(
|
|
1073
|
-
`Faltam ${pending.length}/${activeIds.length} findings: ${pending.join(", ")}.\n` +
|
|
1074
|
-
`Cada watchdog deve gravar ${displayPath(ctx.work.findingsDir)}/<id>.json (schema ${OUTPUT_FORMAT_PATH}).`,
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
const now = systemClock();
|
|
1079
|
-
// Findings re-reportados neste ciclo + carry-over (arquivos inalterados).
|
|
1080
|
-
const merged = mergeCarryover(
|
|
1081
|
-
dedupeFindings(reports),
|
|
1082
|
-
classification.carryover ?? [],
|
|
1083
|
-
);
|
|
1084
|
-
if (classification.mode === "incremental") {
|
|
1085
|
-
messages.push(
|
|
1086
|
-
`Carry-over: ${classification.carryover?.length ?? 0} finding(s) preservado(s) de ${shortSha(classification.reviewedFrom ?? "")}.`,
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Estado de triagem: preserva o que já existia (casando por fingerprint) e
|
|
1091
|
-
// anexa o `id` de cada finding aos itens do relatório. A linha é cache —
|
|
1092
|
-
// recalculada por `houndsman findings sync` após edições. O commit revisado
|
|
1093
|
-
// (`reviewed.headSha`) habilita o review incremental e o diff focado do verify.
|
|
1094
|
-
const state = buildStateFromFindings(merged, {
|
|
1095
|
-
branch,
|
|
1096
|
-
readFile: fileReader(ctx.root),
|
|
1097
|
-
previous: ctx.storage.readFindings(ctx.review.id),
|
|
1098
|
-
reviewedPaths: new Set([
|
|
1099
|
-
...reports.flatMap((report) => report.reviewedFiles),
|
|
1100
|
-
// Arquivo deletado: o achado deixou de existir junto com ele.
|
|
1101
|
-
...classification.deleted,
|
|
1102
|
-
]),
|
|
1103
|
-
now,
|
|
1104
|
-
headSha: classification.headSha,
|
|
1105
|
-
reviewed: classification.headSha
|
|
1106
|
-
? { headSha: classification.headSha, at: now }
|
|
1107
|
-
: undefined,
|
|
1108
|
-
});
|
|
1109
|
-
|
|
1110
|
-
// Findings corrigidos entre revisões (ausentes + done) têm a conversa do PR
|
|
1111
|
-
// resolvida automaticamente; as já resolvidas no GitHub são puladas. Só
|
|
1112
|
-
// toca o `gh` quando há vínculo remoto pendente (fail-open).
|
|
1113
|
-
const threadIds = Object.values(state.findings)
|
|
1114
|
-
.filter(
|
|
1115
|
-
(finding) =>
|
|
1116
|
-
!finding.present &&
|
|
1117
|
-
finding.execState === "done" &&
|
|
1118
|
-
finding.github &&
|
|
1119
|
-
finding.github.kind !== "comment" &&
|
|
1120
|
-
!finding.github.resolvedAt,
|
|
1121
|
-
)
|
|
1122
|
-
.map((finding) => finding.id);
|
|
1123
|
-
if (threadIds.length > 0) {
|
|
1124
|
-
const sync = syncResolvedThreads({ state, ids: threadIds, now });
|
|
1125
|
-
if (!sync.available) {
|
|
1126
|
-
messages.push(`GitHub: threads não sincronizadas (${sync.reason}).`);
|
|
1127
|
-
} else if (sync.checked > 0) {
|
|
1128
|
-
const failures =
|
|
1129
|
-
sync.failed.length > 0 ? `, ${sync.failed.length} falha(s)` : "";
|
|
1130
|
-
messages.push(
|
|
1131
|
-
`GitHub: ${sync.resolved.length} thread(s) resolvida(s), ${sync.skipped.length} já resolvida(s)/pulada(s)${failures}.`,
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
ctx.storage.writeFindings(ctx.review.id, state);
|
|
1137
|
-
ctx.storage.updateReview(ctx.review.id, {
|
|
1138
|
-
base: opts.base,
|
|
1139
|
-
head: opts.head,
|
|
1140
|
-
headSha: classification.headSha,
|
|
1141
|
-
});
|
|
1142
|
-
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
1143
|
-
|
|
1144
|
-
persistContext(ctx);
|
|
1145
|
-
const context = ctx.work.readContextSummary() ?? readPersistedContextSummary(ctx);
|
|
1146
|
-
const technical =
|
|
1147
|
-
readPersistedTechnicalSummary(ctx) ?? buildTechnicalFallback(ctx, classification);
|
|
1148
|
-
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
1149
|
-
const domains = writeDomainReports(
|
|
1150
|
-
ctx,
|
|
1151
|
-
branch,
|
|
1152
|
-
reports,
|
|
1153
|
-
classification,
|
|
1154
|
-
context,
|
|
1155
|
-
nameOf,
|
|
1156
|
-
);
|
|
1157
|
-
const markdown = new ReportRenderer(nameOf).render(
|
|
1158
|
-
branch,
|
|
1159
|
-
reports,
|
|
1160
|
-
merged,
|
|
1161
|
-
{ classified: classification.watchdogs },
|
|
1162
|
-
context,
|
|
1163
|
-
jev.available ? jev.candidates : undefined,
|
|
1164
|
-
domains,
|
|
1165
|
-
);
|
|
1166
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
1167
|
-
name: REVIEW_FILE,
|
|
1168
|
-
kind: "review",
|
|
1169
|
-
content: `${incrementalNote(classification)}${markdown}\n`,
|
|
1170
|
-
});
|
|
1171
|
-
// Resumo final para leitura humana — não participa de nenhuma etapa seguinte.
|
|
1172
|
-
ctx.storage.writeArtifact(ctx.review.id, {
|
|
1173
|
-
name: REVIEW_RESUME_FILE,
|
|
1174
|
-
kind: "resume",
|
|
1175
|
-
content: renderReviewResume({
|
|
1176
|
-
branch,
|
|
1177
|
-
date: now.slice(0, 10),
|
|
1178
|
-
context,
|
|
1179
|
-
classification,
|
|
1180
|
-
findings: merged,
|
|
1181
|
-
technical,
|
|
1182
|
-
}),
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
const location = ctx.storage.location(ctx.review.id);
|
|
1186
|
-
messages.push(
|
|
1187
|
-
`Triagem: ${location} — ${Object.keys(state.findings).length} finding(s).`,
|
|
1188
|
-
);
|
|
1189
|
-
|
|
1190
|
-
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
1191
|
-
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
1192
|
-
messages.push(
|
|
1193
|
-
`Relatório: ${location} — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
1194
|
-
);
|
|
1195
|
-
|
|
1196
|
-
// O staging já cumpriu o papel (briefs/findings/contexto estão no storage).
|
|
1197
|
-
ctx.work.clean();
|
|
1198
|
-
|
|
1199
|
-
return { reportPath: location, blockers, attention, messages };
|
|
52
|
+
return new FinalizePipeline(ctx, config, opts).run();
|
|
1200
53
|
}
|
|
1201
54
|
|
|
1202
55
|
/** Resolve a branch (opção explícita ou a atual do git). */
|