@payconductor/houndsman 0.1.1 → 0.1.3

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 (42) hide show
  1. package/README.md +32 -14
  2. package/package.json +1 -1
  3. package/src/cli/index.ts +22 -11
  4. package/src/core/assets.ts +3 -0
  5. package/src/core/brief.ts +2 -3
  6. package/src/core/commands/findings.ts +232 -4
  7. package/src/core/commands/review.ts +2 -0
  8. package/src/core/commands/scaffold.ts +13 -44
  9. package/src/core/diff-source.ts +4 -4
  10. package/src/core/findings-state.ts +20 -2
  11. package/src/core/fix-batch.ts +236 -0
  12. package/src/core/git-revision.ts +105 -0
  13. package/src/core/jev/index.ts +1 -0
  14. package/src/core/jev/jev-detector.ts +2 -2
  15. package/src/core/jev/jev-verify.ts +178 -0
  16. package/src/core/orchestrator.ts +202 -15
  17. package/src/core/paths.ts +6 -0
  18. package/src/core/report-builder.ts +5 -0
  19. package/src/core/run-status.ts +123 -0
  20. package/src/core/verify.ts +175 -0
  21. package/src/core/watchdog-registry.ts +1 -2
  22. package/src/index.ts +1 -1
  23. package/src/server/artifacts.ts +1 -0
  24. package/src/server/client/RunList.tsx +22 -13
  25. package/src/server/client/constants.ts +32 -1
  26. package/src/server/client/types.ts +4 -0
  27. package/src/server/index.ts +12 -1
  28. package/src/skill/AUDIT-PROTOCOL.md +2 -2
  29. package/src/skill/README.md +29 -5
  30. package/src/skill/SETUP.md +11 -1
  31. package/src/skill/SKILL.md +48 -0
  32. package/src/skill/fix.md +206 -0
  33. package/src/skill/{houndsman.md → hunt.md} +41 -68
  34. package/src/skill/{houndsman-update.md → train.md} +11 -11
  35. package/src/skill/verify.md +70 -0
  36. package/src/tests/findings-commands.test.ts +252 -2
  37. package/src/tests/findings-state.test.ts +52 -1
  38. package/src/tests/fix-batch.test.ts +257 -0
  39. package/src/tests/run-status.test.ts +147 -0
  40. package/src/tests/scaffold.test.ts +59 -0
  41. package/src/tests/server.test.ts +30 -0
  42. package/src/tests/verify.test.ts +170 -0
@@ -1,18 +1,18 @@
1
- # houndsman — {{projectName}} (v1)
1
+ # houndsman — {{projectName}} · `hunt` (revisão de PR)
2
2
 
3
- > **Documento canônico do skill `/houndsman`.** Os `SKILL.md` de cada harness
4
- > (`.agents/skills/houndsman/` e `.claude/skills/houndsman/`) são apenas ponteiros para cá.
3
+ > **Documento canônico da capability `hunt`.** O entry point do skill é o `SKILL.md`
4
+ > (`.agents/skills/houndsman/SKILL.md` e `.claude/skills/houndsman/SKILL.md`).
5
5
 
6
6
  Revisão de PR: classificação de arquivos → revisão paralela por domínio → consolidação.
7
- O motor é o CLI **`houndsman`**; as regras do projeto ficam em `houndsman.config.ts` + `houndsman/`.
7
+ Invocação: `/houndsman` (default) ou `/houndsman hunt`. O motor é o CLI **`houndsman`**;
8
+ as regras do projeto ficam em `houndsman.config.ts` + `.houndsman/`.
8
9
 
9
- **Terminologia:** **houndsman** = orquestrador/treinador (CLI + motor); **watchdog** = cada agente
10
- especialista por domínio que audita o diff (antigo "revisor"); **review/revisão** = o processo;
11
- **Jev** = pré-análise opcional (Passo 1.5).
10
+ **Quando usar:** revisar PR antes de merge · code review de feature branch · validar
11
+ mudanças antes de push.
12
12
 
13
13
  ## Quem executa o quê
14
14
 
15
- - **Você** só invoca o skill: `/houndsman`. Não precisa rodar nenhum script à mão.
15
+ - **Você** só invoca o skill: `/houndsman [hunt]`. Não precisa rodar nenhum script à mão.
16
16
  - **O skill** orquestra: executa o CLI `houndsman` e despacha os subagentes.
17
17
  - **O CLI** tem passos distintos, um comando cada:
18
18
 
@@ -21,9 +21,10 @@ especialista por domínio que audita o diff (antigo "revisor"); **review/revisã
21
21
  | 0. Contexto | `houndsman context` | subagente | `context.json` + `context-brief.md` → `context-summary.json` |
22
22
  | 1. Preparar | `houndsman` | skill | `classification.json` + `candidates.json` + `briefs/` + `manifest.json` |
23
23
  | 2. Revisar | *(subagentes)* | skill (via `runSubagent`) | `findings/<watchdog>.json` (um por watchdog) |
24
- | 3. Finalizar | `houndsman --finalize` | skill | `review.md` |
24
+ | 3. Finalizar | `houndsman --finalize` | skill | `review.md` + `state.json` + `run.json` |
25
25
 
26
26
  > Por que comandos separados? Entre o passo 1 e o 3 acontece trabalho de LLM (os subagentes).
27
+ > Depois do passo 3, a correção dos achados é a capability **`fix`** — ver `fix.md`.
27
28
 
28
29
  **Arquitetura:**
29
30
 
@@ -42,12 +43,6 @@ especialista por domínio que audita o diff (antigo "revisor"); **review/revisã
42
43
  - Cada watchdog recebe apenas seus arquivos relevantes (brief pronto) — nunca o diff completo.
43
44
  - Saída estruturada (JSON) por watchdog → consolidada e deduplicada automaticamente.
44
45
 
45
- ## Quando Usar
46
-
47
- - Revisar PR antes de merge
48
- - Code review de feature branch
49
- - Validar mudanças antes de push
50
-
51
46
  ---
52
47
 
53
48
  ## Passo 0 — Contexto da PR (subagente)
@@ -114,6 +109,20 @@ as regras atômicas dos watchdogs. Respostas acima do limiar viram **candidatos*
114
109
  - **Opcional**: sem chave / erro / timeout → `available:false`; a revisão segue idêntica.
115
110
  - Desativar: `houndsman --no-jev`.
116
111
 
112
+ ### Review incremental (da 2ª revisão em diante)
113
+
114
+ Cada `--finalize` grava o **commit revisado** (`reviewed.headSha` no `state.json`). No prepare
115
+ seguinte, se o `HEAD` descende desse commit (sem rebase), o motor revisa **só o delta** — os
116
+ arquivos alterados desde a última revisão — e **carrega** os findings dos arquivos inalterados
117
+ (carry-over), sem re-revisá-los. O `manifest.json` traz o bloco `incremental` (`from`,
118
+ `changedFiles`, `carried`) e o `review.md` abre com um aviso.
119
+
120
+ - Sem mudanças desde a última revisão → **no-op** (nada a revisar; o estado é preservado).
121
+ - Rebase/force-push (histórico reescrito) → cai automaticamente no **review completo**.
122
+ - Forçar completo: `houndsman --full` (ou `--clean`, que também descarta a triagem anterior).
123
+
124
+ > Custo: o LLM só é gasto nos arquivos que mudaram desde a última revisão — não na PR inteira.
125
+
117
126
  ---
118
127
 
119
128
  ## Passo 2 — Revisar (skill despacha os subagentes)
@@ -151,46 +160,10 @@ Falha com mensagem clara se faltar algum `findings/<watchdog>.json`. Quando todo
151
160
  7. Grava um `review.<watchdog>.md` por watchdog com achados e indexa no `review.md`
152
161
  (**Relatórios por domínio**) — mantém o relatório consolidado enxuto
153
162
  8. Grava `.houndsman/.runs/<branch-slug>/state.json` — a **triagem**: um `id` curto e um
154
- `fingerprint` de conteúdo por finding (Passo 4), preservados entre execuções
163
+ `fingerprint` de conteúdo por finding, preservados entre execuções
155
164
 
156
- > Compatível com `classification.json` antigo: se faltar o churn, o orçamento não é aplicado.
157
-
158
- ---
159
-
160
- ## Passo 4 — Triagem (opcional)
161
-
162
- Cada finding recebe um `id` curto e um `fingerprint` de **conteúdo** no `--finalize`,
163
- gravados em `.houndsman/.runs/<branch-slug>/state.json`. A triagem é feita pela UI ou
164
- pelo agente — sem editar o JSON à mão. Duas dimensões independentes:
165
-
166
- - **decision:** `pending` · `execute` · `ignore` (+ `comment` opcional).
167
- - **execState:** `pending` · `in_progress` · `done` · `stale`.
168
-
169
- ### UI local
170
-
171
- ```bash
172
- houndsman server --open
173
- ```
174
-
175
- Abre a UI (Bun.serve + Preact): lista as runs, triar os findings por severidade, comentar,
176
- marcar como feito e ler os artefatos (`manifest.json`, `classification.json`, `review.md`).
177
- Bind em `127.0.0.1` (ferramenta local, sem auth).
178
-
179
- ### Auto-fix pelo agente
180
-
181
- ```bash
182
- houndsman findings list --decision execute --json # pendentes marcados p/ corrigir
183
- # ... o agente aplica as correções ...
184
- houndsman findings sync # reancora por conteúdo
185
- houndsman findings done <id> # confirma os concluídos
186
- ```
187
-
188
- > **A linha não é a identidade.** O auto-fix desloca linhas; o motor reancora cada finding
189
- > pelo **conteúdo** (trecho + contexto) e não perde itens que se movem. Se a âncora some
190
- > (o trecho foi corrigido), o finding vira `done` automaticamente quando a decisão era
191
- > `execute`, ou `stale` caso contrário.
192
- > Ocorrências byte-idênticas (mesmo trecho E mesmo contexto) compartilham o id — são
193
- > tratadas como um grupo (`×N`); limitação conhecida.
165
+ Após finalizar, corrija os achados com a capability **`fix`** (`fix.md`): a UI ou os
166
+ comandos `findings` levam do achado à correção.
194
167
 
195
168
  ---
196
169
 
@@ -243,9 +216,11 @@ git diff --name-status (base...head)
243
216
  ↓
244
217
  [2] N subagentes em paralelo ──► findings/<watchdog>.json
245
218
  ↓
246
- [3] houndsman --finalize ──► review.md + state.json
219
+ [3] houndsman --finalize ──► review.md + state.json + run.json (review_done)
247
220
  ↓
248
- [4] triagem (UI ou agente) ──► state.json (decision/execState)
221
+ [4] server (UI) ──► validar a revisão + marcar decision: execute
222
+ ↓
223
+ [5] fix ──► correções + state.json/run.json (fix_done)
249
224
  ```
250
225
 
251
226
  ---
@@ -254,25 +229,23 @@ git diff --name-status (base...head)
254
229
 
255
230
  ```
256
231
  /houndsman
257
- /houndsman feature/add-webhook-retry
232
+ /houndsman hunt
233
+ /houndsman hunt feature/add-webhook-retry
258
234
  ```
259
235
 
260
236
  Se a branch não for informada, detecta com `git branch --show-current`. Base do diff: `main`.
261
237
 
262
- **Comandos do CLI:**
238
+ **Comandos do CLI usados no `hunt`:**
263
239
 
264
240
  | Comando | Função |
265
241
  |---|---|
266
242
  | `houndsman context` | Passo 0 — contexto da PR via `gh` |
267
- | `houndsman` | Passo 1 — preparar (briefs + manifesto) |
243
+ | `houndsman` | Passo 1 — preparar (briefs + manifesto; incremental quando possível) |
268
244
  | `houndsman --finalize` | Passo 3 — gerar o relatório |
269
245
  | `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
270
- | `houndsman findings <sub>` | Triagem dos findings (list/show/set/done/reset/sync/summary/path) |
271
- | `houndsman server` | UI local de triagem + leitura de artefatos (`--port`, `--host`, `--open`) |
272
- | `houndsman setup` | Cria a configuração inicial do projeto |
273
- | `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
274
- | `houndsman doctor` | Valida a configuração, o ambiente e a credencial Jev |
275
- | `houndsman test` | Suíte de testes da lib |
246
+
247
+ Flags do review: `--base` · `--head` · `--branch` · `--dir` · `--clean` · `--full` (força
248
+ completo) · `--no-jev` · `--fail-on-blocking` (com `--finalize`).
276
249
 
277
250
  ---
278
251
 
@@ -294,11 +267,11 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
294
267
 
295
268
  ## Manutenção das Regras
296
269
 
297
- > Alterar qualquer regra (adicionar, ajustar severidade, remover, corrigir escopo) → usar o
298
- > skill **`houndsman-update`** (`src/skill/houndsman-update.md`).
270
+ > Alterar qualquer regra (adicionar, ajustar severidade, remover, corrigir escopo) → capability
271
+ > **`train`** (`/houndsman train`, doc `train.md`).
299
272
  > Regra de ouro: só é regra o que o projeto **aplica hoje** — prove com evidência no código
300
273
  > (regra negativa com 0 ocorrências = seguida → **manter**).
301
- > Após qualquer alteração, rodar `houndsman doctor` + `houndsman test` + `ci:fix` + `ci:check`.
274
+ > Após qualquer alteração, rodar `houndsman doctor` + `houndsman test` + `ci:fix` + `ci`.
302
275
 
303
276
  ---
304
277
 
@@ -1,14 +1,14 @@
1
- # Atualização de Lógicas do houndsman
1
+ # houndsman — {{projectName}} · `train` (atualização de regras)
2
2
 
3
- > **Documento canônico do skill `/houndsman-update`.** Os `SKILL.md` de cada harness
4
- > (`.agents/skills/houndsman-update/` e `.claude/skills/houndsman-update/`) são apenas ponteiros para cá.
3
+ > **Documento canônico da capability `train`.** O entry point do skill é o `SKILL.md`
4
+ > (`.agents/skills/houndsman/SKILL.md` e `.claude/skills/houndsman/SKILL.md`).
5
5
 
6
6
  > **Propósito:** manter os watchdogs do skill `houndsman` alinhados ao que o
7
7
  > projeto **realmente aplica** — seja para adicionar/ajustar uma regra a pedido do
8
8
  > usuário, seja para uma auditoria geral após mudanças de padrão no código.
9
9
 
10
- Este skill é o **mantenedor** do `houndsman`. Ele não revisa PRs; ele atualiza as regras
11
- que o `houndsman` usa.
10
+ Invocação: `/houndsman train`. Esta capability é a **mantenedora** do `houndsman`.
11
+ Ela não revisa PRs (isso é `hunt`); ela atualiza as regras que o `houndsman` usa.
12
12
 
13
13
  ---
14
14
 
@@ -86,7 +86,7 @@ Toda regra (nova ou alterada) deve passar por **todos** os itens:
86
86
  - O projeto mudou de padrão (ex.: passou a usar uma lib, mudou autenticação, etc.).
87
87
  - **Auditoria completa** dos watchdogs após revisão geral do código.
88
88
  - **Regra confusa/ambígua** (humano ou IA não entende) — ver Modo C.
89
- - O usuário invoca `/houndsman-update`.
89
+ - O usuário invoca `/houndsman train`.
90
90
 
91
91
  ---
92
92
 
@@ -108,7 +108,7 @@ Toda regra (nova ou alterada) deve passar por **todos** os itens:
108
108
  | `src/core/config-loader.ts` | Descoberta/carregamento de `houndsman.config.ts` |
109
109
  | `src/core/brief.ts` | Render do brief: `defaultRenderBrief` + `PROCEDURE` |
110
110
  | `src/core/registry-check.ts` | Validação anti-drift (config-driven) |
111
- | `src/skill/houndsman.md` | Orquestração + tabela de watchdogs |
111
+ | `src/skill/hunt.md` | Orquestração da revisão (capability `hunt`) |
112
112
  | `src/tests/**` | Suíte da lib (project-agnóstica; usa `fixtures/fake-project.ts`) |
113
113
  | `src/cli/index.ts` | **CLI único** — `houndsman [context, setup, doctor, test, watchdog, --finalize]` |
114
114
 
@@ -169,7 +169,7 @@ verificáveis por leitura local. Candidatos não confirmados pelo watchdog **nã
169
169
  - Ajustar `scope` / `description` / `reviewFocus` / `exclude` / `contextFiles` se necessário.
170
170
  - Se a regra muda para outro domínio (paths), ajustar o `classify` do watchdog.
171
171
  5. **Watchdog novo?** criar `.houndsman/watchdogs/<id>.ts` e registrar em `.houndsman/watchdogs/index.ts` (o id é derivado das chaves).
172
- 6. **Atualizar `src/skill/houndsman.md`/`README.md`** se escopo/regra visível mudou (no motor).
172
+ 6. **Atualizar `src/skill/hunt.md`/`README.md`** se escopo/regra visível mudou (no motor).
173
173
  7. **Atualizar a suíte da lib** se o comportamento do MOTOR mudou (a suíte é project-agnóstica).
174
174
  8. **Verificar** (seção abaixo).
175
175
 
@@ -228,16 +228,16 @@ todos os itens — em especial: **geral/reutilizável** (não regra de negócio
228
228
  bun run houndsman doctor # config sem drift + ambiente + credencial Jev
229
229
  bun run houndsman test # suíte da lib (project-agnóstica)
230
230
  bun run ci:fix # formatação/imports/lint (no repo da lib)
231
- bun run ci:check # tipagem (no repo da lib)
231
+ bun run ci # tipagem (no repo da lib)
232
232
  ```
233
233
 
234
234
  Checklist final:
235
235
 
236
236
  - [ ] `bun run houndsman doctor` → sem problemas reportados.
237
237
  - [ ] `bun run houndsman test` → **0 fail**.
238
- - [ ] `ci:fix` + `ci:check` → exit 0 (warnings pré-existentes não contam).
238
+ - [ ] `ci:fix` + `ci` → exit 0 (warnings pré-existentes não contam).
239
239
  - [ ] `WATCHDOG_DEFINITIONS[<id>].id` bate com a chave e `description` não vazio.
240
- - [ ] `src/skill/houndsman.md` continua consistente com os watchdogs do projeto.
240
+ - [ ] `src/skill/hunt.md` continua consistente com os watchdogs do projeto.
241
241
  - [ ] Toda regra nova/alterada tem **evidência** registrada no PR/relatório
242
242
  (`file:line` se afirmativa; `0 ocorrências do anti-padrão` se negativa).
243
243
  - [ ] Nenhuma regra **negativa** foi removida por "0 ocorrências" (Regra de Ouro).
@@ -0,0 +1,70 @@
1
+ # houndsman — {{projectName}} · `verify` (violações resolvidas?)
2
+
3
+ > **Documento canônico da capability `verify`.** O entry point do skill é o `SKILL.md`
4
+ > (`.agents/skills/houndsman/SKILL.md` e `.claude/skills/houndsman/SKILL.md`).
5
+
6
+ Confere se os findings de uma revisão **já finalizada** foram resolvidos — **sem re-revisar a
7
+ PR**. Invocação: `/houndsman verify`. Consome o `state.json` do `hunt`; não revisa nem altera
8
+ regras.
9
+
10
+ **Quando usar:**
11
+
12
+ - Depois de corrigir achados (`fix`), para confirmar que as violações sumiram.
13
+ - As violações foram resolvidas? · confere se ainda está lá · valida o fix.
14
+
15
+ > **Pré-requisito:** existe `.houndsman/.runs/<branch-slug>/state.json` (gerado no `--finalize`).
16
+
17
+ ---
18
+
19
+ ## Como funciona (2 tiers)
20
+
21
+ 1. **Tier local (grátis, determinístico).** Para cada finding presente, reancora a âncora de
22
+ conteúdo contra o working tree (`core/finding-anchor.ts`):
23
+ - **âncora presente** (`ok`/`moved`) → **persiste** (a violação ainda está lá);
24
+ - **âncora sumida** (`stale`) → provável **resolvido** — mas o trecho pode ter sido reescrito;
25
+ - **arquivo ausente** → fora da verificação (deletado/movido).
26
+
27
+ 2. **Tier Jev (opcional, só nos ambíguos).** Com `TYPESAFE_API_KEY`, os casos `stale` / sem
28
+ âncora são confirmados pelo LLM sobre um **diff focado** do arquivo (`foundAtSha..HEAD`), uma
29
+ pergunta `noul` por finding. Falha-aberto: sem chave/erro → vale o resultado local.
30
+
31
+ **Custo:** ~0 LLM quando o tier local decide; 1 chamada por arquivo ambíguo — nunca uma revisão
32
+ completa.
33
+
34
+ ---
35
+
36
+ ## Procedimento
37
+
38
+ ```bash
39
+ bun run houndsman findings verify # relatório (não altera o estado)
40
+ bun run houndsman findings verify --json # saída estruturada
41
+ bun run houndsman findings verify --apply # marca os resolvidos como done (ping no run.json)
42
+ ```
43
+
44
+ - Sem `--apply`: **só relatório** (grava `verify.md` na pasta da run). Nada muda no `state.json`.
45
+ - Com `--apply`: os findings com status **`resolved`** viram `execState: done` (histórico
46
+ `by: "verify"`). `persisting` e `uncertain` **não** são tocados.
47
+
48
+ Filtros (como no `findings list`): `--severity`, `--watchdog`, `--file`, `--decision`. `--all`
49
+ inclui ausentes. `--no-jev` desliga o tier LLM. `--fail-on-persisting` sai com código 1 se algum
50
+ `blocking` persistir (útil em CI).
51
+
52
+ ---
53
+
54
+ ## Status possíveis
55
+
56
+ | Status | Significado | `--apply`? |
57
+ |---|---|---|
58
+ | `resolved` | Violação não encontrada (âncora sumida; confirmada pelo Jev quando há chave). | ✅ vira `done` |
59
+ | `persisting` | Âncora ainda presente — a violação continua. | ❌ |
60
+ | `uncertain` | Sem âncora e sem confirmação do Jev (ou Jev indisponível). | ❌ |
61
+ | `file-missing` | Arquivo não está no working tree (deletado/movido). | ❌ |
62
+
63
+ ---
64
+
65
+ ## Anti-padrões
66
+
67
+ - ❌ Reexecutar o `hunt` inteiro só para conferir se um fix funcionou — use o `verify`.
68
+ - ❌ Esperar que o `verify` marque `done` sem `--apply` (sem a flag, nada muda no estado).
69
+ - ❌ Tratar `persisting` como resolvido — a âncora ainda está no arquivo.
70
+ - ❌ Esperar verificação 100% pelo LLM — o tier local resolve a maioria de graça.
@@ -1,8 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
3
10
  import { tmpdir } from "node:os";
4
11
  import { join } from "node:path";
5
- import { runFindingsCommand } from "../core/commands/findings";
12
+ import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
6
13
  import { fakeHoundsmanConfig } from "./fixtures/fake-project";
7
14
 
8
15
  /** Run fake em disco: classification.json + findings/alpha.json. */
@@ -223,4 +230,247 @@ describe("findings command", () => {
223
230
  run0.cleanup();
224
231
  }
225
232
  });
233
+
234
+ test("batch agrupa a fila e devolve o grupo atual + outros grupos", () => {
235
+ const dir = mkdtempSync(join(tmpdir(), "houndsman-batch-"));
236
+ const outDir = join(dir, "main");
237
+ mkdirSync(join(outDir, "findings"), { recursive: true });
238
+ writeFileSync(
239
+ join(outDir, "classification.json"),
240
+ JSON.stringify({ watchdogs: { alpha: ["src/a.ts"], beta: ["src/b.ts"] } }),
241
+ );
242
+ writeFileSync(
243
+ join(outDir, "findings", "alpha.json"),
244
+ JSON.stringify({
245
+ watchdogId: "alpha",
246
+ reviewedFiles: ["src/a.ts"],
247
+ skipped: [],
248
+ findings: [
249
+ {
250
+ rule: "R1",
251
+ severity: "blocking",
252
+ file: "src/a.ts",
253
+ line: 1,
254
+ message: "m1",
255
+ action: "a",
256
+ },
257
+ {
258
+ rule: "R2",
259
+ severity: "attention",
260
+ file: "src/a.ts",
261
+ line: 2,
262
+ message: "m2",
263
+ action: "a",
264
+ },
265
+ {
266
+ rule: "R3",
267
+ severity: "attention",
268
+ file: "src/a.ts",
269
+ line: 3,
270
+ message: "m3",
271
+ action: "a",
272
+ },
273
+ {
274
+ rule: "R4",
275
+ severity: "nit",
276
+ file: "src/a.ts",
277
+ line: 4,
278
+ message: "m4",
279
+ action: "a",
280
+ },
281
+ ],
282
+ }),
283
+ );
284
+ writeFileSync(
285
+ join(outDir, "findings", "beta.json"),
286
+ JSON.stringify({
287
+ watchdogId: "beta",
288
+ reviewedFiles: ["src/b.ts"],
289
+ skipped: [],
290
+ findings: [
291
+ {
292
+ rule: "R5",
293
+ severity: "attention",
294
+ file: "src/b.ts",
295
+ line: 1,
296
+ message: "m5",
297
+ action: "a",
298
+ },
299
+ {
300
+ rule: "R6",
301
+ severity: "suggestion",
302
+ file: "src/b.ts",
303
+ line: 2,
304
+ message: "m6",
305
+ action: "a",
306
+ },
307
+ ],
308
+ }),
309
+ );
310
+ try {
311
+ const list = JSON.parse(
312
+ run(["list", "--dir", dir, "--branch", "main", "--json"]).output,
313
+ );
314
+ for (const item of list) {
315
+ run([
316
+ "set",
317
+ item.id,
318
+ "--decision",
319
+ "execute",
320
+ "--dir",
321
+ dir,
322
+ "--branch",
323
+ "main",
324
+ ]);
325
+ }
326
+
327
+ const result = run(["batch", "--dir", dir, "--branch", "main", "--json"]);
328
+ expect(result.exitCode).toBe(0);
329
+ const parsed = JSON.parse(result.output);
330
+ expect(parsed.dimension).toBe("file");
331
+ expect(parsed.totals.queue).toBe(6);
332
+ expect(parsed.totals.groups).toBe(2);
333
+ expect(parsed.current.key).toBe("file:src/a.ts");
334
+ expect(parsed.current.findings).toHaveLength(4);
335
+ expect(parsed.others).toEqual([
336
+ expect.objectContaining({ key: "file:src/b.ts", count: 2 }),
337
+ ]);
338
+
339
+ const markdown = run(["batch", "--dir", dir, "--branch", "main"]);
340
+ expect(markdown.output).toContain("Grupo atual");
341
+ expect(markdown.output).toContain("Outros grupos");
342
+
343
+ const scoped = run([
344
+ "batch",
345
+ "--group",
346
+ "file:src/b.ts",
347
+ "--dir",
348
+ dir,
349
+ "--branch",
350
+ "main",
351
+ "--json",
352
+ ]);
353
+ expect(JSON.parse(scoped.output).current.key).toBe("file:src/b.ts");
354
+ } finally {
355
+ rmSync(dir, { recursive: true, force: true });
356
+ }
357
+ });
358
+
359
+ test("mutações fazem ping no run.json (status)", () => {
360
+ const run0 = makeRun(ONE_FINDING);
361
+ try {
362
+ const id = JSON.parse(
363
+ run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
364
+ )[0].id;
365
+ const runJson = join(run0.dir, "main", "run.json");
366
+
367
+ run([
368
+ "set",
369
+ id,
370
+ "--decision",
371
+ "execute",
372
+ "--dir",
373
+ run0.dir,
374
+ "--branch",
375
+ "main",
376
+ ]);
377
+ expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("review_done");
378
+
379
+ run([
380
+ "set",
381
+ id,
382
+ "--exec",
383
+ "in_progress",
384
+ "--dir",
385
+ run0.dir,
386
+ "--branch",
387
+ "main",
388
+ ]);
389
+ expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("fixing");
390
+
391
+ run(["done", id, "--dir", run0.dir, "--branch", "main"]);
392
+ expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("fix_done");
393
+ } finally {
394
+ run0.cleanup();
395
+ }
396
+ });
397
+ });
398
+
399
+ describe("findings verify", () => {
400
+ const FILE = "src/tests/fixtures/fake-project.ts";
401
+
402
+ /** Run fake apontando para um arquivo real (relativo ao cwd → âncora resolvível). */
403
+ function makeVerifyRun(): { dir: string; outDir: string; cleanup: () => void } {
404
+ const dir = mkdtempSync(join(tmpdir(), "houndsman-verify-"));
405
+ const outDir = join(dir, "main");
406
+ mkdirSync(join(outDir, "findings"), { recursive: true });
407
+ writeFileSync(
408
+ join(outDir, "classification.json"),
409
+ JSON.stringify({ watchdogs: { alpha: [FILE] } }),
410
+ );
411
+ writeFileSync(
412
+ join(outDir, "findings", "alpha.json"),
413
+ JSON.stringify({
414
+ watchdogId: "alpha",
415
+ reviewedFiles: [FILE],
416
+ skipped: [],
417
+ findings: [
418
+ {
419
+ rule: "regra",
420
+ severity: "attention",
421
+ file: FILE,
422
+ line: 1,
423
+ message: "msg",
424
+ action: "ação",
425
+ },
426
+ ],
427
+ }),
428
+ );
429
+ return {
430
+ dir,
431
+ outDir,
432
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
433
+ };
434
+ }
435
+
436
+ test("reporta persisting e grava o verify.md (tier local)", async () => {
437
+ const run0 = makeVerifyRun();
438
+ try {
439
+ const result = await runVerifyCommand(fakeHoundsmanConfig, [
440
+ "verify",
441
+ "--dir",
442
+ run0.dir,
443
+ "--branch",
444
+ "main",
445
+ "--json",
446
+ ]);
447
+ expect(result.exitCode).toBe(0);
448
+ const parsed = JSON.parse(result.output);
449
+ expect(parsed.summary.persisting).toBe(1);
450
+ expect(parsed.entries[0].basis).toBe("anchor");
451
+ expect(existsSync(join(run0.outDir, "verify.md"))).toBe(true);
452
+ } finally {
453
+ run0.cleanup();
454
+ }
455
+ });
456
+
457
+ test("sem --apply não altera o estado", async () => {
458
+ const run0 = makeVerifyRun();
459
+ try {
460
+ await runVerifyCommand(fakeHoundsmanConfig, [
461
+ "verify",
462
+ "--dir",
463
+ run0.dir,
464
+ "--branch",
465
+ "main",
466
+ ]);
467
+ const state = JSON.parse(
468
+ readFileSync(join(run0.outDir, "state.json"), "utf8"),
469
+ ) as { findings: Record<string, { execState: string }> };
470
+ const only = Object.values(state.findings)[0];
471
+ expect(only.execState).toBe("pending");
472
+ } finally {
473
+ run0.cleanup();
474
+ }
475
+ });
226
476
  });
@@ -267,8 +267,59 @@ describe("FindingsStateStore", () => {
267
267
 
268
268
  test("emptyState cria um estado vazio válido", () => {
269
269
  const state = emptyState("feature/x");
270
- expect(state.version).toBe(1);
270
+ expect(state.version).toBe(2);
271
271
  expect(state.slug).toBe("feature-x");
272
272
  expect(state.findings).toEqual({});
273
273
  });
274
274
  });
275
+
276
+ describe("rastreio de revisão (incremental)", () => {
277
+ test("carimba reviewed e foundAtSha nos findings do ciclo", () => {
278
+ const project = makeProject({ "src/f.ts": DUPLICATED });
279
+ try {
280
+ const state = buildStateFromFindings(
281
+ [merged({ file: "src/f.ts", rule: "r", line: 2 })],
282
+ {
283
+ branch: "main",
284
+ readFile: fileReader(project.cwd),
285
+ headSha: "abc1234",
286
+ reviewed: { headSha: "abc1234", at: "2026-01-01T00:00:00.000Z" },
287
+ },
288
+ );
289
+ expect(state.reviewed?.headSha).toBe("abc1234");
290
+ const only = Object.values(state.findings)[0];
291
+ expect(only.foundAtSha).toBe("abc1234");
292
+ } finally {
293
+ project.cleanup();
294
+ }
295
+ });
296
+
297
+ test("carry-over preserva o foundAtSha original", () => {
298
+ const project = makeProject({ "src/f.ts": DUPLICATED });
299
+ try {
300
+ const state = buildStateFromFindings(
301
+ [
302
+ // Novo/re-reportado: recebe o head atual.
303
+ merged({ file: "src/f.ts", rule: "r", line: 2 }),
304
+ // Carry-over: carrega o `foundAtSha` de quando foi observado.
305
+ {
306
+ ...merged({ file: "src/f.ts", rule: "r2", line: 5 }),
307
+ foundAtSha: "old0001",
308
+ },
309
+ ],
310
+ {
311
+ branch: "main",
312
+ readFile: fileReader(project.cwd),
313
+ headSha: "new0002",
314
+ reviewed: { headSha: "new0002", at: "2026-01-02T00:00:00.000Z" },
315
+ },
316
+ );
317
+ const byRule = (rule: string): string | undefined =>
318
+ Object.values(state.findings).find((f) => f.rule === rule)?.foundAtSha;
319
+ expect(byRule("r")).toBe("new0002");
320
+ expect(byRule("r2")).toBe("old0001");
321
+ } finally {
322
+ project.cleanup();
323
+ }
324
+ });
325
+ });