@payconductor/houndsman 0.1.0 → 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 +68 -89
- package/assets/logo-legacy.png +0 -0
- package/assets/logo.png +0 -0
- package/package.json +3 -2
- package/src/cli/index.ts +101 -10
- package/src/core/assets.ts +11 -0
- package/src/core/brief.ts +27 -5
- package/src/core/commands/findings.ts +618 -0
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +33 -44
- package/src/core/config.ts +42 -0
- package/src/core/diff-source.ts +140 -0
- package/src/core/file-classifier.ts +50 -2
- package/src/core/finding-anchor.ts +147 -0
- package/src/core/findings-state.ts +556 -0
- 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-client.ts +72 -11
- package/src/core/jev/jev-detector.ts +21 -19
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +362 -23
- package/src/core/paths.ts +9 -0
- package/src/core/registry-check.ts +15 -0
- package/src/core/report-builder.ts +118 -9
- package/src/core/run-status.ts +123 -0
- package/src/core/types.ts +10 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +78 -2
- package/src/server/artifacts.ts +67 -0
- package/src/server/client/App.tsx +158 -0
- package/src/server/client/ArtifactsPanel.tsx +48 -0
- package/src/server/client/Filters.tsx +83 -0
- package/src/server/client/FindingCard.tsx +122 -0
- package/src/server/client/FindingsList.tsx +54 -0
- package/src/server/client/RunList.tsx +52 -0
- package/src/server/client/SummaryBar.tsx +21 -0
- package/src/server/client/api.ts +37 -0
- package/src/server/client/constants.ts +94 -0
- package/src/server/client/index.tsx +12 -0
- package/src/server/client/types.ts +52 -0
- package/src/server/client-build.ts +47 -0
- package/src/server/index.ts +376 -0
- package/src/skill/AUDIT-PROTOCOL.md +2 -2
- package/src/skill/README.md +43 -11
- package/src/skill/SETUP.md +16 -4
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +83 -33
- package/src/skill/{houndsman-update.md → train.md} +12 -12
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-anchor.test.ts +120 -0
- package/src/tests/findings-commands.test.ts +476 -0
- package/src/tests/findings-state.test.ts +325 -0
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/review-scripts.test.ts +299 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +242 -0
- package/src/tests/verify.test.ts +170 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirmação Jev dos findings ambíguos (tier 1 do `findings verify`).
|
|
3
|
+
*
|
|
4
|
+
* Por arquivo: monta um `state` pequeno (topo do arquivo + diff focado
|
|
5
|
+
* `foundAtSha..head`) e, numa única chamada, pergunta se cada violação ainda
|
|
6
|
+
* ocorre. **Falha-aberto** — sem chave/erro, nada é confirmado: o tier local
|
|
7
|
+
* decide. O custo é uma chamada por arquivo ambíguo, não uma revisão completa.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { diffPatch, readFileAt } from "../git-revision";
|
|
11
|
+
import type { JevClient } from "./jev-client";
|
|
12
|
+
import type { JevAnswer, JevQuestion } from "./jev-types";
|
|
13
|
+
|
|
14
|
+
/** Caso ambíguo a confirmar com o Jev. */
|
|
15
|
+
export type JevVerifyCase = {
|
|
16
|
+
id: string;
|
|
17
|
+
file: string;
|
|
18
|
+
rule: string;
|
|
19
|
+
/** Commit em que o achado foi observado (base do diff focado). */
|
|
20
|
+
foundAtSha?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type JevVerifyOptions = {
|
|
24
|
+
/** Commit atual (head) para o diff focado. */
|
|
25
|
+
headSha?: string;
|
|
26
|
+
/** Nº de arquivos avaliados em paralelo. */
|
|
27
|
+
concurrency?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type JevVerifyOutcome = {
|
|
31
|
+
/** id → confiança (0–1) de a violação ainda ocorrer. */
|
|
32
|
+
confidence: Map<string, number>;
|
|
33
|
+
checked: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
reason?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Linhas iniciais do arquivo incluídas no `state` (contexto de imports). */
|
|
39
|
+
const HEADER_LINES = 60;
|
|
40
|
+
const MAX_STATE_CHARS = 8_000;
|
|
41
|
+
const MAX_DIFF_CHARS = 4_000;
|
|
42
|
+
const DEFAULT_CONCURRENCY = 5;
|
|
43
|
+
|
|
44
|
+
/** Início do diff do grupo: o `foundAtSha` mais antigo informado. */
|
|
45
|
+
function referenceOf(
|
|
46
|
+
group: readonly JevVerifyCase[],
|
|
47
|
+
headSha: string | undefined,
|
|
48
|
+
): { from: string; to: string } | null {
|
|
49
|
+
const from = group.find((item) => item.foundAtSha)?.foundAtSha;
|
|
50
|
+
if (!from || !headSha || from === headSha) return null;
|
|
51
|
+
return { from, to: headSha };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Arquivo → casos daquele arquivo (uma chamada por arquivo). */
|
|
55
|
+
function groupByFile(cases: readonly JevVerifyCase[]): Map<string, JevVerifyCase[]> {
|
|
56
|
+
const map = new Map<string, JevVerifyCase[]>();
|
|
57
|
+
for (const item of cases) {
|
|
58
|
+
const list = map.get(item.file);
|
|
59
|
+
if (list) list.push(item);
|
|
60
|
+
else map.set(item.file, [item]);
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Executa `fn` sobre `items` com concorrência limitada (ordem irrelevante). */
|
|
66
|
+
async function mapWithConcurrency<T>(
|
|
67
|
+
items: readonly T[],
|
|
68
|
+
limit: number,
|
|
69
|
+
fn: (item: T) => Promise<void>,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
let cursor = 0;
|
|
72
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
73
|
+
while (cursor < items.length) {
|
|
74
|
+
const current = items[cursor++];
|
|
75
|
+
await fn(current);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
await Promise.all(workers);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Confirma com o Jev se as violações ambíguas ainda ocorrem. */
|
|
82
|
+
export class JevVerifier {
|
|
83
|
+
private readonly client: JevClient;
|
|
84
|
+
|
|
85
|
+
/** Construtor que recebe o cliente (injeção). */
|
|
86
|
+
constructor(client: JevClient) {
|
|
87
|
+
this.client = client;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `true` se há chave configurada (sem chave o tier 1 é pulado). */
|
|
91
|
+
isConfigured(): boolean {
|
|
92
|
+
return this.client.isConfigured();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Avalia os casos ambíguos e devolve a confiança por id. */
|
|
96
|
+
async verify(
|
|
97
|
+
cases: readonly JevVerifyCase[],
|
|
98
|
+
options: JevVerifyOptions = {},
|
|
99
|
+
): Promise<JevVerifyOutcome> {
|
|
100
|
+
const confidence = new Map<string, number>();
|
|
101
|
+
if (cases.length === 0) return { confidence, checked: 0, skipped: 0 };
|
|
102
|
+
if (!this.client.isConfigured()) {
|
|
103
|
+
return {
|
|
104
|
+
confidence,
|
|
105
|
+
checked: 0,
|
|
106
|
+
skipped: cases.length,
|
|
107
|
+
reason: "TYPESAFE_API_KEY não configurada",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const groups = [...groupByFile(cases).values()];
|
|
112
|
+
const jobs = groups.filter((group) => referenceOf(group, options.headSha));
|
|
113
|
+
const withoutRef =
|
|
114
|
+
cases.length - jobs.reduce((sum, group) => sum + group.length, 0);
|
|
115
|
+
let skipped = withoutRef;
|
|
116
|
+
|
|
117
|
+
await mapWithConcurrency(
|
|
118
|
+
jobs,
|
|
119
|
+
options.concurrency ?? DEFAULT_CONCURRENCY,
|
|
120
|
+
async (group) => {
|
|
121
|
+
try {
|
|
122
|
+
const answers = await this.evaluateGroup(group, options.headSha);
|
|
123
|
+
for (const item of group) {
|
|
124
|
+
const answer = answers[item.id];
|
|
125
|
+
if (answer?.type === "noul") confidence.set(item.id, answer.noul);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
skipped += group.length;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
confidence,
|
|
135
|
+
checked: confidence.size,
|
|
136
|
+
skipped,
|
|
137
|
+
reason:
|
|
138
|
+
withoutRef > 0
|
|
139
|
+
? "sem commit de referência (foundAtSha) em algum arquivo"
|
|
140
|
+
: undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Uma chamada por arquivo: `state` focado + uma pergunta por caso. */
|
|
145
|
+
private async evaluateGroup(
|
|
146
|
+
group: readonly JevVerifyCase[],
|
|
147
|
+
headSha: string | undefined,
|
|
148
|
+
): Promise<Record<string, JevAnswer>> {
|
|
149
|
+
const reference = referenceOf(group, headSha);
|
|
150
|
+
if (!reference) throw new Error("sem commit de referência para o diff focado.");
|
|
151
|
+
|
|
152
|
+
const file = group[0].file;
|
|
153
|
+
const patch =
|
|
154
|
+
diffPatch(`${reference.from}..${reference.to}`, [file]).get(file) ?? "";
|
|
155
|
+
const header = (readFileAt(reference.to, file) ?? "")
|
|
156
|
+
.split("\n")
|
|
157
|
+
.slice(0, HEADER_LINES)
|
|
158
|
+
.join("\n");
|
|
159
|
+
|
|
160
|
+
const parts = [`Arquivo: ${file}`];
|
|
161
|
+
if (header) parts.push("--- topo do arquivo ---", header);
|
|
162
|
+
if (patch) {
|
|
163
|
+
parts.push("--- diff desde a revisão ---", patch.slice(0, MAX_DIFF_CHARS));
|
|
164
|
+
}
|
|
165
|
+
const state = parts.join("\n").slice(0, MAX_STATE_CHARS);
|
|
166
|
+
|
|
167
|
+
const questions: Record<string, JevQuestion> = {};
|
|
168
|
+
for (const item of group) {
|
|
169
|
+
questions[item.id] = {
|
|
170
|
+
type: "noul",
|
|
171
|
+
instructions: `No estado fornecido, a violação a seguir ainda ocorre? "${item.rule}"`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const response = await this.client.evaluate({ state, questions });
|
|
176
|
+
return response.answers;
|
|
177
|
+
}
|
|
178
|
+
}
|