@payconductor/houndsman 0.13.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 +6 -1
- 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/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +20 -3
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- 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 +8 -0
- 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/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 +27 -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 +31 -3
- package/src/core/defaults.ts +50 -4
- 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 +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +90 -0
- package/src/core/hunt/index.ts +2 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +13 -1
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- 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 +8 -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 +16 -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 +29 -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 +18 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +145 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
- package/src/tests/ai-core.test.ts +46 -0
- 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/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-maki.test.ts +58 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- 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
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import { AiClient } from "../core/ai";
|
|
7
7
|
import { AiBriefEnricher } from "../core/ai/features/brief-enricher";
|
|
8
|
+
import { AiBusinessAnalyzer } from "../core/ai/features/business-analyzer";
|
|
9
|
+
import { AiBusinessSummarizer } from "../core/ai/features/business-summarizer";
|
|
8
10
|
import { AiClassifier } from "../core/ai/features/classifier";
|
|
9
11
|
import { AiContextSummarizer } from "../core/ai/features/context-summarizer";
|
|
10
12
|
import { AiPreAnalyzer } from "../core/ai/features/pre-analyzer";
|
|
11
13
|
import { AiTechnicalSummarizer } from "../core/ai/features/technical-summarizer";
|
|
12
14
|
import { AiVerifier } from "../core/ai/features/verifier";
|
|
13
15
|
import { briefInstructions, briefPrompt } from "../core/ai/prompts/brief";
|
|
16
|
+
import { businessInstructions } from "../core/ai/prompts/business";
|
|
14
17
|
import { contextInstructions } from "../core/ai/prompts/context";
|
|
15
18
|
import { technicalInstructions } from "../core/ai/prompts/technical";
|
|
16
19
|
import { renderBrief } from "../core/brief";
|
|
@@ -116,8 +119,8 @@ describe("AiBriefEnricher", () => {
|
|
|
116
119
|
focus: [],
|
|
117
120
|
questions: [],
|
|
118
121
|
risks: [],
|
|
119
|
-
logic: ["nome vs comportamento"],
|
|
120
|
-
domain: ["transição de estado"],
|
|
122
|
+
logic: [{ statement: "nome vs comportamento" }],
|
|
123
|
+
domain: [{ statement: "transição de estado" }],
|
|
121
124
|
},
|
|
122
125
|
});
|
|
123
126
|
expect(markdown).toContain("Consistência lógica");
|
|
@@ -221,10 +224,98 @@ describe("AiPreAnalyzer", () => {
|
|
|
221
224
|
});
|
|
222
225
|
|
|
223
226
|
describe("idioma dos resumos", () => {
|
|
224
|
-
test("prompts de contexto e pontos técnicos exigem pt-BR", () => {
|
|
225
|
-
for (const instructions of [
|
|
227
|
+
test("prompts de contexto, intenção e pontos técnicos exigem pt-BR", () => {
|
|
228
|
+
for (const instructions of [
|
|
229
|
+
contextInstructions(),
|
|
230
|
+
businessInstructions(),
|
|
231
|
+
technicalInstructions(),
|
|
232
|
+
]) {
|
|
226
233
|
expect(instructions).toContain("português do Brasil");
|
|
227
234
|
expect(instructions).toContain("pt-BR");
|
|
228
235
|
}
|
|
229
236
|
});
|
|
230
237
|
});
|
|
238
|
+
|
|
239
|
+
describe("AiBusinessSummarizer", () => {
|
|
240
|
+
const input = {
|
|
241
|
+
classification: {
|
|
242
|
+
files: { "src/alpha/a.ts": { status: "modified" as const } },
|
|
243
|
+
churn: { "src/alpha/a.ts": { additions: 3, deletions: 1 } },
|
|
244
|
+
watchdogs: { alpha: ["src/alpha/a.ts"] },
|
|
245
|
+
},
|
|
246
|
+
domains: [{ id: "alpha", name: "Alpha" }],
|
|
247
|
+
headSha: "abc123",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
test("devolve o modelo com source ai", async () => {
|
|
251
|
+
const provider = new FakeModelProvider(() => ({
|
|
252
|
+
intent: "Entregar o split",
|
|
253
|
+
flows: [{ name: "Renovação", trigger: "job", files: ["src/alpha/a.ts"] }],
|
|
254
|
+
invariants: ["split sempre presente"],
|
|
255
|
+
contracts: [{ name: "AutomaticPix", sides: ["asaas", "processor"] }],
|
|
256
|
+
states: [],
|
|
257
|
+
}));
|
|
258
|
+
const summarizer = new AiBusinessSummarizer(new AiClient({ project, provider }));
|
|
259
|
+
const summary = await summarizer.summarize(input);
|
|
260
|
+
expect(summary?.source).toBe("ai");
|
|
261
|
+
expect(summary?.headSha).toBe("abc123");
|
|
262
|
+
expect(summary?.flows[0]?.name).toBe("Renovação");
|
|
263
|
+
expect(summary?.invariants).toEqual(["split sempre presente"]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("sem IA → null (fallback determinístico no chamador)", async () => {
|
|
267
|
+
const summarizer = new AiBusinessSummarizer(
|
|
268
|
+
new AiClient({ project, provider: null }),
|
|
269
|
+
);
|
|
270
|
+
expect(summarizer.enabled).toBe(false);
|
|
271
|
+
expect(await summarizer.summarize(input)).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("AiBusinessAnalyzer", () => {
|
|
276
|
+
test("gera candidatos do general e descarta arquivo fora do escopo", async () => {
|
|
277
|
+
const provider = new FakeModelProvider(() => ({
|
|
278
|
+
hypotheses: [
|
|
279
|
+
{
|
|
280
|
+
file: "src/alpha/a.ts",
|
|
281
|
+
kind: "domain",
|
|
282
|
+
statement: "renovação perde o split",
|
|
283
|
+
confidence: 0.9,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
file: "src/outro.ts",
|
|
287
|
+
kind: "logic",
|
|
288
|
+
statement: "fora do escopo",
|
|
289
|
+
confidence: 0.9,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
file: "src/alpha/a.ts",
|
|
293
|
+
kind: "logic",
|
|
294
|
+
statement: "confiança baixa",
|
|
295
|
+
confidence: 0.2,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
}));
|
|
299
|
+
const analyzer = new AiBusinessAnalyzer(new AiClient({ project, provider }));
|
|
300
|
+
const candidates = await analyzer.analyze({
|
|
301
|
+
business: {
|
|
302
|
+
source: "deterministic",
|
|
303
|
+
flows: [],
|
|
304
|
+
invariants: [],
|
|
305
|
+
contracts: [],
|
|
306
|
+
states: [],
|
|
307
|
+
},
|
|
308
|
+
payload: {
|
|
309
|
+
classification: {
|
|
310
|
+
files: { "src/alpha/a.ts": { status: "modified" } },
|
|
311
|
+
churn: {},
|
|
312
|
+
watchdogs: {},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
expect(candidates).toHaveLength(1);
|
|
317
|
+
expect(candidates[0]?.watchdogId).toBe("general");
|
|
318
|
+
expect(candidates[0]?.rule).toBe("Domínio: renovação perde o split");
|
|
319
|
+
expect(candidates[0]?.severity).toBe("attention");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -29,12 +29,6 @@ import { defineHoundsmanConfig } from "../core/config";
|
|
|
29
29
|
import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
|
|
30
30
|
import { displayPath } from "../core/paths";
|
|
31
31
|
import { RESUME_MAX_CHARS } from "../core/renderers/resume-renderer";
|
|
32
|
-
import {
|
|
33
|
-
type MergedFinding,
|
|
34
|
-
renderDomainReport,
|
|
35
|
-
renderReport,
|
|
36
|
-
type WatchdogReport,
|
|
37
|
-
} from "../core/report-builder";
|
|
38
32
|
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
39
33
|
import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
40
34
|
import { defineWatchdog } from "../core/types";
|
|
@@ -157,112 +151,6 @@ describe("baseline — brief", () => {
|
|
|
157
151
|
});
|
|
158
152
|
});
|
|
159
153
|
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// Relatório
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
const reports: WatchdogReport[] = [
|
|
165
|
-
{
|
|
166
|
-
watchdogId: "alpha",
|
|
167
|
-
reviewedFiles: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
168
|
-
skipped: ["src/alpha/c.ts"],
|
|
169
|
-
findings: [
|
|
170
|
-
{
|
|
171
|
-
rule: "Nunca usar `any`.",
|
|
172
|
-
severity: "blocking",
|
|
173
|
-
file: "src/alpha/a.ts",
|
|
174
|
-
line: 1,
|
|
175
|
-
message: "any detectado",
|
|
176
|
-
action: "tipar",
|
|
177
|
-
evidence: "export const a: any = 1;",
|
|
178
|
-
id: "id-alpha",
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
rule: "Promover: validar a entrada.",
|
|
182
|
-
severity: "attention",
|
|
183
|
-
file: "src/alpha/b.ts",
|
|
184
|
-
line: 2,
|
|
185
|
-
message: "sem validação",
|
|
186
|
-
action: "adicionar schema",
|
|
187
|
-
id: "id-beta",
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
watchdogId: "beta",
|
|
193
|
-
reviewedFiles: ["src/beta/x.test.ts"],
|
|
194
|
-
skipped: [],
|
|
195
|
-
findings: [],
|
|
196
|
-
},
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
const merged: MergedFinding[] = [
|
|
200
|
-
{ ...reports[0].findings[0], watchdogs: ["alpha"] },
|
|
201
|
-
{ ...reports[0].findings[1], watchdogs: ["alpha", "beta"] },
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
const reportContext = {
|
|
205
|
-
available: true,
|
|
206
|
-
source: {
|
|
207
|
-
number: 42,
|
|
208
|
-
title: "Entrega X",
|
|
209
|
-
url: "https://example.test/pull/42",
|
|
210
|
-
head: "feature/x",
|
|
211
|
-
base: "main",
|
|
212
|
-
},
|
|
213
|
-
objective: "Entregar X",
|
|
214
|
-
motivation: "Reduzir Y",
|
|
215
|
-
changeSummary: ["Faz A", "Remove B"],
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
describe("baseline — relatório", () => {
|
|
219
|
-
test("renderReport consolidado", () => {
|
|
220
|
-
const md = renderReport(
|
|
221
|
-
"feature/x",
|
|
222
|
-
reports,
|
|
223
|
-
merged,
|
|
224
|
-
{ classified: { alpha: ["src/alpha/a.ts", "src/alpha/b.ts"], beta: [] } },
|
|
225
|
-
reportContext,
|
|
226
|
-
[
|
|
227
|
-
{
|
|
228
|
-
watchdogId: "alpha",
|
|
229
|
-
rule: "Nunca usar `any`.",
|
|
230
|
-
severity: "blocking",
|
|
231
|
-
file: "src/alpha/a.ts",
|
|
232
|
-
confidence: 0.82,
|
|
233
|
-
questionKey: "alpha::rule-0",
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
(id) => (id === "alpha" ? "Alpha" : "Beta"),
|
|
237
|
-
[
|
|
238
|
-
{
|
|
239
|
-
id: "alpha",
|
|
240
|
-
name: "Alpha",
|
|
241
|
-
file: "review.alpha.md",
|
|
242
|
-
findings: 2,
|
|
243
|
-
reviewedFiles: 2,
|
|
244
|
-
expectedFiles: 2,
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
expect(normalizeArtifacts(md)).toMatchSnapshot();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("renderDomainReport isolado", () => {
|
|
253
|
-
const md = renderDomainReport(
|
|
254
|
-
"feature/x",
|
|
255
|
-
reports[0],
|
|
256
|
-
merged,
|
|
257
|
-
2,
|
|
258
|
-
reportContext,
|
|
259
|
-
(id) => (id === "alpha" ? "Alpha" : "Beta"),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
expect(normalizeArtifacts(md)).toMatchSnapshot();
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
154
|
// ---------------------------------------------------------------------------
|
|
267
155
|
// Camada gerada pelo setup/update
|
|
268
156
|
// ---------------------------------------------------------------------------
|
|
@@ -321,7 +209,7 @@ describe("baseline — camada gerada", () => {
|
|
|
321
209
|
// ---------------------------------------------------------------------------
|
|
322
210
|
|
|
323
211
|
describe("baseline — artefatos de prepare/finalize", () => {
|
|
324
|
-
test("gera classification, brief, manifest,
|
|
212
|
+
test("gera classification, brief, manifest, resumo e state", async () => {
|
|
325
213
|
const cwd = mkdtempSync(join(tmpdir(), "houndsman-baseline-run-"));
|
|
326
214
|
const originalCwd = process.cwd();
|
|
327
215
|
const originalHome = process.env.HOUNDSMAN_HOME;
|
|
@@ -388,9 +276,17 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
388
276
|
.replaceAll(homeDisplay, "<cwd>"),
|
|
389
277
|
);
|
|
390
278
|
|
|
391
|
-
const prepared = await prepare(
|
|
279
|
+
const prepared = await prepare(ctx, config, opts);
|
|
392
280
|
expect(normalize(prepared.messages.join("\n"))).toMatchSnapshot();
|
|
393
281
|
|
|
282
|
+
// Instrumentação do prepare: etapas medidas sem alterar o manifesto.
|
|
283
|
+
const prepareReport = storage.readArtifact(review.id, "prepare-report.json");
|
|
284
|
+
expect(prepareReport?.kind).toBe("prepare");
|
|
285
|
+
const report = JSON.parse(prepareReport?.content ?? "{}") as {
|
|
286
|
+
stages?: { stage: string; durationMs: number }[];
|
|
287
|
+
};
|
|
288
|
+
expect((report.stages ?? []).length).toBeGreaterThan(0);
|
|
289
|
+
|
|
394
290
|
for (const name of ["classification.json", "manifest.json"]) {
|
|
395
291
|
const artifact = storage.readArtifact(review.id, name);
|
|
396
292
|
expect(normalize(artifact?.content ?? "")).toMatchSnapshot(name);
|
|
@@ -425,19 +321,32 @@ describe("baseline — artefatos de prepare/finalize", () => {
|
|
|
425
321
|
2,
|
|
426
322
|
)}\n`,
|
|
427
323
|
);
|
|
324
|
+
writeFileSync(
|
|
325
|
+
ctx.work.findingFile("general"),
|
|
326
|
+
`${JSON.stringify(
|
|
327
|
+
{
|
|
328
|
+
watchdogId: "general",
|
|
329
|
+
reviewedFiles: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
330
|
+
skipped: [],
|
|
331
|
+
findings: [],
|
|
332
|
+
},
|
|
333
|
+
null,
|
|
334
|
+
2,
|
|
335
|
+
)}\n`,
|
|
336
|
+
);
|
|
428
337
|
|
|
429
|
-
const finalized = finalize(
|
|
338
|
+
const finalized = finalize(ctx, config, opts);
|
|
430
339
|
expect(normalize(finalized.messages.join("\n"))).toMatchSnapshot();
|
|
431
340
|
|
|
432
|
-
|
|
433
|
-
const artifact = storage.readArtifact(review.id, name);
|
|
434
|
-
expect(normalize(artifact?.content ?? "")).toMatchSnapshot(name);
|
|
435
|
-
}
|
|
341
|
+
// Único artefato humano: o resumo final (negócio, técnico, cobertura, conclusão).
|
|
436
342
|
const resume = storage.readArtifact(review.id, "review-resume.md");
|
|
437
343
|
expect(resume?.kind).toBe("resume");
|
|
438
344
|
expect(resume?.content).toContain("## Pontos técnicos");
|
|
439
345
|
expect(resume?.content).toContain("Remoções");
|
|
346
|
+
expect(resume?.content).toContain("## Cobertura por domínio");
|
|
347
|
+
expect(resume?.content).toContain("## Conclusão");
|
|
440
348
|
expect((resume?.content ?? "").length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
349
|
+
expect(normalize(resume?.content ?? "")).toMatchSnapshot("review-resume.md");
|
|
441
350
|
expect(
|
|
442
351
|
normalize(JSON.stringify(storage.readFindings(review.id), null, 2)),
|
|
443
352
|
).toMatchSnapshot("state.json");
|
|
@@ -500,7 +409,7 @@ const REQUIRED_EXPORTS = [
|
|
|
500
409
|
"ProjectScaffolder",
|
|
501
410
|
"FixBatchBuilder",
|
|
502
411
|
"BriefRenderer",
|
|
503
|
-
"
|
|
412
|
+
"renderReviewResume",
|
|
504
413
|
"ManifestWriter",
|
|
505
414
|
"applyFindingUpdate",
|
|
506
415
|
"assetPath",
|
|
@@ -537,7 +446,6 @@ const REQUIRED_EXPORTS = [
|
|
|
537
446
|
"reanchor",
|
|
538
447
|
"relFromRoot",
|
|
539
448
|
"renderBrief",
|
|
540
|
-
"renderDomainReport",
|
|
541
449
|
"reorder",
|
|
542
450
|
"resolveConfigPath",
|
|
543
451
|
"resolveJevConcurrency",
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief — recorte do modelo de intenção e injeção dos hunks.
|
|
3
|
+
*
|
|
4
|
+
* Depois do `business-brief.md`, cada watchdog recebia o modelo inteiro e
|
|
5
|
+
* perseguia fluxos de outros domínios; o brief também não trazia o código
|
|
6
|
+
* alterado, forçando a releitura dos arquivos. Estes testes congelam o recorte.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { renderBrief } from "../core/brief";
|
|
11
|
+
import { generalWatchdog } from "../core/builtins";
|
|
12
|
+
import type { BusinessSummary } from "../core/business-summary";
|
|
13
|
+
import {
|
|
14
|
+
buildBriefSnippets,
|
|
15
|
+
generalFlowFiles,
|
|
16
|
+
shouldSliceGeneral,
|
|
17
|
+
truncatePatch,
|
|
18
|
+
} from "../core/engine/prepare-pipeline";
|
|
19
|
+
import { buildPrepareReport } from "../core/prepare-report";
|
|
20
|
+
import type { WatchdogRenderArgs } from "../core/types";
|
|
21
|
+
import { alphaWatchdog } from "./fixtures/fake-project";
|
|
22
|
+
|
|
23
|
+
/** Modelo com um fluxo do domínio alpha e outro de fora. */
|
|
24
|
+
const business: BusinessSummary = {
|
|
25
|
+
source: "ai",
|
|
26
|
+
intent: "Renovar assinaturas sem perder a credencial da conta.",
|
|
27
|
+
flows: [
|
|
28
|
+
{
|
|
29
|
+
name: "Alpha flow",
|
|
30
|
+
trigger: "webhook de renovação",
|
|
31
|
+
expected: "assinatura renovada",
|
|
32
|
+
files: ["src/alpha/a.ts"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Beta flow",
|
|
36
|
+
trigger: "job de cobrança",
|
|
37
|
+
expected: "cobrança criada",
|
|
38
|
+
files: ["src/beta/b.ts"],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
invariants: ["A conta do pagador é preservada."],
|
|
42
|
+
contracts: [],
|
|
43
|
+
states: [],
|
|
44
|
+
openQuestions: ["Pergunta global?"],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const baseArgs: WatchdogRenderArgs = {
|
|
48
|
+
watchdogId: "alpha",
|
|
49
|
+
files: ["src/alpha/a.ts"],
|
|
50
|
+
statuses: { "src/alpha/a.ts": { status: "modified" } },
|
|
51
|
+
deleted: [],
|
|
52
|
+
branch: "feature/x",
|
|
53
|
+
outputPath: "findings/alpha.json",
|
|
54
|
+
outputFormatPath: "OUTPUT-FORMAT.md",
|
|
55
|
+
docs: [],
|
|
56
|
+
business,
|
|
57
|
+
businessSummaryPath: "/home/.houndsman/work/review/business-summary.json",
|
|
58
|
+
snippets: [
|
|
59
|
+
{
|
|
60
|
+
file: "src/alpha/a.ts",
|
|
61
|
+
snippet: "@@ -1 +1 @@\n-const a = 1;\n+const a: any = 1;",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("brief — recorte do modelo de intenção", () => {
|
|
67
|
+
test("watchdog de domínio vê só os fluxos dos seus arquivos", () => {
|
|
68
|
+
const markdown = renderBrief(alphaWatchdog, baseArgs);
|
|
69
|
+
expect(markdown).toContain("Alpha flow");
|
|
70
|
+
expect(markdown).not.toContain("Beta flow");
|
|
71
|
+
expect(markdown).toContain("1 fluxo(s) fora do seu domínio foram omitidos");
|
|
72
|
+
expect(markdown).toContain("não os investigue");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("domínio não recebe o caminho canônico (evita leitura extra)", () => {
|
|
76
|
+
const markdown = renderBrief(alphaWatchdog, baseArgs);
|
|
77
|
+
expect(markdown).not.toContain("business-summary.json");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("general recebe o modelo completo e o caminho canônico", () => {
|
|
81
|
+
const markdown = renderBrief(generalWatchdog, {
|
|
82
|
+
...baseArgs,
|
|
83
|
+
watchdogId: generalWatchdog.id,
|
|
84
|
+
files: ["src/alpha/a.ts", "src/beta/b.ts"],
|
|
85
|
+
});
|
|
86
|
+
expect(markdown).toContain("Alpha flow");
|
|
87
|
+
expect(markdown).toContain("Beta flow");
|
|
88
|
+
expect(markdown).toContain("Pergunta global?");
|
|
89
|
+
expect(markdown).toContain("business-summary.json");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("brief — trechos alterados", () => {
|
|
94
|
+
test("injeta o diff e proíbe reler o arquivo inteiro", () => {
|
|
95
|
+
const markdown = renderBrief(alphaWatchdog, baseArgs);
|
|
96
|
+
expect(markdown).toContain("## Trechos alterados (diff)");
|
|
97
|
+
expect(markdown).toContain("const a: any = 1;");
|
|
98
|
+
expect(markdown).toContain("não abra os arquivos");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("sem trechos, a seção não aparece", () => {
|
|
102
|
+
const markdown = renderBrief(alphaWatchdog, {
|
|
103
|
+
...baseArgs,
|
|
104
|
+
snippets: undefined,
|
|
105
|
+
});
|
|
106
|
+
expect(markdown).not.toContain("## Trechos alterados (diff)");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("renderiza propagação mecânica e lacunas de importadores", () => {
|
|
110
|
+
const markdown = renderBrief(alphaWatchdog, {
|
|
111
|
+
...baseArgs,
|
|
112
|
+
propagation: {
|
|
113
|
+
lines: ["await pay(charge, true);"],
|
|
114
|
+
files: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
115
|
+
outliers: [
|
|
116
|
+
{ file: "src/alpha/c.ts", lines: ["await pay(charge, false);"] },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
propagationGaps: [{ file: "src/alpha/a.ts", importers: ["src/beta/b.ts"] }],
|
|
120
|
+
});
|
|
121
|
+
expect(markdown).toContain("## Propagação mecânica");
|
|
122
|
+
expect(markdown).toContain("await pay(charge, true)");
|
|
123
|
+
expect(markdown).toContain("Outliers (revisar)");
|
|
124
|
+
expect(markdown).toContain("## Possível propagação incompleta");
|
|
125
|
+
expect(markdown).toContain("src/beta/b.ts");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("prepare-report", () => {
|
|
130
|
+
test("preserva etapas, uso de IA e falhas", () => {
|
|
131
|
+
const report = buildPrepareReport({
|
|
132
|
+
startedAt: "2026-10-25T00:00:00.000Z",
|
|
133
|
+
durationMs: 1_500,
|
|
134
|
+
stages: [
|
|
135
|
+
{ stage: "business", durationMs: 900 },
|
|
136
|
+
{ stage: "technical", durationMs: 300 },
|
|
137
|
+
],
|
|
138
|
+
ai: {
|
|
139
|
+
inputTokens: 10,
|
|
140
|
+
outputTokens: 5,
|
|
141
|
+
totalTokens: 15,
|
|
142
|
+
calls: 1,
|
|
143
|
+
attempts: 2,
|
|
144
|
+
failures: [
|
|
145
|
+
{
|
|
146
|
+
feature: "brief",
|
|
147
|
+
model: "deepseek-v4-pro",
|
|
148
|
+
durationMs: 12,
|
|
149
|
+
error: "Resposta truncada",
|
|
150
|
+
finishReason: "length",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
expect(report.version).toBe(1);
|
|
156
|
+
expect(report.durationMs).toBe(1_500);
|
|
157
|
+
expect(report.stages.map((item) => item.stage)).toEqual([
|
|
158
|
+
"business",
|
|
159
|
+
"technical",
|
|
160
|
+
]);
|
|
161
|
+
expect(report.ai?.calls).toBe(1);
|
|
162
|
+
expect(report.ai?.attempts).toBe(2);
|
|
163
|
+
expect(report.ai?.failures[0]?.finishReason).toBe("length");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("prepare — orçamento de snippets", () => {
|
|
168
|
+
const patch = (chars: number) =>
|
|
169
|
+
Array.from(
|
|
170
|
+
{ length: Math.ceil(chars / 40) },
|
|
171
|
+
() => "+linha de codigo alterada",
|
|
172
|
+
).join("\n");
|
|
173
|
+
|
|
174
|
+
test("distribui o teto entre todos os arquivos (não só nos primeiros)", () => {
|
|
175
|
+
const files = Array.from({ length: 10 }, (_, i) => `src/f${i}.ts`);
|
|
176
|
+
const snippets = buildBriefSnippets(
|
|
177
|
+
files,
|
|
178
|
+
new Map(files.map((file) => [file, patch(2_000)])),
|
|
179
|
+
{ total: 10_000, perFile: 4_000 },
|
|
180
|
+
);
|
|
181
|
+
expect(snippets).toHaveLength(10);
|
|
182
|
+
// Todos começam com uma parte igual; o excedente vai para os primeiros.
|
|
183
|
+
expect(snippets[0]?.snippet.length).toBeGreaterThan(
|
|
184
|
+
snippets[9]?.snippet.length ?? 0,
|
|
185
|
+
);
|
|
186
|
+
expect(snippets.every((item) => item.snippet.length > 0)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("corta em linha inteira e sinaliza o trecho truncado", () => {
|
|
190
|
+
const cut = truncatePatch(patch(1_000), 120);
|
|
191
|
+
expect(cut.length).toBeLessThanOrEqual(120);
|
|
192
|
+
expect(cut).toContain("[trecho truncado");
|
|
193
|
+
expect(cut.startsWith("+linha de codigo")).toBe(true);
|
|
194
|
+
expect(truncatePatch("curto", 100)).toBe("curto");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("general — recorte por fluxo", () => {
|
|
199
|
+
const flowBusiness: BusinessSummary = {
|
|
200
|
+
source: "ai",
|
|
201
|
+
flows: [
|
|
202
|
+
{
|
|
203
|
+
name: "Renovação",
|
|
204
|
+
trigger: "job",
|
|
205
|
+
expected: "assinatura renovada",
|
|
206
|
+
files: ["./src/alpha/a.ts"],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
invariants: [],
|
|
210
|
+
contracts: [],
|
|
211
|
+
states: [],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
test("dispara por nº de arquivos ou por linhas alteradas", () => {
|
|
215
|
+
const manyFiles = Array.from({ length: 41 }, (_, i) => `src/f${i}.ts`);
|
|
216
|
+
expect(shouldSliceGeneral({ watchdogs: { general: manyFiles }, churn: {} })).toBe(
|
|
217
|
+
true,
|
|
218
|
+
);
|
|
219
|
+
expect(
|
|
220
|
+
shouldSliceGeneral({
|
|
221
|
+
watchdogs: { general: ["src/a.ts"] },
|
|
222
|
+
churn: { "src/a.ts": { additions: 1_200, deletions: 900 } },
|
|
223
|
+
}),
|
|
224
|
+
).toBe(true);
|
|
225
|
+
expect(
|
|
226
|
+
shouldSliceGeneral({
|
|
227
|
+
watchdogs: { general: ["src/a.ts"] },
|
|
228
|
+
churn: { "src/a.ts": { additions: 10, deletions: 5 } },
|
|
229
|
+
}),
|
|
230
|
+
).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("prioriza os arquivos dos fluxos e limita o recorte", () => {
|
|
234
|
+
const files = [
|
|
235
|
+
"src/alpha/a.ts",
|
|
236
|
+
...Array.from({ length: 19 }, (_, i) => `src/f${i}.ts`),
|
|
237
|
+
];
|
|
238
|
+
const result = generalFlowFiles(
|
|
239
|
+
{
|
|
240
|
+
watchdogs: { general: files },
|
|
241
|
+
churn: { "src/f19.ts": { additions: 100, deletions: 0 } },
|
|
242
|
+
},
|
|
243
|
+
flowBusiness,
|
|
244
|
+
3,
|
|
245
|
+
);
|
|
246
|
+
expect(result.selected).toHaveLength(3);
|
|
247
|
+
expect(result.selected[0]).toBe("src/alpha/a.ts");
|
|
248
|
+
expect(result.overflow).toHaveLength(17);
|
|
249
|
+
});
|
|
250
|
+
});
|