@payconductor/houndsman 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
|
@@ -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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
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: "
|
|
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 {
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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-pro");
|
|
20
|
+
expect(project.aiModel).toBe("deepseek-v4-pro");
|
|
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
|
+
});
|