@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/cli/index.ts
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Comandos:
|
|
14
14
|
* (default) Passo 1 — preparar (classificação + briefs + manifesto)
|
|
15
|
-
* --finalize Passo 3 — consolidar os findings
|
|
15
|
+
* --finalize Passo 3 — consolidar os findings no resumo final
|
|
16
16
|
* context Passo 0 — coletar o contexto da PR via `gh`
|
|
17
17
|
* findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
|
|
18
|
+
* hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
|
|
18
19
|
* status [ping] Mostra o status da run; `ping` renova o heartbeat da execução
|
|
19
20
|
* server Sobe a UI local de triagem (Bun.serve)
|
|
20
21
|
* setup Cria a configuração inicial do projeto (config + skills)
|
|
@@ -30,14 +31,16 @@
|
|
|
30
31
|
import type { HoundsmanConfig } from "../core/config";
|
|
31
32
|
import { loadConfig } from "../core/config-loader";
|
|
32
33
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
33
|
-
import { logger } from "../core/logging";
|
|
34
|
+
import { formatDuration, logger } from "../core/logging";
|
|
34
35
|
import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
|
|
35
36
|
import { ArgParser } from "./arg-parser";
|
|
36
37
|
import type { CliContext } from "./commands/command";
|
|
38
|
+
import { CompareCommand } from "./commands/compare-command";
|
|
37
39
|
import { ContextCommand } from "./commands/context-command";
|
|
38
40
|
import { DoctorCommand } from "./commands/doctor-command";
|
|
39
41
|
import { FindingsCommand } from "./commands/findings-command";
|
|
40
42
|
import { HelpCommand } from "./commands/help-command";
|
|
43
|
+
import { HuntCommand } from "./commands/hunt-command";
|
|
41
44
|
import { CommandRegistry } from "./commands/registry";
|
|
42
45
|
import { ReviewCommand } from "./commands/review-command";
|
|
43
46
|
import { ServerCommand } from "./commands/server-command";
|
|
@@ -57,7 +60,9 @@ function buildRegistry(): CommandRegistry {
|
|
|
57
60
|
return new CommandRegistry()
|
|
58
61
|
.register(new ReviewCommand(), { fallback: true })
|
|
59
62
|
.register(new HelpCommand())
|
|
63
|
+
.register(new HuntCommand())
|
|
60
64
|
.register(new ContextCommand())
|
|
65
|
+
.register(new CompareCommand())
|
|
61
66
|
.register(new FindingsCommand())
|
|
62
67
|
.register(new StatusCommand())
|
|
63
68
|
.register(new WatchdogCommand())
|
|
@@ -88,14 +93,26 @@ export async function main(): Promise<void> {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
let loaded: Awaited<ReturnType<typeof loadConfig>> | null = null;
|
|
96
|
+
// `--json` é contrato de máquina: nada de progresso no stdout.
|
|
97
|
+
const silentProgress = parser.has("--json");
|
|
91
98
|
const config = async (): Promise<HoundsmanConfig> => {
|
|
92
99
|
loaded ??= await (async () => {
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
if (!silentProgress) logger.step("Carregando a configuração do projeto…");
|
|
93
102
|
// Rodando dentro de um worktree, a camada gerada (`.houndsman/core`)
|
|
94
103
|
// não existe (é ignorada pelo git). Repõe a partir do clone principal
|
|
95
104
|
// antes de importar a config — sem isso os watchdogs não resolvem
|
|
96
105
|
// `../core/types`. No clone principal é no-op.
|
|
97
106
|
syncProjectLayerForRoot(process.cwd());
|
|
98
|
-
|
|
107
|
+
const result = await loadConfig({ configPath: parser.value("--config") });
|
|
108
|
+
if (!silentProgress) {
|
|
109
|
+
logger.success(
|
|
110
|
+
`Configuração: ${result.config.projectName} · ` +
|
|
111
|
+
`${result.config.watchdogIds.length} watchdog(s) ` +
|
|
112
|
+
`(${formatDuration(Date.now() - started)})`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
99
116
|
})();
|
|
100
117
|
return loaded.config;
|
|
101
118
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progresso do CLI por etapa do prepare (apresentação).
|
|
3
|
+
*
|
|
4
|
+
* O core emite eventos (`PrepareProgressEvent`); aqui eles viram spinner/linha
|
|
5
|
+
* com a duração de cada fase — o `hunt` começa a dar sinal de vida antes do
|
|
6
|
+
* primeiro `Discovery:`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatDuration, type LoggerLike, ParallelPhaseReporter } from "../core/logging";
|
|
10
|
+
import type { PrepareProgress, PrepareStage } from "../core/orchestrator";
|
|
11
|
+
|
|
12
|
+
/** Texto de cada etapa (início no gerúndio, fim no particípio). */
|
|
13
|
+
export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: string }> = {
|
|
14
|
+
diff: { start: "Lendo o diff da revisão", done: "Diff lido" },
|
|
15
|
+
ast: { start: "Analisando a AST dos arquivos alterados", done: "AST analisada" },
|
|
16
|
+
classify: {
|
|
17
|
+
start: "Classificando os arquivos por watchdog",
|
|
18
|
+
done: "Classificação concluída",
|
|
19
|
+
},
|
|
20
|
+
impact: { start: "Medindo o impacto nos importadores", done: "Impacto calculado" },
|
|
21
|
+
jev: { start: "Pré-analisando o diff (Jev/IA)", done: "Pré-análise concluída" },
|
|
22
|
+
context: { start: "Resumindo o contexto da PR", done: "Contexto resumido" },
|
|
23
|
+
business: {
|
|
24
|
+
start: "Reconstruindo a intenção da PR",
|
|
25
|
+
done: "Intenção reconstruída",
|
|
26
|
+
},
|
|
27
|
+
technical: {
|
|
28
|
+
start: "Consolidando os pontos técnicos",
|
|
29
|
+
done: "Pontos técnicos prontos",
|
|
30
|
+
},
|
|
31
|
+
"ai-plans": { start: "Planejando os briefs com IA", done: "Planos de brief prontos" },
|
|
32
|
+
briefs: { start: "Gerando os briefs", done: "Briefs gerados" },
|
|
33
|
+
manifest: { start: "Gravando o manifesto da revisão", done: "Manifesto gravado" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Cria o callback de progresso do prepare ligado ao logger do CLI. */
|
|
37
|
+
export function prepareProgress(logger: LoggerLike): PrepareProgress {
|
|
38
|
+
// O prepare roda etapas em paralelo; o reporter aceita fases concorrentes.
|
|
39
|
+
const reporter = new ParallelPhaseReporter(logger);
|
|
40
|
+
return (event) => {
|
|
41
|
+
const text = PREPARE_STAGE_TEXT[event.stage];
|
|
42
|
+
if (event.type === "start") reporter.begin(event.stage, text.start);
|
|
43
|
+
else reporter.end(event.stage, text.done, event.durationMs);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Log de um passo concluído com a duração (usado fora do prepare). */
|
|
48
|
+
export function stepDone(logger: LoggerLike, message: string, startedMs: number): void {
|
|
49
|
+
logger.success(`${message} (${formatDuration(Date.now() - startedMs)})`);
|
|
50
|
+
}
|
package/src/core/ai/ai-client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { AiProviderOptions } from "../config";
|
|
|
10
10
|
import type { ProjectConfig } from "../project-config";
|
|
11
11
|
import { AiConfigurationError, errorMessage } from "./errors";
|
|
12
12
|
import { ModelFactory } from "./model-factory";
|
|
13
|
+
import { LEAN_PROVIDER_OPTIONS } from "./provider-capabilities";
|
|
13
14
|
import type { AiResult, AiUsage, ModelProvider, ModelRequest } from "./types";
|
|
14
15
|
import { AiUsageBudget } from "./usage-budget";
|
|
15
16
|
import { AiUsageRecorder } from "./usage-recorder";
|
|
@@ -109,7 +110,13 @@ export class AiClient {
|
|
|
109
110
|
): Promise<AiResult<T>> {
|
|
110
111
|
return this.run(
|
|
111
112
|
feature,
|
|
112
|
-
{
|
|
113
|
+
{
|
|
114
|
+
...request,
|
|
115
|
+
outputSchema: request.outputSchema,
|
|
116
|
+
providerOptions:
|
|
117
|
+
request.providerOptions ??
|
|
118
|
+
leanProviderOptions(this.project.aiProviderOptions),
|
|
119
|
+
},
|
|
113
120
|
model,
|
|
114
121
|
);
|
|
115
122
|
}
|
|
@@ -183,6 +190,7 @@ export class AiClient {
|
|
|
183
190
|
return { ok: false, error: errorMessage(error) };
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
const startedAt = Date.now();
|
|
186
194
|
try {
|
|
187
195
|
const provider = await this.requireProvider(model);
|
|
188
196
|
const response = await provider.generate({
|
|
@@ -193,6 +201,17 @@ export class AiClient {
|
|
|
193
201
|
),
|
|
194
202
|
signal: combineSignals(request.signal, this.project.aiTimeoutMs),
|
|
195
203
|
});
|
|
204
|
+
// Objeto truncado não é resultado parcial útil: sem o JSON completo a
|
|
205
|
+
// feature inteira caía silenciosamente no fallback. Registra e falha.
|
|
206
|
+
if (request.outputSchema && response.finishReason === "length") {
|
|
207
|
+
const error = `Resposta truncada (finishReason=length; maxOutputTokens=${request.maxOutputTokens ?? "sem teto"}).`;
|
|
208
|
+
this.recorder.recordFailure(feature, provider.modelId, {
|
|
209
|
+
durationMs: Date.now() - startedAt,
|
|
210
|
+
error,
|
|
211
|
+
finishReason: response.finishReason,
|
|
212
|
+
});
|
|
213
|
+
return { ok: false, error };
|
|
214
|
+
}
|
|
196
215
|
this.budget.record(response.usage);
|
|
197
216
|
this.recorder.record(feature, provider.modelId, response.usage);
|
|
198
217
|
return {
|
|
@@ -202,7 +221,12 @@ export class AiClient {
|
|
|
202
221
|
model: provider.modelId,
|
|
203
222
|
};
|
|
204
223
|
} catch (error) {
|
|
205
|
-
|
|
224
|
+
const message = errorMessage(error);
|
|
225
|
+
this.recorder.recordFailure(feature, model ?? this.modelId, {
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
error: message,
|
|
228
|
+
});
|
|
229
|
+
return { ok: false, error: message };
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
}
|
|
@@ -221,6 +245,18 @@ function mergeProviderOptions(
|
|
|
221
245
|
return merged;
|
|
222
246
|
}
|
|
223
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Opções enxutas para objetos estruturados: thinking desligado, salvo quando o
|
|
250
|
+
* projeto já configurou `thinking` explicitamente — aí a config manda.
|
|
251
|
+
*/
|
|
252
|
+
function leanProviderOptions(
|
|
253
|
+
base: AiProviderOptions | undefined,
|
|
254
|
+
): AiProviderOptions | undefined {
|
|
255
|
+
const configured = base?.deepseek;
|
|
256
|
+
if (configured && "thinking" in configured) return undefined;
|
|
257
|
+
return LEAN_PROVIDER_OPTIONS;
|
|
258
|
+
}
|
|
259
|
+
|
|
224
260
|
/** Combina o sinal do chamador com o timeout configurado. */
|
|
225
261
|
function combineSignals(
|
|
226
262
|
signal: AbortSignal | undefined,
|
|
@@ -31,6 +31,43 @@ function numberOrZero(value: unknown): number {
|
|
|
31
31
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Silencia o emissor default de avisos do AI SDK.
|
|
36
|
+
*
|
|
37
|
+
* O default usa `process.emitWarning` — com stack trace — a cada chamada, o que
|
|
38
|
+
* polui o stderr do CLI. Os avisos continuam disponíveis no resultado da
|
|
39
|
+
* chamada (`ModelResponse.warnings`). Um chamador que queira o comportamento
|
|
40
|
+
* original basta definir `AI_SDK_LOG_WARNINGS` antes de usar o SDK.
|
|
41
|
+
*/
|
|
42
|
+
function installWarningSink(): void {
|
|
43
|
+
if (globalThis.AI_SDK_LOG_WARNINGS === undefined) {
|
|
44
|
+
globalThis.AI_SDK_LOG_WARNINGS = () => {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
installWarningSink();
|
|
49
|
+
|
|
50
|
+
/** Normaliza um aviso do provider em texto legível (puro). */
|
|
51
|
+
export function formatSdkWarning(raw: unknown): string {
|
|
52
|
+
const warning = (raw ?? {}) as Record<string, unknown>;
|
|
53
|
+
const label = [warning.type, warning.feature, warning.setting]
|
|
54
|
+
.filter((value): value is string => typeof value === "string" && value !== "")
|
|
55
|
+
.join(": ");
|
|
56
|
+
const detail =
|
|
57
|
+
typeof warning.details === "string"
|
|
58
|
+
? warning.details
|
|
59
|
+
: typeof warning.message === "string"
|
|
60
|
+
? warning.message
|
|
61
|
+
: "";
|
|
62
|
+
const head = label || "aviso";
|
|
63
|
+
return detail ? `${head} — ${detail}` : head;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lista de avisos do provider em texto (puro). */
|
|
67
|
+
export function formatSdkWarnings(raw: unknown): string[] {
|
|
68
|
+
return Array.isArray(raw) ? raw.map(formatSdkWarning) : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
34
71
|
/** Argumentos aceitos por `generateText` (inclui `providerOptions` do SDK). */
|
|
35
72
|
type GenerateTextArgs = Parameters<typeof generateText>[0];
|
|
36
73
|
|
|
@@ -86,10 +123,12 @@ export class AiSdkModelProvider implements ModelProvider {
|
|
|
86
123
|
const output = request.outputSchema
|
|
87
124
|
? (result.output as T)
|
|
88
125
|
: ((result.text ?? "") as unknown as T);
|
|
126
|
+
const warnings = formatSdkWarnings(result.warnings);
|
|
89
127
|
return {
|
|
90
128
|
output,
|
|
91
129
|
usage: mapUsage(result.usage),
|
|
92
130
|
finishReason: result.finishReason ?? "unknown",
|
|
131
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
93
132
|
};
|
|
94
133
|
} catch (error) {
|
|
95
134
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hipóteses de lógica/domínio via IA (`AiBusinessAnalyzer`).
|
|
3
|
+
*
|
|
4
|
+
* Produz **candidatos** para o revisor `general` confirmar ou refutar — nunca
|
|
5
|
+
* achados. Serve para dar cobertura a PRs maiores que o orçamento do brief.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GENERAL_WATCHDOG_ID } from "../../builtins";
|
|
9
|
+
import type { BusinessInput, BusinessSummary } from "../../business-summary";
|
|
10
|
+
import { buildBusinessPromptPayload } from "../../business-summary";
|
|
11
|
+
import type { JevCandidate } from "../../jev";
|
|
12
|
+
import type { AiClient } from "../ai-client";
|
|
13
|
+
import {
|
|
14
|
+
businessAnalysisInstructions,
|
|
15
|
+
businessAnalysisPrompt,
|
|
16
|
+
} from "../prompts/business";
|
|
17
|
+
import { BusinessHypothesesSchema, type BusinessHypothesis } from "../schemas";
|
|
18
|
+
|
|
19
|
+
/** Confiança mínima para a hipótese virar candidato. */
|
|
20
|
+
const MIN_CONFIDENCE = 0.5;
|
|
21
|
+
|
|
22
|
+
/** Gera hipóteses de fluxo para o watchdog `general`. */
|
|
23
|
+
export class AiBusinessAnalyzer {
|
|
24
|
+
constructor(private readonly client: AiClient) {}
|
|
25
|
+
|
|
26
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
27
|
+
get enabled(): boolean {
|
|
28
|
+
return this.client.available && this.client.features.business;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Candidatos validados contra os arquivos do escopo. */
|
|
32
|
+
async analyze(input: {
|
|
33
|
+
business: BusinessSummary;
|
|
34
|
+
payload: BusinessInput;
|
|
35
|
+
}): Promise<JevCandidate[]> {
|
|
36
|
+
if (!this.enabled) return [];
|
|
37
|
+
const files = new Set(Object.keys(input.payload.classification.files));
|
|
38
|
+
const result = await this.client.generateObject<{
|
|
39
|
+
hypotheses: BusinessHypothesis[];
|
|
40
|
+
}>("business-analysis", {
|
|
41
|
+
instructions: businessAnalysisInstructions(),
|
|
42
|
+
prompt: businessAnalysisPrompt(
|
|
43
|
+
input.business,
|
|
44
|
+
buildBusinessPromptPayload(input.payload),
|
|
45
|
+
),
|
|
46
|
+
outputSchema: BusinessHypothesesSchema,
|
|
47
|
+
maxOutputTokens: 1_600,
|
|
48
|
+
});
|
|
49
|
+
if (!result.ok) return [];
|
|
50
|
+
return result.output.hypotheses
|
|
51
|
+
.filter((item) => files.has(item.file) && item.confidence >= MIN_CONFIDENCE)
|
|
52
|
+
.map((item, index) => ({
|
|
53
|
+
watchdogId: GENERAL_WATCHDOG_ID,
|
|
54
|
+
rule: `${item.kind === "domain" ? "Domínio" : "Lógica"}: ${item.statement}`,
|
|
55
|
+
severity: "attention" as const,
|
|
56
|
+
file: item.file,
|
|
57
|
+
confidence: item.confidence,
|
|
58
|
+
questionKey: `business-${index}`,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo de intenção via IA (`AiBusinessSummarizer`).
|
|
3
|
+
*
|
|
4
|
+
* Fail-open: sem IA/chave/feature a feature fica desligada e o prepare mantém o
|
|
5
|
+
* modelo determinístico. A saída é validada por schema — nada de texto solto.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type BusinessInput,
|
|
10
|
+
type BusinessSummary,
|
|
11
|
+
buildBusinessPromptPayload,
|
|
12
|
+
parseBusinessSummary,
|
|
13
|
+
} from "../../business-summary";
|
|
14
|
+
import type { AiClient } from "../ai-client";
|
|
15
|
+
import { businessInstructions, businessPrompt } from "../prompts/business";
|
|
16
|
+
import { type BusinessSummaryPlan, BusinessSummarySchema } from "../schemas";
|
|
17
|
+
|
|
18
|
+
/** Reconstrói a intenção da PR a partir do diff. */
|
|
19
|
+
export class AiBusinessSummarizer {
|
|
20
|
+
constructor(private readonly client: AiClient) {}
|
|
21
|
+
|
|
22
|
+
/** `true` quando a feature está ligada e há IA disponível. */
|
|
23
|
+
get enabled(): boolean {
|
|
24
|
+
return this.client.available && this.client.features.business;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Falha-aberto: sem IA ou saída inválida → `null`. */
|
|
28
|
+
async summarize(input: BusinessInput): Promise<BusinessSummary | null> {
|
|
29
|
+
if (!this.enabled) return null;
|
|
30
|
+
const result = await this.client.generateObject<BusinessSummaryPlan>("business", {
|
|
31
|
+
instructions: businessInstructions(),
|
|
32
|
+
prompt: businessPrompt(buildBusinessPromptPayload(input)),
|
|
33
|
+
outputSchema: BusinessSummarySchema,
|
|
34
|
+
// Schema permite 12 fluxos/12 contratos: 1.400 truncava com thinking ligado.
|
|
35
|
+
maxOutputTokens: 2_000,
|
|
36
|
+
});
|
|
37
|
+
if (!result.ok) return null;
|
|
38
|
+
return parseBusinessSummary(
|
|
39
|
+
{ ...result.output, source: "ai", headSha: input.headSha },
|
|
40
|
+
"ai",
|
|
41
|
+
input.headSha,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/core/ai/index.ts
CHANGED
|
@@ -10,7 +10,12 @@ export type {
|
|
|
10
10
|
TextRequest,
|
|
11
11
|
} from "./ai-client";
|
|
12
12
|
export { AiClient } from "./ai-client";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
AiSdkModelProvider,
|
|
15
|
+
formatSdkWarning,
|
|
16
|
+
formatSdkWarnings,
|
|
17
|
+
mapUsage,
|
|
18
|
+
} from "./ai-sdk-model-provider";
|
|
14
19
|
export {
|
|
15
20
|
AiBudgetError,
|
|
16
21
|
AiConfigurationError,
|
|
@@ -18,6 +23,8 @@ export {
|
|
|
18
23
|
errorMessage,
|
|
19
24
|
} from "./errors";
|
|
20
25
|
export { AiBriefEnricher } from "./features/brief-enricher";
|
|
26
|
+
export { AiBusinessAnalyzer } from "./features/business-analyzer";
|
|
27
|
+
export { AiBusinessSummarizer } from "./features/business-summarizer";
|
|
21
28
|
export { AiClassifier } from "./features/classifier";
|
|
22
29
|
export { AiContextSummarizer } from "./features/context-summarizer";
|
|
23
30
|
export { AiPreAnalyzer } from "./features/pre-analyzer";
|
|
@@ -28,8 +35,14 @@ export {
|
|
|
28
35
|
type AiCandidate,
|
|
29
36
|
type AiClassification,
|
|
30
37
|
type AiVerdict,
|
|
38
|
+
type BriefCheck,
|
|
39
|
+
BriefCheckSchema,
|
|
31
40
|
type BriefPlan,
|
|
32
41
|
BriefPlanSchema,
|
|
42
|
+
BusinessHypothesesSchema,
|
|
43
|
+
type BusinessHypothesis,
|
|
44
|
+
type BusinessSummaryPlan,
|
|
45
|
+
BusinessSummarySchema,
|
|
33
46
|
CandidateSchema,
|
|
34
47
|
ClassificationSchema,
|
|
35
48
|
type ContextSummaryPlan,
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { AstFileBrief } from "../../ast/types";
|
|
9
|
+
import { type BusinessSummary, renderBusinessLines } from "../../business-summary";
|
|
10
|
+
import type { ChangedScope } from "../../changed-scope";
|
|
9
11
|
import type { ChurnMap, Rule, WatchdogRenderContext } from "../../types";
|
|
10
12
|
import { buildSystem } from "./base";
|
|
11
13
|
|
|
@@ -21,6 +23,12 @@ export type BriefPromptInput = {
|
|
|
21
23
|
context?: WatchdogRenderContext | null;
|
|
22
24
|
ast?: readonly AstFileBrief[];
|
|
23
25
|
docs?: readonly string[];
|
|
26
|
+
/** Modelo de intenção da PR (contrato esperado). */
|
|
27
|
+
business?: BusinessSummary | null;
|
|
28
|
+
/** Trechos alterados — a única fonte real para verificar lógica. */
|
|
29
|
+
snippets?: readonly { file: string; snippet: string }[];
|
|
30
|
+
/** Faixas revisáveis por arquivo. */
|
|
31
|
+
changedScope?: ChangedScope;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
/** Instruções de sistema do enriquecimento. */
|
|
@@ -29,11 +37,13 @@ export function briefInstructions(): string {
|
|
|
29
37
|
"Você é um revisor sênior que prepara um plano de leitura para um watchdog (revisor) especializado.",
|
|
30
38
|
[
|
|
31
39
|
"As regras fornecidas são o PISO da revisão, não o teto.",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
40
|
+
"Derive cada verificação dos trechos alterados fornecidos — não apenas do nome do arquivo.",
|
|
41
|
+
"Toda verificação cita arquivo e, quando possível, símbolo; sem âncora, não sugira.",
|
|
42
|
+
"Além das regras, procure riscos de consistência lógica: nome/contrato vs comportamento (ex.: `fazA` que faz `fazB`), fluxos incompletos, estados inválidos, off-by-one e bordas.",
|
|
43
|
+
"Procure também riscos do domínio de negócio: transições de estado, idempotência, arredondamento, timezone, autorização, paridade de capacidade e invariantes implícitas.",
|
|
44
|
+
"Sugira foco de leitura (até 5), perguntas (até 3), riscos (até 3), verificações de lógica (até 4) e de domínio (até 4).",
|
|
35
45
|
"Sugira verificações, nunca afirme violação; não crie regra nova nem invente arquivo, linha ou achado.",
|
|
36
|
-
"Se uma suspeita não couber em nenhuma regra, coloque-a em `logic`/`domain` como
|
|
46
|
+
"Se uma suspeita não couber em nenhuma regra, coloque-a em `logic`/`domain` como verificação ancorada.",
|
|
37
47
|
"Não repita o texto literal das regras; o brief já as contém.",
|
|
38
48
|
],
|
|
39
49
|
);
|
|
@@ -62,6 +72,15 @@ export function briefPrompt(input: BriefPromptInput): string {
|
|
|
62
72
|
lines.push(`Motivação: ${input.context.motivation}`);
|
|
63
73
|
for (const item of input.context.changeSummary ?? []) lines.push(`- ${item}`);
|
|
64
74
|
}
|
|
75
|
+
if (input.business) {
|
|
76
|
+
const business = renderBusinessLines(input.business, {
|
|
77
|
+
flows: 8,
|
|
78
|
+
invariants: 8,
|
|
79
|
+
contracts: 8,
|
|
80
|
+
states: 4,
|
|
81
|
+
});
|
|
82
|
+
if (business.length > 0) lines.push("", "## Modelo de intenção", ...business);
|
|
83
|
+
}
|
|
65
84
|
if (input.ast && input.ast.length > 0) {
|
|
66
85
|
lines.push("", "## Estrutura (AST)");
|
|
67
86
|
for (const entry of input.ast) {
|
|
@@ -71,15 +90,22 @@ export function briefPrompt(input: BriefPromptInput): string {
|
|
|
71
90
|
);
|
|
72
91
|
}
|
|
73
92
|
}
|
|
93
|
+
if (input.snippets && input.snippets.length > 0) {
|
|
94
|
+
lines.push("", "## Trechos alterados (diff)");
|
|
95
|
+
for (const item of input.snippets.slice(0, 12)) {
|
|
96
|
+
lines.push(`### ${item.file}`, "```diff", item.snippet.trimEnd(), "```");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
74
99
|
if (input.docs && input.docs.length > 0) {
|
|
75
100
|
lines.push("", "## Docs disponíveis", ...input.docs.map((doc) => `- ${doc}`));
|
|
76
101
|
}
|
|
77
102
|
lines.push(
|
|
78
103
|
"",
|
|
79
104
|
"## Revisão além das regras",
|
|
80
|
-
"As regras são o piso. Aponte verificações de
|
|
81
|
-
"
|
|
82
|
-
"
|
|
105
|
+
"As regras são o piso. Aponte verificações de lógica e domínio ancoradas nos",
|
|
106
|
+
"trechos alterados (nome vs comportamento, estados, transições, idempotência,",
|
|
107
|
+
"paridade de capacidade, credencial, bordas). Sugira verificações; não afirme",
|
|
108
|
+
"violações.",
|
|
83
109
|
"",
|
|
84
110
|
"Produza o plano de revisão em JSON (focus, questions, risks, logic, domain).",
|
|
85
111
|
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt do modelo de intenção (`AiBusinessSummarizer`) e das hipóteses
|
|
3
|
+
* (`AiBusinessAnalyzer`).
|
|
4
|
+
*
|
|
5
|
+
* Os dois derivam **do diff**, nunca da prosa da PR. O primeiro descreve o
|
|
6
|
+
* contrato esperado; o segundo aponta suspeitas para o revisor `general`
|
|
7
|
+
* confirmar ou refutar (nunca viram achado sozinhas).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { buildSystem, clip, OUTPUT_LANGUAGE_RULE } from "./base";
|
|
11
|
+
|
|
12
|
+
/** Instruções do resumo de negócio. */
|
|
13
|
+
export function businessInstructions(): string {
|
|
14
|
+
return buildSystem(
|
|
15
|
+
"Você reconstrói a intenção de uma Pull Request a partir do diff real.",
|
|
16
|
+
[
|
|
17
|
+
OUTPUT_LANGUAGE_RULE,
|
|
18
|
+
"Descreva o comportamento que a PR promete entregar, não a estrutura do código.",
|
|
19
|
+
"Derive do diff fornecido: fluxos, invariantes, contratos e estados.",
|
|
20
|
+
"Paridade de capacidade é prioridade: se um lado declara feature/método/evento, verifique se o outro lado o honra.",
|
|
21
|
+
"Fluxos de ciclo de vida contam: criação, renovação, cancelamento, retry, webhook e erro.",
|
|
22
|
+
"Registre o que não estiver claro em `openQuestions`; não invente.",
|
|
23
|
+
"NÃO produza achados, severidade, arquivo:linha ou recomendação de correção.",
|
|
24
|
+
"Seja específico e curto: nomes de fluxo/contrato citam símbolos e arquivos reais.",
|
|
25
|
+
],
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Prompt do usuário com o payload compactado. */
|
|
30
|
+
export function businessPrompt(payload: unknown): string {
|
|
31
|
+
return [
|
|
32
|
+
"Reconstrua a intenção da PR abaixo.",
|
|
33
|
+
"",
|
|
34
|
+
"```json",
|
|
35
|
+
clip(JSON.stringify(payload), 24_000),
|
|
36
|
+
"```",
|
|
37
|
+
"",
|
|
38
|
+
"Produza o modelo de intenção em JSON.",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Instruções das hipóteses de lógica/domínio. */
|
|
43
|
+
export function businessAnalysisInstructions(): string {
|
|
44
|
+
return buildSystem(
|
|
45
|
+
"Você aponta suspeitas de gap de lógica/domínio para um revisor confirmar.",
|
|
46
|
+
[
|
|
47
|
+
OUTPUT_LANGUAGE_RULE,
|
|
48
|
+
"Use o modelo de intenção e os trechos alterados fornecidos.",
|
|
49
|
+
"Cada hipótese precisa citar um arquivo do escopo e uma afirmação concreta.",
|
|
50
|
+
"Prefira gaps de fluxo: caminho incompleto, capacidade declarada e não honrada, credencial/contexto divergente, estado sem tratamento, identificador trocado.",
|
|
51
|
+
"Confiança calibrada (0–1); abaixo de 0.5 não vale reportar.",
|
|
52
|
+
'`kind: "domain"` para invariantes/fluxo de negócio; `kind: "logic"` para nome/contrato vs comportamento.',
|
|
53
|
+
"NÃO invente arquivos e NÃO produza severidade — isto não é um achado.",
|
|
54
|
+
"No máximo 12 hipóteses.",
|
|
55
|
+
],
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Prompt do usuário com intenção + trechos alterados. */
|
|
60
|
+
export function businessAnalysisPrompt(business: unknown, payload: unknown): string {
|
|
61
|
+
return [
|
|
62
|
+
"## Modelo de intenção",
|
|
63
|
+
"",
|
|
64
|
+
"```json",
|
|
65
|
+
clip(JSON.stringify(business), 8_000),
|
|
66
|
+
"```",
|
|
67
|
+
"",
|
|
68
|
+
"## Diff compactado",
|
|
69
|
+
"",
|
|
70
|
+
"```json",
|
|
71
|
+
clip(JSON.stringify(payload), 16_000),
|
|
72
|
+
"```",
|
|
73
|
+
"",
|
|
74
|
+
"Produza as hipóteses em JSON.",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
@@ -43,3 +43,15 @@ export function temperatureApplies(
|
|
|
43
43
|
if (provider !== "deepseek") return true;
|
|
44
44
|
return !deepSeekThinkingEnabled(modelId, providerOptions);
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Opções para tarefas estruturadas curtas (resumo, plano, classificação).
|
|
49
|
+
*
|
|
50
|
+
* O thinking do DeepSeek V4 consome o teto de saída e dominava a latência: a
|
|
51
|
+
* resposta JSON truncava (`finishReason: "length"`) e a feature caía no
|
|
52
|
+
* fallback sem produzir nada. Desligar o raciocínio nessas chamadas mantém o
|
|
53
|
+
* schema e corta a latência. Providers não-DeepSeek ignoram a chave.
|
|
54
|
+
*/
|
|
55
|
+
export const LEAN_PROVIDER_OPTIONS: AiProviderOptions = {
|
|
56
|
+
deepseek: { thinking: { type: "disabled" } },
|
|
57
|
+
};
|
package/src/core/ai/schemas.ts
CHANGED
|
@@ -43,18 +43,86 @@ export const TechnicalSummarySchema = z.object({
|
|
|
43
43
|
});
|
|
44
44
|
export type TechnicalSummaryPlan = z.infer<typeof TechnicalSummarySchema>;
|
|
45
45
|
|
|
46
|
+
/** Verificação sugerida — sempre ancorada em arquivo/símbolo quando possível. */
|
|
47
|
+
export const BriefCheckSchema = z.object({
|
|
48
|
+
statement: z.string().min(1).max(240),
|
|
49
|
+
file: z.string().max(200).optional(),
|
|
50
|
+
symbol: z.string().max(120).optional(),
|
|
51
|
+
/** Invariante que a verificação protege (`domain`). */
|
|
52
|
+
invariant: z.string().max(200).optional(),
|
|
53
|
+
});
|
|
54
|
+
export type BriefCheck = z.infer<typeof BriefCheckSchema>;
|
|
55
|
+
|
|
46
56
|
/** Plano de revisão sugerido por IA para um brief. */
|
|
47
57
|
export const BriefPlanSchema = z.object({
|
|
48
58
|
focus: z.array(z.string()).max(5).default([]),
|
|
49
59
|
questions: z.array(z.string()).max(3).default([]),
|
|
50
60
|
risks: z.array(z.string()).max(3).default([]),
|
|
51
61
|
/** Verificações de consistência lógica (fora das regras explícitas). */
|
|
52
|
-
logic: z.array(
|
|
62
|
+
logic: z.array(BriefCheckSchema).max(4).default([]),
|
|
53
63
|
/** Riscos do domínio de negócio (fora das regras explícitas). */
|
|
54
|
-
domain: z.array(
|
|
64
|
+
domain: z.array(BriefCheckSchema).max(4).default([]),
|
|
55
65
|
});
|
|
56
66
|
export type BriefPlan = z.infer<typeof BriefPlanSchema>;
|
|
57
67
|
|
|
68
|
+
/** Modelo de intenção da PR (`business-summary.json`). */
|
|
69
|
+
export const BusinessSummarySchema = z.object({
|
|
70
|
+
intent: z.string().max(800).optional(),
|
|
71
|
+
flows: z
|
|
72
|
+
.array(
|
|
73
|
+
z.object({
|
|
74
|
+
name: z.string().min(1).max(120),
|
|
75
|
+
trigger: z.string().max(240).optional(),
|
|
76
|
+
expected: z.string().max(400).optional(),
|
|
77
|
+
files: z.array(z.string().max(200)).max(20).optional(),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
.max(12)
|
|
81
|
+
.default([]),
|
|
82
|
+
invariants: z.array(z.string().max(300)).max(12).default([]),
|
|
83
|
+
contracts: z
|
|
84
|
+
.array(
|
|
85
|
+
z.object({
|
|
86
|
+
name: z.string().min(1).max(120),
|
|
87
|
+
sides: z.array(z.string().max(160)).max(8).optional(),
|
|
88
|
+
expectation: z.string().max(400).optional(),
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
.max(12)
|
|
92
|
+
.default([]),
|
|
93
|
+
states: z
|
|
94
|
+
.array(
|
|
95
|
+
z.object({
|
|
96
|
+
name: z.string().min(1).max(120),
|
|
97
|
+
transitions: z.array(z.string().max(200)).max(12).optional(),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
.max(8)
|
|
101
|
+
.default([]),
|
|
102
|
+
nonGoals: z.array(z.string().max(240)).max(8).optional(),
|
|
103
|
+
openQuestions: z.array(z.string().max(300)).max(8).optional(),
|
|
104
|
+
});
|
|
105
|
+
export type BusinessSummaryPlan = z.infer<typeof BusinessSummarySchema>;
|
|
106
|
+
|
|
107
|
+
/** Hipóteses de lógica/domínio (candidatos, nunca achados). */
|
|
108
|
+
export const BusinessHypothesesSchema = z.object({
|
|
109
|
+
hypotheses: z
|
|
110
|
+
.array(
|
|
111
|
+
z.object({
|
|
112
|
+
file: z.string().min(1),
|
|
113
|
+
kind: z.enum(["logic", "domain"]),
|
|
114
|
+
statement: z.string().min(1).max(300),
|
|
115
|
+
invariant: z.string().max(200).optional(),
|
|
116
|
+
confidence: z.number().min(0).max(1),
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
.max(12)
|
|
120
|
+
.default([]),
|
|
121
|
+
});
|
|
122
|
+
export type BusinessHypothesis = z.infer<
|
|
123
|
+
typeof BusinessHypothesesSchema
|
|
124
|
+
>["hypotheses"][number];
|
|
125
|
+
|
|
58
126
|
/** Classificação de arquivos não classificados. */
|
|
59
127
|
export const ClassificationSchema = z.object({
|
|
60
128
|
assignments: z
|