@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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline do finalize (Passo 2): findings → dedupe → estado de triagem →
|
|
3
|
+
* resumo final (`review-resume.md`).
|
|
4
|
+
*
|
|
5
|
+
* O contexto da run entra no construtor; sem estado ambiente.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { summarizeBusiness } from "../business-summary";
|
|
9
|
+
import type { HoundsmanConfig } from "../config";
|
|
10
|
+
import { buildStateFromFindings } from "../findings-state";
|
|
11
|
+
import { syncResolvedThreads } from "../github-review";
|
|
12
|
+
import type { JevCandidatesFile } from "../jev";
|
|
13
|
+
import { displayPath, OUTPUT_FORMAT_PATH, REVIEW_RESUME_FILE } from "../paths";
|
|
14
|
+
import { ProjectConfig } from "../project-config";
|
|
15
|
+
import {
|
|
16
|
+
type ManifestAi,
|
|
17
|
+
type ManifestAst,
|
|
18
|
+
type ManifestBusiness,
|
|
19
|
+
ManifestWriter,
|
|
20
|
+
} from "../renderers/manifest-writer";
|
|
21
|
+
import { type ResumeDomain, renderReviewResume } from "../renderers/resume-renderer";
|
|
22
|
+
import { dedupeFindings, type WatchdogReport } from "../report-builder";
|
|
23
|
+
import { writeRunStatus } from "../run-status";
|
|
24
|
+
import { systemClock } from "../services/clock";
|
|
25
|
+
import { findWatchdog } from "../watchdog-registry";
|
|
26
|
+
import { mergeCarryover } from "./carryover";
|
|
27
|
+
import type { ReviewContext } from "./review-context";
|
|
28
|
+
import type { ReviewOptions } from "./review-options";
|
|
29
|
+
import type { RunClassification } from "./review-scope";
|
|
30
|
+
import { RunArtifacts } from "./run-artifacts";
|
|
31
|
+
import { shortSha } from "./run-format";
|
|
32
|
+
|
|
33
|
+
/** Aviso no topo do resumo quando a revisão foi incremental (senão vazio). */
|
|
34
|
+
function incrementalNote(classification: RunClassification): string {
|
|
35
|
+
if (classification.mode !== "incremental" || !classification.reviewedFrom) return "";
|
|
36
|
+
const carried = classification.carryover?.length ?? 0;
|
|
37
|
+
return (
|
|
38
|
+
`> **Revisão incremental** — desde \`${shortSha(classification.reviewedFrom)}\`; ` +
|
|
39
|
+
`${Object.keys(classification.files).length} arquivo(s) alterado(s), ` +
|
|
40
|
+
`${carried} finding(s) carregado(s) do review anterior.\n\n`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cobertura efetiva de um watchdog: só conta arquivos que estavam no escopo.
|
|
46
|
+
*
|
|
47
|
+
* `reviewedFiles` é auto-declarado pelo subagente e costuma incluir chamadores e
|
|
48
|
+
* arquivos de outros domínios; sem o recorte, a tabela mostrava "25/13" e
|
|
49
|
+
* escondia o que realmente ficou de fora.
|
|
50
|
+
*/
|
|
51
|
+
export function coverageOf(
|
|
52
|
+
reviewedFiles: readonly string[],
|
|
53
|
+
expectedFiles: readonly string[],
|
|
54
|
+
): { reviewed: number; expected: number } {
|
|
55
|
+
const expected = new Set(expectedFiles);
|
|
56
|
+
const reviewed = new Set(reviewedFiles.filter((file) => expected.has(file)));
|
|
57
|
+
return { reviewed: reviewed.size, expected: expected.size };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Arquivos do escopo que não foram revisados nem declarados em `skipped`. */
|
|
61
|
+
export function unaccountedFiles(
|
|
62
|
+
reviewedFiles: readonly string[],
|
|
63
|
+
skippedFiles: readonly string[],
|
|
64
|
+
expectedFiles: readonly string[],
|
|
65
|
+
): string[] {
|
|
66
|
+
const accounted = new Set([...reviewedFiles, ...skippedFiles]);
|
|
67
|
+
return expectedFiles.filter((file) => !accounted.has(file));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Arquivos do escopo declarados em `skipped` (e não revisados). */
|
|
71
|
+
export function skippedInScope(
|
|
72
|
+
reviewedFiles: readonly string[],
|
|
73
|
+
skippedFiles: readonly string[],
|
|
74
|
+
expectedFiles: readonly string[],
|
|
75
|
+
): number {
|
|
76
|
+
const reviewed = new Set(reviewedFiles);
|
|
77
|
+
const expected = new Set(expectedFiles);
|
|
78
|
+
return new Set(
|
|
79
|
+
skippedFiles.filter((file) => expected.has(file) && !reviewed.has(file)),
|
|
80
|
+
).size;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cobertura por domínio (achados × arquivos revisados/esperados) para a tabela
|
|
85
|
+
* do resumo final. Não grava relatório por domínio: os achados vivem na triagem
|
|
86
|
+
* (`findings`), que já é filtrável por watchdog na UI.
|
|
87
|
+
*/
|
|
88
|
+
function summarizeDomains(
|
|
89
|
+
reports: readonly WatchdogReport[],
|
|
90
|
+
classification: RunClassification,
|
|
91
|
+
nameOf: (id: string) => string,
|
|
92
|
+
): ResumeDomain[] {
|
|
93
|
+
return reports
|
|
94
|
+
.map((report) => {
|
|
95
|
+
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
96
|
+
const coverage = coverageOf(report.reviewedFiles, expected);
|
|
97
|
+
return {
|
|
98
|
+
id: report.watchdogId,
|
|
99
|
+
name: nameOf(report.watchdogId),
|
|
100
|
+
findings: dedupeFindings([report]).length,
|
|
101
|
+
reviewedFiles: coverage.reviewed,
|
|
102
|
+
expectedFiles: coverage.expected,
|
|
103
|
+
skippedFiles: skippedInScope(
|
|
104
|
+
report.reviewedFiles,
|
|
105
|
+
report.skipped,
|
|
106
|
+
expected,
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.sort((a, b) => b.findings - a.findings || a.name.localeCompare(b.name));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type FinalizeResult = {
|
|
114
|
+
reportPath: string;
|
|
115
|
+
blockers: number;
|
|
116
|
+
attention: number;
|
|
117
|
+
messages: string[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Passo 2 — consolida os findings no resumo final (`review-resume.md`).
|
|
122
|
+
* Lança se houver findings pendentes (o CLI decide o exit code).
|
|
123
|
+
*/
|
|
124
|
+
export class FinalizePipeline {
|
|
125
|
+
private readonly artifacts: RunArtifacts;
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
private readonly ctx: ReviewContext,
|
|
129
|
+
private readonly config: HoundsmanConfig,
|
|
130
|
+
private readonly opts: ReviewOptions,
|
|
131
|
+
) {
|
|
132
|
+
this.artifacts = new RunArtifacts(ctx.storage, ctx.review, ctx.work);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
run(): FinalizeResult {
|
|
136
|
+
const { ctx, config, opts } = this;
|
|
137
|
+
const branch = ctx.review.branch;
|
|
138
|
+
// `--finalize` só consolida um ciclo preparado (status `reviewing`). Sem esta
|
|
139
|
+
// guarda, um prepare no-op seguido de `--finalize` reescreveria o resumo
|
|
140
|
+
// anterior com a classificação antiga (perdendo a cobertura por domínio).
|
|
141
|
+
// O `ctx.review` é um snapshot: relê o status atual do storage.
|
|
142
|
+
const status = ctx.storage.getReview(ctx.review.id)?.status ?? null;
|
|
143
|
+
if (status !== "reviewing") {
|
|
144
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
145
|
+
const present = Object.values(state?.findings ?? {}).filter(
|
|
146
|
+
(finding) => finding.present,
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
reportPath: ctx.storage.location(ctx.review.id),
|
|
150
|
+
blockers: present.filter((f) => f.severity === "blocking").length,
|
|
151
|
+
attention: present.filter((f) => f.severity === "attention").length,
|
|
152
|
+
messages: [
|
|
153
|
+
`Nada a finalizar: nenhum ciclo de revisão pendente (status: ${status ?? "sem status"}).`,
|
|
154
|
+
"Se nada mudou desde a última revisão, o resumo anterior foi mantido; use `--full` para re-revisar.",
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const classification = this.artifacts.classification();
|
|
160
|
+
const activeIds = Object.keys(classification.watchdogs);
|
|
161
|
+
|
|
162
|
+
const messages: string[] = [];
|
|
163
|
+
const { reports, pending, warnings } = ctx.work.readFindings(activeIds);
|
|
164
|
+
messages.push(...warnings);
|
|
165
|
+
const jev = this.artifacts.candidates(config);
|
|
166
|
+
const extras = this.artifacts.manifestExtras();
|
|
167
|
+
const business = this.artifacts.persistBusiness();
|
|
168
|
+
const manifestBusiness: ManifestBusiness | undefined = business
|
|
169
|
+
? { source: business.source, ...summarizeBusiness(business) }
|
|
170
|
+
: extras.business;
|
|
171
|
+
this.writeManifest(
|
|
172
|
+
classification,
|
|
173
|
+
activeIds,
|
|
174
|
+
pending,
|
|
175
|
+
jev,
|
|
176
|
+
extras.ast,
|
|
177
|
+
extras.ai,
|
|
178
|
+
manifestBusiness,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (pending.length > 0) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Faltam ${pending.length}/${activeIds.length} findings: ${pending.join(", ")}.\n` +
|
|
184
|
+
`Cada watchdog deve gravar ${displayPath(ctx.work.findingsDir)}/<id>.json (schema ${OUTPUT_FORMAT_PATH}).`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const now = systemClock();
|
|
189
|
+
// Findings re-reportados neste ciclo + carry-over (arquivos inalterados).
|
|
190
|
+
const merged = mergeCarryover(
|
|
191
|
+
dedupeFindings(reports),
|
|
192
|
+
classification.carryover ?? [],
|
|
193
|
+
);
|
|
194
|
+
if (classification.mode === "incremental") {
|
|
195
|
+
messages.push(
|
|
196
|
+
`Carry-over: ${classification.carryover?.length ?? 0} finding(s) preservado(s) de ${shortSha(classification.reviewedFrom ?? "")}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Estado de triagem: preserva o que já existia (casando por fingerprint) e
|
|
201
|
+
// anexa o `id` de cada finding aos itens do relatório. A linha é cache —
|
|
202
|
+
// recalculada por `houndsman findings sync` após edições. O commit revisado
|
|
203
|
+
// (`reviewed.headSha`) habilita o review incremental e o diff focado do verify.
|
|
204
|
+
const state = buildStateFromFindings(merged, {
|
|
205
|
+
branch,
|
|
206
|
+
readFile: ctx.files.reader(),
|
|
207
|
+
previous: ctx.storage.readFindings(ctx.review.id),
|
|
208
|
+
reviewedPaths: new Set([
|
|
209
|
+
...reports.flatMap((report) => report.reviewedFiles),
|
|
210
|
+
// Arquivo deletado: o achado deixou de existir junto com ele.
|
|
211
|
+
...classification.deleted,
|
|
212
|
+
]),
|
|
213
|
+
now,
|
|
214
|
+
headSha: classification.headSha,
|
|
215
|
+
reviewed: classification.headSha
|
|
216
|
+
? { headSha: classification.headSha, at: now }
|
|
217
|
+
: undefined,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Findings corrigidos entre revisões (ausentes + done) têm a conversa do PR
|
|
221
|
+
// resolvida automaticamente; as já resolvidas no GitHub são puladas. Só
|
|
222
|
+
// toca o `gh` quando há vínculo remoto pendente (fail-open).
|
|
223
|
+
const threadIds = Object.values(state.findings)
|
|
224
|
+
.filter(
|
|
225
|
+
(finding) =>
|
|
226
|
+
!finding.present &&
|
|
227
|
+
finding.execState === "done" &&
|
|
228
|
+
finding.github &&
|
|
229
|
+
finding.github.kind !== "comment" &&
|
|
230
|
+
!finding.github.resolvedAt,
|
|
231
|
+
)
|
|
232
|
+
.map((finding) => finding.id);
|
|
233
|
+
if (threadIds.length > 0) {
|
|
234
|
+
const sync = syncResolvedThreads({ state, ids: threadIds, now });
|
|
235
|
+
if (!sync.available) {
|
|
236
|
+
messages.push(`GitHub: threads não sincronizadas (${sync.reason}).`);
|
|
237
|
+
} else if (sync.checked > 0) {
|
|
238
|
+
const failures =
|
|
239
|
+
sync.failed.length > 0 ? `, ${sync.failed.length} falha(s)` : "";
|
|
240
|
+
messages.push(
|
|
241
|
+
`GitHub: ${sync.resolved.length} thread(s) resolvida(s), ${sync.skipped.length} já resolvida(s)/pulada(s)${failures}.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
247
|
+
ctx.storage.updateReview(ctx.review.id, {
|
|
248
|
+
base: opts.base,
|
|
249
|
+
head: opts.head,
|
|
250
|
+
headSha: classification.headSha,
|
|
251
|
+
});
|
|
252
|
+
writeRunStatus(ctx.storage, ctx.review.id, { status: "review_done" });
|
|
253
|
+
|
|
254
|
+
this.artifacts.persistContext();
|
|
255
|
+
const context = ctx.work.readContextSummary() ?? this.artifacts.contextSummary();
|
|
256
|
+
const technical =
|
|
257
|
+
this.artifacts.technicalSummary() ??
|
|
258
|
+
this.artifacts.technicalFallback(classification);
|
|
259
|
+
const nameOf = (id: string): string => findWatchdog(config, id)?.name ?? id;
|
|
260
|
+
const domains = summarizeDomains(reports, classification, nameOf);
|
|
261
|
+
// Resumo final — único artefato humano: negócio, técnico, cobertura e
|
|
262
|
+
// conclusão. Os achados ficam na triagem (storage/UI), não duplicados aqui.
|
|
263
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
264
|
+
name: REVIEW_RESUME_FILE,
|
|
265
|
+
kind: "resume",
|
|
266
|
+
content: renderReviewResume({
|
|
267
|
+
branch,
|
|
268
|
+
date: now.slice(0, 10),
|
|
269
|
+
note: incrementalNote(classification),
|
|
270
|
+
context,
|
|
271
|
+
business,
|
|
272
|
+
classification,
|
|
273
|
+
findings: merged,
|
|
274
|
+
technical,
|
|
275
|
+
domains,
|
|
276
|
+
candidates: jev.available ? jev.candidates : undefined,
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
281
|
+
messages.push(
|
|
282
|
+
`Triagem: ${location} — ${Object.keys(state.findings).length} finding(s).`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const blockers = merged.filter((f) => f.severity === "blocking").length;
|
|
286
|
+
const attention = merged.filter((f) => f.severity === "attention").length;
|
|
287
|
+
messages.push(
|
|
288
|
+
`Resumo: ${location} (${REVIEW_RESUME_FILE}) — ${blockers} bloqueante(s), ${attention} atenção.`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Lacuna de cobertura: arquivos do escopo que ninguém declarou ter revisado.
|
|
292
|
+
const gaps = reports.flatMap((report) => {
|
|
293
|
+
const expected = classification.watchdogs[report.watchdogId] ?? [];
|
|
294
|
+
const missing = unaccountedFiles(
|
|
295
|
+
report.reviewedFiles,
|
|
296
|
+
report.skipped,
|
|
297
|
+
expected,
|
|
298
|
+
);
|
|
299
|
+
if (missing.length === 0) return [];
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
id: report.watchdogId,
|
|
303
|
+
missing,
|
|
304
|
+
coverage: coverageOf(report.reviewedFiles, expected),
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
});
|
|
308
|
+
if (gaps.length > 0) {
|
|
309
|
+
const total = gaps.reduce((sum, gap) => sum + gap.missing.length, 0);
|
|
310
|
+
const detail = gaps
|
|
311
|
+
.map((gap) => {
|
|
312
|
+
const sample = gap.missing.slice(0, 3).join(", ");
|
|
313
|
+
const rest =
|
|
314
|
+
gap.missing.length > 3 ? ` (+${gap.missing.length - 3})` : "";
|
|
315
|
+
return `${nameOf(gap.id)} (${gap.coverage.reviewed}/${gap.coverage.expected}): ${sample}${rest}`;
|
|
316
|
+
})
|
|
317
|
+
.join("; ");
|
|
318
|
+
messages.push(
|
|
319
|
+
`Cobertura: ${total} arquivo(s) sem revisão declarada — ${detail}.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// O staging já cumpriu o papel (briefs/findings/contexto estão no storage).
|
|
324
|
+
ctx.work.clean();
|
|
325
|
+
|
|
326
|
+
return { reportPath: location, blockers, attention, messages };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Grava o manifesto da run (delegado ao `ManifestWriter`). */
|
|
330
|
+
private writeManifest(
|
|
331
|
+
classification: RunClassification,
|
|
332
|
+
activeIds: string[],
|
|
333
|
+
pending: string[],
|
|
334
|
+
jev: JevCandidatesFile,
|
|
335
|
+
ast?: ManifestAst,
|
|
336
|
+
ai?: ManifestAi,
|
|
337
|
+
business?: ManifestBusiness,
|
|
338
|
+
): void {
|
|
339
|
+
new ManifestWriter(
|
|
340
|
+
this.ctx.storage,
|
|
341
|
+
this.ctx.review,
|
|
342
|
+
this.ctx.work,
|
|
343
|
+
new ProjectConfig(this.config),
|
|
344
|
+
this.opts,
|
|
345
|
+
).write(classification, activeIds, pending, jev, ast, ai, business);
|
|
346
|
+
}
|
|
347
|
+
}
|