@payconductor/houndsman 0.2.0 → 0.4.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 (107) hide show
  1. package/README.md +18 -6
  2. package/package.json +1 -1
  3. package/src/cli/arg-parser.ts +8 -0
  4. package/src/cli/commands/command.ts +23 -0
  5. package/src/cli/commands/context-command.ts +40 -0
  6. package/src/cli/commands/doctor-command.ts +60 -0
  7. package/src/cli/commands/findings-command.ts +38 -0
  8. package/src/cli/commands/help-command.ts +13 -0
  9. package/src/cli/commands/registry.ts +34 -0
  10. package/src/cli/commands/review-command.ts +21 -0
  11. package/src/cli/commands/server-command.ts +37 -0
  12. package/src/cli/commands/setup-command.ts +34 -0
  13. package/src/cli/commands/watchdog-command.ts +34 -0
  14. package/src/cli/help.ts +47 -0
  15. package/src/cli/index.ts +60 -268
  16. package/src/core/arg-parser.ts +48 -0
  17. package/src/core/commands/context.ts +29 -13
  18. package/src/core/commands/findings.ts +185 -260
  19. package/src/core/commands/review.ts +21 -39
  20. package/src/core/commands/scaffold.ts +189 -80
  21. package/src/core/commands/setup.ts +16 -11
  22. package/src/core/commands/update.ts +7 -7
  23. package/src/core/commands/watchdog.ts +8 -80
  24. package/src/core/config.ts +24 -25
  25. package/src/core/defaults.ts +20 -0
  26. package/src/core/diff-source.ts +1 -13
  27. package/src/core/doc-discoverer.ts +25 -15
  28. package/src/core/domain/budget.ts +42 -0
  29. package/src/core/domain/classification.ts +94 -0
  30. package/src/core/domain/diff.ts +53 -0
  31. package/src/core/domain/watchdog.ts +96 -0
  32. package/src/core/engine/carryover.ts +67 -0
  33. package/src/core/engine/houndsman-engine.ts +65 -0
  34. package/src/core/engine/review-scope.ts +95 -0
  35. package/src/core/engine/review-session.ts +79 -0
  36. package/src/core/file-classifier.ts +11 -67
  37. package/src/core/findings-state.ts +193 -100
  38. package/src/core/git-revision.ts +14 -40
  39. package/src/core/git-runner.ts +46 -0
  40. package/src/core/github-review.ts +4 -33
  41. package/src/core/jev/jev-candidates.ts +49 -35
  42. package/src/core/orchestrator.ts +247 -421
  43. package/src/core/paths.ts +26 -11
  44. package/src/core/pr-context.ts +39 -51
  45. package/src/core/project-config.ts +56 -0
  46. package/src/core/renderers/batch-presenter.ts +40 -0
  47. package/src/core/renderers/brief-renderer.ts +23 -0
  48. package/src/core/renderers/findings-presenter.ts +74 -0
  49. package/src/core/renderers/manifest-writer.ts +123 -0
  50. package/src/core/renderers/report-renderer.ts +66 -0
  51. package/src/core/renderers/verify-presenter.ts +50 -0
  52. package/src/core/renderers/watchdog-presenter.ts +100 -0
  53. package/src/core/report-builder.ts +12 -7
  54. package/src/core/run-layout.ts +8 -0
  55. package/src/core/run-status.ts +29 -59
  56. package/src/core/services/clock.ts +12 -0
  57. package/src/core/services/file-classifier.ts +34 -0
  58. package/src/core/services/fix-batch-builder.ts +13 -0
  59. package/src/core/services/gh-client.ts +54 -0
  60. package/src/core/services/git-repository.ts +139 -0
  61. package/src/core/services/project-files.ts +91 -0
  62. package/src/core/services/verifier.ts +58 -0
  63. package/src/core/storage/factory.ts +24 -0
  64. package/src/core/storage/file/file-storage.ts +269 -0
  65. package/src/core/storage/paths.ts +64 -0
  66. package/src/core/storage/repo-identity.ts +70 -0
  67. package/src/core/storage/sqlite/mappers.ts +160 -0
  68. package/src/core/storage/sqlite/schema.ts +108 -0
  69. package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
  70. package/src/core/storage/types.ts +143 -0
  71. package/src/core/storage/work-area.ts +191 -0
  72. package/src/core/verify.ts +2 -1
  73. package/src/core/watchdog-registry.ts +58 -16
  74. package/src/index.ts +57 -6
  75. package/src/server/artifacts.ts +8 -61
  76. package/src/server/client/ArtifactsPanel.tsx +137 -23
  77. package/src/server/client/FindingCard.tsx +37 -6
  78. package/src/server/client/MarkdownText.tsx +101 -0
  79. package/src/server/client/SubmitModal.tsx +9 -4
  80. package/src/server/client/code-highlight.ts +422 -0
  81. package/src/server/client/constants.ts +130 -24
  82. package/src/server/client/json-highlight.ts +55 -0
  83. package/src/server/client/markdown.ts +153 -0
  84. package/src/server/client/types.ts +23 -0
  85. package/src/server/index.ts +155 -245
  86. package/src/server/run-repository.ts +299 -0
  87. package/src/skill/OUTPUT-FORMAT.md +6 -4
  88. package/src/skill/README.md +22 -14
  89. package/src/skill/SETUP.md +4 -4
  90. package/src/skill/fix.md +5 -5
  91. package/src/skill/hunt.md +27 -18
  92. package/src/skill/train.md +1 -1
  93. package/src/skill/verify.md +5 -5
  94. package/src/tests/__snapshots__/baseline.test.ts.snap +825 -0
  95. package/src/tests/baseline.test.ts +555 -493
  96. package/src/tests/cli.test.ts +40 -19
  97. package/src/tests/code-highlight.test.ts +123 -0
  98. package/src/tests/domain.test.ts +328 -0
  99. package/src/tests/findings-commands.test.ts +153 -131
  100. package/src/tests/findings-state.test.ts +6 -3
  101. package/src/tests/fixtures/fake-project.ts +0 -1
  102. package/src/tests/infrastructure.test.ts +205 -0
  103. package/src/tests/markdown.test.ts +89 -0
  104. package/src/tests/review-scripts.test.ts +8 -5
  105. package/src/tests/run-status.test.ts +33 -23
  106. package/src/tests/server.test.ts +108 -75
  107. package/src/tests/storage.test.ts +386 -0
@@ -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: [] };
@@ -36,6 +36,50 @@ export type JevCandidatesFile = {
36
36
 
37
37
  const SEVERITY_SET = new Set<string>(SEVERITY_ORDER);
38
38
 
39
+ /** Serializa o artefato `candidates.json` (sempre com quebra de linha final). */
40
+ export function serializeCandidates(data: JevCandidatesFile): string {
41
+ return `${JSON.stringify(data, null, 2)}\n`;
42
+ }
43
+
44
+ /** Valida/parseia o JSON bruto do artefato; null se inválido. */
45
+ export function parseCandidates(
46
+ raw: unknown,
47
+ validIds: readonly string[] = [],
48
+ ): JevCandidatesFile | null {
49
+ if (typeof raw !== "object" || raw === null) return null;
50
+ const value = raw as Record<string, unknown>;
51
+ const idSet = validIds.length > 0 ? new Set(validIds) : null;
52
+ const isCandidate = (entry: unknown): entry is JevCandidate => {
53
+ if (typeof entry !== "object" || entry === null) return false;
54
+ const c = entry as Record<string, unknown>;
55
+ return (
56
+ typeof c.watchdogId === "string" &&
57
+ (idSet === null || idSet.has(c.watchdogId)) &&
58
+ typeof c.rule === "string" &&
59
+ typeof c.severity === "string" &&
60
+ SEVERITY_SET.has(c.severity) &&
61
+ typeof c.file === "string" &&
62
+ typeof c.confidence === "number" &&
63
+ typeof c.questionKey === "string"
64
+ );
65
+ };
66
+ const candidates = Array.isArray(value.candidates)
67
+ ? value.candidates.filter(isCandidate)
68
+ : [];
69
+ return {
70
+ available: value.available === true,
71
+ reason: typeof value.reason === "string" ? value.reason : undefined,
72
+ model: typeof value.model === "string" ? value.model : undefined,
73
+ threshold: typeof value.threshold === "number" ? value.threshold : undefined,
74
+ analyzedFiles:
75
+ typeof value.analyzedFiles === "number" ? value.analyzedFiles : undefined,
76
+ candidates,
77
+ skipped: Array.isArray(value.skipped)
78
+ ? (value.skipped as JevSkippedFile[])
79
+ : undefined,
80
+ };
81
+ }
82
+
39
83
  /** Persistência do artefato `candidates.json` para um diretório de review. */
40
84
  export class JevCandidatesStore {
41
85
  /** Limiar padrão de confiança (`noul`) para virar candidato. */
@@ -57,49 +101,19 @@ export class JevCandidatesStore {
57
101
 
58
102
  /** Grava o `candidates.json` (sempre com candidatos válidos apenas). */
59
103
  write(data: JevCandidatesFile): void {
60
- writeFileSync(this.filePath, `${JSON.stringify(data, null, 2)}\n`);
104
+ writeFileSync(this.filePath, serializeCandidates(data));
61
105
  }
62
106
 
63
107
  /** Lê o `candidates.json`; null se ausente/inválido. */
64
108
  read(): JevCandidatesFile | null {
65
109
  if (!existsSync(this.filePath)) return null;
66
110
  try {
67
- const raw = JSON.parse(readFileSync(this.filePath, "utf8")) as Record<
68
- string,
69
- unknown
70
- >;
71
- const candidates = Array.isArray(raw.candidates)
72
- ? raw.candidates.filter((c) => this.isCandidate(c))
73
- : [];
74
- return {
75
- available: raw.available === true,
76
- reason: typeof raw.reason === "string" ? raw.reason : undefined,
77
- model: typeof raw.model === "string" ? raw.model : undefined,
78
- threshold: typeof raw.threshold === "number" ? raw.threshold : undefined,
79
- analyzedFiles:
80
- typeof raw.analyzedFiles === "number" ? raw.analyzedFiles : undefined,
81
- candidates,
82
- skipped: Array.isArray(raw.skipped)
83
- ? (raw.skipped as JevSkippedFile[])
84
- : undefined,
85
- };
111
+ return parseCandidates(
112
+ JSON.parse(readFileSync(this.filePath, "utf8")),
113
+ this.watchdogIdSet ? [...this.watchdogIdSet] : [],
114
+ );
86
115
  } catch {
87
116
  return null;
88
117
  }
89
118
  }
90
-
91
- private isCandidate(value: unknown): value is JevCandidate {
92
- if (typeof value !== "object" || value === null) return false;
93
- const c = value as Record<string, unknown>;
94
- return (
95
- typeof c.watchdogId === "string" &&
96
- (this.watchdogIdSet === null || this.watchdogIdSet.has(c.watchdogId)) &&
97
- typeof c.rule === "string" &&
98
- typeof c.severity === "string" &&
99
- SEVERITY_SET.has(c.severity) &&
100
- typeof c.file === "string" &&
101
- typeof c.confidence === "number" &&
102
- typeof c.questionKey === "string"
103
- );
104
- }
105
119
  }