@payconductor/houndsman 0.1.4 → 0.3.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/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -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 +22 -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 +43 -0
- package/src/cli/index.ts +62 -260
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +163 -147
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- 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 +9 -19
- 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 +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +189 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +423 -0
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -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 +112 -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 +108 -0
- package/src/core/run-status.ts +6 -11
- 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 +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- 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 +316 -222
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -0
- package/src/tests/cli.test.ts +167 -0
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/github-review.test.ts +320 -0
- package/src/tests/infrastructure.test.ts +210 -0
- package/src/tests/server.test.ts +117 -0
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
type FindingsFilter,
|
|
28
28
|
type FindingsStateFile,
|
|
29
29
|
FindingsStateStore,
|
|
30
|
-
type FindingsSummary,
|
|
31
30
|
type FindingUpdate,
|
|
32
31
|
fileReader,
|
|
33
32
|
filterFindings,
|
|
@@ -35,22 +34,24 @@ import {
|
|
|
35
34
|
summarize,
|
|
36
35
|
syncState,
|
|
37
36
|
} from "../findings-state";
|
|
38
|
-
import {
|
|
39
|
-
|
|
37
|
+
import {
|
|
38
|
+
buildSubmitPreview,
|
|
39
|
+
defaultGhRunner,
|
|
40
|
+
resolvePrTarget,
|
|
41
|
+
type SubmitOutcome,
|
|
42
|
+
type SubmitPreview,
|
|
43
|
+
type SubmitSelection,
|
|
44
|
+
submitFindings,
|
|
45
|
+
} from "../github-review";
|
|
40
46
|
import { relFromRoot, VERIFY_FILE } from "../paths";
|
|
41
47
|
import { currentBranch } from "../pr-context";
|
|
48
|
+
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
49
|
+
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
50
|
+
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
42
51
|
import { pingFindingsStatus } from "../run-status";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
applyResolutions,
|
|
47
|
-
selectVerifiable,
|
|
48
|
-
summarizeVerify,
|
|
49
|
-
type VerifyEntry,
|
|
50
|
-
type VerifyStatus,
|
|
51
|
-
type VerifySummary,
|
|
52
|
-
verifyLocal,
|
|
53
|
-
} from "../verify";
|
|
52
|
+
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
53
|
+
import { Verifier } from "../services/verifier";
|
|
54
|
+
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
54
55
|
|
|
55
56
|
export type FindingsCommandResult = {
|
|
56
57
|
output: string;
|
|
@@ -133,90 +134,16 @@ function loadState(config: HoundsmanConfig, parsed: ParsedArgs): FindingsStateFi
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
// ---------------------------------------------------------------------------
|
|
136
|
-
//
|
|
137
|
+
// Apresentação
|
|
137
138
|
// ---------------------------------------------------------------------------
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function renderFindingLine(finding: FindingState): string[] {
|
|
145
|
-
const emoji = SEVERITY_EMOJI[finding.severity];
|
|
146
|
-
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
147
|
-
const flags = `${decisionLabel(finding.decision)}/${finding.execState}`;
|
|
148
|
-
const count = finding.count > 1 ? ` ×${finding.count}` : "";
|
|
149
|
-
const lines = [
|
|
150
|
-
`- \`${finding.id}\` ${emoji} **${finding.severity}** \`${location}\` — ${finding.rule}${count}`,
|
|
151
|
-
` ${finding.message}`,
|
|
152
|
-
` _${flags}_ · watchdogs: ${finding.watchdogIds.join(", ") || "—"}`,
|
|
153
|
-
];
|
|
154
|
-
if (finding.comment) lines.push(` **Comentário:** ${finding.comment}`);
|
|
155
|
-
return lines;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function renderList(state: FindingsStateFile, findings: FindingState[]): string {
|
|
159
|
-
if (findings.length === 0) return "Nenhum finding para o filtro informado.";
|
|
160
|
-
const lines = [`# Findings — ${state.branch} (${findings.length})`, ""];
|
|
161
|
-
for (const finding of findings) lines.push(...renderFindingLine(finding));
|
|
162
|
-
return lines.join("\n");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function renderSummary(state: FindingsStateFile, summary: FindingsSummary): string {
|
|
166
|
-
const decisions = FINDING_DECISIONS.map((d) => `${d} ${summary.byDecision[d]}`).join(
|
|
167
|
-
" · ",
|
|
168
|
-
);
|
|
169
|
-
const exec = FINDING_EXEC_STATES.map((e) => `${e} ${summary.byExecState[e]}`).join(
|
|
170
|
-
" · ",
|
|
171
|
-
);
|
|
172
|
-
const severities = SEVERITY_ORDER.map((s) => `${s} ${summary.bySeverity[s]}`).join(
|
|
173
|
-
" · ",
|
|
174
|
-
);
|
|
175
|
-
return [
|
|
176
|
-
`# Triagem — ${state.branch}`,
|
|
177
|
-
"",
|
|
178
|
-
`Total: ${summary.total} (presentes: ${summary.present})`,
|
|
179
|
-
`Decisões: ${decisions}`,
|
|
180
|
-
`Execução: ${exec}`,
|
|
181
|
-
`Severidade: ${severities}`,
|
|
182
|
-
].join("\n");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Mix de severidade compacto (ex.: `❌1 ⚠️2`). */
|
|
186
|
-
function severityBreakdown(bySeverity: Record<Severity, number>): string {
|
|
187
|
-
return SEVERITY_ORDER.filter((severity) => bySeverity[severity] > 0)
|
|
188
|
-
.map((severity) => `${SEVERITY_EMOJI[severity]}${bySeverity[severity]}`)
|
|
189
|
-
.join(" ");
|
|
190
|
-
}
|
|
140
|
+
const presenters = {
|
|
141
|
+
findings: new FindingsPresenter(),
|
|
142
|
+
batch: new BatchPresenter(),
|
|
143
|
+
verify: new VerifyPresenter(),
|
|
144
|
+
};
|
|
191
145
|
|
|
192
|
-
|
|
193
|
-
function renderBatch(result: FixBatchResult): string {
|
|
194
|
-
const { dimension, current, others, totals } = result;
|
|
195
|
-
if (totals.queue === 0) return "Nenhum finding pendente de correção.";
|
|
196
|
-
|
|
197
|
-
const lines = [
|
|
198
|
-
`# Lote de correção — agrupado por ${dimension} (${totals.groups} grupo(s))`,
|
|
199
|
-
"",
|
|
200
|
-
`## Grupo atual — ${current.key} (${current.total} finding(s), mostrando ${current.returned})`,
|
|
201
|
-
];
|
|
202
|
-
for (const finding of current.findings) lines.push(...renderFindingLine(finding));
|
|
203
|
-
if (current.remaining > 0) {
|
|
204
|
-
lines.push("", `_… +${current.remaining} neste grupo (use \`--offset\`)._`);
|
|
205
|
-
}
|
|
206
|
-
if (others.length > 0) {
|
|
207
|
-
lines.push("", "## Outros grupos disponíveis");
|
|
208
|
-
for (const group of others) {
|
|
209
|
-
lines.push(
|
|
210
|
-
`- ${group.key} — ${group.count} (${severityBreakdown(group.bySeverity)})`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
lines.push(
|
|
215
|
-
"",
|
|
216
|
-
`fila: ${totals.queue} · retornados: ${totals.returned} · grupos: ${totals.groups}`,
|
|
217
|
-
);
|
|
218
|
-
return lines.join("\n");
|
|
219
|
-
}
|
|
146
|
+
const fixBatchBuilder = new FixBatchBuilder();
|
|
220
147
|
|
|
221
148
|
// ---------------------------------------------------------------------------
|
|
222
149
|
// Execução
|
|
@@ -331,7 +258,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
331
258
|
return {
|
|
332
259
|
output: parsed.bools.has("--json")
|
|
333
260
|
? JSON.stringify(findings, null, 2)
|
|
334
|
-
:
|
|
261
|
+
: presenters.findings.list(state, findings),
|
|
335
262
|
exitCode: 0,
|
|
336
263
|
};
|
|
337
264
|
}
|
|
@@ -344,7 +271,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
344
271
|
return {
|
|
345
272
|
output: parsed.bools.has("--json")
|
|
346
273
|
? JSON.stringify(finding, null, 2)
|
|
347
|
-
:
|
|
274
|
+
: presenters.findings.list(state, [finding]),
|
|
348
275
|
exitCode: 0,
|
|
349
276
|
};
|
|
350
277
|
}
|
|
@@ -416,7 +343,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
416
343
|
return {
|
|
417
344
|
output: parsed.bools.has("--json")
|
|
418
345
|
? JSON.stringify(summary, null, 2)
|
|
419
|
-
:
|
|
346
|
+
: presenters.findings.summary(state, summary),
|
|
420
347
|
exitCode: 0,
|
|
421
348
|
};
|
|
422
349
|
}
|
|
@@ -429,7 +356,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
429
356
|
const state = loadState(config, parsed);
|
|
430
357
|
const severity = parsed.values["--severity"];
|
|
431
358
|
const offset = Number(parsed.values["--offset"]);
|
|
432
|
-
const result =
|
|
359
|
+
const result = fixBatchBuilder.build(state, {
|
|
433
360
|
severity: severity ? (severity as Severity) : undefined,
|
|
434
361
|
includeDone: parsed.bools.has("--include-done"),
|
|
435
362
|
offset: Number.isFinite(offset) ? offset : undefined,
|
|
@@ -438,7 +365,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
438
365
|
return {
|
|
439
366
|
output: parsed.bools.has("--json")
|
|
440
367
|
? JSON.stringify(result, null, 2)
|
|
441
|
-
:
|
|
368
|
+
: presenters.batch.render(result),
|
|
442
369
|
exitCode: 0,
|
|
443
370
|
};
|
|
444
371
|
}
|
|
@@ -478,43 +405,6 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
478
405
|
// verify — as violações foram resolvidas? (sem re-revisar a PR)
|
|
479
406
|
// ---------------------------------------------------------------------------
|
|
480
407
|
|
|
481
|
-
/** Rótulo curto de cada status de verificação. */
|
|
482
|
-
const VERIFY_LABEL: Record<VerifyStatus, string> = {
|
|
483
|
-
resolved: "✅ resolvido",
|
|
484
|
-
persisting: "❌ persiste",
|
|
485
|
-
uncertain: "⚠️ incerto",
|
|
486
|
-
"file-missing": "🗑️ arquivo ausente",
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
/** Render da verificação: contagem por status + uma linha por finding. */
|
|
490
|
-
function renderVerify(
|
|
491
|
-
state: FindingsStateFile,
|
|
492
|
-
entries: VerifyEntry[],
|
|
493
|
-
summary: VerifySummary,
|
|
494
|
-
meta: { jevChecked: number; jevReason?: string },
|
|
495
|
-
): string {
|
|
496
|
-
if (entries.length === 0) return "Nenhum finding para verificar.";
|
|
497
|
-
const lines = [
|
|
498
|
-
`# Verificação — ${state.branch} (${entries.length})`,
|
|
499
|
-
"",
|
|
500
|
-
`Resolvidos: ${summary.resolved} · Persistem: ${summary.persisting} · Incertos: ${summary.uncertain} · Arquivo ausente: ${summary["file-missing"]}`,
|
|
501
|
-
];
|
|
502
|
-
if (meta.jevChecked > 0) lines.push(`Confirmados por Jev: ${meta.jevChecked}.`);
|
|
503
|
-
else if (meta.jevReason) lines.push(`Jev: pulado (${meta.jevReason}).`);
|
|
504
|
-
lines.push("");
|
|
505
|
-
for (const entry of entries) {
|
|
506
|
-
const location = entry.line ? `${entry.file}:${entry.line}` : entry.file;
|
|
507
|
-
const via =
|
|
508
|
-
entry.basis === "jev" ? ` · Jev ${(entry.confidence ?? 0).toFixed(2)}` : "";
|
|
509
|
-
lines.push(
|
|
510
|
-
`- \`${entry.id}\` ${VERIFY_LABEL[entry.status]} — **${entry.severity}** \`${location}\` — ${entry.rule}${via}`,
|
|
511
|
-
` ${entry.message}`,
|
|
512
|
-
);
|
|
513
|
-
if (entry.detail) lines.push(` _${entry.detail}_`);
|
|
514
|
-
}
|
|
515
|
-
return lines.join("\n");
|
|
516
|
-
}
|
|
517
|
-
|
|
518
408
|
/**
|
|
519
409
|
* Subcomando `findings verify` — checa se as violações foram resolvidas, sem
|
|
520
410
|
* re-revisar a PR. Tier local (grátis) + confirmação Jev dos casos ambíguos
|
|
@@ -543,22 +433,26 @@ async function executeVerify(
|
|
|
543
433
|
const store = FindingsStateStore.at(dir, branch);
|
|
544
434
|
const state = loadState(config, parsed);
|
|
545
435
|
|
|
546
|
-
|
|
547
|
-
const
|
|
436
|
+
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
437
|
+
const verifier = new Verifier();
|
|
438
|
+
const { JevCandidatesStore, JevClient, JevVerifier } = await import("../jev");
|
|
439
|
+
|
|
440
|
+
const findings = verifier.select(state, filterFrom(parsed));
|
|
441
|
+
const entries = verifier.verifyLocal(findings, fileReader(process.cwd()));
|
|
548
442
|
|
|
549
443
|
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
550
444
|
let jevChecked = 0;
|
|
551
445
|
let jevReason: string | undefined;
|
|
552
446
|
const ambiguous = entries.filter((entry) => entry.ambiguous);
|
|
553
447
|
if (ambiguous.length > 0) {
|
|
554
|
-
const
|
|
448
|
+
const jevVerifier = new JevVerifier(new JevClient());
|
|
555
449
|
if (parsed.bools.has("--no-jev")) {
|
|
556
450
|
jevReason = "desativado (--no-jev)";
|
|
557
|
-
} else if (!
|
|
451
|
+
} else if (!jevVerifier.isConfigured()) {
|
|
558
452
|
jevReason = "TYPESAFE_API_KEY não configurada";
|
|
559
453
|
} else {
|
|
560
454
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
561
|
-
const outcome = await
|
|
455
|
+
const outcome = await jevVerifier.verify(
|
|
562
456
|
ambiguous.map((entry) => ({
|
|
563
457
|
id: entry.id,
|
|
564
458
|
file: entry.file,
|
|
@@ -570,23 +464,23 @@ async function executeVerify(
|
|
|
570
464
|
concurrency: resolveJevConcurrency(config),
|
|
571
465
|
},
|
|
572
466
|
);
|
|
573
|
-
applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
467
|
+
verifier.applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
574
468
|
jevChecked = outcome.checked;
|
|
575
469
|
jevReason = outcome.reason;
|
|
576
470
|
}
|
|
577
471
|
}
|
|
578
472
|
|
|
579
|
-
const summary =
|
|
473
|
+
const summary = verifier.summarize(entries);
|
|
580
474
|
const reportPath = join(runDir(dir, branch), VERIFY_FILE);
|
|
581
475
|
writeFileSync(
|
|
582
476
|
reportPath,
|
|
583
|
-
`${
|
|
477
|
+
`${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
584
478
|
);
|
|
585
479
|
|
|
586
480
|
let applied: string[] = [];
|
|
587
481
|
if (parsed.bools.has("--apply")) {
|
|
588
482
|
store.update((current) => {
|
|
589
|
-
applied = applyResolutions(current, entries);
|
|
483
|
+
applied = verifier.applyResolutions(current, entries);
|
|
590
484
|
});
|
|
591
485
|
pingStatus(config, parsed);
|
|
592
486
|
}
|
|
@@ -598,7 +492,10 @@ async function executeVerify(
|
|
|
598
492
|
2,
|
|
599
493
|
)
|
|
600
494
|
: [
|
|
601
|
-
|
|
495
|
+
presenters.verify.render(state, entries, summary, {
|
|
496
|
+
jevChecked,
|
|
497
|
+
jevReason,
|
|
498
|
+
}),
|
|
602
499
|
"",
|
|
603
500
|
`Relatório: ${relFromRoot(reportPath)}`,
|
|
604
501
|
parsed.bools.has("--apply")
|
|
@@ -616,3 +513,122 @@ async function executeVerify(
|
|
|
616
513
|
|
|
617
514
|
return { output, exitCode: failOnPersisting ? 1 : 0 };
|
|
618
515
|
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Envio (findings submit)
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
/** Flag de lista `a,b,c` → itens não vazios. */
|
|
522
|
+
function splitList(raw: string | undefined): string[] {
|
|
523
|
+
return (raw ?? "")
|
|
524
|
+
.split(",")
|
|
525
|
+
.map((value) => value.trim())
|
|
526
|
+
.filter(Boolean);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Seleção do envio a partir das flags (`--severity`, `--exec`, `--include-sent`). */
|
|
530
|
+
function submitSelection(parsed: ParsedArgs): SubmitSelection {
|
|
531
|
+
const severities = splitList(parsed.values["--severity"]).filter(
|
|
532
|
+
(value): value is Severity =>
|
|
533
|
+
(SEVERITY_ORDER as readonly string[]).includes(value),
|
|
534
|
+
);
|
|
535
|
+
const execStates = splitList(parsed.values["--exec"]).filter(
|
|
536
|
+
(value): value is FindingExecState =>
|
|
537
|
+
(FINDING_EXEC_STATES as readonly string[]).includes(value),
|
|
538
|
+
);
|
|
539
|
+
return {
|
|
540
|
+
severities: severities.length > 0 ? severities : undefined,
|
|
541
|
+
execStates: execStates.length > 0 ? execStates : undefined,
|
|
542
|
+
includeSent: parsed.bools.has("--include-sent"),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Render da prévia do envio (markdown ou JSON). */
|
|
547
|
+
function renderSubmitPreview(preview: SubmitPreview, json: boolean): string {
|
|
548
|
+
if (json) return JSON.stringify(preview, null, 2);
|
|
549
|
+
const totals = preview.totals;
|
|
550
|
+
if (!preview.available) {
|
|
551
|
+
return [
|
|
552
|
+
`PR indisponível para envio: ${preview.reason}`,
|
|
553
|
+
`Prévia local: ${totals.candidates} candidato(s) — ${totals.line} na linha · ${totals.file} no arquivo · ${totals.sent} já enviado(s).`,
|
|
554
|
+
].join("\n");
|
|
555
|
+
}
|
|
556
|
+
const lines = [
|
|
557
|
+
`# Envio da revisão — PR #${preview.target?.number} (${preview.target?.repo})`,
|
|
558
|
+
"",
|
|
559
|
+
`A enviar: ${totals.toSend} (${totals.line} na linha · ${totals.file} no arquivo) · já enviados: ${totals.sent}`,
|
|
560
|
+
];
|
|
561
|
+
if (totals.toSend === 0) lines.push("", "Nada a enviar.");
|
|
562
|
+
return lines.join("\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Render do resultado do envio (markdown ou JSON). */
|
|
566
|
+
function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
|
|
567
|
+
if (json) return JSON.stringify(outcome, null, 2);
|
|
568
|
+
const lines = [
|
|
569
|
+
"# Envio da revisão",
|
|
570
|
+
"",
|
|
571
|
+
`Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
|
|
572
|
+
];
|
|
573
|
+
if (outcome.general) {
|
|
574
|
+
lines.push(
|
|
575
|
+
`Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
for (const failure of outcome.failed) {
|
|
579
|
+
lines.push(`- ❌ \`${failure.id}\`: ${failure.reason}`);
|
|
580
|
+
}
|
|
581
|
+
return lines.join("\n");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Subcomando `findings submit` — envia os findings selecionados como comentários
|
|
586
|
+
* na PR (via `gh`). `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
|
|
587
|
+
*/
|
|
588
|
+
export async function runSubmitCommand(
|
|
589
|
+
config: HoundsmanConfig,
|
|
590
|
+
args: string[],
|
|
591
|
+
): Promise<FindingsCommandResult> {
|
|
592
|
+
const parsed = parseArgs(args);
|
|
593
|
+
try {
|
|
594
|
+
return await executeSubmit(config, parsed);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return {
|
|
597
|
+
output: error instanceof Error ? error.message : String(error),
|
|
598
|
+
exitCode: 1,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function executeSubmit(
|
|
604
|
+
config: HoundsmanConfig,
|
|
605
|
+
parsed: ParsedArgs,
|
|
606
|
+
): Promise<FindingsCommandResult> {
|
|
607
|
+
const state = loadState(config, parsed);
|
|
608
|
+
const selection = submitSelection(parsed);
|
|
609
|
+
const target = resolvePrTarget(defaultGhRunner, state.branch);
|
|
610
|
+
const preview = buildSubmitPreview(state, target, selection);
|
|
611
|
+
const json = parsed.bools.has("--json");
|
|
612
|
+
|
|
613
|
+
// Prévia explícita, ou alvo indisponível (fail-open) — nada a postar.
|
|
614
|
+
if (parsed.bools.has("--dry-run") || !target.ok) {
|
|
615
|
+
return { output: renderSubmitPreview(preview, json), exitCode: 0 };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const outcome = submitFindings({
|
|
619
|
+
state,
|
|
620
|
+
target: target.target,
|
|
621
|
+
runner: defaultGhRunner,
|
|
622
|
+
selection,
|
|
623
|
+
});
|
|
624
|
+
FindingsStateStore.at(
|
|
625
|
+
parsed.values["--dir"] ?? resolveArtifactDir(config),
|
|
626
|
+
state.branch,
|
|
627
|
+
).write(state);
|
|
628
|
+
pingStatus(config, parsed);
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
output: renderSubmitOutcome(outcome, json),
|
|
632
|
+
exitCode: outcome.failed.length > 0 ? 1 : 0,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
@@ -4,49 +4,32 @@
|
|
|
4
4
|
* Biblioteca: NÃO usa `process.argv`/`process.exit` (o CLI decide o exit code).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { ArgParser } from "../arg-parser";
|
|
7
8
|
import { defaultOptions, type ReviewOptions } from "../orchestrator";
|
|
8
9
|
import { ARTIFACT_DIR } from "../paths";
|
|
9
10
|
|
|
10
|
-
const VALUE_FLAGS = new Set(["--base", "--head", "--branch", "--dir"]);
|
|
11
|
-
const BOOLEAN_FLAGS = new Set([
|
|
12
|
-
"--finalize",
|
|
13
|
-
"--report-only",
|
|
14
|
-
"--fail-on-blocking",
|
|
15
|
-
"--clean",
|
|
16
|
-
"--no-jev",
|
|
17
|
-
"--full",
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
function applyValue(opts: ReviewOptions, flag: string, value: string | undefined): void {
|
|
21
|
-
if (!value) return;
|
|
22
|
-
if (flag === "--base") opts.base = value;
|
|
23
|
-
else if (flag === "--head") opts.head = value;
|
|
24
|
-
else if (flag === "--branch") opts.branch = value;
|
|
25
|
-
else if (flag === "--dir") opts.dir = value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function applyBoolean(opts: ReviewOptions, flag: string): void {
|
|
29
|
-
if (flag === "--finalize" || flag === "--report-only") opts.finalize = true;
|
|
30
|
-
else if (flag === "--fail-on-blocking") opts.failOnBlocking = true;
|
|
31
|
-
else if (flag === "--clean") opts.clean = true;
|
|
32
|
-
else if (flag === "--no-jev") opts.noJev = true;
|
|
33
|
-
else if (flag === "--full") opts.full = true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
11
|
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
37
12
|
export function parseReviewArgs(
|
|
38
13
|
args: string[],
|
|
39
14
|
artifactDir: string = ARTIFACT_DIR,
|
|
40
15
|
): ReviewOptions {
|
|
16
|
+
const parser = ArgParser.from(args);
|
|
41
17
|
const opts = defaultOptions(artifactDir);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
18
|
+
|
|
19
|
+
const base = parser.value("--base");
|
|
20
|
+
const head = parser.value("--head");
|
|
21
|
+
const branch = parser.value("--branch");
|
|
22
|
+
const dir = parser.value("--dir");
|
|
23
|
+
if (base) opts.base = base;
|
|
24
|
+
if (head) opts.head = head;
|
|
25
|
+
if (branch) opts.branch = branch;
|
|
26
|
+
if (dir) opts.dir = dir;
|
|
27
|
+
|
|
28
|
+
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
29
|
+
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
30
|
+
if (parser.has("--clean")) opts.clean = true;
|
|
31
|
+
if (parser.has("--no-jev")) opts.noJev = true;
|
|
32
|
+
if (parser.has("--full")) opts.full = true;
|
|
33
|
+
|
|
51
34
|
return opts;
|
|
52
35
|
}
|