@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.
- 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/golden-command.ts +92 -0
- package/src/cli/commands/hunt-command.ts +105 -4
- package/src/cli/commands/review-command.ts +38 -0
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +6 -1
- package/src/cli/index.ts +5 -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 +278 -16
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +266 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/golden.ts +176 -0
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +29 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +310 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +41 -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 +371 -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 +22 -2
- package/src/core/github-review.ts +5 -2
- package/src/core/golden.ts +346 -0
- package/src/core/hunt/hunt-runner.ts +621 -34
- package/src/core/hunt/index.ts +40 -1
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-prompt.ts +298 -5
- package/src/core/hunt/maki-runner.ts +93 -16
- package/src/core/hunt/pool.ts +4 -26
- package/src/core/hunt/review-checks.ts +460 -0
- 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 +18 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +90 -27
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +116 -278
- package/src/core/severity.ts +245 -0
- 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 +51 -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 +55 -10
- package/src/skill/README.md +19 -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 +205 -58
- package/src/skill/train.md +3 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +208 -184
- 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 +256 -0
- package/src/tests/findings-state.test.ts +82 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/golden.test.ts +195 -0
- package/src/tests/hunt-maki.test.ts +710 -2
- 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 +125 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/severity.test.ts +235 -0
- 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";
|
|
@@ -48,6 +49,7 @@ import { findWatchdog, getWatchdogContext } from "../core/watchdog-registry";
|
|
|
48
49
|
|
|
49
50
|
import {
|
|
50
51
|
alphaWatchdog,
|
|
52
|
+
betaWatchdog,
|
|
51
53
|
FAKE_WATCHDOG_DEFINITIONS,
|
|
52
54
|
fakeHoundsmanConfig,
|
|
53
55
|
} from "./fixtures/fake-project";
|
|
@@ -158,6 +160,24 @@ describe("registry-check", () => {
|
|
|
158
160
|
expect(warnings.some((w) => w.includes("duplicada"))).toBe(true);
|
|
159
161
|
});
|
|
160
162
|
|
|
163
|
+
test("mesma regra com severidades diferentes entre watchdogs gera aviso", () => {
|
|
164
|
+
const config = {
|
|
165
|
+
...fakeHoundsmanConfig,
|
|
166
|
+
watchdogs: {
|
|
167
|
+
...fakeHoundsmanConfig.watchdogs,
|
|
168
|
+
beta: {
|
|
169
|
+
...betaWatchdog,
|
|
170
|
+
rules: [
|
|
171
|
+
...betaWatchdog.rules,
|
|
172
|
+
{ severity: "suggestion" as const, text: "Nunca usar `any`." },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
178
|
+
expect(warnings.some((w) => w.includes("severidades divergentes"))).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
161
181
|
test("texto de regra em reviewFocus/contextFiles gera aviso", () => {
|
|
162
182
|
const config = {
|
|
163
183
|
...fakeHoundsmanConfig,
|
|
@@ -451,6 +471,83 @@ describe("triage", () => {
|
|
|
451
471
|
expect(overflow).toEqual(["a.ts"]);
|
|
452
472
|
});
|
|
453
473
|
|
|
474
|
+
test("prioridade soma ao churn (linhas equivalentes) sem virar tier rígido", () => {
|
|
475
|
+
const priorities = [{ pattern: /^server\/db\//, bonus: 300 }];
|
|
476
|
+
const files = ["client/big.ts", "server/db/small.ts"];
|
|
477
|
+
expect(
|
|
478
|
+
sortByRisk(
|
|
479
|
+
files,
|
|
480
|
+
{
|
|
481
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
482
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
483
|
+
},
|
|
484
|
+
priorities,
|
|
485
|
+
),
|
|
486
|
+
).toEqual(["server/db/small.ts", "client/big.ts"]);
|
|
487
|
+
// Churn puro continua vencendo quando é muito maior (400 > 10 + 300).
|
|
488
|
+
expect(
|
|
489
|
+
sortByRisk(
|
|
490
|
+
files,
|
|
491
|
+
{
|
|
492
|
+
"client/big.ts": { additions: 400, deletions: 0 },
|
|
493
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
494
|
+
},
|
|
495
|
+
priorities,
|
|
496
|
+
),
|
|
497
|
+
).toEqual(["client/big.ts", "server/db/small.ts"]);
|
|
498
|
+
// Sem prioridade configurada, comportamento antigo.
|
|
499
|
+
expect(
|
|
500
|
+
sortByRisk(files, {
|
|
501
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
502
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
503
|
+
}),
|
|
504
|
+
).toEqual(["client/big.ts", "server/db/small.ts"]);
|
|
505
|
+
// O orçamento respeita a prioridade.
|
|
506
|
+
expect(
|
|
507
|
+
selectByBudget(
|
|
508
|
+
files,
|
|
509
|
+
{
|
|
510
|
+
"client/big.ts": { additions: 200, deletions: 0 },
|
|
511
|
+
"server/db/small.ts": { additions: 10, deletions: 0 },
|
|
512
|
+
},
|
|
513
|
+
1,
|
|
514
|
+
priorities,
|
|
515
|
+
).selected,
|
|
516
|
+
).toEqual(["server/db/small.ts"]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("maxRiskFromChurn satura o superchurn de caminho com baixa criticidade", () => {
|
|
520
|
+
const priorities = [
|
|
521
|
+
{ pattern: /^server\/db\//, bonus: 300 },
|
|
522
|
+
{ pattern: /^client\//, maxRiskFromChurn: 120 },
|
|
523
|
+
];
|
|
524
|
+
const files = ["client/table.tsx", "server/db/migration.ts"];
|
|
525
|
+
const churn = {
|
|
526
|
+
"client/table.tsx": { additions: 1900, deletions: 100 },
|
|
527
|
+
"server/db/migration.ts": { additions: 20, deletions: 0 },
|
|
528
|
+
};
|
|
529
|
+
// 2.000 linhas de UI "boba" viram 120; 20 linhas de db valem 320.
|
|
530
|
+
expect(riskOf("client/table.tsx", churn, priorities)).toBe(120);
|
|
531
|
+
expect(sortByRisk(files, churn, priorities)).toEqual([
|
|
532
|
+
"server/db/migration.ts",
|
|
533
|
+
"client/table.tsx",
|
|
534
|
+
]);
|
|
535
|
+
// Abaixo do teto, o caminho mantém o peso natural do churn.
|
|
536
|
+
expect(
|
|
537
|
+
riskOf(
|
|
538
|
+
"client/table.tsx",
|
|
539
|
+
{
|
|
540
|
+
"client/table.tsx": { additions: 15, deletions: 0 },
|
|
541
|
+
},
|
|
542
|
+
priorities,
|
|
543
|
+
),
|
|
544
|
+
).toBe(15);
|
|
545
|
+
// Sem teto configurado, o churn puro continua vencendo.
|
|
546
|
+
expect(
|
|
547
|
+
riskOf("client/table.tsx", churn, [{ pattern: /^server\/db\//, bonus: 300 }]),
|
|
548
|
+
).toBe(2000);
|
|
549
|
+
});
|
|
550
|
+
|
|
454
551
|
test("maxFiles não-positivo desativa o orçamento", () => {
|
|
455
552
|
const { selected, overflow } = selectByBudget(["a.ts", "b.ts"], churn, 0);
|
|
456
553
|
expect(selected).toHaveLength(2);
|
|
@@ -572,196 +669,35 @@ describe("report-builder", () => {
|
|
|
572
669
|
expect(merged[0].message).toBe("maior");
|
|
573
670
|
});
|
|
574
671
|
|
|
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
|
-
],
|
|
672
|
+
test("cobertura ignora arquivos declarados fora do escopo", () => {
|
|
673
|
+
const coverage = coverageOf(
|
|
674
|
+
["src/a.ts", "src/b.ts", "src/outro.ts", "src/a.ts"],
|
|
675
|
+
["src/a.ts", "src/b.ts", "src/c.ts"],
|
|
671
676
|
);
|
|
672
|
-
expect(
|
|
673
|
-
expect(md).toContain("review.alpha.md");
|
|
677
|
+
expect(coverage).toEqual({ reviewed: 2, expected: 3 });
|
|
674
678
|
});
|
|
675
679
|
|
|
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
|
-
);
|
|
680
|
+
test("lista arquivos sem revisão e sem declaração em skipped", () => {
|
|
681
|
+
expect(
|
|
682
|
+
unaccountedFiles(
|
|
683
|
+
["src/a.ts", "src/fora.ts"],
|
|
684
|
+
["src/c.ts"],
|
|
685
|
+
["src/a.ts", "src/c.ts", "src/b.ts"],
|
|
686
|
+
),
|
|
687
|
+
).toEqual(["src/b.ts"]);
|
|
747
688
|
});
|
|
748
689
|
|
|
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");
|
|
690
|
+
test("avisa quando um arquivo entra em vários domínios", () => {
|
|
691
|
+
expect(
|
|
692
|
+
overlapMessage({
|
|
693
|
+
watchdogs: { alpha: ["a.ts", "b.ts"], beta: ["b.ts"], gamma: [] },
|
|
694
|
+
}),
|
|
695
|
+
).toEqual([
|
|
696
|
+
"Escopo: 1 arquivo(s) em mais de um domínio (máx. 2) — cada domínio revisa o arquivo com o seu recorte.",
|
|
697
|
+
]);
|
|
698
|
+
expect(
|
|
699
|
+
overlapMessage({ watchdogs: { alpha: ["a.ts"], beta: ["b.ts"] } }),
|
|
700
|
+
).toEqual([]);
|
|
765
701
|
});
|
|
766
702
|
});
|
|
767
703
|
|
|
@@ -803,35 +739,6 @@ describe("context-summary", () => {
|
|
|
803
739
|
expect(summary?.objective?.endsWith("…")).toBe(true);
|
|
804
740
|
expect(summary?.changeSummary).toHaveLength(6);
|
|
805
741
|
});
|
|
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
742
|
});
|
|
836
743
|
|
|
837
744
|
// ---------------------------------------------------------------------------
|
|
@@ -894,7 +801,7 @@ describe("houndsman brief", () => {
|
|
|
894
801
|
expect(brief).toContain("**Objetivo:** Entregar X");
|
|
895
802
|
});
|
|
896
803
|
|
|
897
|
-
test("mostra churn e lista arquivos fora do
|
|
804
|
+
test("mostra churn e lista arquivos fora do recorte", () => {
|
|
898
805
|
const brief = renderBrief(alphaWatchdog, {
|
|
899
806
|
watchdogId: "alpha",
|
|
900
807
|
files: ["src/alpha/keep.ts"],
|
|
@@ -908,7 +815,7 @@ describe("houndsman brief", () => {
|
|
|
908
815
|
deleted: [],
|
|
909
816
|
});
|
|
910
817
|
expect(brief).toContain("+12/−3");
|
|
911
|
-
expect(brief).toContain("Fora do
|
|
818
|
+
expect(brief).toContain("Fora do recorte");
|
|
912
819
|
expect(brief).toContain("src/alpha/skip.ts");
|
|
913
820
|
});
|
|
914
821
|
|
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
|
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Severidade determinística: ancoragem por regra e por classe de impacto. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import type { Finding, WatchdogReport } from "../core/report-builder";
|
|
5
|
+
import {
|
|
6
|
+
anchorReports,
|
|
7
|
+
IMPACT_CLASSES,
|
|
8
|
+
normalizeRuleText,
|
|
9
|
+
resolveDeclaredSeverity,
|
|
10
|
+
SEVERITY_BY_IMPACT,
|
|
11
|
+
severityAnchoringMessages,
|
|
12
|
+
} from "../core/severity";
|
|
13
|
+
import type { Rule } from "../core/types";
|
|
14
|
+
import { FAKE_WATCHDOG_DEFINITIONS } from "./fixtures/fake-project";
|
|
15
|
+
|
|
16
|
+
/** União das regras do projeto fake (o mesmo que o finalize monta). */
|
|
17
|
+
function declaredRules(): Rule[] {
|
|
18
|
+
return Object.values(FAKE_WATCHDOG_DEFINITIONS).flatMap((def) => [...def.rules]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function finding(partial: Partial<Finding> & { rule: string }): Finding {
|
|
22
|
+
return {
|
|
23
|
+
rule: partial.rule,
|
|
24
|
+
severity: partial.severity ?? "attention",
|
|
25
|
+
file: partial.file ?? "src/alpha/a.ts",
|
|
26
|
+
line: partial.line,
|
|
27
|
+
message: partial.message ?? "msg",
|
|
28
|
+
action: partial.action ?? "ação",
|
|
29
|
+
basis: partial.basis,
|
|
30
|
+
impactClass: partial.impactClass,
|
|
31
|
+
evidence: partial.evidence,
|
|
32
|
+
scenario: partial.scenario,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function report(findings: Finding[], watchdogId = "alpha"): WatchdogReport {
|
|
37
|
+
return {
|
|
38
|
+
watchdogId,
|
|
39
|
+
reviewedFiles: ["src/alpha/a.ts"],
|
|
40
|
+
skipped: [],
|
|
41
|
+
findings,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("normalizeRuleText", () => {
|
|
46
|
+
test("remove markdown, emoji, bullet e prefixo de severidade do brief", () => {
|
|
47
|
+
expect(
|
|
48
|
+
normalizeRuleText(
|
|
49
|
+
"- ❌ **blocking** — Promover: todo fluxo tem caminho completo.",
|
|
50
|
+
),
|
|
51
|
+
).toBe("promover: todo fluxo tem caminho completo");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("colapsa espaços e ignora caixa", () => {
|
|
55
|
+
expect(normalizeRuleText("Evitar CONSOLE.log")).toBe("evitar console.log");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("resolveDeclaredSeverity", () => {
|
|
60
|
+
test("casa pelo texto exato normalizado", () => {
|
|
61
|
+
expect(
|
|
62
|
+
resolveDeclaredSeverity("Nunca usar `any`.", declaredRules())?.severity,
|
|
63
|
+
).toBe("blocking");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("casa quando o watchdog cita um trecho da regra", () => {
|
|
67
|
+
const declared = resolveDeclaredSeverity(
|
|
68
|
+
"Vale para todo handler: validar entrada do usuário",
|
|
69
|
+
declaredRules(),
|
|
70
|
+
);
|
|
71
|
+
expect(declared?.severity).toBe("attention");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("texto sem casamento devolve null", () => {
|
|
75
|
+
expect(
|
|
76
|
+
resolveDeclaredSeverity(
|
|
77
|
+
"Regra inventada que não está no brief.",
|
|
78
|
+
declaredRules(),
|
|
79
|
+
),
|
|
80
|
+
).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("mesmo texto em dois watchdogs fica com a severidade mais grave", () => {
|
|
84
|
+
const rules: Rule[] = [
|
|
85
|
+
{ severity: "attention", text: "Promover: todo fluxo tem caminho completo." },
|
|
86
|
+
{ severity: "blocking", text: "Promover: todo fluxo tem caminho completo." },
|
|
87
|
+
];
|
|
88
|
+
expect(
|
|
89
|
+
resolveDeclaredSeverity("Promover: todo fluxo tem caminho completo.", rules)
|
|
90
|
+
?.severity,
|
|
91
|
+
).toBe("blocking");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("impactClass", () => {
|
|
96
|
+
test("toda classe tem severidade mapeada", () => {
|
|
97
|
+
expect(IMPACT_CLASSES).toHaveLength(4);
|
|
98
|
+
expect(SEVERITY_BY_IMPACT["flow-break"]).toBe("blocking");
|
|
99
|
+
expect(SEVERITY_BY_IMPACT["contract-drift"]).toBe("attention");
|
|
100
|
+
expect(SEVERITY_BY_IMPACT.degradation).toBe("suggestion");
|
|
101
|
+
expect(SEVERITY_BY_IMPACT.cosmetic).toBe("nit");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("anchorReports", () => {
|
|
106
|
+
test("finding de regra assume a severidade declarada e registra a divergência", () => {
|
|
107
|
+
const anchoring = anchorReports(
|
|
108
|
+
[report([finding({ rule: "Nunca usar `any`.", severity: "attention" })])],
|
|
109
|
+
declaredRules(),
|
|
110
|
+
);
|
|
111
|
+
expect(anchoring.reports[0]?.findings[0]?.severity).toBe("blocking");
|
|
112
|
+
expect(anchoring.anchored).toBe(1);
|
|
113
|
+
expect(anchoring.diverged).toHaveLength(1);
|
|
114
|
+
expect(anchoring.diverged[0]).toMatchObject({
|
|
115
|
+
emitted: "attention",
|
|
116
|
+
resolved: "blocking",
|
|
117
|
+
source: "rule",
|
|
118
|
+
watchdogId: "alpha",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("finding de regra sem casamento mantém o valor e entra em unmatched", () => {
|
|
123
|
+
const anchoring = anchorReports(
|
|
124
|
+
[
|
|
125
|
+
report([
|
|
126
|
+
finding({
|
|
127
|
+
rule: "Regra parafraseada fora do brief",
|
|
128
|
+
severity: "nit",
|
|
129
|
+
}),
|
|
130
|
+
]),
|
|
131
|
+
],
|
|
132
|
+
declaredRules(),
|
|
133
|
+
);
|
|
134
|
+
expect(anchoring.reports[0]?.findings[0]?.severity).toBe("nit");
|
|
135
|
+
expect(anchoring.unmatched).toBe(1);
|
|
136
|
+
expect(anchoring.diverged).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("emergente com flow-break e prova vira blocking", () => {
|
|
140
|
+
const anchoring = anchorReports(
|
|
141
|
+
[
|
|
142
|
+
report([
|
|
143
|
+
finding({
|
|
144
|
+
rule: "Lógica: renovação não fecha o fluxo",
|
|
145
|
+
severity: "suggestion",
|
|
146
|
+
basis: "logic",
|
|
147
|
+
impactClass: "flow-break",
|
|
148
|
+
scenario: "renovação de assinatura",
|
|
149
|
+
evidence: "await renew(data)",
|
|
150
|
+
}),
|
|
151
|
+
]),
|
|
152
|
+
],
|
|
153
|
+
declaredRules(),
|
|
154
|
+
);
|
|
155
|
+
expect(anchoring.reports[0]?.findings[0]?.severity).toBe("blocking");
|
|
156
|
+
expect(anchoring.classified).toBe(1);
|
|
157
|
+
expect(anchoring.diverged[0]).toMatchObject({
|
|
158
|
+
source: "impact",
|
|
159
|
+
resolved: "blocking",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("flow-break sem cenário+evidência cai para attention", () => {
|
|
164
|
+
const anchoring = anchorReports(
|
|
165
|
+
[
|
|
166
|
+
report([
|
|
167
|
+
finding({
|
|
168
|
+
rule: "Domínio: estado sem tratamento",
|
|
169
|
+
severity: "suggestion",
|
|
170
|
+
basis: "domain",
|
|
171
|
+
impactClass: "flow-break",
|
|
172
|
+
}),
|
|
173
|
+
]),
|
|
174
|
+
],
|
|
175
|
+
declaredRules(),
|
|
176
|
+
);
|
|
177
|
+
expect(anchoring.reports[0]?.findings[0]?.severity).toBe("attention");
|
|
178
|
+
expect(anchoring.diverged[0]?.resolved).toBe("attention");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("emergente sem impactClass mantém o valor emitido", () => {
|
|
182
|
+
const anchoring = anchorReports(
|
|
183
|
+
[
|
|
184
|
+
report([
|
|
185
|
+
finding({
|
|
186
|
+
rule: "Domínio: algo",
|
|
187
|
+
severity: "nit",
|
|
188
|
+
basis: "domain",
|
|
189
|
+
impactClass: "desconhecida",
|
|
190
|
+
}),
|
|
191
|
+
]),
|
|
192
|
+
],
|
|
193
|
+
declaredRules(),
|
|
194
|
+
);
|
|
195
|
+
expect(anchoring.reports[0]?.findings[0]?.severity).toBe("nit");
|
|
196
|
+
expect(anchoring.classified).toBe(0);
|
|
197
|
+
expect(anchoring.unmatched).toBe(0);
|
|
198
|
+
expect(anchoring.diverged).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("severityAnchoringMessages", () => {
|
|
203
|
+
test("resume contagens, exemplos e a falta de casamento", () => {
|
|
204
|
+
const anchoring = anchorReports(
|
|
205
|
+
[
|
|
206
|
+
report([
|
|
207
|
+
finding({ rule: "Nunca usar `any`.", severity: "attention" }),
|
|
208
|
+
finding({
|
|
209
|
+
rule: "Lógica: fluxo não fecha",
|
|
210
|
+
severity: "nit",
|
|
211
|
+
basis: "logic",
|
|
212
|
+
impactClass: "flow-break",
|
|
213
|
+
scenario: "gatilho",
|
|
214
|
+
evidence: "trecho",
|
|
215
|
+
}),
|
|
216
|
+
finding({ rule: "Regra inventada", severity: "nit" }),
|
|
217
|
+
]),
|
|
218
|
+
],
|
|
219
|
+
declaredRules(),
|
|
220
|
+
);
|
|
221
|
+
const messages = severityAnchoringMessages(anchoring);
|
|
222
|
+
expect(messages[0]).toContain("1 pela regra");
|
|
223
|
+
expect(messages[0]).toContain("1 pelo impacto");
|
|
224
|
+
expect(messages[0]).toContain("2 divergência(s)");
|
|
225
|
+
expect(messages.some((message) => message.includes("sem casamento"))).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("silencia quando nada é resolvido", () => {
|
|
229
|
+
const anchoring = anchorReports(
|
|
230
|
+
[report([finding({ rule: "Lógica: x", basis: "logic" })])],
|
|
231
|
+
declaredRules(),
|
|
232
|
+
);
|
|
233
|
+
expect(severityAnchoringMessages(anchoring)).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
});
|