@payconductor/houndsman 0.4.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 (196) hide show
  1. package/README.md +35 -41
  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/commands/status-command.ts +46 -0
  11. package/src/cli/help.ts +18 -1
  12. package/src/cli/index.ts +13 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +120 -6
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/status.ts +63 -0
  45. package/src/core/commands/update.ts +5 -3
  46. package/src/core/config.ts +85 -0
  47. package/src/core/defaults.ts +27 -0
  48. package/src/core/domain/watchdog.ts +43 -5
  49. package/src/core/engine/houndsman-engine.ts +16 -1
  50. package/src/core/engine/review-scope.ts +77 -13
  51. package/src/core/engine/review-session.ts +27 -2
  52. package/src/core/env.ts +101 -0
  53. package/src/core/file-classifier.ts +25 -4
  54. package/src/core/findings-state.ts +21 -4
  55. package/src/core/github-review.ts +620 -57
  56. package/src/core/jev/jev-client.ts +8 -7
  57. package/src/core/jev/jev-verify.ts +2 -1
  58. package/src/core/logging/index.ts +17 -0
  59. package/src/core/logging/logger.ts +187 -0
  60. package/src/core/orchestrator.ts +454 -16
  61. package/src/core/paths.ts +3 -0
  62. package/src/core/project-config.ts +123 -1
  63. package/src/core/registry-check.ts +148 -6
  64. package/src/core/renderers/manifest-writer.ts +33 -0
  65. package/src/core/renderers/resume-renderer.ts +151 -0
  66. package/src/core/report-builder.ts +7 -4
  67. package/src/core/rule-text.ts +97 -0
  68. package/src/core/run-status.ts +39 -5
  69. package/src/core/services/file-classifier.ts +10 -3
  70. package/src/core/services/verifier.ts +14 -3
  71. package/src/core/storage/file/file-storage.ts +2 -0
  72. package/src/core/storage/paths.ts +6 -5
  73. package/src/core/storage/sqlite/mappers.ts +2 -0
  74. package/src/core/storage/sqlite/schema.ts +9 -1
  75. package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
  76. package/src/core/storage/types.ts +15 -0
  77. package/src/core/storage/work-area.ts +12 -0
  78. package/src/core/types.ts +37 -1
  79. package/src/core/verify.ts +17 -5
  80. package/src/harness/README.md +15 -0
  81. package/src/harness/agent/agent-context.ts +33 -0
  82. package/src/harness/agent/agent-errors.ts +30 -0
  83. package/src/harness/agent/agent-factory.ts +45 -0
  84. package/src/harness/agent/agent-reporter.ts +34 -0
  85. package/src/harness/agent/agent-result.ts +16 -0
  86. package/src/harness/agent/agent-runner.ts +32 -0
  87. package/src/harness/agent/agent-scheduler.ts +37 -0
  88. package/src/harness/agent/review-agent.ts +391 -0
  89. package/src/harness/config/harness-config.ts +84 -0
  90. package/src/harness/hunt/hunt-harness.ts +83 -0
  91. package/src/harness/hunt/hunt-options.ts +75 -0
  92. package/src/harness/hunt/hunt-result.ts +17 -0
  93. package/src/harness/hunt/hunt-session.ts +372 -0
  94. package/src/harness/hunt/review-planner.ts +98 -0
  95. package/src/harness/index.ts +36 -0
  96. package/src/harness/investigation/agent-state.ts +134 -0
  97. package/src/harness/investigation/evidence-verifier.ts +129 -0
  98. package/src/harness/investigation/evidence.ts +110 -0
  99. package/src/harness/investigation/finding-critic.ts +96 -0
  100. package/src/harness/investigation/investigation-planner.ts +118 -0
  101. package/src/harness/investigation/investigation-task.ts +10 -0
  102. package/src/harness/investigation/review-journal.ts +87 -0
  103. package/src/harness/investigation/rule-analyzer.ts +178 -0
  104. package/src/harness/investigation/rule-signals.ts +110 -0
  105. package/src/harness/prompts/agent-schemas.ts +57 -0
  106. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  107. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  108. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  109. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  110. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  111. package/src/harness/tools/agent-tool.ts +102 -0
  112. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  113. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  114. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  115. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  116. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  117. package/src/harness/tools/builtin/helpers.ts +115 -0
  118. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  119. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  120. package/src/harness/tools/builtin/index.ts +59 -0
  121. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  122. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  123. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  124. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  125. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  126. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  127. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  128. package/src/harness/tools/path-guard.ts +90 -0
  129. package/src/harness/tools/result-limiter.ts +49 -0
  130. package/src/harness/tools/secret-redactor.ts +54 -0
  131. package/src/harness/tools/tool-cache.ts +50 -0
  132. package/src/harness/tools/tool-call-budget.ts +60 -0
  133. package/src/harness/tools/tool-context.ts +5 -0
  134. package/src/harness/tools/tool-middleware.ts +105 -0
  135. package/src/harness/tools/tool-policy.ts +51 -0
  136. package/src/harness/tools/tool-registry.ts +112 -0
  137. package/src/harness/tools/tool-result.ts +15 -0
  138. package/src/harness/tools/toolbox.ts +36 -0
  139. package/src/harness/validation/finding-validator.ts +119 -0
  140. package/src/index.ts +46 -0
  141. package/src/server/client/App.tsx +26 -9
  142. package/src/server/client/ArtifactsPanel.tsx +12 -5
  143. package/src/server/client/AutoRefresh.tsx +1 -1
  144. package/src/server/client/Filters.tsx +7 -2
  145. package/src/server/client/FindingCard.tsx +14 -10
  146. package/src/server/client/FindingsList.tsx +2 -2
  147. package/src/server/client/MarkdownText.tsx +92 -12
  148. package/src/server/client/ResumePanel.tsx +368 -0
  149. package/src/server/client/RunList.tsx +3 -3
  150. package/src/server/client/SubmitModal.tsx +49 -6
  151. package/src/server/client/SummaryBar.tsx +1 -1
  152. package/src/server/client/icons.tsx +17 -0
  153. package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
  154. package/src/server/client/utils/markdown.ts +319 -0
  155. package/src/server/client/{types.ts → utils/types.ts} +64 -0
  156. package/src/server/index.ts +66 -7
  157. package/src/server/run-repository.ts +9 -0
  158. package/src/skill/OUTPUT-FORMAT.md +12 -0
  159. package/src/skill/SETUP.md +6 -2
  160. package/src/skill/example-review.md +22 -14
  161. package/src/skill/hunt.md +45 -19
  162. package/src/skill/verify.md +3 -2
  163. package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
  164. package/src/tests/ai-core.test.ts +120 -0
  165. package/src/tests/ai-features.test.ts +183 -0
  166. package/src/tests/ast.test.ts +335 -0
  167. package/src/tests/baseline.test.ts +3 -0
  168. package/src/tests/code-highlight.test.ts +1 -1
  169. package/src/tests/env.test.ts +69 -0
  170. package/src/tests/fallback.test.ts +172 -0
  171. package/src/tests/findings-state.test.ts +50 -2
  172. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  173. package/src/tests/fixtures/fake-investigation.ts +64 -0
  174. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  175. package/src/tests/github-review.test.ts +409 -60
  176. package/src/tests/harness-agent.test.ts +122 -0
  177. package/src/tests/harness-investigation.test.ts +206 -0
  178. package/src/tests/harness-tools.test.ts +148 -0
  179. package/src/tests/hunt-command.test.ts +212 -0
  180. package/src/tests/logger.test.ts +83 -0
  181. package/src/tests/markdown.test.ts +61 -1
  182. package/src/tests/review-resume.test.ts +101 -0
  183. package/src/tests/review-scope.test.ts +302 -0
  184. package/src/tests/review-scripts.test.ts +73 -0
  185. package/src/tests/rule-analyzer.test.ts +61 -0
  186. package/src/tests/run-status-view.test.ts +94 -0
  187. package/src/tests/run-status.test.ts +53 -0
  188. package/src/tests/server.test.ts +76 -21
  189. package/src/tests/storage.test.ts +63 -0
  190. package/src/tests/tool-contracts.test.ts +82 -0
  191. package/src/tests/verify.test.ts +1 -1
  192. package/src/server/client/markdown.ts +0 -153
  193. /package/src/server/client/{api.ts → utils/api.ts} +0 -0
  194. /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
  195. /package/src/server/client/{format.ts → utils/format.ts} +0 -0
  196. /package/src/server/client/{json-highlight.ts → utils/json-highlight.ts} +0 -0
@@ -88,23 +88,6 @@ exports[`baseline — relatório renderReport consolidado 1`] = `
88
88
  - Faz A
89
89
  - Remove B
90
90
 
91
- ## ❌ blocking
92
-
93
- ### \`src/alpha/a.ts\`
94
-
95
- - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`id-alpha\`
96
- any detectado
97
- **Ação:** tipar
98
- **Evidência:** \`export const a: any = 1;\`
99
-
100
- ## ⚠️ attention
101
-
102
- ### \`src/alpha/b.ts\`
103
-
104
- - **Promover: validar a entrada.** — \`src/alpha/b.ts:2\` · _Alpha, Beta_ · \`id-beta\`
105
- sem validação
106
- **Ação:** adicionar schema
107
-
108
91
  ## Relatórios por domínio
109
92
 
110
93
  | Watchdog | Achados | Revisados | Relatório |
@@ -121,7 +104,25 @@ exports[`baseline — relatório renderReport consolidado 1`] = `
121
104
  ## Conclusão
122
105
 
123
106
  **Status:** ❌ Mudanças Necessárias
124
- **Resumo:** 1 violações bloqueantes, 1 pontos de atenção, 0 sugestões e 0 nits."
107
+ **Resumo:** 1 violações bloqueantes, 1 pontos de atenção, 0 sugestões e 0 nits.
108
+
109
+ ## ❌ blocking
110
+
111
+ ### \`src/alpha/a.ts\`
112
+
113
+ - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`id-alpha\`
114
+ any detectado
115
+ **Ação:** tipar
116
+ **Evidência:** \`export const a: any = 1;\`
117
+
118
+ ## ⚠️ attention
119
+
120
+ ### \`src/alpha/b.ts\`
121
+
122
+ - **Promover: validar a entrada.** — \`src/alpha/b.ts:2\` · _Alpha, Beta_ · \`id-beta\`
123
+ sem validação
124
+ **Ação:** adicionar schema
125
+ "
125
126
  `;
126
127
 
127
128
  exports[`baseline — relatório renderDomainReport isolado 1`] = `
@@ -171,6 +172,10 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 1
171
172
  ".agents/skills/houndsman/SKILL.md",
172
173
  ".claude/skills/houndsman/SKILL.md",
173
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",
174
179
  ".houndsman/core/skill/AUDIT-PROTOCOL.md",
175
180
  ".houndsman/core/skill/OUTPUT-FORMAT.md",
176
181
  ".houndsman/core/skill/README.md",
@@ -183,6 +188,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 1
183
188
  ".houndsman/core/types.ts",
184
189
  ".houndsman/doc-mappings.ts",
185
190
  ".houndsman/exclusions.ts",
191
+ ".houndsman/watchdogs/default.ts",
186
192
  ".houndsman/watchdogs/example.ts",
187
193
  ".houndsman/watchdogs/index.ts",
188
194
  "houndsman.config.ts",
@@ -329,11 +335,21 @@ export type Rule = {
329
335
  severity: Severity;
330
336
  text: string;
331
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[];
332
348
  };
333
349
 
334
350
  /** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
335
351
  export type ClassifyRule = {
336
- type?: "filename" | "content";
352
+ type?: "filename" | "content" | "symbol" | "import" | "export" | "decorator";
337
353
  value: string;
338
354
  };
339
355
 
@@ -350,6 +366,10 @@ export type WatchdogDefinition = WatchdogMeta & {
350
366
  id: string;
351
367
  description: string;
352
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;
353
373
  reviewFocus?: readonly string[];
354
374
  exclude?: readonly string[];
355
375
  };
@@ -388,6 +408,54 @@ export type HoundsmanStorage = {
388
408
  path?: string;
389
409
  };
390
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
+
391
459
  /** Contrato de configuração do motor. */
392
460
  export type HoundsmanConfig<TId extends string = string> = {
393
461
  projectName: string;
@@ -399,6 +467,8 @@ export type HoundsmanConfig<TId extends string = string> = {
399
467
  alwaysIncludeDocs?: readonly string[];
400
468
  budgets?: HoundsmanBudgets;
401
469
  concurrency?: HoundsmanConcurrency;
470
+ ast?: HoundsmanAst;
471
+ ai?: HoundsmanAi;
402
472
  };
403
473
 
404
474
  /** Helper de autoria (identidade) — apenas tipagem. */
@@ -418,9 +488,10 @@ export function defineHoundsmanConfig<TId extends string>(
418
488
  "/**
419
489
  * Mapeamento de padrões nos paths → documentação relevante.
420
490
  * Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
421
- */\`core/\` é regenerado por \`houndsman update\`.
422
- # Runs, findings e artefatos vivem no storage global (fora do projeto).
423
- core
491
+ *
492
+ * Exemplo: [/^src\\//, ["README.md"]]
493
+ */
494
+ export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [];
424
495
  "
425
496
  ,
426
497
  ".houndsman/exclusions.ts":
@@ -443,7 +514,8 @@ export function isExcluded(filePath: string): boolean {
443
514
 
444
515
  /**
445
516
  * Watchdog de exemplo — substitua pelas regras reais do projeto.
446
- * 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\`).
447
519
  * O texto de cada regra começa com a polaridade: \`Promover:\` (DEVE acontecer)
448
520
  * ou \`Evitar:\`/\`Não\`/\`Nunca\` (NÃO DEVE acontecer).
449
521
  */
@@ -451,8 +523,8 @@ export const exampleWatchdog = defineWatchdog({
451
523
  id: "example",
452
524
  name: "Exemplo",
453
525
  description: "Watchdog de exemplo — descreva o propósito em uma frase.",
454
- scope: "Todo o repositório (ajuste o escopo)",
455
- classify: [{ value: "^src/" }],
526
+ scope: "src/example/** (ajuste o escopo)",
527
+ classify: [{ value: "^src/example/" }],
456
528
  rules: [
457
529
  {
458
530
  severity: "attention",
@@ -469,13 +541,15 @@ export const exampleWatchdog = defineWatchdog({
469
541
  "
470
542
  ,
471
543
  ".houndsman/watchdogs/index.ts":
472
- "import { exampleWatchdog } from "./example";
544
+ "import { defaultWatchdog } from "./default";
545
+ import { exampleWatchdog } from "./example";
473
546
 
474
547
  /**
475
548
  * Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
476
549
  * Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
477
550
  */
478
551
  export const WATCHDOG_DEFINITIONS = {
552
+ default: defaultWatchdog,
479
553
  example: exampleWatchdog,
480
554
  };
481
555
 
@@ -516,7 +590,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
516
590
  },
517
591
  {
518
592
  "file": ".houndsman/core/skill/OUTPUT-FORMAT.md",
519
- "sha256": "06aedccd5688aaa8",
593
+ "sha256": "94ea2b8def871784",
520
594
  },
521
595
  {
522
596
  "file": ".houndsman/core/skill/README.md",
@@ -524,11 +598,11 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
524
598
  },
525
599
  {
526
600
  "file": ".houndsman/core/skill/SETUP.md",
527
- "sha256": "7ce23b97d9609ac2",
601
+ "sha256": "b74af7e1979bf1b4",
528
602
  },
529
603
  {
530
604
  "file": ".houndsman/core/skill/example-review.md",
531
- "sha256": "a2627e99ac5caead",
605
+ "sha256": "d9e75054cc9811dc",
532
606
  },
533
607
  {
534
608
  "file": ".houndsman/core/skill/fix.md",
@@ -536,7 +610,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
536
610
  },
537
611
  {
538
612
  "file": ".houndsman/core/skill/hunt.md",
539
- "sha256": "c47a24c6c36c1c03",
613
+ "sha256": "b55507d62d3c1fcd",
540
614
  },
541
615
  {
542
616
  "file": ".houndsman/core/skill/train.md",
@@ -544,7 +618,7 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
544
618
  },
545
619
  {
546
620
  "file": ".houndsman/core/skill/verify.md",
547
- "sha256": "7ae6e1ca8bf0a868",
621
+ "sha256": "780f4b078daa104b",
548
622
  },
549
623
  ]
550
624
  `;
@@ -554,6 +628,7 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
554
628
  Briefs: <cwd>/.home/work/<uuid>/briefs/
555
629
  Deletado (breaking change): src/alpha/old.ts
556
630
  Jev: pulado (desativado (--no-jev)).
631
+ AST: desabilitado ou sem grammar compatível.
557
632
  Sem contexto da PR (rode \`bun run houndsman context\` antes, se houver PR).
558
633
 
559
634
  Próximo passo: despachar os sub-agentes (um por brief em <cwd>/.home/work/<uuid>/briefs/).
@@ -602,8 +677,6 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
602
677
  "alpha": 2
603
678
  },
604
679
  "excludedCount": 0,
605
- "base": "<sha>",
606
- "head": "HEAD",
607
680
  "headSha": "<sha>",
608
681
  "mode": "full"
609
682
  }
@@ -649,6 +722,16 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
649
722
  "available": false,
650
723
  "reason": "desativado (--no-jev)"
651
724
  },
725
+ "ast": {
726
+ "enabled": false,
727
+ "languages": [],
728
+ "files": 0,
729
+ "impact": {
730
+ "affectedFiles": 0,
731
+ "importers": 0,
732
+ "exportedChanges": 0
733
+ }
734
+ },
652
735
  "deleted": [
653
736
  "src/alpha/old.ts"
654
737
  ],
@@ -713,15 +796,6 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
713
796
  **Watchdogs ativos:** alpha
714
797
  **Achados:** 1 bloqueantes · 0 atenção · 0 sugestões · 0 nits
715
798
 
716
- ## ❌ blocking
717
-
718
- ### \`src/alpha/a.ts\`
719
-
720
- - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`<id>\`
721
- any detectado
722
- **Ação:** tipar
723
- **Evidência:** \`export const a: any = 1;\`
724
-
725
799
  ## Relatórios por domínio
726
800
 
727
801
  | Watchdog | Achados | Revisados | Relatório |
@@ -738,6 +812,16 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
738
812
 
739
813
  **Status:** ❌ Mudanças Necessárias
740
814
  **Resumo:** 1 violações bloqueantes, 0 pontos de atenção, 0 sugestões e 0 nits.
815
+
816
+ ## ❌ blocking
817
+
818
+ ### \`src/alpha/a.ts\`
819
+
820
+ - **Nunca usar \`any\`.** — \`src/alpha/a.ts:1\` · _Alpha_ · \`<id>\`
821
+ any detectado
822
+ **Ação:** tipar
823
+ **Evidência:** \`export const a: any = 1;\`
824
+
741
825
  "
742
826
  `;
743
827
 
@@ -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
+ });