@payconductor/houndsman 0.1.3 → 0.2.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.
- package/package.json +1 -1
- package/src/cli/index.ts +31 -21
- package/src/core/commands/findings.ts +137 -11
- package/src/core/findings-state.ts +19 -0
- package/src/core/github-review.ts +452 -0
- package/src/core/registry-check.ts +53 -0
- package/src/server/client/App.tsx +174 -49
- package/src/server/client/ArtifactsPanel.tsx +2 -2
- package/src/server/client/AutoRefresh.tsx +38 -0
- package/src/server/client/Filters.tsx +72 -53
- package/src/server/client/FindingCard.tsx +212 -71
- package/src/server/client/FindingsList.tsx +8 -6
- package/src/server/client/RunList.tsx +30 -7
- package/src/server/client/SubmitModal.tsx +418 -0
- package/src/server/client/SummaryBar.tsx +65 -12
- package/src/server/client/constants.ts +9 -12
- package/src/server/client/format.ts +40 -0
- package/src/server/client/icons.tsx +143 -0
- package/src/server/client/types.ts +69 -0
- package/src/server/index.ts +352 -88
- package/src/skill/AUDIT-PROTOCOL.md +5 -0
- package/src/skill/README.md +3 -1
- package/src/skill/SETUP.md +0 -1
- package/src/skill/hunt.md +2 -1
- package/src/skill/train.md +29 -8
- package/src/tests/__snapshots__/baseline.test.ts.snap +0 -0
- package/src/tests/baseline.test.ts +493 -0
- package/src/tests/cli.test.ts +167 -0
- package/src/tests/github-review.test.ts +320 -0
- package/src/tests/review-scripts.test.ts +36 -0
- package/src/tests/server.test.ts +117 -0
package/src/skill/train.md
CHANGED
|
@@ -72,10 +72,29 @@ Toda regra (nova ou alterada) deve passar por **todos** os itens:
|
|
|
72
72
|
direta da afirmação). Aí o prefixo lidera o sentido dominante e o outro entra como oração
|
|
73
73
|
subordinada — nunca como segundo juízo coordenado.
|
|
74
74
|
9. **Escopo coerente** — a regra pertence ao domínio do watchdog.
|
|
75
|
-
10. **Sem duplicata** — nenhuma regra repetida
|
|
75
|
+
10. **Sem duplicata** — nenhuma regra repetida (texto normalizado) no mesmo watchdog.
|
|
76
|
+
11. **Lugar único** — a regra vive **apenas** em `rules`. Nunca repita o texto (nem o anti-padrão)
|
|
77
|
+
em `reviewFocus`, `contextFiles`, `scope`, `description` ou `exclude`.
|
|
76
78
|
|
|
77
79
|
> O `houndsman doctor` emite um **aviso** (não bloqueante) para regra sem polaridade explícita
|
|
78
|
-
> (`Promover:`, `Evitar:`, `Não` ou `Nunca`)
|
|
80
|
+
> (`Promover:`, `Evitar:`, `Não` ou `Nunca`), para regra duplicada e para texto de regra que
|
|
81
|
+
> vazou para `reviewFocus`/`contextFiles`/`exclude`.
|
|
82
|
+
|
|
83
|
+
### Onde cada coisa vive (não duplique a regra)
|
|
84
|
+
|
|
85
|
+
Cada informação tem o seu campo — não espelhe a regra em vários lugares:
|
|
86
|
+
|
|
87
|
+
| O que você quer expressar | Campo | Observação |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| A **regra** (o que DEVE/NÃO DEVE acontecer) | `rules[]` | Único lugar da regra: `{ severity, text, jev? }`. |
|
|
90
|
+
| **Quais arquivos** o watchdog cobre | `classify` | Regex de path (ou de conteúdo). |
|
|
91
|
+
| **Que arquivos** olhar primeiro | `reviewFocus` | **Paths/áreas** do código — não texto de regra. |
|
|
92
|
+
| **Que docs** consultar | `contextFiles` | Caminhos de **documentação** — não regra nem código. |
|
|
93
|
+
| O que **nunca** carregar | `exclude` | Paths a ignorar. |
|
|
94
|
+
| O **território** em prosa | `scope` / `description` | Contexto do domínio, não regra. |
|
|
95
|
+
|
|
96
|
+
> A regra aparece **uma única vez** no arquivo — em `rules`. Repeti-la em
|
|
97
|
+
> `reviewFocus`/`contextFiles`/`scope`/`description`/`exclude` só cria ruído no brief do subagente.
|
|
79
98
|
|
|
80
99
|
---
|
|
81
100
|
|
|
@@ -166,7 +185,9 @@ verificáveis por leitura local. Candidatos não confirmados pelo watchdog **nã
|
|
|
166
185
|
`Evitar:`/`Não`/`Nunca` (NÃO DEVE), conforme as Convenções de escrita.
|
|
167
186
|
- Regra que mistura afirmação e negação (dois sentidos) → dividir em **duas entradas** (*Unipolar*, item 8).
|
|
168
187
|
- `jev` (opcional): `{ kind: "noul" }`, com `instructions` e/ou `criteria` — ver seção acima.
|
|
169
|
-
-
|
|
188
|
+
- **A regra vai só em `rules`** — não a repita em `reviewFocus`/`contextFiles`/`scope`/
|
|
189
|
+
`description`/`exclude`. Só toque nesses campos se o **domínio/paths** mudou (ver
|
|
190
|
+
*Onde cada coisa vive*), nunca para reafirmar a regra.
|
|
170
191
|
- Se a regra muda para outro domínio (paths), ajustar o `classify` do watchdog.
|
|
171
192
|
5. **Watchdog novo?** criar `.houndsman/watchdogs/<id>.ts` e registrar em `.houndsman/watchdogs/index.ts` (o id é derivado das chaves).
|
|
172
193
|
6. **Atualizar `src/skill/hunt.md`/`README.md`** se escopo/regra visível mudou (no motor).
|
|
@@ -226,16 +247,14 @@ todos os itens — em especial: **geral/reutilizável** (não regra de negócio
|
|
|
226
247
|
|
|
227
248
|
```bash
|
|
228
249
|
bun run houndsman doctor # config sem drift + ambiente + credencial Jev
|
|
229
|
-
bun run houndsman test # suíte da lib (project-agnóstica)
|
|
230
|
-
bun run ci:fix # formatação/imports/lint (no repo da lib)
|
|
231
|
-
bun run ci # tipagem (no repo da lib)
|
|
232
250
|
```
|
|
233
251
|
|
|
252
|
+
> `bun run houndsman test`, `bun run ci:fix` e `bun run ci` são comandos para **manter a lib** do
|
|
253
|
+
> houndsman (repo do pacote) — não para quem só usa o houndsman no projeto.
|
|
254
|
+
|
|
234
255
|
Checklist final:
|
|
235
256
|
|
|
236
257
|
- [ ] `bun run houndsman doctor` → sem problemas reportados.
|
|
237
|
-
- [ ] `bun run houndsman test` → **0 fail**.
|
|
238
|
-
- [ ] `ci:fix` + `ci` → exit 0 (warnings pré-existentes não contam).
|
|
239
258
|
- [ ] `WATCHDOG_DEFINITIONS[<id>].id` bate com a chave e `description` não vazio.
|
|
240
259
|
- [ ] `src/skill/hunt.md` continua consistente com os watchdogs do projeto.
|
|
241
260
|
- [ ] Toda regra nova/alterada tem **evidência** registrada no PR/relatório
|
|
@@ -244,6 +263,8 @@ Checklist final:
|
|
|
244
263
|
- [ ] Brief de amostra não contém regra removida (dry-run abaixo).
|
|
245
264
|
- [ ] Toda regra passa nas convenções de escrita (geral/reutilizável, sem jargão abreviado,
|
|
246
265
|
sem qualificador vago, com polaridade explícita e **unipolar**) — conferir via grep.
|
|
266
|
+
- [ ] O texto de cada regra aparece **exatamente uma vez** no arquivo (só em `rules`) — nada em
|
|
267
|
+
`reviewFocus`/`contextFiles`/`scope`/`description`/`exclude`.
|
|
247
268
|
|
|
248
269
|
```bash
|
|
249
270
|
# Dry-run: gerar briefs e conferir ausência de regras removidas
|
|
File without changes
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline de comportamento (Fase 0 do refactor OO).
|
|
3
|
+
*
|
|
4
|
+
* Congela a saída observável do motor — briefs, relatórios, camada gerada e
|
|
5
|
+
* artefatos de `prepare`/`finalize` — para provar que o refactor não muda
|
|
6
|
+
* comportamento. Qualquer divergência nesses snapshots é uma regressão.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import {
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
statSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join, relative } from "node:path";
|
|
22
|
+
import { renderBrief } from "../core/brief";
|
|
23
|
+
import {
|
|
24
|
+
createScaffoldContext,
|
|
25
|
+
syncGeneratedFiles,
|
|
26
|
+
writeUserLayer,
|
|
27
|
+
} from "../core/commands/scaffold";
|
|
28
|
+
import { defineHoundsmanConfig } from "../core/config";
|
|
29
|
+
import { defaultOptions, finalize, prepare } from "../core/orchestrator";
|
|
30
|
+
import {
|
|
31
|
+
type MergedFinding,
|
|
32
|
+
renderDomainReport,
|
|
33
|
+
renderReport,
|
|
34
|
+
type WatchdogReport,
|
|
35
|
+
} from "../core/report-builder";
|
|
36
|
+
import { defineWatchdog } from "../core/types";
|
|
37
|
+
import * as facade from "../index";
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Config baseline (projeto fake, 1 watchdog para artefatos determinísticos)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const baselineWatchdog = defineWatchdog({
|
|
44
|
+
id: "alpha",
|
|
45
|
+
name: "Alpha",
|
|
46
|
+
description: "Watchdog baseline.",
|
|
47
|
+
scope: "src/alpha/**",
|
|
48
|
+
classify: [{ value: "^src/alpha/" }],
|
|
49
|
+
rules: [
|
|
50
|
+
{ severity: "blocking", text: "Nunca usar `any`." },
|
|
51
|
+
{ severity: "attention", text: "Promover: validar a entrada." },
|
|
52
|
+
],
|
|
53
|
+
contextFiles: ["README.md"],
|
|
54
|
+
breakingChange: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const baselineConfig = defineHoundsmanConfig({
|
|
58
|
+
projectName: "Baseline",
|
|
59
|
+
artifactDir: ".houndsman/.runs",
|
|
60
|
+
watchdogs: { alpha: baselineWatchdog },
|
|
61
|
+
exclusions: [/^dist\//],
|
|
62
|
+
docMappings: [[/^src\//, ["README.md"]]],
|
|
63
|
+
alwaysIncludeDocs: ["README.md"],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** Substitui valores voláteis (ids, timestamps, SHAs) por marcadores estáveis. */
|
|
71
|
+
function normalizeArtifacts(text: string): string {
|
|
72
|
+
return text
|
|
73
|
+
.replace(
|
|
74
|
+
/"(createdAt|updatedAt|generatedAt|startedAt|at)": "[^"]*"/g,
|
|
75
|
+
'"$1": "<timestamp>"',
|
|
76
|
+
)
|
|
77
|
+
.replace(/"\d{4}-\d{2}-\d{2}T[^"]*"/g, '"<timestamp>"')
|
|
78
|
+
.replace(/\b[0-9a-f]{40}\b/g, "<sha>")
|
|
79
|
+
.replace(/\b[0-9a-f]{16}\b/g, "<fingerprint>")
|
|
80
|
+
.replace(/\b[0-9a-f]{10}\b/g, "<id>")
|
|
81
|
+
.replace(/\d{4}-\d{2}-\d{2}/g, "<date>");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Lista recursiva de arquivos (caminhos relativos, ordenados). */
|
|
85
|
+
function listFiles(root: string): string[] {
|
|
86
|
+
const files: string[] = [];
|
|
87
|
+
const walk = (dir: string): void => {
|
|
88
|
+
for (const name of readdirSync(dir)) {
|
|
89
|
+
const full = join(dir, name);
|
|
90
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
91
|
+
else files.push(relative(root, full).replace(/\\/g, "/"));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
walk(root);
|
|
95
|
+
return files.sort();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Executa git no diretório e devolve stdout (lança em falha). */
|
|
99
|
+
function git(cwd: string, args: string[]): string {
|
|
100
|
+
const result = Bun.spawnSync(["git", ...args], {
|
|
101
|
+
cwd,
|
|
102
|
+
stdout: "pipe",
|
|
103
|
+
stderr: "pipe",
|
|
104
|
+
});
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
throw new Error(`git ${args.join(" ")}: ${result.stderr.toString()}`);
|
|
107
|
+
}
|
|
108
|
+
return result.stdout.toString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Captura o stdout de `fn` (o baseline inclui a saída textual do CLI). */
|
|
112
|
+
async function captureStdout(fn: () => Promise<void>): Promise<string> {
|
|
113
|
+
const original = console.log;
|
|
114
|
+
const lines: string[] = [];
|
|
115
|
+
console.log = (...args: unknown[]) => {
|
|
116
|
+
lines.push(args.map(String).join(" "));
|
|
117
|
+
};
|
|
118
|
+
try {
|
|
119
|
+
await fn();
|
|
120
|
+
} finally {
|
|
121
|
+
console.log = original;
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Brief
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe("baseline — brief", () => {
|
|
131
|
+
test("defaultRenderBrief (contexto, churn, overflow, candidatos e deletados)", () => {
|
|
132
|
+
const brief = renderBrief(baselineWatchdog, {
|
|
133
|
+
watchdogId: "alpha",
|
|
134
|
+
files: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
135
|
+
statuses: {
|
|
136
|
+
"src/alpha/a.ts": { status: "modified" },
|
|
137
|
+
"src/alpha/b.ts": { status: "added" },
|
|
138
|
+
},
|
|
139
|
+
churn: {
|
|
140
|
+
"src/alpha/a.ts": { additions: 3, deletions: 1 },
|
|
141
|
+
"src/alpha/b.ts": { additions: 2, deletions: 0 },
|
|
142
|
+
},
|
|
143
|
+
overflow: ["src/alpha/c.ts"],
|
|
144
|
+
deleted: ["src/alpha/old.ts"],
|
|
145
|
+
branch: "feature/x",
|
|
146
|
+
outputPath: ".houndsman/.runs/feature-x/findings/alpha.json",
|
|
147
|
+
outputFormatPath: ".houndsman/core/skill/OUTPUT-FORMAT.md",
|
|
148
|
+
docs: ["AGENTS.md", "README.md"],
|
|
149
|
+
context: {
|
|
150
|
+
objective: "Entregar X",
|
|
151
|
+
motivation: "Reduzir Y",
|
|
152
|
+
changeSummary: ["Faz A", "Remove B"],
|
|
153
|
+
},
|
|
154
|
+
candidates: [
|
|
155
|
+
{
|
|
156
|
+
file: "src/alpha/a.ts",
|
|
157
|
+
severity: "blocking",
|
|
158
|
+
confidence: 0.82,
|
|
159
|
+
rule: "Nunca usar `any`.",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(brief).toMatchSnapshot();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Relatório
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const reports: WatchdogReport[] = [
|
|
173
|
+
{
|
|
174
|
+
watchdogId: "alpha",
|
|
175
|
+
reviewedFiles: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
176
|
+
skipped: ["src/alpha/c.ts"],
|
|
177
|
+
findings: [
|
|
178
|
+
{
|
|
179
|
+
rule: "Nunca usar `any`.",
|
|
180
|
+
severity: "blocking",
|
|
181
|
+
file: "src/alpha/a.ts",
|
|
182
|
+
line: 1,
|
|
183
|
+
message: "any detectado",
|
|
184
|
+
action: "tipar",
|
|
185
|
+
evidence: "export const a: any = 1;",
|
|
186
|
+
id: "id-alpha",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
rule: "Promover: validar a entrada.",
|
|
190
|
+
severity: "attention",
|
|
191
|
+
file: "src/alpha/b.ts",
|
|
192
|
+
line: 2,
|
|
193
|
+
message: "sem validação",
|
|
194
|
+
action: "adicionar schema",
|
|
195
|
+
id: "id-beta",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
watchdogId: "beta",
|
|
201
|
+
reviewedFiles: ["src/beta/x.test.ts"],
|
|
202
|
+
skipped: [],
|
|
203
|
+
findings: [],
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const merged: MergedFinding[] = [
|
|
208
|
+
{ ...reports[0].findings[0], watchdogs: ["alpha"] },
|
|
209
|
+
{ ...reports[0].findings[1], watchdogs: ["alpha", "beta"] },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const reportContext = {
|
|
213
|
+
available: true,
|
|
214
|
+
source: {
|
|
215
|
+
number: 42,
|
|
216
|
+
title: "Entrega X",
|
|
217
|
+
url: "https://example.test/pull/42",
|
|
218
|
+
head: "feature/x",
|
|
219
|
+
base: "main",
|
|
220
|
+
},
|
|
221
|
+
objective: "Entregar X",
|
|
222
|
+
motivation: "Reduzir Y",
|
|
223
|
+
changeSummary: ["Faz A", "Remove B"],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
describe("baseline — relatório", () => {
|
|
227
|
+
test("renderReport consolidado", () => {
|
|
228
|
+
const md = renderReport(
|
|
229
|
+
"feature/x",
|
|
230
|
+
reports,
|
|
231
|
+
merged,
|
|
232
|
+
{ classified: { alpha: ["src/alpha/a.ts", "src/alpha/b.ts"], beta: [] } },
|
|
233
|
+
reportContext,
|
|
234
|
+
[
|
|
235
|
+
{
|
|
236
|
+
watchdogId: "alpha",
|
|
237
|
+
rule: "Nunca usar `any`.",
|
|
238
|
+
severity: "blocking",
|
|
239
|
+
file: "src/alpha/a.ts",
|
|
240
|
+
confidence: 0.82,
|
|
241
|
+
questionKey: "alpha::rule-0",
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
(id) => (id === "alpha" ? "Alpha" : "Beta"),
|
|
245
|
+
[
|
|
246
|
+
{
|
|
247
|
+
id: "alpha",
|
|
248
|
+
name: "Alpha",
|
|
249
|
+
file: "review.alpha.md",
|
|
250
|
+
findings: 2,
|
|
251
|
+
reviewedFiles: 2,
|
|
252
|
+
expectedFiles: 2,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(md).toMatchSnapshot();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("renderDomainReport isolado", () => {
|
|
261
|
+
const md = renderDomainReport(
|
|
262
|
+
"feature/x",
|
|
263
|
+
reports[0],
|
|
264
|
+
merged,
|
|
265
|
+
2,
|
|
266
|
+
reportContext,
|
|
267
|
+
(id) => (id === "alpha" ? "Alpha" : "Beta"),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(md).toMatchSnapshot();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Camada gerada pelo setup/update
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/** Arquivos-template cujo conteúdo precisa permanecer idêntico. */
|
|
279
|
+
const TEMPLATE_FILES = [
|
|
280
|
+
"houndsman.config.ts",
|
|
281
|
+
".houndsman/core/types.ts",
|
|
282
|
+
".houndsman/watchdogs/example.ts",
|
|
283
|
+
".houndsman/watchdogs/index.ts",
|
|
284
|
+
".houndsman/exclusions.ts",
|
|
285
|
+
".houndsman/doc-mappings.ts",
|
|
286
|
+
".houndsman/.gitignore",
|
|
287
|
+
".agents/skills/houndsman/SKILL.md",
|
|
288
|
+
".claude/skills/houndsman/SKILL.md",
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
describe("baseline — camada gerada", () => {
|
|
292
|
+
test("setup gera os mesmos arquivos e conteúdos", () => {
|
|
293
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-baseline-scaffold-"));
|
|
294
|
+
try {
|
|
295
|
+
const ctx = createScaffoldContext(cwd, ["--name", "acme"], false);
|
|
296
|
+
writeUserLayer(ctx);
|
|
297
|
+
syncGeneratedFiles(ctx);
|
|
298
|
+
|
|
299
|
+
const files = listFiles(cwd);
|
|
300
|
+
expect(files).toMatchSnapshot();
|
|
301
|
+
|
|
302
|
+
const contents = Object.fromEntries(
|
|
303
|
+
TEMPLATE_FILES.map((file) => [file, readFileSync(join(cwd, file), "utf8")]),
|
|
304
|
+
);
|
|
305
|
+
expect(contents).toMatchSnapshot();
|
|
306
|
+
|
|
307
|
+
// Assets copiados (skill docs): hash estável, sem colar o markdown inteiro.
|
|
308
|
+
const skillDocs = files
|
|
309
|
+
.filter((file) => file.startsWith(".houndsman/core/skill/"))
|
|
310
|
+
.map((file) => ({
|
|
311
|
+
file,
|
|
312
|
+
sha256: createHash("sha256")
|
|
313
|
+
.update(readFileSync(join(cwd, file), "utf8"))
|
|
314
|
+
.digest("hex")
|
|
315
|
+
.slice(0, 16),
|
|
316
|
+
}));
|
|
317
|
+
expect(skillDocs).toMatchSnapshot();
|
|
318
|
+
} finally {
|
|
319
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// prepare → findings → finalize (end-to-end em repo git temporário)
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
describe("baseline — artefatos de prepare/finalize", () => {
|
|
329
|
+
test("gera classification, brief, manifest, review e state", async () => {
|
|
330
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-baseline-run-"));
|
|
331
|
+
const originalCwd = process.cwd();
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
git(cwd, ["init", "-b", "main"]);
|
|
335
|
+
git(cwd, ["config", "user.email", "baseline@example.test"]);
|
|
336
|
+
git(cwd, ["config", "user.name", "Baseline"]);
|
|
337
|
+
mkdirSync(join(cwd, "src", "alpha"), { recursive: true });
|
|
338
|
+
writeFileSync(join(cwd, "src", "alpha", "a.ts"), "export const a = 1;\n");
|
|
339
|
+
writeFileSync(join(cwd, "src", "alpha", "old.ts"), "export const old = 1;\n");
|
|
340
|
+
writeFileSync(join(cwd, "README.md"), "# Baseline\n");
|
|
341
|
+
git(cwd, ["add", "."]);
|
|
342
|
+
git(cwd, ["commit", "-m", "initial"]);
|
|
343
|
+
const base = git(cwd, ["rev-parse", "HEAD"]).trim();
|
|
344
|
+
|
|
345
|
+
writeFileSync(
|
|
346
|
+
join(cwd, "src", "alpha", "a.ts"),
|
|
347
|
+
"export const a: any = 1;\nexport const b = 2;\n",
|
|
348
|
+
);
|
|
349
|
+
writeFileSync(join(cwd, "src", "alpha", "b.ts"), "export const b = 3;\n");
|
|
350
|
+
rmSync(join(cwd, "src", "alpha", "old.ts"));
|
|
351
|
+
git(cwd, ["add", "-A"]);
|
|
352
|
+
git(cwd, ["commit", "-m", "change"]);
|
|
353
|
+
|
|
354
|
+
const opts = {
|
|
355
|
+
...defaultOptions(".houndsman/.runs"),
|
|
356
|
+
base,
|
|
357
|
+
head: "HEAD",
|
|
358
|
+
noJev: true,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
process.chdir(cwd);
|
|
362
|
+
|
|
363
|
+
const prepareOutput = await captureStdout(async () => {
|
|
364
|
+
await prepare(opts, "main", baselineConfig);
|
|
365
|
+
});
|
|
366
|
+
expect(normalizeArtifacts(prepareOutput)).toMatchSnapshot();
|
|
367
|
+
|
|
368
|
+
const runDir = join(cwd, ".houndsman", ".runs", "main");
|
|
369
|
+
for (const file of ["classification.json", "manifest.json", "briefs/alpha.md"]) {
|
|
370
|
+
expect(
|
|
371
|
+
normalizeArtifacts(readFileSync(join(runDir, file), "utf8")),
|
|
372
|
+
).toMatchSnapshot(file);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
writeFileSync(
|
|
376
|
+
join(runDir, "findings", "alpha.json"),
|
|
377
|
+
`${JSON.stringify(
|
|
378
|
+
{
|
|
379
|
+
watchdogId: "alpha",
|
|
380
|
+
reviewedFiles: ["src/alpha/a.ts", "src/alpha/b.ts"],
|
|
381
|
+
skipped: [],
|
|
382
|
+
findings: [
|
|
383
|
+
{
|
|
384
|
+
rule: "Nunca usar `any`.",
|
|
385
|
+
severity: "blocking",
|
|
386
|
+
file: "src/alpha/a.ts",
|
|
387
|
+
line: 1,
|
|
388
|
+
message: "any detectado",
|
|
389
|
+
action: "tipar",
|
|
390
|
+
evidence: "export const a: any = 1;",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
null,
|
|
395
|
+
2,
|
|
396
|
+
)}\n`,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const finalizeOutput = await captureStdout(async () => {
|
|
400
|
+
finalize(opts, "main", baselineConfig);
|
|
401
|
+
});
|
|
402
|
+
expect(normalizeArtifacts(finalizeOutput)).toMatchSnapshot();
|
|
403
|
+
|
|
404
|
+
for (const file of [
|
|
405
|
+
"review.md",
|
|
406
|
+
"review.alpha.md",
|
|
407
|
+
"state.json",
|
|
408
|
+
"run.json",
|
|
409
|
+
]) {
|
|
410
|
+
expect(
|
|
411
|
+
normalizeArtifacts(readFileSync(join(runDir, file), "utf8")),
|
|
412
|
+
).toMatchSnapshot(file);
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
process.chdir(originalCwd);
|
|
416
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Contrato da API pública (facade src/index.ts)
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
/** Símbolos que consumidores importam de `@payconductor/houndsman`. */
|
|
426
|
+
const REQUIRED_EXPORTS = [
|
|
427
|
+
"PROCEDURE",
|
|
428
|
+
"SEVERITY_EMOJI",
|
|
429
|
+
"SEVERITY_ORDER",
|
|
430
|
+
"FINDING_DECISIONS",
|
|
431
|
+
"FINDING_EXEC_STATES",
|
|
432
|
+
"FINDINGS_STATE_VERSION",
|
|
433
|
+
"FindingsStateStore",
|
|
434
|
+
"applyFindingUpdate",
|
|
435
|
+
"assetPath",
|
|
436
|
+
"buildAnchor",
|
|
437
|
+
"buildStateFromFindings",
|
|
438
|
+
"buildStateFromRunDir",
|
|
439
|
+
"checkRegistry",
|
|
440
|
+
"churnOf",
|
|
441
|
+
"classifyEntries",
|
|
442
|
+
"classifyRange",
|
|
443
|
+
"collectChurn",
|
|
444
|
+
"collectDiff",
|
|
445
|
+
"compareFindings",
|
|
446
|
+
"createRequestHandler",
|
|
447
|
+
"defaultRenderBrief",
|
|
448
|
+
"defineHoundsmanConfig",
|
|
449
|
+
"defineWatchdog",
|
|
450
|
+
"emptyState",
|
|
451
|
+
"fileReader",
|
|
452
|
+
"filterFindings",
|
|
453
|
+
"findWatchdog",
|
|
454
|
+
"fingerprintOf",
|
|
455
|
+
"getWatchdog",
|
|
456
|
+
"getWatchdogContext",
|
|
457
|
+
"hashKey",
|
|
458
|
+
"isExcludedPath",
|
|
459
|
+
"loadConfig",
|
|
460
|
+
"locateAnchor",
|
|
461
|
+
"matchWatchdogs",
|
|
462
|
+
"newFindingId",
|
|
463
|
+
"normalizeLine",
|
|
464
|
+
"parseDiffIndex",
|
|
465
|
+
"parseEntries",
|
|
466
|
+
"parseNumstat",
|
|
467
|
+
"reanchor",
|
|
468
|
+
"relFromRoot",
|
|
469
|
+
"renderBrief",
|
|
470
|
+
"renderDomainReport",
|
|
471
|
+
"reorder",
|
|
472
|
+
"resolveConfigPath",
|
|
473
|
+
"resolveJevConcurrency",
|
|
474
|
+
"resolveMaxFilesPerBrief",
|
|
475
|
+
"runDir",
|
|
476
|
+
"runFindingsCommand",
|
|
477
|
+
"runVerifyCommand",
|
|
478
|
+
"selectByBudget",
|
|
479
|
+
"severityRank",
|
|
480
|
+
"sortByRisk",
|
|
481
|
+
"startServer",
|
|
482
|
+
"stateFile",
|
|
483
|
+
"summarize",
|
|
484
|
+
"syncState",
|
|
485
|
+
] as const;
|
|
486
|
+
|
|
487
|
+
describe("baseline — API pública", () => {
|
|
488
|
+
test("a facade mantém todos os símbolos exportados", () => {
|
|
489
|
+
for (const name of REQUIRED_EXPORTS) {
|
|
490
|
+
expect(facade).toHaveProperty(name);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
});
|