@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.
Files changed (175) hide show
  1. package/README.md +24 -4
  2. package/assets/grammars/tree-sitter-javascript.wasm +0 -0
  3. package/assets/grammars/tree-sitter-tsx.wasm +0 -0
  4. package/assets/grammars/tree-sitter-typescript.wasm +0 -0
  5. package/assets/grammars/web-tree-sitter.wasm +0 -0
  6. package/package.json +11 -3
  7. package/src/cli/commands/command.ts +5 -0
  8. package/src/cli/commands/doctor-command.ts +116 -24
  9. package/src/cli/commands/hunt-command.ts +25 -0
  10. package/src/cli/commands/server-command.ts +7 -1
  11. package/src/cli/help.ts +18 -2
  12. package/src/cli/index.ts +10 -6
  13. package/src/core/ai/ai-client.ts +221 -0
  14. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  15. package/src/core/ai/errors.ts +36 -0
  16. package/src/core/ai/features/brief-enricher.ts +62 -0
  17. package/src/core/ai/features/classifier.ts +50 -0
  18. package/src/core/ai/features/context-summarizer.ts +31 -0
  19. package/src/core/ai/features/pre-analyzer.ts +51 -0
  20. package/src/core/ai/features/verifier.ts +45 -0
  21. package/src/core/ai/index.ts +56 -0
  22. package/src/core/ai/model-factory.ts +137 -0
  23. package/src/core/ai/prompts/base.ts +36 -0
  24. package/src/core/ai/prompts/brief.ts +87 -0
  25. package/src/core/ai/prompts/classification.ts +36 -0
  26. package/src/core/ai/prompts/context.ts +29 -0
  27. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  28. package/src/core/ai/prompts/verify.ts +40 -0
  29. package/src/core/ai/schemas.ts +110 -0
  30. package/src/core/ai/types.ts +76 -0
  31. package/src/core/ai/usage-budget.ts +75 -0
  32. package/src/core/ai/usage-recorder.ts +79 -0
  33. package/src/core/assets.ts +16 -0
  34. package/src/core/ast/diff.ts +220 -0
  35. package/src/core/ast/impact.ts +127 -0
  36. package/src/core/ast/parser.ts +167 -0
  37. package/src/core/ast/service.ts +233 -0
  38. package/src/core/ast/symbols.ts +294 -0
  39. package/src/core/ast/types.ts +125 -0
  40. package/src/core/brief.ts +138 -0
  41. package/src/core/commands/findings.ts +65 -2
  42. package/src/core/commands/review.ts +2 -0
  43. package/src/core/commands/scaffold.ts +159 -10
  44. package/src/core/commands/update.ts +5 -3
  45. package/src/core/config.ts +85 -0
  46. package/src/core/defaults.ts +27 -0
  47. package/src/core/domain/watchdog.ts +43 -5
  48. package/src/core/engine/houndsman-engine.ts +16 -1
  49. package/src/core/engine/review-scope.ts +52 -6
  50. package/src/core/engine/review-session.ts +27 -2
  51. package/src/core/env.ts +101 -0
  52. package/src/core/file-classifier.ts +25 -4
  53. package/src/core/git-runner.ts +25 -13
  54. package/src/core/github-review.ts +10 -7
  55. package/src/core/jev/jev-client.ts +8 -7
  56. package/src/core/jev/jev-verify.ts +2 -1
  57. package/src/core/logging/index.ts +17 -0
  58. package/src/core/logging/logger.ts +187 -0
  59. package/src/core/orchestrator.ts +357 -9
  60. package/src/core/project-config.ts +123 -1
  61. package/src/core/registry-check.ts +148 -6
  62. package/src/core/renderers/manifest-writer.ts +33 -0
  63. package/src/core/rule-text.ts +97 -0
  64. package/src/core/services/file-classifier.ts +10 -3
  65. package/src/core/services/gh-client.ts +38 -22
  66. package/src/core/services/verifier.ts +14 -3
  67. package/src/core/storage/paths.ts +6 -5
  68. package/src/core/storage/types.ts +6 -0
  69. package/src/core/storage/work-area.ts +12 -0
  70. package/src/core/types.ts +37 -1
  71. package/src/core/verify.ts +17 -5
  72. package/src/harness/README.md +15 -0
  73. package/src/harness/agent/agent-context.ts +33 -0
  74. package/src/harness/agent/agent-errors.ts +30 -0
  75. package/src/harness/agent/agent-factory.ts +45 -0
  76. package/src/harness/agent/agent-reporter.ts +34 -0
  77. package/src/harness/agent/agent-result.ts +16 -0
  78. package/src/harness/agent/agent-runner.ts +32 -0
  79. package/src/harness/agent/agent-scheduler.ts +37 -0
  80. package/src/harness/agent/review-agent.ts +391 -0
  81. package/src/harness/config/harness-config.ts +84 -0
  82. package/src/harness/hunt/hunt-harness.ts +83 -0
  83. package/src/harness/hunt/hunt-options.ts +75 -0
  84. package/src/harness/hunt/hunt-result.ts +17 -0
  85. package/src/harness/hunt/hunt-session.ts +372 -0
  86. package/src/harness/hunt/review-planner.ts +98 -0
  87. package/src/harness/index.ts +36 -0
  88. package/src/harness/investigation/agent-state.ts +134 -0
  89. package/src/harness/investigation/evidence-verifier.ts +129 -0
  90. package/src/harness/investigation/evidence.ts +110 -0
  91. package/src/harness/investigation/finding-critic.ts +96 -0
  92. package/src/harness/investigation/investigation-planner.ts +118 -0
  93. package/src/harness/investigation/investigation-task.ts +10 -0
  94. package/src/harness/investigation/review-journal.ts +87 -0
  95. package/src/harness/investigation/rule-analyzer.ts +178 -0
  96. package/src/harness/investigation/rule-signals.ts +110 -0
  97. package/src/harness/prompts/agent-schemas.ts +57 -0
  98. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  99. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  100. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  101. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  102. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  103. package/src/harness/tools/agent-tool.ts +102 -0
  104. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  105. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  106. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  107. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  108. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  109. package/src/harness/tools/builtin/helpers.ts +115 -0
  110. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  111. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  112. package/src/harness/tools/builtin/index.ts +59 -0
  113. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  114. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  115. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  116. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  117. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  118. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  119. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  120. package/src/harness/tools/path-guard.ts +90 -0
  121. package/src/harness/tools/result-limiter.ts +49 -0
  122. package/src/harness/tools/secret-redactor.ts +54 -0
  123. package/src/harness/tools/tool-cache.ts +50 -0
  124. package/src/harness/tools/tool-call-budget.ts +60 -0
  125. package/src/harness/tools/tool-context.ts +5 -0
  126. package/src/harness/tools/tool-middleware.ts +105 -0
  127. package/src/harness/tools/tool-policy.ts +51 -0
  128. package/src/harness/tools/tool-registry.ts +112 -0
  129. package/src/harness/tools/tool-result.ts +15 -0
  130. package/src/harness/tools/toolbox.ts +36 -0
  131. package/src/harness/validation/finding-validator.ts +119 -0
  132. package/src/index.ts +46 -0
  133. package/src/server/client/App.tsx +38 -12
  134. package/src/server/client/CommentsPanel.tsx +255 -0
  135. package/src/server/client/FindingCard.tsx +12 -4
  136. package/src/server/client/FindingsList.tsx +4 -1
  137. package/src/server/client/Sidebar.tsx +397 -0
  138. package/src/server/client/SubmitModal.tsx +61 -5
  139. package/src/server/client/icons.tsx +19 -0
  140. package/src/server/client/index.tsx +1 -1
  141. package/src/server/client/utils/constants.ts +29 -5
  142. package/src/server/client/utils/format.ts +47 -2
  143. package/src/server/client/utils/types.ts +88 -0
  144. package/src/server/editor.ts +129 -0
  145. package/src/server/index.ts +115 -5
  146. package/src/server/pr-comments.ts +256 -0
  147. package/src/server/pr-inbox.ts +202 -0
  148. package/src/server/run-repository.ts +33 -1
  149. package/src/skill/OUTPUT-FORMAT.md +12 -0
  150. package/src/skill/SETUP.md +6 -2
  151. package/src/skill/hunt.md +14 -2
  152. package/src/skill/verify.md +1 -1
  153. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  154. package/src/tests/ai-core.test.ts +120 -0
  155. package/src/tests/ai-features.test.ts +183 -0
  156. package/src/tests/ast.test.ts +335 -0
  157. package/src/tests/cli.test.ts +33 -0
  158. package/src/tests/env.test.ts +69 -0
  159. package/src/tests/fallback.test.ts +172 -0
  160. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  161. package/src/tests/fixtures/fake-investigation.ts +64 -0
  162. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  163. package/src/tests/github-review.test.ts +9 -0
  164. package/src/tests/harness-agent.test.ts +122 -0
  165. package/src/tests/harness-investigation.test.ts +206 -0
  166. package/src/tests/harness-tools.test.ts +148 -0
  167. package/src/tests/hunt-command.test.ts +212 -0
  168. package/src/tests/logger.test.ts +83 -0
  169. package/src/tests/review-scope.test.ts +11 -11
  170. package/src/tests/rule-analyzer.test.ts +61 -0
  171. package/src/tests/server-editor.test.ts +100 -0
  172. package/src/tests/server.test.ts +295 -2
  173. package/src/tests/tool-contracts.test.ts +82 -0
  174. package/src/tests/verify.test.ts +1 -1
  175. package/src/server/client/RunList.tsx +0 -75
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 = "TYPESAFE_API_KEY não configurada";
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
  },
@@ -599,18 +657,23 @@ function splitList(raw: string | undefined): string[] {
599
657
  .filter(Boolean);
600
658
  }
601
659
 
602
- /** Seleção do envio a partir das flags (`--severity`, `--exec`, `--include-sent`). */
660
+ /** Seleção do envio a partir das flags (`--severity`, `--decision`, `--exec`). */
603
661
  function submitSelection(parsed: ParsedArgs): SubmitSelection {
604
662
  const severities = splitList(parsed.values["--severity"]).filter(
605
663
  (value): value is Severity =>
606
664
  (SEVERITY_ORDER as readonly string[]).includes(value),
607
665
  );
666
+ const decisions = splitList(parsed.values["--decision"]).filter(
667
+ (value): value is FindingDecision =>
668
+ (FINDING_DECISIONS as readonly string[]).includes(value),
669
+ );
608
670
  const execStates = splitList(parsed.values["--exec"]).filter(
609
671
  (value): value is FindingExecState =>
610
672
  (FINDING_EXEC_STATES as readonly string[]).includes(value),
611
673
  );
612
674
  return {
613
675
  severities: severities.length > 0 ? severities : undefined,
676
+ decisions: decisions.length > 0 ? decisions : undefined,
614
677
  execStates: execStates.length > 0 ? execStates : undefined,
615
678
  includeSent: parsed.bools.has("--include-sent"),
616
679
  };
@@ -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 { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
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 conteúdo).
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: "Todo o repositório (ajuste o escopo)",
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 WATCHDOGS_INDEX = `import { exampleWatchdog } from "./example";
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
- */\`core/\` é regenerado por \`houndsman update\`.
271
- # Runs, findings e artefatos vivem no storage global (fora do projeto).
272
- core
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`, `.houndsman/.gitignore` e os
5
- * ponteiros de skill dos harnesses. Nunca toca nos arquivos do usuário
6
- * (`watchdogs/`, `doc-mappings.ts`, `exclusions.ts`, `houndsman.config.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";
@@ -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. */