@payconductor/houndsman 0.2.0 → 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 (68) 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 +58 -266
  15. package/src/core/arg-parser.ts +48 -0
  16. package/src/core/commands/findings.ts +34 -144
  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 +170 -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 +4 -33
  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/index.ts +64 -234
  64. package/src/server/run-repository.ts +291 -0
  65. package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
  66. package/src/tests/baseline.test.ts +513 -493
  67. package/src/tests/domain.test.ts +328 -0
  68. package/src/tests/infrastructure.test.ts +210 -0
@@ -11,31 +11,20 @@
11
11
  * (`parseEntries`) — não reimplementa leitura de diff.
12
12
  */
13
13
 
14
- import {
15
- DEFAULT_MAX_PER_FILE_CHARS,
16
- type DiffSource,
17
- parseDiffIndex,
18
- parseNumstat,
19
- } from "./diff-source";
14
+ import { DEFAULT_MAX_PER_FILE_CHARS, type DiffSource } from "./diff-source";
20
15
  import { parseEntries } from "./file-classifier";
16
+ import { runGit } from "./git-runner";
17
+ import { GitRepository } from "./services/git-repository";
21
18
  import type { FileChurn } from "./types";
22
19
 
23
- /** Executa `git` (paths literais) e devolve o stdout; lança se falhar. */
24
- export function runGit(args: string[]): string {
25
- const result = Bun.spawnSync(["git", "-c", "core.quotepath=false", ...args], {
26
- stdout: "pipe",
27
- stderr: "pipe",
28
- });
29
- if (result.exitCode !== 0) {
30
- const stderr = new TextDecoder().decode(result.stderr).trim();
31
- throw new Error(`git ${args.join(" ")} falhou: ${stderr}`);
32
- }
33
- return new TextDecoder().decode(result.stdout);
34
- }
20
+ export { runGit };
21
+
22
+ /** Repositório padrão (runner real) usado pelos helpers funcionais. */
23
+ const git = new GitRepository();
35
24
 
36
25
  /** SHA completo de uma ref (`git rev-parse`). Lança se a ref não resolver. */
37
26
  export function resolveSha(ref: string): string {
38
- return runGit(["rev-parse", ref]).trim();
27
+ return git.resolveSha(ref);
39
28
  }
40
29
 
41
30
  /** SHA do commit atual (`HEAD`). */
@@ -50,31 +39,22 @@ export function currentHeadSha(): string {
50
39
  * comportamento seguro.
51
40
  */
52
41
  export function isAncestor(ancestor: string, descendant: string): boolean {
53
- const result = Bun.spawnSync(
54
- ["git", "merge-base", "--is-ancestor", ancestor, descendant],
55
- { stdout: "ignore", stderr: "ignore" },
56
- );
57
- return result.exitCode === 0;
42
+ return git.isAncestor(ancestor, descendant);
58
43
  }
59
44
 
60
45
  /** Conteúdo de um arquivo num commit (`git show sha:path`); null se ausente. */
61
46
  export function readFileAt(sha: string, path: string): string | null {
62
- const result = Bun.spawnSync(
63
- ["git", "-c", "core.quotepath=false", "show", `${sha}:${path}`],
64
- { stdout: "pipe", stderr: "ignore" },
65
- );
66
- if (result.exitCode !== 0) return null;
67
- return new TextDecoder().decode(result.stdout);
47
+ return git.readFileAt(sha, path);
68
48
  }
69
49
 
70
50
  /** Arquivos alterados num intervalo (`git diff range --name-status`). */
71
51
  export function diffEntries(range: string): ReturnType<typeof parseEntries> {
72
- return parseEntries(runGit(["diff", range, "--name-status"]));
52
+ return parseEntries(git.run(["diff", range, "--name-status"]));
73
53
  }
74
54
 
75
55
  /** Churn por arquivo de um intervalo (`git diff --numstat -z range`). */
76
56
  export function diffNumstat(range: string): Map<string, FileChurn> {
77
- return parseNumstat(runGit(["diff", "--numstat", "-z", range]));
57
+ return git.diffNumstat(range);
78
58
  }
79
59
 
80
60
  /**
@@ -86,9 +66,7 @@ export function diffPatch(
86
66
  paths?: readonly string[],
87
67
  maxPerFileChars: number = DEFAULT_MAX_PER_FILE_CHARS,
88
68
  ): Map<string, string> {
89
- const args = ["diff", range];
90
- if (paths && paths.length > 0) args.push("--", ...paths);
91
- return parseDiffIndex(runGit(args), maxPerFileChars);
69
+ return git.diffPatch(range, paths, maxPerFileChars);
92
70
  }
93
71
 
94
72
  /** Diff (patch + churn) de `from..to` — 2 spawns, base do review incremental. */
@@ -97,9 +75,5 @@ export function collectDiffSince(
97
75
  to: string,
98
76
  maxPerFileChars: number = DEFAULT_MAX_PER_FILE_CHARS,
99
77
  ): DiffSource {
100
- const range = `${from}..${to}`;
101
- return {
102
- byFile: parseDiffIndex(runGit(["diff", range]), maxPerFileChars),
103
- churn: diffNumstat(range),
104
- };
78
+ return git.collectDiffSince(from, to, maxPerFileChars);
105
79
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Execução do binário `git` (biblioteca) — única camada que fala com o processo.
3
+ *
4
+ * `core.quotepath=false` mantém paths literais (acentos/unicode). O runner é
5
+ * injetável nos testes; `GitRepository` é a superfície OO sobre estes helpers.
6
+ */
7
+
8
+ /** Resultado cru de uma execução do git (nunca lança). */
9
+ export type GitResult = {
10
+ ok: boolean;
11
+ code: number;
12
+ stdout: string;
13
+ stderr: string;
14
+ };
15
+
16
+ /** Executa `git` e devolve o resultado cru. Injetável nos testes. */
17
+ export type GitRunner = (args: string[]) => GitResult;
18
+
19
+ /** Runner padrão: `git -c core.quotepath=false <args>` a partir do cwd. */
20
+ export const defaultGitRunner: GitRunner = (args) => {
21
+ const result = Bun.spawnSync(["git", "-c", "core.quotepath=false", ...args], {
22
+ stdout: "pipe",
23
+ stderr: "pipe",
24
+ });
25
+ const code = result.exitCode ?? 1;
26
+ return {
27
+ ok: code === 0,
28
+ code,
29
+ stdout: new TextDecoder().decode(result.stdout),
30
+ stderr: new TextDecoder().decode(result.stderr),
31
+ };
32
+ };
33
+
34
+ /** Executa e devolve o stdout; lança com o stderr quando o git falha. */
35
+ export function runGit(args: string[], runner: GitRunner = defaultGitRunner): string {
36
+ const result = runner(args);
37
+ if (!result.ok) {
38
+ throw new Error(`git ${args.join(" ")} falhou: ${result.stderr.trim()}`);
39
+ }
40
+ return result.stdout;
41
+ }
42
+
43
+ /** Executa o git sem lançar (para sondagens como `merge-base`/`show`). */
44
+ export function tryGit(args: string[], runner: GitRunner = defaultGitRunner): GitResult {
45
+ return runner(args);
46
+ }
@@ -20,40 +20,11 @@ import {
20
20
  type FindingState,
21
21
  type FindingsStateFile,
22
22
  } from "./findings-state";
23
+ import { systemClock } from "./services/clock";
24
+ import { defaultGhRunner, type GhResult, type GhRunner } from "./services/gh-client";
23
25
  import { SEVERITY_EMOJI, type Severity } from "./types";
24
26
 
25
- // ---------------------------------------------------------------------------
26
- // Runner do `gh` (injetável nos testes)
27
- // ---------------------------------------------------------------------------
28
-
29
- export type GhResult = { ok: boolean; code: number; stdout: string; stderr: string };
30
-
31
- /** Executa o `gh` e devolve o resultado cru (sem lançar). */
32
- export type GhRunner = (args: string[]) => GhResult;
33
-
34
- /** Runner padrão: `gh` a partir do cwd; ausência do binário → `ok:false`. */
35
- export const defaultGhRunner: GhRunner = (args) => {
36
- try {
37
- const result = Bun.spawnSync(["gh", ...args], {
38
- stdout: "pipe",
39
- stderr: "pipe",
40
- });
41
- const code = result.exitCode ?? 1;
42
- return {
43
- ok: code === 0,
44
- code,
45
- stdout: new TextDecoder().decode(result.stdout).trim(),
46
- stderr: new TextDecoder().decode(result.stderr).trim(),
47
- };
48
- } catch (error) {
49
- return {
50
- ok: false,
51
- code: 127,
52
- stdout: "",
53
- stderr: error instanceof Error ? error.message : String(error),
54
- };
55
- }
56
- };
27
+ export { type GhResult, type GhRunner, defaultGhRunner };
57
28
 
58
29
  // ---------------------------------------------------------------------------
59
30
  // Alvo (PR + repositório)
@@ -371,7 +342,7 @@ export function submitFindings(options: {
371
342
  now?: string;
372
343
  }): SubmitOutcome {
373
344
  const { state, target, runner } = options;
374
- const now = options.now ?? new Date().toISOString();
345
+ const now = options.now ?? systemClock();
375
346
  const candidates = selectSubmitCandidates(state, options.selection);
376
347
 
377
348
  const outcome: SubmitOutcome = { posted: [], failed: [], skipped: [] };