@payconductor/houndsman 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/help.ts +17 -1
- 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 +59 -1
- 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/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/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/utils/constants.ts +27 -5
- package/src/server/client/utils/types.ts +6 -0
- package/src/server/index.ts +3 -2
- 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/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/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/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
package/src/core/brief.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* específico do domínio vive nas regras do watchdog (`rules`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { AstDelta } from "./ast/types";
|
|
8
9
|
import {
|
|
9
10
|
type Rule,
|
|
10
11
|
SEVERITY_EMOJI,
|
|
@@ -117,6 +118,141 @@ function deletedSection(args: WatchdogRenderArgs, def: WatchdogDefinition): stri
|
|
|
117
118
|
];
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/** Escapa um valor para uso em code-span (evita quebrar o markdown). */
|
|
122
|
+
function inlineCode(value: string): string {
|
|
123
|
+
return `\`${value.replace(/`/g, "'")}\``;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Trunca um texto longo (assinatura) para o brief. */
|
|
127
|
+
function shorten(value: string, max = 120): string {
|
|
128
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
129
|
+
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Linhas de um delta estrutural (compactas e determinísticas). */
|
|
133
|
+
function deltaLines(delta: AstDelta): string[] {
|
|
134
|
+
const lines: string[] = [];
|
|
135
|
+
for (const symbol of delta.addedSymbols.slice(0, 4)) {
|
|
136
|
+
lines.push(`+ ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
137
|
+
}
|
|
138
|
+
for (const symbol of delta.removedSymbols.slice(0, 4)) {
|
|
139
|
+
lines.push(`− ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
140
|
+
}
|
|
141
|
+
for (const symbol of delta.modifiedSymbols.slice(0, 4)) {
|
|
142
|
+
lines.push(`~ ${inlineCode(symbol.name)} (${symbol.kind})`);
|
|
143
|
+
}
|
|
144
|
+
for (const change of delta.changedSignatures.slice(0, 3)) {
|
|
145
|
+
lines.push(
|
|
146
|
+
`assinatura ${inlineCode(change.name)}: ${inlineCode(shorten(change.before) || "—")} → ${inlineCode(shorten(change.after) || "—")}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (delta.addedImports.length > 0) {
|
|
150
|
+
lines.push(
|
|
151
|
+
`imports + ${delta.addedImports.slice(0, 4).map(inlineCode).join(", ")}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (delta.removedImports.length > 0) {
|
|
155
|
+
lines.push(
|
|
156
|
+
`imports − ${delta.removedImports.slice(0, 4).map(inlineCode).join(", ")}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Plano de revisão sugerido por IA — complementa (nunca substitui) as regras.
|
|
164
|
+
*/
|
|
165
|
+
function aiPlanSection(args: WatchdogRenderArgs): string[] {
|
|
166
|
+
const plan = args.aiPlan;
|
|
167
|
+
if (!plan) return [];
|
|
168
|
+
const focus = plan.focus ?? [];
|
|
169
|
+
const questions = plan.questions ?? [];
|
|
170
|
+
const risks = plan.risks ?? [];
|
|
171
|
+
const logic = plan.logic ?? [];
|
|
172
|
+
const domain = plan.domain ?? [];
|
|
173
|
+
if (
|
|
174
|
+
focus.length === 0 &&
|
|
175
|
+
questions.length === 0 &&
|
|
176
|
+
risks.length === 0 &&
|
|
177
|
+
logic.length === 0 &&
|
|
178
|
+
domain.length === 0
|
|
179
|
+
) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const lines = [
|
|
183
|
+
"## Plano de revisão (IA) — sugestão",
|
|
184
|
+
"",
|
|
185
|
+
"> Sugestão automática para orientar a leitura. **Não** substitui as regras",
|
|
186
|
+
"> do watchdog nem dispensa confirmar cada violação no código.",
|
|
187
|
+
"",
|
|
188
|
+
];
|
|
189
|
+
if (focus.length > 0) {
|
|
190
|
+
lines.push("**Foco de leitura:**", "");
|
|
191
|
+
for (const item of focus.slice(0, 5)) lines.push(`- ${item}`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
}
|
|
194
|
+
if (questions.length > 0) {
|
|
195
|
+
lines.push("**Perguntas de verificação:**", "");
|
|
196
|
+
for (const item of questions.slice(0, 3)) lines.push(`- ${item}`);
|
|
197
|
+
lines.push("");
|
|
198
|
+
}
|
|
199
|
+
if (risks.length > 0) {
|
|
200
|
+
lines.push("**Riscos prováveis:**", "");
|
|
201
|
+
for (const item of risks.slice(0, 3)) lines.push(`- ${item}`);
|
|
202
|
+
lines.push("");
|
|
203
|
+
}
|
|
204
|
+
if (logic.length > 0) {
|
|
205
|
+
lines.push("**Consistência lógica:**", "");
|
|
206
|
+
for (const item of logic.slice(0, 3)) lines.push(`- ${item}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
if (domain.length > 0) {
|
|
210
|
+
lines.push("**Domínio de negócio:**", "");
|
|
211
|
+
for (const item of domain.slice(0, 3)) lines.push(`- ${item}`);
|
|
212
|
+
lines.push("");
|
|
213
|
+
}
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Estrutura/delta dos arquivos do watchdog (AST). É um mapa para orientar a
|
|
219
|
+
* leitura — o watchdog confirma no código e não trata como achado.
|
|
220
|
+
*/
|
|
221
|
+
function astSection(args: WatchdogRenderArgs): string[] {
|
|
222
|
+
const entries = args.ast ?? [];
|
|
223
|
+
if (entries.length === 0) return [];
|
|
224
|
+
const lines: string[] = [
|
|
225
|
+
"## Estrutura (AST)",
|
|
226
|
+
"",
|
|
227
|
+
"> Delta estrutural dos arquivos do seu domínio. Use como mapa para",
|
|
228
|
+
"> priorizar a leitura; **não** é achado por si só.",
|
|
229
|
+
"",
|
|
230
|
+
];
|
|
231
|
+
let rendered = 0;
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const parts = deltaLines(entry.delta);
|
|
234
|
+
if (entry.importers && entry.importers.length > 0) {
|
|
235
|
+
parts.push(
|
|
236
|
+
`impacto: ${entry.importers.slice(0, 5).map(inlineCode).join(", ")}` +
|
|
237
|
+
(entry.importers.length > 5
|
|
238
|
+
? ` (+${entry.importers.length - 5})`
|
|
239
|
+
: ""),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (parts.length === 0) continue;
|
|
243
|
+
lines.push(
|
|
244
|
+
`### ${inlineCode(entry.file)}`,
|
|
245
|
+
"",
|
|
246
|
+
...parts.map((part) => `- ${part}`),
|
|
247
|
+
"",
|
|
248
|
+
);
|
|
249
|
+
rendered++;
|
|
250
|
+
if (rendered >= 12) break;
|
|
251
|
+
}
|
|
252
|
+
if (rendered === 0) return [];
|
|
253
|
+
return lines;
|
|
254
|
+
}
|
|
255
|
+
|
|
120
256
|
/**
|
|
121
257
|
* Render padrão do brief de um watchdog (ordem estável).
|
|
122
258
|
* `def` é a definição do próprio watchdog (evita ciclo com o registry).
|
|
@@ -139,6 +275,8 @@ export function defaultRenderBrief(
|
|
|
139
275
|
|
|
140
276
|
if (args.context) lines.push(...renderBriefContext(args.context));
|
|
141
277
|
|
|
278
|
+
lines.push(...aiPlanSection(args));
|
|
279
|
+
lines.push(...astSection(args));
|
|
142
280
|
lines.push(...optionalList("Prioridade de leitura", def.reviewFocus));
|
|
143
281
|
lines.push(
|
|
144
282
|
"## Regras (por severidade)",
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* com o UUID interno no banco.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { AiClient } from "../ai/ai-client";
|
|
14
|
+
import { AiVerifier } from "../ai/features/verifier";
|
|
13
15
|
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
16
|
+
import { ENV_NAMES } from "../env";
|
|
14
17
|
import {
|
|
15
18
|
applyFindingUpdate,
|
|
16
19
|
FINDING_DECISIONS,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
} from "../github-review";
|
|
41
44
|
import { VERIFY_FILE } from "../paths";
|
|
42
45
|
import { currentBranch } from "../pr-context";
|
|
46
|
+
import { ProjectConfig } from "../project-config";
|
|
43
47
|
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
44
48
|
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
45
49
|
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
@@ -485,6 +489,20 @@ export async function runVerifyCommand(
|
|
|
485
489
|
}
|
|
486
490
|
}
|
|
487
491
|
|
|
492
|
+
/** Trecho de ~20 linhas ao redor do finding (entrada da verificação por IA). */
|
|
493
|
+
function snippetsAround(content: string | null, line: number | null): string {
|
|
494
|
+
if (!content) return "(arquivo indisponível)";
|
|
495
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
496
|
+
const center = line && line > 0 ? line : 1;
|
|
497
|
+
const start = Math.max(1, center - 10);
|
|
498
|
+
const end = Math.min(lines.length, center + 10);
|
|
499
|
+
return lines
|
|
500
|
+
.slice(start - 1, end)
|
|
501
|
+
.map((text, index) => `${start + index}: ${text}`)
|
|
502
|
+
.join("\n")
|
|
503
|
+
.slice(0, 4_000);
|
|
504
|
+
}
|
|
505
|
+
|
|
488
506
|
async function executeVerify(
|
|
489
507
|
config: HoundsmanConfig,
|
|
490
508
|
storage: StorageProvider,
|
|
@@ -510,7 +528,7 @@ async function executeVerify(
|
|
|
510
528
|
if (parsed.bools.has("--no-jev")) {
|
|
511
529
|
jevReason = "desativado (--no-jev)";
|
|
512
530
|
} else if (!jevVerifier.isConfigured()) {
|
|
513
|
-
jevReason =
|
|
531
|
+
jevReason = `${ENV_NAMES.typeSafeApiKey} não configurada`;
|
|
514
532
|
} else {
|
|
515
533
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
516
534
|
const outcome = await jevVerifier.verify(
|
|
@@ -531,6 +549,44 @@ async function executeVerify(
|
|
|
531
549
|
}
|
|
532
550
|
}
|
|
533
551
|
|
|
552
|
+
// Tier 2 — IA opcional para os ambíguos que o Jev não respondeu.
|
|
553
|
+
let aiChecked = 0;
|
|
554
|
+
let aiReason: string | undefined;
|
|
555
|
+
const stillAmbiguous = entries.filter(
|
|
556
|
+
(entry) => entry.ambiguous && entry.basis === "anchor",
|
|
557
|
+
);
|
|
558
|
+
if (stillAmbiguous.length > 0) {
|
|
559
|
+
const client = new AiClient({ project: new ProjectConfig(config) });
|
|
560
|
+
const aiVerifier = new AiVerifier(client);
|
|
561
|
+
if (parsed.bools.has("--no-ai")) {
|
|
562
|
+
aiReason = "desativado (--no-ai)";
|
|
563
|
+
} else if (!client.available) {
|
|
564
|
+
aiReason = "IA não configurada";
|
|
565
|
+
} else if (!aiVerifier.enabled) {
|
|
566
|
+
aiReason = "feature ai.features.verify desligada";
|
|
567
|
+
} else {
|
|
568
|
+
const readFile = fileReader(process.cwd());
|
|
569
|
+
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
570
|
+
const confidence = await aiVerifier.verify({
|
|
571
|
+
findings: stillAmbiguous.map((entry) => ({
|
|
572
|
+
id: entry.id,
|
|
573
|
+
rule: entry.rule,
|
|
574
|
+
file: entry.file,
|
|
575
|
+
line: entry.line,
|
|
576
|
+
message: byId.get(entry.id)?.message ?? entry.message,
|
|
577
|
+
snippets: snippetsAround(readFile(entry.file), entry.line),
|
|
578
|
+
})),
|
|
579
|
+
});
|
|
580
|
+
aiChecked = verifier.applyConfidence(
|
|
581
|
+
entries,
|
|
582
|
+
confidence,
|
|
583
|
+
JevCandidatesStore.THRESHOLD,
|
|
584
|
+
"ai",
|
|
585
|
+
);
|
|
586
|
+
aiReason = `${aiChecked} caso(s) confirmado(s) pela IA`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
534
590
|
const summary = verifier.summarize(entries);
|
|
535
591
|
const location = ctx.storage.location(ctx.review.id);
|
|
536
592
|
ctx.storage.writeArtifact(ctx.review.id, {
|
|
@@ -557,6 +613,8 @@ async function executeVerify(
|
|
|
557
613
|
summary,
|
|
558
614
|
jevChecked,
|
|
559
615
|
jevReason,
|
|
616
|
+
aiChecked,
|
|
617
|
+
aiReason,
|
|
560
618
|
applied,
|
|
561
619
|
entries,
|
|
562
620
|
},
|
|
@@ -28,7 +28,9 @@ export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
|
28
28
|
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
29
29
|
if (parser.has("--clean")) opts.clean = true;
|
|
30
30
|
if (parser.has("--no-jev")) opts.noJev = true;
|
|
31
|
+
if (parser.has("--no-ast")) opts.noAst = true;
|
|
31
32
|
if (parser.has("--full")) opts.full = true;
|
|
33
|
+
if (parser.has("--no-ai")) opts.noAi = true;
|
|
32
34
|
|
|
33
35
|
return opts;
|
|
34
36
|
}
|
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
* ├── .gitignore AUTO — ignora core/ (gerado)
|
|
9
9
|
* ├── core/types.ts AUTO — snapshot dos tipos + helpers
|
|
10
10
|
* ├── core/skill/*.md AUTO — docs do skill (com {{projectName}} resolvido)
|
|
11
|
+
* ├── core/grammars/*.wasm AUTO — runtime + grammars do AST
|
|
11
12
|
* ├── watchdogs/*.ts USUÁRIO
|
|
12
13
|
* ├── doc-mappings.ts USUÁRIO
|
|
13
14
|
* └── exclusions.ts USUÁRIO
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
copyFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
17
25
|
import { basename, join } from "node:path";
|
|
18
|
-
import { SKILL_DIR, SKILL_FILE } from "../assets";
|
|
26
|
+
import { GRAMMAR_FILES, grammarPath, SKILL_DIR, SKILL_FILE } from "../assets";
|
|
19
27
|
import { CORE_DIR, CORE_SKILL_DIR, OUTPUT_FORMAT_PATH, PROJECT_DIR } from "../paths";
|
|
20
28
|
|
|
21
29
|
export type Harness = "agents" | "claude";
|
|
@@ -123,11 +131,21 @@ export type Rule = {
|
|
|
123
131
|
severity: Severity;
|
|
124
132
|
text: string;
|
|
125
133
|
jev?: JevRuleSpec;
|
|
134
|
+
hints?: RuleHints;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Sinais opcionais de uma regra (símbolos, imports, decorators, paths, checks). */
|
|
138
|
+
export type RuleHints = {
|
|
139
|
+
symbols?: string[];
|
|
140
|
+
imports?: string[];
|
|
141
|
+
decorators?: string[];
|
|
142
|
+
paths?: string[];
|
|
143
|
+
checks?: string[];
|
|
126
144
|
};
|
|
127
145
|
|
|
128
146
|
/** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
|
|
129
147
|
export type ClassifyRule = {
|
|
130
|
-
type?: "filename" | "content";
|
|
148
|
+
type?: "filename" | "content" | "symbol" | "import" | "export" | "decorator";
|
|
131
149
|
value: string;
|
|
132
150
|
};
|
|
133
151
|
|
|
@@ -144,6 +162,10 @@ export type WatchdogDefinition = WatchdogMeta & {
|
|
|
144
162
|
id: string;
|
|
145
163
|
description: string;
|
|
146
164
|
breakingChange?: boolean;
|
|
165
|
+
/** Só assume arquivos que nenhum outro watchdog casou. */
|
|
166
|
+
fallback?: boolean;
|
|
167
|
+
/** Permite findings emergentes (Lógica:/Domínio:) fora das regras. */
|
|
168
|
+
allowEmergentFindings?: boolean;
|
|
147
169
|
reviewFocus?: readonly string[];
|
|
148
170
|
exclude?: readonly string[];
|
|
149
171
|
};
|
|
@@ -182,6 +204,54 @@ export type HoundsmanStorage = {
|
|
|
182
204
|
path?: string;
|
|
183
205
|
};
|
|
184
206
|
|
|
207
|
+
/** Linguagens suportadas pelo AST na primeira fase. */
|
|
208
|
+
export type AstLanguage = "typescript" | "tsx" | "javascript";
|
|
209
|
+
|
|
210
|
+
/** Configuração do AST determinístico (classificação estrutural, brief e impacto). */
|
|
211
|
+
export type HoundsmanAst = {
|
|
212
|
+
enabled?: boolean;
|
|
213
|
+
maxFileBytes?: number;
|
|
214
|
+
maxImpactFiles?: number;
|
|
215
|
+
languages?: Record<string, AstLanguage>;
|
|
216
|
+
aliases?: Record<string, string>;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Providers suportados pela camada de IA. */
|
|
220
|
+
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
221
|
+
|
|
222
|
+
/** Features de IA do fluxo normal (todas fail-open). */
|
|
223
|
+
export type HoundsmanAiFeatures = {
|
|
224
|
+
context?: boolean;
|
|
225
|
+
brief?: boolean;
|
|
226
|
+
classification?: boolean;
|
|
227
|
+
preAnalysis?: boolean;
|
|
228
|
+
verify?: boolean;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** Overrides de IA por watchdog. */
|
|
232
|
+
export type HoundsmanAiWatchdog = {
|
|
233
|
+
model?: string;
|
|
234
|
+
maxSteps?: number;
|
|
235
|
+
maxOutputTokens?: number;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/** Configuração da camada de IA (briefs, contexto, classificação e hunt nativo). */
|
|
239
|
+
export type HoundsmanAi = {
|
|
240
|
+
enabled?: boolean;
|
|
241
|
+
provider?: AiProviderKind;
|
|
242
|
+
model?: string;
|
|
243
|
+
baseURL?: string;
|
|
244
|
+
concurrency?: number;
|
|
245
|
+
maxSteps?: number;
|
|
246
|
+
maxOutputTokens?: number;
|
|
247
|
+
temperature?: number;
|
|
248
|
+
timeoutMs?: number;
|
|
249
|
+
maxCallsPerReview?: number;
|
|
250
|
+
maxTokensPerReview?: number;
|
|
251
|
+
features?: HoundsmanAiFeatures;
|
|
252
|
+
watchdogs?: Record<string, HoundsmanAiWatchdog>;
|
|
253
|
+
};
|
|
254
|
+
|
|
185
255
|
/** Contrato de configuração do motor. */
|
|
186
256
|
export type HoundsmanConfig<TId extends string = string> = {
|
|
187
257
|
projectName: string;
|
|
@@ -193,6 +263,8 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
193
263
|
alwaysIncludeDocs?: readonly string[];
|
|
194
264
|
budgets?: HoundsmanBudgets;
|
|
195
265
|
concurrency?: HoundsmanConcurrency;
|
|
266
|
+
ast?: HoundsmanAst;
|
|
267
|
+
ai?: HoundsmanAi;
|
|
196
268
|
};
|
|
197
269
|
|
|
198
270
|
/** Helper de autoria (identidade) — apenas tipagem. */
|
|
@@ -212,7 +284,8 @@ const WATCHDOG_EXAMPLE = `import { defineWatchdog } from "../core/types";
|
|
|
212
284
|
|
|
213
285
|
/**
|
|
214
286
|
* Watchdog de exemplo — substitua pelas regras reais do projeto.
|
|
215
|
-
* Um watchdog declara seus arquivos via \`classify\` (regex de path ou
|
|
287
|
+
* Um watchdog declara seus arquivos via \`classify\` (regex de path, conteúdo ou
|
|
288
|
+
* fatos AST: \`symbol\`/\`import\`/\`export\`/\`decorator\`).
|
|
216
289
|
* O texto de cada regra começa com a polaridade: \`Promover:\` (DEVE acontecer)
|
|
217
290
|
* ou \`Evitar:\`/\`Não\`/\`Nunca\` (NÃO DEVE acontecer).
|
|
218
291
|
*/
|
|
@@ -220,8 +293,8 @@ export const exampleWatchdog = defineWatchdog({
|
|
|
220
293
|
id: "example",
|
|
221
294
|
name: "Exemplo",
|
|
222
295
|
description: "Watchdog de exemplo — descreva o propósito em uma frase.",
|
|
223
|
-
scope: "
|
|
224
|
-
classify: [{ value: "^src/" }],
|
|
296
|
+
scope: "src/example/** (ajuste o escopo)",
|
|
297
|
+
classify: [{ value: "^src/example/" }],
|
|
225
298
|
rules: [
|
|
226
299
|
{
|
|
227
300
|
severity: "attention",
|
|
@@ -237,13 +310,65 @@ export const exampleWatchdog = defineWatchdog({
|
|
|
237
310
|
});
|
|
238
311
|
`;
|
|
239
312
|
|
|
240
|
-
const
|
|
313
|
+
const DEFAULT_WATCHDOG = `import { defineWatchdog } from "../core/types";
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Watchdog de padrões gerais e lógica (fallback).
|
|
317
|
+
*
|
|
318
|
+
* Só assume arquivos que nenhum outro watchdog casou. Cobre consistência
|
|
319
|
+
* lógica, invariantes de domínio e defeitos genéricos que não pertencem a um
|
|
320
|
+
* domínio específico. Findings fora das regras abaixo podem ser reportados com
|
|
321
|
+
* os prefixos reservados "Lógica:" e "Domínio:" (opt-in abaixo).
|
|
322
|
+
*/
|
|
323
|
+
export const defaultWatchdog = defineWatchdog({
|
|
324
|
+
id: "default",
|
|
325
|
+
name: "Padrões gerais e lógica",
|
|
326
|
+
description:
|
|
327
|
+
"Cobre arquivos sem domínio específico: consistência lógica, domínio e defeitos genéricos.",
|
|
328
|
+
scope: "Arquivos não classificados por outros watchdogs",
|
|
329
|
+
fallback: true,
|
|
330
|
+
allowEmergentFindings: true,
|
|
331
|
+
rules: [
|
|
332
|
+
{
|
|
333
|
+
severity: "attention",
|
|
334
|
+
text: "Promover: nome, contrato e documentação do símbolo correspondem ao comportamento real.",
|
|
335
|
+
hints: { checks: ["symbol_context"] },
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
severity: "attention",
|
|
339
|
+
text: "Evitar: engolir erros/exceções sem propagar, logar ou tratar.",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
severity: "attention",
|
|
343
|
+
text: "Promover: transições de estado e efeitos colaterais do domínio são explícitos, ordenados e idempotentes.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
severity: "suggestion",
|
|
347
|
+
text: "Evitar: lógica de negócio duplicada ou divergente entre camadas.",
|
|
348
|
+
hints: { checks: ["find_references"] },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
severity: "suggestion",
|
|
352
|
+
text: "Promover: casos de borda do domínio (vazio, zero, negativo, timezone, arredondamento) são tratados.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
severity: "suggestion",
|
|
356
|
+
text: "Promover: a lógica do domínio respeita as invariantes e contratos documentados.",
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
contextFiles: ["AGENTS.md"],
|
|
360
|
+
});
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
const WATCHDOGS_INDEX = `import { defaultWatchdog } from "./default";
|
|
364
|
+
import { exampleWatchdog } from "./example";
|
|
241
365
|
|
|
242
366
|
/**
|
|
243
367
|
* Definições dos watchdogs — as chaves definem o union \`WatchdogId\`.
|
|
244
368
|
* Adicionar um watchdog = criar \`<id>.ts\` e registrá-lo aqui.
|
|
245
369
|
*/
|
|
246
370
|
export const WATCHDOG_DEFINITIONS = {
|
|
371
|
+
default: defaultWatchdog,
|
|
247
372
|
example: exampleWatchdog,
|
|
248
373
|
};
|
|
249
374
|
|
|
@@ -267,9 +392,10 @@ export function isExcluded(filePath: string): boolean {
|
|
|
267
392
|
const DOC_MAPPINGS_TEMPLATE = `/**
|
|
268
393
|
* Mapeamento de padrões nos paths → documentação relevante.
|
|
269
394
|
* Consumido pelo motor via \`HoundsmanConfig.docMappings\`.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
395
|
+
*
|
|
396
|
+
* Exemplo: [/^src\\//, ["README.md"]]
|
|
397
|
+
*/
|
|
398
|
+
export const DOC_MAPPINGS: readonly (readonly [RegExp, readonly string[]])[] = [];
|
|
273
399
|
`;
|
|
274
400
|
|
|
275
401
|
const PROJECT_GITIGNORE = `# Diretórios gerados pelo houndsman — \`core/\` é regenerado por \`houndsman update\`
|
|
@@ -307,6 +433,14 @@ class WatchdogExampleTemplate implements ScaffoldTemplate {
|
|
|
307
433
|
}
|
|
308
434
|
}
|
|
309
435
|
|
|
436
|
+
class DefaultWatchdogTemplate implements ScaffoldTemplate {
|
|
437
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "default.ts");
|
|
438
|
+
|
|
439
|
+
render(): string {
|
|
440
|
+
return DEFAULT_WATCHDOG;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
310
444
|
class WatchdogsIndexTemplate implements ScaffoldTemplate {
|
|
311
445
|
readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
|
|
312
446
|
|
|
@@ -402,6 +536,7 @@ export class ProjectScaffolder {
|
|
|
402
536
|
const context = this.context();
|
|
403
537
|
const templates: ScaffoldTemplate[] = [
|
|
404
538
|
new ConfigTemplate(),
|
|
539
|
+
new DefaultWatchdogTemplate(),
|
|
405
540
|
new WatchdogExampleTemplate(),
|
|
406
541
|
new WatchdogsIndexTemplate(),
|
|
407
542
|
new ExclusionsTemplate(),
|
|
@@ -415,6 +550,7 @@ export class ProjectScaffolder {
|
|
|
415
550
|
const context = this.context();
|
|
416
551
|
this.writeGeneratedFile(new TypesTemplate(), context);
|
|
417
552
|
this.writeGeneratedFile(new GitignoreTemplate(), context);
|
|
553
|
+
this.syncGrammars();
|
|
418
554
|
|
|
419
555
|
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
|
|
420
556
|
// point dos harnesses, não um doc da camada.
|
|
@@ -428,6 +564,19 @@ export class ProjectScaffolder {
|
|
|
428
564
|
}
|
|
429
565
|
}
|
|
430
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Copia as grammars wasm para a camada. O consumidor não precisa rodar nada:
|
|
569
|
+
* `setup` e `update` sincronizam os binários junto dos demais auto-gerados.
|
|
570
|
+
*/
|
|
571
|
+
private syncGrammars(): void {
|
|
572
|
+
const directory = join(this.ctx.cwd, CORE_DIR, "grammars");
|
|
573
|
+
mkdirSync(directory, { recursive: true });
|
|
574
|
+
for (const name of GRAMMAR_FILES) {
|
|
575
|
+
copyFileSync(grammarPath(name), join(directory, name));
|
|
576
|
+
}
|
|
577
|
+
this.ctx.generated.push(join(CORE_DIR, "grammars", "*.wasm"));
|
|
578
|
+
}
|
|
579
|
+
|
|
431
580
|
private context(): TemplateContext {
|
|
432
581
|
return { projectName: this.ctx.projectName };
|
|
433
582
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcomando `update` — sincroniza os arquivos **auto-gerados** da camada `.houndsman/`.
|
|
3
3
|
*
|
|
4
|
-
* Reescreve `core/types.ts`, `core/skill/*.md`,
|
|
5
|
-
* ponteiros de skill dos harnesses. Nunca toca nos
|
|
6
|
-
* (`watchdogs/`, `doc-mappings.ts`, `exclusions.ts`,
|
|
4
|
+
* Reescreve `core/types.ts`, `core/skill/*.md`, `core/grammars/*.wasm`,
|
|
5
|
+
* `.houndsman/.gitignore` e os ponteiros de skill dos harnesses. Nunca toca nos
|
|
6
|
+
* arquivos do usuário (`watchdogs/`, `doc-mappings.ts`, `exclusions.ts`,
|
|
7
|
+
* `houndsman.config.ts`). As grammars do AST são sincronizadas aqui — o
|
|
8
|
+
* consumidor não precisa rodar nenhum comando extra.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { createScaffoldContext, syncGeneratedFiles } from "./scaffold";
|
package/src/core/config.ts
CHANGED
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
* (camada `houndsman/`) — único ponto de troca ao portar para outro repositório.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { AstLanguage } from "./ast/types";
|
|
9
10
|
import { ProjectConfig } from "./project-config";
|
|
10
11
|
import type { WatchdogDefinition } from "./types";
|
|
11
12
|
|
|
12
13
|
export {
|
|
14
|
+
DEFAULT_AI_CONCURRENCY,
|
|
15
|
+
DEFAULT_AI_MAX_CALLS_PER_REVIEW,
|
|
16
|
+
DEFAULT_AI_MAX_OUTPUT_TOKENS,
|
|
17
|
+
DEFAULT_AI_MAX_STEPS,
|
|
18
|
+
DEFAULT_AI_MODEL,
|
|
19
|
+
DEFAULT_AI_PROVIDER,
|
|
20
|
+
DEFAULT_AI_TIMEOUT_MS,
|
|
13
21
|
DEFAULT_ALWAYS_INCLUDE_DOCS,
|
|
22
|
+
DEFAULT_AST_MAX_FILE_BYTES,
|
|
23
|
+
DEFAULT_AST_MAX_IMPACT_FILES,
|
|
14
24
|
DEFAULT_JEV_CONCURRENCY,
|
|
15
25
|
DEFAULT_MAX_FILES_PER_BRIEF,
|
|
16
26
|
} from "./defaults";
|
|
@@ -21,12 +31,83 @@ export type HoundsmanBudgets = {
|
|
|
21
31
|
maxFilesPerBrief?: number;
|
|
22
32
|
};
|
|
23
33
|
|
|
34
|
+
/** Configuração do subsistema AST (determinístico, sem LLM). */
|
|
35
|
+
export type HoundsmanAst = {
|
|
36
|
+
/**
|
|
37
|
+
* Liga/desliga o AST (default: `true`). Com grammars ausentes ou ABI
|
|
38
|
+
* incompatível, o motor segue sem AST (fail-open).
|
|
39
|
+
*/
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
/** Teto de bytes por arquivo (default: 512_000). */
|
|
42
|
+
maxFileBytes?: number;
|
|
43
|
+
/** Teto de arquivos candidatos na análise de impacto (default: 200). */
|
|
44
|
+
maxImpactFiles?: number;
|
|
45
|
+
/** Override de extensão → linguagem (`".vue": "typescript"`). */
|
|
46
|
+
languages?: Record<string, AstLanguage>;
|
|
47
|
+
/** Aliases de import (`"@/": "src/"`). */
|
|
48
|
+
aliases?: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
|
|
24
51
|
/** Concorrência das fases paralelas internas. */
|
|
25
52
|
export type HoundsmanConcurrency = {
|
|
26
53
|
/** Chamadas Jev simultâneas (default: 5). */
|
|
27
54
|
jev?: number;
|
|
28
55
|
};
|
|
29
56
|
|
|
57
|
+
/** Providers suportados pela camada de IA. */
|
|
58
|
+
export type AiProviderKind = "deepseek" | "openai" | "anthropic";
|
|
59
|
+
|
|
60
|
+
/** Features de IA do fluxo determinístico (todas fail-open). */
|
|
61
|
+
export type HoundsmanAiFeatures = {
|
|
62
|
+
/** Resume o contexto da PR (`context-summary`) quando o subagente não gerou. */
|
|
63
|
+
context?: boolean;
|
|
64
|
+
/** Enriquece os briefs com um plano de revisão sugerido. */
|
|
65
|
+
brief?: boolean;
|
|
66
|
+
/** Classifica arquivos que nenhum watchdog casou. */
|
|
67
|
+
classification?: boolean;
|
|
68
|
+
/** Gera candidatos de pré-análise (substitui o Jev quando disponível). */
|
|
69
|
+
preAnalysis?: boolean;
|
|
70
|
+
/** Confirma/refuta findings ambíguos no `verify`. */
|
|
71
|
+
verify?: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Overrides de IA por watchdog. */
|
|
75
|
+
export type HoundsmanAiWatchdog = {
|
|
76
|
+
model?: string;
|
|
77
|
+
maxSteps?: number;
|
|
78
|
+
maxOutputTokens?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Configuração da camada de IA (genérica; o harness nativo também a consome). */
|
|
82
|
+
export type HoundsmanAi = {
|
|
83
|
+
/** Liga a IA do fluxo normal (default: `true`; sem chave é fail-open). */
|
|
84
|
+
enabled?: boolean;
|
|
85
|
+
/** Provider de modelo (default: `deepseek`). */
|
|
86
|
+
provider?: AiProviderKind;
|
|
87
|
+
/** Modelo (default em `defaults.ts`). */
|
|
88
|
+
model?: string;
|
|
89
|
+
/** Base URL customizada do provider. */
|
|
90
|
+
baseURL?: string;
|
|
91
|
+
/** Chamadas simultâneas (default: 2). */
|
|
92
|
+
concurrency?: number;
|
|
93
|
+
/** Passos por agente (default: 12). */
|
|
94
|
+
maxSteps?: number;
|
|
95
|
+
/** Teto de tokens de saída por chamada. */
|
|
96
|
+
maxOutputTokens?: number;
|
|
97
|
+
/** Temperatura (default: 0). */
|
|
98
|
+
temperature?: number;
|
|
99
|
+
/** Timeout por chamada em ms (default: 120_000). */
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
/** Teto de chamadas por review (default: 40). */
|
|
102
|
+
maxCallsPerReview?: number;
|
|
103
|
+
/** Teto de tokens por review. */
|
|
104
|
+
maxTokensPerReview?: number;
|
|
105
|
+
/** Features do fluxo normal. */
|
|
106
|
+
features?: HoundsmanAiFeatures;
|
|
107
|
+
/** Overrides por watchdog. */
|
|
108
|
+
watchdogs?: Record<string, HoundsmanAiWatchdog>;
|
|
109
|
+
};
|
|
110
|
+
|
|
30
111
|
/** Backend de persistência (reviews, findings e artefatos). */
|
|
31
112
|
export type HoundsmanStorage = {
|
|
32
113
|
/**
|
|
@@ -68,6 +149,10 @@ export type HoundsmanConfig<TId extends string = string> = {
|
|
|
68
149
|
budgets?: HoundsmanBudgets;
|
|
69
150
|
/** Concorrência das fases paralelas internas. */
|
|
70
151
|
concurrency?: HoundsmanConcurrency;
|
|
152
|
+
/** AST determinístico (classificação estrutural, brief e impacto). */
|
|
153
|
+
ast?: HoundsmanAst;
|
|
154
|
+
/** Camada de IA (briefs, contexto, classificação e o harness nativo). */
|
|
155
|
+
ai?: HoundsmanAi;
|
|
71
156
|
};
|
|
72
157
|
|
|
73
158
|
/** Entrada de `defineHoundsmanConfig` — `watchdogIds` é derivado, não informado. */
|