@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/ai/types.ts
CHANGED
|
@@ -50,6 +50,8 @@ export type ModelResponse<T> = {
|
|
|
50
50
|
output: T;
|
|
51
51
|
usage: AiUsage;
|
|
52
52
|
finishReason: string;
|
|
53
|
+
/** Avisos do provider já normalizados em texto (ex.: modo de compatibilidade). */
|
|
54
|
+
warnings?: string[];
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
/** Provider de modelo (adaptador do AI SDK ou fake nos testes). */
|
|
@@ -17,17 +17,36 @@ export type AiUsageEntry = {
|
|
|
17
17
|
usage: AiUsage;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/** Chamada que falhou (timeout, truncamento, schema) — não consome tokens. */
|
|
21
|
+
export type AiFailureEntry = {
|
|
22
|
+
feature: string;
|
|
23
|
+
model: string;
|
|
24
|
+
at: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
finishReason?: string;
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Uso agregado + tentativas/falhas (vai para `prepare-report.json`). */
|
|
31
|
+
export type AiUsageSummary = AiUsage & {
|
|
32
|
+
attempts: number;
|
|
33
|
+
failures: AiFailureEntry[];
|
|
34
|
+
};
|
|
35
|
+
|
|
20
36
|
/** Formato persistido em `ai-usage.json`. */
|
|
21
37
|
export type AiUsageFile = {
|
|
22
38
|
provider: string;
|
|
23
39
|
model: string;
|
|
24
40
|
total: AiUsage;
|
|
41
|
+
attempts: number;
|
|
25
42
|
entries: AiUsageEntry[];
|
|
43
|
+
failures: AiFailureEntry[];
|
|
26
44
|
};
|
|
27
45
|
|
|
28
46
|
/** Acumulador de uso (em memória) com persistência opcional. */
|
|
29
47
|
export class AiUsageRecorder {
|
|
30
48
|
private readonly entries: AiUsageEntry[] = [];
|
|
49
|
+
private readonly failed: AiFailureEntry[] = [];
|
|
31
50
|
private readonly provider: string;
|
|
32
51
|
private readonly model: string;
|
|
33
52
|
|
|
@@ -41,7 +60,23 @@ export class AiUsageRecorder {
|
|
|
41
60
|
this.entries.push({ feature, model, at: systemClock(), usage: { ...usage } });
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
/**
|
|
63
|
+
/** Registra uma tentativa que falhou (fail-open: nunca derruba a revisão). */
|
|
64
|
+
recordFailure(
|
|
65
|
+
feature: string,
|
|
66
|
+
model: string,
|
|
67
|
+
detail: { durationMs: number; error: string; finishReason?: string },
|
|
68
|
+
): void {
|
|
69
|
+
this.failed.push({
|
|
70
|
+
feature,
|
|
71
|
+
model,
|
|
72
|
+
at: systemClock(),
|
|
73
|
+
durationMs: detail.durationMs,
|
|
74
|
+
error: detail.error,
|
|
75
|
+
...(detail.finishReason ? { finishReason: detail.finishReason } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Total agregado de todas as chamadas concluídas. */
|
|
45
80
|
get total(): AiUsage {
|
|
46
81
|
return this.entries.reduce(
|
|
47
82
|
(acc, entry) => addUsage(acc, entry.usage),
|
|
@@ -49,13 +84,30 @@ export class AiUsageRecorder {
|
|
|
49
84
|
);
|
|
50
85
|
}
|
|
51
86
|
|
|
87
|
+
/** Tentativas (sucesso + falha) — a métrica honesta de custo/latência. */
|
|
88
|
+
get attempts(): number {
|
|
89
|
+
return this.entries.length + this.failed.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Falhas registradas, na ordem em que ocorreram. */
|
|
93
|
+
get failures(): readonly AiFailureEntry[] {
|
|
94
|
+
return this.failed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resumo para o `prepare-report.json`. */
|
|
98
|
+
get summary(): AiUsageSummary {
|
|
99
|
+
return { ...this.total, attempts: this.attempts, failures: [...this.failed] };
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
/** Snapshot serializável. */
|
|
53
103
|
toJSON(): AiUsageFile {
|
|
54
104
|
return {
|
|
55
105
|
provider: this.provider,
|
|
56
106
|
model: this.model,
|
|
57
107
|
total: this.total,
|
|
108
|
+
attempts: this.attempts,
|
|
58
109
|
entries: [...this.entries],
|
|
110
|
+
failures: [...this.failed],
|
|
59
111
|
};
|
|
60
112
|
}
|
|
61
113
|
|
package/src/core/brief.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AstDelta } from "./ast/types";
|
|
9
|
+
import { GENERAL_WATCHDOG_ID } from "./builtins";
|
|
10
|
+
import { type BusinessSummary, renderBusinessLines } from "./business-summary";
|
|
9
11
|
import { formatRanges, formatSymbols } from "./changed-scope";
|
|
10
12
|
import {
|
|
11
13
|
type Rule,
|
|
@@ -19,15 +21,49 @@ import {
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Procedimento de revisão — compartilhado por todos os watchdogs.
|
|
24
|
+
*
|
|
25
|
+
* O eixo é intenção → escopo → rastreio → prova: as regras continuam sendo o
|
|
26
|
+
* piso, mas a revisão começa entendendo o que a mudança deve entregar.
|
|
22
27
|
*/
|
|
23
28
|
export const PROCEDURE: readonly string[] = [
|
|
24
|
-
"1. **
|
|
25
|
-
"2. **
|
|
26
|
-
"3. **
|
|
27
|
-
"4. **
|
|
28
|
-
|
|
29
|
+
"1. **RECONSTRUCT** — leia o recorte do *Modelo de intenção (negócio)* e diga para si mesmo o que a mudança DEVE entregar (fluxos, invariantes, contratos). Sem modelo, derive do contexto da PR.",
|
|
30
|
+
"2. **SCOPE** — os *Trechos alterados (diff)* já estão no brief: **não releia o arquivo inteiro** para vê-los. Trecho com o marcador `… [trecho truncado]` pode ser aberto, uma vez, para o restante.",
|
|
31
|
+
"3. **TRACE** — siga cada símbolo alterado até seus chamadores, adapters e tipos compartilhados **dentro do seu domínio** (use os importadores do AST), **dentro do orçamento de exploração** (seção acima). Priorize os arquivos de maior churn.",
|
|
32
|
+
"4. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas, **dentro do orçamento**. Pare quando tiver a prova: repetir busca não aumenta certeza.",
|
|
33
|
+
"5. **PROVE** — para cada suspeita, descreva gatilho → comportamento observado → invariante/regra violada. Sem prova, não é achado. Trecho com o marcador de truncamento só sustenta achado depois de abrir o arquivo para ver o restante.",
|
|
34
|
+
'6. **CONCLUDE** — grave o JSON no caminho de output. `reviewedFiles` é um subconjunto de `## Arquivos do seu domínio` (só o que você revisou; **não** liste chamadores/importadores nem arquivos de outros domínios) e o que ficou de fora entra em `skipped` ("budget", "truncated" ou "fora do fluxo"). Máximo de 20 achados: priorize severidade e agrupe ocorrências idênticas. `line` dentro das faixas alteradas. Sem achados → `"findings": []` (arquivo obrigatório). Não escreva markdown livre.',
|
|
29
35
|
];
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Orçamento de exploração por brief.
|
|
39
|
+
*
|
|
40
|
+
* O diff já cobre o essencial; o resto é para chamador/contexto de um símbolo
|
|
41
|
+
* citado. Sem teto, o subagente entrava em loop de leitura e cada brief levava
|
|
42
|
+
* minutos — o `general` era o pior caso (PR inteira).
|
|
43
|
+
*/
|
|
44
|
+
const EXPLORATION_BUDGET = {
|
|
45
|
+
general: { reads: 10, searches: 10 },
|
|
46
|
+
domain: { reads: 6, searches: 6 },
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Seção com o teto de leituras/buscas do brief (visível para o modelo). */
|
|
50
|
+
function explorationSection(args: WatchdogRenderArgs): string[] {
|
|
51
|
+
const budget =
|
|
52
|
+
args.watchdogId === GENERAL_WATCHDOG_ID
|
|
53
|
+
? EXPLORATION_BUDGET.general
|
|
54
|
+
: EXPLORATION_BUDGET.domain;
|
|
55
|
+
return [
|
|
56
|
+
"## Orçamento de exploração",
|
|
57
|
+
"",
|
|
58
|
+
`> O diff já está no brief. Limite **neste brief**: ${budget.reads} leitura(s) de arquivo`,
|
|
59
|
+
`> e ${budget.searches} busca(s) (grep).`,
|
|
60
|
+
"> Abra arquivo só para chamador/contexto de um símbolo citado nos trechos.",
|
|
61
|
+
"> Se a prova não estiver no brief nem nesse orçamento, **não é achado** —",
|
|
62
|
+
"> registre a dúvida no campo `action` do finding mais próximo ou descarte.",
|
|
63
|
+
"",
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
/** Limites do contexto injetado em cada brief (o resumo final é maior). */
|
|
32
68
|
const BRIEF_CONTEXT_LIMITS = {
|
|
33
69
|
objective: 200,
|
|
@@ -157,18 +193,18 @@ function fileItems(args: WatchdogRenderArgs): string[] {
|
|
|
157
193
|
}
|
|
158
194
|
|
|
159
195
|
/**
|
|
160
|
-
* Arquivos do domínio fora do
|
|
196
|
+
* Arquivos do domínio fora do recorte (orçamento ou fluxo): o watchdog NÃO
|
|
161
197
|
* deve lê-los, mas precisa registrá-los em `skipped` — cobertura honesta.
|
|
162
198
|
*/
|
|
163
199
|
function overflowSection(args: WatchdogRenderArgs): string[] {
|
|
164
200
|
const overflow = args.overflow ?? [];
|
|
165
201
|
if (overflow.length === 0) return [];
|
|
166
202
|
return [
|
|
167
|
-
"## Fora do
|
|
203
|
+
"## Fora do recorte (NÃO revisar)",
|
|
168
204
|
"",
|
|
169
|
-
`> ${overflow.length} arquivo(s) do
|
|
170
|
-
"> (
|
|
171
|
-
|
|
205
|
+
`> ${overflow.length} arquivo(s) do escopo ficaram fora do recorte`,
|
|
206
|
+
"> (orçamento ou prioridade de fluxo). Não os leia: registre-os em `skipped`",
|
|
207
|
+
"> com o motivo (`budget` ou `fora do fluxo`) para que a cobertura fique honesta.",
|
|
172
208
|
"",
|
|
173
209
|
...overflow.map(codeItem),
|
|
174
210
|
"",
|
|
@@ -227,6 +263,143 @@ function deltaLines(delta: AstDelta): string[] {
|
|
|
227
263
|
return lines;
|
|
228
264
|
}
|
|
229
265
|
|
|
266
|
+
/** Rótulo curto da fonte do modelo de intenção. */
|
|
267
|
+
const BUSINESS_SOURCE_LABEL: Record<BusinessSummary["source"], string> = {
|
|
268
|
+
ai: "IA",
|
|
269
|
+
reviewer: "revisor de fluxo",
|
|
270
|
+
deterministic: "determinístico",
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Recorte do modelo de intenção: só fluxos que tocam os arquivos do watchdog.
|
|
275
|
+
*
|
|
276
|
+
* O modelo é global; injetá-lo inteiro em cada brief fazia o subagente
|
|
277
|
+
* perseguir fluxos de outros domínios (mais leituras e buscas sem achado).
|
|
278
|
+
*/
|
|
279
|
+
function scopeBusinessToFiles(
|
|
280
|
+
business: BusinessSummary,
|
|
281
|
+
files: readonly string[],
|
|
282
|
+
): BusinessSummary {
|
|
283
|
+
if (files.length === 0) return business;
|
|
284
|
+
// O modelo vem da IA e pode usar `./`, `\` ou caminho absoluto: normaliza
|
|
285
|
+
// antes de comparar para não perder um fluxo do próprio domínio.
|
|
286
|
+
const normalize = (value: string): string =>
|
|
287
|
+
value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
288
|
+
const own = new Set(files.map(normalize));
|
|
289
|
+
const flows = business.flows.filter(
|
|
290
|
+
(flow) =>
|
|
291
|
+
!flow.files ||
|
|
292
|
+
flow.files.length === 0 ||
|
|
293
|
+
flow.files.some((file) => own.has(normalize(file))),
|
|
294
|
+
);
|
|
295
|
+
if (flows.length === business.flows.length) return business;
|
|
296
|
+
return { ...business, flows };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Modelo de intenção da PR — o contrato esperado antes de julgar o código.
|
|
301
|
+
*
|
|
302
|
+
* Injetado em todos os briefs; o revisor `general` recebe o modelo completo, os
|
|
303
|
+
* watchdogs de domínio recebem só o recorte dos seus arquivos.
|
|
304
|
+
*/
|
|
305
|
+
function businessSection(args: WatchdogRenderArgs): string[] {
|
|
306
|
+
const business = args.business;
|
|
307
|
+
if (!business) return [];
|
|
308
|
+
const isGeneral = args.watchdogId === GENERAL_WATCHDOG_ID;
|
|
309
|
+
const scoped = isGeneral
|
|
310
|
+
? business
|
|
311
|
+
: { ...scopeBusinessToFiles(business, args.files), openQuestions: [] };
|
|
312
|
+
const omitted = business.flows.length - scoped.flows.length;
|
|
313
|
+
// O caminho canônico só interessa a quem reconstrói o modelo (`general`):
|
|
314
|
+
// nos demais, citá-lo induzia uma leitura extra do JSON completo.
|
|
315
|
+
const canonical =
|
|
316
|
+
args.businessSummaryPath && (isGeneral || args.flowReview)
|
|
317
|
+
? ` · \`${args.businessSummaryPath}\` (versão canônica no staging)`
|
|
318
|
+
: "";
|
|
319
|
+
const lines = [
|
|
320
|
+
"## Modelo de intenção (negócio)",
|
|
321
|
+
"",
|
|
322
|
+
`> Fonte: ${BUSINESS_SOURCE_LABEL[business.source]}${canonical}. Use para saber o que a mudança **deve** entregar; não é achado.`,
|
|
323
|
+
"",
|
|
324
|
+
];
|
|
325
|
+
if (!isGeneral) {
|
|
326
|
+
lines.push(
|
|
327
|
+
"> Recorte do seu domínio: fluxos/arquivos de outros domínios pertencem a outros watchdogs — não os investigue.",
|
|
328
|
+
...(omitted > 0
|
|
329
|
+
? [`> ${omitted} fluxo(s) fora do seu domínio foram omitidos.`]
|
|
330
|
+
: []),
|
|
331
|
+
"",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const rendered = renderBusinessLines(
|
|
335
|
+
scoped,
|
|
336
|
+
isGeneral
|
|
337
|
+
? { flows: 12, invariants: 12, contracts: 12, states: 8 }
|
|
338
|
+
: { flows: 3, invariants: 6, contracts: 2, states: 2 },
|
|
339
|
+
);
|
|
340
|
+
while (rendered[0] === "") rendered.shift();
|
|
341
|
+
lines.push(...rendered, "");
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Trechos alterados (diff) — todos os watchdogs recebem o patch do seu domínio. */
|
|
346
|
+
function snippetsSection(args: WatchdogRenderArgs): string[] {
|
|
347
|
+
const snippets = args.snippets ?? [];
|
|
348
|
+
if (snippets.length === 0) return [];
|
|
349
|
+
const lines = [
|
|
350
|
+
"## Trechos alterados (diff)",
|
|
351
|
+
"",
|
|
352
|
+
"> Trechos já extraídos do diff (`-U0`, só linhas alteradas). Eles bastam",
|
|
353
|
+
"> para achar violações: **não abra os arquivos apenas para reler o que já",
|
|
354
|
+
"> está aqui**. Use o arquivo só para contexto adjacente ou chamadores.",
|
|
355
|
+
"",
|
|
356
|
+
];
|
|
357
|
+
for (const item of snippets) {
|
|
358
|
+
lines.push(
|
|
359
|
+
`### \`${item.file}\``,
|
|
360
|
+
"",
|
|
361
|
+
"```diff",
|
|
362
|
+
item.snippet.trimEnd(),
|
|
363
|
+
"```",
|
|
364
|
+
"",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
return lines;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Obrigação de output do revisor de fluxo: ele reconstrói e persiste o modelo
|
|
372
|
+
* de intenção (ou devolve `businessSummary` no harness local).
|
|
373
|
+
*/
|
|
374
|
+
function flowReviewSection(args: WatchdogRenderArgs): string[] {
|
|
375
|
+
if (!args.flowReview) return [];
|
|
376
|
+
const output = args.businessSummaryPath ?? "business-summary.json";
|
|
377
|
+
return [
|
|
378
|
+
"## Obrigação de output (fluxo)",
|
|
379
|
+
"",
|
|
380
|
+
"1. **Recomece pelo modelo**: leia o diff e reconstrua intenção, fluxos, invariantes,",
|
|
381
|
+
" contratos e estados. Se o modelo existente estiver incompleto, corrija-o.",
|
|
382
|
+
`2. **Persista** o modelo em \`${output}\` com \`source: "reviewer"\`.`,
|
|
383
|
+
" No harness local (maki), devolva o mesmo objeto em `businessSummary` no JSON.",
|
|
384
|
+
"3. **Prove cada gap**: fora de regra, preencha `scenario` (gatilho concreto),",
|
|
385
|
+
" `impact` (consequência observável) e `evidence` (trecho). Sem `scenario` + `evidence`,",
|
|
386
|
+
" a severidade máxima é `attention`.",
|
|
387
|
+
"",
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Linha de verificação (string legada ou objeto ancorado). */
|
|
392
|
+
function checkItem(
|
|
393
|
+
item:
|
|
394
|
+
| string
|
|
395
|
+
| { statement: string; file?: string; symbol?: string; invariant?: string },
|
|
396
|
+
): string {
|
|
397
|
+
if (typeof item === "string") return `- ${item}`;
|
|
398
|
+
const anchor = [item.file, item.symbol].filter(Boolean).join(" · ");
|
|
399
|
+
const invariant = item.invariant ? ` (invariante: ${item.invariant})` : "";
|
|
400
|
+
return `- ${item.statement}${anchor ? ` — \`${anchor}\`` : ""}${invariant}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
230
403
|
/**
|
|
231
404
|
* Plano de revisão sugerido por IA — complementa (nunca substitui) as regras.
|
|
232
405
|
*/
|
|
@@ -271,17 +444,72 @@ function aiPlanSection(args: WatchdogRenderArgs): string[] {
|
|
|
271
444
|
}
|
|
272
445
|
if (logic.length > 0) {
|
|
273
446
|
lines.push("**Consistência lógica:**", "");
|
|
274
|
-
for (const item of logic.slice(0,
|
|
447
|
+
for (const item of logic.slice(0, 4)) lines.push(checkItem(item));
|
|
275
448
|
lines.push("");
|
|
276
449
|
}
|
|
277
450
|
if (domain.length > 0) {
|
|
278
451
|
lines.push("**Domínio de negócio:**", "");
|
|
279
|
-
for (const item of domain.slice(0,
|
|
452
|
+
for (const item of domain.slice(0, 4)) lines.push(checkItem(item));
|
|
280
453
|
lines.push("");
|
|
281
454
|
}
|
|
282
455
|
return lines;
|
|
283
456
|
}
|
|
284
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Micro-alterações iguais em muitos arquivos: mostra o padrão dominante e os
|
|
460
|
+
* outliers em vez de N diffs de uma linha.
|
|
461
|
+
*/
|
|
462
|
+
function propagationSection(args: WatchdogRenderArgs): string[] {
|
|
463
|
+
const cluster = args.propagation;
|
|
464
|
+
if (!cluster || cluster.files.length === 0) return [];
|
|
465
|
+
const sample = cluster.files.slice(0, 5).map(inlineCode).join(", ");
|
|
466
|
+
const rest = cluster.files.length > 5 ? ` (+${cluster.files.length - 5})` : "";
|
|
467
|
+
const lines = [
|
|
468
|
+
"## Propagação mecânica",
|
|
469
|
+
"",
|
|
470
|
+
`> ${cluster.files.length} arquivo(s) com a mesma micro-alteração (≤3 linhas).`,
|
|
471
|
+
"> Revise o padrão e os outliers; **não abra** os demais — registre-os em",
|
|
472
|
+
"> `skipped` com o motivo `propagação mecânica`.",
|
|
473
|
+
"",
|
|
474
|
+
"```diff",
|
|
475
|
+
...cluster.lines.map((line) => `+${line}`),
|
|
476
|
+
"```",
|
|
477
|
+
"",
|
|
478
|
+
`Arquivos: ${sample}${rest}`,
|
|
479
|
+
"",
|
|
480
|
+
];
|
|
481
|
+
if (cluster.outliers.length > 0) {
|
|
482
|
+
lines.push("**Outliers (revisar):**", "");
|
|
483
|
+
for (const outlier of cluster.outliers) {
|
|
484
|
+
lines.push(
|
|
485
|
+
`- ${inlineCode(outlier.file)}: \`${shorten(outlier.lines.join(" ⏎ "), 160)}\``,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
lines.push("");
|
|
489
|
+
}
|
|
490
|
+
return lines;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Contratos alterados cujos importadores não aparecem no diff. */
|
|
494
|
+
function propagationGapSection(args: WatchdogRenderArgs): string[] {
|
|
495
|
+
const gaps = args.propagationGaps ?? [];
|
|
496
|
+
if (gaps.length === 0) return [];
|
|
497
|
+
const lines = [
|
|
498
|
+
"## Possível propagação incompleta",
|
|
499
|
+
"",
|
|
500
|
+
"> Contratos alterados cujos importadores **não** aparecem no diff. Confirme",
|
|
501
|
+
"> se cada um precisa acompanhar a mudança antes de concluir.",
|
|
502
|
+
"",
|
|
503
|
+
];
|
|
504
|
+
for (const gap of gaps) {
|
|
505
|
+
lines.push(
|
|
506
|
+
`- ${inlineCode(gap.file)} → ${gap.importers.map(inlineCode).join(", ")}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
lines.push("");
|
|
510
|
+
return lines;
|
|
511
|
+
}
|
|
512
|
+
|
|
285
513
|
/**
|
|
286
514
|
* Estrutura/delta dos arquivos do watchdog (AST). É um mapa para orientar a
|
|
287
515
|
* leitura — o watchdog confirma no código e não trata como achado.
|
|
@@ -346,6 +574,11 @@ export function defaultRenderBrief(
|
|
|
346
574
|
|
|
347
575
|
if (args.context) lines.push(...renderBriefContext(args.context));
|
|
348
576
|
|
|
577
|
+
lines.push(...businessSection(args));
|
|
578
|
+
lines.push(...snippetsSection(args));
|
|
579
|
+
lines.push(...propagationSection(args));
|
|
580
|
+
lines.push(...propagationGapSection(args));
|
|
581
|
+
lines.push(...flowReviewSection(args));
|
|
349
582
|
lines.push(...aiPlanSection(args));
|
|
350
583
|
lines.push(...astSection(args));
|
|
351
584
|
lines.push(...optionalList("Prioridade de leitura", def.reviewFocus));
|
|
@@ -378,6 +611,7 @@ export function defaultRenderBrief(
|
|
|
378
611
|
|
|
379
612
|
lines.push(...deletedSection(args, def));
|
|
380
613
|
lines.push(...optionalList("Não carregar", def.exclude));
|
|
614
|
+
lines.push(...explorationSection(args));
|
|
381
615
|
lines.push("## Procedimento", "", ...PROCEDURE);
|
|
382
616
|
|
|
383
617
|
return lines;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watchdogs embutidos do motor.
|
|
3
|
+
*
|
|
4
|
+
* Diferente dos watchdogs do consumidor, estes existem por padrão e não podem
|
|
5
|
+
* ser desligados: são parte do fluxo de revisão. O `general` audita a lógica e o
|
|
6
|
+
* domínio do diff inteiro (visão de fluxo, não de arquivo), usando o modelo de
|
|
7
|
+
* intenção (`business-summary.json`) como contrato esperado.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { defineWatchdog, type WatchdogDefinition } from "./types";
|
|
11
|
+
|
|
12
|
+
/** Id reservado do revisor de lógica/domínio. */
|
|
13
|
+
export const GENERAL_WATCHDOG_ID = "general";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Revisor de fluxo: recebe todos os arquivos alterados e procura gaps de
|
|
17
|
+
* negócio (fluxo incompleto, capacidade não honrada, credencial divergente,
|
|
18
|
+
* estado sem tratamento, identificador trocado, idempotência ausente).
|
|
19
|
+
*
|
|
20
|
+
* As regras são o **piso**; findings fora delas usam `Lógica:`/`Domínio:`
|
|
21
|
+
* (`allowEmergentFindings`). Severidade `blocking` só é aceita com cenário +
|
|
22
|
+
* evidência (normalizado no `report-builder`).
|
|
23
|
+
*/
|
|
24
|
+
export const generalWatchdog: WatchdogDefinition = defineWatchdog({
|
|
25
|
+
id: GENERAL_WATCHDOG_ID,
|
|
26
|
+
name: "Lógica e domínio (fluxo)",
|
|
27
|
+
description:
|
|
28
|
+
"Audita o fluxo alterado ponta a ponta: intenção, invariantes, estados, capacidades e credenciais.",
|
|
29
|
+
scope: "Todos os arquivos alterados (visão de fluxo, independente de domínio)",
|
|
30
|
+
classify: [],
|
|
31
|
+
fallback: false,
|
|
32
|
+
allowEmergentFindings: true,
|
|
33
|
+
// Deletados não têm conteúdo para ler: entram como breaking change no brief.
|
|
34
|
+
breakingChange: true,
|
|
35
|
+
reviewFocus: [
|
|
36
|
+
"Reconstrua o modelo de intenção (business-summary.json) antes de julgar o código.",
|
|
37
|
+
"Rastreie cada símbolo alterado até seus chamadores, adapters e tipos compartilhados.",
|
|
38
|
+
"Verifique paridade de capacidade: feature declarada, método exposto, evento/webhook e lista de métodos.",
|
|
39
|
+
"Verifique propagação de credencial/conta/contexto em toda a cadeia (auth, token, split, subconta).",
|
|
40
|
+
"Percorra o ciclo de vida completo dos fluxos tocados: criação, renovação, cancelamento, retry e erro.",
|
|
41
|
+
"Confirme o identificador persistido contra a entidade que o contrato espera.",
|
|
42
|
+
"Só reporte gap com gatilho concreto, comportamento observado e invariante/regra violada.",
|
|
43
|
+
],
|
|
44
|
+
rules: [
|
|
45
|
+
{
|
|
46
|
+
severity: "attention",
|
|
47
|
+
text: "Promover: todo fluxo alterado tem caminho completo (criação, renovação, cancelamento) e o caminho de erro chega a um estado tratado.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
severity: "attention",
|
|
51
|
+
text: "Promover: toda capacidade declarada (feature, método, evento, webhook) existe e é honrada em todos os lados do contrato.",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
severity: "attention",
|
|
55
|
+
text: "Promover: a credencial, conta e contexto de execução são os mesmos em toda a cadeia alterada (autenticação, token, split, subconta).",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
severity: "attention",
|
|
59
|
+
text: "Promover: transições de estado e efeitos colaterais do domínio são explícitos, ordenados e idempotentes.",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
severity: "suggestion",
|
|
63
|
+
text: "Promover: o identificador persistido corresponde à entidade que o contrato espera (cobrança, autorização, pedido, assinatura).",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
contextFiles: [],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Watchdogs embutidos, por id. */
|
|
70
|
+
export const BUILTIN_WATCHDOGS: Readonly<Record<string, WatchdogDefinition>> = {
|
|
71
|
+
[GENERAL_WATCHDOG_ID]: generalWatchdog,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Definição embutida de um id (ou null). */
|
|
75
|
+
export function builtinWatchdog(id: string): WatchdogDefinition | null {
|
|
76
|
+
return BUILTIN_WATCHDOGS[id] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mescla a definição do consumidor com o piso embutido do `general`.
|
|
81
|
+
*
|
|
82
|
+
* O consumidor pode ajustar nome/escopo/docs/regras, mas não pode remover a
|
|
83
|
+
* auditoria de lógica nem os findings emergentes.
|
|
84
|
+
*/
|
|
85
|
+
export function mergeGeneralWatchdog(
|
|
86
|
+
configured: WatchdogDefinition | undefined,
|
|
87
|
+
): WatchdogDefinition {
|
|
88
|
+
if (!configured || configured.id !== GENERAL_WATCHDOG_ID) return generalWatchdog;
|
|
89
|
+
const rules = [...configured.rules];
|
|
90
|
+
for (const rule of generalWatchdog.rules) {
|
|
91
|
+
if (!rules.some((item) => item.text === rule.text)) rules.push(rule);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
...generalWatchdog,
|
|
95
|
+
name: configured.name || generalWatchdog.name,
|
|
96
|
+
description: configured.description || generalWatchdog.description,
|
|
97
|
+
scope: configured.scope || generalWatchdog.scope,
|
|
98
|
+
reviewFocus: [
|
|
99
|
+
...(configured.reviewFocus ?? []),
|
|
100
|
+
...(generalWatchdog.reviewFocus ?? []),
|
|
101
|
+
],
|
|
102
|
+
exclude: configured.exclude,
|
|
103
|
+
contextFiles: configured.contextFiles,
|
|
104
|
+
rules,
|
|
105
|
+
allowEmergentFindings: true,
|
|
106
|
+
fallback: false,
|
|
107
|
+
};
|
|
108
|
+
}
|