@payconductor/houndsman 0.11.0 → 0.12.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 (118) hide show
  1. package/README.md +4 -19
  2. package/package.json +1 -1
  3. package/src/cli/help.ts +0 -7
  4. package/src/cli/index.ts +0 -2
  5. package/src/core/ai/ai-client.ts +3 -7
  6. package/src/core/ai/ai-sdk-model-provider.ts +4 -24
  7. package/src/core/ai/features/brief-enricher.ts +0 -1
  8. package/src/core/ai/features/classifier.ts +0 -1
  9. package/src/core/ai/features/context-summarizer.ts +0 -1
  10. package/src/core/ai/features/pre-analyzer.ts +0 -1
  11. package/src/core/ai/features/technical-summarizer.ts +0 -1
  12. package/src/core/ai/features/verifier.ts +0 -1
  13. package/src/core/ai/index.ts +0 -3
  14. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  15. package/src/core/ai/schemas.ts +1 -23
  16. package/src/core/ai/types.ts +2 -15
  17. package/src/core/ai/usage-budget.ts +1 -1
  18. package/src/core/ast/diff.ts +37 -2
  19. package/src/core/ast/service.ts +1 -1
  20. package/src/core/brief.ts +54 -5
  21. package/src/core/changed-scope.ts +207 -0
  22. package/src/core/commands/scaffold.ts +1 -18
  23. package/src/core/config.ts +2 -15
  24. package/src/core/defaults.ts +0 -6
  25. package/src/core/engine/review-scope.ts +55 -8
  26. package/src/core/jev/jev-detector.ts +7 -2
  27. package/src/core/logging/index.ts +1 -1
  28. package/src/core/logging/logger.ts +3 -3
  29. package/src/core/orchestrator.ts +19 -14
  30. package/src/core/project-config.ts +0 -28
  31. package/src/core/registry-check.ts +2 -12
  32. package/src/core/rule-text.ts +3 -55
  33. package/src/core/services/git-repository.ts +8 -0
  34. package/src/core/storage/types.ts +0 -1
  35. package/src/core/types.ts +3 -14
  36. package/src/index.ts +25 -3
  37. package/src/server/client/utils/constants.ts +3 -6
  38. package/src/server/client/utils/types.ts +0 -1
  39. package/src/skill/OUTPUT-FORMAT.md +5 -0
  40. package/src/skill/README.md +2 -1
  41. package/src/skill/SETUP.md +1 -1
  42. package/src/skill/fix.md +1 -1
  43. package/src/skill/hunt.md +2 -6
  44. package/src/tests/__snapshots__/baseline.test.ts.snap +50 -32
  45. package/src/tests/changed-scope.test.ts +197 -0
  46. package/src/tests/fallback.test.ts +1 -96
  47. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  48. package/src/tests/review-scope.test.ts +33 -0
  49. package/src/tests/worktree.test.ts +1 -3
  50. package/src/cli/commands/hunt-command.ts +0 -51
  51. package/src/harness/README.md +0 -15
  52. package/src/harness/agent/agent-context.ts +0 -33
  53. package/src/harness/agent/agent-errors.ts +0 -30
  54. package/src/harness/agent/agent-factory.ts +0 -46
  55. package/src/harness/agent/agent-reporter.ts +0 -34
  56. package/src/harness/agent/agent-result.ts +0 -16
  57. package/src/harness/agent/agent-runner.ts +0 -32
  58. package/src/harness/agent/agent-scheduler.ts +0 -37
  59. package/src/harness/agent/review-agent.ts +0 -401
  60. package/src/harness/config/harness-config.ts +0 -89
  61. package/src/harness/hunt/hunt-harness.ts +0 -83
  62. package/src/harness/hunt/hunt-options.ts +0 -92
  63. package/src/harness/hunt/hunt-result.ts +0 -17
  64. package/src/harness/hunt/hunt-session.ts +0 -359
  65. package/src/harness/hunt/review-planner.ts +0 -98
  66. package/src/harness/index.ts +0 -36
  67. package/src/harness/investigation/agent-state.ts +0 -134
  68. package/src/harness/investigation/evidence-verifier.ts +0 -129
  69. package/src/harness/investigation/evidence.ts +0 -110
  70. package/src/harness/investigation/finding-critic.ts +0 -96
  71. package/src/harness/investigation/investigation-planner.ts +0 -118
  72. package/src/harness/investigation/investigation-task.ts +0 -10
  73. package/src/harness/investigation/review-journal.ts +0 -87
  74. package/src/harness/investigation/rule-analyzer.ts +0 -178
  75. package/src/harness/investigation/rule-signals.ts +0 -110
  76. package/src/harness/prompts/agent-schemas.ts +0 -57
  77. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  78. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  79. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  80. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  81. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  82. package/src/harness/tools/agent-tool.ts +0 -102
  83. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  84. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  85. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  86. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  87. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  88. package/src/harness/tools/builtin/helpers.ts +0 -115
  89. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  90. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  91. package/src/harness/tools/builtin/index.ts +0 -59
  92. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  93. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  94. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  95. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  96. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  97. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  98. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  99. package/src/harness/tools/path-guard.ts +0 -90
  100. package/src/harness/tools/result-limiter.ts +0 -49
  101. package/src/harness/tools/secret-redactor.ts +0 -54
  102. package/src/harness/tools/tool-cache.ts +0 -50
  103. package/src/harness/tools/tool-call-budget.ts +0 -60
  104. package/src/harness/tools/tool-context.ts +0 -5
  105. package/src/harness/tools/tool-middleware.ts +0 -105
  106. package/src/harness/tools/tool-policy.ts +0 -51
  107. package/src/harness/tools/tool-registry.ts +0 -112
  108. package/src/harness/tools/tool-result.ts +0 -15
  109. package/src/harness/tools/toolbox.ts +0 -36
  110. package/src/harness/validation/finding-validator.ts +0 -119
  111. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  112. package/src/tests/fixtures/fake-investigation.ts +0 -64
  113. package/src/tests/harness-agent.test.ts +0 -141
  114. package/src/tests/harness-investigation.test.ts +0 -206
  115. package/src/tests/harness-tools.test.ts +0 -148
  116. package/src/tests/hunt-command.test.ts +0 -212
  117. package/src/tests/rule-analyzer.test.ts +0 -61
  118. package/src/tests/tool-contracts.test.ts +0 -82
@@ -1,14 +1,10 @@
1
1
  /**
2
- * Texto de regra (biblioteca) — fonte única de polaridade e prefixos reservados.
2
+ * Texto de regra (biblioteca) — fonte única dos prefixos de polaridade.
3
3
  *
4
- * `RuleAnalyzer`, `registry-check` e o harness usam as mesmas listas: evita
5
- * divergência entre “Promover/Evitar” no analyzer e as validações anti-drift.
6
- * Também define o vocabulário de findings emergentes (lógica/domínio).
4
+ * `registry-check` usa a mesma lista para validar que a regra declara
5
+ * “Promover/Evitar” explicitamente.
7
6
  */
8
7
 
9
- /** Intenção de uma regra. */
10
- export type RuleIntent = "promote" | "avoid";
11
-
12
8
  /** Palavras que abrem uma regra de “deve acontecer”. */
13
9
  export const PROMOTE_PREFIXES = [
14
10
  "promover",
@@ -29,12 +25,6 @@ export const AVOID_PREFIXES = [
29
25
  "jamais",
30
26
  ] as const;
31
27
 
32
- /** Conjunto normalizado (usado para remover polaridade das keywords). */
33
- export const POLARITY_WORDS: ReadonlySet<string> = new Set([
34
- ...PROMOTE_PREFIXES,
35
- ...AVOID_PREFIXES,
36
- ]);
37
-
38
28
  /** Escapa um literal para uso em regex. */
39
29
  function escapeWord(word: string): string {
40
30
  return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -48,50 +38,8 @@ function patternFor(words: readonly string[]): RegExp {
48
38
  const PROMOTE_PATTERN = patternFor(PROMOTE_PREFIXES);
49
39
  const AVOID_PATTERN = patternFor(AVOID_PREFIXES);
50
40
 
51
- /** Polaridade explícita; sem prefixo conhecido, assume `avoid`. */
52
- export function polarityOf(text: string): RuleIntent {
53
- const normalized = text.trim().toLowerCase();
54
- if (PROMOTE_PATTERN.test(normalized)) return "promote";
55
- if (AVOID_PATTERN.test(normalized)) return "avoid";
56
- return "avoid";
57
- }
58
-
59
41
  /** `true` quando a regra declara polaridade explícita. */
60
42
  export function hasExplicitPolarity(text: string): boolean {
61
43
  const normalized = text.trim().toLowerCase();
62
44
  return PROMOTE_PATTERN.test(normalized) || AVOID_PATTERN.test(normalized);
63
45
  }
64
-
65
- /** Prefixos reservados para findings emergentes (fora das regras explícitas). */
66
- export const EMERGENT_PREFIXES = {
67
- logic: "Lógica:",
68
- domain: "Domínio:",
69
- } as const;
70
-
71
- /** Espécie de finding emergente. */
72
- export type EmergentKind = keyof typeof EMERGENT_PREFIXES;
73
-
74
- /** Espécie de um finding emergente (`null` quando não é emergente). */
75
- export function emergentKind(rule: string): EmergentKind | null {
76
- const normalized = rule.trim();
77
- if (normalized.startsWith(EMERGENT_PREFIXES.logic)) return "logic";
78
- if (normalized.startsWith(EMERGENT_PREFIXES.domain)) return "domain";
79
- return null;
80
- }
81
-
82
- /** `true` quando a regra usa um prefixo emergente. */
83
- export function isEmergentRule(rule: string): boolean {
84
- return emergentKind(rule) !== null;
85
- }
86
-
87
- /** Texto que sobra após o prefixo (vazio é inválido). */
88
- export function emergentText(rule: string): string {
89
- const kind = emergentKind(rule);
90
- if (!kind) return "";
91
- return rule.trim().slice(EMERGENT_PREFIXES[kind].length).trim();
92
- }
93
-
94
- /** Monta o texto de um finding emergente. */
95
- export function emergentRule(kind: EmergentKind, text: string): string {
96
- return `${EMERGENT_PREFIXES[kind]} ${text.trim()}`;
97
- }
@@ -138,6 +138,14 @@ export class GitRepository {
138
138
  return parseNumstat(this.run(["diff", "--numstat", "-z", range]));
139
139
  }
140
140
 
141
+ /**
142
+ * Patch com contexto zero (`git diff -U0 range`), sem truncar por arquivo.
143
+ * É compacto e exato nas linhas — base do escopo do diff e do delta AST.
144
+ */
145
+ patchZeroContext(range: string): Map<string, string> {
146
+ return parseDiffIndex(this.run(["diff", "-U0", range]));
147
+ }
148
+
141
149
  /** Patch por arquivo de um intervalo, opcionalmente restrito a `paths`. */
142
150
  diffPatch(
143
151
  range: string,
@@ -110,7 +110,6 @@ export type ArtifactKind =
110
110
  | "ai"
111
111
  | "ai-usage"
112
112
  | "ai-brief"
113
- | "harness-transcript"
114
113
  | "other";
115
114
 
116
115
  /** Artefato persistido de uma review. */
package/src/core/types.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { BriefPlan } from "./ai/schemas";
10
10
  import type { AstFileBrief } from "./ast/types";
11
+ import type { ChangedScope } from "./changed-scope";
11
12
  import type { JevRuleSpec } from "./jev/jev-types";
12
13
 
13
14
  /** Severidade de um achado. `blocking` bloqueia o merge. */
@@ -29,19 +30,6 @@ export const SEVERITY_EMOJI: Record<Severity, string> = {
29
30
  nit: "🔎",
30
31
  };
31
32
 
32
- /**
33
- * Sinais opcionais de uma regra: ajudam o `RuleAnalyzer` a transformar texto
34
- * em checagens concretas (símbolos, imports, decorators, paths e tools).
35
- */
36
- export type RuleHints = {
37
- symbols?: string[];
38
- imports?: string[];
39
- decorators?: string[];
40
- paths?: string[];
41
- /** Nomes de tools sugeridos (ex.: `symbol_context`). */
42
- checks?: string[];
43
- };
44
-
45
33
  /**
46
34
  * Regra do watchdog: severidade + texto. O texto declara a polaridade de forma
47
35
  * explícita — `Promover:` (algo que DEVE acontecer) ou `Evitar:`/`Não`/`Nunca`
@@ -54,7 +42,6 @@ export type Rule = {
54
42
  severity: Severity;
55
43
  text: string;
56
44
  jev?: JevRuleSpec;
57
- hints?: RuleHints;
58
45
  };
59
46
 
60
47
  /**
@@ -144,6 +131,8 @@ export type WatchdogRenderArgs = {
144
131
  ast?: readonly AstFileBrief[];
145
132
  /** Plano de revisão sugerido por IA (nunca substitui as regras). */
146
133
  aiPlan?: BriefPlan;
134
+ /** Escopo do diff por arquivo (símbolos alterados; hunks como fallback). */
135
+ changedScope?: ChangedScope;
147
136
  };
148
137
 
149
138
  /** Definição completa de um watchdog — 1 por arquivo em `project/watchdogs/`. */
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@
7
7
 
8
8
  export type {
9
9
  AiResult,
10
- AiTool,
11
10
  AiUsage,
12
11
  ModelProvider,
13
12
  ModelRequest,
@@ -26,7 +25,14 @@ export {
26
25
  } from "./core/ai";
27
26
  export { ArgParser } from "./core/arg-parser";
28
27
  export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
29
- export { computeDelta, emptyDelta, parseHunks } from "./core/ast/diff";
28
+ export {
29
+ changedAnchorLines,
30
+ changedAnchors,
31
+ changedNewLines,
32
+ computeDelta,
33
+ emptyDelta,
34
+ parseHunks,
35
+ } from "./core/ast/diff";
30
36
  export { buildImpact, resolveImport } from "./core/ast/impact";
31
37
  export { AstParser, languageForPath } from "./core/ast/parser";
32
38
  export { AstService } from "./core/ast/service";
@@ -44,6 +50,23 @@ export type {
44
50
  AstSymbolKind,
45
51
  } from "./core/ast/types";
46
52
  export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
53
+ export type {
54
+ BuildChangedScopeInput,
55
+ ChangedScope,
56
+ ChangedScopeSource,
57
+ ChangedSymbol,
58
+ FileChangedScope,
59
+ LineRange,
60
+ ScopeEntry,
61
+ } from "./core/changed-scope";
62
+ export {
63
+ buildChangedScope,
64
+ formatRanges,
65
+ formatSymbols,
66
+ isLineInScope,
67
+ mergeRanges,
68
+ summarizeScope,
69
+ } from "./core/changed-scope";
47
70
  export type { FindingsCommandResult } from "./core/commands/findings";
48
71
  export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
49
72
  export { ProjectScaffolder } from "./core/commands/scaffold";
@@ -231,7 +254,6 @@ export type {
231
254
  FileStatus,
232
255
  FileStatusMap,
233
256
  Rule,
234
- RuleHints,
235
257
  Severity,
236
258
  WatchdogDefinition,
237
259
  WatchdogMeta,
@@ -97,7 +97,6 @@ const KIND_ORDER: ArtifactKind[] = [
97
97
  "ai",
98
98
  "ai-usage",
99
99
  "ai-brief",
100
- "harness-transcript",
101
100
  "context",
102
101
  "context-brief",
103
102
  "context-summary",
@@ -119,10 +118,9 @@ const GROUP_BY_KIND: Record<ArtifactKind, { key: string; label: string; order: n
119
118
  candidates: { key: "core", label: "Núcleo da revisão", order: 3 },
120
119
  ast: { key: "core", label: "Núcleo da revisão", order: 3 },
121
120
  "ast-impact": { key: "core", label: "Núcleo da revisão", order: 3 },
122
- ai: { key: "ai", label: "IA e harness", order: 4 },
123
- "ai-usage": { key: "ai", label: "IA e harness", order: 4 },
124
- "ai-brief": { key: "ai", label: "IA e harness", order: 4 },
125
- "harness-transcript": { key: "ai", label: "IA e harness", order: 4 },
121
+ ai: { key: "ai", label: "IA", order: 4 },
122
+ "ai-usage": { key: "ai", label: "IA", order: 4 },
123
+ "ai-brief": { key: "ai", label: "IA", order: 4 },
126
124
  context: { key: "context", label: "Contexto da PR", order: 5 },
127
125
  "context-brief": { key: "context", label: "Contexto da PR", order: 5 },
128
126
  "context-summary": { key: "context", label: "Contexto da PR", order: 5 },
@@ -146,7 +144,6 @@ export const ARTIFACT_KIND_LABELS: Record<ArtifactKind, string> = {
146
144
  ai: "ia",
147
145
  "ai-usage": "uso ia",
148
146
  "ai-brief": "plano ia",
149
- "harness-transcript": "transcript",
150
147
  context: "contexto",
151
148
  "context-brief": "brief ctx",
152
149
  "context-summary": "resumo ctx",
@@ -70,7 +70,6 @@ export type ArtifactKind =
70
70
  | "ai"
71
71
  | "ai-usage"
72
72
  | "ai-brief"
73
- | "harness-transcript"
74
73
  | "other";
75
74
 
76
75
  /** Metadados de artefato retornados pela API (sem conteúdo). */
@@ -69,6 +69,11 @@ staging global da review:
69
69
  - Sem achados → `"findings": []` (arquivo **obrigatório**).
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
+ - **Escopo do diff:** a `line` deve cair dentro das faixas listadas em
73
+ *Escopo do diff (símbolos alterados)* no brief. Código fora delas é contexto — não é
74
+ achado desta revisão. Se a alteração quebra código não alterado, ancore o finding na
75
+ linha alterada que causou o problema.
76
+ - Achado **sem linha** só quando for inerente à alteração (vira comentário de arquivo).
72
77
  - Não inventar achados — se não houver violação, o array fica vazio.
73
78
 
74
79
  ## Findings emergentes (opt-in)
@@ -18,7 +18,8 @@ Um único skill (`/houndsman`), quatro capabilities (detalhadas em `hunt.md`, `t
18
18
 
19
19
  - **houndsman** — o orquestrador/treinador (CLI + motor).
20
20
  - **watchdog** — cada agente especialista por domínio que audita o diff.
21
- - **brief** — instruções prontas para um watchdog (o prompt do subagente).
21
+ - **brief** — instruções prontas para um watchdog (o prompt do subagente), incluindo o
22
+ **escopo do diff** (faixas/símbolos alterados que podem virar finding).
22
23
  - **findings** — os achados que o subagente grava em JSON (único output consumido).
23
24
  - **candidato (Jev)** — indício automático; **não** é achado (o watchdog confirma ou refuta).
24
25
  - **churn** — linhas adicionadas/removidas por arquivo; usado para priorizar a leitura.
@@ -9,7 +9,7 @@ no seu projeto é **autocontida** — não exige nenhuma dependência local do p
9
9
  - **git** — o diff usa `git diff <base>...<head> --name-status`.
10
10
  - **gh** (Opcional) — Obtém contexto da PR e dispara comentários para o GitHub
11
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.
12
+ - **`HOUNDSMAN_DEEP_SEEK_API_KEY`** (opcional) — features de IA (briefs, contexto, classificação, resumo técnico).
13
13
  - **`HOUNDSMAN_OPENAI_API_KEY`** / **`HOUNDSMAN_ANTHROPIC_API_KEY`** (opcional) — apenas se
14
14
  `ai.provider` for `openai`/`anthropic`.
15
15
 
package/src/skill/fix.md CHANGED
@@ -132,7 +132,7 @@ Volte ao passo 1. A fila encolhe a cada volta; o `batch` traz o próximo grupo s
132
132
 
133
133
  ## Perguntas de decisão ao usuário
134
134
 
135
- Pergunte (ferramenta de perguntas do harness) **antes** de corrigir quando o achado for:
135
+ Pergunte (ferramenta de perguntas do agente) **antes** de corrigir quando o achado for:
136
136
 
137
137
  - ambíguo ou com **múltiplas correções válidas**;
138
138
  - uma mudança de **produto/arquitetura** (não é "só código");
package/src/skill/hunt.md CHANGED
@@ -25,12 +25,6 @@ Traduza o pedido do usuário para flags **antes** de começar:
25
25
  **Quando usar:** revisar PR antes de merge · code review de feature branch · validar
26
26
  mudanças antes de push.
27
27
 
28
- > **Harness nativo (opcional):** além deste skill, o CLI tem `houndsman hunt` — o mesmo
29
- > fluxo executado pelo próprio houndsman, com tools read-only, investigação por evidência
30
- > e `VERIFY` obrigatório. Use `houndsman hunt --dry-run` para inspecionar sem IA. Para uma
31
- > PR específica, `houndsman hunt --pr 456` roda no worktree automaticamente. Detalhes em
32
- > `docs/HARNESS.md`.
33
-
34
28
  ## Quem executa o quê
35
29
 
36
30
  - **Você** só invoca o skill: `/houndsman [hunt]`. Não precisa rodar nenhum script à mão.
@@ -62,6 +56,8 @@ mudanças antes de push.
62
56
  - **As regras refletem os padrões APLICADOS hoje** — não validar práticas que o projeto
63
57
  não adota. Regra sem aplicação real no código gera falso positivo.
64
58
  - Cada watchdog recebe apenas seus arquivos relevantes (brief pronto) — nunca o diff completo.
59
+ - O brief traz o **escopo do diff** (símbolos alterados e faixas revisáveis); achado fora
60
+ dessas faixas é código pré-existente, não escopo desta revisão.
65
61
  - Saída estruturada (JSON) por watchdog → consolidada e deduplicada automaticamente.
66
62
 
67
63
  ---
@@ -11,7 +11,7 @@ exports[`baseline — brief defaultRenderBrief (contexto, churn, overflow, candi
11
11
 
12
12
  Watchdog baseline.
13
13
 
14
- ## Escopo
14
+ ## Escopo do domínio
15
15
 
16
16
  src/alpha/**
17
17
 
@@ -63,10 +63,10 @@ src/alpha/**
63
63
  ## Procedimento
64
64
 
65
65
  1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).
66
- 2. **READ HEAD** — leia os arquivos seletivamente (primeiras ~30 linhas). NUNCA o diff completo.
67
- 3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.
68
- 4. **DEEP-READ** — aprofunde apenas os arquivos com suspeita de violação.
69
- 5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre."
66
+ 2. **READ SCOPE** — carregue apenas as faixas alteradas em *Escopo do diff*; o restante do arquivo é só contexto.
67
+ 3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas.
68
+ 4. **DEEP-READ** — aprofunde apenas as faixas com suspeita de violação dentro do escopo.
69
+ 5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido e \`line\` dentro das faixas alteradas. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre."
70
70
  `;
71
71
 
72
72
  exports[`baseline — relatório renderReport consolidado 1`] = `
@@ -405,16 +405,6 @@ export type Rule = {
405
405
  severity: Severity;
406
406
  text: string;
407
407
  jev?: JevRuleSpec;
408
- hints?: RuleHints;
409
- };
410
-
411
- /** Sinais opcionais de uma regra (símbolos, imports, decorators, paths, checks). */
412
- export type RuleHints = {
413
- symbols?: string[];
414
- imports?: string[];
415
- decorators?: string[];
416
- paths?: string[];
417
- checks?: string[];
418
408
  };
419
409
 
420
410
  /** Como o watchdog casa um arquivo alterado. \`value\` é sempre regex. */
@@ -509,20 +499,15 @@ export type HoundsmanAiFeatures = {
509
499
  /** Overrides de IA por watchdog. */
510
500
  export type HoundsmanAiWatchdog = {
511
501
  model?: string;
512
- maxSteps?: number;
513
- maxOutputTokens?: number;
514
502
  };
515
503
 
516
- /** Configuração da camada de IA (briefs, contexto, classificação e hunt nativo). */
504
+ /** Configuração da camada de IA (briefs, contexto, classificação, pré-análise, verify). */
517
505
  export type HoundsmanAi = {
518
506
  enabled?: boolean;
519
507
  provider?: AiProviderKind;
520
508
  model?: string;
521
509
  baseURL?: string;
522
510
  concurrency?: number;
523
- maxSteps?: number;
524
- maxOutputTokens?: number;
525
- temperature?: number;
526
511
  providerOptions?: AiProviderOptions;
527
512
  timeoutMs?: number;
528
513
  maxCallsPerReview?: number;
@@ -665,15 +650,15 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
665
650
  },
666
651
  {
667
652
  "file": ".houndsman/core/skill/OUTPUT-FORMAT.md",
668
- "sha256": "94ea2b8def871784",
653
+ "sha256": "a78ed16d4d211889",
669
654
  },
670
655
  {
671
656
  "file": ".houndsman/core/skill/README.md",
672
- "sha256": "1738ef9c6d9ec919",
657
+ "sha256": "9aadf51b48c7f304",
673
658
  },
674
659
  {
675
660
  "file": ".houndsman/core/skill/SETUP.md",
676
- "sha256": "b74af7e1979bf1b4",
661
+ "sha256": "5e5335870f493aba",
677
662
  },
678
663
  {
679
664
  "file": ".houndsman/core/skill/example-review.md",
@@ -681,11 +666,11 @@ exports[`baseline — camada gerada setup gera os mesmos arquivos e conteúdos 3
681
666
  },
682
667
  {
683
668
  "file": ".houndsman/core/skill/fix.md",
684
- "sha256": "9f10b2d846dc7db5",
669
+ "sha256": "5782f4880af78d29",
685
670
  },
686
671
  {
687
672
  "file": ".houndsman/core/skill/hunt.md",
688
- "sha256": "1cf68076325da4ab",
673
+ "sha256": "dfaec1a665d7ee65",
689
674
  },
690
675
  {
691
676
  "file": ".houndsman/core/skill/train.md",
@@ -702,6 +687,7 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
702
687
  "Discovery: 1 watchdog(s) ativado(s), 3 arquivo(s), 1 deletado(s).
703
688
  Briefs: <cwd>/.home/work/<uuid>/briefs/
704
689
  Deletado (breaking change): src/alpha/old.ts
690
+ Escopo: 2 arquivo(s), 0 símbolo(s) em 2 faixa(s) alterada(s).
705
691
  Jev: pulado (desativado (--no-jev)).
706
692
  AST: desabilitado ou sem grammar compatível.
707
693
  Sem contexto da PR (rode \`bun run houndsman context\` antes, se houver PR).
@@ -753,7 +739,29 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
753
739
  },
754
740
  "excludedCount": 0,
755
741
  "headSha": "<sha>",
756
- "mode": "full"
742
+ "mode": "full",
743
+ "changedScope": {
744
+ "src/alpha/a.ts": {
745
+ "ranges": [
746
+ {
747
+ "start": 1,
748
+ "end": 2
749
+ }
750
+ ],
751
+ "symbols": [],
752
+ "source": "hunks"
753
+ },
754
+ "src/alpha/b.ts": {
755
+ "ranges": [
756
+ {
757
+ "start": 1,
758
+ "end": 1
759
+ }
760
+ ],
761
+ "symbols": [],
762
+ "source": "hunks"
763
+ }
764
+ }
757
765
  }
758
766
  "
759
767
  `;
@@ -827,10 +835,20 @@ exports[`baseline — artefatos de prepare/finalize gera classification, brief,
827
835
 
828
836
  Watchdog baseline.
829
837
 
830
- ## Escopo
838
+ ## Escopo do domínio
831
839
 
832
840
  src/alpha/**
833
841
 
842
+ ## Escopo do diff (símbolos alterados)
843
+
844
+ > **Regra de escopo:** um finding só é válido quando a \`line\` está nas faixas
845
+ > abaixo. Código fora delas é contexto — use para entender o impacto, nunca
846
+ > como achado. Se a mudança quebra código não alterado, ancore o finding na
847
+ > linha alterada que causou o problema.
848
+
849
+ - \`src/alpha/a.ts\` — linhas 1-2 _(faixas do diff)_
850
+ - \`src/alpha/b.ts\` — linhas 1 _(faixas do diff)_
851
+
834
852
  ## Regras (por severidade)
835
853
 
836
854
  - ❌ **blocking** — Nunca usar \`any\`.
@@ -852,10 +870,10 @@ src/alpha/**
852
870
  ## Procedimento
853
871
 
854
872
  1. **FILTER** — descarte o que não for do seu domínio (ver *Prioridade de leitura* e *Não carregar*).
855
- 2. **READ HEAD** — leia os arquivos seletivamente (primeiras ~30 linhas). NUNCA o diff completo.
856
- 3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações.
857
- 4. **DEEP-READ** — aprofunde apenas os arquivos com suspeita de violação.
858
- 5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre.
873
+ 2. **READ SCOPE** — carregue apenas as faixas alteradas em *Escopo do diff*; o restante do arquivo é só contexto.
874
+ 3. **GREP** — use as regras críticas/anti-padrões como heurística para localizar violações nas faixas alteradas.
875
+ 4. **DEEP-READ** — aprofunde apenas as faixas com suspeita de violação dentro do escopo.
876
+ 5. **CONCLUDE** — grave o JSON no caminho de output, com \`reviewedFiles\` preenchido e \`line\` dentro das faixas alteradas. Sem achados → \`"findings": []\` (arquivo obrigatório). Não escreva markdown livre.
859
877
  "
860
878
  `;
861
879
 
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Escopo do diff — âncoras, símbolos alterados e fallback nos hunks.
3
+ *
4
+ * Congela a regra que impede o watchdog de reportar código pré-existente: só as
5
+ * faixas tocadas pela revisão (símbolo folha do AST ou hunk) são revisáveis.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { changedAnchors } from "../core/ast/diff";
10
+ import type { AstAnalysis, AstFacts, AstSymbol } from "../core/ast/types";
11
+ import {
12
+ buildChangedScope,
13
+ formatRanges,
14
+ formatSymbols,
15
+ isLineInScope,
16
+ mergeRanges,
17
+ summarizeScope,
18
+ } from "../core/changed-scope";
19
+ import type { DiffSource } from "../core/diff-source";
20
+
21
+ /** Cabecalho padrão de um patch de arquivo. */
22
+ function patch(body: string): string {
23
+ return `diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n${body}`;
24
+ }
25
+
26
+ function diffOf(patchText: string): DiffSource {
27
+ return {
28
+ byFile: new Map([["src/a.ts", patchText]]),
29
+ churn: new Map(),
30
+ };
31
+ }
32
+
33
+ function symbol(
34
+ overrides: Partial<AstSymbol> & Pick<AstSymbol, "name" | "kind">,
35
+ ): AstSymbol {
36
+ return {
37
+ exported: false,
38
+ startLine: 1,
39
+ endLine: 1,
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ function analysis(facts: AstFacts): AstAnalysis {
45
+ return { facts };
46
+ }
47
+
48
+ const FACTS: AstFacts = {
49
+ language: "typescript",
50
+ imports: [],
51
+ exports: [],
52
+ decorators: [],
53
+ hasSyntaxErrors: false,
54
+ symbols: [
55
+ symbol({ name: "Outer", kind: "class", startLine: 1, endLine: 20 }),
56
+ symbol({ name: "Outer.inner", kind: "method", startLine: 5, endLine: 10 }),
57
+ symbol({ name: "top", kind: "function", startLine: 25, endLine: 30 }),
58
+ ],
59
+ };
60
+
61
+ describe("changedAnchors", () => {
62
+ test("adições e remoções apontam para o lado novo", () => {
63
+ const anchors = changedAnchors(
64
+ patch(
65
+ "@@ -10,3 +10,4 @@\n const before = 1;\n-const old = 2;\n+const current = 2;\n+const extra = 3;\n const after = 4;\n",
66
+ ),
67
+ );
68
+
69
+ expect([...anchors.added].sort((a, b) => a - b)).toEqual([11, 12]);
70
+ expect([...anchors.deletedAt]).toEqual([11]);
71
+ });
72
+
73
+ test("remoção pura ancora no ponto do arquivo (sem consumir linha nova)", () => {
74
+ const anchors = changedAnchors(patch("@@ -10,2 +10,0 @@\n-a\n-b\n"));
75
+
76
+ expect([...anchors.added]).toEqual([]);
77
+ expect([...anchors.deletedAt]).toEqual([10]);
78
+ });
79
+ });
80
+
81
+ describe("buildChangedScope", () => {
82
+ const entries = [{ path: "src/a.ts", status: "modified" }];
83
+
84
+ test("âncora dentro de símbolo folha vira o intervalo do símbolo", () => {
85
+ const text = patch(
86
+ [
87
+ "@@ -2,0 +3,1 @@",
88
+ "+export const header = 1;",
89
+ "@@ -7,0 +8,1 @@",
90
+ "+ const inside = 1;",
91
+ "@@ -26,0 +27,1 @@",
92
+ "+ return 1;",
93
+ "",
94
+ ].join("\n"),
95
+ );
96
+ const scope = buildChangedScope({
97
+ entries,
98
+ diff: diffOf(text),
99
+ ast: new Map([["src/a.ts", analysis(FACTS)]]),
100
+ });
101
+
102
+ const file = scope["src/a.ts"];
103
+ expect(file?.source).toBe("ast");
104
+ // `inner` (5-10) e `top` (25-30); o container `Outer` não expande.
105
+ expect(file?.symbols.map((item) => item.name)).toEqual(["Outer.inner", "top"]);
106
+ // Cabeçalho da classe (linha 3) fica como faixa avulsa.
107
+ expect(file?.ranges).toEqual([
108
+ { start: 3, end: 3 },
109
+ { start: 5, end: 10 },
110
+ { start: 25, end: 30 },
111
+ ]);
112
+ });
113
+
114
+ test("sem AST cai para os hunks do diff", () => {
115
+ const text = patch("@@ -10,0 +11,2 @@\n+a\n+b\n");
116
+ const scope = buildChangedScope({ entries, diff: diffOf(text) });
117
+
118
+ const file = scope["src/a.ts"];
119
+ expect(file?.source).toBe("hunks");
120
+ expect(file?.symbols).toEqual([]);
121
+ expect(file?.ranges).toEqual([{ start: 11, end: 12 }]);
122
+ });
123
+
124
+ test("rename/binário sem hunks fica sem faixas", () => {
125
+ const scope = buildChangedScope({
126
+ entries: [{ path: "src/a.ts", status: "renamed" }],
127
+ diff: diffOf(patch("")),
128
+ });
129
+
130
+ expect(scope["src/a.ts"]).toEqual({ ranges: [], symbols: [], source: "hunks" });
131
+ });
132
+
133
+ test("arquivo deletado nem entra no escopo", () => {
134
+ const scope = buildChangedScope({
135
+ entries: [{ path: "src/a.ts", status: "deleted" }],
136
+ diff: diffOf(patch("@@ -1,1 +0,0 @@\n-x\n")),
137
+ });
138
+
139
+ expect(scope["src/a.ts"]).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe("helpers de escopo", () => {
144
+ test("isLineInScope respeita as faixas", () => {
145
+ const scope = {
146
+ "src/a.ts": {
147
+ ranges: [{ start: 5, end: 10 }],
148
+ symbols: [],
149
+ source: "hunks" as const,
150
+ },
151
+ };
152
+
153
+ expect(isLineInScope(scope, "src/a.ts", 7)).toBe(true);
154
+ expect(isLineInScope(scope, "src/a.ts", 11)).toBe(false);
155
+ expect(isLineInScope(scope, "src/b.ts", 7)).toBe(false);
156
+ });
157
+
158
+ test("mergeRanges une sobrepostos e adjacentes", () => {
159
+ expect(
160
+ mergeRanges([
161
+ { start: 10, end: 12 },
162
+ { start: 11, end: 15 },
163
+ { start: 16, end: 18 },
164
+ { start: 30, end: 30 },
165
+ ]),
166
+ ).toEqual([
167
+ { start: 10, end: 18 },
168
+ { start: 30, end: 30 },
169
+ ]);
170
+ });
171
+
172
+ test("formatadores compactos", () => {
173
+ expect(formatRanges([])).toBe("sem linhas alteradas");
174
+ expect(
175
+ formatRanges([
176
+ { start: 10, end: 14 },
177
+ { start: 22, end: 22 },
178
+ ]),
179
+ ).toBe("10-14, 22");
180
+ expect(
181
+ formatSymbols([
182
+ { name: "foo", kind: "function", startLine: 10, endLine: 14 },
183
+ ]),
184
+ ).toBe("`foo` (10-14)");
185
+ expect(
186
+ summarizeScope({
187
+ "src/a.ts": {
188
+ ranges: [{ start: 1, end: 2 }],
189
+ symbols: [
190
+ { name: "foo", kind: "function", startLine: 1, endLine: 2 },
191
+ ],
192
+ source: "ast",
193
+ },
194
+ }),
195
+ ).toEqual({ files: 1, ranges: 1, symbols: 1 });
196
+ });
197
+ });