popeye-cli 2.2.0 → 2.7.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/dist/adapters/gemini.d.ts +14 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +41 -6
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +14 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +42 -6
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +44 -5
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +324 -20
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +3 -2
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +21 -6
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +55 -4
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/templates/fullstack.js +1 -1
- package/dist/generators/templates/website-components.js +1 -1
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +4 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +17 -11
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-conversion.js +1 -1
- package/dist/generators/templates/website-conversion.js.map +1 -1
- package/dist/generators/templates/website-landing.js +1 -1
- package/dist/generators/templates/website-landing.js.map +1 -1
- package/dist/generators/templates/website-layout.d.ts +36 -4
- package/dist/generators/templates/website-layout.d.ts.map +1 -1
- package/dist/generators/templates/website-layout.js +466 -23
- package/dist/generators/templates/website-layout.js.map +1 -1
- package/dist/generators/templates/website-pricing.js +1 -1
- package/dist/generators/templates/website-pricing.js.map +1 -1
- package/dist/generators/templates/website-sections.js +1 -1
- package/dist/generators/templates/website-sections.js.map +1 -1
- package/dist/generators/templates/website-seo.d.ts.map +1 -1
- package/dist/generators/templates/website-seo.js +4 -1
- package/dist/generators/templates/website-seo.js.map +1 -1
- package/dist/generators/templates/website.d.ts +1 -1
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +1 -1
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-ai.d.ts +52 -0
- package/dist/generators/website-content-ai.d.ts.map +1 -0
- package/dist/generators/website-content-ai.js +141 -0
- package/dist/generators/website-content-ai.js.map +1 -0
- package/dist/generators/website-content-scanner.d.ts +1 -1
- package/dist/generators/website-content-scanner.d.ts.map +1 -1
- package/dist/generators/website-content-scanner.js +98 -1
- package/dist/generators/website-content-scanner.js.map +1 -1
- package/dist/generators/website-context.d.ts +34 -1
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +131 -9
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +12 -0
- package/dist/generators/website-debug.d.ts.map +1 -1
- package/dist/generators/website-debug.js +16 -0
- package/dist/generators/website-debug.js.map +1 -1
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +26 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/pipeline/auto-recovery.d.ts +56 -0
- package/dist/pipeline/auto-recovery.d.ts.map +1 -0
- package/dist/pipeline/auto-recovery.js +185 -0
- package/dist/pipeline/auto-recovery.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +39 -0
- package/dist/pipeline/change-request.d.ts.map +1 -1
- package/dist/pipeline/change-request.js +40 -1
- package/dist/pipeline/change-request.js.map +1 -1
- package/dist/pipeline/check-runner.d.ts +30 -1
- package/dist/pipeline/check-runner.d.ts.map +1 -1
- package/dist/pipeline/check-runner.js +122 -1
- package/dist/pipeline/check-runner.js.map +1 -1
- package/dist/pipeline/command-resolver.d.ts.map +1 -1
- package/dist/pipeline/command-resolver.js +33 -2
- package/dist/pipeline/command-resolver.js.map +1 -1
- package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
- package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
- package/dist/pipeline/consensus/arbitrator-query.js +70 -0
- package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
- package/dist/pipeline/consensus/consensus-runner.js +809 -35
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/cr-lifecycle.d.ts +42 -0
- package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
- package/dist/pipeline/cr-lifecycle.js +89 -0
- package/dist/pipeline/cr-lifecycle.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +1 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -1
- package/dist/pipeline/gate-engine.js +26 -7
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +306 -16
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
- package/dist/pipeline/phases/architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/architecture.js +5 -3
- package/dist/pipeline/phases/architecture.js.map +1 -1
- package/dist/pipeline/phases/audit.d.ts.map +1 -1
- package/dist/pipeline/phases/audit.js +5 -3
- package/dist/pipeline/phases/audit.js.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.js +10 -1
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.js +10 -3
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.js +10 -1
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
- package/dist/pipeline/phases/done.d.ts.map +1 -1
- package/dist/pipeline/phases/done.js +9 -4
- package/dist/pipeline/phases/done.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +7 -3
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/phase-context.d.ts +2 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
- package/dist/pipeline/phases/phase-context.js +3 -1
- package/dist/pipeline/phases/phase-context.js.map +1 -1
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
- package/dist/pipeline/phases/production-gate.js +28 -3
- package/dist/pipeline/phases/production-gate.js.map +1 -1
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
- package/dist/pipeline/phases/qa-validation.js +38 -5
- package/dist/pipeline/phases/qa-validation.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +200 -6
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- package/dist/pipeline/phases/review.d.ts.map +1 -1
- package/dist/pipeline/phases/review.js +58 -28
- package/dist/pipeline/phases/review.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +18 -2
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/phases/stuck.d.ts.map +1 -1
- package/dist/pipeline/phases/stuck.js +10 -0
- package/dist/pipeline/phases/stuck.js.map +1 -1
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
- package/dist/pipeline/repo-snapshot.js +3 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -1
- package/dist/pipeline/role-execution-adapter.d.ts +2 -1
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
- package/dist/pipeline/role-execution-adapter.js +22 -7
- package/dist/pipeline/role-execution-adapter.js.map +1 -1
- package/dist/pipeline/skill-loader.d.ts +19 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -1
- package/dist/pipeline/skill-loader.js +22 -0
- package/dist/pipeline/skill-loader.js.map +1 -1
- package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
- package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
- package/dist/pipeline/skills/coverage-gate.js +143 -0
- package/dist/pipeline/skills/coverage-gate.js.map +1 -0
- package/dist/pipeline/skills/usage-registry.d.ts +48 -0
- package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
- package/dist/pipeline/skills/usage-registry.js +55 -0
- package/dist/pipeline/skills/usage-registry.js.map +1 -0
- package/dist/pipeline/strategy-context.d.ts +20 -0
- package/dist/pipeline/strategy-context.d.ts.map +1 -0
- package/dist/pipeline/strategy-context.js +55 -0
- package/dist/pipeline/strategy-context.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +25 -5
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +4 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +25 -13
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +18 -8
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.js +4 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +104 -18
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.js +17 -1
- package/dist/pipeline/type-defs/packets.js.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +160 -16
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +26 -1
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/shared/text-utils.d.ts +23 -0
- package/dist/shared/text-utils.d.ts.map +1 -0
- package/dist/shared/text-utils.js +66 -0
- package/dist/shared/text-utils.js.map +1 -0
- package/dist/shared/website-strategy-format.d.ts +18 -0
- package/dist/shared/website-strategy-format.d.ts.map +1 -0
- package/dist/shared/website-strategy-format.js +47 -0
- package/dist/shared/website-strategy-format.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +57 -8
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +1 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +1 -1
- package/dist/types/workflow.d.ts +447 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +3 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +6 -3
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +1 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +2 -29
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +3 -2
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +51 -6
- package/src/adapters/grok.ts +51 -6
- package/src/adapters/openai.ts +53 -5
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/interactive.ts +333 -19
- package/src/generators/all.ts +3 -2
- package/src/generators/doc-parser.ts +75 -15
- package/src/generators/templates/fullstack.ts +1 -1
- package/src/generators/templates/website-components.ts +1 -1
- package/src/generators/templates/website-config.ts +23 -11
- package/src/generators/templates/website-conversion.ts +1 -1
- package/src/generators/templates/website-landing.ts +1 -1
- package/src/generators/templates/website-layout.ts +491 -23
- package/src/generators/templates/website-pricing.ts +1 -1
- package/src/generators/templates/website-sections.ts +1 -1
- package/src/generators/templates/website-seo.ts +4 -1
- package/src/generators/templates/website.ts +3 -0
- package/src/generators/website-content-ai.ts +186 -0
- package/src/generators/website-content-scanner.ts +113 -1
- package/src/generators/website-context.ts +151 -12
- package/src/generators/website-debug.ts +26 -0
- package/src/generators/website.ts +28 -3
- package/src/pipeline/auto-recovery.ts +283 -0
- package/src/pipeline/change-request.ts +63 -1
- package/src/pipeline/check-runner.ts +141 -2
- package/src/pipeline/command-resolver.ts +34 -2
- package/src/pipeline/consensus/arbitrator-query.ts +101 -0
- package/src/pipeline/consensus/consensus-runner.ts +1099 -42
- package/src/pipeline/cr-lifecycle.ts +103 -0
- package/src/pipeline/gate-engine.ts +35 -7
- package/src/pipeline/orchestrator.ts +361 -16
- package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
- package/src/pipeline/phases/architecture.ts +6 -3
- package/src/pipeline/phases/audit.ts +6 -3
- package/src/pipeline/phases/consensus-architecture.ts +10 -1
- package/src/pipeline/phases/consensus-master-plan.ts +10 -3
- package/src/pipeline/phases/consensus-role-plans.ts +10 -1
- package/src/pipeline/phases/done.ts +15 -4
- package/src/pipeline/phases/intake.ts +7 -3
- package/src/pipeline/phases/phase-context.ts +6 -1
- package/src/pipeline/phases/production-gate.ts +41 -3
- package/src/pipeline/phases/qa-validation.ts +51 -5
- package/src/pipeline/phases/recovery-loop.ts +229 -7
- package/src/pipeline/phases/review.ts +73 -30
- package/src/pipeline/phases/role-planning.ts +21 -2
- package/src/pipeline/phases/stuck.ts +10 -0
- package/src/pipeline/repo-snapshot.ts +3 -0
- package/src/pipeline/role-execution-adapter.ts +30 -4
- package/src/pipeline/skill-loader.ts +33 -0
- package/src/pipeline/skills/coverage-gate.ts +199 -0
- package/src/pipeline/skills/usage-registry.ts +87 -0
- package/src/pipeline/strategy-context.ts +60 -0
- package/src/pipeline/type-defs/artifacts.ts +4 -0
- package/src/pipeline/type-defs/checks.ts +4 -0
- package/src/pipeline/type-defs/packets.ts +18 -1
- package/src/pipeline/type-defs/state.ts +26 -1
- package/src/shared/text-utils.ts +70 -0
- package/src/shared/website-strategy-format.ts +56 -0
- package/src/state/index.ts +60 -8
- package/src/types/consensus.ts +1 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +9 -3
- package/src/workflow/consensus.ts +1 -0
- package/src/workflow/website-strategy.ts +2 -36
- package/src/workflow/website-updater.ts +4 -2
- package/tests/adapters/gemini.test.ts +165 -0
- package/tests/adapters/grok.test.ts +137 -0
- package/tests/adapters/openai.test.ts +128 -0
- package/tests/generators/doc-parser.test.ts +88 -9
- package/tests/generators/quality-gate.test.ts +19 -3
- package/tests/generators/website-components.test.ts +34 -0
- package/tests/generators/website-content-ai.test.ts +308 -0
- package/tests/generators/website-content-scanner.test.ts +86 -0
- package/tests/generators/website-context.test.ts +3 -2
- package/tests/integration/smokestack-scaffold.test.ts +385 -0
- package/tests/pipeline/auto-recovery.test.ts +337 -0
- package/tests/pipeline/change-request.test.ts +70 -0
- package/tests/pipeline/command-resolver.test.ts +42 -0
- package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
- package/tests/pipeline/consensus-runner.test.ts +1333 -10
- package/tests/pipeline/consensus-scoring.test.ts +602 -18
- package/tests/pipeline/gate-engine.test.ts +34 -0
- package/tests/pipeline/install-check.test.ts +261 -0
- package/tests/pipeline/orchestrator.test.ts +1506 -15
- package/tests/pipeline/packets/builders.test.ts +29 -6
- package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
- package/tests/pipeline/pipeline-persistence.test.ts +230 -0
- package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
- package/tests/pipeline/role-execution-adapter.test.ts +88 -0
- package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
- package/tests/pipeline/skills/usage-registry.test.ts +114 -0
- package/tests/pipeline/strategy-context.test.ts +148 -0
- package/tests/shared/text-utils.test.ts +155 -0
- package/tests/state/progress-analysis.test.ts +375 -0
- package/tests/upgrade/handlers.test.ts +33 -2
- package/tests/workflow/consensus.test.ts +6 -0
- package/tsconfig.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Consensus Scoring tests —
|
|
3
|
-
*
|
|
2
|
+
* Consensus Scoring tests — Option B scoring, normalization, REJECT guard,
|
|
3
|
+
* force-zero semantics, backward compat.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
7
|
import { computeConsensusScore } from '../../src/pipeline/packets/consensus-packet-builder.js';
|
|
8
8
|
import { buildConsensusPacket } from '../../src/pipeline/packets/consensus-packet-builder.js';
|
|
9
|
+
import { normalizeVoteBlockers } from '../../src/pipeline/consensus/consensus-runner.js';
|
|
9
10
|
import type { ReviewerVote, ArtifactRef } from '../../src/pipeline/types.js';
|
|
10
11
|
|
|
11
12
|
function makeVote(overrides: Partial<ReviewerVote> = {}): ReviewerVote {
|
|
@@ -32,7 +33,7 @@ const mockRef: ArtifactRef = {
|
|
|
32
33
|
type: 'master_plan',
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
describe('computeConsensusScore', () => {
|
|
36
|
+
describe('computeConsensusScore (Option B)', () => {
|
|
36
37
|
it('should return 0 for empty votes', () => {
|
|
37
38
|
const result = computeConsensusScore([]);
|
|
38
39
|
expect(result.score).toBe(0);
|
|
@@ -46,7 +47,8 @@ describe('computeConsensusScore', () => {
|
|
|
46
47
|
];
|
|
47
48
|
const result = computeConsensusScore(votes);
|
|
48
49
|
expect(result.score).toBe(1.0);
|
|
49
|
-
|
|
50
|
+
// Option B: (1.0*0.9 + 1.0*0.8) / 2 = 0.85
|
|
51
|
+
expect(result.weighted_score).toBeCloseTo(0.85, 3);
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
it('should return 0 for all REJECT votes', () => {
|
|
@@ -65,33 +67,111 @@ describe('computeConsensusScore', () => {
|
|
|
65
67
|
];
|
|
66
68
|
const result = computeConsensusScore(votes);
|
|
67
69
|
expect(result.score).toBe(0); // Simple: 0 approves / 1 total
|
|
68
|
-
|
|
70
|
+
// Option B: (0.5 * 1.0) / 1 = 0.5
|
|
71
|
+
expect(result.weighted_score).toBe(0.5);
|
|
69
72
|
});
|
|
70
73
|
|
|
71
|
-
it('should
|
|
74
|
+
it('should average per reviewer (Option B)', () => {
|
|
72
75
|
const votes = [
|
|
73
76
|
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
74
77
|
makeVote({ vote: 'REJECT', confidence: 0.1, reviewer_id: 'r2' }),
|
|
75
78
|
];
|
|
76
79
|
const result = computeConsensusScore(votes);
|
|
77
80
|
expect(result.score).toBe(0.5); // Simple: 1/2
|
|
78
|
-
//
|
|
79
|
-
expect(result.weighted_score).
|
|
81
|
+
// Option B: (1.0*1.0 + 0.0*0.1) / 2 = 0.5
|
|
82
|
+
expect(result.weighted_score).toBe(0.5);
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
it('should
|
|
85
|
+
it('should return honest weighted_score when REJECT vote has real blocking issues (v2.4.2: no force-zero)', () => {
|
|
83
86
|
const votes = [
|
|
84
87
|
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
85
88
|
makeVote({
|
|
86
|
-
vote: '
|
|
87
|
-
confidence: 0.
|
|
89
|
+
vote: 'REJECT',
|
|
90
|
+
confidence: 0.5,
|
|
88
91
|
blocking_issues: ['Critical bug found'],
|
|
89
92
|
reviewer_id: 'r2',
|
|
90
93
|
}),
|
|
91
94
|
];
|
|
92
95
|
const result = computeConsensusScore(votes);
|
|
93
96
|
expect(result.score).toBe(0.5); // Simple score unaffected
|
|
94
|
-
|
|
97
|
+
// v2.4.2: honest score (1.0*1.0 + 0.0*0.5) / 2 = 0.5
|
|
98
|
+
expect(result.weighted_score).toBeCloseTo(0.5, 3);
|
|
99
|
+
expect(result.has_true_blockers).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should NOT force-zero when only non-REJECT votes have blocking_issues', () => {
|
|
103
|
+
// After normalization, APPROVE/CONDITIONAL votes never have blocking_issues.
|
|
104
|
+
// But the scorer must still handle pre-normalization edge cases gracefully.
|
|
105
|
+
const votes = [
|
|
106
|
+
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
107
|
+
makeVote({
|
|
108
|
+
vote: 'CONDITIONAL',
|
|
109
|
+
confidence: 0.9,
|
|
110
|
+
blocking_issues: ['Consider auth approach'],
|
|
111
|
+
reviewer_id: 'r2',
|
|
112
|
+
}),
|
|
113
|
+
];
|
|
114
|
+
const result = computeConsensusScore(votes);
|
|
115
|
+
// Force-zero only fires for REJECT votes with real blockers
|
|
116
|
+
expect(result.weighted_score).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should NOT force weighted_score to 0 when blocking_issues contains only none-variants (defense-in-depth)', () => {
|
|
120
|
+
const votes = [
|
|
121
|
+
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
122
|
+
makeVote({
|
|
123
|
+
vote: 'REJECT',
|
|
124
|
+
confidence: 0.5,
|
|
125
|
+
blocking_issues: ['No blocking issues found'],
|
|
126
|
+
reviewer_id: 'r2',
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
const result = computeConsensusScore(votes);
|
|
130
|
+
expect(result.score).toBe(0.5);
|
|
131
|
+
// Defense-in-depth: none-variant filtered, so score is NOT zeroed
|
|
132
|
+
expect(result.weighted_score).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('REJECT vote with real blocking issues returns honest score + has_true_blockers=true (v2.4.2)', () => {
|
|
136
|
+
const votes = [
|
|
137
|
+
makeVote({
|
|
138
|
+
vote: 'REJECT',
|
|
139
|
+
confidence: 0.95,
|
|
140
|
+
blocking_issues: ['SQL injection vulnerability'],
|
|
141
|
+
reviewer_id: 'r1',
|
|
142
|
+
}),
|
|
143
|
+
makeVote({ vote: 'APPROVE', confidence: 0.9, reviewer_id: 'r2' }),
|
|
144
|
+
];
|
|
145
|
+
const result = computeConsensusScore(votes);
|
|
146
|
+
// v2.4.2: honest score (0.0*0.95 + 1.0*0.9) / 2 = 0.45
|
|
147
|
+
expect(result.weighted_score).toBeCloseTo(0.45, 3);
|
|
148
|
+
expect(result.has_true_blockers).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('REJECT vote with no blockers: does NOT force-zero (scores 0 by weight, not by force)', () => {
|
|
152
|
+
const votes = [
|
|
153
|
+
makeVote({
|
|
154
|
+
vote: 'REJECT',
|
|
155
|
+
confidence: 0.5,
|
|
156
|
+
blocking_issues: [],
|
|
157
|
+
reviewer_id: 'r1',
|
|
158
|
+
}),
|
|
159
|
+
makeVote({ vote: 'APPROVE', confidence: 0.96, reviewer_id: 'r2' }),
|
|
160
|
+
];
|
|
161
|
+
const result = computeConsensusScore(votes);
|
|
162
|
+
// Option B: (0*0.5 + 1.0*0.96) / 2 = 0.48
|
|
163
|
+
expect(result.weighted_score).toBeCloseTo(0.48, 3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('APPROVE votes after normalization (no blockers left): not force-zeroed', () => {
|
|
167
|
+
const votes = [
|
|
168
|
+
makeVote({ vote: 'APPROVE', confidence: 0.96, blocking_issues: [], reviewer_id: 'r1' }),
|
|
169
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, blocking_issues: [], reviewer_id: 'r2' }),
|
|
170
|
+
];
|
|
171
|
+
const result = computeConsensusScore(votes);
|
|
172
|
+
// Option B: (1.0*0.96 + 1.0*0.97) / 2 = 0.965
|
|
173
|
+
expect(result.weighted_score).toBeCloseTo(0.965, 3);
|
|
174
|
+
expect(result.weighted_score).toBeGreaterThan(0);
|
|
95
175
|
});
|
|
96
176
|
|
|
97
177
|
it('should handle mixed votes with varied confidence', () => {
|
|
@@ -102,8 +182,491 @@ describe('computeConsensusScore', () => {
|
|
|
102
182
|
];
|
|
103
183
|
const result = computeConsensusScore(votes);
|
|
104
184
|
expect(result.score).toBeCloseTo(1 / 3, 3); // 1 approve / 3 total
|
|
105
|
-
//
|
|
106
|
-
expect(result.weighted_score).toBeCloseTo(1.1 /
|
|
185
|
+
// Option B: (1.0*0.8 + 0.5*0.6 + 0.0*0.4) / 3 = 1.1/3 ≈ 0.367
|
|
186
|
+
expect(result.weighted_score).toBeCloseTo(1.1 / 3, 3);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should NOT force weighted_score to 0 when concerns exist but no blocking issues', () => {
|
|
190
|
+
const votes = [
|
|
191
|
+
makeVote({ vote: 'APPROVE', confidence: 0.9, blocking_issues: [], suggestions: ['improve naming'], reviewer_id: 'r1' }),
|
|
192
|
+
makeVote({ vote: 'APPROVE', confidence: 0.85, blocking_issues: [], suggestions: ['add caching'], reviewer_id: 'r2' }),
|
|
193
|
+
];
|
|
194
|
+
const result = computeConsensusScore(votes);
|
|
195
|
+
expect(result.weighted_score).toBeGreaterThan(0.8);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('CONDITIONAL-only votes should produce correct weighted_score', () => {
|
|
199
|
+
const votes = [
|
|
200
|
+
makeVote({ vote: 'CONDITIONAL', confidence: 0.90, reviewer_id: 'r1' }),
|
|
201
|
+
makeVote({ vote: 'CONDITIONAL', confidence: 0.875, reviewer_id: 'r2' }),
|
|
202
|
+
];
|
|
203
|
+
const result = computeConsensusScore(votes);
|
|
204
|
+
// Option B: (0.5*0.90 + 0.5*0.875) / 2 = 0.44375
|
|
205
|
+
expect(result.weighted_score).toBeCloseTo(0.44375, 3);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('mixed APPROVE+CONDITIONAL should produce correct weighted_score', () => {
|
|
209
|
+
const votes = [
|
|
210
|
+
makeVote({ vote: 'APPROVE', confidence: 0.96, reviewer_id: 'r1' }),
|
|
211
|
+
makeVote({ vote: 'CONDITIONAL', confidence: 0.90, reviewer_id: 'r2' }),
|
|
212
|
+
];
|
|
213
|
+
const result = computeConsensusScore(votes);
|
|
214
|
+
// Option B: (1.0*0.96 + 0.5*0.90) / 2 = 1.41/2 = 0.705
|
|
215
|
+
expect(result.weighted_score).toBeCloseTo(0.705, 3);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('APPROVE with suggestions but no blocking_issues should NOT zero score', () => {
|
|
219
|
+
const votes = [
|
|
220
|
+
makeVote({
|
|
221
|
+
vote: 'APPROVE', confidence: 0.96,
|
|
222
|
+
blocking_issues: [], suggestions: ['consider caching', 'improve naming'],
|
|
223
|
+
reviewer_id: 'r1',
|
|
224
|
+
}),
|
|
225
|
+
makeVote({
|
|
226
|
+
vote: 'APPROVE', confidence: 0.95,
|
|
227
|
+
blocking_issues: [], suggestions: ['add monitoring'],
|
|
228
|
+
reviewer_id: 'r2',
|
|
229
|
+
}),
|
|
230
|
+
];
|
|
231
|
+
const result = computeConsensusScore(votes);
|
|
232
|
+
// Option B: (1.0*0.96 + 1.0*0.95) / 2 = 0.955
|
|
233
|
+
expect(result.weighted_score).toBeCloseTo(0.955, 3);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// v2.4.2: has_true_blockers tests
|
|
237
|
+
it('computeConsensusScore returns has_true_blockers=true when REJECT has real blockers', () => {
|
|
238
|
+
const votes = [
|
|
239
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, reviewer_id: 'r1' }),
|
|
240
|
+
makeVote({ vote: 'REJECT', confidence: 0.3, blocking_issues: ['Missing auth'], reviewer_id: 'r2' }),
|
|
241
|
+
];
|
|
242
|
+
const result = computeConsensusScore(votes);
|
|
243
|
+
expect(result.has_true_blockers).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('computeConsensusScore returns has_true_blockers=false when no REJECT has blockers', () => {
|
|
247
|
+
const votes = [
|
|
248
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, reviewer_id: 'r1' }),
|
|
249
|
+
makeVote({ vote: 'REJECT', confidence: 0.3, blocking_issues: [], reviewer_id: 'r2' }),
|
|
250
|
+
];
|
|
251
|
+
const result = computeConsensusScore(votes);
|
|
252
|
+
expect(result.has_true_blockers).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('computeConsensusScore returns has_true_blockers=false for empty votes', () => {
|
|
256
|
+
const result = computeConsensusScore([]);
|
|
257
|
+
expect(result.has_true_blockers).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('normalizeVoteBlockers', () => {
|
|
262
|
+
it('APPROVE + soft blockers -> moved to suggestions, blocking_issues cleared', () => {
|
|
263
|
+
const votes = [makeVote({
|
|
264
|
+
vote: 'APPROVE',
|
|
265
|
+
confidence: 0.96,
|
|
266
|
+
blocking_issues: ['Consider auth approach', 'Add caching layer'],
|
|
267
|
+
suggestions: ['Improve naming'],
|
|
268
|
+
reviewer_id: 'r1',
|
|
269
|
+
})];
|
|
270
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
271
|
+
expect(norm[0].blocking_issues).toEqual([]);
|
|
272
|
+
expect(norm[0].suggestions).toContain('Consider auth approach');
|
|
273
|
+
expect(norm[0].suggestions).toContain('Add caching layer');
|
|
274
|
+
expect(norm[0].suggestions).toContain('Improve naming');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('CONDITIONAL + soft blockers -> moved to required_changes, blocking_issues cleared', () => {
|
|
278
|
+
const votes = [makeVote({
|
|
279
|
+
vote: 'CONDITIONAL',
|
|
280
|
+
confidence: 0.88,
|
|
281
|
+
blocking_issues: ['Add error handling', 'Need input validation'],
|
|
282
|
+
suggestions: ['Improve docs'],
|
|
283
|
+
reviewer_id: 'r1',
|
|
284
|
+
})];
|
|
285
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
286
|
+
expect(norm[0].blocking_issues).toEqual([]);
|
|
287
|
+
expect(norm[0].required_changes).toContain('Add error handling');
|
|
288
|
+
expect(norm[0].required_changes).toContain('Need input validation');
|
|
289
|
+
expect(norm[0].suggestions).toContain('Improve docs');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('REJECT + blockers -> unchanged', () => {
|
|
293
|
+
const votes = [makeVote({
|
|
294
|
+
vote: 'REJECT',
|
|
295
|
+
confidence: 0.5,
|
|
296
|
+
blocking_issues: ['Critical security flaw'],
|
|
297
|
+
suggestions: ['Rewrite auth module'],
|
|
298
|
+
reviewer_id: 'r1',
|
|
299
|
+
})];
|
|
300
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
301
|
+
expect(norm[0].blocking_issues).toContain('Critical security flaw');
|
|
302
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('APPROVE + [BLOCKER] tagged issue -> vote forced to REJECT, reviewer_inconsistency=true', () => {
|
|
306
|
+
const votes = [makeVote({
|
|
307
|
+
vote: 'APPROVE',
|
|
308
|
+
confidence: 0.96,
|
|
309
|
+
blocking_issues: ['[BLOCKER] Missing authentication layer'],
|
|
310
|
+
suggestions: [],
|
|
311
|
+
reviewer_id: 'r1',
|
|
312
|
+
})];
|
|
313
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
314
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
315
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
316
|
+
expect(norm[0].blocking_issues).toContain('Missing authentication layer');
|
|
317
|
+
expect(summary.forced_rejects).toBe(1);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('APPROVE + "SQL injection" pattern -> vote forced to REJECT', () => {
|
|
321
|
+
const votes = [makeVote({
|
|
322
|
+
vote: 'APPROVE',
|
|
323
|
+
confidence: 0.96,
|
|
324
|
+
blocking_issues: ['The code has SQL injection vulnerabilities'],
|
|
325
|
+
suggestions: [],
|
|
326
|
+
reviewer_id: 'r1',
|
|
327
|
+
})];
|
|
328
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
329
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
330
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('Mixed votes: only REJECT retains blockers after normalization', () => {
|
|
334
|
+
const votes = [
|
|
335
|
+
makeVote({
|
|
336
|
+
vote: 'APPROVE',
|
|
337
|
+
confidence: 0.96,
|
|
338
|
+
blocking_issues: ['Consider caching'],
|
|
339
|
+
reviewer_id: 'r1',
|
|
340
|
+
}),
|
|
341
|
+
makeVote({
|
|
342
|
+
vote: 'REJECT',
|
|
343
|
+
confidence: 0.5,
|
|
344
|
+
blocking_issues: ['Critical data loss issue'],
|
|
345
|
+
reviewer_id: 'r2',
|
|
346
|
+
}),
|
|
347
|
+
];
|
|
348
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
349
|
+
expect(norm[0].blocking_issues).toEqual([]);
|
|
350
|
+
expect(norm[1].blocking_issues).toContain('Critical data loss issue');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('Tag prefixes stripped when moving issues', () => {
|
|
354
|
+
const votes = [makeVote({
|
|
355
|
+
vote: 'CONDITIONAL',
|
|
356
|
+
confidence: 0.88,
|
|
357
|
+
blocking_issues: ['[REQUIRED] Add error handling'],
|
|
358
|
+
suggestions: ['[SUGGESTION] Improve docs'],
|
|
359
|
+
reviewer_id: 'r1',
|
|
360
|
+
})];
|
|
361
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
362
|
+
expect(norm[0].required_changes).toContain('Add error handling');
|
|
363
|
+
expect(norm[0].suggestions).toContain('Improve docs');
|
|
364
|
+
// Tags should be stripped
|
|
365
|
+
expect(norm[0].required_changes?.some(r => r.includes('[REQUIRED]'))).toBe(false);
|
|
366
|
+
expect(norm[0].suggestions.some(s => s.includes('[SUGGESTION]'))).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('Idempotency: normalizeVoteBlockers(normalizeVoteBlockers(votes)) deep-equals single pass', () => {
|
|
370
|
+
const votes = [makeVote({
|
|
371
|
+
vote: 'CONDITIONAL',
|
|
372
|
+
confidence: 0.88,
|
|
373
|
+
blocking_issues: ['Add error handling', 'Need validation'],
|
|
374
|
+
suggestions: ['Improve docs'],
|
|
375
|
+
reviewer_id: 'r1',
|
|
376
|
+
})];
|
|
377
|
+
const { votes: first } = normalizeVoteBlockers(votes);
|
|
378
|
+
const { votes: second } = normalizeVoteBlockers(first);
|
|
379
|
+
expect(second[0].blocking_issues).toEqual(first[0].blocking_issues);
|
|
380
|
+
expect(second[0].required_changes).toEqual(first[0].required_changes);
|
|
381
|
+
expect(second[0].suggestions).toEqual(first[0].suggestions);
|
|
382
|
+
expect(second[0].vote).toEqual(first[0].vote);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('Tags override vote: APPROVE with [BLOCKER] tag -> forced REJECT + blocker in output', () => {
|
|
386
|
+
const votes = [makeVote({
|
|
387
|
+
vote: 'APPROVE',
|
|
388
|
+
confidence: 0.98,
|
|
389
|
+
blocking_issues: [],
|
|
390
|
+
suggestions: ['[BLOCKER] XSS vulnerability in form handler'],
|
|
391
|
+
reviewer_id: 'r1',
|
|
392
|
+
})];
|
|
393
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
394
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
395
|
+
expect(norm[0].blocking_issues).toContain('XSS vulnerability in form handler');
|
|
396
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('Hard pattern in suggestions triggers forced REJECT: APPROVE with "SQL injection" in suggestions', () => {
|
|
400
|
+
const votes = [makeVote({
|
|
401
|
+
vote: 'APPROVE',
|
|
402
|
+
confidence: 0.97,
|
|
403
|
+
blocking_issues: [],
|
|
404
|
+
suggestions: ['Watch out for SQL injection in the user input handler'],
|
|
405
|
+
reviewer_id: 'r1',
|
|
406
|
+
})];
|
|
407
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
408
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
409
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// v2.4.4: Vote-aware contradiction guard tests
|
|
413
|
+
it('CONDITIONAL + hard pattern in suggestions only -> NOT forced REJECT', () => {
|
|
414
|
+
const votes = [makeVote({
|
|
415
|
+
vote: 'CONDITIONAL',
|
|
416
|
+
confidence: 0.88,
|
|
417
|
+
blocking_issues: [],
|
|
418
|
+
suggestions: ['Watch out for SQL injection in the user input handler'],
|
|
419
|
+
reviewer_id: 'r1',
|
|
420
|
+
})];
|
|
421
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
422
|
+
// CONDITIONAL with hard pattern only in suggestions stays CONDITIONAL
|
|
423
|
+
expect(norm[0].vote).toBe('CONDITIONAL');
|
|
424
|
+
expect(summary.forced_rejects).toBe(0);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('CONDITIONAL + hard pattern in blocking_issues (untagged) -> forced REJECT', () => {
|
|
428
|
+
const votes = [makeVote({
|
|
429
|
+
vote: 'CONDITIONAL',
|
|
430
|
+
confidence: 0.88,
|
|
431
|
+
blocking_issues: ['SQL injection vulnerability in user input'],
|
|
432
|
+
suggestions: [],
|
|
433
|
+
reviewer_id: 'r1',
|
|
434
|
+
})];
|
|
435
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
436
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
437
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
438
|
+
expect(summary.forced_rejects).toBe(1);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('CONDITIONAL + [BLOCKER] tagged item -> forced REJECT', () => {
|
|
442
|
+
const votes = [makeVote({
|
|
443
|
+
vote: 'CONDITIONAL',
|
|
444
|
+
confidence: 0.88,
|
|
445
|
+
blocking_issues: ['[BLOCKER] Critical data loss vulnerability'],
|
|
446
|
+
suggestions: [],
|
|
447
|
+
reviewer_id: 'r1',
|
|
448
|
+
})];
|
|
449
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
450
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
451
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
452
|
+
expect(summary.forced_rejects).toBe(1);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('CONDITIONAL + [REQUIRED] SQL injection in suggestions -> NOT forced REJECT', () => {
|
|
456
|
+
const votes = [makeVote({
|
|
457
|
+
vote: 'CONDITIONAL',
|
|
458
|
+
confidence: 0.88,
|
|
459
|
+
blocking_issues: [],
|
|
460
|
+
suggestions: ['[REQUIRED] SQL injection needs parameterized queries'],
|
|
461
|
+
reviewer_id: 'r1',
|
|
462
|
+
})];
|
|
463
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
464
|
+
// [REQUIRED] tag reclassifies to required, NOT blocker. Hard pattern only in
|
|
465
|
+
// required_changes (not blocker-origin), so CONDITIONAL stays.
|
|
466
|
+
expect(norm[0].vote).toBe('CONDITIONAL');
|
|
467
|
+
expect(summary.forced_rejects).toBe(0);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('APPROVE + hard pattern in suggestions -> forced REJECT (genuinely inconsistent)', () => {
|
|
471
|
+
const votes = [makeVote({
|
|
472
|
+
vote: 'APPROVE',
|
|
473
|
+
confidence: 0.97,
|
|
474
|
+
blocking_issues: [],
|
|
475
|
+
suggestions: ['Beware of XSS vulnerabilities in form handler'],
|
|
476
|
+
reviewer_id: 'r1',
|
|
477
|
+
})];
|
|
478
|
+
const { votes: norm, summary } = normalizeVoteBlockers(votes);
|
|
479
|
+
// APPROVE scans ALL text for hard patterns -> forced REJECT
|
|
480
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
481
|
+
expect(norm[0].reviewer_inconsistency).toBe(true);
|
|
482
|
+
expect(summary.forced_rejects).toBe(1);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('normalizeVoteBlockers + computeConsensusScore (integration)', () => {
|
|
487
|
+
it('APPROVE with "consider auth" -> normalize -> weighted_score not force-zeroed', () => {
|
|
488
|
+
const votes = [
|
|
489
|
+
makeVote({
|
|
490
|
+
vote: 'APPROVE',
|
|
491
|
+
confidence: 0.96,
|
|
492
|
+
blocking_issues: ['Consider auth approach'],
|
|
493
|
+
reviewer_id: 'r1',
|
|
494
|
+
}),
|
|
495
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, reviewer_id: 'r2' }),
|
|
496
|
+
];
|
|
497
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
498
|
+
const result = computeConsensusScore(norm);
|
|
499
|
+
expect(result.weighted_score).toBeGreaterThan(0);
|
|
500
|
+
// Option B: (1.0*0.96 + 1.0*0.97) / 2 = 0.965
|
|
501
|
+
expect(result.weighted_score).toBeCloseTo(0.965, 3);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('APPROVE with "SQL injection" -> normalize -> forced REJECT -> has_true_blockers (v2.4.2)', () => {
|
|
505
|
+
const votes = [
|
|
506
|
+
makeVote({
|
|
507
|
+
vote: 'APPROVE',
|
|
508
|
+
confidence: 0.96,
|
|
509
|
+
blocking_issues: ['SQL injection vulnerability found'],
|
|
510
|
+
reviewer_id: 'r1',
|
|
511
|
+
}),
|
|
512
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, reviewer_id: 'r2' }),
|
|
513
|
+
];
|
|
514
|
+
const { votes: norm } = normalizeVoteBlockers(votes);
|
|
515
|
+
// First vote should have been forced to REJECT
|
|
516
|
+
expect(norm[0].vote).toBe('REJECT');
|
|
517
|
+
expect(norm[0].blocking_issues.length).toBeGreaterThan(0);
|
|
518
|
+
|
|
519
|
+
const result = computeConsensusScore(norm);
|
|
520
|
+
// v2.4.2: honest score > 0, has_true_blockers flags the issue
|
|
521
|
+
expect(result.weighted_score).toBeGreaterThan(0);
|
|
522
|
+
expect(result.has_true_blockers).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe('REJECT guard in buildConsensusPacket', () => {
|
|
527
|
+
it('REJECT with true blockers prevents APPROVED final_status', () => {
|
|
528
|
+
// Even if somehow score threshold is met, REJECT with blockers blocks APPROVED
|
|
529
|
+
const votes = [
|
|
530
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r1' }),
|
|
531
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r2' }),
|
|
532
|
+
makeVote({
|
|
533
|
+
vote: 'REJECT',
|
|
534
|
+
confidence: 0.3,
|
|
535
|
+
blocking_issues: ['Critical security flaw'],
|
|
536
|
+
reviewer_id: 'r3',
|
|
537
|
+
}),
|
|
538
|
+
];
|
|
539
|
+
const packet = buildConsensusPacket({
|
|
540
|
+
planPacketRef: mockRef,
|
|
541
|
+
votes,
|
|
542
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
543
|
+
});
|
|
544
|
+
// Force-zero kicks in from scorer, plus REJECT guard
|
|
545
|
+
expect(packet.final_status).toBe('REJECTED');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('REJECT without blockers does NOT prevent APPROVED final_status', () => {
|
|
549
|
+
const votes = [
|
|
550
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r1' }),
|
|
551
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r2' }),
|
|
552
|
+
makeVote({
|
|
553
|
+
vote: 'REJECT',
|
|
554
|
+
confidence: 0.3,
|
|
555
|
+
blocking_issues: [],
|
|
556
|
+
reviewer_id: 'r3',
|
|
557
|
+
}),
|
|
558
|
+
];
|
|
559
|
+
const packet = buildConsensusPacket({
|
|
560
|
+
planPacketRef: mockRef,
|
|
561
|
+
votes,
|
|
562
|
+
// Low threshold so raw score passes
|
|
563
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
564
|
+
});
|
|
565
|
+
// Option B: (0.99 + 0.99 + 0) / 3 = 0.66 >= 0.5
|
|
566
|
+
expect(packet.consensus_result.weighted_score).toBeGreaterThan(0);
|
|
567
|
+
expect(packet.final_status).toBe('APPROVED');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('governance guard prevents APPROVED even when honest weighted_score passes threshold (v2.4.2)', () => {
|
|
571
|
+
// v2.4.2: with force-zero removed, weighted_score may exceed threshold
|
|
572
|
+
// but governance guard still blocks APPROVED when REJECT has real blockers
|
|
573
|
+
const votes = [
|
|
574
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r1' }),
|
|
575
|
+
makeVote({ vote: 'APPROVE', confidence: 0.99, reviewer_id: 'r2' }),
|
|
576
|
+
makeVote({
|
|
577
|
+
vote: 'REJECT',
|
|
578
|
+
confidence: 0.3,
|
|
579
|
+
blocking_issues: ['Critical security flaw'],
|
|
580
|
+
reviewer_id: 'r3',
|
|
581
|
+
}),
|
|
582
|
+
];
|
|
583
|
+
const packet = buildConsensusPacket({
|
|
584
|
+
planPacketRef: mockRef,
|
|
585
|
+
votes,
|
|
586
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
587
|
+
});
|
|
588
|
+
// v2.4.2: weighted_score is honest (>0) but governance guard blocks APPROVED
|
|
589
|
+
expect(packet.consensus_result.weighted_score).toBeGreaterThan(0);
|
|
590
|
+
expect(packet.consensus_result.has_true_blockers).toBe(true);
|
|
591
|
+
expect(packet.final_status).toBe('REJECTED');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('REJECT guard does not affect ARBITRATED status', () => {
|
|
595
|
+
const votes = [
|
|
596
|
+
makeVote({
|
|
597
|
+
vote: 'REJECT',
|
|
598
|
+
confidence: 0.3,
|
|
599
|
+
blocking_issues: ['Critical bug'],
|
|
600
|
+
reviewer_id: 'r1',
|
|
601
|
+
}),
|
|
602
|
+
];
|
|
603
|
+
const packet = buildConsensusPacket({
|
|
604
|
+
planPacketRef: mockRef,
|
|
605
|
+
votes,
|
|
606
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
607
|
+
arbitratorResult: { decision: 'Override: plan is acceptable with amendments' },
|
|
608
|
+
});
|
|
609
|
+
// Arbitration takes precedence over REJECT guard
|
|
610
|
+
expect(packet.final_status).toBe('ARBITRATED');
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('normalization summary', () => {
|
|
615
|
+
it('summary counts tagged_blockers_demoted_to_required for CONDITIONAL votes', () => {
|
|
616
|
+
const votes = [makeVote({
|
|
617
|
+
vote: 'CONDITIONAL',
|
|
618
|
+
confidence: 0.88,
|
|
619
|
+
blocking_issues: ['[BLOCKER] Need error handling', 'Add validation'],
|
|
620
|
+
reviewer_id: 'r1',
|
|
621
|
+
})];
|
|
622
|
+
// Note: [BLOCKER] tag on CONDITIONAL triggers forced REJECT, NOT demotion
|
|
623
|
+
// Let's test with untagged blockers instead
|
|
624
|
+
const votesClean = [makeVote({
|
|
625
|
+
vote: 'CONDITIONAL',
|
|
626
|
+
confidence: 0.88,
|
|
627
|
+
blocking_issues: ['Need error handling', 'Add validation'],
|
|
628
|
+
suggestions: [],
|
|
629
|
+
reviewer_id: 'r1',
|
|
630
|
+
})];
|
|
631
|
+
const { summary } = normalizeVoteBlockers(votesClean);
|
|
632
|
+
expect(summary.untagged_from_blocking_routed_to_required).toBe(2);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('summary counts tagged_blockers_demoted_to_suggestions for APPROVE votes', () => {
|
|
636
|
+
// APPROVE with tagged [BLOCKER] triggers forced REJECT, not demotion.
|
|
637
|
+
// Test with untagged (soft) blockers that get demoted to suggestions.
|
|
638
|
+
const votes = [makeVote({
|
|
639
|
+
vote: 'APPROVE',
|
|
640
|
+
confidence: 0.96,
|
|
641
|
+
blocking_issues: ['Consider caching', 'Maybe add retry logic'],
|
|
642
|
+
suggestions: [],
|
|
643
|
+
reviewer_id: 'r1',
|
|
644
|
+
})];
|
|
645
|
+
const { summary } = normalizeVoteBlockers(votes);
|
|
646
|
+
// Untagged items from blocking → suggestions for APPROVE
|
|
647
|
+
// tagged_blockers_demoted_to_suggestions is for [BLOCKER]-tagged items specifically
|
|
648
|
+
expect(summary.tagged_blockers_demoted_to_suggestions).toBe(0);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('summary counts forced_rejects for contradiction guard triggers', () => {
|
|
652
|
+
const votes = [
|
|
653
|
+
makeVote({
|
|
654
|
+
vote: 'APPROVE',
|
|
655
|
+
confidence: 0.96,
|
|
656
|
+
blocking_issues: ['[BLOCKER] Missing auth'],
|
|
657
|
+
reviewer_id: 'r1',
|
|
658
|
+
}),
|
|
659
|
+
makeVote({
|
|
660
|
+
vote: 'CONDITIONAL',
|
|
661
|
+
confidence: 0.88,
|
|
662
|
+
suggestions: ['The code has SQL injection issues'],
|
|
663
|
+
reviewer_id: 'r2',
|
|
664
|
+
}),
|
|
665
|
+
];
|
|
666
|
+
const { summary } = normalizeVoteBlockers(votes);
|
|
667
|
+
// v2.4.4: Only APPROVE is forced to REJECT (tagged [BLOCKER]).
|
|
668
|
+
// CONDITIONAL with hard pattern only in suggestions is NOT forced.
|
|
669
|
+
expect(summary.forced_rejects).toBe(1);
|
|
107
670
|
});
|
|
108
671
|
});
|
|
109
672
|
|
|
@@ -115,7 +678,8 @@ describe('buildConsensusPacket', () => {
|
|
|
115
678
|
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
116
679
|
});
|
|
117
680
|
expect(packet.consensus_result.weighted_score).toBeDefined();
|
|
118
|
-
|
|
681
|
+
// Option B: (1.0 * 0.9) / 1 = 0.9
|
|
682
|
+
expect(packet.consensus_result.weighted_score).toBeCloseTo(0.9, 3);
|
|
119
683
|
});
|
|
120
684
|
|
|
121
685
|
it('should maintain backward-compatible simple score', () => {
|
|
@@ -130,16 +694,20 @@ describe('buildConsensusPacket', () => {
|
|
|
130
694
|
expect(packet.consensus_result.score).toBe(0.5);
|
|
131
695
|
});
|
|
132
696
|
|
|
133
|
-
it('should APPROVE when
|
|
697
|
+
it('should APPROVE when weighted_score meets threshold', () => {
|
|
134
698
|
const packet = buildConsensusPacket({
|
|
135
699
|
planPacketRef: mockRef,
|
|
136
|
-
votes: [
|
|
700
|
+
votes: [
|
|
701
|
+
makeVote({ vote: 'APPROVE', confidence: 0.96 }),
|
|
702
|
+
makeVote({ vote: 'APPROVE', confidence: 0.97, reviewer_id: 'r2' }),
|
|
703
|
+
],
|
|
137
704
|
rules: { threshold: 0.95, quorum: 1, min_reviewers: 1 },
|
|
138
705
|
});
|
|
706
|
+
// Option B: (0.96 + 0.97) / 2 = 0.965 >= 0.95
|
|
139
707
|
expect(packet.final_status).toBe('APPROVED');
|
|
140
708
|
});
|
|
141
709
|
|
|
142
|
-
it('should REJECT when
|
|
710
|
+
it('should REJECT when weighted_score below threshold', () => {
|
|
143
711
|
const packet = buildConsensusPacket({
|
|
144
712
|
planPacketRef: mockRef,
|
|
145
713
|
votes: [
|
|
@@ -160,4 +728,20 @@ describe('buildConsensusPacket', () => {
|
|
|
160
728
|
});
|
|
161
729
|
expect(packet.final_status).toBe('ARBITRATED');
|
|
162
730
|
});
|
|
731
|
+
|
|
732
|
+
it('should include normalization_moves when provided', () => {
|
|
733
|
+
const packet = buildConsensusPacket({
|
|
734
|
+
planPacketRef: mockRef,
|
|
735
|
+
votes: [makeVote({ vote: 'APPROVE', confidence: 0.96 })],
|
|
736
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
737
|
+
normalizationMoves: {
|
|
738
|
+
tagged_blockers_demoted_to_suggestions: 2,
|
|
739
|
+
tagged_blockers_demoted_to_required: 1,
|
|
740
|
+
untagged_from_blocking_routed_to_required: 3,
|
|
741
|
+
forced_rejects: 0,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
expect(packet.normalization_moves).toBeDefined();
|
|
745
|
+
expect(packet.normalization_moves?.tagged_blockers_demoted_to_suggestions).toBe(2);
|
|
746
|
+
});
|
|
163
747
|
});
|