@payconductor/houndsman 0.13.0 → 0.14.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/README.md +6 -1
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +20 -3
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/index.ts +3 -1
- package/src/cli/progress.ts +7 -2
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +8 -0
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +27 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +31 -3
- package/src/core/defaults.ts +50 -4
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +14 -1
- package/src/core/engine/review-session.ts +11 -13
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +90 -0
- package/src/core/hunt/index.ts +2 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +13 -1
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/logging/index.ts +6 -1
- package/src/core/logging/progress.ts +85 -1
- package/src/core/orchestrator.ts +33 -1289
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +8 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +16 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +29 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +18 -14
- package/src/skill/SETUP.md +1 -0
- package/src/skill/SKILL.md +13 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +145 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +187 -178
- package/src/tests/ai-core.test.ts +46 -0
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-maki.test.ts +58 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +29 -1
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +38 -29
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
package/src/tests/server.test.ts
CHANGED
|
@@ -64,8 +64,8 @@ function makeEnv(): {
|
|
|
64
64
|
content: JSON.stringify({ watchdogs: { alpha: ["src/alpha/f.ts"] } }),
|
|
65
65
|
});
|
|
66
66
|
storage.writeArtifact(review.id, {
|
|
67
|
-
name: "review.md",
|
|
68
|
-
kind: "
|
|
67
|
+
name: "review-resume.md",
|
|
68
|
+
kind: "resume",
|
|
69
69
|
content: "# Review\n",
|
|
70
70
|
});
|
|
71
71
|
storage.writeArtifact(review.id, {
|
|
@@ -386,7 +386,7 @@ describe("server API", () => {
|
|
|
386
386
|
expect.arrayContaining([
|
|
387
387
|
"manifest.json",
|
|
388
388
|
"classification.json",
|
|
389
|
-
"review.md",
|
|
389
|
+
"review-resume.md",
|
|
390
390
|
"briefs/alpha.md",
|
|
391
391
|
]),
|
|
392
392
|
);
|
|
@@ -406,7 +406,10 @@ describe("server API", () => {
|
|
|
406
406
|
expect(ok.status).toBe(200);
|
|
407
407
|
expect(await ok.json()).toEqual({ branch: "main" });
|
|
408
408
|
|
|
409
|
-
const review = await get(
|
|
409
|
+
const review = await get(
|
|
410
|
+
handler,
|
|
411
|
+
"/api/runs/main/artifacts/review-resume.md",
|
|
412
|
+
);
|
|
410
413
|
expect(review.status).toBe(200);
|
|
411
414
|
expect(await review.text()).toContain("# Review");
|
|
412
415
|
|
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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).
|