@payconductor/houndsman 0.14.0 → 0.16.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/commands/golden-command.ts +92 -0
- package/src/cli/commands/hunt-command.ts +85 -1
- package/src/cli/help.ts +3 -0
- package/src/cli/index.ts +2 -0
- package/src/core/brief.ts +35 -7
- package/src/core/commands/compare.ts +33 -0
- package/src/core/commands/golden.ts +176 -0
- package/src/core/commands/scaffold.ts +2 -0
- package/src/core/compare.ts +67 -0
- package/src/core/config.ts +10 -0
- package/src/core/engine/finalize-pipeline.ts +25 -1
- package/src/core/findings-state.ts +9 -1
- package/src/core/golden.ts +346 -0
- package/src/core/hunt/hunt-runner.ts +546 -49
- package/src/core/hunt/index.ts +38 -1
- package/src/core/hunt/maki-prompt.ts +289 -8
- package/src/core/hunt/maki-runner.ts +93 -16
- package/src/core/hunt/review-checks.ts +460 -0
- package/src/core/project-config.ts +10 -0
- package/src/core/registry-check.ts +30 -6
- package/src/core/report-builder.ts +9 -1
- package/src/core/severity.ts +245 -0
- package/src/core/storage/types.ts +1 -0
- package/src/index.ts +22 -0
- package/src/skill/OUTPUT-FORMAT.md +20 -1
- package/src/skill/README.md +1 -0
- package/src/skill/hunt.md +60 -0
- package/src/skill/train.md +3 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +24 -9
- package/src/tests/compare.test.ts +23 -0
- package/src/tests/findings-state.test.ts +82 -0
- package/src/tests/golden.test.ts +195 -0
- package/src/tests/hunt-maki.test.ts +652 -2
- package/src/tests/review-scripts.test.ts +19 -0
- package/src/tests/severity.test.ts +235 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden set — parse, casamento por âncora e relatório.
|
|
3
|
+
*
|
|
4
|
+
* O ponto do golden é trocar "quantos findings" por recall/precisão sobre
|
|
5
|
+
* defeitos confirmados; os testes cobrem a âncora (arquivo + faixa + trecho),
|
|
6
|
+
* a severidade mínima e o uso único de cada finding.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
commitMatches,
|
|
12
|
+
type GoldenFinding,
|
|
13
|
+
type GoldenSet,
|
|
14
|
+
goldenToJson,
|
|
15
|
+
parseGoldenSet,
|
|
16
|
+
renderGoldenReport,
|
|
17
|
+
scoreGolden,
|
|
18
|
+
} from "../core/golden";
|
|
19
|
+
|
|
20
|
+
function finding(over: Partial<GoldenFinding> = {}): GoldenFinding {
|
|
21
|
+
return {
|
|
22
|
+
fingerprint: "fp",
|
|
23
|
+
file: "src/a.ts",
|
|
24
|
+
line: 100,
|
|
25
|
+
rule: "Domínio: x",
|
|
26
|
+
severity: "attention",
|
|
27
|
+
message: "mensagem",
|
|
28
|
+
watchdogs: ["general"],
|
|
29
|
+
...over,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const set: GoldenSet = {
|
|
34
|
+
version: 1,
|
|
35
|
+
title: "PR",
|
|
36
|
+
commit: "56b5213",
|
|
37
|
+
expected: [
|
|
38
|
+
{ id: "seller-key", file: "src/a.ts", line: 116, match: "sellerApiKey" },
|
|
39
|
+
{ id: "split", file: "src/b.ts", line: 706, severity: "attention" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("golden — parse", () => {
|
|
44
|
+
test("aceita o objeto com `expected` e normaliza caminhos", () => {
|
|
45
|
+
const parsed = parseGoldenSet({
|
|
46
|
+
title: "PR",
|
|
47
|
+
commit: "abc",
|
|
48
|
+
expected: [{ id: "x", file: ".\\src\\a.ts", line: 3 }],
|
|
49
|
+
});
|
|
50
|
+
expect(parsed.set?.title).toBe("PR");
|
|
51
|
+
expect(parsed.set?.expected[0].file).toBe("src/a.ts");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("aceita lista direta e ignora itens sem id/file com aviso", () => {
|
|
55
|
+
const parsed = parseGoldenSet([
|
|
56
|
+
{ id: "ok", file: "a.ts" },
|
|
57
|
+
{ id: "sem-file" },
|
|
58
|
+
"lixo",
|
|
59
|
+
]);
|
|
60
|
+
expect(parsed.set?.expected).toHaveLength(1);
|
|
61
|
+
expect(parsed.warnings).toHaveLength(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("sem expectativas válidas não há set", () => {
|
|
65
|
+
expect(parseGoldenSet({ expected: [] }).set).toBeNull();
|
|
66
|
+
expect(parseGoldenSet(null).set).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("severidade inválida é descartada (não vira filtro)", () => {
|
|
70
|
+
const parsed = parseGoldenSet({
|
|
71
|
+
expected: [{ id: "x", file: "a.ts", severity: "urgente" }],
|
|
72
|
+
});
|
|
73
|
+
expect(parsed.set?.expected[0].severity).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("golden — score", () => {
|
|
78
|
+
test("casa por arquivo + tolerância + trecho", () => {
|
|
79
|
+
const score = scoreGolden(set, [
|
|
80
|
+
finding({ file: "src/a.ts", line: 130, message: "sellerApiKey vaza" }),
|
|
81
|
+
]);
|
|
82
|
+
expect(score.matched.map((item) => item.expectation.id)).toEqual(["seller-key"]);
|
|
83
|
+
expect(score.missed.map((item) => item.id)).toEqual(["split"]);
|
|
84
|
+
expect(score.recall).toBe(0.5);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("fora da tolerância não casa", () => {
|
|
88
|
+
const score = scoreGolden(set, [
|
|
89
|
+
finding({ file: "src/a.ts", line: 400, message: "sellerApiKey vaza" }),
|
|
90
|
+
]);
|
|
91
|
+
expect(score.matched).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("exige severidade mínima", () => {
|
|
95
|
+
const score = scoreGolden(set, [
|
|
96
|
+
finding({ file: "src/b.ts", line: 710, severity: "nit" }),
|
|
97
|
+
]);
|
|
98
|
+
expect(score.missed.map((item) => item.id)).toEqual(["seller-key", "split"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("um finding não comprova dois defeitos", () => {
|
|
102
|
+
const duplicated: GoldenSet = {
|
|
103
|
+
version: 1,
|
|
104
|
+
expected: [
|
|
105
|
+
{ id: "a", file: "src/a.ts", line: 10 },
|
|
106
|
+
{ id: "b", file: "src/a.ts", line: 12 },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
const score = scoreGolden(duplicated, [finding({ file: "src/a.ts", line: 11 })]);
|
|
110
|
+
expect(score.matched).toHaveLength(1);
|
|
111
|
+
expect(score.missed.map((item) => item.id)).toEqual(["b"]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("escolhe o candidato mais próximo da âncora", () => {
|
|
115
|
+
const score = scoreGolden(set, [
|
|
116
|
+
finding({ file: "src/a.ts", line: 145, message: "sellerApiKey vaza" }),
|
|
117
|
+
finding({ file: "src/a.ts", line: 117, message: "sellerApiKey vaza" }),
|
|
118
|
+
]);
|
|
119
|
+
expect(score.matched[0].finding.line).toBe(117);
|
|
120
|
+
expect(score.unexpected).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("precisão conta o que sobrou e colapsa âncoras repetidas", () => {
|
|
124
|
+
const score = scoreGolden(set, [
|
|
125
|
+
finding({ file: "src/a.ts", line: 116, message: "sellerApiKey" }),
|
|
126
|
+
finding({ file: "src/a.ts", line: 116, message: "outra regra" }),
|
|
127
|
+
finding({ file: "src/z.ts", line: 9, message: "ruído" }),
|
|
128
|
+
]);
|
|
129
|
+
expect(score.issues).toBe(2);
|
|
130
|
+
expect(score.matched).toHaveLength(1);
|
|
131
|
+
expect(score.unexpected).toHaveLength(2);
|
|
132
|
+
expect(score.precision).toBeCloseTo(1 / 3);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("sem findings, recall e precisão são zero", () => {
|
|
136
|
+
const score = scoreGolden(set, []);
|
|
137
|
+
expect(score.recall).toBe(0);
|
|
138
|
+
expect(score.precision).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("golden — relatório", () => {
|
|
143
|
+
test("tabela por run, faltantes e fora do golden", () => {
|
|
144
|
+
const runs = [
|
|
145
|
+
{
|
|
146
|
+
slug: "run-a",
|
|
147
|
+
label: "otimizado-3",
|
|
148
|
+
scope: "branch",
|
|
149
|
+
findings: 1,
|
|
150
|
+
score: scoreGolden(set, [
|
|
151
|
+
finding({ file: "src/a.ts", line: 116, message: "sellerApiKey" }),
|
|
152
|
+
finding({ file: "src/z.ts", line: 1, message: "ruído" }),
|
|
153
|
+
]),
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const markdown = renderGoldenReport(set, runs);
|
|
157
|
+
expect(markdown).toContain("| Run | Escopo |");
|
|
158
|
+
expect(markdown).toContain("50% (1/2)");
|
|
159
|
+
expect(markdown).toContain("`split` · `src/b.ts:706`");
|
|
160
|
+
expect(markdown).toContain("Fora do golden (1)");
|
|
161
|
+
expect(markdown).toContain("**commit de referência:** `56b5213`");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("JSON é estável e inclui set + runs", () => {
|
|
165
|
+
const json = JSON.parse(goldenToJson(set, [])) as {
|
|
166
|
+
set: GoldenSet;
|
|
167
|
+
runs: unknown[];
|
|
168
|
+
};
|
|
169
|
+
expect(json.set.expected).toHaveLength(2);
|
|
170
|
+
expect(json.runs).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("commit divergente vira aviso, não erro", () => {
|
|
174
|
+
expect(commitMatches("56b52138be18b49a", "56b52138be18b49a0000")).toBe(true);
|
|
175
|
+
expect(commitMatches("56b52138", "9ff00000")).toBe(false);
|
|
176
|
+
// Sem referência de um dos lados não há divergência afirmável.
|
|
177
|
+
expect(commitMatches(undefined, "9ff00000")).toBe(true);
|
|
178
|
+
expect(commitMatches("56b5213", undefined)).toBe(true);
|
|
179
|
+
|
|
180
|
+
const markdown = renderGoldenReport(set, [
|
|
181
|
+
{
|
|
182
|
+
slug: "run-velha",
|
|
183
|
+
label: "otimizado-3",
|
|
184
|
+
scope: "branch",
|
|
185
|
+
commit: "9ff0000aaaa",
|
|
186
|
+
commitMismatch: true,
|
|
187
|
+
findings: 0,
|
|
188
|
+
score: scoreGolden(set, []),
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
expect(markdown).toContain("Commit divergente em 1 run(s)");
|
|
192
|
+
expect(markdown).toContain("otimizado-3@9ff0000");
|
|
193
|
+
expect(markdown).toContain("revalide o golden");
|
|
194
|
+
});
|
|
195
|
+
});
|