@payconductor/houndsman 0.13.0 → 0.16.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 (116) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/compare-command.ts +112 -0
  4. package/src/cli/commands/context-command.ts +1 -0
  5. package/src/cli/commands/findings-command.ts +11 -1
  6. package/src/cli/commands/golden-command.ts +92 -0
  7. package/src/cli/commands/hunt-command.ts +105 -4
  8. package/src/cli/commands/review-command.ts +38 -0
  9. package/src/cli/commands/status-command.ts +3 -2
  10. package/src/cli/help.ts +6 -1
  11. package/src/cli/index.ts +5 -1
  12. package/src/cli/progress.ts +7 -2
  13. package/src/core/ai/ai-client.ts +38 -2
  14. package/src/core/ai/features/brief-enricher.ts +1 -1
  15. package/src/core/ai/features/business-analyzer.ts +61 -0
  16. package/src/core/ai/features/business-summarizer.ts +44 -0
  17. package/src/core/ai/index.ts +8 -0
  18. package/src/core/ai/prompts/brief.ts +33 -7
  19. package/src/core/ai/prompts/business.ts +76 -0
  20. package/src/core/ai/provider-capabilities.ts +12 -0
  21. package/src/core/ai/schemas.ts +70 -2
  22. package/src/core/ai/usage-recorder.ts +53 -1
  23. package/src/core/brief.ts +278 -16
  24. package/src/core/builtins.ts +108 -0
  25. package/src/core/business-summary.ts +583 -0
  26. package/src/core/commands/compare.ts +266 -0
  27. package/src/core/commands/context.ts +6 -2
  28. package/src/core/commands/findings.ts +38 -30
  29. package/src/core/commands/golden.ts +176 -0
  30. package/src/core/commands/review.ts +6 -0
  31. package/src/core/commands/scaffold.ts +29 -0
  32. package/src/core/commands/status.ts +9 -4
  33. package/src/core/compare.ts +310 -0
  34. package/src/core/concurrency.ts +30 -0
  35. package/src/core/config.ts +41 -3
  36. package/src/core/defaults.ts +50 -4
  37. package/src/core/domain/budget.ts +62 -4
  38. package/src/core/domain/classification.ts +12 -2
  39. package/src/core/engine/carryover.ts +4 -0
  40. package/src/core/engine/finalize-pipeline.ts +371 -0
  41. package/src/core/engine/prepare-pipeline.ts +1294 -0
  42. package/src/core/engine/review-context.ts +51 -0
  43. package/src/core/engine/review-options.ts +47 -0
  44. package/src/core/engine/review-scope.ts +14 -1
  45. package/src/core/engine/review-session.ts +11 -13
  46. package/src/core/engine/run-artifacts.ts +196 -0
  47. package/src/core/engine/run-format.ts +4 -0
  48. package/src/core/findings-state.ts +22 -2
  49. package/src/core/github-review.ts +5 -2
  50. package/src/core/golden.ts +346 -0
  51. package/src/core/hunt/hunt-runner.ts +621 -34
  52. package/src/core/hunt/index.ts +40 -1
  53. package/src/core/hunt/maki-intent-prompt.ts +50 -0
  54. package/src/core/hunt/maki-prompt.ts +298 -5
  55. package/src/core/hunt/maki-runner.ts +93 -16
  56. package/src/core/hunt/pool.ts +4 -26
  57. package/src/core/hunt/review-checks.ts +460 -0
  58. package/src/core/logging/index.ts +6 -1
  59. package/src/core/logging/progress.ts +85 -1
  60. package/src/core/orchestrator.ts +33 -1289
  61. package/src/core/paths.ts +6 -3
  62. package/src/core/pr-context.ts +2 -4
  63. package/src/core/prepare-report.ts +61 -0
  64. package/src/core/project-config.ts +18 -1
  65. package/src/core/propagation.ts +127 -0
  66. package/src/core/registry-check.ts +90 -27
  67. package/src/core/renderers/manifest-writer.ts +13 -0
  68. package/src/core/renderers/resume-renderer.ts +151 -11
  69. package/src/core/report-builder.ts +116 -278
  70. package/src/core/severity.ts +245 -0
  71. package/src/core/storage/file/file-storage.ts +7 -3
  72. package/src/core/storage/sqlite/mappers.ts +10 -0
  73. package/src/core/storage/sqlite/schema.ts +104 -23
  74. package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
  75. package/src/core/storage/types.ts +17 -4
  76. package/src/core/storage/work-area.ts +59 -1
  77. package/src/core/types.ts +31 -0
  78. package/src/core/watchdog-registry.ts +21 -3
  79. package/src/index.ts +51 -4
  80. package/src/server/client/ArtifactsPanel.tsx +4 -2
  81. package/src/server/client/FindingCard.tsx +21 -1
  82. package/src/server/client/RunsSection.tsx +5 -0
  83. package/src/server/client/utils/constants.ts +21 -11
  84. package/src/server/client/utils/markdown.ts +1 -1
  85. package/src/server/client/utils/types.ts +14 -2
  86. package/src/server/run-repository.ts +35 -14
  87. package/src/server/styles.css +2 -0
  88. package/src/skill/OUTPUT-FORMAT.md +55 -10
  89. package/src/skill/README.md +19 -14
  90. package/src/skill/SETUP.md +1 -0
  91. package/src/skill/SKILL.md +13 -4
  92. package/src/skill/example-resume.md +43 -0
  93. package/src/skill/hunt.md +205 -58
  94. package/src/skill/train.md +3 -0
  95. package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
  96. package/src/tests/ai-core.test.ts +46 -0
  97. package/src/tests/ai-features.test.ts +95 -4
  98. package/src/tests/baseline.test.ts +29 -121
  99. package/src/tests/brief-scope.test.ts +250 -0
  100. package/src/tests/business-summary.test.ts +258 -0
  101. package/src/tests/compare.test.ts +256 -0
  102. package/src/tests/findings-state.test.ts +82 -0
  103. package/src/tests/fixtures/fake-model-provider.ts +2 -1
  104. package/src/tests/golden.test.ts +195 -0
  105. package/src/tests/hunt-maki.test.ts +710 -2
  106. package/src/tests/markdown.test.ts +3 -3
  107. package/src/tests/progress.test.ts +29 -1
  108. package/src/tests/propagation.test.ts +119 -0
  109. package/src/tests/review-resume.test.ts +60 -1
  110. package/src/tests/review-scope.test.ts +38 -29
  111. package/src/tests/review-scripts.test.ts +125 -218
  112. package/src/tests/server.test.ts +7 -4
  113. package/src/tests/severity.test.ts +235 -0
  114. package/src/tests/storage.test.ts +194 -9
  115. package/src/core/renderers/report-renderer.ts +0 -66
  116. package/src/skill/example-review.md +0 -66
@@ -142,6 +142,59 @@ function contract(
142
142
  }
143
143
  });
144
144
 
145
+ test("variantes A/B: mesma branch, runs separadas e findings isolados", () => {
146
+ const { storage, cleanup } = make();
147
+ try {
148
+ const repo = buildRepoIdentity(null, "/tmp/app");
149
+ const main = storage.getOrCreateReview({
150
+ repo,
151
+ branch: "feature/x",
152
+ slug: "feature-x",
153
+ });
154
+ const a = storage.getOrCreateReview({
155
+ repo,
156
+ branch: "feature/x",
157
+ variant: "baseline",
158
+ slug: "feature-x--baseline-abc123",
159
+ });
160
+ const b = storage.getOrCreateReview({
161
+ repo,
162
+ branch: "feature/x",
163
+ variant: "otimizado",
164
+ slug: "feature-x--otimizado-def456",
165
+ });
166
+ expect(new Set([main.id, a.id, b.id]).size).toBe(3);
167
+ expect(storage.findReview(repo.key, "feature/x")?.id).toBe(main.id);
168
+ expect(storage.findReview(repo.key, "feature/x", "baseline")?.id).toBe(
169
+ a.id,
170
+ );
171
+ expect(storage.findReview(repo.key, "feature/x", "otimizado")?.id).toBe(
172
+ b.id,
173
+ );
174
+ expect(storage.findReview(repo.key, "feature/x", "ausente")).toBeNull();
175
+
176
+ // Idempotente por (branch, variante).
177
+ expect(
178
+ storage.getOrCreateReview({
179
+ repo,
180
+ branch: "feature/x",
181
+ variant: "baseline",
182
+ slug: "outro",
183
+ }).id,
184
+ ).toBe(a.id);
185
+ expect(storage.listReviews(repo.key)).toHaveLength(3);
186
+
187
+ // Findings/staging são por review.
188
+ storage.writeFindings(a.id, sampleState());
189
+ expect(storage.readFindings(a.id)?.findings.ab12cd34ef?.rule).toBe(
190
+ "regra",
191
+ );
192
+ expect(storage.readFindings(b.id)).toBeNull();
193
+ } finally {
194
+ cleanup();
195
+ }
196
+ });
197
+
145
198
  test("findings: round-trip completo e preservação do short id", () => {
146
199
  const { storage, cleanup } = make();
147
200
  try {
@@ -195,18 +248,18 @@ function contract(
195
248
  slug: "main",
196
249
  });
197
250
  const first = storage.writeArtifact(review.id, {
198
- name: "review.md",
199
- kind: "review",
251
+ name: "review-resume.md",
252
+ kind: "resume",
200
253
  content: "# Review\n",
201
254
  });
202
255
  expect(first.id).toMatch(UUID);
203
256
  const second = storage.writeArtifact(review.id, {
204
- name: "review.md",
205
- kind: "review",
257
+ name: "review-resume.md",
258
+ kind: "resume",
206
259
  content: "# Review 2\n",
207
260
  });
208
261
  expect(second.id).toBe(first.id);
209
- expect(storage.readArtifact(review.id, "review.md")?.content).toBe(
262
+ expect(storage.readArtifact(review.id, "review-resume.md")?.content).toBe(
210
263
  "# Review 2\n",
211
264
  );
212
265
  storage.writeArtifact(review.id, {
@@ -215,7 +268,7 @@ function contract(
215
268
  content: "{}",
216
269
  });
217
270
  expect(storage.listArtifacts(review.id).map((item) => item.name)).toEqual(
218
- ["manifest.json", "review.md"],
271
+ ["manifest.json", "review-resume.md"],
219
272
  );
220
273
  storage.deleteArtifact(review.id, "manifest.json");
221
274
  expect(storage.listArtifacts(review.id)).toHaveLength(1);
@@ -235,14 +288,14 @@ function contract(
235
288
  });
236
289
  storage.writeFindings(review.id, sampleState("main"));
237
290
  storage.writeArtifact(review.id, {
238
- name: "review.md",
239
- kind: "review",
291
+ name: "review-resume.md",
292
+ kind: "resume",
240
293
  content: "# Review\n",
241
294
  });
242
295
  storage.deleteReview(review.id);
243
296
  expect(storage.getReview(review.id)).toBeNull();
244
297
  expect(storage.readFindings(review.id)).toBeNull();
245
- expect(storage.readArtifact(review.id, "review.md")).toBeNull();
298
+ expect(storage.readArtifact(review.id, "review-resume.md")).toBeNull();
246
299
  } finally {
247
300
  cleanup();
248
301
  }
@@ -446,4 +499,136 @@ describe("migração do schema SQLite", () => {
446
499
  rmSync(dir, { recursive: true, force: true });
447
500
  }
448
501
  });
502
+
503
+ test("banco v4 ganha `variant` sem perder findings (rebuild da reviews)", () => {
504
+ const dir = mkdtempSync(join(tmpdir(), "houndsman-migrate-v4-"));
505
+ const path = join(dir, "houndsman.db");
506
+ const reviewId = "22222222-2222-4222-8222-222222222222";
507
+ const legacy = new Database(path);
508
+ try {
509
+ legacy.exec(`
510
+ CREATE TABLE reviews (
511
+ id TEXT PRIMARY KEY,
512
+ repo_key TEXT NOT NULL,
513
+ repo_root TEXT NOT NULL,
514
+ repo_remote TEXT,
515
+ branch TEXT NOT NULL,
516
+ slug TEXT NOT NULL,
517
+ base TEXT,
518
+ head TEXT,
519
+ head_sha TEXT,
520
+ reviewed_sha TEXT,
521
+ reviewed_at TEXT,
522
+ status TEXT,
523
+ started_at TEXT,
524
+ heartbeat_at TEXT,
525
+ worktree_path TEXT,
526
+ pr_number INTEGER,
527
+ created_at TEXT NOT NULL,
528
+ updated_at TEXT NOT NULL,
529
+ UNIQUE(repo_key, branch)
530
+ );
531
+ CREATE TABLE findings (
532
+ id TEXT PRIMARY KEY,
533
+ review_id TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
534
+ short_id TEXT NOT NULL,
535
+ fingerprint TEXT NOT NULL,
536
+ watchdog_ids TEXT NOT NULL,
537
+ rule TEXT NOT NULL,
538
+ severity TEXT NOT NULL,
539
+ file TEXT NOT NULL,
540
+ line INTEGER,
541
+ message TEXT NOT NULL,
542
+ action TEXT NOT NULL,
543
+ evidence TEXT,
544
+ scenario TEXT,
545
+ impact TEXT,
546
+ confidence REAL,
547
+ basis TEXT,
548
+ anchor TEXT,
549
+ found_at_sha TEXT,
550
+ count INTEGER NOT NULL DEFAULT 1,
551
+ present INTEGER NOT NULL DEFAULT 1,
552
+ decision TEXT NOT NULL,
553
+ comment TEXT,
554
+ exec_state TEXT NOT NULL,
555
+ github TEXT,
556
+ created_at TEXT NOT NULL,
557
+ updated_at TEXT NOT NULL,
558
+ UNIQUE(review_id, short_id),
559
+ UNIQUE(review_id, fingerprint)
560
+ );
561
+ PRAGMA user_version = 4;
562
+ `);
563
+ legacy.run(
564
+ `INSERT INTO reviews
565
+ (id, repo_key, repo_root, branch, slug, created_at, updated_at)
566
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
567
+ [
568
+ reviewId,
569
+ "path:/repo",
570
+ dir,
571
+ "feature/x",
572
+ "feature-x",
573
+ "2026-01-01T00:00:00.000Z",
574
+ "2026-01-01T00:00:00.000Z",
575
+ ],
576
+ );
577
+ legacy.run(
578
+ `INSERT INTO findings
579
+ (id, review_id, short_id, fingerprint, watchdog_ids, rule, severity,
580
+ file, line, message, action, decision, exec_state, created_at, updated_at)
581
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
582
+ [
583
+ "33333333-3333-4333-8333-333333333333",
584
+ reviewId,
585
+ "ab12cd34ef",
586
+ "fp-legado",
587
+ '["alpha"]',
588
+ "regra",
589
+ "attention",
590
+ "src/a.ts",
591
+ 3,
592
+ "msg",
593
+ "ação",
594
+ "pending",
595
+ "pending",
596
+ "2026-01-01T00:00:00.000Z",
597
+ "2026-01-01T00:00:00.000Z",
598
+ ],
599
+ );
600
+ } finally {
601
+ legacy.close();
602
+ }
603
+
604
+ const storage = new SQLiteStorageProvider(path);
605
+ try {
606
+ const review = storage.getReview(reviewId);
607
+ expect(review?.variant).toBeUndefined();
608
+ // O rebuild preserva os filhos (FKs desligadas durante a troca).
609
+ expect(storage.readFindings(reviewId)?.findings.ab12cd34ef?.rule).toBe(
610
+ "regra",
611
+ );
612
+ // E a identidade agora aceita variantes na mesma branch.
613
+ const repo = buildRepoIdentity(null, dir);
614
+ const a = storage.getOrCreateReview({
615
+ repo,
616
+ branch: "feature/x",
617
+ variant: "baseline",
618
+ slug: "feature-x--baseline-abc123",
619
+ });
620
+ const b = storage.getOrCreateReview({
621
+ repo,
622
+ branch: "feature/x",
623
+ variant: "otimizado",
624
+ slug: "feature-x--otimizado-def456",
625
+ });
626
+ expect(a.id).not.toBe(b.id);
627
+ expect(a.id).not.toBe(reviewId);
628
+ expect(storage.findReview(a.repoKey, "feature/x", "baseline")?.id).toBe(a.id);
629
+ } finally {
630
+ storage.close();
631
+ rmSync(dir, { recursive: true, force: true });
632
+ }
633
+ });
449
634
  });
@@ -1,66 +0,0 @@
1
- /**
2
- * Renderização do relatório (biblioteca) — superfície OO sobre `report-builder`.
3
- *
4
- * O texto continua nas funções puras; a classe fixa o resolver de nomes e evita
5
- * repetir a lista de argumentos no orquestrador.
6
- */
7
-
8
- import type { JevCandidate } from "../jev";
9
- import {
10
- type ContextSummary,
11
- type DomainReport,
12
- type MergedFinding,
13
- renderDomainReport,
14
- renderReport,
15
- type WatchdogNameResolver,
16
- type WatchdogReport,
17
- } from "../report-builder";
18
-
19
- /** Renderiza o relatório consolidado e os relatórios por domínio. */
20
- export class ReportRenderer {
21
- private readonly nameOf: WatchdogNameResolver;
22
-
23
- constructor(nameOf: WatchdogNameResolver) {
24
- this.nameOf = nameOf;
25
- }
26
-
27
- /** Relatório consolidado (`review.md`). */
28
- render(
29
- branch: string,
30
- reports: WatchdogReport[],
31
- merged: MergedFinding[],
32
- coverage: { classified?: Record<string, string[]> },
33
- context?: ContextSummary | null,
34
- candidates?: JevCandidate[],
35
- domains: readonly DomainReport[] = [],
36
- ): string {
37
- return renderReport(
38
- branch,
39
- reports,
40
- merged,
41
- coverage,
42
- context,
43
- candidates,
44
- this.nameOf,
45
- domains,
46
- );
47
- }
48
-
49
- /** Relatório isolado de um watchdog (`review.<id>.md`). */
50
- renderDomain(
51
- branch: string,
52
- report: WatchdogReport,
53
- merged: MergedFinding[],
54
- expectedFiles: number,
55
- context?: ContextSummary | null,
56
- ): string {
57
- return renderDomainReport(
58
- branch,
59
- report,
60
- merged,
61
- expectedFiles,
62
- context,
63
- this.nameOf,
64
- );
65
- }
66
- }
@@ -1,66 +0,0 @@
1
- # PR Review — feature/add-webhook-retry
2
-
3
- **Data:** 2026-04-20
4
- **Watchdogs ativos:** api-contracts, backend-architecture, payments-integrations, acquirer-integrations
5
- **Achados:** 2 bloqueantes · 1 atenção · 1 sugestões · 1 nit
6
-
7
- ## Contexto da PR
8
- **PR:** #482 Adicionar retry de webhooks — https://github.com/org/repo/pull/482
9
- **Branch:** `feature/add-webhook-retry` → `main`
10
- **Objetivo:** Tornar o envio de webhooks resiliente a falhas temporárias do destinatário.
11
- **Motivação:** Webhooks com falha exigiam reenvio manual e causavam perda de eventos em integradores.
12
- **Resumo das alterações:**
13
- - Nova fila de retry para webhooks que falharam.
14
- - Novo endpoint interno para reprocessar eventos manualmente.
15
- - Registro de tentativas no handler de envio.
16
-
17
- ## Relatórios por domínio
18
-
19
- | Watchdog | Achados | Revisados | Relatório |
20
- |---|---|---|---|
21
- | Arquitetura Backend | 2 | 3/3 | [`review.backend-architecture.md`](review.backend-architecture.md) |
22
- | Pagamentos & Integrações | 1 | 4/4 | [`review.payments-integrations.md`](review.payments-integrations.md) |
23
- | Acquirer Integrations | 2 | 3/4 | [`review.acquirer-integrations.md`](review.acquirer-integrations.md) |
24
-
25
- ## Cobertura
26
-
27
- | Watchdog | Arquivos revisados | Findings |
28
- |---|---|---|
29
- | API & Contratos | 2 | 0 |
30
- | Arquitetura Backend | 3 | 2 |
31
- | Pagamentos & Integrações | 4 | 1 |
32
- | Acquirer Integrations | 3/4 (faltam 1) | 2 |
33
-
34
- ## Conclusão
35
-
36
- **Status:** ❌ Mudanças Necessárias
37
- **Resumo:** 2 violações bloqueantes, 1 ponto de atenção, 1 sugestão e 1 nit.
38
-
39
- ## ❌ blocking
40
-
41
- - **Webhook handler sem verifySignature** — `server/src/services/integrations/acquirer/webhook/handle-charge-event.ts:15` · _Acquirer Integrations, Pagamentos & Integrações_
42
- Payload processado antes de validar a assinatura do provider.
43
- **Ação:** Validar `verifyWebhookSignature(payload, signature, secret)` antes de qualquer efeito.
44
- **Evidência:** `const event = JSON.parse(body)`
45
-
46
- - **Promise não aguardada** — `server/src/services/webhook/webhook-retry-service.ts:92` · _Pagamentos & Integrações_
47
- `updateWebhookStatus(id, 'failed')` sem `await` — status pode não persistir antes do retorno.
48
- **Ação:** Adicionar `await`.
49
-
50
- ## ⚠️ attention
51
-
52
- - **Retry sem backoff** — `server/src/services/webhook/webhook-retry-service.ts:55` · _Acquirer Integrations_
53
- Retry com intervalo fixo de 1s.
54
- **Ação:** Usar exponential backoff com jitter.
55
-
56
- ## 💡 suggestion
57
-
58
- - **Complexidade** — `server/src/services/webhook/webhook-retry-service.ts:45-78` · _Arquitetura Backend_
59
- `retryWithBackoff` com 6 níveis de indentação.
60
- **Ação:** Extrair `calculateBackoffDelay(attempt, config)` e usar early returns.
61
-
62
- ## 🔎 nit
63
-
64
- - **Ordem de membros** — `server/src/services/webhook/webhook-retry-service.ts:12` · _Arquitetura Backend_
65
- Campo privado declarado antes do construtor público.
66
- **Ação:** Reordenar membros (públicos primeiro).