@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.
Files changed (84) hide show
  1. package/package.json +1 -1
  2. package/src/cli/arg-parser.ts +8 -0
  3. package/src/cli/commands/command.ts +23 -0
  4. package/src/cli/commands/context-command.ts +38 -0
  5. package/src/cli/commands/doctor-command.ts +55 -0
  6. package/src/cli/commands/findings-command.ts +28 -0
  7. package/src/cli/commands/help-command.ts +13 -0
  8. package/src/cli/commands/registry.ts +34 -0
  9. package/src/cli/commands/review-command.ts +22 -0
  10. package/src/cli/commands/server-command.ts +37 -0
  11. package/src/cli/commands/setup-command.ts +34 -0
  12. package/src/cli/commands/watchdog-command.ts +34 -0
  13. package/src/cli/help.ts +43 -0
  14. package/src/cli/index.ts +62 -260
  15. package/src/core/arg-parser.ts +48 -0
  16. package/src/core/commands/findings.ts +163 -147
  17. package/src/core/commands/review.ts +18 -35
  18. package/src/core/commands/scaffold.ts +174 -64
  19. package/src/core/commands/setup.ts +16 -11
  20. package/src/core/commands/update.ts +7 -7
  21. package/src/core/commands/watchdog.ts +8 -80
  22. package/src/core/config.ts +9 -19
  23. package/src/core/defaults.ts +20 -0
  24. package/src/core/diff-source.ts +1 -13
  25. package/src/core/doc-discoverer.ts +25 -15
  26. package/src/core/domain/budget.ts +42 -0
  27. package/src/core/domain/classification.ts +94 -0
  28. package/src/core/domain/diff.ts +53 -0
  29. package/src/core/domain/watchdog.ts +96 -0
  30. package/src/core/engine/carryover.ts +67 -0
  31. package/src/core/engine/houndsman-engine.ts +51 -0
  32. package/src/core/engine/review-scope.ts +95 -0
  33. package/src/core/engine/review-session.ts +51 -0
  34. package/src/core/file-classifier.ts +11 -67
  35. package/src/core/findings-state.ts +189 -38
  36. package/src/core/git-revision.ts +14 -40
  37. package/src/core/git-runner.ts +46 -0
  38. package/src/core/github-review.ts +423 -0
  39. package/src/core/orchestrator.ts +116 -337
  40. package/src/core/paths.ts +9 -0
  41. package/src/core/pr-context.ts +28 -54
  42. package/src/core/project-config.ts +57 -0
  43. package/src/core/renderers/batch-presenter.ts +40 -0
  44. package/src/core/renderers/brief-renderer.ts +23 -0
  45. package/src/core/renderers/findings-presenter.ts +74 -0
  46. package/src/core/renderers/manifest-writer.ts +112 -0
  47. package/src/core/renderers/report-renderer.ts +66 -0
  48. package/src/core/renderers/verify-presenter.ts +50 -0
  49. package/src/core/renderers/watchdog-presenter.ts +100 -0
  50. package/src/core/report-builder.ts +12 -7
  51. package/src/core/run-layout.ts +108 -0
  52. package/src/core/run-status.ts +6 -11
  53. package/src/core/services/clock.ts +12 -0
  54. package/src/core/services/file-classifier.ts +34 -0
  55. package/src/core/services/fix-batch-builder.ts +13 -0
  56. package/src/core/services/gh-client.ts +54 -0
  57. package/src/core/services/git-repository.ts +123 -0
  58. package/src/core/services/project-files.ts +79 -0
  59. package/src/core/services/verifier.ts +58 -0
  60. package/src/core/verify.ts +2 -1
  61. package/src/core/watchdog-registry.ts +58 -16
  62. package/src/index.ts +31 -1
  63. package/src/server/client/App.tsx +174 -49
  64. package/src/server/client/ArtifactsPanel.tsx +2 -2
  65. package/src/server/client/AutoRefresh.tsx +38 -0
  66. package/src/server/client/Filters.tsx +72 -53
  67. package/src/server/client/FindingCard.tsx +212 -71
  68. package/src/server/client/FindingsList.tsx +8 -6
  69. package/src/server/client/RunList.tsx +30 -7
  70. package/src/server/client/SubmitModal.tsx +418 -0
  71. package/src/server/client/SummaryBar.tsx +65 -12
  72. package/src/server/client/constants.ts +9 -12
  73. package/src/server/client/format.ts +40 -0
  74. package/src/server/client/icons.tsx +143 -0
  75. package/src/server/client/types.ts +69 -0
  76. package/src/server/index.ts +316 -222
  77. package/src/server/run-repository.ts +291 -0
  78. package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
  79. package/src/tests/baseline.test.ts +513 -0
  80. package/src/tests/cli.test.ts +167 -0
  81. package/src/tests/domain.test.ts +328 -0
  82. package/src/tests/github-review.test.ts +320 -0
  83. package/src/tests/infrastructure.test.ts +210 -0
  84. 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 { buildFixBatch, type FixBatchResult } from "../fix-batch";
39
- import { JevCandidatesStore, JevClient, JevVerifier } from "../jev";
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 { SEVERITY_EMOJI, SEVERITY_ORDER, type Severity } from "../types";
44
- import {
45
- applyJev,
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
- // Render
137
+ // Apresentação
137
138
  // ---------------------------------------------------------------------------
138
139
 
139
- function decisionLabel(decision: FindingDecision): string {
140
- return decision;
141
- }
142
-
143
- /** Uma linha compacta por finding (uso no agente). */
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
- /** Render do lote de correção: grupo atual paginado + outros grupos. */
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
- : renderList(state, findings),
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
- : renderList(state, [finding]),
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
- : renderSummary(state, summary),
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 = buildFixBatch(state, {
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
- : renderBatch(result),
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
- const findings = selectVerifiable(state, filterFrom(parsed));
547
- const entries = verifyLocal(findings, fileReader(process.cwd()));
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 verifier = new JevVerifier(new JevClient());
448
+ const jevVerifier = new JevVerifier(new JevClient());
555
449
  if (parsed.bools.has("--no-jev")) {
556
450
  jevReason = "desativado (--no-jev)";
557
- } else if (!verifier.isConfigured()) {
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 verifier.verify(
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 = summarizeVerify(entries);
473
+ const summary = verifier.summarize(entries);
580
474
  const reportPath = join(runDir(dir, branch), VERIFY_FILE);
581
475
  writeFileSync(
582
476
  reportPath,
583
- `${renderVerify(state, entries, summary, { jevChecked, jevReason })}\n`,
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
- renderVerify(state, entries, summary, { jevChecked, jevReason }),
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
- for (let i = 0; i < args.length; i++) {
43
- const arg = args[i];
44
- if (VALUE_FLAGS.has(arg)) {
45
- applyValue(opts, arg, args[i + 1]);
46
- i++;
47
- } else if (BOOLEAN_FLAGS.has(arg)) {
48
- applyBoolean(opts, arg);
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
  }