@payconductor/houndsman 0.2.0 → 0.4.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/README.md +18 -6
- package/package.json +1 -1
- package/src/cli/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +40 -0
- package/src/cli/commands/doctor-command.ts +60 -0
- package/src/cli/commands/findings-command.ts +38 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +21 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +47 -0
- package/src/cli/index.ts +60 -268
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +185 -260
- package/src/core/commands/review.ts +21 -39
- package/src/core/commands/scaffold.ts +189 -80
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +24 -25
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +65 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +79 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +193 -100
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +247 -421
- package/src/core/paths.ts +26 -11
- package/src/core/pr-context.ts +39 -51
- package/src/core/project-config.ts +56 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +123 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +8 -0
- package/src/core/run-status.ts +29 -59
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +139 -0
- package/src/core/services/project-files.ts +91 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +57 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +155 -245
- package/src/server/run-repository.ts +299 -0
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
- package/src/tests/baseline.test.ts +555 -493
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +205 -0
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcomando `findings` — triagem dos findings da revisão (biblioteca).
|
|
3
3
|
*
|
|
4
|
-
* Lê/grava o
|
|
5
|
-
* (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do
|
|
6
|
-
* usa `list`/`set`/`done` (e `batch` para lotes de correção) para
|
|
4
|
+
* Lê/grava o estado de triagem no storage (SQLite por default). Devolve o texto
|
|
5
|
+
* pronto (markdown ou JSON) + exit code; o CLI decide o log/exit. O agente do
|
|
6
|
+
* usuário usa `list`/`set`/`done` (e `batch` para lotes de correção) para
|
|
7
|
+
* interagir sem abrir o banco à mão.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Ids aceitam prefixo único (exato primeiro) — útil porque o id curto convive
|
|
10
|
+
* com o UUID interno no banco.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
type HoundsmanConfig,
|
|
16
|
-
resolveArtifactDir,
|
|
17
|
-
resolveJevConcurrency,
|
|
18
|
-
} from "../config";
|
|
13
|
+
import { type HoundsmanConfig, resolveJevConcurrency } from "../config";
|
|
19
14
|
import {
|
|
20
15
|
applyFindingUpdate,
|
|
21
|
-
buildStateFromRunDir,
|
|
22
16
|
FINDING_DECISIONS,
|
|
23
17
|
FINDING_EXEC_STATES,
|
|
24
18
|
type FindingDecision,
|
|
@@ -26,16 +20,14 @@ import {
|
|
|
26
20
|
type FindingState,
|
|
27
21
|
type FindingsFilter,
|
|
28
22
|
type FindingsStateFile,
|
|
29
|
-
FindingsStateStore,
|
|
30
|
-
type FindingsSummary,
|
|
31
23
|
type FindingUpdate,
|
|
32
24
|
fileReader,
|
|
33
25
|
filterFindings,
|
|
34
|
-
|
|
26
|
+
reorder,
|
|
27
|
+
resolveFindingId,
|
|
35
28
|
summarize,
|
|
36
29
|
syncState,
|
|
37
30
|
} from "../findings-state";
|
|
38
|
-
import { buildFixBatch, type FixBatchResult } from "../fix-batch";
|
|
39
31
|
import {
|
|
40
32
|
buildSubmitPreview,
|
|
41
33
|
defaultGhRunner,
|
|
@@ -45,13 +37,18 @@ import {
|
|
|
45
37
|
type SubmitSelection,
|
|
46
38
|
submitFindings,
|
|
47
39
|
} from "../github-review";
|
|
48
|
-
import {
|
|
40
|
+
import { VERIFY_FILE } from "../paths";
|
|
49
41
|
import { currentBranch } from "../pr-context";
|
|
42
|
+
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
43
|
+
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
44
|
+
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
50
45
|
import { pingFindingsStatus } from "../run-status";
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import type {
|
|
46
|
+
import { systemClock } from "../services/clock";
|
|
47
|
+
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
48
|
+
import { Verifier } from "../services/verifier";
|
|
49
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
|
|
50
|
+
import { ReviewWorkArea } from "../storage/work-area";
|
|
51
|
+
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
55
52
|
|
|
56
53
|
export type FindingsCommandResult = {
|
|
57
54
|
output: string;
|
|
@@ -65,7 +62,6 @@ const VALUE_FLAGS = new Set([
|
|
|
65
62
|
"--severity",
|
|
66
63
|
"--file",
|
|
67
64
|
"--branch",
|
|
68
|
-
"--dir",
|
|
69
65
|
"--limit",
|
|
70
66
|
"--comment",
|
|
71
67
|
"--by",
|
|
@@ -85,6 +81,11 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
85
81
|
const parsed: ParsedArgs = { positionals: [], values: {}, bools: new Set() };
|
|
86
82
|
for (let i = 0; i < args.length; i++) {
|
|
87
83
|
const arg = args[i];
|
|
84
|
+
if (arg === "--dir") {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
88
89
|
if (arg.startsWith("--")) {
|
|
89
90
|
if (VALUE_FLAGS.has(arg)) {
|
|
90
91
|
const value = args[i + 1];
|
|
@@ -101,123 +102,76 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
101
102
|
return parsed;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
dir: parsed.values["--dir"] ?? resolveArtifactDir(config),
|
|
111
|
-
branch: parsed.values["--branch"] ?? currentBranch(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
105
|
+
/** Contexto de uma review (storage + registro + staging). */
|
|
106
|
+
type CommandContext = {
|
|
107
|
+
storage: StorageProvider;
|
|
108
|
+
review: ReviewRecord;
|
|
109
|
+
work: ReviewWorkArea;
|
|
110
|
+
};
|
|
114
111
|
|
|
115
|
-
/**
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
branch,
|
|
125
|
-
fileReader(process.cwd()),
|
|
126
|
-
);
|
|
127
|
-
if (!built) {
|
|
112
|
+
/** Resolve a review da branch atual (explícita ou via git). */
|
|
113
|
+
function openContext(
|
|
114
|
+
storage: StorageProvider,
|
|
115
|
+
repo: RepoIdentity,
|
|
116
|
+
parsed: ParsedArgs,
|
|
117
|
+
): CommandContext {
|
|
118
|
+
const branch = parsed.values["--branch"] ?? currentBranch();
|
|
119
|
+
const review = storage.findReview(repo.key, branch);
|
|
120
|
+
if (!review) {
|
|
128
121
|
throw new Error(
|
|
129
|
-
`
|
|
122
|
+
`Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
130
123
|
);
|
|
131
124
|
}
|
|
132
|
-
|
|
133
|
-
return built;
|
|
125
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
/** Lê o estado de triagem; lança se a review ainda não tem findings. */
|
|
129
|
+
function loadState(ctx: CommandContext): FindingsStateFile {
|
|
130
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
131
|
+
if (!state) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Nenhum estado de triagem para '${ctx.review.branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return state;
|
|
142
137
|
}
|
|
143
138
|
|
|
144
|
-
/**
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (finding.comment) lines.push(` **Comentário:** ${finding.comment}`);
|
|
156
|
-
return lines;
|
|
139
|
+
/** Lê-modifica-grava o estado, atualizando `updatedAt` e a ordem. */
|
|
140
|
+
function mutateState(
|
|
141
|
+
ctx: CommandContext,
|
|
142
|
+
mutator: (state: FindingsStateFile) => void,
|
|
143
|
+
): FindingsStateFile {
|
|
144
|
+
const state = loadState(ctx);
|
|
145
|
+
mutator(state);
|
|
146
|
+
state.updatedAt = systemClock();
|
|
147
|
+
reorder(state);
|
|
148
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
149
|
+
return state;
|
|
157
150
|
}
|
|
158
151
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
152
|
+
/** Resolve um id (exato ou prefixo único), lançando erro explicativo. */
|
|
153
|
+
function requireFindingId(state: FindingsStateFile, input: string): string {
|
|
154
|
+
const resolution = resolveFindingId(state, input);
|
|
155
|
+
if (resolution.status === "found") return resolution.id;
|
|
156
|
+
if (resolution.status === "ambiguous") {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Id '${input}' ambíguo — candidatos: ${resolution.matches.join(", ")}.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Finding '${input}' não encontrado.`);
|
|
164
162
|
}
|
|
165
163
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
const exec = FINDING_EXEC_STATES.map((e) => `${e} ${summary.byExecState[e]}`).join(
|
|
171
|
-
" · ",
|
|
172
|
-
);
|
|
173
|
-
const severities = SEVERITY_ORDER.map((s) => `${s} ${summary.bySeverity[s]}`).join(
|
|
174
|
-
" · ",
|
|
175
|
-
);
|
|
176
|
-
return [
|
|
177
|
-
`# Triagem — ${state.branch}`,
|
|
178
|
-
"",
|
|
179
|
-
`Total: ${summary.total} (presentes: ${summary.present})`,
|
|
180
|
-
`Decisões: ${decisions}`,
|
|
181
|
-
`Execução: ${exec}`,
|
|
182
|
-
`Severidade: ${severities}`,
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Apresentação
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
185
167
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
168
|
+
const presenters = {
|
|
169
|
+
findings: new FindingsPresenter(),
|
|
170
|
+
batch: new BatchPresenter(),
|
|
171
|
+
verify: new VerifyPresenter(),
|
|
172
|
+
};
|
|
192
173
|
|
|
193
|
-
|
|
194
|
-
function renderBatch(result: FixBatchResult): string {
|
|
195
|
-
const { dimension, current, others, totals } = result;
|
|
196
|
-
if (totals.queue === 0) return "Nenhum finding pendente de correção.";
|
|
197
|
-
|
|
198
|
-
const lines = [
|
|
199
|
-
`# Lote de correção — agrupado por ${dimension} (${totals.groups} grupo(s))`,
|
|
200
|
-
"",
|
|
201
|
-
`## Grupo atual — ${current.key} (${current.total} finding(s), mostrando ${current.returned})`,
|
|
202
|
-
];
|
|
203
|
-
for (const finding of current.findings) lines.push(...renderFindingLine(finding));
|
|
204
|
-
if (current.remaining > 0) {
|
|
205
|
-
lines.push("", `_… +${current.remaining} neste grupo (use \`--offset\`)._`);
|
|
206
|
-
}
|
|
207
|
-
if (others.length > 0) {
|
|
208
|
-
lines.push("", "## Outros grupos disponíveis");
|
|
209
|
-
for (const group of others) {
|
|
210
|
-
lines.push(
|
|
211
|
-
`- ${group.key} — ${group.count} (${severityBreakdown(group.bySeverity)})`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
lines.push(
|
|
216
|
-
"",
|
|
217
|
-
`fila: ${totals.queue} · retornados: ${totals.returned} · grupos: ${totals.groups}`,
|
|
218
|
-
);
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
174
|
+
const fixBatchBuilder = new FixBatchBuilder();
|
|
221
175
|
|
|
222
176
|
// ---------------------------------------------------------------------------
|
|
223
177
|
// Execução
|
|
@@ -255,64 +209,52 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
|
|
|
255
209
|
};
|
|
256
210
|
}
|
|
257
211
|
|
|
258
|
-
/** Faz ping no
|
|
259
|
-
function pingStatus(
|
|
260
|
-
|
|
261
|
-
const state = FindingsStateStore.at(dir, branch).read();
|
|
262
|
-
if (state) pingFindingsStatus(runDir(dir, branch), state);
|
|
212
|
+
/** Faz ping no status da review (derivado do estado atual) após uma mutação. */
|
|
213
|
+
function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
214
|
+
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
263
215
|
}
|
|
264
216
|
|
|
265
217
|
/** Aplica uma atualização a um finding e persiste. */
|
|
266
218
|
function updateOne(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
id: string,
|
|
219
|
+
ctx: CommandContext,
|
|
220
|
+
input: string,
|
|
270
221
|
update: FindingUpdate,
|
|
271
222
|
): FindingState {
|
|
272
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
273
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
274
223
|
let updated: FindingState | undefined;
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
updated = applyFindingUpdate(finding, update);
|
|
224
|
+
mutateState(ctx, (state) => {
|
|
225
|
+
const id = requireFindingId(state, input);
|
|
226
|
+
updated = applyFindingUpdate(state.findings[id], update);
|
|
279
227
|
});
|
|
280
|
-
if (!updated) throw new Error(`Finding '${
|
|
228
|
+
if (!updated) throw new Error(`Finding '${input}' não encontrado.`);
|
|
281
229
|
return updated;
|
|
282
230
|
}
|
|
283
231
|
|
|
284
232
|
/** Aplica a mesma atualização a vários ids (done/reset). */
|
|
285
233
|
function updateMany(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
ids: string[],
|
|
234
|
+
ctx: CommandContext,
|
|
235
|
+
inputs: string[],
|
|
289
236
|
update: FindingUpdate,
|
|
290
237
|
): FindingState[] {
|
|
291
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
292
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
293
238
|
const updated: FindingState[] = [];
|
|
294
|
-
|
|
239
|
+
mutateState(ctx, (state) => {
|
|
240
|
+
const ids = inputs.map((input) => requireFindingId(state, input));
|
|
295
241
|
for (const id of ids) {
|
|
296
|
-
|
|
297
|
-
if (!finding) continue;
|
|
298
|
-
updated.push(applyFindingUpdate(finding, update));
|
|
242
|
+
updated.push(applyFindingUpdate(state.findings[id], update));
|
|
299
243
|
}
|
|
300
244
|
});
|
|
301
|
-
const missing = ids.filter((id) => !updated.some((f) => f.id === id));
|
|
302
|
-
if (missing.length > 0) {
|
|
303
|
-
throw new Error(`Findings não encontrados: ${missing.join(", ")}.`);
|
|
304
|
-
}
|
|
305
245
|
return updated;
|
|
306
246
|
}
|
|
307
247
|
|
|
308
248
|
/** Executa um subcomando de `findings` e devolve o texto + exit code. */
|
|
309
249
|
export function runFindingsCommand(
|
|
310
250
|
config: HoundsmanConfig,
|
|
251
|
+
storage: StorageProvider,
|
|
252
|
+
repo: RepoIdentity,
|
|
311
253
|
args: string[],
|
|
312
254
|
): FindingsCommandResult {
|
|
313
|
-
const parsed = parseArgs(args);
|
|
314
255
|
try {
|
|
315
|
-
|
|
256
|
+
const parsed = parseArgs(args);
|
|
257
|
+
return execute(config, storage, repo, parsed);
|
|
316
258
|
} catch (error) {
|
|
317
259
|
return {
|
|
318
260
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -321,10 +263,16 @@ export function runFindingsCommand(
|
|
|
321
263
|
}
|
|
322
264
|
}
|
|
323
265
|
|
|
324
|
-
function execute(
|
|
266
|
+
function execute(
|
|
267
|
+
_config: HoundsmanConfig,
|
|
268
|
+
storage: StorageProvider,
|
|
269
|
+
repo: RepoIdentity,
|
|
270
|
+
parsed: ParsedArgs,
|
|
271
|
+
): FindingsCommandResult {
|
|
272
|
+
const ctx = openContext(storage, repo, parsed);
|
|
325
273
|
switch (parsed.sub ?? "list") {
|
|
326
274
|
case "list": {
|
|
327
|
-
const state = loadState(
|
|
275
|
+
const state = loadState(ctx);
|
|
328
276
|
const filtered = filterFindings(state, filterFrom(parsed));
|
|
329
277
|
const limit = Number(parsed.values["--limit"]);
|
|
330
278
|
const findings =
|
|
@@ -332,34 +280,35 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
332
280
|
return {
|
|
333
281
|
output: parsed.bools.has("--json")
|
|
334
282
|
? JSON.stringify(findings, null, 2)
|
|
335
|
-
:
|
|
283
|
+
: presenters.findings.list(state, findings),
|
|
336
284
|
exitCode: 0,
|
|
337
285
|
};
|
|
338
286
|
}
|
|
339
287
|
case "show": {
|
|
340
|
-
const state = loadState(
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
288
|
+
const state = loadState(ctx);
|
|
289
|
+
const input = parsed.positionals[0];
|
|
290
|
+
if (!input) throw new Error("Informe o id: houndsman findings show <id>");
|
|
291
|
+
const id = requireFindingId(state, input);
|
|
343
292
|
const finding = state.findings[id];
|
|
344
|
-
if (!finding) throw new Error(`Finding '${id}' não encontrado.`);
|
|
345
293
|
return {
|
|
346
294
|
output: parsed.bools.has("--json")
|
|
347
295
|
? JSON.stringify(finding, null, 2)
|
|
348
|
-
:
|
|
296
|
+
: presenters.findings.list(state, [finding]),
|
|
349
297
|
exitCode: 0,
|
|
350
298
|
};
|
|
351
299
|
}
|
|
352
300
|
case "set": {
|
|
353
|
-
const
|
|
354
|
-
if (!
|
|
301
|
+
const input = parsed.positionals[0];
|
|
302
|
+
if (!input)
|
|
303
|
+
throw new Error("Informe o id: houndsman findings set <id> [flags]");
|
|
355
304
|
const update = updateFrom(parsed);
|
|
356
305
|
if (!hasUpdate(update)) {
|
|
357
306
|
throw new Error(
|
|
358
307
|
"Nada para atualizar (use --decision, --exec ou --comment).",
|
|
359
308
|
);
|
|
360
309
|
}
|
|
361
|
-
const finding = updateOne(
|
|
362
|
-
pingStatus(
|
|
310
|
+
const finding = updateOne(ctx, input, update);
|
|
311
|
+
pingStatus(ctx, loadState(ctx));
|
|
363
312
|
return {
|
|
364
313
|
output: parsed.bools.has("--json")
|
|
365
314
|
? JSON.stringify(finding, null, 2)
|
|
@@ -369,8 +318,8 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
369
318
|
}
|
|
370
319
|
case "done": {
|
|
371
320
|
const ids = requireIds(parsed, "done");
|
|
372
|
-
const updated = updateMany(
|
|
373
|
-
pingStatus(
|
|
321
|
+
const updated = updateMany(ctx, ids, { execState: "done" });
|
|
322
|
+
pingStatus(ctx, loadState(ctx));
|
|
374
323
|
return {
|
|
375
324
|
output: `Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
376
325
|
exitCode: 0,
|
|
@@ -378,28 +327,26 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
378
327
|
}
|
|
379
328
|
case "reset": {
|
|
380
329
|
const ids = requireIds(parsed, "reset");
|
|
381
|
-
const updated = updateMany(
|
|
330
|
+
const updated = updateMany(ctx, ids, {
|
|
382
331
|
decision: "pending",
|
|
383
332
|
execState: "pending",
|
|
384
333
|
comment: "",
|
|
385
334
|
});
|
|
386
|
-
pingStatus(
|
|
335
|
+
pingStatus(ctx, loadState(ctx));
|
|
387
336
|
return {
|
|
388
337
|
output: `Triagem reiniciada: ${updated.map((f) => f.id).join(", ")}.`,
|
|
389
338
|
exitCode: 0,
|
|
390
339
|
};
|
|
391
340
|
}
|
|
392
341
|
case "sync": {
|
|
393
|
-
const
|
|
394
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
395
|
-
const state = loadState(config, parsed);
|
|
342
|
+
const state = loadState(ctx);
|
|
396
343
|
const result = syncState(state, fileReader(process.cwd()));
|
|
397
|
-
|
|
398
|
-
pingStatus(
|
|
344
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
345
|
+
pingStatus(ctx, state);
|
|
399
346
|
const output = parsed.bools.has("--json")
|
|
400
347
|
? JSON.stringify(result, null, 2)
|
|
401
348
|
: [
|
|
402
|
-
`Sync — ${branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
349
|
+
`Sync — ${ctx.review.branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
403
350
|
result.moved.length > 0
|
|
404
351
|
? `Movidos: ${result.moved.join(", ")}`
|
|
405
352
|
: "",
|
|
@@ -412,25 +359,26 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
412
359
|
return { output, exitCode: 0 };
|
|
413
360
|
}
|
|
414
361
|
case "summary": {
|
|
415
|
-
const state = loadState(
|
|
362
|
+
const state = loadState(ctx);
|
|
416
363
|
const summary = summarize(state);
|
|
417
364
|
return {
|
|
418
365
|
output: parsed.bools.has("--json")
|
|
419
366
|
? JSON.stringify(summary, null, 2)
|
|
420
|
-
:
|
|
367
|
+
: presenters.findings.summary(state, summary),
|
|
421
368
|
exitCode: 0,
|
|
422
369
|
};
|
|
423
370
|
}
|
|
424
371
|
case "path": {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
372
|
+
return {
|
|
373
|
+
output: ctx.storage.location(ctx.review.id),
|
|
374
|
+
exitCode: 0,
|
|
375
|
+
};
|
|
428
376
|
}
|
|
429
377
|
case "batch": {
|
|
430
|
-
const state = loadState(
|
|
378
|
+
const state = loadState(ctx);
|
|
431
379
|
const severity = parsed.values["--severity"];
|
|
432
380
|
const offset = Number(parsed.values["--offset"]);
|
|
433
|
-
const result =
|
|
381
|
+
const result = fixBatchBuilder.build(state, {
|
|
434
382
|
severity: severity ? (severity as Severity) : undefined,
|
|
435
383
|
includeDone: parsed.bools.has("--include-done"),
|
|
436
384
|
offset: Number.isFinite(offset) ? offset : undefined,
|
|
@@ -439,7 +387,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
439
387
|
return {
|
|
440
388
|
output: parsed.bools.has("--json")
|
|
441
389
|
? JSON.stringify(result, null, 2)
|
|
442
|
-
:
|
|
390
|
+
: presenters.batch.render(result),
|
|
443
391
|
exitCode: 0,
|
|
444
392
|
};
|
|
445
393
|
}
|
|
@@ -479,43 +427,6 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
479
427
|
// verify — as violações foram resolvidas? (sem re-revisar a PR)
|
|
480
428
|
// ---------------------------------------------------------------------------
|
|
481
429
|
|
|
482
|
-
/** Rótulo curto de cada status de verificação. */
|
|
483
|
-
const VERIFY_LABEL: Record<VerifyStatus, string> = {
|
|
484
|
-
resolved: "✅ resolvido",
|
|
485
|
-
persisting: "❌ persiste",
|
|
486
|
-
uncertain: "⚠️ incerto",
|
|
487
|
-
"file-missing": "🗑️ arquivo ausente",
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
/** Render da verificação: contagem por status + uma linha por finding. */
|
|
491
|
-
function renderVerify(
|
|
492
|
-
state: FindingsStateFile,
|
|
493
|
-
entries: VerifyEntry[],
|
|
494
|
-
summary: VerifySummary,
|
|
495
|
-
meta: { jevChecked: number; jevReason?: string },
|
|
496
|
-
): string {
|
|
497
|
-
if (entries.length === 0) return "Nenhum finding para verificar.";
|
|
498
|
-
const lines = [
|
|
499
|
-
`# Verificação — ${state.branch} (${entries.length})`,
|
|
500
|
-
"",
|
|
501
|
-
`Resolvidos: ${summary.resolved} · Persistem: ${summary.persisting} · Incertos: ${summary.uncertain} · Arquivo ausente: ${summary["file-missing"]}`,
|
|
502
|
-
];
|
|
503
|
-
if (meta.jevChecked > 0) lines.push(`Confirmados por Jev: ${meta.jevChecked}.`);
|
|
504
|
-
else if (meta.jevReason) lines.push(`Jev: pulado (${meta.jevReason}).`);
|
|
505
|
-
lines.push("");
|
|
506
|
-
for (const entry of entries) {
|
|
507
|
-
const location = entry.line ? `${entry.file}:${entry.line}` : entry.file;
|
|
508
|
-
const via =
|
|
509
|
-
entry.basis === "jev" ? ` · Jev ${(entry.confidence ?? 0).toFixed(2)}` : "";
|
|
510
|
-
lines.push(
|
|
511
|
-
`- \`${entry.id}\` ${VERIFY_LABEL[entry.status]} — **${entry.severity}** \`${location}\` — ${entry.rule}${via}`,
|
|
512
|
-
` ${entry.message}`,
|
|
513
|
-
);
|
|
514
|
-
if (entry.detail) lines.push(` _${entry.detail}_`);
|
|
515
|
-
}
|
|
516
|
-
return lines.join("\n");
|
|
517
|
-
}
|
|
518
|
-
|
|
519
430
|
/**
|
|
520
431
|
* Subcomando `findings verify` — checa se as violações foram resolvidas, sem
|
|
521
432
|
* re-revisar a PR. Tier local (grátis) + confirmação Jev dos casos ambíguos
|
|
@@ -523,11 +434,13 @@ function renderVerify(
|
|
|
523
434
|
*/
|
|
524
435
|
export async function runVerifyCommand(
|
|
525
436
|
config: HoundsmanConfig,
|
|
437
|
+
storage: StorageProvider,
|
|
438
|
+
repo: RepoIdentity,
|
|
526
439
|
args: string[],
|
|
527
440
|
): Promise<FindingsCommandResult> {
|
|
528
|
-
const parsed = parseArgs(args);
|
|
529
441
|
try {
|
|
530
|
-
|
|
442
|
+
const parsed = parseArgs(args);
|
|
443
|
+
return await executeVerify(config, storage, repo, parsed);
|
|
531
444
|
} catch (error) {
|
|
532
445
|
return {
|
|
533
446
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -538,34 +451,33 @@ export async function runVerifyCommand(
|
|
|
538
451
|
|
|
539
452
|
async function executeVerify(
|
|
540
453
|
config: HoundsmanConfig,
|
|
454
|
+
storage: StorageProvider,
|
|
455
|
+
repo: RepoIdentity,
|
|
541
456
|
parsed: ParsedArgs,
|
|
542
457
|
): Promise<FindingsCommandResult> {
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const state = loadState(config, parsed);
|
|
458
|
+
const ctx = openContext(storage, repo, parsed);
|
|
459
|
+
const state = loadState(ctx);
|
|
546
460
|
|
|
547
|
-
// Lazy: `../
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
{ JevCandidatesStore, JevClient, JevVerifier },
|
|
551
|
-
] = await Promise.all([import("../verify"), import("../jev")]);
|
|
461
|
+
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
462
|
+
const verifier = new Verifier();
|
|
463
|
+
const { JevCandidatesStore, JevClient, JevVerifier } = await import("../jev");
|
|
552
464
|
|
|
553
|
-
const findings =
|
|
554
|
-
const entries = verifyLocal(findings, fileReader(process.cwd()));
|
|
465
|
+
const findings = verifier.select(state, filterFrom(parsed));
|
|
466
|
+
const entries = verifier.verifyLocal(findings, fileReader(process.cwd()));
|
|
555
467
|
|
|
556
468
|
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
557
469
|
let jevChecked = 0;
|
|
558
470
|
let jevReason: string | undefined;
|
|
559
471
|
const ambiguous = entries.filter((entry) => entry.ambiguous);
|
|
560
472
|
if (ambiguous.length > 0) {
|
|
561
|
-
const
|
|
473
|
+
const jevVerifier = new JevVerifier(new JevClient());
|
|
562
474
|
if (parsed.bools.has("--no-jev")) {
|
|
563
475
|
jevReason = "desativado (--no-jev)";
|
|
564
|
-
} else if (!
|
|
476
|
+
} else if (!jevVerifier.isConfigured()) {
|
|
565
477
|
jevReason = "TYPESAFE_API_KEY não configurada";
|
|
566
478
|
} else {
|
|
567
479
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
568
|
-
const outcome = await
|
|
480
|
+
const outcome = await jevVerifier.verify(
|
|
569
481
|
ambiguous.map((entry) => ({
|
|
570
482
|
id: entry.id,
|
|
571
483
|
file: entry.file,
|
|
@@ -577,37 +489,48 @@ async function executeVerify(
|
|
|
577
489
|
concurrency: resolveJevConcurrency(config),
|
|
578
490
|
},
|
|
579
491
|
);
|
|
580
|
-
applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
492
|
+
verifier.applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
581
493
|
jevChecked = outcome.checked;
|
|
582
494
|
jevReason = outcome.reason;
|
|
583
495
|
}
|
|
584
496
|
}
|
|
585
497
|
|
|
586
|
-
const summary =
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
498
|
+
const summary = verifier.summarize(entries);
|
|
499
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
500
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
501
|
+
name: VERIFY_FILE,
|
|
502
|
+
kind: "verify",
|
|
503
|
+
content: `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
504
|
+
});
|
|
592
505
|
|
|
593
506
|
let applied: string[] = [];
|
|
594
507
|
if (parsed.bools.has("--apply")) {
|
|
595
|
-
|
|
596
|
-
applied = applyResolutions(
|
|
508
|
+
const current = mutateState(ctx, (draft) => {
|
|
509
|
+
applied = verifier.applyResolutions(draft, entries);
|
|
597
510
|
});
|
|
598
|
-
pingStatus(
|
|
511
|
+
pingStatus(ctx, current);
|
|
599
512
|
}
|
|
600
513
|
|
|
601
514
|
const output = parsed.bools.has("--json")
|
|
602
515
|
? JSON.stringify(
|
|
603
|
-
{
|
|
516
|
+
{
|
|
517
|
+
branch: ctx.review.branch,
|
|
518
|
+
summary,
|
|
519
|
+
jevChecked,
|
|
520
|
+
jevReason,
|
|
521
|
+
applied,
|
|
522
|
+
entries,
|
|
523
|
+
},
|
|
604
524
|
null,
|
|
605
525
|
2,
|
|
606
526
|
)
|
|
607
527
|
: [
|
|
608
|
-
|
|
528
|
+
presenters.verify.render(state, entries, summary, {
|
|
529
|
+
jevChecked,
|
|
530
|
+
jevReason,
|
|
531
|
+
}),
|
|
609
532
|
"",
|
|
610
|
-
`Relatório: ${
|
|
533
|
+
`Relatório: ${location} (${VERIFY_FILE})`,
|
|
611
534
|
parsed.bools.has("--apply")
|
|
612
535
|
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
613
536
|
: "",
|
|
@@ -697,11 +620,13 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
697
620
|
*/
|
|
698
621
|
export async function runSubmitCommand(
|
|
699
622
|
config: HoundsmanConfig,
|
|
623
|
+
storage: StorageProvider,
|
|
624
|
+
repo: RepoIdentity,
|
|
700
625
|
args: string[],
|
|
701
626
|
): Promise<FindingsCommandResult> {
|
|
702
|
-
const parsed = parseArgs(args);
|
|
703
627
|
try {
|
|
704
|
-
|
|
628
|
+
const parsed = parseArgs(args);
|
|
629
|
+
return await executeSubmit(config, storage, repo, parsed);
|
|
705
630
|
} catch (error) {
|
|
706
631
|
return {
|
|
707
632
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -711,10 +636,13 @@ export async function runSubmitCommand(
|
|
|
711
636
|
}
|
|
712
637
|
|
|
713
638
|
async function executeSubmit(
|
|
714
|
-
|
|
639
|
+
_config: HoundsmanConfig,
|
|
640
|
+
storage: StorageProvider,
|
|
641
|
+
repo: RepoIdentity,
|
|
715
642
|
parsed: ParsedArgs,
|
|
716
643
|
): Promise<FindingsCommandResult> {
|
|
717
|
-
const
|
|
644
|
+
const ctx = openContext(storage, repo, parsed);
|
|
645
|
+
const state = loadState(ctx);
|
|
718
646
|
const selection = submitSelection(parsed);
|
|
719
647
|
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
720
648
|
const preview = buildSubmitPreview(state, target, selection);
|
|
@@ -731,11 +659,8 @@ async function executeSubmit(
|
|
|
731
659
|
runner: defaultGhRunner,
|
|
732
660
|
selection,
|
|
733
661
|
});
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
state.branch,
|
|
737
|
-
).write(state);
|
|
738
|
-
pingStatus(config, parsed);
|
|
662
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
663
|
+
pingStatus(ctx, state);
|
|
739
664
|
|
|
740
665
|
return {
|
|
741
666
|
output: renderSubmitOutcome(outcome, json),
|