@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.
Files changed (106) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/compare-command.ts +112 -0
  4. package/src/cli/commands/context-command.ts +1 -0
  5. package/src/cli/commands/findings-command.ts +11 -1
  6. package/src/cli/commands/hunt-command.ts +20 -3
  7. package/src/cli/commands/review-command.ts +38 -0
  8. package/src/cli/commands/status-command.ts +3 -2
  9. package/src/cli/help.ts +3 -1
  10. package/src/cli/index.ts +3 -1
  11. package/src/cli/progress.ts +7 -2
  12. package/src/core/ai/ai-client.ts +38 -2
  13. package/src/core/ai/features/brief-enricher.ts +1 -1
  14. package/src/core/ai/features/business-analyzer.ts +61 -0
  15. package/src/core/ai/features/business-summarizer.ts +44 -0
  16. package/src/core/ai/index.ts +8 -0
  17. package/src/core/ai/prompts/brief.ts +33 -7
  18. package/src/core/ai/prompts/business.ts +76 -0
  19. package/src/core/ai/provider-capabilities.ts +12 -0
  20. package/src/core/ai/schemas.ts +70 -2
  21. package/src/core/ai/usage-recorder.ts +53 -1
  22. package/src/core/brief.ts +246 -12
  23. package/src/core/builtins.ts +108 -0
  24. package/src/core/business-summary.ts +583 -0
  25. package/src/core/commands/compare.ts +233 -0
  26. package/src/core/commands/context.ts +6 -2
  27. package/src/core/commands/findings.ts +38 -30
  28. package/src/core/commands/review.ts +6 -0
  29. package/src/core/commands/scaffold.ts +27 -0
  30. package/src/core/commands/status.ts +9 -4
  31. package/src/core/compare.ts +243 -0
  32. package/src/core/concurrency.ts +30 -0
  33. package/src/core/config.ts +31 -3
  34. package/src/core/defaults.ts +50 -4
  35. package/src/core/domain/budget.ts +62 -4
  36. package/src/core/domain/classification.ts +12 -2
  37. package/src/core/engine/carryover.ts +4 -0
  38. package/src/core/engine/finalize-pipeline.ts +347 -0
  39. package/src/core/engine/prepare-pipeline.ts +1294 -0
  40. package/src/core/engine/review-context.ts +51 -0
  41. package/src/core/engine/review-options.ts +47 -0
  42. package/src/core/engine/review-scope.ts +14 -1
  43. package/src/core/engine/review-session.ts +11 -13
  44. package/src/core/engine/run-artifacts.ts +196 -0
  45. package/src/core/engine/run-format.ts +4 -0
  46. package/src/core/findings-state.ts +13 -1
  47. package/src/core/github-review.ts +5 -2
  48. package/src/core/hunt/hunt-runner.ts +90 -0
  49. package/src/core/hunt/index.ts +2 -0
  50. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  51. package/src/core/hunt/maki-prompt.ts +13 -1
  52. package/src/core/hunt/pool.ts +4 -26
  53. package/src/core/logging/index.ts +6 -1
  54. package/src/core/logging/progress.ts +85 -1
  55. package/src/core/orchestrator.ts +33 -1289
  56. package/src/core/paths.ts +6 -3
  57. package/src/core/pr-context.ts +2 -4
  58. package/src/core/prepare-report.ts +61 -0
  59. package/src/core/project-config.ts +8 -1
  60. package/src/core/propagation.ts +127 -0
  61. package/src/core/registry-check.ts +60 -21
  62. package/src/core/renderers/manifest-writer.ts +13 -0
  63. package/src/core/renderers/resume-renderer.ts +151 -11
  64. package/src/core/report-builder.ts +108 -278
  65. package/src/core/storage/file/file-storage.ts +7 -3
  66. package/src/core/storage/sqlite/mappers.ts +10 -0
  67. package/src/core/storage/sqlite/schema.ts +104 -23
  68. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  69. package/src/core/storage/types.ts +16 -4
  70. package/src/core/storage/work-area.ts +59 -1
  71. package/src/core/types.ts +31 -0
  72. package/src/core/watchdog-registry.ts +21 -3
  73. package/src/index.ts +29 -4
  74. package/src/server/client/ArtifactsPanel.tsx +4 -2
  75. package/src/server/client/FindingCard.tsx +21 -1
  76. package/src/server/client/RunsSection.tsx +5 -0
  77. package/src/server/client/utils/constants.ts +21 -11
  78. package/src/server/client/utils/markdown.ts +1 -1
  79. package/src/server/client/utils/types.ts +14 -2
  80. package/src/server/run-repository.ts +35 -14
  81. package/src/server/styles.css +2 -0
  82. package/src/skill/OUTPUT-FORMAT.md +36 -10
  83. package/src/skill/README.md +18 -14
  84. package/src/skill/SETUP.md +1 -0
  85. package/src/skill/SKILL.md +13 -4
  86. package/src/skill/example-resume.md +43 -0
  87. package/src/skill/hunt.md +145 -58
  88. package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
  89. package/src/tests/ai-core.test.ts +46 -0
  90. package/src/tests/ai-features.test.ts +95 -4
  91. package/src/tests/baseline.test.ts +29 -121
  92. package/src/tests/brief-scope.test.ts +250 -0
  93. package/src/tests/business-summary.test.ts +258 -0
  94. package/src/tests/compare.test.ts +233 -0
  95. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  96. package/src/tests/hunt-maki.test.ts +58 -0
  97. package/src/tests/markdown.test.ts +3 -3
  98. package/src/tests/progress.test.ts +29 -1
  99. package/src/tests/propagation.test.ts +119 -0
  100. package/src/tests/review-resume.test.ts +60 -1
  101. package/src/tests/review-scope.test.ts +38 -29
  102. package/src/tests/review-scripts.test.ts +106 -218
  103. package/src/tests/server.test.ts +7 -4
  104. package/src/tests/storage.test.ts +194 -9
  105. package/src/core/renderers/report-renderer.ts +0 -66
  106. package/src/skill/example-review.md +0 -66
package/README.md CHANGED
@@ -15,6 +15,11 @@ agente especialista por domínio que audita o diff · **review/revisão** = o pr
15
15
  **Jev** = pré-análise opcional. O vocabulário completo (brief, findings, manifest, budget,
16
16
  overflow, churn, candidato…) está em [`docs/GLOSSARY.md`](docs/GLOSSARY.md).
17
17
 
18
+ > Toda revisão tem duas etapas fixas: um **modelo de intenção** da PR
19
+ > (`business-summary.json`) e o revisor de fluxo **`general`**. Nenhum dos dois é
20
+ > configurável — o primeiro enriquece os briefs; o segundo procura gaps de lógica/domínio,
21
+ > não só violações de regra.
22
+
18
23
  ---
19
24
 
20
25
  ## Como iniciar
@@ -128,7 +133,7 @@ mostra o worktree ativo de cada PR e permite criá-lo pela sidebar.
128
133
  ├─ Passo 0 contexto da PR (gh, opcional)
129
134
  ├─ Passo 1 classifica o diff → briefs por watchdog (incremental quando possível)
130
135
  ├─ Passo 2 N subagentes revisam em paralelo
131
- ├─ Passo 3 consolida em review.md + triagem no storage
136
+ ├─ Passo 3 grava o resumo final (review-resume.md) + triagem no storage
132
137
  ├─ Passo 4 valida na UI (server) e marca os achados a executar
133
138
  ├─ Passo 5 fix corrige o grupo atual → status fix_done
134
139
  └─ Passo 6 verify confere o que resolveu, sem re-revisar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Comando `compare` — teste A/B entre duas runs (por slug ou branch+variantes).
3
+ *
4
+ * Exemplo:
5
+ * houndsman compare --branch feature/x --a baseline --b otimizado
6
+ * houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
7
+ */
8
+
9
+ import { listRunsForCompare, runCompare } from "../../core/commands/compare";
10
+ import { ArgParser } from "../arg-parser";
11
+ import type { CliContext, Command } from "./command";
12
+
13
+ /** Flags com valor (não entram nos positionais). */
14
+ const VALUE_FLAGS = new Set(["--branch", "--a", "--b", "--config"]);
15
+
16
+ export const COMPARE_HELP = `Uso: houndsman compare [<slugA> <slugB>] [flags]
17
+
18
+ Compara duas runs da mesma branch e grava o relatório na run B.
19
+
20
+ Flags:
21
+ --branch <nome> Branch alvo (default: a atual do git)
22
+ --a <variante> Rótulo A (default: run principal)
23
+ --b <variante> Rótulo B (obrigatório sem slugs)
24
+ --list Lista as runs/variantes com contagem por severidade
25
+ --json Saída JSON (sem markdown)
26
+
27
+ Exemplos:
28
+ houndsman compare --branch feature/x --a baseline --b otimizado
29
+ houndsman compare feature-x--baseline-1a2b3c feature-x--otimizado-9f8e7d
30
+ houndsman compare --list --branch feature/x`;
31
+
32
+ /** Argumentos posicionais (ignora flags com valor). */
33
+ function positionals(args: readonly string[]): string[] {
34
+ const result: string[] = [];
35
+ for (let index = 0; index < args.length; index++) {
36
+ const arg = args[index];
37
+ if (arg.startsWith("--")) {
38
+ if (VALUE_FLAGS.has(arg)) index++;
39
+ continue;
40
+ }
41
+ result.push(arg);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ export class CompareCommand implements Command {
47
+ readonly name = "compare";
48
+
49
+ async run(args: string[], context: CliContext): Promise<number> {
50
+ const parser = ArgParser.from(args);
51
+ if (parser.has("--help") || parser.has("-h")) {
52
+ context.out(COMPARE_HELP);
53
+ return 0;
54
+ }
55
+
56
+ const engine = await context.engine();
57
+ const branch = parser.value("--branch");
58
+
59
+ if (parser.has("--list")) {
60
+ const runs = listRunsForCompare(engine.storage, engine.repo, branch);
61
+ if (runs.length === 0) {
62
+ context.out("Nenhuma run encontrada.");
63
+ engine.close();
64
+ return 0;
65
+ }
66
+ context.logger.table([
67
+ ["slug", "variante", "findings", "blocking", "attention", "atualizada"],
68
+ ...runs.map((run) => [
69
+ run.slug,
70
+ run.variant ?? "(principal)",
71
+ String(run.findings),
72
+ String(run.bySeverity.blocking),
73
+ String(run.bySeverity.attention),
74
+ run.updatedAt,
75
+ ]),
76
+ ]);
77
+ engine.close();
78
+ return 0;
79
+ }
80
+
81
+ const [slugA, slugB] = positionals(args);
82
+ if (slugA && !slugB) {
83
+ context.err(
84
+ "Passe dois slugs (`houndsman compare <a> <b>`) ou use --b <variante>.",
85
+ );
86
+ return 1;
87
+ }
88
+ try {
89
+ const result = runCompare(engine.storage, engine.repo, {
90
+ slugA,
91
+ slugB,
92
+ branch,
93
+ variantA: parser.value("--a"),
94
+ variantB: parser.value("--b"),
95
+ });
96
+ if (parser.has("--json")) {
97
+ context.out(result.json.trimEnd());
98
+ } else {
99
+ context.out(result.markdown.trimEnd());
100
+ context.logger.success(
101
+ `Comparação salva em ${result.artifacts.markdown} (run ${result.b.slug}).`,
102
+ );
103
+ }
104
+ return 0;
105
+ } catch (error) {
106
+ context.err(error instanceof Error ? error.message : String(error));
107
+ return 1;
108
+ } finally {
109
+ engine.close();
110
+ }
111
+ }
112
+ }
@@ -51,6 +51,7 @@ export class ContextCommand implements Command {
51
51
  engine.repo,
52
52
  {
53
53
  branch: workspace?.branch ?? parser.value("--branch"),
54
+ variant: parser.value("--variant"),
54
55
  worktreePath: workspace?.worktreePath,
55
56
  prNumber: workspace?.prNumber,
56
57
  },
@@ -14,21 +14,31 @@ export class FindingsCommand implements Command {
14
14
  async run(args: string[], context: CliContext): Promise<number> {
15
15
  const engine = await context.engine();
16
16
  const sub = ArgParser.from(args).command();
17
+ // Raiz/git do engine: o comando nunca lê `process.cwd()` implicitamente.
18
+ const options = { root: engine.root, git: engine.git };
17
19
  const result =
18
20
  sub === "verify"
19
- ? await runVerifyCommand(engine.config, engine.storage, engine.repo, args)
21
+ ? await runVerifyCommand(
22
+ engine.config,
23
+ engine.storage,
24
+ engine.repo,
25
+ args,
26
+ options,
27
+ )
20
28
  : sub === "submit"
21
29
  ? await runSubmitCommand(
22
30
  engine.config,
23
31
  engine.storage,
24
32
  engine.repo,
25
33
  args,
34
+ options,
26
35
  )
27
36
  : runFindingsCommand(
28
37
  engine.config,
29
38
  engine.storage,
30
39
  engine.repo,
31
40
  args,
41
+ options,
32
42
  );
33
43
 
34
44
  if (result.exitCode !== 0) context.err(result.output);
@@ -26,7 +26,7 @@ import {
26
26
  resolveMakiModel,
27
27
  } from "../../core/hunt/maki-model";
28
28
  import { SpawnMakiRunner } from "../../core/hunt/maki-runner";
29
- import { formatDuration, ProgressBoard } from "../../core/logging";
29
+ import { formatDuration, type LoggerLike, ProgressBoard } from "../../core/logging";
30
30
  import { displayPath } from "../../core/paths";
31
31
  import { ProjectConfig } from "../../core/project-config";
32
32
  import { touchRunStatus } from "../../core/run-status";
@@ -49,6 +49,7 @@ Subcomandos:
49
49
  Flags:
50
50
  --pr <n> Revisa a PR em worktree dedicado
51
51
  --branch <b> Branch alvo
52
+ --variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
52
53
  --worktree <path> Worktree já existente
53
54
  --allow-partial Finaliza mesmo se algum watchdog falhar (findings vazios)
54
55
  --no-context Não coleta o contexto da PR via gh
@@ -180,6 +181,7 @@ export class HuntCommand implements Command {
180
181
  engine.repo,
181
182
  {
182
183
  branch: workspace.branch,
184
+ variant: options.variant,
183
185
  worktreePath: workspace.worktreePath,
184
186
  prNumber: workspace.prNumber,
185
187
  },
@@ -236,7 +238,7 @@ export class HuntCommand implements Command {
236
238
  timeoutMs,
237
239
  retries,
238
240
  runner: new SpawnMakiRunner(binary.path),
239
- onEvent: (event) => this.logEvent(board, event),
241
+ onEvent: (event) => this.logEvent(board, event, context.logger),
240
242
  });
241
243
  // Runs longas não podem parecer “timeout” na UI: a skill pinga antes de
242
244
  // cada onda; o CLI renova o heartbeat enquanto o maki trabalha.
@@ -409,8 +411,23 @@ export class HuntCommand implements Command {
409
411
  }
410
412
 
411
413
  /** Log de progresso por watchdog. */
412
- private logEvent(board: ProgressBoard, event: HuntEvent): void {
414
+ private logEvent(board: ProgressBoard, event: HuntEvent, logger: LoggerLike): void {
413
415
  switch (event.type) {
416
+ case "intent":
417
+ if (event.status === "start") {
418
+ logger.step("Reconstruindo a intenção da PR (maki)…");
419
+ } else if (event.status === "done") {
420
+ logger.success(
421
+ `Intenção da PR reconstruída (${event.source ?? "reviewer"}).`,
422
+ );
423
+ } else if (event.status === "skipped") {
424
+ logger.info(`Intenção da PR já disponível (${event.source}).`);
425
+ } else {
426
+ logger.warn(
427
+ `Intenção da PR manteve o modelo determinístico: ${event.error ?? "falha"}`,
428
+ );
429
+ }
430
+ return;
414
431
  case "start":
415
432
  board.add(event.watchdogId, "revisando");
416
433
  return;
@@ -1,5 +1,7 @@
1
1
  /** Comando default — prepara ou finaliza a revisão. */
2
2
 
3
+ import { ArgParser } from "../../core/arg-parser";
4
+ import { runContextCommand } from "../../core/commands/context";
3
5
  import { parseReviewArgs } from "../../core/commands/review";
4
6
  import { resolveReviewWorkspace } from "../../core/engine/review-workspace";
5
7
  import { displayPath } from "../../core/paths";
@@ -52,6 +54,42 @@ export class ReviewCommand implements Command {
52
54
  pr: workspace.prNumber,
53
55
  worktreePath: workspace.worktreePath,
54
56
  };
57
+ // PR: coleta o contexto via `gh` aqui, como o `hunt` faz. Sem isso, a skill
58
+ // gastava um subagente só para rodar `houndsman context` antes do prepare.
59
+ if (
60
+ options.pr !== undefined &&
61
+ !options.finalize &&
62
+ !ArgParser.from(args).has("--no-context")
63
+ ) {
64
+ const contextStarted = Date.now();
65
+ context.logger.step("Coletando o contexto da PR via gh…");
66
+ try {
67
+ const collected = runContextCommand(
68
+ engine.config,
69
+ engine.storage,
70
+ engine.repo,
71
+ {
72
+ branch: workspace.branch,
73
+ variant: options.variant,
74
+ worktreePath: workspace.worktreePath,
75
+ prNumber: workspace.prNumber,
76
+ },
77
+ );
78
+ stepDone(
79
+ context.logger,
80
+ collected.context.available
81
+ ? "Contexto da PR coletado via gh"
82
+ : `Contexto da PR indisponível (${collected.context.reason})`,
83
+ contextStarted,
84
+ );
85
+ } catch (error) {
86
+ context.logger.warn(
87
+ `Contexto da PR indisponível: ${
88
+ error instanceof Error ? error.message : String(error)
89
+ }`,
90
+ );
91
+ }
92
+ }
55
93
  if (options.finalize) context.logger.step("Consolidando os findings…");
56
94
  const result = await engine
57
95
  .session(resolved)
@@ -11,6 +11,7 @@ export class StatusCommand implements Command {
11
11
  const parser = ArgParser.from(args);
12
12
  const action = parser.command();
13
13
  const branch = parser.value("--branch");
14
+ const variant = parser.value("--variant");
14
15
 
15
16
  if (action && action !== "ping" && action !== "show") {
16
17
  context.err(
@@ -22,8 +23,8 @@ export class StatusCommand implements Command {
22
23
  const engine = await context.engine();
23
24
  const result =
24
25
  action === "ping"
25
- ? pingStatus(engine.storage, engine.repo, branch)
26
- : readStatus(engine.storage, engine.repo, branch);
26
+ ? pingStatus(engine.storage, engine.repo, branch, variant)
27
+ : readStatus(engine.storage, engine.repo, branch, variant);
27
28
 
28
29
  if (action === "ping") {
29
30
  context.out(
package/src/cli/help.ts CHANGED
@@ -3,8 +3,9 @@ export const HELP = `Uso: houndsman [comando] [flags]
3
3
 
4
4
  Comandos:
5
5
  (nenhum) Prepara a revisão (classification + briefs + manifesto)
6
- --finalize Consolida os findings em review.md
6
+ --finalize Consolida os findings e grava o resumo final
7
7
  context Coleta o contexto da PR via gh (Passo 0; --pr/--worktree)
8
+ compare [a] [b] Compara duas runs (A/B) por slug ou --branch/--a/--b
8
9
  findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
9
10
  hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
10
11
  (a skill /houndsman hunt continua existindo p/ agentes externos)
@@ -25,6 +26,7 @@ Flags:
25
26
  --base <branch> Base do diff (default: main)
26
27
  --head <ref> Head do diff (default: HEAD)
27
28
  --branch <nome> Branch alvo (default: detecta do git)
29
+ --variant <label> Rótulo da variante A/B (runs separadas para a mesma branch)
28
30
  --pr <n> Revisa a PR em um worktree dedicado (cria/reusa automaticamente)
29
31
  --worktree <path> Revisa em um worktree já existente
30
32
  --clean Descarta findings brutos do staging anterior
package/src/cli/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Comandos:
14
14
  * (default) Passo 1 — preparar (classificação + briefs + manifesto)
15
- * --finalize Passo 3 — consolidar os findings em review.md
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
18
  * hunt [flags] Harness local (maki): prepare → watchdogs via maki → finalize
@@ -35,6 +35,7 @@ import { formatDuration, logger } from "../core/logging";
35
35
  import { syncProjectLayerForRoot } from "../core/services/worktree-manager";
36
36
  import { ArgParser } from "./arg-parser";
37
37
  import type { CliContext } from "./commands/command";
38
+ import { CompareCommand } from "./commands/compare-command";
38
39
  import { ContextCommand } from "./commands/context-command";
39
40
  import { DoctorCommand } from "./commands/doctor-command";
40
41
  import { FindingsCommand } from "./commands/findings-command";
@@ -61,6 +62,7 @@ function buildRegistry(): CommandRegistry {
61
62
  .register(new HelpCommand())
62
63
  .register(new HuntCommand())
63
64
  .register(new ContextCommand())
65
+ .register(new CompareCommand())
64
66
  .register(new FindingsCommand())
65
67
  .register(new StatusCommand())
66
68
  .register(new WatchdogCommand())
@@ -6,7 +6,7 @@
6
6
  * primeiro `Discovery:`.
7
7
  */
8
8
 
9
- import { formatDuration, type LoggerLike, PhaseReporter } from "../core/logging";
9
+ import { formatDuration, type LoggerLike, ParallelPhaseReporter } from "../core/logging";
10
10
  import type { PrepareProgress, PrepareStage } from "../core/orchestrator";
11
11
 
12
12
  /** Texto de cada etapa (início no gerúndio, fim no particípio). */
@@ -20,6 +20,10 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
20
20
  impact: { start: "Medindo o impacto nos importadores", done: "Impacto calculado" },
21
21
  jev: { start: "Pré-analisando o diff (Jev/IA)", done: "Pré-análise concluída" },
22
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
+ },
23
27
  technical: {
24
28
  start: "Consolidando os pontos técnicos",
25
29
  done: "Pontos técnicos prontos",
@@ -31,7 +35,8 @@ export const PREPARE_STAGE_TEXT: Record<PrepareStage, { start: string; done: str
31
35
 
32
36
  /** Cria o callback de progresso do prepare ligado ao logger do CLI. */
33
37
  export function prepareProgress(logger: LoggerLike): PrepareProgress {
34
- const reporter = new PhaseReporter(logger);
38
+ // O prepare roda etapas em paralelo; o reporter aceita fases concorrentes.
39
+ const reporter = new ParallelPhaseReporter(logger);
35
40
  return (event) => {
36
41
  const text = PREPARE_STAGE_TEXT[event.stage];
37
42
  if (event.type === "start") reporter.begin(event.stage, text.start);
@@ -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
- { ...request, outputSchema: request.outputSchema },
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
- return { ok: false, error: errorMessage(error) };
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,
@@ -27,7 +27,7 @@ export class AiBriefEnricher {
27
27
  instructions: briefInstructions(),
28
28
  prompt: briefPrompt(input),
29
29
  outputSchema: BriefPlanSchema,
30
- maxOutputTokens: 700,
30
+ maxOutputTokens: 1_400,
31
31
  },
32
32
  this.client.projectConfig.aiModelFor(input.watchdogId),
33
33
  );
@@ -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
+ }
@@ -23,6 +23,8 @@ export {
23
23
  errorMessage,
24
24
  } from "./errors";
25
25
  export { AiBriefEnricher } from "./features/brief-enricher";
26
+ export { AiBusinessAnalyzer } from "./features/business-analyzer";
27
+ export { AiBusinessSummarizer } from "./features/business-summarizer";
26
28
  export { AiClassifier } from "./features/classifier";
27
29
  export { AiContextSummarizer } from "./features/context-summarizer";
28
30
  export { AiPreAnalyzer } from "./features/pre-analyzer";
@@ -33,8 +35,14 @@ export {
33
35
  type AiCandidate,
34
36
  type AiClassification,
35
37
  type AiVerdict,
38
+ type BriefCheck,
39
+ BriefCheckSchema,
36
40
  type BriefPlan,
37
41
  BriefPlanSchema,
42
+ BusinessHypothesesSchema,
43
+ type BusinessHypothesis,
44
+ type BusinessSummaryPlan,
45
+ BusinessSummarySchema,
38
46
  CandidateSchema,
39
47
  ClassificationSchema,
40
48
  type ContextSummaryPlan,