@payconductor/houndsman 0.5.0 → 0.6.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 (153) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/help.ts +17 -1
  11. package/src/cli/index.ts +10 -6
  12. package/src/core/ai/ai-client.ts +221 -0
  13. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  14. package/src/core/ai/errors.ts +36 -0
  15. package/src/core/ai/features/brief-enricher.ts +62 -0
  16. package/src/core/ai/features/classifier.ts +50 -0
  17. package/src/core/ai/features/context-summarizer.ts +31 -0
  18. package/src/core/ai/features/pre-analyzer.ts +51 -0
  19. package/src/core/ai/features/verifier.ts +45 -0
  20. package/src/core/ai/index.ts +56 -0
  21. package/src/core/ai/model-factory.ts +137 -0
  22. package/src/core/ai/prompts/base.ts +36 -0
  23. package/src/core/ai/prompts/brief.ts +87 -0
  24. package/src/core/ai/prompts/classification.ts +36 -0
  25. package/src/core/ai/prompts/context.ts +29 -0
  26. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  27. package/src/core/ai/prompts/verify.ts +40 -0
  28. package/src/core/ai/schemas.ts +110 -0
  29. package/src/core/ai/types.ts +76 -0
  30. package/src/core/ai/usage-budget.ts +75 -0
  31. package/src/core/ai/usage-recorder.ts +79 -0
  32. package/src/core/assets.ts +16 -0
  33. package/src/core/ast/diff.ts +220 -0
  34. package/src/core/ast/impact.ts +127 -0
  35. package/src/core/ast/parser.ts +167 -0
  36. package/src/core/ast/service.ts +233 -0
  37. package/src/core/ast/symbols.ts +294 -0
  38. package/src/core/ast/types.ts +125 -0
  39. package/src/core/brief.ts +138 -0
  40. package/src/core/commands/findings.ts +59 -1
  41. package/src/core/commands/review.ts +2 -0
  42. package/src/core/commands/scaffold.ts +159 -10
  43. package/src/core/commands/update.ts +5 -3
  44. package/src/core/config.ts +85 -0
  45. package/src/core/defaults.ts +27 -0
  46. package/src/core/domain/watchdog.ts +43 -5
  47. package/src/core/engine/houndsman-engine.ts +16 -1
  48. package/src/core/engine/review-scope.ts +52 -6
  49. package/src/core/engine/review-session.ts +27 -2
  50. package/src/core/env.ts +101 -0
  51. package/src/core/file-classifier.ts +25 -4
  52. package/src/core/jev/jev-client.ts +8 -7
  53. package/src/core/jev/jev-verify.ts +2 -1
  54. package/src/core/logging/index.ts +17 -0
  55. package/src/core/logging/logger.ts +187 -0
  56. package/src/core/orchestrator.ts +357 -9
  57. package/src/core/project-config.ts +123 -1
  58. package/src/core/registry-check.ts +148 -6
  59. package/src/core/renderers/manifest-writer.ts +33 -0
  60. package/src/core/rule-text.ts +97 -0
  61. package/src/core/services/file-classifier.ts +10 -3
  62. package/src/core/services/verifier.ts +14 -3
  63. package/src/core/storage/paths.ts +6 -5
  64. package/src/core/storage/types.ts +6 -0
  65. package/src/core/storage/work-area.ts +12 -0
  66. package/src/core/types.ts +37 -1
  67. package/src/core/verify.ts +17 -5
  68. package/src/harness/README.md +15 -0
  69. package/src/harness/agent/agent-context.ts +33 -0
  70. package/src/harness/agent/agent-errors.ts +30 -0
  71. package/src/harness/agent/agent-factory.ts +45 -0
  72. package/src/harness/agent/agent-reporter.ts +34 -0
  73. package/src/harness/agent/agent-result.ts +16 -0
  74. package/src/harness/agent/agent-runner.ts +32 -0
  75. package/src/harness/agent/agent-scheduler.ts +37 -0
  76. package/src/harness/agent/review-agent.ts +391 -0
  77. package/src/harness/config/harness-config.ts +84 -0
  78. package/src/harness/hunt/hunt-harness.ts +83 -0
  79. package/src/harness/hunt/hunt-options.ts +75 -0
  80. package/src/harness/hunt/hunt-result.ts +17 -0
  81. package/src/harness/hunt/hunt-session.ts +372 -0
  82. package/src/harness/hunt/review-planner.ts +98 -0
  83. package/src/harness/index.ts +36 -0
  84. package/src/harness/investigation/agent-state.ts +134 -0
  85. package/src/harness/investigation/evidence-verifier.ts +129 -0
  86. package/src/harness/investigation/evidence.ts +110 -0
  87. package/src/harness/investigation/finding-critic.ts +96 -0
  88. package/src/harness/investigation/investigation-planner.ts +118 -0
  89. package/src/harness/investigation/investigation-task.ts +10 -0
  90. package/src/harness/investigation/review-journal.ts +87 -0
  91. package/src/harness/investigation/rule-analyzer.ts +178 -0
  92. package/src/harness/investigation/rule-signals.ts +110 -0
  93. package/src/harness/prompts/agent-schemas.ts +57 -0
  94. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  95. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  96. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  97. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  98. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  99. package/src/harness/tools/agent-tool.ts +102 -0
  100. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  101. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  102. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  103. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  104. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  105. package/src/harness/tools/builtin/helpers.ts +115 -0
  106. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  107. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  108. package/src/harness/tools/builtin/index.ts +59 -0
  109. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  110. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  111. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  112. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  113. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  114. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  115. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  116. package/src/harness/tools/path-guard.ts +90 -0
  117. package/src/harness/tools/result-limiter.ts +49 -0
  118. package/src/harness/tools/secret-redactor.ts +54 -0
  119. package/src/harness/tools/tool-cache.ts +50 -0
  120. package/src/harness/tools/tool-call-budget.ts +60 -0
  121. package/src/harness/tools/tool-context.ts +5 -0
  122. package/src/harness/tools/tool-middleware.ts +105 -0
  123. package/src/harness/tools/tool-policy.ts +51 -0
  124. package/src/harness/tools/tool-registry.ts +112 -0
  125. package/src/harness/tools/tool-result.ts +15 -0
  126. package/src/harness/tools/toolbox.ts +36 -0
  127. package/src/harness/validation/finding-validator.ts +119 -0
  128. package/src/index.ts +46 -0
  129. package/src/server/client/utils/constants.ts +27 -5
  130. package/src/server/client/utils/types.ts +6 -0
  131. package/src/server/index.ts +3 -2
  132. package/src/skill/OUTPUT-FORMAT.md +12 -0
  133. package/src/skill/SETUP.md +6 -2
  134. package/src/skill/hunt.md +14 -2
  135. package/src/skill/verify.md +1 -1
  136. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  137. package/src/tests/ai-core.test.ts +120 -0
  138. package/src/tests/ai-features.test.ts +183 -0
  139. package/src/tests/ast.test.ts +335 -0
  140. package/src/tests/env.test.ts +69 -0
  141. package/src/tests/fallback.test.ts +172 -0
  142. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  143. package/src/tests/fixtures/fake-investigation.ts +64 -0
  144. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  145. package/src/tests/harness-agent.test.ts +122 -0
  146. package/src/tests/harness-investigation.test.ts +206 -0
  147. package/src/tests/harness-tools.test.ts +148 -0
  148. package/src/tests/hunt-command.test.ts +212 -0
  149. package/src/tests/logger.test.ts +83 -0
  150. package/src/tests/review-scope.test.ts +11 -11
  151. package/src/tests/rule-analyzer.test.ts +61 -0
  152. package/src/tests/tool-contracts.test.ts +82 -0
  153. package/src/tests/verify.test.ts +1 -1
package/src/skill/hunt.md CHANGED
@@ -10,6 +10,11 @@ as regras do projeto ficam em `houndsman.config.ts` + `.houndsman/`.
10
10
  **Quando usar:** revisar PR antes de merge · code review de feature branch · validar
11
11
  mudanças antes de push.
12
12
 
13
+ > **Harness nativo (opcional):** além deste skill, o CLI tem `houndsman hunt` — o mesmo
14
+ > fluxo executado pelo próprio houndsman, com tools read-only, investigação por evidência
15
+ > e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA. Detalhes
16
+ > em `docs/HARNESS.md`.
17
+
13
18
  ## Quem executa o quê
14
19
 
15
20
  - **Você** só invoca o skill: `/houndsman [hunt]`. Não precisa rodar nenhum script à mão.
@@ -76,6 +81,8 @@ staging dos subagentes em `<HOUNDSMAN_HOME>/work/<reviewId>/`:
76
81
  | Artefato | Conteúdo |
77
82
  |---|---|
78
83
  | `classification.json` | Watchdogs ativados por arquivo + status (A/M/D/R) + churn (storage) |
84
+ | `ast-summary.json` | Fatos estruturais + delta por arquivo (AST TS/JS) (storage) |
85
+ | `ast-impact.json` | Importadores reversos e exports públicos alterados (storage) |
79
86
  | `candidates.json` | Indícios da pré-análise Jev (regra + arquivo + confiança) (storage) |
80
87
  | `briefs/<watchdog>.md` | Instruções prontas para cada subagente (staging + storage, para auditoria) |
81
88
  | `manifest.json` | Resumo: watchdogs, arquivos, deletados, status, contexto, Jev, orçamento (storage) |
@@ -83,6 +90,11 @@ staging dos subagentes em `<HOUNDSMAN_HOME>/work/<reviewId>/`:
83
90
  > O prepare imprime o caminho absoluto do diretório de briefs e cada brief traz o
84
91
  > `outputPath` do JSON que o subagente deve gravar. Briefs e contexto também ficam
85
92
  > persistidos no storage para auditoria; o staging é limpo após o `--finalize`.
93
+ >
94
+ > **AST (TS/JS, ligado por padrão):** cada brief ganha a seção `Estrutura (AST)` com
95
+ > símbolos adicionados/removidos/alterados, assinaturas e imports; o impacto reverso
96
+ > (quem importa os arquivos alterados) aparece como mapa de leitura. É fail-open:
97
+ > sem grammar/ABI, a revisão segue só com regex. Desative com `--no-ast`.
86
98
 
87
99
  **Excluídos automaticamente:** lock files, `dist/`, `build/`, `.env*`, imagens, source
88
100
  maps (padrões do projeto em `.houndsman/exclusions.ts`).
@@ -106,7 +118,7 @@ arquivos da PR. Cada brief recebe no máximo `budgets.maxFilesPerBrief` arquivos
106
118
 
107
119
  ### Pré-análise Jev (Passo 1.5, opcional)
108
120
 
109
- Se `TYPESAFE_API_KEY` estiver definida, cada arquivo é avaliado pelo modelo **Jev** contra
121
+ Se `HOUNDSMAN_TYPE_SAFE_API_KEY` estiver definida, cada arquivo é avaliado pelo modelo **Jev** contra
110
122
  as regras atômicas dos watchdogs. Respostas acima do limiar viram **candidatos** em
111
123
  `candidates.json` e entram no brief como *warm start* — o subagente **confirma ou refuta**.
112
124
 
@@ -264,7 +276,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
264
276
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
265
277
 
266
278
  Flags do review: `--base` · `--head` · `--branch` · `--clean` · `--full` (força
267
- completo) · `--no-jev` · `--fail-on-blocking` (com `--finalize`).
279
+ completo) · `--no-jev` · `--no-ast` · `--fail-on-blocking` (com `--finalize`).
268
280
 
269
281
  > Persistência: SQLite global em `<HOUNDSMAN_HOME>/houndsman.db`. O staging dos subagentes
270
282
  > (`briefs/`, `findings/`, contexto) vive em `<HOUNDSMAN_HOME>/work/<reviewId>/` e é limpo
@@ -24,7 +24,7 @@ altera regras.
24
24
  - **âncora sumida** (`stale`) → provável **resolvido** — mas o trecho pode ter sido reescrito;
25
25
  - **arquivo ausente** → fora da verificação (deletado/movido).
26
26
 
27
- 2. **Tier Jev (opcional, só nos ambíguos).** Com `TYPESAFE_API_KEY`, os casos `stale` / sem
27
+ 2. **Tier Jev (opcional, só nos ambíguos).** Com `HOUNDSMAN_TYPE_SAFE_API_KEY`, os casos `stale` / sem
28
28
  âncora são confirmados pelo LLM sobre um **diff focado** do arquivo (`foundAtSha..HEAD`), uma
29
29
  pergunta `noul` por finding. Falha-aberto: sem chave/erro → vale o resultado local.
30
30
 
@@ -172,6 +172,10 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 1
172
172
  ".agents/skills/houndsman/SKILL.md",
173
173
  ".claude/skills/houndsman/SKILL.md",
174
174
  ".houndsman/.gitignore",
175
+ ".houndsman/core/grammars/tree-sitter-javascript.wasm",
176
+ ".houndsman/core/grammars/tree-sitter-tsx.wasm",
177
+ ".houndsman/core/grammars/tree-sitter-typescript.wasm",
178
+ ".houndsman/core/grammars/web-tree-sitter.wasm",
175
179
  ".houndsman/core/skill/AUDIT-PROTOCOL.md",
176
180
  ".houndsman/core/skill/OUTPUT-FORMAT.md",
177
181
  ".houndsman/core/skill/README.md",
@@ -184,6 +188,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 1
184
188
  ".houndsman/core/types.ts",
185
189
  ".houndsman/doc-mappings.ts",
186
190
  ".houndsman/exclusions.ts",
191
+ ".houndsman/watchdogs/default.ts",
187
192
  ".houndsman/watchdogs/example.ts",
188
193
  ".houndsman/watchdogs/index.ts",
189
194
  "houndsman.config.ts",
@@ -330,11 +335,21 @@ export type Rule = {
330
335
  severity: Severity;
331
336
  text: string;
332
337
  jev?: JevRuleSpec;
338
+ hints?: RuleHints;
339
+ };
340
+
341
+ /** Sinais opcionais de uma regra (símbolos, imports, decorators, paths, checks). */
342
+ export type RuleHints = {
343
+ symbols?: string[];
344
+ imports?: string[];
345
+ decorators?: string[];
346
+ paths?: string[];
347
+ checks?: string[];
333
348
  };
334
349
 
335
350
  /** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
336
351
  export type ClassifyRule = {
337
- type?: "filename" | "content";
352
+ type?: "filename" | "content" | "symbol" | "import" | "export" | "decorator";
338
353
  value: string;
339
354
  };
340
355
 
@@ -351,6 +366,10 @@ export type WatchdogDefinition = WatchdogMeta & {
351
366
  id: string;
352
367
  description: string;
353
368
  breakingChange?: boolean;
369
+ /** Só assume arquivos que nenhum outro watchdog casou. */
370
+ fallback?: boolean;
371
+ /** Permite findings emergentes (Lógica:/Domínio:) fora das regras. */
372
+ allowEmergentFindings?: boolean;
354
373
  reviewFocus?: readonly string[];
355
374
  exclude?: readonly string[];
356
375
  };
@@ -389,6 +408,54 @@ export type HoundsmanStorage = {
389
408
  path?: string;
390
409
  };
391
410
 
411
+ /** Linguagens suportadas pelo AST na primeira fase. */
412
+ export type AstLanguage = "typescript" | "tsx" | "javascript";
413
+
414
+ /** Configuração do AST determinístico (classificação estrutural, brief e impacto). */
415
+ export type HoundsmanAst = {
416
+ enabled?: boolean;
417
+ maxFileBytes?: number;
418
+ maxImpactFiles?: number;
419
+ languages?: Record<string, AstLanguage>;
420
+ aliases?: Record<string, string>;
421
+ };
422
+
423
+ /** Providers suportados pela camada de IA. */
424
+ export type AiProviderKind = "deepseek" | "openai" | "anthropic";
425
+
426
+ /** Features de IA do fluxo normal (todas fail-open). */
427
+ export type HoundsmanAiFeatures = {
428
+ context?: boolean;
429
+ brief?: boolean;
430
+ classification?: boolean;
431
+ preAnalysis?: boolean;
432
+ verify?: boolean;
433
+ };
434
+
435
+ /** Overrides de IA por watchdog. */
436
+ export type HoundsmanAiWatchdog = {
437
+ model?: string;
438
+ maxSteps?: number;
439
+ maxOutputTokens?: number;
440
+ };
441
+
442
+ /** Configuração da camada de IA (briefs, contexto, classificação e hunt nativo). */
443
+ export type HoundsmanAi = {
444
+ enabled?: boolean;
445
+ provider?: AiProviderKind;
446
+ model?: string;
447
+ baseURL?: string;
448
+ concurrency?: number;
449
+ maxSteps?: number;
450
+ maxOutputTokens?: number;
451
+ temperature?: number;
452
+ timeoutMs?: number;
453
+ maxCallsPerReview?: number;
454
+ maxTokensPerReview?: number;
455
+ features?: HoundsmanAiFeatures;
456
+ watchdogs?: Record<string, HoundsmanAiWatchdog>;
457
+ };
458
+
392
459
  /** Contrato de configuração do motor. */
393
460
  export type HoundsmanConfig<TId extends string = string> = {
394
461
  projectName: string;
@@ -400,6 +467,8 @@ export type HoundsmanConfig<TId extends string = string> = {
400
467
  alwaysIncludeDocs?: readonly string[];
401
468
  budgets?: HoundsmanBudgets;
402
469
  concurrency?: HoundsmanConcurrency;
470
+ ast?: HoundsmanAst;
471
+ ai?: HoundsmanAi;
403
472
  };
404
473
 
405
474
  /** Helper de autoria (identidade) — apenas tipagem. */
@@ -419,9 +488,10 @@ export function defineHoundsmanConfig<TId extends string>(
419
488
  "/**
420
489
  * Mapeamento de padrões nos paths → documentação relevante.
421
490
  * Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
422
- */\`core/\` é regenerado por \`houndsman update\`.
423
- # Runs, findings e artefatos vivem no storage global (fora do projeto).
424
- core
491
+ *
492
+ * Exemplo: [/^src\\//, ["README.md"]]
493
+ */
494
+ export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [];
425
495
  "
426
496
  ,
427
497
  ".houndsman/exclusions.ts":
@@ -444,7 +514,8 @@ export function isExcluded(filePath: string): boolean {
444
514
 
445
515
  /**
446
516
  * Watchdog de exemplo — substitua pelas regras reais do projeto.
447
- * Um watchdog declara seus arquivos via \`classify\` (regex de path ou conteúdo).
517
+ * Um watchdog declara seus arquivos via \`classify\` (regex de path, conteúdo ou
518
+ * fatos AST: \`symbol\`/\`import\`/\`export\`/\`decorator\`).
448
519
  * O texto de cada regra começa com a polaridade: \`Promover:\` (DEVE acontecer)
449
520
  * ou \`Evitar:\`/\`Não\`/\`Nunca\` (NÃO DEVE acontecer).
450
521
  */
@@ -452,8 +523,8 @@ export const exampleWatchdog = defineWatchdog({
452
523
  id: "example",
453
524
  name: "Exemplo",
454
525
  description: "Watchdog de exemplo — descreva o propósito em uma frase.",
455
- scope: "Todo o repositório (ajuste o escopo)",
456
- classify: [{ value: "^src/" }],
526
+ scope: "src/example/** (ajuste o escopo)",
527
+ classify: [{ value: "^src/example/" }],
457
528
  rules: [
458
529
  {
459
530
  severity: "attention",
@@ -470,13 +541,15 @@ export const exampleWatchdog = defineWatchdog({
470
541
  "
471
542
  ,
472
543
  ".houndsman/watchdogs/index.ts":
473
- "import { exampleWatchdog } from "./example";
544
+ "import { defaultWatchdog } from "./default";
545
+ import { exampleWatchdog } from "./example";
474
546
 
475
547
  /**
476
548
  * Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
477
549
  * Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
478
550
  */
479
551
  export const WATCHDOG_DEFINITIONS = {
552
+ default: defaultWatchdog,
480
553
  example: exampleWatchdog,
481
554
  };
482
555
 
@@ -517,7 +590,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
517
590
  },
518
591
  {
519
592
  "file": ".houndsman/core/skill/OUTPUT-FORMAT.md",
520
- "sha256": "06aedccd5688aaa8",
593
+ "sha256": "94ea2b8def871784",
521
594
  },
522
595
  {
523
596
  "file": ".houndsman/core/skill/README.md",
@@ -525,7 +598,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
525
598
  },
526
599
  {
527
600
  "file": ".houndsman/core/skill/SETUP.md",
528
- "sha256": "7ce23b97d9609ac2",
601
+ "sha256": "b74af7e1979bf1b4",
529
602
  },
530
603
  {
531
604
  "file": ".houndsman/core/skill/example-review.md",
@@ -537,7 +610,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
537
610
  },
538
611
  {
539
612
  "file": ".houndsman/core/skill/hunt.md",
540
- "sha256": "c978287603f4e088",
613
+ "sha256": "b55507d62d3c1fcd",
541
614
  },
542
615
  {
543
616
  "file": ".houndsman/core/skill/train.md",
@@ -545,7 +618,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
545
618
  },
546
619
  {
547
620
  "file": ".houndsman/core/skill/verify.md",
548
- "sha256": "29bfdeb7def2298e",
621
+ "sha256": "780f4b078daa104b",
549
622
  },
550
623
  ]
551
624
  `;
@@ -555,6 +628,7 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
555
628
  Briefs: <cwd>/.home/work/<uuid>/briefs/
556
629
  Deletado (breaking change): src/alpha/old.ts
557
630
  Jev: pulado (desativado (--no-jev)).
631
+ AST: desabilitado ou sem grammar compatível.
558
632
  Sem contexto da PR (rode \`bun run houndsman context\` antes, se houver PR).
559
633
 
560
634
  Próximo passo: despachar os sub-agentes (um por brief em <cwd>/.home/work/<uuid>/briefs/).
@@ -603,8 +677,6 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
603
677
  "alpha": 2
604
678
  },
605
679
  "excludedCount": 0,
606
- "base": "<sha>",
607
- "head": "HEAD",
608
680
  "headSha": "<sha>",
609
681
  "mode": "full"
610
682
  }
@@ -650,6 +722,16 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
650
722
  "available": false,
651
723
  "reason": "desativado (--no-jev)"
652
724
  },
725
+ "ast": {
726
+ "enabled": false,
727
+ "languages": [],
728
+ "files": 0,
729
+ "impact": {
730
+ "affectedFiles": 0,
731
+ "importers": 0,
732
+ "exportedChanges": 0
733
+ }
734
+ },
653
735
  "deleted": [
654
736
  "src/alpha/old.ts"
655
737
  ],
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Fundação de IA: cliente fail-open, budget, provider fake e config.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { z } from "zod";
7
+ import { AiClient, AiUsageBudget, AiUsageRecorder } from "../core/ai";
8
+ import { defineHoundsmanConfig } from "../core/config";
9
+ import { DEFAULT_AI_MODEL } from "../core/defaults";
10
+ import { ProjectConfig } from "../core/project-config";
11
+ import { checkRegistry } from "../core/registry-check";
12
+ import { FakeModelProvider } from "./fixtures/fake-model-provider";
13
+ import { fakeHoundsmanConfig } from "./fixtures/fake-project";
14
+
15
+ const project = new ProjectConfig(fakeHoundsmanConfig);
16
+
17
+ describe("config de IA", () => {
18
+ test("modelo default do DeepSeek é 4.1 flash", () => {
19
+ expect(DEFAULT_AI_MODEL).toBe("deepseek-v4.1-flash");
20
+ expect(project.aiModel).toBe("deepseek-v4.1-flash");
21
+ });
22
+ });
23
+
24
+ describe("AiClient", () => {
25
+ test("sem provider/chave → indisponível e fail-open", async () => {
26
+ const client = new AiClient({ project, provider: null });
27
+ expect(client.available).toBe(false);
28
+ const result = await client.generateObject("x", {
29
+ prompt: "oi",
30
+ outputSchema: z.object({ ok: z.boolean() }),
31
+ });
32
+ expect(result.ok).toBe(false);
33
+ });
34
+
35
+ test("provider fake devolve saída, uso e registra no recorder", async () => {
36
+ const provider = new FakeModelProvider(() => ({ ok: true }));
37
+ const client = new AiClient({ project, provider });
38
+ expect(client.available).toBe(true);
39
+ const result = await client.generateObject("context", {
40
+ prompt: "oi",
41
+ outputSchema: z.object({ ok: z.boolean() }),
42
+ });
43
+ expect(result.ok).toBe(true);
44
+ if (result.ok) expect(result.output.ok).toBe(true);
45
+ expect(client.recorder.total.calls).toBe(1);
46
+ expect(provider.calls).toBe(1);
47
+ });
48
+
49
+ test("budget estourado vira fail-open sem chamar o provider", async () => {
50
+ const provider = new FakeModelProvider(() => ({ ok: true }));
51
+ const client = new AiClient({
52
+ project,
53
+ provider,
54
+ budget: new AiUsageBudget({ maxCalls: 0 }),
55
+ });
56
+ const result = await client.generateObject("x", {
57
+ prompt: "oi",
58
+ outputSchema: z.object({ ok: z.boolean() }),
59
+ });
60
+ expect(result.ok).toBe(false);
61
+ expect(provider.calls).toBe(0);
62
+ });
63
+ describe("validateCredential", () => {
64
+ test("ok, inválida e ausente", async () => {
65
+ const okClient = new AiClient({
66
+ project,
67
+ provider: new FakeModelProvider(() => "OK"),
68
+ });
69
+ expect((await okClient.validateCredential()).kind).toBe("ok");
70
+
71
+ const invalid = new AiClient({
72
+ project,
73
+ provider: new FakeModelProvider(() => {
74
+ throw new Error("401 invalid api key");
75
+ }),
76
+ });
77
+ expect((await invalid.validateCredential()).kind).toBe("invalid");
78
+
79
+ const missing = new AiClient({ project, provider: null });
80
+ expect((await missing.validateCredential()).kind).toBe("missing");
81
+ });
82
+ });
83
+ });
84
+
85
+ describe("AiUsageBudget / AiUsageRecorder", () => {
86
+ test("acumula uso e bloqueia no teto", () => {
87
+ const budget = new AiUsageBudget({ maxCalls: 2 });
88
+ budget.record({ inputTokens: 1, outputTokens: 1, totalTokens: 2, calls: 1 });
89
+ budget.record({ inputTokens: 1, outputTokens: 1, totalTokens: 2, calls: 1 });
90
+ expect(budget.canCall).toBe(false);
91
+ expect(() => budget.assertCanCall()).toThrow();
92
+ });
93
+
94
+ test("recorder serializa total e entradas", () => {
95
+ const recorder = new AiUsageRecorder("deepseek", "fake");
96
+ recorder.record("brief", "fake", {
97
+ inputTokens: 10,
98
+ outputTokens: 5,
99
+ totalTokens: 15,
100
+ calls: 1,
101
+ });
102
+ const data = recorder.toJSON();
103
+ expect(data.total.calls).toBe(1);
104
+ expect(data.total.totalTokens).toBe(15);
105
+ expect(data.entries[0]?.feature).toBe("brief");
106
+ expect(recorder.serialize()).toContain('"calls": 1');
107
+ });
108
+ });
109
+
110
+ describe("checkRegistry (IA)", () => {
111
+ test("provider inválido é problema; budgets inválidos são aviso", () => {
112
+ const config = defineHoundsmanConfig({
113
+ ...fakeHoundsmanConfig,
114
+ ai: { provider: "nope" as never, concurrency: 0 },
115
+ });
116
+ const { problems, warnings } = checkRegistry(config);
117
+ expect(problems.some((item) => item.includes("ai.provider"))).toBe(true);
118
+ expect(warnings.some((item) => item.includes("ai.concurrency"))).toBe(true);
119
+ });
120
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Features de IA do fluxo normal (brief, contexto, classificação, verify).
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { AiClient } from "../core/ai";
7
+ import { AiBriefEnricher } from "../core/ai/features/brief-enricher";
8
+ import { AiClassifier } from "../core/ai/features/classifier";
9
+ import { AiContextSummarizer } from "../core/ai/features/context-summarizer";
10
+ import { AiPreAnalyzer } from "../core/ai/features/pre-analyzer";
11
+ import { AiVerifier } from "../core/ai/features/verifier";
12
+ import { briefInstructions, briefPrompt } from "../core/ai/prompts/brief";
13
+ import { renderBrief } from "../core/brief";
14
+ import { ProjectConfig } from "../core/project-config";
15
+ import { FakeModelProvider } from "./fixtures/fake-model-provider";
16
+ import { fakeHoundsmanConfig } from "./fixtures/fake-project";
17
+
18
+ const project = new ProjectConfig(fakeHoundsmanConfig);
19
+ const alpha = fakeHoundsmanConfig.watchdogs.alpha;
20
+
21
+ describe("AiBriefEnricher", () => {
22
+ test("devolve o plano do modelo", async () => {
23
+ const provider = new FakeModelProvider(() => ({
24
+ focus: ["checar any"],
25
+ questions: ["há any?"],
26
+ risks: ["tipagem fraca"],
27
+ }));
28
+ const enricher = new AiBriefEnricher(new AiClient({ project, provider }));
29
+ const plan = await enricher.enrich({
30
+ watchdogId: "alpha",
31
+ name: "Alpha",
32
+ description: "x",
33
+ scope: "src/alpha",
34
+ rules: alpha.rules,
35
+ files: ["src/alpha/a.ts"],
36
+ });
37
+ expect(plan?.focus[0]).toBe("checar any");
38
+ });
39
+
40
+ test("sem IA → null (brief inalterado)", async () => {
41
+ const enricher = new AiBriefEnricher(new AiClient({ project, provider: null }));
42
+ expect(enricher.enabled).toBe(false);
43
+ expect(
44
+ await enricher.enrich({
45
+ watchdogId: "alpha",
46
+ name: "Alpha",
47
+ description: "x",
48
+ scope: "src/alpha",
49
+ rules: alpha.rules,
50
+ files: [],
51
+ }),
52
+ ).toBeNull();
53
+ });
54
+
55
+ test("brief com aiPlan ganha a seção de sugestão", () => {
56
+ const args = {
57
+ watchdogId: "alpha",
58
+ files: ["src/alpha/a.ts"],
59
+ statuses: { "src/alpha/a.ts": { status: "modified" as const } },
60
+ deleted: [],
61
+ branch: "main",
62
+ outputPath: "findings/alpha.json",
63
+ outputFormatPath: "OUTPUT-FORMAT.md",
64
+ docs: [],
65
+ aiPlan: {
66
+ focus: ["foco"],
67
+ questions: ["pergunta"],
68
+ risks: ["risco"],
69
+ logic: [],
70
+ domain: [],
71
+ },
72
+ };
73
+ const markdown = renderBrief(alpha, args);
74
+ expect(markdown).toContain("## Plano de revisão (IA)");
75
+ expect(markdown).toContain("foco");
76
+ });
77
+
78
+ test("brief sem aiPlan é byte-idêntico ao render sem a seção", () => {
79
+ const base = {
80
+ watchdogId: "alpha",
81
+ files: ["src/alpha/a.ts"],
82
+ statuses: { "src/alpha/a.ts": { status: "modified" as const } },
83
+ deleted: [],
84
+ branch: "main",
85
+ outputPath: "findings/alpha.json",
86
+ outputFormatPath: "OUTPUT-FORMAT.md",
87
+ docs: [],
88
+ };
89
+ expect(renderBrief(alpha, base)).not.toContain("Plano de revisão (IA)");
90
+ });
91
+
92
+ test("prompt e render incluem lógica e domínio", () => {
93
+ expect(briefInstructions()).toContain("PISO");
94
+ const prompt = briefPrompt({
95
+ watchdogId: "alpha",
96
+ name: "Alpha",
97
+ description: "x",
98
+ scope: "src/alpha",
99
+ rules: alpha.rules,
100
+ files: ["src/alpha/a.ts"],
101
+ });
102
+ expect(prompt).toContain("Revisão além das regras");
103
+ const markdown = renderBrief(alpha, {
104
+ watchdogId: "alpha",
105
+ files: ["src/alpha/a.ts"],
106
+ statuses: { "src/alpha/a.ts": { status: "modified" as const } },
107
+ deleted: [],
108
+ branch: "main",
109
+ outputPath: "findings/alpha.json",
110
+ outputFormatPath: "OUTPUT-FORMAT.md",
111
+ docs: [],
112
+ aiPlan: {
113
+ focus: [],
114
+ questions: [],
115
+ risks: [],
116
+ logic: ["nome vs comportamento"],
117
+ domain: ["transição de estado"],
118
+ },
119
+ });
120
+ expect(markdown).toContain("Consistência lógica");
121
+ expect(markdown).toContain("Domínio de negócio");
122
+ });
123
+ });
124
+
125
+ describe("AiContextSummarizer", () => {
126
+ test("resume contexto bruto", async () => {
127
+ const provider = new FakeModelProvider(() => ({
128
+ available: true,
129
+ objective: "Entregar Y",
130
+ changeSummary: ["Faz X"],
131
+ }));
132
+ const summarizer = new AiContextSummarizer(new AiClient({ project, provider }));
133
+ const summary = await summarizer.summarize('{"title":"PR"}');
134
+ expect(summary?.objective).toBe("Entregar Y");
135
+ });
136
+ });
137
+
138
+ describe("AiClassifier", () => {
139
+ test("filtra ids e arquivos desconhecidos", async () => {
140
+ const provider = new FakeModelProvider(() => ({
141
+ assignments: [
142
+ { file: "a.ts", watchdogId: "alpha" },
143
+ { file: "a.ts", watchdogId: "nope" },
144
+ { file: "b.ts", watchdogId: "alpha" },
145
+ ],
146
+ }));
147
+ const classifier = new AiClassifier(new AiClient({ project, provider }));
148
+ // Feature desligada por default no fake config.
149
+ expect(classifier.enabled).toBe(false);
150
+ expect(
151
+ await classifier.classify(["a.ts"], [{ id: "alpha", name: "A", scope: "x" }]),
152
+ ).toEqual(new Map());
153
+ });
154
+ });
155
+
156
+ describe("AiVerifier", () => {
157
+ test("devolve confiança apenas para ids conhecidos", async () => {
158
+ const provider = new FakeModelProvider(() => ({
159
+ verdicts: [
160
+ { id: "f1", persisting: true, confidence: 0.9 },
161
+ { id: "nope", persisting: false, confidence: 0 },
162
+ ],
163
+ }));
164
+ const verifier = new AiVerifier(new AiClient({ project, provider }));
165
+ expect(verifier.enabled).toBe(false);
166
+ });
167
+ });
168
+
169
+ describe("AiPreAnalyzer", () => {
170
+ test("sem a feature ligada não gera candidatos", async () => {
171
+ const provider = new FakeModelProvider(() => ({ candidates: [] }));
172
+ const analyzer = new AiPreAnalyzer(new AiClient({ project, provider }));
173
+ expect(analyzer.enabled).toBe(false);
174
+ expect(
175
+ await analyzer.analyze({
176
+ watchdogId: "alpha",
177
+ name: "Alpha",
178
+ rules: alpha.rules,
179
+ files: [{ file: "a.ts", snippet: "x" }],
180
+ }),
181
+ ).toEqual([]);
182
+ });
183
+ });