@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.
- package/README.md +32 -14
- package/package.json +1 -1
- package/src/cli/index.ts +22 -11
- package/src/core/assets.ts +3 -0
- package/src/core/brief.ts +2 -3
- package/src/core/commands/findings.ts +232 -4
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +13 -44
- package/src/core/diff-source.ts +4 -4
- package/src/core/findings-state.ts +20 -2
- package/src/core/fix-batch.ts +236 -0
- package/src/core/git-revision.ts +105 -0
- package/src/core/jev/index.ts +1 -0
- package/src/core/jev/jev-detector.ts +2 -2
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +202 -15
- package/src/core/paths.ts +6 -0
- package/src/core/report-builder.ts +5 -0
- package/src/core/run-status.ts +123 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +1 -1
- package/src/server/artifacts.ts +1 -0
- package/src/server/client/RunList.tsx +22 -13
- package/src/server/client/constants.ts +32 -1
- package/src/server/client/types.ts +4 -0
- package/src/server/index.ts +12 -1
- package/src/skill/AUDIT-PROTOCOL.md +2 -2
- package/src/skill/README.md +29 -5
- package/src/skill/SETUP.md +11 -1
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +41 -68
- package/src/skill/{houndsman-update.md → train.md} +11 -11
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-commands.test.ts +252 -2
- package/src/tests/findings-state.test.ts +52 -1
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +30 -0
- package/src/tests/verify.test.ts +170 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# houndsman — {{projectName}} (
|
|
1
|
+
# houndsman — {{projectName}} · `hunt` (revisão de PR)
|
|
2
2
|
|
|
3
|
-
> **Documento canônico do skill
|
|
4
|
-
> (`.agents/skills/houndsman
|
|
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`**;
|
|
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
|
-
**
|
|
10
|
-
|
|
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
|
|
163
|
+
`fingerprint` de conteúdo por finding, preservados entre execuções
|
|
155
164
|
|
|
156
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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) →
|
|
298
|
-
>
|
|
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
|
|
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
|
-
#
|
|
1
|
+
# houndsman — {{projectName}} · `train` (atualização de regras)
|
|
2
2
|
|
|
3
|
-
> **Documento canônico do skill
|
|
4
|
-
> (`.agents/skills/houndsman
|
|
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
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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/
|
|
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 {
|
|
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(
|
|
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
|
+
});
|