@payconductor/houndsman 0.12.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 +23 -2
- 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/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- 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 +14 -1
- 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/types.ts +2 -0
- 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 +38 -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 +57 -1
- package/src/core/defaults.ts +63 -2
- 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 +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- 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 +46 -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 +17 -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 +78 -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 +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- 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/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- 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
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
} from "../core/config";
|
|
15
15
|
import { parseDiffIndex, parseNumstat } from "../core/diff-source";
|
|
16
16
|
import { discoverDocs } from "../core/doc-discoverer";
|
|
17
|
+
import { riskOf } from "../core/domain/budget";
|
|
18
|
+
import { coverageOf, unaccountedFiles } from "../core/engine/finalize-pipeline";
|
|
19
|
+
import { overlapMessage } from "../core/engine/prepare-pipeline";
|
|
17
20
|
import {
|
|
18
21
|
type ClassificationResult,
|
|
19
22
|
churnOf,
|
|
@@ -37,8 +40,6 @@ import {
|
|
|
37
40
|
dedupeFindings,
|
|
38
41
|
parseContextSummary,
|
|
39
42
|
parseWatchdogReport,
|
|
40
|
-
renderDomainReport,
|
|
41
|
-
renderReport,
|
|
42
43
|
sanitizeBranch,
|
|
43
44
|
type WatchdogReport,
|
|
44
45
|
} from "../core/report-builder";
|
|
@@ -451,6 +452,83 @@ describe("triage", () => {
|
|
|
451
452
|
expect(overflow).toEqual(["a.ts"]);
|
|
452
453
|
});
|
|
453
454
|
|
|
455
|
+
test("prioridade soma ao churn (linhas equivalentes) sem virar tier rígido", () => {
|
|
456
|
+
const priorities = [{ pattern: /^server\/db\//, bonus: 300 }];
|
|
457
|
+
const files = ["client/big.ts", "server/db/small.ts"];
|
|
458
|
+
expect(
|
|
459
|
+
sortByRisk(
|
|
460
|
+
files,
|
|
461
|
+
{
|
|
462
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
463
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
464
|
+
},
|
|
465
|
+
priorities,
|
|
466
|
+
),
|
|
467
|
+
).toEqual(["server/db/small.ts", "client/big.ts"]);
|
|
468
|
+
// Churn puro continua vencendo quando é muito maior (400 > 10 + 300).
|
|
469
|
+
expect(
|
|
470
|
+
sortByRisk(
|
|
471
|
+
files,
|
|
472
|
+
{
|
|
473
|
+
"client/big.ts": { additions: 400, deletions: 0 },
|
|
474
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
475
|
+
},
|
|
476
|
+
priorities,
|
|
477
|
+
),
|
|
478
|
+
).toEqual(["client/big.ts", "server/db/small.ts"]);
|
|
479
|
+
// Sem prioridade configurada, comportamento antigo.
|
|
480
|
+
expect(
|
|
481
|
+
sortByRisk(files, {
|
|
482
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
483
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
484
|
+
}),
|
|
485
|
+
).toEqual(["client/big.ts", "server/db/small.ts"]);
|
|
486
|
+
// O orçamento respeita a prioridade.
|
|
487
|
+
expect(
|
|
488
|
+
selectByBudget(
|
|
489
|
+
files,
|
|
490
|
+
{
|
|
491
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
492
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
493
|
+
},
|
|
494
|
+
1,
|
|
495
|
+
priorities,
|
|
496
|
+
).selected,
|
|
497
|
+
).toEqual(["server/db/small.ts"]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("maxRiskFromChurn satura o superchurn de caminho com baixa criticidade", () => {
|
|
501
|
+
const priorities = [
|
|
502
|
+
{ pattern: /^server\/db\//, bonus: 300 },
|
|
503
|
+
{ pattern: /^client\//, maxRiskFromChurn: 120 },
|
|
504
|
+
];
|
|
505
|
+
const files = ["client/table.tsx", "server/db/migration.ts"];
|
|
506
|
+
const churn = {
|
|
507
|
+
"client/table.tsx": { additions: 1900, deletions: 100 },
|
|
508
|
+
"server/db/migration.ts": { additions: 20, deletions: 0 },
|
|
509
|
+
};
|
|
510
|
+
// 2.000 linhas de UI "boba" viram 120; 20 linhas de db valem 320.
|
|
511
|
+
expect(riskOf("client/table.tsx", churn, priorities)).toBe(120);
|
|
512
|
+
expect(sortByRisk(files, churn, priorities)).toEqual([
|
|
513
|
+
"server/db/migration.ts",
|
|
514
|
+
"client/table.tsx",
|
|
515
|
+
]);
|
|
516
|
+
// Abaixo do teto, o caminho mantém o peso natural do churn.
|
|
517
|
+
expect(
|
|
518
|
+
riskOf(
|
|
519
|
+
"client/table.tsx",
|
|
520
|
+
{
|
|
521
|
+
"client/table.tsx": { additions: 15, deletions: 0 },
|
|
522
|
+
},
|
|
523
|
+
priorities,
|
|
524
|
+
),
|
|
525
|
+
).toBe(15);
|
|
526
|
+
// Sem teto configurado, o churn puro continua vencendo.
|
|
527
|
+
expect(
|
|
528
|
+
riskOf("client/table.tsx", churn, [{ pattern: /^server\/db\//, bonus: 300 }]),
|
|
529
|
+
).toBe(2000);
|
|
530
|
+
});
|
|
531
|
+
|
|
454
532
|
test("maxFiles não-positivo desativa o orçamento", () => {
|
|
455
533
|
const { selected, overflow } = selectByBudget(["a.ts", "b.ts"], churn, 0);
|
|
456
534
|
expect(selected).toHaveLength(2);
|
|
@@ -572,196 +650,35 @@ describe("report-builder", () => {
|
|
|
572
650
|
expect(merged[0].message).toBe("maior");
|
|
573
651
|
});
|
|
574
652
|
|
|
575
|
-
test("
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
expect(renderReport("main", reports, [], {})).toContain("✅ Aprovado");
|
|
581
|
-
|
|
582
|
-
const blockers = renderReport(
|
|
583
|
-
"main",
|
|
584
|
-
reports,
|
|
585
|
-
[
|
|
586
|
-
{
|
|
587
|
-
rule: "Regra bloqueante",
|
|
588
|
-
severity: "blocking",
|
|
589
|
-
file: "s.ts",
|
|
590
|
-
message: "m",
|
|
591
|
-
action: "a",
|
|
592
|
-
watchdogs: ["alpha"],
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
{},
|
|
596
|
-
);
|
|
597
|
-
expect(blockers).toContain("❌ Mudanças Necessárias");
|
|
598
|
-
expect(blockers).toContain("## ❌ blocking");
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
test("renderReport mostra cobertura pendente", () => {
|
|
602
|
-
const reports: WatchdogReport[] = [
|
|
603
|
-
{ watchdogId: "alpha", reviewedFiles: ["s.ts"], skipped: [], findings: [] },
|
|
604
|
-
];
|
|
605
|
-
const md = renderReport("main", reports, [], {
|
|
606
|
-
classified: { alpha: ["s.ts", "t.ts"] },
|
|
607
|
-
});
|
|
608
|
-
expect(md).toContain("faltam 1");
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
test("renderReport usa o resolver de nome injetado", () => {
|
|
612
|
-
const reports: WatchdogReport[] = [
|
|
613
|
-
{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] },
|
|
614
|
-
];
|
|
615
|
-
const md = renderReport("main", reports, [], {}, undefined, undefined, (id) =>
|
|
616
|
-
id === "alpha" ? "Watchdog Alpha" : id,
|
|
617
|
-
);
|
|
618
|
-
expect(md).toContain("Watchdog Alpha");
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test("renderReport agrupa achados por arquivo", () => {
|
|
622
|
-
const merged = [
|
|
623
|
-
{
|
|
624
|
-
rule: "R1",
|
|
625
|
-
severity: "blocking" as const,
|
|
626
|
-
file: "a.ts",
|
|
627
|
-
line: 1,
|
|
628
|
-
message: "m",
|
|
629
|
-
action: "a",
|
|
630
|
-
watchdogs: ["alpha"],
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
rule: "R2",
|
|
634
|
-
severity: "blocking" as const,
|
|
635
|
-
file: "b.ts",
|
|
636
|
-
line: 2,
|
|
637
|
-
message: "m",
|
|
638
|
-
action: "a",
|
|
639
|
-
watchdogs: ["alpha"],
|
|
640
|
-
},
|
|
641
|
-
];
|
|
642
|
-
const md = renderReport(
|
|
643
|
-
"main",
|
|
644
|
-
[{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] }],
|
|
645
|
-
merged,
|
|
646
|
-
{},
|
|
647
|
-
);
|
|
648
|
-
expect(md).toContain("### `a.ts`");
|
|
649
|
-
expect(md).toContain("### `b.ts`");
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
test("renderReport indexa os relatórios por domínio", () => {
|
|
653
|
-
const md = renderReport(
|
|
654
|
-
"main",
|
|
655
|
-
[{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] }],
|
|
656
|
-
[],
|
|
657
|
-
{},
|
|
658
|
-
undefined,
|
|
659
|
-
undefined,
|
|
660
|
-
undefined,
|
|
661
|
-
[
|
|
662
|
-
{
|
|
663
|
-
id: "alpha",
|
|
664
|
-
name: "Alpha",
|
|
665
|
-
file: "review.alpha.md",
|
|
666
|
-
findings: 2,
|
|
667
|
-
reviewedFiles: 3,
|
|
668
|
-
expectedFiles: 5,
|
|
669
|
-
},
|
|
670
|
-
],
|
|
653
|
+
test("cobertura ignora arquivos declarados fora do escopo", () => {
|
|
654
|
+
const coverage = coverageOf(
|
|
655
|
+
["src/a.ts", "src/b.ts", "src/outro.ts", "src/a.ts"],
|
|
656
|
+
["src/a.ts", "src/b.ts", "src/c.ts"],
|
|
671
657
|
);
|
|
672
|
-
expect(
|
|
673
|
-
expect(md).toContain("review.alpha.md");
|
|
658
|
+
expect(coverage).toEqual({ reviewed: 2, expected: 3 });
|
|
674
659
|
});
|
|
675
660
|
|
|
676
|
-
test("
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
file: "a.ts",
|
|
685
|
-
line: 1,
|
|
686
|
-
message: "m",
|
|
687
|
-
action: "a",
|
|
688
|
-
watchdogs: ["alpha"],
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
{ classified: { alpha: ["a.ts"] } },
|
|
692
|
-
null,
|
|
693
|
-
undefined,
|
|
694
|
-
undefined,
|
|
695
|
-
[
|
|
696
|
-
{
|
|
697
|
-
id: "alpha",
|
|
698
|
-
name: "Alpha",
|
|
699
|
-
file: "review.alpha.md",
|
|
700
|
-
findings: 1,
|
|
701
|
-
reviewedFiles: 1,
|
|
702
|
-
expectedFiles: 1,
|
|
703
|
-
},
|
|
704
|
-
],
|
|
705
|
-
);
|
|
706
|
-
const sections = [
|
|
707
|
-
"## Relatórios por domínio",
|
|
708
|
-
"## Cobertura",
|
|
709
|
-
"## Conclusão",
|
|
710
|
-
"## ❌ blocking",
|
|
711
|
-
].map((section) => md.indexOf(section));
|
|
712
|
-
expect(sections.every((index) => index >= 0)).toBe(true);
|
|
713
|
-
expect([...sections].sort((a, b) => a - b)).toEqual(sections);
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
test("renderReport anexa candidatos Jev depois dos achados", () => {
|
|
717
|
-
const md = renderReport(
|
|
718
|
-
"main",
|
|
719
|
-
[{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] }],
|
|
720
|
-
[
|
|
721
|
-
{
|
|
722
|
-
rule: "R1",
|
|
723
|
-
severity: "blocking" as const,
|
|
724
|
-
file: "a.ts",
|
|
725
|
-
line: 1,
|
|
726
|
-
message: "m",
|
|
727
|
-
action: "a",
|
|
728
|
-
watchdogs: ["alpha"],
|
|
729
|
-
},
|
|
730
|
-
],
|
|
731
|
-
{},
|
|
732
|
-
null,
|
|
733
|
-
[
|
|
734
|
-
{
|
|
735
|
-
watchdogId: "alpha",
|
|
736
|
-
rule: "R2",
|
|
737
|
-
severity: "attention",
|
|
738
|
-
file: "b.ts",
|
|
739
|
-
confidence: 0.6,
|
|
740
|
-
questionKey: "q",
|
|
741
|
-
},
|
|
742
|
-
],
|
|
743
|
-
);
|
|
744
|
-
expect(md.indexOf("## ❌ blocking")).toBeLessThan(
|
|
745
|
-
md.indexOf("## 🔎 Candidatos não verificados"),
|
|
746
|
-
);
|
|
661
|
+
test("lista arquivos sem revisão e sem declaração em skipped", () => {
|
|
662
|
+
expect(
|
|
663
|
+
unaccountedFiles(
|
|
664
|
+
["src/a.ts", "src/fora.ts"],
|
|
665
|
+
["src/c.ts"],
|
|
666
|
+
["src/a.ts", "src/c.ts", "src/b.ts"],
|
|
667
|
+
),
|
|
668
|
+
).toEqual(["src/b.ts"]);
|
|
747
669
|
});
|
|
748
670
|
|
|
749
|
-
test("
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
[],
|
|
759
|
-
|
|
760
|
-
null,
|
|
761
|
-
);
|
|
762
|
-
expect(md).toContain("1/2");
|
|
763
|
-
expect(md).toContain("Não revisados");
|
|
764
|
-
expect(md).toContain("Nenhum achado");
|
|
671
|
+
test("avisa quando um arquivo entra em vários domínios", () => {
|
|
672
|
+
expect(
|
|
673
|
+
overlapMessage({
|
|
674
|
+
watchdogs: { alpha: ["a.ts", "b.ts"], beta: ["b.ts"], gamma: [] },
|
|
675
|
+
}),
|
|
676
|
+
).toEqual([
|
|
677
|
+
"Escopo: 1 arquivo(s) em mais de um domínio (máx. 2) — cada domínio revisa o arquivo com o seu recorte.",
|
|
678
|
+
]);
|
|
679
|
+
expect(
|
|
680
|
+
overlapMessage({ watchdogs: { alpha: ["a.ts"], beta: ["b.ts"] } }),
|
|
681
|
+
).toEqual([]);
|
|
765
682
|
});
|
|
766
683
|
});
|
|
767
684
|
|
|
@@ -803,35 +720,6 @@ describe("context-summary", () => {
|
|
|
803
720
|
expect(summary?.objective?.endsWith("…")).toBe(true);
|
|
804
721
|
expect(summary?.changeSummary).toHaveLength(6);
|
|
805
722
|
});
|
|
806
|
-
|
|
807
|
-
test("renderReport coloca a seção de contexto quando presente", () => {
|
|
808
|
-
const reports: WatchdogReport[] = [
|
|
809
|
-
{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] },
|
|
810
|
-
];
|
|
811
|
-
const md = renderReport(
|
|
812
|
-
"main",
|
|
813
|
-
reports,
|
|
814
|
-
[],
|
|
815
|
-
{},
|
|
816
|
-
{
|
|
817
|
-
available: true,
|
|
818
|
-
objective: "Entregar X",
|
|
819
|
-
motivation: "Reduzir Y",
|
|
820
|
-
changeSummary: ["Faz A"],
|
|
821
|
-
},
|
|
822
|
-
);
|
|
823
|
-
expect(md).toContain("## Contexto da PR");
|
|
824
|
-
expect(md).toContain("**Objetivo:** Entregar X");
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
test("renderReport omite a seção sem contexto", () => {
|
|
828
|
-
const reports: WatchdogReport[] = [
|
|
829
|
-
{ watchdogId: "alpha", reviewedFiles: [], skipped: [], findings: [] },
|
|
830
|
-
];
|
|
831
|
-
expect(renderReport("main", reports, [], {}, null)).not.toContain(
|
|
832
|
-
"## Contexto da PR",
|
|
833
|
-
);
|
|
834
|
-
});
|
|
835
723
|
});
|
|
836
724
|
|
|
837
725
|
// ---------------------------------------------------------------------------
|
|
@@ -894,7 +782,7 @@ describe("houndsman brief", () => {
|
|
|
894
782
|
expect(brief).toContain("**Objetivo:** Entregar X");
|
|
895
783
|
});
|
|
896
784
|
|
|
897
|
-
test("mostra churn e lista arquivos fora do
|
|
785
|
+
test("mostra churn e lista arquivos fora do recorte", () => {
|
|
898
786
|
const brief = renderBrief(alphaWatchdog, {
|
|
899
787
|
watchdogId: "alpha",
|
|
900
788
|
files: ["src/alpha/keep.ts"],
|
|
@@ -908,7 +796,7 @@ describe("houndsman brief", () => {
|
|
|
908
796
|
deleted: [],
|
|
909
797
|
});
|
|
910
798
|
expect(brief).toContain("+12/−3");
|
|
911
|
-
expect(brief).toContain("Fora do
|
|
799
|
+
expect(brief).toContain("Fora do recorte");
|
|
912
800
|
expect(brief).toContain("src/alpha/skip.ts");
|
|
913
801
|
});
|
|
914
802
|
|
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
|
});
|