@payconductor/houndsman 0.1.4 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payconductor/houndsman",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Orquestrador de revisão de Pull Requests: classifica o diff, despacha watchdogs especializados por domínio e consolida o relatório.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/index.ts CHANGED
@@ -7,11 +7,14 @@
7
7
  * cwd) e orquestra funções/classes de `core/`. Não importa nenhum caminho do
8
8
  * projeto consumidor estaticamente.
9
9
  *
10
+ * Cada comando carrega seus módulos por import dinâmico (lazy) — o startup não
11
+ * paga por `server`, Jev (axios) ou `orchestrator` que o comando não usa.
12
+ *
10
13
  * Comandos:
11
14
  * (default) Passo 1 — preparar (classificação + briefs + manifesto)
12
15
  * --finalize Passo 3 — consolidar os findings em review.md
13
16
  * context Passo 0 — coletar o contexto da PR via `gh`
14
- * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/...)
17
+ * findings <sub> Triagem/correção dos findings (list/set/done/sync/batch/verify/submit/...)
15
18
  * server Sobe a UI local de triagem (Bun.serve)
16
19
  * setup Cria a configuração inicial do projeto (config + skills)
17
20
  * update Sincroniza os arquivos auto-gerados (.houndsman/core)
@@ -23,20 +26,9 @@
23
26
  * `--config <path>` · `--clean` · `--full` · `--no-jev` · `--fail-on-blocking`.
24
27
  */
25
28
 
26
- import { runContextCommand } from "../core/commands/context";
27
- import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
28
- import { parseReviewArgs } from "../core/commands/review";
29
- import { runSetupCommand } from "../core/commands/setup";
30
- import { runLibTests } from "../core/commands/test";
31
- import { runUpdateCommand } from "../core/commands/update";
32
- import { renderWatchdogContext } from "../core/commands/watchdog";
33
29
  import { type HoundsmanConfig, resolveArtifactDir } from "../core/config";
34
30
  import { loadConfig } from "../core/config-loader";
35
- import { JevClient } from "../core/jev";
36
- import { finalize, prepare, resolveBranch } from "../core/orchestrator";
37
31
  import { relFromRoot } from "../core/paths";
38
- import { checkRegistry } from "../core/registry-check";
39
- import { startServer } from "../server";
40
32
 
41
33
  const HELP = `Uso: houndsman [comando] [flags]
42
34
 
@@ -44,7 +36,7 @@ Comandos:
44
36
  (nenhum) Prepara a revisão (classification + briefs + manifesto)
45
37
  --finalize Consolida os findings em review.md
46
38
  context Coleta o contexto da PR via gh (Passo 0)
47
- findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · path)
39
+ findings <sub> Triagem/correção dos findings (list · show · set · done · reset · sync · summary · batch · verify · submit · path)
48
40
  server Sobe a UI local de triagem (--port, --host, --open)
49
41
  setup Cria a configuração inicial do projeto (config + skills)
50
42
  update Sincroniza os arquivos auto-gerados (.houndsman/core)
@@ -77,7 +69,9 @@ Flags de findings:
77
69
  --offset <n> Paginação dentro do grupo (batch)
78
70
  --include-done Inclui já concluídos no batch
79
71
  --apply (verify) Marca os resolvidos como done
80
- --fail-on-persisting (verify) Exit 1 se algum blocking persistir`;
72
+ --fail-on-persisting (verify) Exit 1 se algum blocking persistir
73
+ --include-sent (submit) Reenvia findings já enviados à PR
74
+ --dry-run (submit) Só a prévia do envio, sem postar`;
81
75
 
82
76
  function readFlag(args: string[], flag: string): string | undefined {
83
77
  const index = args.indexOf(flag);
@@ -85,7 +79,8 @@ function readFlag(args: string[], flag: string): string | undefined {
85
79
  }
86
80
 
87
81
  /** Passo 0 — contexto da PR via `gh`. */
88
- function runContext(config: HoundsmanConfig, args: string[]): void {
82
+ async function runContext(config: HoundsmanConfig, args: string[]): Promise<void> {
83
+ const { runContextCommand } = await import("../core/commands/context");
89
84
  const { paths, context } = runContextCommand(config, {
90
85
  branch: readFlag(args, "--branch"),
91
86
  dir: readFlag(args, "--dir"),
@@ -114,6 +109,7 @@ function runContext(config: HoundsmanConfig, args: string[]): void {
114
109
 
115
110
  /** Valida a configuração do projeto e o ambiente. */
116
111
  async function runDoctor(config: HoundsmanConfig): Promise<void> {
112
+ const { checkRegistry } = await import("../core/registry-check");
117
113
  console.log("→ Validando configuração do projeto...");
118
114
  const { problems, warnings, jevQuestions } = checkRegistry(config);
119
115
 
@@ -142,6 +138,7 @@ async function runDoctor(config: HoundsmanConfig): Promise<void> {
142
138
  * Fail-open — nunca altera o exit code (`doctor` é diagnóstico).
143
139
  */
144
140
  async function reportJevCredential(): Promise<void> {
141
+ const { JevClient } = await import("../core/jev");
145
142
  const client = new JevClient();
146
143
  if (!client.isConfigured()) {
147
144
  console.log(" ⚠️ TYPESAFE_API_KEY (opcional — pré-análise Jev)");
@@ -158,7 +155,8 @@ async function reportJevCredential(): Promise<void> {
158
155
  }
159
156
 
160
157
  /** Contexto de um watchdog. */
161
- function runWatchdog(config: HoundsmanConfig, args: string[]): void {
158
+ async function runWatchdog(config: HoundsmanConfig, args: string[]): Promise<void> {
159
+ const { renderWatchdogContext } = await import("../core/commands/watchdog");
162
160
  const id = args.find((arg) => !arg.startsWith("-"));
163
161
 
164
162
  if (!id || renderWatchdogContext(config, id) === null) {
@@ -185,11 +183,16 @@ function runWatchdog(config: HoundsmanConfig, args: string[]): void {
185
183
  * o JSON. O `verify` é assíncrono (consulta opcional ao Jev) — roteado à parte.
186
184
  */
187
185
  async function runFindings(config: HoundsmanConfig, args: string[]): Promise<void> {
186
+ const { runFindingsCommand, runSubmitCommand, runVerifyCommand } = await import(
187
+ "../core/commands/findings"
188
+ );
188
189
  const sub = args.find((arg) => !arg.startsWith("-"));
189
190
  const result =
190
191
  sub === "verify"
191
192
  ? await runVerifyCommand(config, args)
192
- : runFindingsCommand(config, args);
193
+ : sub === "submit"
194
+ ? await runSubmitCommand(config, args)
195
+ : runFindingsCommand(config, args);
193
196
  if (result.exitCode !== 0) {
194
197
  console.error(result.output);
195
198
  process.exit(result.exitCode);
@@ -213,7 +216,8 @@ function openBrowser(url: string): void {
213
216
  }
214
217
 
215
218
  /** Subcomando `server` — sobe a UI local de triagem. */
216
- function runServer(config: HoundsmanConfig, args: string[]): void {
219
+ async function runServer(config: HoundsmanConfig, args: string[]): Promise<void> {
220
+ const { startServer } = await import("../server");
217
221
  const port = Number(readFlag(args, "--port")) || 7331;
218
222
  const host = readFlag(args, "--host") ?? "127.0.0.1";
219
223
  const { url } = startServer({ config, port, host });
@@ -224,6 +228,9 @@ function runServer(config: HoundsmanConfig, args: string[]): void {
224
228
 
225
229
  /** Passo 1/3 — preparar ou finalizar. */
226
230
  async function runReview(config: HoundsmanConfig, args: string[]): Promise<void> {
231
+ const [{ parseReviewArgs }, { finalize, prepare, resolveBranch }] = await Promise.all(
232
+ [import("../core/commands/review"), import("../core/orchestrator")],
233
+ );
227
234
  const opts = parseReviewArgs(args, resolveArtifactDir(config));
228
235
  const branch = resolveBranch(opts);
229
236
 
@@ -246,13 +253,16 @@ export async function main(): Promise<void> {
246
253
  const rest = command ? argv.slice(1) : argv;
247
254
 
248
255
  if (command === "test") {
256
+ const { runLibTests } = await import("../core/commands/test");
249
257
  process.exit(runLibTests());
250
258
  }
251
259
  if (command === "setup") {
260
+ const { runSetupCommand } = await import("../core/commands/setup");
252
261
  await runSetupCommand({ cwd: process.cwd(), args: rest });
253
262
  return;
254
263
  }
255
264
  if (command === "update") {
265
+ const { runUpdateCommand } = await import("../core/commands/update");
256
266
  await runUpdateCommand({ cwd: process.cwd(), args: rest });
257
267
  return;
258
268
  }
@@ -268,20 +278,20 @@ export async function main(): Promise<void> {
268
278
 
269
279
  switch (command) {
270
280
  case "context":
271
- runContext(config, rest);
281
+ await runContext(config, rest);
272
282
  return;
273
283
  case "doctor":
274
284
  case "check":
275
285
  await runDoctor(config);
276
286
  return;
277
287
  case "watchdog":
278
- runWatchdog(config, rest);
288
+ await runWatchdog(config, rest);
279
289
  return;
280
290
  case "findings":
281
291
  await runFindings(config, rest);
282
292
  return;
283
293
  case "server":
284
- runServer(config, rest);
294
+ await runServer(config, rest);
285
295
  return;
286
296
  case undefined:
287
297
  await runReview(config, rest);
@@ -36,21 +36,22 @@ import {
36
36
  syncState,
37
37
  } from "../findings-state";
38
38
  import { buildFixBatch, type FixBatchResult } from "../fix-batch";
39
- import { JevCandidatesStore, JevClient, JevVerifier } from "../jev";
39
+ import {
40
+ buildSubmitPreview,
41
+ defaultGhRunner,
42
+ resolvePrTarget,
43
+ type SubmitOutcome,
44
+ type SubmitPreview,
45
+ type SubmitSelection,
46
+ submitFindings,
47
+ } from "../github-review";
40
48
  import { relFromRoot, VERIFY_FILE } from "../paths";
41
49
  import { currentBranch } from "../pr-context";
42
50
  import { pingFindingsStatus } from "../run-status";
43
51
  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
+ // Tipos são apagados no transpile (custo zero). Os **valores** de `../verify` e de
53
+ // `../jev` (axios) entram por import dinâmico — só o subcomando `verify` os usa.
54
+ import type { VerifyEntry, VerifyStatus, VerifySummary } from "../verify";
54
55
 
55
56
  export type FindingsCommandResult = {
56
57
  output: string;
@@ -543,6 +544,12 @@ async function executeVerify(
543
544
  const store = FindingsStateStore.at(dir, branch);
544
545
  const state = loadState(config, parsed);
545
546
 
547
+ // Lazy: `../verify` (leve) e `../jev` (axios) só entram no caminho do verify.
548
+ const [
549
+ { selectVerifiable, verifyLocal, summarizeVerify, applyJev, applyResolutions },
550
+ { JevCandidatesStore, JevClient, JevVerifier },
551
+ ] = await Promise.all([import("../verify"), import("../jev")]);
552
+
546
553
  const findings = selectVerifiable(state, filterFrom(parsed));
547
554
  const entries = verifyLocal(findings, fileReader(process.cwd()));
548
555
 
@@ -616,3 +623,122 @@ async function executeVerify(
616
623
 
617
624
  return { output, exitCode: failOnPersisting ? 1 : 0 };
618
625
  }
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // Envio (findings submit)
629
+ // ---------------------------------------------------------------------------
630
+
631
+ /** Flag de lista `a,b,c` → itens não vazios. */
632
+ function splitList(raw: string | undefined): string[] {
633
+ return (raw ?? "")
634
+ .split(",")
635
+ .map((value) => value.trim())
636
+ .filter(Boolean);
637
+ }
638
+
639
+ /** Seleção do envio a partir das flags (`--severity`, `--exec`, `--include-sent`). */
640
+ function submitSelection(parsed: ParsedArgs): SubmitSelection {
641
+ const severities = splitList(parsed.values["--severity"]).filter(
642
+ (value): value is Severity =>
643
+ (SEVERITY_ORDER as readonly string[]).includes(value),
644
+ );
645
+ const execStates = splitList(parsed.values["--exec"]).filter(
646
+ (value): value is FindingExecState =>
647
+ (FINDING_EXEC_STATES as readonly string[]).includes(value),
648
+ );
649
+ return {
650
+ severities: severities.length > 0 ? severities : undefined,
651
+ execStates: execStates.length > 0 ? execStates : undefined,
652
+ includeSent: parsed.bools.has("--include-sent"),
653
+ };
654
+ }
655
+
656
+ /** Render da prévia do envio (markdown ou JSON). */
657
+ function renderSubmitPreview(preview: SubmitPreview, json: boolean): string {
658
+ if (json) return JSON.stringify(preview, null, 2);
659
+ const totals = preview.totals;
660
+ if (!preview.available) {
661
+ return [
662
+ `PR indisponível para envio: ${preview.reason}`,
663
+ `Prévia local: ${totals.candidates} candidato(s) — ${totals.line} na linha · ${totals.file} no arquivo · ${totals.sent} já enviado(s).`,
664
+ ].join("\n");
665
+ }
666
+ const lines = [
667
+ `# Envio da revisão — PR #${preview.target?.number} (${preview.target?.repo})`,
668
+ "",
669
+ `A enviar: ${totals.toSend} (${totals.line} na linha · ${totals.file} no arquivo) · já enviados: ${totals.sent}`,
670
+ ];
671
+ if (totals.toSend === 0) lines.push("", "Nada a enviar.");
672
+ return lines.join("\n");
673
+ }
674
+
675
+ /** Render do resultado do envio (markdown ou JSON). */
676
+ function renderSubmitOutcome(outcome: SubmitOutcome, json: boolean): string {
677
+ if (json) return JSON.stringify(outcome, null, 2);
678
+ const lines = [
679
+ "# Envio da revisão",
680
+ "",
681
+ `Enviados: ${outcome.posted.length} · Falhas: ${outcome.failed.length} · Pulados: ${outcome.skipped.length}`,
682
+ ];
683
+ if (outcome.general) {
684
+ lines.push(
685
+ `Comentário geral: ${outcome.general.url ?? outcome.general.commentId} (${outcome.general.ids.length})`,
686
+ );
687
+ }
688
+ for (const failure of outcome.failed) {
689
+ lines.push(`- ❌ \`${failure.id}\`: ${failure.reason}`);
690
+ }
691
+ return lines.join("\n");
692
+ }
693
+
694
+ /**
695
+ * Subcomando `findings submit` — envia os findings selecionados como comentários
696
+ * na PR (via `gh`). `--dry-run` mostra a prévia sem postar. Assíncrono (gh).
697
+ */
698
+ export async function runSubmitCommand(
699
+ config: HoundsmanConfig,
700
+ args: string[],
701
+ ): Promise<FindingsCommandResult> {
702
+ const parsed = parseArgs(args);
703
+ try {
704
+ return await executeSubmit(config, parsed);
705
+ } catch (error) {
706
+ return {
707
+ output: error instanceof Error ? error.message : String(error),
708
+ exitCode: 1,
709
+ };
710
+ }
711
+ }
712
+
713
+ async function executeSubmit(
714
+ config: HoundsmanConfig,
715
+ parsed: ParsedArgs,
716
+ ): Promise<FindingsCommandResult> {
717
+ const state = loadState(config, parsed);
718
+ const selection = submitSelection(parsed);
719
+ const target = resolvePrTarget(defaultGhRunner, state.branch);
720
+ const preview = buildSubmitPreview(state, target, selection);
721
+ const json = parsed.bools.has("--json");
722
+
723
+ // Prévia explícita, ou alvo indisponível (fail-open) — nada a postar.
724
+ if (parsed.bools.has("--dry-run") || !target.ok) {
725
+ return { output: renderSubmitPreview(preview, json), exitCode: 0 };
726
+ }
727
+
728
+ const outcome = submitFindings({
729
+ state,
730
+ target: target.target,
731
+ runner: defaultGhRunner,
732
+ selection,
733
+ });
734
+ FindingsStateStore.at(
735
+ parsed.values["--dir"] ?? resolveArtifactDir(config),
736
+ state.branch,
737
+ ).write(state);
738
+ pingStatus(config, parsed);
739
+
740
+ return {
741
+ output: renderSubmitOutcome(outcome, json),
742
+ exitCode: outcome.failed.length > 0 ? 1 : 0,
743
+ };
744
+ }
@@ -67,6 +67,22 @@ export type FindingHistoryEntry = {
67
67
  by?: string;
68
68
  };
69
69
 
70
+ /**
71
+ * Vínculo com o comentário remoto no GitHub (envio da revisão). Gravado pelo
72
+ * `findings submit` / UI — permite pular reenvios e abrir o comentário.
73
+ */
74
+ export type GitHubLink = {
75
+ /** Id do comentário (review comment inline ou issue comment geral). */
76
+ commentId: number;
77
+ url?: string;
78
+ /** Origem do comentário remoto: linha, arquivo ou comentário geral. */
79
+ kind: "line" | "file" | "comment";
80
+ /** Número da PR de destino. */
81
+ pr: number;
82
+ /** Quando foi enviado (ISO). */
83
+ at: string;
84
+ };
85
+
70
86
  /** Marco da revisão que originou o estado: commit revisado + quando. */
71
87
  export type FindingsReviewed = {
72
88
  /** SHA do commit revisado (head do diff no `--finalize`). */
@@ -99,6 +115,8 @@ export type FindingState = {
99
115
  createdAt: string;
100
116
  updatedAt: string;
101
117
  history: FindingHistoryEntry[];
118
+ /** Vínculo com o comentário remoto no GitHub (envio da revisão). */
119
+ github?: GitHubLink;
102
120
  };
103
121
 
104
122
  /** Conteúdo do `state.json`. */
@@ -345,6 +363,7 @@ export function buildStateFromFindings(
345
363
  createdAt: prev?.createdAt ?? now,
346
364
  updatedAt: now,
347
365
  history: prev?.history ?? [],
366
+ github: prev?.github,
348
367
  };
349
368
  byFingerprint.set(fingerprint, created);
350
369
  result[created.id] = created;