@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.
- package/README.md +35 -41
- 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/status-command.ts +46 -0
- package/src/cli/help.ts +18 -1
- package/src/cli/index.ts +13 -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 +120 -6
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/status.ts +63 -0
- 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 +77 -13
- 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/findings-state.ts +21 -4
- package/src/core/github-review.ts +620 -57
- 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 +454 -16
- package/src/core/paths.ts +3 -0
- 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/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/rule-text.ts +97 -0
- package/src/core/run-status.ts +39 -5
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/file/file-storage.ts +2 -0
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/sqlite/mappers.ts +2 -0
- package/src/core/storage/sqlite/schema.ts +9 -1
- package/src/core/storage/sqlite/sqlite-storage.ts +5 -1
- package/src/core/storage/types.ts +15 -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 +26 -9
- package/src/server/client/ArtifactsPanel.tsx +12 -5
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +14 -10
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +92 -12
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +49 -6
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/{constants.ts → utils/constants.ts} +61 -10
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +64 -0
- package/src/server/index.ts +66 -7
- package/src/server/run-repository.ts +9 -0
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/example-review.md +22 -14
- package/src/skill/hunt.md +45 -19
- package/src/skill/verify.md +3 -2
- package/src/tests/__snapshots__/baseline.test.ts.snap +126 -42
- 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/baseline.test.ts +3 -0
- package/src/tests/code-highlight.test.ts +1 -1
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/findings-state.test.ts +50 -2
- 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 +409 -60
- 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/markdown.test.ts +61 -1
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +73 -0
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +53 -0
- package/src/tests/server.test.ts +76 -21
- package/src/tests/storage.test.ts +63 -0
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/markdown.ts +0 -153
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{code-highlight.ts → utils/code-highlight.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -0
- /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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
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: "
|
|
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 {
|
|
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": "
|
|
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": "
|
|
601
|
+
"sha256": "b74af7e1979bf1b4",
|
|
528
602
|
},
|
|
529
603
|
{
|
|
530
604
|
"file": ".houndsman/core/skill/example-review.md",
|
|
531
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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
|
+
});
|