@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.
Files changed (153) 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/help.ts +17 -1
  11. package/src/cli/index.ts +10 -6
  12. package/src/core/ai/ai-client.ts +221 -0
  13. package/src/core/ai/ai-sdk-model-provider.ts +101 -0
  14. package/src/core/ai/errors.ts +36 -0
  15. package/src/core/ai/features/brief-enricher.ts +62 -0
  16. package/src/core/ai/features/classifier.ts +50 -0
  17. package/src/core/ai/features/context-summarizer.ts +31 -0
  18. package/src/core/ai/features/pre-analyzer.ts +51 -0
  19. package/src/core/ai/features/verifier.ts +45 -0
  20. package/src/core/ai/index.ts +56 -0
  21. package/src/core/ai/model-factory.ts +137 -0
  22. package/src/core/ai/prompts/base.ts +36 -0
  23. package/src/core/ai/prompts/brief.ts +87 -0
  24. package/src/core/ai/prompts/classification.ts +36 -0
  25. package/src/core/ai/prompts/context.ts +29 -0
  26. package/src/core/ai/prompts/pre-analysis.ts +49 -0
  27. package/src/core/ai/prompts/verify.ts +40 -0
  28. package/src/core/ai/schemas.ts +110 -0
  29. package/src/core/ai/types.ts +76 -0
  30. package/src/core/ai/usage-budget.ts +75 -0
  31. package/src/core/ai/usage-recorder.ts +79 -0
  32. package/src/core/assets.ts +16 -0
  33. package/src/core/ast/diff.ts +220 -0
  34. package/src/core/ast/impact.ts +127 -0
  35. package/src/core/ast/parser.ts +167 -0
  36. package/src/core/ast/service.ts +233 -0
  37. package/src/core/ast/symbols.ts +294 -0
  38. package/src/core/ast/types.ts +125 -0
  39. package/src/core/brief.ts +138 -0
  40. package/src/core/commands/findings.ts +59 -1
  41. package/src/core/commands/review.ts +2 -0
  42. package/src/core/commands/scaffold.ts +159 -10
  43. package/src/core/commands/update.ts +5 -3
  44. package/src/core/config.ts +85 -0
  45. package/src/core/defaults.ts +27 -0
  46. package/src/core/domain/watchdog.ts +43 -5
  47. package/src/core/engine/houndsman-engine.ts +16 -1
  48. package/src/core/engine/review-scope.ts +52 -6
  49. package/src/core/engine/review-session.ts +27 -2
  50. package/src/core/env.ts +101 -0
  51. package/src/core/file-classifier.ts +25 -4
  52. package/src/core/jev/jev-client.ts +8 -7
  53. package/src/core/jev/jev-verify.ts +2 -1
  54. package/src/core/logging/index.ts +17 -0
  55. package/src/core/logging/logger.ts +187 -0
  56. package/src/core/orchestrator.ts +357 -9
  57. package/src/core/project-config.ts +123 -1
  58. package/src/core/registry-check.ts +148 -6
  59. package/src/core/renderers/manifest-writer.ts +33 -0
  60. package/src/core/rule-text.ts +97 -0
  61. package/src/core/services/file-classifier.ts +10 -3
  62. package/src/core/services/verifier.ts +14 -3
  63. package/src/core/storage/paths.ts +6 -5
  64. package/src/core/storage/types.ts +6 -0
  65. package/src/core/storage/work-area.ts +12 -0
  66. package/src/core/types.ts +37 -1
  67. package/src/core/verify.ts +17 -5
  68. package/src/harness/README.md +15 -0
  69. package/src/harness/agent/agent-context.ts +33 -0
  70. package/src/harness/agent/agent-errors.ts +30 -0
  71. package/src/harness/agent/agent-factory.ts +45 -0
  72. package/src/harness/agent/agent-reporter.ts +34 -0
  73. package/src/harness/agent/agent-result.ts +16 -0
  74. package/src/harness/agent/agent-runner.ts +32 -0
  75. package/src/harness/agent/agent-scheduler.ts +37 -0
  76. package/src/harness/agent/review-agent.ts +391 -0
  77. package/src/harness/config/harness-config.ts +84 -0
  78. package/src/harness/hunt/hunt-harness.ts +83 -0
  79. package/src/harness/hunt/hunt-options.ts +75 -0
  80. package/src/harness/hunt/hunt-result.ts +17 -0
  81. package/src/harness/hunt/hunt-session.ts +372 -0
  82. package/src/harness/hunt/review-planner.ts +98 -0
  83. package/src/harness/index.ts +36 -0
  84. package/src/harness/investigation/agent-state.ts +134 -0
  85. package/src/harness/investigation/evidence-verifier.ts +129 -0
  86. package/src/harness/investigation/evidence.ts +110 -0
  87. package/src/harness/investigation/finding-critic.ts +96 -0
  88. package/src/harness/investigation/investigation-planner.ts +118 -0
  89. package/src/harness/investigation/investigation-task.ts +10 -0
  90. package/src/harness/investigation/review-journal.ts +87 -0
  91. package/src/harness/investigation/rule-analyzer.ts +178 -0
  92. package/src/harness/investigation/rule-signals.ts +110 -0
  93. package/src/harness/prompts/agent-schemas.ts +57 -0
  94. package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
  95. package/src/harness/prompts/states/conclude-prompt.ts +16 -0
  96. package/src/harness/prompts/states/investigate-prompt.ts +37 -0
  97. package/src/harness/prompts/states/orient-prompt.ts +15 -0
  98. package/src/harness/prompts/states/verify-prompt.ts +22 -0
  99. package/src/harness/tools/agent-tool.ts +102 -0
  100. package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
  101. package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
  102. package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
  103. package/src/harness/tools/builtin/find-references-tool.ts +66 -0
  104. package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
  105. package/src/harness/tools/builtin/helpers.ts +115 -0
  106. package/src/harness/tools/builtin/impact-tool.ts +48 -0
  107. package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
  108. package/src/harness/tools/builtin/index.ts +59 -0
  109. package/src/harness/tools/builtin/list-files-tool.ts +85 -0
  110. package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
  111. package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
  112. package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
  113. package/src/harness/tools/builtin/search-code-tool.ts +65 -0
  114. package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
  115. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
  116. package/src/harness/tools/path-guard.ts +90 -0
  117. package/src/harness/tools/result-limiter.ts +49 -0
  118. package/src/harness/tools/secret-redactor.ts +54 -0
  119. package/src/harness/tools/tool-cache.ts +50 -0
  120. package/src/harness/tools/tool-call-budget.ts +60 -0
  121. package/src/harness/tools/tool-context.ts +5 -0
  122. package/src/harness/tools/tool-middleware.ts +105 -0
  123. package/src/harness/tools/tool-policy.ts +51 -0
  124. package/src/harness/tools/tool-registry.ts +112 -0
  125. package/src/harness/tools/tool-result.ts +15 -0
  126. package/src/harness/tools/toolbox.ts +36 -0
  127. package/src/harness/validation/finding-validator.ts +119 -0
  128. package/src/index.ts +46 -0
  129. package/src/server/client/utils/constants.ts +27 -5
  130. package/src/server/client/utils/types.ts +6 -0
  131. package/src/server/index.ts +3 -2
  132. package/src/skill/OUTPUT-FORMAT.md +12 -0
  133. package/src/skill/SETUP.md +6 -2
  134. package/src/skill/hunt.md +14 -2
  135. package/src/skill/verify.md +1 -1
  136. package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
  137. package/src/tests/ai-core.test.ts +120 -0
  138. package/src/tests/ai-features.test.ts +183 -0
  139. package/src/tests/ast.test.ts +335 -0
  140. package/src/tests/env.test.ts +69 -0
  141. package/src/tests/fallback.test.ts +172 -0
  142. package/src/tests/fixtures/fake-agent-tools.ts +69 -0
  143. package/src/tests/fixtures/fake-investigation.ts +64 -0
  144. package/src/tests/fixtures/fake-model-provider.ts +48 -0
  145. package/src/tests/harness-agent.test.ts +122 -0
  146. package/src/tests/harness-investigation.test.ts +206 -0
  147. package/src/tests/harness-tools.test.ts +148 -0
  148. package/src/tests/hunt-command.test.ts +212 -0
  149. package/src/tests/logger.test.ts +83 -0
  150. package/src/tests/review-scope.test.ts +11 -11
  151. package/src/tests/rule-analyzer.test.ts +61 -0
  152. package/src/tests/tool-contracts.test.ts +82 -0
  153. package/src/tests/verify.test.ts +1 -1
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Middleware de execução das tools.
3
+ *
4
+ * Cadeia: validação Zod → cache → budget → execução → limites → redaction →
5
+ * auditoria. Toda tool passa por aqui; nenhuma executa direto.
6
+ */
7
+
8
+ import type { ToolResult } from "./agent-tool";
9
+ import { limitText, TOOL_LIMITS } from "./result-limiter";
10
+ import { redactSecrets, redactValue } from "./secret-redactor";
11
+ import { ToolCache } from "./tool-cache";
12
+ import { ToolBudgetError } from "./tool-call-budget";
13
+ import type { ToolContext } from "./tool-context";
14
+ import type { AgentStateName } from "./tool-policy";
15
+ import type { RegisteredTool } from "./tool-registry";
16
+
17
+ /** Executor com validação, cache, budget, limite e auditoria. */
18
+ export class ToolMiddleware {
19
+ async run(
20
+ tool: RegisteredTool,
21
+ input: unknown,
22
+ context: ToolContext,
23
+ state: AgentStateName,
24
+ ): Promise<ToolResult<unknown>> {
25
+ const parsed = tool.inputSchema.safeParse(input);
26
+ if (!parsed.success) {
27
+ return {
28
+ ok: false,
29
+ error: `input inválido para '${tool.metadata.name}': ${parsed.error.message}`,
30
+ };
31
+ }
32
+
33
+ const maxPerAgent = tool.metadata.maxCallsPerAgent;
34
+ if (
35
+ maxPerAgent !== undefined &&
36
+ context.state.callsOf(tool.metadata.name) >= maxPerAgent
37
+ ) {
38
+ return {
39
+ ok: false,
40
+ error: `Limite de ${maxPerAgent} chamada(s) da tool '${tool.metadata.name}'.`,
41
+ };
42
+ }
43
+
44
+ const key = ToolCache.key(tool.metadata.name, parsed.data);
45
+ const cached = context.cache.get<ToolResult<unknown>>(key);
46
+ if (cached) return cached;
47
+
48
+ try {
49
+ context.budget.consume(tool.metadata.name);
50
+ } catch (error) {
51
+ return {
52
+ ok: false,
53
+ error: error instanceof ToolBudgetError ? error.message : String(error),
54
+ };
55
+ }
56
+
57
+ const started = performance.now();
58
+ try {
59
+ // Contravariância: a tool concreta aceita seu próprio input.
60
+ const result = (await tool.execute(
61
+ parsed.data as never,
62
+ context,
63
+ )) as ToolResult<unknown>;
64
+ const limited = limitResult(result);
65
+ const redacted = redactValue(limited);
66
+ context.cache.set(key, redacted, tool.metadata.cacheTtlMs ?? 60_000);
67
+ context.state.recordToolCall(tool.metadata.name);
68
+ const stored = (redacted.evidence ?? []).map(
69
+ (evidence) =>
70
+ context.state.getEvidence(evidence.id) ??
71
+ context.state.addEvidence(evidence),
72
+ );
73
+ if (stored.length > 0) redacted.evidence = stored;
74
+ context.logger.journal.recordToolCall(
75
+ tool.metadata.name,
76
+ state,
77
+ performance.now() - started,
78
+ redacted,
79
+ );
80
+ return redacted;
81
+ } catch (error) {
82
+ const message = redactSecrets(
83
+ error instanceof Error ? error.message : String(error),
84
+ );
85
+ context.logger.journal.recordToolCall(
86
+ tool.metadata.name,
87
+ state,
88
+ performance.now() - started,
89
+ { ok: false, error: message },
90
+ );
91
+ return { ok: false, error: message };
92
+ }
93
+ }
94
+ }
95
+
96
+ /** Aplica tetos de texto ao resultado da tool. */
97
+ function limitResult(result: ToolResult<unknown>): ToolResult<unknown> {
98
+ const copy: ToolResult<unknown> = { ...result };
99
+ if (typeof copy.data === "string") {
100
+ const limited = limitText(copy.data, TOOL_LIMITS.maxBytes);
101
+ copy.data = limited.text;
102
+ if (limited.truncated) copy.truncated = true;
103
+ }
104
+ return copy;
105
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Política de tools por estado do agente.
3
+ *
4
+ * O modelo só vê as tools permitidas no estado atual. Adicionar uma tool não
5
+ * exige alterar o agente — basta declarar capabilities compatíveis.
6
+ */
7
+
8
+ import type { ToolCapability, ToolMetadata } from "./agent-tool";
9
+
10
+ /** Estados da máquina de investigação. */
11
+ export type AgentStateName = "ORIENT" | "PLAN" | "INVESTIGATE" | "VERIFY" | "CONCLUDE";
12
+
13
+ /** Capabilities liberadas por estado. */
14
+ const DEFAULT_POLICY: Record<AgentStateName, readonly ToolCapability[]> = {
15
+ ORIENT: ["read", "outline", "ast", "impact", "rules", "symbol"],
16
+ PLAN: ["rules", "symbol", "impact", "ast"],
17
+ INVESTIGATE: [
18
+ "read",
19
+ "search",
20
+ "outline",
21
+ "symbol",
22
+ "references",
23
+ "ast",
24
+ "impact",
25
+ "rules",
26
+ "tests",
27
+ "docs",
28
+ ],
29
+ VERIFY: ["symbol", "references", "search", "read"],
30
+ CONCLUDE: [],
31
+ };
32
+
33
+ /** Política substituível. */
34
+ export class ToolPolicy {
35
+ private readonly policy: Record<AgentStateName, readonly ToolCapability[]>;
36
+
37
+ constructor(policy: Partial<Record<AgentStateName, readonly ToolCapability[]>> = {}) {
38
+ this.policy = { ...DEFAULT_POLICY, ...policy };
39
+ }
40
+
41
+ /** Capabilities liberadas no estado. */
42
+ capabilities(state: AgentStateName): readonly ToolCapability[] {
43
+ return this.policy[state];
44
+ }
45
+
46
+ /** `true` se a tool pode ser usada no estado. */
47
+ allows(state: AgentStateName, metadata: ToolMetadata): boolean {
48
+ const allowed = this.policy[state];
49
+ return metadata.capabilities.some((capability) => allowed.includes(capability));
50
+ }
51
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Registro das tools do harness.
3
+ *
4
+ * Módulos registram tools; nomes duplicados falham no startup. O registry gera
5
+ * as descrições do prompt a partir do metadata (nunca hardcoded).
6
+ */
7
+
8
+ import type { AiTool } from "../../core/ai";
9
+ import type {
10
+ RegisterableTool,
11
+ ToolContext,
12
+ ToolMetadata,
13
+ ToolModule,
14
+ ToolRegistryLike,
15
+ ToolResult,
16
+ } from "./agent-tool";
17
+ import type { ToolMiddleware } from "./tool-middleware";
18
+ import type { AgentStateName, ToolPolicy } from "./tool-policy";
19
+
20
+ /** Tool registrada, com a forma type-erased aceita pelo middleware. */
21
+ export type RegisteredTool = RegisterableTool;
22
+
23
+ /** Registry de tools. */
24
+ export class ToolRegistry implements ToolRegistryLike {
25
+ private readonly tools = new Map<string, RegisteredTool>();
26
+
27
+ /** Registra uma tool; nome duplicado é erro de configuração. */
28
+ registerTool(tool: RegisterableTool): void {
29
+ if (this.tools.has(tool.metadata.name)) {
30
+ throw new Error(`Tool duplicada: '${tool.metadata.name}'.`);
31
+ }
32
+ this.tools.set(tool.metadata.name, tool);
33
+ }
34
+
35
+ /** Registra um módulo (agrupa tools). */
36
+ registerModule(module: ToolModule): void {
37
+ module.register(this);
38
+ }
39
+
40
+ /** Tool por nome. */
41
+ get(name: string): RegisteredTool | undefined {
42
+ return this.tools.get(name);
43
+ }
44
+
45
+ /** Todas as tools (ordem de registro). */
46
+ list(): RegisteredTool[] {
47
+ return [...this.tools.values()];
48
+ }
49
+
50
+ /** Tools com uma capability. */
51
+ listByCapability(capability: string): RegisteredTool[] {
52
+ return this.list().filter((tool) =>
53
+ (tool.metadata.capabilities as readonly string[]).includes(capability),
54
+ );
55
+ }
56
+
57
+ /** Descrição compacta das tools permitidas (para o prompt). */
58
+ describeForPrompt(policy: ToolPolicy, state: AgentStateName): string {
59
+ return this.list()
60
+ .filter((tool) => policy.allows(state, tool.metadata))
61
+ .map(
62
+ (tool) =>
63
+ `- ${tool.metadata.name} (${tool.metadata.capabilities.join(", ")}): ${tool.metadata.description}`,
64
+ )
65
+ .join("\n");
66
+ }
67
+
68
+ /** Converte as tools permitidas no formato do modelo. */
69
+ toModelTools(
70
+ policy: ToolPolicy,
71
+ state: AgentStateName,
72
+ context: ToolContext,
73
+ middleware: ToolMiddleware,
74
+ ): Record<string, AiTool> {
75
+ const entries = this.list()
76
+ .filter((tool) => policy.allows(state, tool.metadata))
77
+ .map((tool) => [
78
+ tool.metadata.name,
79
+ {
80
+ name: tool.metadata.name,
81
+ description: tool.metadata.description,
82
+ inputSchema: tool.inputSchema,
83
+ execute: async (input: unknown): Promise<string> => {
84
+ const result = await middleware.run(tool, input, context, state);
85
+ return JSON.stringify(result);
86
+ },
87
+ },
88
+ ]);
89
+ return Object.fromEntries(entries);
90
+ }
91
+
92
+ /** Metadados registrados. */
93
+ metadata(): ToolMetadata[] {
94
+ return this.list().map((tool) => tool.metadata);
95
+ }
96
+
97
+ /** Executa uma tool com middleware (usado nos testes). */
98
+ execute(
99
+ name: string,
100
+ input: unknown,
101
+ context: ToolContext,
102
+ middleware: ToolMiddleware,
103
+ state: AgentStateName = "INVESTIGATE",
104
+ ): Promise<ToolResult<unknown>> {
105
+ const tool = this.tools.get(name);
106
+ if (!tool)
107
+ return Promise.resolve({ ok: false, error: `tool desconhecida: ${name}` });
108
+ return middleware.run(tool, input, context, state) as Promise<
109
+ ToolResult<unknown>
110
+ >;
111
+ }
112
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resultado padrão de uma tool (reexport de contrato).
3
+ */
4
+
5
+ export type { ToolResult } from "./agent-tool";
6
+
7
+ /** Resultado de sucesso. */
8
+ export function ok<T>(data: T, extra: Record<string, unknown> = {}) {
9
+ return { ok: true as const, data, ...extra };
10
+ }
11
+
12
+ /** Resultado de erro estruturado. */
13
+ export function fail(error: string) {
14
+ return { ok: false as const, error };
15
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Visão de tools por agente (registry + policy + middleware).
3
+ *
4
+ * O agente pede `toModelTools(state)` e recebe apenas o que pode usar naquele
5
+ * estado, já com cache/budget/redaction aplicados.
6
+ */
7
+
8
+ import type { AiTool } from "../../core/ai";
9
+ import type { ToolContext } from "./tool-context";
10
+ import type { ToolMiddleware } from "./tool-middleware";
11
+ import type { AgentStateName, ToolPolicy } from "./tool-policy";
12
+ import type { ToolRegistry } from "./tool-registry";
13
+
14
+ /** Fachada por agente. */
15
+ export class Toolbox {
16
+ constructor(
17
+ readonly registry: ToolRegistry,
18
+ readonly policy: ToolPolicy,
19
+ readonly middleware: ToolMiddleware,
20
+ ) {}
21
+
22
+ /** Tools permitidas no estado, no formato do modelo. */
23
+ toModelTools(state: AgentStateName, context: ToolContext): Record<string, AiTool> {
24
+ return this.registry.toModelTools(this.policy, state, context, this.middleware);
25
+ }
26
+
27
+ /** Descrição das tools permitidas (prompt). */
28
+ describe(state: AgentStateName): string {
29
+ return this.registry.describeForPrompt(this.policy, state);
30
+ }
31
+
32
+ /** Executa uma tool fora do modelo (testes/uso determinístico). */
33
+ execute(name: string, input: unknown, context: ToolContext, state: AgentStateName) {
34
+ return this.registry.execute(name, input, context, this.middleware, state);
35
+ }
36
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Validação final do relatório de um watchdog.
3
+ *
4
+ * Descarta findings que citam regra inexistente, arquivo fora do domínio/impacto,
5
+ * overflow (não lido) ou linha inválida. `reviewedFiles` só aceita arquivos
6
+ * existentes; overflow entra em `skipped`.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import type { WatchdogReport } from "../../core/report-builder";
11
+ import { emergentText, isEmergentRule } from "../../core/rule-text";
12
+ import { SEVERITY_ORDER, type Severity } from "../../core/types";
13
+ import { capSeverity } from "../investigation/finding-critic";
14
+
15
+ /** Contexto de validação. */
16
+ export type ValidationContext = {
17
+ watchdogId: string;
18
+ knownRules: ReadonlySet<string>;
19
+ allowedFiles: ReadonlySet<string>;
20
+ overflow: ReadonlySet<string>;
21
+ readFile: (path: string) => string | null;
22
+ /** Aceita findings `Lógica:`/`Domínio:` fora das regras. */
23
+ allowEmergentFindings?: boolean;
24
+ };
25
+
26
+ /** Resultado da validação. */
27
+ export type ValidationResult = {
28
+ report: WatchdogReport;
29
+ dropped: { finding: string; reason: string }[];
30
+ };
31
+
32
+ const SEVERITIES = new Set<string>(SEVERITY_ORDER);
33
+
34
+ /** Validador de relatórios. */
35
+ export class FindingValidator {
36
+ private readonly context: ValidationContext;
37
+
38
+ constructor(context: ValidationContext) {
39
+ this.context = context;
40
+ }
41
+
42
+ /** Valida/normaliza o relatório. */
43
+ validate(report: WatchdogReport): ValidationResult {
44
+ const dropped: { finding: string; reason: string }[] = [];
45
+ const findings = report.findings
46
+ .filter((finding) => {
47
+ const reason = this.reasonToDrop(finding);
48
+ if (!reason) return true;
49
+ dropped.push({
50
+ finding: `${finding.file}:${finding.line ?? "?"}`,
51
+ reason,
52
+ });
53
+ return false;
54
+ })
55
+ .map((finding) =>
56
+ isEmergentRule(finding.rule)
57
+ ? {
58
+ ...finding,
59
+ severity: capSeverity(finding.severity, "attention"),
60
+ }
61
+ : finding,
62
+ );
63
+
64
+ const reviewedFiles = report.reviewedFiles
65
+ .filter((file) => this.context.allowedFiles.has(file))
66
+ .filter((file) => this.context.readFile(file) !== null);
67
+
68
+ const skipped = new Set(report.skipped);
69
+ for (const file of this.context.overflow) skipped.add(`${file} (budget)`);
70
+
71
+ return {
72
+ report: {
73
+ watchdogId: this.context.watchdogId,
74
+ reviewedFiles: [...new Set(reviewedFiles)],
75
+ skipped: [...skipped],
76
+ findings,
77
+ },
78
+ dropped,
79
+ };
80
+ }
81
+
82
+ /** Motivo para descartar (ou `null` se válido). */
83
+ private reasonToDrop(finding: {
84
+ rule: string;
85
+ severity: Severity;
86
+ file: string;
87
+ line?: number;
88
+ }): string | null {
89
+ if (isEmergentRule(finding.rule)) {
90
+ if (!this.context.allowEmergentFindings)
91
+ return "finding emergente desabilitado";
92
+ if (!emergentText(finding.rule)) return "finding emergente sem descrição";
93
+ } else if (!this.context.knownRules.has(finding.rule.trim())) {
94
+ return "regra inexistente";
95
+ }
96
+ if (!SEVERITIES.has(finding.severity)) return "severidade inválida";
97
+ if (this.context.overflow.has(finding.file)) return "arquivo fora do orçamento";
98
+ if (!this.context.allowedFiles.has(finding.file))
99
+ return "arquivo fora do domínio";
100
+ const content = this.context.readFile(finding.file);
101
+ if (content === null) return "arquivo ausente";
102
+ if (finding.line !== undefined) {
103
+ const total = content.split("\n").length;
104
+ if (finding.line < 1 || finding.line > total) return "linha fora do arquivo";
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /** Leitor de arquivos padrão (UTF-8; `null` se ausente). */
111
+ export function defaultFileReader(root: string): (path: string) => string | null {
112
+ return (path: string) => {
113
+ try {
114
+ return readFileSync(`${root}/${path}`, "utf8");
115
+ } catch {
116
+ return null;
117
+ }
118
+ };
119
+ }
package/src/index.ts CHANGED
@@ -5,13 +5,53 @@
5
5
  * (`houndsman.config.ts`): watchdogs, tipos e helpers de autoria.
6
6
  */
7
7
 
8
+ export type {
9
+ AiResult,
10
+ AiTool,
11
+ AiUsage,
12
+ ModelProvider,
13
+ ModelRequest,
14
+ ModelResponse,
15
+ } from "./core/ai";
16
+ export {
17
+ AiBriefEnricher,
18
+ AiClassifier,
19
+ AiClient,
20
+ AiContextSummarizer,
21
+ AiPreAnalyzer,
22
+ AiUsageBudget,
23
+ AiUsageRecorder,
24
+ AiVerifier,
25
+ ModelFactory,
26
+ } from "./core/ai";
8
27
  export { ArgParser } from "./core/arg-parser";
9
28
  export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
29
+ export { computeDelta, emptyDelta, parseHunks } from "./core/ast/diff";
30
+ export { buildImpact, resolveImport } from "./core/ast/impact";
31
+ export { AstParser, languageForPath } from "./core/ast/parser";
32
+ export { AstService } from "./core/ast/service";
33
+ export type {
34
+ AstAnalysis,
35
+ AstDelta,
36
+ AstEntry,
37
+ AstExport,
38
+ AstFacts,
39
+ AstFileBrief,
40
+ AstImpact,
41
+ AstImport,
42
+ AstLanguage,
43
+ AstSymbol,
44
+ AstSymbolKind,
45
+ } from "./core/ast/types";
10
46
  export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
11
47
  export type { FindingsCommandResult } from "./core/commands/findings";
12
48
  export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
13
49
  export { ProjectScaffolder } from "./core/commands/scaffold";
14
50
  export type {
51
+ AiProviderKind,
52
+ HoundsmanAi,
53
+ HoundsmanAiFeatures,
54
+ HoundsmanAst,
15
55
  HoundsmanBudgets,
16
56
  HoundsmanConcurrency,
17
57
  HoundsmanConfig,
@@ -20,6 +60,8 @@ export type {
20
60
  } from "./core/config";
21
61
  export {
22
62
  DEFAULT_ALWAYS_INCLUDE_DOCS,
63
+ DEFAULT_AST_MAX_FILE_BYTES,
64
+ DEFAULT_AST_MAX_IMPACT_FILES,
23
65
  DEFAULT_JEV_CONCURRENCY,
24
66
  DEFAULT_MAX_FILES_PER_BRIEF,
25
67
  defineHoundsmanConfig,
@@ -47,6 +89,8 @@ export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
47
89
  export { HoundsmanEngine } from "./core/engine/houndsman-engine";
48
90
  export { ReviewScopeResolver } from "./core/engine/review-scope";
49
91
  export { ReviewSession } from "./core/engine/review-session";
92
+ export type { HoundsmanEnv } from "./core/env";
93
+ export { ENV_NAMES, env, readEnv } from "./core/env";
50
94
  export {
51
95
  churnOf,
52
96
  classifyEntries,
@@ -159,6 +203,7 @@ export type {
159
203
  FileStatus,
160
204
  FileStatusMap,
161
205
  Rule,
206
+ RuleHints,
162
207
  Severity,
163
208
  WatchdogDefinition,
164
209
  WatchdogMeta,
@@ -172,6 +217,7 @@ export {
172
217
  SEVERITY_ORDER,
173
218
  severityRank,
174
219
  } from "./core/types";
220
+ export { applyConfidence, applyJev, summarizeVerify, verifyLocal } from "./core/verify";
175
221
  export {
176
222
  findWatchdog,
177
223
  getWatchdog,
@@ -91,6 +91,12 @@ const KIND_ORDER: ArtifactKind[] = [
91
91
  "manifest",
92
92
  "classification",
93
93
  "candidates",
94
+ "ast",
95
+ "ast-impact",
96
+ "ai",
97
+ "ai-usage",
98
+ "ai-brief",
99
+ "harness-transcript",
94
100
  "context",
95
101
  "context-brief",
96
102
  "context-summary",
@@ -109,11 +115,17 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
109
115
  manifest: { key: "core", label: "Núcleo da revisão", order: 3 },
110
116
  classification: { key: "core", label: "Núcleo da revisão", order: 3 },
111
117
  candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
112
- context: { key: "context", label: "Contexto da PR", order: 4 },
113
- "context-brief": { key: "context", label: "Contexto da PR", order: 4 },
114
- "context-summary": { key: "context", label: "Contexto da PR", order: 4 },
115
- "finding-report": { key: "other", label: "Outros", order: 5 },
116
- other: { key: "other", label: "Outros", order: 5 },
118
+ ast: { key: "core", label: "Núcleo da revisão", order: 3 },
119
+ "ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
120
+ ai: { key: "ai", label: "IA e harness", order: 4 },
121
+ "ai-usage": { key: "ai", label: "IA e harness", order: 4 },
122
+ "ai-brief": { key: "ai", label: "IA e harness", order: 4 },
123
+ "harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
124
+ context: { key: "context", label: "Contexto da PR", order: 5 },
125
+ "context-brief": { key: "context", label: "Contexto da PR", order: 5 },
126
+ "context-summary": { key: "context", label: "Contexto da PR", order: 5 },
127
+ "finding-report": { key: "other", label: "Outros", order: 6 },
128
+ other: { key: "other", label: "Outros", order: 6 },
117
129
  };
118
130
 
119
131
  /** Rótulo curto do tipo (badge do item). */
@@ -126,6 +138,12 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
126
138
  manifest: "manifesto",
127
139
  classification: "classificação",
128
140
  candidates: "jev",
141
+ ast: "ast",
142
+ "ast-impact": "impacto",
143
+ ai: "ia",
144
+ "ai-usage": "uso ia",
145
+ "ai-brief": "plano ia",
146
+ "harness-transcript": "transcript",
129
147
  context: "contexto",
130
148
  "context-brief": "brief ctx",
131
149
  "context-summary": "resumo ctx",
@@ -159,6 +177,10 @@ export function describeArtifact(meta: ArtifactMeta): string {
159
177
  return "Classificação do diff: watchdogs ativados por arquivo, status (A/M/D/R) e churn.";
160
178
  case "candidates":
161
179
  return "Indícios da pré-análise Jev (regra + arquivo + confiança) — não são achados.";
180
+ case "ast":
181
+ return "Fatos estruturais e delta por arquivo (símbolos, imports, exports e assinaturas).";
182
+ case "ast-impact":
183
+ return "Impacto reverso: arquivos que importam os alterados e exports públicos modificados.";
162
184
  case "context":
163
185
  return "Payload bruto da PR coletado via `gh` (ou marcador de indisponível).";
164
186
  case "context-brief":
@@ -64,6 +64,12 @@ export type ArtifactKind =
64
64
  | "resume"
65
65
  | "domain-review"
66
66
  | "verify"
67
+ | "ast"
68
+ | "ast-impact"
69
+ | "ai"
70
+ | "ai-usage"
71
+ | "ai-brief"
72
+ | "harness-transcript"
67
73
  | "other";
68
74
 
69
75
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -14,6 +14,7 @@ import {
14
14
  type GhRunner,
15
15
  type SubmitSelection,
16
16
  } from "../core/github-review";
17
+ import { logger } from "../core/logging";
17
18
  import { createStorage } from "../core/storage/factory";
18
19
  import { resolveRepoIdentity } from "../core/storage/repo-identity";
19
20
  import type { RepoIdentity, StorageProvider } from "../core/storage/types";
@@ -190,8 +191,8 @@ export class HoundsmanServer {
190
191
  try {
191
192
  this.runs.maintain();
192
193
  } catch (error) {
193
- console.warn(
194
- `[houndsman] VACUUM falhou (seguindo sem compactar): ${
194
+ logger.warn(
195
+ `VACUUM falhou (seguindo sem compactar): ${
195
196
  error instanceof Error ? error.message : String(error)
196
197
  }`,
197
198
  );
@@ -70,3 +70,15 @@ staging global da review:
70
70
  - Um achado por regra violada; não agrupar regras diferentes.
71
71
  - `file:line` sempre que possível; `evidence` quando o achado não é óbvio.
72
72
  - Não inventar achados — se não houver violação, o array fica vazio.
73
+
74
+ ## Findings emergentes (opt-in)
75
+
76
+ Quando o watchdog declara `allowEmergentFindings: true`, você pode reportar falhas de
77
+ **lógica** ou **domínio** que não correspondem a uma regra explícita usando os prefixos
78
+ reservados:
79
+
80
+ - `Lógica: <descrição>` — nome/contrato vs comportamento, fluxos, estados, bordas.
81
+ - `Domínio: <descrição>` — transições, idempotência, invariantes, arredondamento, timezone.
82
+
83
+ Esses findings continuam exigindo `evidence` e têm severidade máxima `attention`. Sem o
84
+ opt-in, todo finding precisa citar uma regra existente.
@@ -7,8 +7,11 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
7
7
 
8
8
  - **Bun** — o CLI e o motor usam a runtime Bun (execução de TS, `Bun.spawnSync`).
9
9
  - **git** — o diff usa `git diff <base>...<head> --name-status`.
10
- - Opcional: **`gh`** (contexto da PR) e **`TYPESAFE_API_KEY`** (pré-análise Jev). O pipeline
11
- segue normalmente sem eles.
10
+ - **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
11
+ - **`HOUNDSMAN_TYPE_SAFE_API_KEY`** (opcional) — pré-análise Jev.
12
+ - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — IA própria do `hunt` nativo e das features de IA.
13
+ - **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
14
+ `ai.provider` for `openai`/`anthropic`.
12
15
 
13
16
  ## Instalação (global)
14
17
 
@@ -87,6 +90,7 @@ Definido em `src/core/config.ts` (pacote) e espelhado em `.houndsman/core/types.
87
90
  | `alwaysIncludeDocs` | Docs sempre presentes nos briefs (default `["AGENTS.md"]`). |
88
91
  | `budgets` | **Opcional** — teto de arquivos por brief (`maxFilesPerBrief`, default `40`); o excedente vira "fora do orçamento" (registrar em `skipped`). |
89
92
  | `concurrency` | **Opcional** — chamadas Jev simultâneas (`jev`, default `5`). |
93
+ | `ast` | **Opcional** — AST determinístico para TS/JS (`enabled`, `maxFileBytes`, `maxImpactFiles`, `languages`, `aliases`). Default: ligado; falhas são fail-open. |
90
94
 
91
95
  > O motor **não contém** ids, paths ou regras do projeto — tudo chega por `HoundsmanConfig`,
92
96
  > carregado em runtime a partir de `houndsman.config.ts` (ou `--config <path>`).