@payconductor/houndsman 0.3.0 → 0.5.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 +25 -39
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/commands/status-command.ts +46 -0
- package/src/cli/help.ts +8 -3
- package/src/cli/index.ts +5 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +216 -125
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/commands/status.ts +63 -0
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-scope.ts +26 -8
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +53 -75
- package/src/core/github-review.ts +620 -57
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +271 -134
- package/src/core/paths.ts +26 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/renderers/resume-renderer.ts +151 -0
- package/src/core/report-builder.ts +7 -4
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +59 -50
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +271 -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 +162 -0
- package/src/core/storage/sqlite/schema.ts +116 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +431 -0
- package/src/core/storage/types.ts +152 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/App.tsx +26 -9
- package/src/server/client/ArtifactsPanel.tsx +144 -23
- package/src/server/client/AutoRefresh.tsx +1 -1
- package/src/server/client/Filters.tsx +7 -2
- package/src/server/client/FindingCard.tsx +50 -15
- package/src/server/client/FindingsList.tsx +2 -2
- package/src/server/client/MarkdownText.tsx +181 -0
- package/src/server/client/ResumePanel.tsx +368 -0
- package/src/server/client/RunList.tsx +3 -3
- package/src/server/client/SubmitModal.tsx +58 -10
- package/src/server/client/SummaryBar.tsx +1 -1
- package/src/server/client/icons.tsx +17 -0
- package/src/server/client/utils/code-highlight.ts +422 -0
- package/src/server/client/utils/constants.ts +226 -0
- package/src/server/client/utils/json-highlight.ts +55 -0
- package/src/server/client/utils/markdown.ts +319 -0
- package/src/server/client/{types.ts → utils/types.ts} +81 -0
- package/src/server/index.ts +154 -16
- package/src/server/run-repository.ts +153 -136
- 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/example-review.md +22 -14
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +55 -32
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +7 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +74 -61
- package/src/tests/baseline.test.ts +79 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +56 -5
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/github-review.test.ts +409 -60
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +149 -0
- package/src/tests/review-resume.test.ts +101 -0
- package/src/tests/review-scope.test.ts +302 -0
- package/src/tests/review-scripts.test.ts +81 -5
- package/src/tests/run-status-view.test.ts +94 -0
- package/src/tests/run-status.test.ts +86 -23
- package/src/tests/server.test.ts +184 -96
- package/src/tests/storage.test.ts +449 -0
- package/src/server/client/constants.ts +0 -91
- /package/src/server/client/{api.ts → utils/api.ts} +0 -0
- /package/src/server/client/{format.ts → utils/format.ts} +0 -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,11 +20,11 @@ import {
|
|
|
26
20
|
type FindingState,
|
|
27
21
|
type FindingsFilter,
|
|
28
22
|
type FindingsStateFile,
|
|
29
|
-
FindingsStateStore,
|
|
30
23
|
type FindingUpdate,
|
|
31
24
|
fileReader,
|
|
32
25
|
filterFindings,
|
|
33
|
-
|
|
26
|
+
reorder,
|
|
27
|
+
resolveFindingId,
|
|
34
28
|
summarize,
|
|
35
29
|
syncState,
|
|
36
30
|
} from "../findings-state";
|
|
@@ -42,15 +36,19 @@ import {
|
|
|
42
36
|
type SubmitPreview,
|
|
43
37
|
type SubmitSelection,
|
|
44
38
|
submitFindings,
|
|
39
|
+
syncResolvedThreads,
|
|
45
40
|
} from "../github-review";
|
|
46
|
-
import {
|
|
41
|
+
import { VERIFY_FILE } from "../paths";
|
|
47
42
|
import { currentBranch } from "../pr-context";
|
|
48
43
|
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
49
44
|
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
50
45
|
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
51
46
|
import { pingFindingsStatus } from "../run-status";
|
|
47
|
+
import { systemClock } from "../services/clock";
|
|
52
48
|
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
53
49
|
import { Verifier } from "../services/verifier";
|
|
50
|
+
import type { RepoIdentity, ReviewRecord, StorageProvider } from "../storage/types";
|
|
51
|
+
import { ReviewWorkArea } from "../storage/work-area";
|
|
54
52
|
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
55
53
|
|
|
56
54
|
export type FindingsCommandResult = {
|
|
@@ -65,7 +63,6 @@ const VALUE_FLAGS = new Set([
|
|
|
65
63
|
"--severity",
|
|
66
64
|
"--file",
|
|
67
65
|
"--branch",
|
|
68
|
-
"--dir",
|
|
69
66
|
"--limit",
|
|
70
67
|
"--comment",
|
|
71
68
|
"--by",
|
|
@@ -85,6 +82,11 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
85
82
|
const parsed: ParsedArgs = { positionals: [], values: {}, bools: new Set() };
|
|
86
83
|
for (let i = 0; i < args.length; i++) {
|
|
87
84
|
const arg = args[i];
|
|
85
|
+
if (arg === "--dir") {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
88
90
|
if (arg.startsWith("--")) {
|
|
89
91
|
if (VALUE_FLAGS.has(arg)) {
|
|
90
92
|
const value = args[i + 1];
|
|
@@ -101,36 +103,63 @@ function parseArgs(args: string[]): ParsedArgs {
|
|
|
101
103
|
return parsed;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
/** Contexto de uma review (storage + registro + staging). */
|
|
107
|
+
type CommandContext = {
|
|
108
|
+
storage: StorageProvider;
|
|
109
|
+
review: ReviewRecord;
|
|
110
|
+
work: ReviewWorkArea;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Resolve a review da branch atual (explícita ou via git). */
|
|
114
|
+
function openContext(
|
|
115
|
+
storage: StorageProvider,
|
|
116
|
+
repo: RepoIdentity,
|
|
107
117
|
parsed: ParsedArgs,
|
|
108
|
-
):
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
): CommandContext {
|
|
119
|
+
const branch = parsed.values["--branch"] ?? currentBranch();
|
|
120
|
+
const review = storage.findReview(repo.key, branch);
|
|
121
|
+
if (!review) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Nenhuma revisão para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return { storage, review, work: new ReviewWorkArea(review) };
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
/** Lê o estado
|
|
116
|
-
function loadState(
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const existing = store.read();
|
|
120
|
-
if (existing) return existing;
|
|
121
|
-
|
|
122
|
-
const built = buildStateFromRunDir(
|
|
123
|
-
runDir(dir, branch),
|
|
124
|
-
branch,
|
|
125
|
-
fileReader(process.cwd()),
|
|
126
|
-
);
|
|
127
|
-
if (!built) {
|
|
129
|
+
/** Lê o estado de triagem; lança se a review ainda não tem findings. */
|
|
130
|
+
function loadState(ctx: CommandContext): FindingsStateFile {
|
|
131
|
+
const state = ctx.storage.readFindings(ctx.review.id);
|
|
132
|
+
if (!state) {
|
|
128
133
|
throw new Error(
|
|
129
|
-
`Nenhum estado de triagem para '${branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
134
|
+
`Nenhum estado de triagem para '${ctx.review.branch}'. Rode \`houndsman --finalize\` primeiro.`,
|
|
130
135
|
);
|
|
131
136
|
}
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Lê-modifica-grava o estado, atualizando `updatedAt` e a ordem. */
|
|
141
|
+
function mutateState(
|
|
142
|
+
ctx: CommandContext,
|
|
143
|
+
mutator: (state: FindingsStateFile) => void,
|
|
144
|
+
): FindingsStateFile {
|
|
145
|
+
const state = loadState(ctx);
|
|
146
|
+
mutator(state);
|
|
147
|
+
state.updatedAt = systemClock();
|
|
148
|
+
reorder(state);
|
|
149
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
150
|
+
return state;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Resolve um id (exato ou prefixo único), lançando erro explicativo. */
|
|
154
|
+
function requireFindingId(state: FindingsStateFile, input: string): string {
|
|
155
|
+
const resolution = resolveFindingId(state, input);
|
|
156
|
+
if (resolution.status === "found") return resolution.id;
|
|
157
|
+
if (resolution.status === "ambiguous") {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Id '${input}' ambíguo — candidatos: ${resolution.matches.join(", ")}.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Finding '${input}' não encontrado.`);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
// ---------------------------------------------------------------------------
|
|
@@ -181,64 +210,71 @@ function filterFrom(parsed: ParsedArgs): FindingsFilter {
|
|
|
181
210
|
};
|
|
182
211
|
}
|
|
183
212
|
|
|
184
|
-
/** Faz ping no
|
|
185
|
-
function pingStatus(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
213
|
+
/** Faz ping no status da review (derivado do estado atual) após uma mutação. */
|
|
214
|
+
function pingStatus(ctx: CommandContext, state: FindingsStateFile): void {
|
|
215
|
+
pingFindingsStatus(ctx.storage, ctx.review.id, state);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve no GitHub as threads dos findings recém-resolvidos localmente.
|
|
220
|
+
* Fail-open (`gh` ausente/sem PR ⇒ só a mensagem). Devolve a linha para o
|
|
221
|
+
* output ou `null` quando não há vínculo remoto pendente.
|
|
222
|
+
*/
|
|
223
|
+
function syncThreadsOutput(ctx: CommandContext, ids: readonly string[]): string | null {
|
|
224
|
+
if (ids.length === 0) return null;
|
|
225
|
+
const state = loadState(ctx);
|
|
226
|
+
const outcome = syncResolvedThreads({ state, ids });
|
|
227
|
+
if (outcome.checked === 0) return null;
|
|
228
|
+
if (outcome.updated.length > 0) ctx.storage.writeFindings(ctx.review.id, state);
|
|
229
|
+
if (!outcome.available) {
|
|
230
|
+
return `GitHub: threads não sincronizadas (${outcome.reason}).`;
|
|
231
|
+
}
|
|
232
|
+
const failures =
|
|
233
|
+
outcome.failed.length > 0 ? `, ${outcome.failed.length} falha(s)` : "";
|
|
234
|
+
return `GitHub: ${outcome.resolved.length} thread(s) resolvida(s), ${outcome.skipped.length} já resolvida(s)/pulada(s)${failures}.`;
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
/** Aplica uma atualização a um finding e persiste. */
|
|
192
238
|
function updateOne(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
id: string,
|
|
239
|
+
ctx: CommandContext,
|
|
240
|
+
input: string,
|
|
196
241
|
update: FindingUpdate,
|
|
197
242
|
): FindingState {
|
|
198
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
199
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
200
243
|
let updated: FindingState | undefined;
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
updated = applyFindingUpdate(finding, update);
|
|
244
|
+
mutateState(ctx, (state) => {
|
|
245
|
+
const id = requireFindingId(state, input);
|
|
246
|
+
updated = applyFindingUpdate(state.findings[id], update);
|
|
205
247
|
});
|
|
206
|
-
if (!updated) throw new Error(`Finding '${
|
|
248
|
+
if (!updated) throw new Error(`Finding '${input}' não encontrado.`);
|
|
207
249
|
return updated;
|
|
208
250
|
}
|
|
209
251
|
|
|
210
252
|
/** Aplica a mesma atualização a vários ids (done/reset). */
|
|
211
253
|
function updateMany(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
ids: string[],
|
|
254
|
+
ctx: CommandContext,
|
|
255
|
+
inputs: string[],
|
|
215
256
|
update: FindingUpdate,
|
|
216
257
|
): FindingState[] {
|
|
217
|
-
const { dir, branch } = resolveDir(config, parsed);
|
|
218
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
219
258
|
const updated: FindingState[] = [];
|
|
220
|
-
|
|
259
|
+
mutateState(ctx, (state) => {
|
|
260
|
+
const ids = inputs.map((input) => requireFindingId(state, input));
|
|
221
261
|
for (const id of ids) {
|
|
222
|
-
|
|
223
|
-
if (!finding) continue;
|
|
224
|
-
updated.push(applyFindingUpdate(finding, update));
|
|
262
|
+
updated.push(applyFindingUpdate(state.findings[id], update));
|
|
225
263
|
}
|
|
226
264
|
});
|
|
227
|
-
const missing = ids.filter((id) => !updated.some((f) => f.id === id));
|
|
228
|
-
if (missing.length > 0) {
|
|
229
|
-
throw new Error(`Findings não encontrados: ${missing.join(", ")}.`);
|
|
230
|
-
}
|
|
231
265
|
return updated;
|
|
232
266
|
}
|
|
233
267
|
|
|
234
268
|
/** Executa um subcomando de `findings` e devolve o texto + exit code. */
|
|
235
269
|
export function runFindingsCommand(
|
|
236
270
|
config: HoundsmanConfig,
|
|
271
|
+
storage: StorageProvider,
|
|
272
|
+
repo: RepoIdentity,
|
|
237
273
|
args: string[],
|
|
238
274
|
): FindingsCommandResult {
|
|
239
|
-
const parsed = parseArgs(args);
|
|
240
275
|
try {
|
|
241
|
-
|
|
276
|
+
const parsed = parseArgs(args);
|
|
277
|
+
return execute(config, storage, repo, parsed);
|
|
242
278
|
} catch (error) {
|
|
243
279
|
return {
|
|
244
280
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -247,10 +283,16 @@ export function runFindingsCommand(
|
|
|
247
283
|
}
|
|
248
284
|
}
|
|
249
285
|
|
|
250
|
-
function execute(
|
|
286
|
+
function execute(
|
|
287
|
+
_config: HoundsmanConfig,
|
|
288
|
+
storage: StorageProvider,
|
|
289
|
+
repo: RepoIdentity,
|
|
290
|
+
parsed: ParsedArgs,
|
|
291
|
+
): FindingsCommandResult {
|
|
292
|
+
const ctx = openContext(storage, repo, parsed);
|
|
251
293
|
switch (parsed.sub ?? "list") {
|
|
252
294
|
case "list": {
|
|
253
|
-
const state = loadState(
|
|
295
|
+
const state = loadState(ctx);
|
|
254
296
|
const filtered = filterFindings(state, filterFrom(parsed));
|
|
255
297
|
const limit = Number(parsed.values["--limit"]);
|
|
256
298
|
const findings =
|
|
@@ -263,11 +305,11 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
263
305
|
};
|
|
264
306
|
}
|
|
265
307
|
case "show": {
|
|
266
|
-
const state = loadState(
|
|
267
|
-
const
|
|
268
|
-
if (!
|
|
308
|
+
const state = loadState(ctx);
|
|
309
|
+
const input = parsed.positionals[0];
|
|
310
|
+
if (!input) throw new Error("Informe o id: houndsman findings show <id>");
|
|
311
|
+
const id = requireFindingId(state, input);
|
|
269
312
|
const finding = state.findings[id];
|
|
270
|
-
if (!finding) throw new Error(`Finding '${id}' não encontrado.`);
|
|
271
313
|
return {
|
|
272
314
|
output: parsed.bools.has("--json")
|
|
273
315
|
? JSON.stringify(finding, null, 2)
|
|
@@ -276,56 +318,71 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
276
318
|
};
|
|
277
319
|
}
|
|
278
320
|
case "set": {
|
|
279
|
-
const
|
|
280
|
-
if (!
|
|
321
|
+
const input = parsed.positionals[0];
|
|
322
|
+
if (!input)
|
|
323
|
+
throw new Error("Informe o id: houndsman findings set <id> [flags]");
|
|
281
324
|
const update = updateFrom(parsed);
|
|
282
325
|
if (!hasUpdate(update)) {
|
|
283
326
|
throw new Error(
|
|
284
327
|
"Nada para atualizar (use --decision, --exec ou --comment).",
|
|
285
328
|
);
|
|
286
329
|
}
|
|
287
|
-
const finding = updateOne(
|
|
288
|
-
pingStatus(
|
|
330
|
+
const finding = updateOne(ctx, input, update);
|
|
331
|
+
pingStatus(ctx, loadState(ctx));
|
|
332
|
+
const threads =
|
|
333
|
+
update.execState === "done" ? syncThreadsOutput(ctx, [finding.id]) : null;
|
|
289
334
|
return {
|
|
290
335
|
output: parsed.bools.has("--json")
|
|
291
336
|
? JSON.stringify(finding, null, 2)
|
|
292
|
-
:
|
|
337
|
+
: [
|
|
338
|
+
`Atualizado: \`${finding.id}\` → ${finding.decision}/${finding.execState}.`,
|
|
339
|
+
threads,
|
|
340
|
+
]
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
.join("\n"),
|
|
293
343
|
exitCode: 0,
|
|
294
344
|
};
|
|
295
345
|
}
|
|
296
346
|
case "done": {
|
|
297
347
|
const ids = requireIds(parsed, "done");
|
|
298
|
-
const updated = updateMany(
|
|
299
|
-
pingStatus(
|
|
348
|
+
const updated = updateMany(ctx, ids, { execState: "done" });
|
|
349
|
+
pingStatus(ctx, loadState(ctx));
|
|
350
|
+
const threads = syncThreadsOutput(
|
|
351
|
+
ctx,
|
|
352
|
+
updated.map((finding) => finding.id),
|
|
353
|
+
);
|
|
300
354
|
return {
|
|
301
|
-
output:
|
|
355
|
+
output: [
|
|
356
|
+
`Marcados como done: ${updated.map((f) => f.id).join(", ")}.`,
|
|
357
|
+
threads,
|
|
358
|
+
]
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.join("\n"),
|
|
302
361
|
exitCode: 0,
|
|
303
362
|
};
|
|
304
363
|
}
|
|
305
364
|
case "reset": {
|
|
306
365
|
const ids = requireIds(parsed, "reset");
|
|
307
|
-
const updated = updateMany(
|
|
366
|
+
const updated = updateMany(ctx, ids, {
|
|
308
367
|
decision: "pending",
|
|
309
368
|
execState: "pending",
|
|
310
369
|
comment: "",
|
|
311
370
|
});
|
|
312
|
-
pingStatus(
|
|
371
|
+
pingStatus(ctx, loadState(ctx));
|
|
313
372
|
return {
|
|
314
373
|
output: `Triagem reiniciada: ${updated.map((f) => f.id).join(", ")}.`,
|
|
315
374
|
exitCode: 0,
|
|
316
375
|
};
|
|
317
376
|
}
|
|
318
377
|
case "sync": {
|
|
319
|
-
const
|
|
320
|
-
const store = FindingsStateStore.at(dir, branch);
|
|
321
|
-
const state = loadState(config, parsed);
|
|
378
|
+
const state = loadState(ctx);
|
|
322
379
|
const result = syncState(state, fileReader(process.cwd()));
|
|
323
|
-
|
|
324
|
-
pingStatus(
|
|
380
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
381
|
+
pingStatus(ctx, state);
|
|
325
382
|
const output = parsed.bools.has("--json")
|
|
326
383
|
? JSON.stringify(result, null, 2)
|
|
327
384
|
: [
|
|
328
|
-
`Sync — ${branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
385
|
+
`Sync — ${ctx.review.branch}: ${result.ok} ok, ${result.moved.length} movido(s), ${result.stale.length} stale.`,
|
|
329
386
|
result.moved.length > 0
|
|
330
387
|
? `Movidos: ${result.moved.join(", ")}`
|
|
331
388
|
: "",
|
|
@@ -338,7 +395,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
338
395
|
return { output, exitCode: 0 };
|
|
339
396
|
}
|
|
340
397
|
case "summary": {
|
|
341
|
-
const state = loadState(
|
|
398
|
+
const state = loadState(ctx);
|
|
342
399
|
const summary = summarize(state);
|
|
343
400
|
return {
|
|
344
401
|
output: parsed.bools.has("--json")
|
|
@@ -348,12 +405,13 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
348
405
|
};
|
|
349
406
|
}
|
|
350
407
|
case "path": {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
408
|
+
return {
|
|
409
|
+
output: ctx.storage.location(ctx.review.id),
|
|
410
|
+
exitCode: 0,
|
|
411
|
+
};
|
|
354
412
|
}
|
|
355
413
|
case "batch": {
|
|
356
|
-
const state = loadState(
|
|
414
|
+
const state = loadState(ctx);
|
|
357
415
|
const severity = parsed.values["--severity"];
|
|
358
416
|
const offset = Number(parsed.values["--offset"]);
|
|
359
417
|
const result = fixBatchBuilder.build(state, {
|
|
@@ -412,11 +470,13 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
412
470
|
*/
|
|
413
471
|
export async function runVerifyCommand(
|
|
414
472
|
config: HoundsmanConfig,
|
|
473
|
+
storage: StorageProvider,
|
|
474
|
+
repo: RepoIdentity,
|
|
415
475
|
args: string[],
|
|
416
476
|
): Promise<FindingsCommandResult> {
|
|
417
|
-
const parsed = parseArgs(args);
|
|
418
477
|
try {
|
|
419
|
-
|
|
478
|
+
const parsed = parseArgs(args);
|
|
479
|
+
return await executeVerify(config, storage, repo, parsed);
|
|
420
480
|
} catch (error) {
|
|
421
481
|
return {
|
|
422
482
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -427,11 +487,12 @@ export async function runVerifyCommand(
|
|
|
427
487
|
|
|
428
488
|
async function executeVerify(
|
|
429
489
|
config: HoundsmanConfig,
|
|
490
|
+
storage: StorageProvider,
|
|
491
|
+
repo: RepoIdentity,
|
|
430
492
|
parsed: ParsedArgs,
|
|
431
493
|
): Promise<FindingsCommandResult> {
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const state = loadState(config, parsed);
|
|
494
|
+
const ctx = openContext(storage, repo, parsed);
|
|
495
|
+
const state = loadState(ctx);
|
|
435
496
|
|
|
436
497
|
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
437
498
|
const verifier = new Verifier();
|
|
@@ -471,23 +532,34 @@ async function executeVerify(
|
|
|
471
532
|
}
|
|
472
533
|
|
|
473
534
|
const summary = verifier.summarize(entries);
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
535
|
+
const location = ctx.storage.location(ctx.review.id);
|
|
536
|
+
ctx.storage.writeArtifact(ctx.review.id, {
|
|
537
|
+
name: VERIFY_FILE,
|
|
538
|
+
kind: "verify",
|
|
539
|
+
content: `${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
540
|
+
});
|
|
479
541
|
|
|
480
542
|
let applied: string[] = [];
|
|
481
543
|
if (parsed.bools.has("--apply")) {
|
|
482
|
-
|
|
483
|
-
applied = verifier.applyResolutions(
|
|
544
|
+
const current = mutateState(ctx, (draft) => {
|
|
545
|
+
applied = verifier.applyResolutions(draft, entries);
|
|
484
546
|
});
|
|
485
|
-
pingStatus(
|
|
547
|
+
pingStatus(ctx, current);
|
|
486
548
|
}
|
|
487
549
|
|
|
550
|
+
// Resolvidos localmente ⇒ resolve as conversas correspondentes no PR.
|
|
551
|
+
const threads = syncThreadsOutput(ctx, applied);
|
|
552
|
+
|
|
488
553
|
const output = parsed.bools.has("--json")
|
|
489
554
|
? JSON.stringify(
|
|
490
|
-
{
|
|
555
|
+
{
|
|
556
|
+
branch: ctx.review.branch,
|
|
557
|
+
summary,
|
|
558
|
+
jevChecked,
|
|
559
|
+
jevReason,
|
|
560
|
+
applied,
|
|
561
|
+
entries,
|
|
562
|
+
},
|
|
491
563
|
null,
|
|
492
564
|
2,
|
|
493
565
|
)
|
|
@@ -497,10 +569,11 @@ async function executeVerify(
|
|
|
497
569
|
jevReason,
|
|
498
570
|
}),
|
|
499
571
|
"",
|
|
500
|
-
`Relatório: ${
|
|
572
|
+
`Relatório: ${location} (${VERIFY_FILE})`,
|
|
501
573
|
parsed.bools.has("--apply")
|
|
502
574
|
? `Aplicados (done): ${applied.length > 0 ? applied.join(", ") : "—"}.`
|
|
503
575
|
: "",
|
|
576
|
+
threads,
|
|
504
577
|
]
|
|
505
578
|
.filter(Boolean)
|
|
506
579
|
.join("\n");
|
|
@@ -570,6 +643,21 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
570
643
|
"",
|
|
571
644
|
`Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
|
|
572
645
|
];
|
|
646
|
+
const review = outcome.review;
|
|
647
|
+
if (review?.id !== undefined) {
|
|
648
|
+
const target = review.url ? ` — ${review.url}` : "";
|
|
649
|
+
if (review.created) {
|
|
650
|
+
const status = review.finalized
|
|
651
|
+
? `finalizada (${review.event})`
|
|
652
|
+
: "criada, mas não finalizada";
|
|
653
|
+
lines.push(`Revisão #${review.id} ${status}${target}`);
|
|
654
|
+
} else {
|
|
655
|
+
lines.push(
|
|
656
|
+
`Comentários adicionados à revisão pendente #${review.id}${target}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (review?.error) lines.push(`Revisão: ❌ ${review.error}`);
|
|
573
661
|
if (outcome.general) {
|
|
574
662
|
lines.push(
|
|
575
663
|
`Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
|
|
@@ -582,16 +670,19 @@ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
|
582
670
|
}
|
|
583
671
|
|
|
584
672
|
/**
|
|
585
|
-
* Subcomando `findings submit` — envia os findings selecionados
|
|
586
|
-
*
|
|
673
|
+
* Subcomando `findings submit` — envia os findings selecionados numa revisão da
|
|
674
|
+
* PR (via `gh`): reaproveita a revisão pendente ou cria/finaliza uma nova.
|
|
675
|
+
* `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
|
|
587
676
|
*/
|
|
588
677
|
export async function runSubmitCommand(
|
|
589
678
|
config: HoundsmanConfig,
|
|
679
|
+
storage: StorageProvider,
|
|
680
|
+
repo: RepoIdentity,
|
|
590
681
|
args: string[],
|
|
591
682
|
): Promise<FindingsCommandResult> {
|
|
592
|
-
const parsed = parseArgs(args);
|
|
593
683
|
try {
|
|
594
|
-
|
|
684
|
+
const parsed = parseArgs(args);
|
|
685
|
+
return await executeSubmit(config, storage, repo, parsed);
|
|
595
686
|
} catch (error) {
|
|
596
687
|
return {
|
|
597
688
|
output: error instanceof Error ? error.message : String(error),
|
|
@@ -601,10 +692,13 @@ export async function runSubmitCommand(
|
|
|
601
692
|
}
|
|
602
693
|
|
|
603
694
|
async function executeSubmit(
|
|
604
|
-
|
|
695
|
+
_config: HoundsmanConfig,
|
|
696
|
+
storage: StorageProvider,
|
|
697
|
+
repo: RepoIdentity,
|
|
605
698
|
parsed: ParsedArgs,
|
|
606
699
|
): Promise<FindingsCommandResult> {
|
|
607
|
-
const
|
|
700
|
+
const ctx = openContext(storage, repo, parsed);
|
|
701
|
+
const state = loadState(ctx);
|
|
608
702
|
const selection = submitSelection(parsed);
|
|
609
703
|
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
610
704
|
const preview = buildSubmitPreview(state, target, selection);
|
|
@@ -621,14 +715,11 @@ async function executeSubmit(
|
|
|
621
715
|
runner: defaultGhRunner,
|
|
622
716
|
selection,
|
|
623
717
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
state.branch,
|
|
627
|
-
).write(state);
|
|
628
|
-
pingStatus(config, parsed);
|
|
718
|
+
ctx.storage.writeFindings(ctx.review.id, state);
|
|
719
|
+
pingStatus(ctx, state);
|
|
629
720
|
|
|
630
721
|
return {
|
|
631
722
|
output: renderSubmitOutcome(outcome, json),
|
|
632
|
-
exitCode: outcome.failed.length > 0 ? 1 : 0,
|
|
723
|
+
exitCode: outcome.failed.length > 0 || Boolean(outcome.review?.error) ? 1 : 0,
|
|
633
724
|
};
|
|
634
725
|
}
|
|
@@ -6,24 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
import { ArgParser } from "../arg-parser";
|
|
8
8
|
import { defaultOptions, type ReviewOptions } from "../orchestrator";
|
|
9
|
-
import { ARTIFACT_DIR } from "../paths";
|
|
10
9
|
|
|
11
10
|
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
12
|
-
export function parseReviewArgs(
|
|
13
|
-
args: string[],
|
|
14
|
-
artifactDir: string = ARTIFACT_DIR,
|
|
15
|
-
): ReviewOptions {
|
|
11
|
+
export function parseReviewArgs(args: string[]): ReviewOptions {
|
|
16
12
|
const parser = ArgParser.from(args);
|
|
17
|
-
|
|
13
|
+
if (parser.has("--dir")) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"--dir foi removido; configure `storage` no houndsman.config.ts.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const opts = defaultOptions();
|
|
18
19
|
|
|
19
20
|
const base = parser.value("--base");
|
|
20
21
|
const head = parser.value("--head");
|
|
21
22
|
const branch = parser.value("--branch");
|
|
22
|
-
const dir = parser.value("--dir");
|
|
23
23
|
if (base) opts.base = base;
|
|
24
24
|
if (head) opts.head = head;
|
|
25
25
|
if (branch) opts.branch = branch;
|
|
26
|
-
if (dir) opts.dir = dir;
|
|
27
26
|
|
|
28
27
|
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
29
28
|
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|