popeye-cli 1.10.0 → 2.1.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/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +38 -3
- package/README.md +104 -18
- package/dist/adapters/gemini.js +3 -3
- package/dist/adapters/openai.js +2 -2
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/gemini.js +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +11 -5
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +9 -1
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +29 -3
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/popeye-md.d.ts +32 -0
- package/dist/config/popeye-md.d.ts.map +1 -0
- package/dist/config/popeye-md.js +111 -0
- package/dist/config/popeye-md.js.map +1 -0
- package/dist/config/schema.d.ts +3 -21
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +21 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts +47 -0
- package/dist/pipeline/artifact-manager.d.ts.map +1 -0
- package/dist/pipeline/artifact-manager.js +251 -0
- package/dist/pipeline/artifact-manager.js.map +1 -0
- package/dist/pipeline/artifact-validators.d.ts +29 -0
- package/dist/pipeline/artifact-validators.d.ts.map +1 -0
- package/dist/pipeline/artifact-validators.js +173 -0
- package/dist/pipeline/artifact-validators.js.map +1 -0
- package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
- package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
- package/dist/pipeline/bridges/review-bridge.js +266 -0
- package/dist/pipeline/bridges/review-bridge.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +47 -0
- package/dist/pipeline/change-request.d.ts.map +1 -0
- package/dist/pipeline/change-request.js +91 -0
- package/dist/pipeline/change-request.js.map +1 -0
- package/dist/pipeline/check-runner.d.ts +47 -0
- package/dist/pipeline/check-runner.d.ts.map +1 -0
- package/dist/pipeline/check-runner.js +417 -0
- package/dist/pipeline/check-runner.js.map +1 -0
- package/dist/pipeline/command-resolver.d.ts +9 -0
- package/dist/pipeline/command-resolver.d.ts.map +1 -0
- package/dist/pipeline/command-resolver.js +140 -0
- package/dist/pipeline/command-resolver.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.js +212 -0
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
- package/dist/pipeline/constitution.d.ts +45 -0
- package/dist/pipeline/constitution.d.ts.map +1 -0
- package/dist/pipeline/constitution.js +82 -0
- package/dist/pipeline/constitution.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +55 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -0
- package/dist/pipeline/gate-engine.js +270 -0
- package/dist/pipeline/gate-engine.js.map +1 -0
- package/dist/pipeline/index.d.ts +26 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +35 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/migration.d.ts +15 -0
- package/dist/pipeline/migration.d.ts.map +1 -0
- package/dist/pipeline/migration.js +76 -0
- package/dist/pipeline/migration.js.map +1 -0
- package/dist/pipeline/orchestrator.d.ts +30 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +242 -0
- package/dist/pipeline/orchestrator.js.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.js +32 -0
- package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/index.d.ts +12 -0
- package/dist/pipeline/packets/index.d.ts.map +1 -0
- package/dist/pipeline/packets/index.js +8 -0
- package/dist/pipeline/packets/index.js.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.js +27 -0
- package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.js +22 -0
- package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
- package/dist/pipeline/phases/architecture.d.ts +7 -0
- package/dist/pipeline/phases/architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/architecture.js +60 -0
- package/dist/pipeline/phases/architecture.js.map +1 -0
- package/dist/pipeline/phases/audit.d.ts +8 -0
- package/dist/pipeline/phases/audit.d.ts.map +1 -0
- package/dist/pipeline/phases/audit.js +144 -0
- package/dist/pipeline/phases/audit.js.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.js +84 -0
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.js +81 -0
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.js +85 -0
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
- package/dist/pipeline/phases/done.d.ts +7 -0
- package/dist/pipeline/phases/done.d.ts.map +1 -0
- package/dist/pipeline/phases/done.js +45 -0
- package/dist/pipeline/phases/done.js.map +1 -0
- package/dist/pipeline/phases/implementation.d.ts +8 -0
- package/dist/pipeline/phases/implementation.d.ts.map +1 -0
- package/dist/pipeline/phases/implementation.js +45 -0
- package/dist/pipeline/phases/implementation.js.map +1 -0
- package/dist/pipeline/phases/index.d.ts +20 -0
- package/dist/pipeline/phases/index.d.ts.map +1 -0
- package/dist/pipeline/phases/index.js +19 -0
- package/dist/pipeline/phases/index.js.map +1 -0
- package/dist/pipeline/phases/intake.d.ts +8 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -0
- package/dist/pipeline/phases/intake.js +49 -0
- package/dist/pipeline/phases/intake.js.map +1 -0
- package/dist/pipeline/phases/phase-context.d.ts +30 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
- package/dist/pipeline/phases/phase-context.js +33 -0
- package/dist/pipeline/phases/phase-context.js.map +1 -0
- package/dist/pipeline/phases/production-gate.d.ts +8 -0
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
- package/dist/pipeline/phases/production-gate.js +84 -0
- package/dist/pipeline/phases/production-gate.js.map +1 -0
- package/dist/pipeline/phases/qa-validation.d.ts +7 -0
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
- package/dist/pipeline/phases/qa-validation.js +50 -0
- package/dist/pipeline/phases/qa-validation.js.map +1 -0
- package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
- package/dist/pipeline/phases/recovery-loop.js +93 -0
- package/dist/pipeline/phases/recovery-loop.js.map +1 -0
- package/dist/pipeline/phases/review.d.ts +8 -0
- package/dist/pipeline/phases/review.d.ts.map +1 -0
- package/dist/pipeline/phases/review.js +127 -0
- package/dist/pipeline/phases/review.js.map +1 -0
- package/dist/pipeline/phases/role-planning.d.ts +7 -0
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
- package/dist/pipeline/phases/role-planning.js +75 -0
- package/dist/pipeline/phases/role-planning.js.map +1 -0
- package/dist/pipeline/phases/stuck.d.ts +7 -0
- package/dist/pipeline/phases/stuck.d.ts.map +1 -0
- package/dist/pipeline/phases/stuck.js +51 -0
- package/dist/pipeline/phases/stuck.js.map +1 -0
- package/dist/pipeline/repo-snapshot.d.ts +24 -0
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
- package/dist/pipeline/repo-snapshot.js +343 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -0
- package/dist/pipeline/role-execution-adapter.d.ts +59 -0
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
- package/dist/pipeline/role-execution-adapter.js +159 -0
- package/dist/pipeline/role-execution-adapter.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +34 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +156 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skills/defaults.d.ts +16 -0
- package/dist/pipeline/skills/defaults.d.ts.map +1 -0
- package/dist/pipeline/skills/defaults.js +189 -0
- package/dist/pipeline/skills/defaults.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +207 -0
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
- package/dist/pipeline/type-defs/artifacts.js +67 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -0
- package/dist/pipeline/type-defs/audit.d.ts +259 -0
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
- package/dist/pipeline/type-defs/audit.js +54 -0
- package/dist/pipeline/type-defs/audit.js.map +1 -0
- package/dist/pipeline/type-defs/checks.d.ts +82 -0
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
- package/dist/pipeline/type-defs/checks.js +38 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -0
- package/dist/pipeline/type-defs/enums.d.ts +43 -0
- package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
- package/dist/pipeline/type-defs/enums.js +55 -0
- package/dist/pipeline/type-defs/enums.js.map +1 -0
- package/dist/pipeline/type-defs/index.d.ts +12 -0
- package/dist/pipeline/type-defs/index.d.ts.map +1 -0
- package/dist/pipeline/type-defs/index.js +12 -0
- package/dist/pipeline/type-defs/index.js.map +1 -0
- package/dist/pipeline/type-defs/packets.d.ts +821 -0
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
- package/dist/pipeline/type-defs/packets.js +109 -0
- package/dist/pipeline/type-defs/packets.js.map +1 -0
- package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
- package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
- package/dist/pipeline/type-defs/snapshot.js +35 -0
- package/dist/pipeline/type-defs/snapshot.js.map +1 -0
- package/dist/pipeline/type-defs/state.d.ts +455 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -0
- package/dist/pipeline/type-defs/state.js +90 -0
- package/dist/pipeline/type-defs/state.js.map +1 -0
- package/dist/pipeline/types.d.ts +16 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +16 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/types/audit.d.ts +6 -6
- package/dist/types/consensus.d.ts +5 -1
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +15 -4
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +39 -10
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +1 -7
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +1 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.js +5 -5
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +52 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/website-strategy.js +1 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/package.json +1 -1
- package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
- package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
- package/src/adapters/gemini.ts +3 -3
- package/src/adapters/openai.ts +2 -2
- package/src/auth/gemini.ts +1 -1
- package/src/cli/commands/create.ts +12 -6
- package/src/cli/commands/resume.ts +9 -1
- package/src/cli/interactive.ts +32 -3
- package/src/config/defaults.ts +7 -2
- package/src/config/popeye-md.ts +139 -0
- package/src/config/schema.ts +21 -8
- package/src/pipeline/artifact-manager.ts +339 -0
- package/src/pipeline/artifact-validators.ts +224 -0
- package/src/pipeline/bridges/review-bridge.ts +371 -0
- package/src/pipeline/change-request.ts +119 -0
- package/src/pipeline/check-runner.ts +504 -0
- package/src/pipeline/command-resolver.ts +168 -0
- package/src/pipeline/consensus/consensus-runner.ts +317 -0
- package/src/pipeline/constitution.ts +109 -0
- package/src/pipeline/gate-engine.ts +347 -0
- package/src/pipeline/index.ts +82 -0
- package/src/pipeline/migration.ts +91 -0
- package/src/pipeline/orchestrator.ts +322 -0
- package/src/pipeline/packets/audit-report-builder.ts +47 -0
- package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
- package/src/pipeline/packets/index.ts +15 -0
- package/src/pipeline/packets/plan-packet-builder.ts +52 -0
- package/src/pipeline/packets/rca-packet-builder.ts +38 -0
- package/src/pipeline/phases/architecture.ts +73 -0
- package/src/pipeline/phases/audit.ts +193 -0
- package/src/pipeline/phases/consensus-architecture.ts +104 -0
- package/src/pipeline/phases/consensus-master-plan.ts +100 -0
- package/src/pipeline/phases/consensus-role-plans.ts +105 -0
- package/src/pipeline/phases/done.ts +68 -0
- package/src/pipeline/phases/implementation.ts +52 -0
- package/src/pipeline/phases/index.ts +21 -0
- package/src/pipeline/phases/intake.ts +68 -0
- package/src/pipeline/phases/phase-context.ts +86 -0
- package/src/pipeline/phases/production-gate.ts +113 -0
- package/src/pipeline/phases/qa-validation.ts +63 -0
- package/src/pipeline/phases/recovery-loop.ts +120 -0
- package/src/pipeline/phases/review.ts +149 -0
- package/src/pipeline/phases/role-planning.ts +92 -0
- package/src/pipeline/phases/stuck.ts +62 -0
- package/src/pipeline/repo-snapshot.ts +395 -0
- package/src/pipeline/role-execution-adapter.ts +238 -0
- package/src/pipeline/skill-loader.ts +192 -0
- package/src/pipeline/skills/defaults.ts +215 -0
- package/src/pipeline/type-defs/artifacts.ts +82 -0
- package/src/pipeline/type-defs/audit.ts +67 -0
- package/src/pipeline/type-defs/checks.ts +47 -0
- package/src/pipeline/type-defs/enums.ts +62 -0
- package/src/pipeline/type-defs/index.ts +12 -0
- package/src/pipeline/type-defs/packets.ts +131 -0
- package/src/pipeline/type-defs/snapshot.ts +55 -0
- package/src/pipeline/type-defs/state.ts +167 -0
- package/src/pipeline/types.ts +16 -0
- package/src/types/consensus.ts +16 -4
- package/src/types/index.ts +1 -0
- package/src/types/project.ts +39 -10
- package/src/types/workflow.ts +1 -1
- package/src/upgrade/handlers.ts +5 -5
- package/src/workflow/index.ts +52 -0
- package/src/workflow/website-strategy.ts +1 -1
- package/tests/cli/model-command.test.ts +19 -9
- package/tests/config/config.test.ts +3 -3
- package/tests/config/popeye-md.test.ts +168 -0
- package/tests/pipeline/artifact-manager.test.ts +183 -0
- package/tests/pipeline/artifact-validators.test.ts +207 -0
- package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
- package/tests/pipeline/change-request.test.ts +180 -0
- package/tests/pipeline/check-runner.test.ts +157 -0
- package/tests/pipeline/command-resolver.test.ts +159 -0
- package/tests/pipeline/consensus-runner.test.ts +206 -0
- package/tests/pipeline/consensus-scoring.test.ts +163 -0
- package/tests/pipeline/constitution.test.ts +122 -0
- package/tests/pipeline/gate-engine.test.ts +195 -0
- package/tests/pipeline/migration.test.ts +133 -0
- package/tests/pipeline/orchestrator.test.ts +614 -0
- package/tests/pipeline/packets/builders.test.ts +347 -0
- package/tests/pipeline/repo-snapshot.test.ts +189 -0
- package/tests/pipeline/role-execution-adapter.test.ts +299 -0
- package/tests/pipeline/session-guidance.test.ts +205 -0
- package/tests/pipeline/skill-loader.test.ts +186 -0
- package/tests/pipeline/start-env-checks.test.ts +123 -0
- package/tests/pipeline/types.test.ts +156 -0
- package/tests/types/consensus.test.ts +1 -1
- package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator tests — full loop with mocked phases, recovery/rewind/stuck.
|
|
3
|
+
* Tests the deterministic transition logic (P0-A) without real LLM calls.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { createGateEngine } from '../../src/pipeline/gate-engine.js';
|
|
8
|
+
import { createDefaultPipelineState } from '../../src/pipeline/types.js';
|
|
9
|
+
import type { PipelinePhase, PipelineState, ArtifactEntry } from '../../src/pipeline/types.js';
|
|
10
|
+
import type { GateResult } from '../../src/pipeline/gate-engine.js';
|
|
11
|
+
|
|
12
|
+
// We test the orchestrator's core logic by simulating gate/phase behavior
|
|
13
|
+
// rather than importing runPipeline (which pulls in LLM deps)
|
|
14
|
+
|
|
15
|
+
function makeArtifact(type: string, phase: string): ArtifactEntry {
|
|
16
|
+
return {
|
|
17
|
+
id: `test-${type}-${phase}-${Date.now()}`,
|
|
18
|
+
type: type as ArtifactEntry['type'],
|
|
19
|
+
phase: phase as PipelinePhase,
|
|
20
|
+
version: 1,
|
|
21
|
+
path: `docs/${type}.md`,
|
|
22
|
+
sha256: 'abc123',
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
immutable: true,
|
|
25
|
+
content_type: 'markdown',
|
|
26
|
+
group_id: `group-${type}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simulates the orchestrator's core transition logic without
|
|
32
|
+
* actually running phase handlers or importing LLM dependencies.
|
|
33
|
+
*/
|
|
34
|
+
function simulateOrchestratorLoop(
|
|
35
|
+
startPhase: PipelinePhase,
|
|
36
|
+
pipeline: PipelineState,
|
|
37
|
+
phaseOutcomes: Map<PipelinePhase, boolean>, // phase -> gate pass?
|
|
38
|
+
maxIterations = 50,
|
|
39
|
+
): { finalPhase: PipelinePhase; recoveryCount: number; phaseLog: PipelinePhase[] } {
|
|
40
|
+
const engine = createGateEngine();
|
|
41
|
+
let phase = startPhase;
|
|
42
|
+
let failedPhase: PipelinePhase | null = null;
|
|
43
|
+
const phaseLog: PipelinePhase[] = [phase];
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
46
|
+
if (phase === 'DONE' || phase === 'STUCK') break;
|
|
47
|
+
|
|
48
|
+
const gatePass = phaseOutcomes.get(phase) ?? false;
|
|
49
|
+
|
|
50
|
+
if (gatePass) {
|
|
51
|
+
if (phase === 'RECOVERY_LOOP') {
|
|
52
|
+
// Recovery succeeded — go back to failed phase
|
|
53
|
+
phase = failedPhase ?? 'QA_VALIDATION';
|
|
54
|
+
failedPhase = null;
|
|
55
|
+
} else {
|
|
56
|
+
const gateResult: GateResult = {
|
|
57
|
+
phase,
|
|
58
|
+
pass: true,
|
|
59
|
+
blockers: [],
|
|
60
|
+
missingArtifacts: [],
|
|
61
|
+
failedChecks: [],
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
phase = engine.getNextPhase(phase, gateResult);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
if (pipeline.recoveryCount >= pipeline.maxRecoveryIterations) {
|
|
68
|
+
phase = 'STUCK';
|
|
69
|
+
} else {
|
|
70
|
+
failedPhase = phase;
|
|
71
|
+
phase = 'RECOVERY_LOOP';
|
|
72
|
+
pipeline.recoveryCount++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pipeline.pipelinePhase = phase;
|
|
77
|
+
phaseLog.push(phase);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { finalPhase: phase, recoveryCount: pipeline.recoveryCount, phaseLog };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('Orchestrator Transition Logic', () => {
|
|
84
|
+
let pipeline: PipelineState;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
pipeline = createDefaultPipelineState();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('happy path', () => {
|
|
91
|
+
it('should progress through all phases to DONE', () => {
|
|
92
|
+
// All phases pass
|
|
93
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
94
|
+
['INTAKE', true],
|
|
95
|
+
['CONSENSUS_MASTER_PLAN', true],
|
|
96
|
+
['ARCHITECTURE', true],
|
|
97
|
+
['CONSENSUS_ARCHITECTURE', true],
|
|
98
|
+
['ROLE_PLANNING', true],
|
|
99
|
+
['CONSENSUS_ROLE_PLANS', true],
|
|
100
|
+
['IMPLEMENTATION', true],
|
|
101
|
+
['QA_VALIDATION', true],
|
|
102
|
+
['REVIEW', true],
|
|
103
|
+
['AUDIT', true],
|
|
104
|
+
['PRODUCTION_GATE', true],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
|
|
108
|
+
expect(result.finalPhase).toBe('DONE');
|
|
109
|
+
expect(result.recoveryCount).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should follow the correct phase sequence', () => {
|
|
113
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
114
|
+
['INTAKE', true],
|
|
115
|
+
['CONSENSUS_MASTER_PLAN', true],
|
|
116
|
+
['ARCHITECTURE', true],
|
|
117
|
+
['CONSENSUS_ARCHITECTURE', true],
|
|
118
|
+
['ROLE_PLANNING', true],
|
|
119
|
+
['CONSENSUS_ROLE_PLANS', true],
|
|
120
|
+
['IMPLEMENTATION', true],
|
|
121
|
+
['QA_VALIDATION', true],
|
|
122
|
+
['REVIEW', true],
|
|
123
|
+
['AUDIT', true],
|
|
124
|
+
['PRODUCTION_GATE', true],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
|
|
128
|
+
expect(result.phaseLog).toEqual([
|
|
129
|
+
'INTAKE',
|
|
130
|
+
'CONSENSUS_MASTER_PLAN',
|
|
131
|
+
'ARCHITECTURE',
|
|
132
|
+
'CONSENSUS_ARCHITECTURE',
|
|
133
|
+
'ROLE_PLANNING',
|
|
134
|
+
'CONSENSUS_ROLE_PLANS',
|
|
135
|
+
'IMPLEMENTATION',
|
|
136
|
+
'QA_VALIDATION',
|
|
137
|
+
'REVIEW',
|
|
138
|
+
'AUDIT',
|
|
139
|
+
'PRODUCTION_GATE',
|
|
140
|
+
'DONE',
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('recovery loop', () => {
|
|
146
|
+
it('should enter RECOVERY_LOOP on gate failure', () => {
|
|
147
|
+
// INTAKE passes, CONSENSUS fails first time, then passes
|
|
148
|
+
let consensusAttempt = 0;
|
|
149
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
150
|
+
['INTAKE', true],
|
|
151
|
+
['RECOVERY_LOOP', true],
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
// Override CONSENSUS to fail once then pass
|
|
155
|
+
const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
|
|
156
|
+
// INTAKE passes -> CONSENSUS (not in outcomes, defaults to false) -> RECOVERY -> CONSENSUS (still false)
|
|
157
|
+
// Eventually hits max recovery
|
|
158
|
+
expect(result.phaseLog).toContain('RECOVERY_LOOP');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should go to STUCK after max recovery iterations', () => {
|
|
162
|
+
pipeline.maxRecoveryIterations = 3;
|
|
163
|
+
|
|
164
|
+
// Only INTAKE passes, everything else fails
|
|
165
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
166
|
+
['INTAKE', true],
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
|
|
170
|
+
expect(result.finalPhase).toBe('STUCK');
|
|
171
|
+
expect(result.recoveryCount).toBe(3);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return to failed phase after successful recovery', () => {
|
|
175
|
+
let qaAttempts = 0;
|
|
176
|
+
|
|
177
|
+
// Custom simulation: QA fails once, recovery succeeds, QA succeeds on retry
|
|
178
|
+
const engine = createGateEngine();
|
|
179
|
+
let phase: PipelinePhase = 'QA_VALIDATION';
|
|
180
|
+
let failedPhase: PipelinePhase | null = null;
|
|
181
|
+
const log: PipelinePhase[] = [phase];
|
|
182
|
+
|
|
183
|
+
// Pre-fill pipeline to reach QA
|
|
184
|
+
// Step 1: QA fails
|
|
185
|
+
failedPhase = 'QA_VALIDATION';
|
|
186
|
+
phase = 'RECOVERY_LOOP';
|
|
187
|
+
pipeline.recoveryCount++;
|
|
188
|
+
log.push(phase);
|
|
189
|
+
|
|
190
|
+
// Step 2: RECOVERY passes, returns to QA
|
|
191
|
+
phase = failedPhase;
|
|
192
|
+
failedPhase = null;
|
|
193
|
+
log.push(phase);
|
|
194
|
+
|
|
195
|
+
// Step 3: QA passes this time
|
|
196
|
+
const gr: GateResult = { phase, pass: true, blockers: [], missingArtifacts: [], failedChecks: [], timestamp: '' };
|
|
197
|
+
phase = engine.getNextPhase('QA_VALIDATION', gr);
|
|
198
|
+
log.push(phase);
|
|
199
|
+
|
|
200
|
+
expect(log).toEqual(['QA_VALIDATION', 'RECOVERY_LOOP', 'QA_VALIDATION', 'REVIEW']);
|
|
201
|
+
expect(pipeline.recoveryCount).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('resume from mid-pipeline', () => {
|
|
206
|
+
it('should resume from ARCHITECTURE', () => {
|
|
207
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
208
|
+
['ARCHITECTURE', true],
|
|
209
|
+
['CONSENSUS_ARCHITECTURE', true],
|
|
210
|
+
['ROLE_PLANNING', true],
|
|
211
|
+
['CONSENSUS_ROLE_PLANS', true],
|
|
212
|
+
['IMPLEMENTATION', true],
|
|
213
|
+
['QA_VALIDATION', true],
|
|
214
|
+
['REVIEW', true],
|
|
215
|
+
['AUDIT', true],
|
|
216
|
+
['PRODUCTION_GATE', true],
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
const result = simulateOrchestratorLoop('ARCHITECTURE', pipeline, outcomes);
|
|
220
|
+
expect(result.finalPhase).toBe('DONE');
|
|
221
|
+
expect(result.phaseLog[0]).toBe('ARCHITECTURE');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should resume from IMPLEMENTATION', () => {
|
|
225
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
226
|
+
['IMPLEMENTATION', true],
|
|
227
|
+
['QA_VALIDATION', true],
|
|
228
|
+
['REVIEW', true],
|
|
229
|
+
['AUDIT', true],
|
|
230
|
+
['PRODUCTION_GATE', true],
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const result = simulateOrchestratorLoop('IMPLEMENTATION', pipeline, outcomes);
|
|
234
|
+
expect(result.finalPhase).toBe('DONE');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('gate engine integration', () => {
|
|
239
|
+
it('should evaluate INTAKE gate correctly', () => {
|
|
240
|
+
const engine = createGateEngine();
|
|
241
|
+
|
|
242
|
+
// Missing artifacts
|
|
243
|
+
const result1 = engine.evaluateGate('INTAKE', pipeline);
|
|
244
|
+
expect(result1.pass).toBe(false);
|
|
245
|
+
|
|
246
|
+
// Add required artifacts (including constitution for v1.1)
|
|
247
|
+
pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
|
|
248
|
+
pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
|
|
249
|
+
pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
|
|
250
|
+
|
|
251
|
+
const result2 = engine.evaluateGate('INTAKE', pipeline);
|
|
252
|
+
expect(result2.pass).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should fail gate when constitution is invalid (v1.1)', () => {
|
|
256
|
+
const engine = createGateEngine();
|
|
257
|
+
|
|
258
|
+
// Add all required artifacts
|
|
259
|
+
pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
|
|
260
|
+
pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
|
|
261
|
+
pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
|
|
262
|
+
|
|
263
|
+
// Gate should pass normally
|
|
264
|
+
const passResult = engine.evaluateGate('INTAKE', pipeline);
|
|
265
|
+
expect(passResult.pass).toBe(true);
|
|
266
|
+
|
|
267
|
+
// With constitutionValid=false, gate should fail
|
|
268
|
+
const failResult = engine.evaluateGate('INTAKE', pipeline, {
|
|
269
|
+
constitutionValid: false,
|
|
270
|
+
constitutionReason: 'Constitution has been modified since pipeline start',
|
|
271
|
+
});
|
|
272
|
+
expect(failResult.pass).toBe(false);
|
|
273
|
+
expect(failResult.blockers.some((b: string) => b.includes('Constitution'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should evaluate PRODUCTION_GATE with check results', () => {
|
|
277
|
+
const engine = createGateEngine();
|
|
278
|
+
|
|
279
|
+
// Add required artifacts (production_readiness + audit_report)
|
|
280
|
+
pipeline.artifacts.push(makeArtifact('production_readiness', 'PRODUCTION_GATE'));
|
|
281
|
+
pipeline.artifacts.push(makeArtifact('audit_report', 'AUDIT'));
|
|
282
|
+
|
|
283
|
+
// Without check results — still should fail
|
|
284
|
+
const result1 = engine.evaluateGate('PRODUCTION_GATE', pipeline);
|
|
285
|
+
expect(result1.pass).toBe(false);
|
|
286
|
+
|
|
287
|
+
// Add check results
|
|
288
|
+
pipeline.gateChecks['PRODUCTION_GATE'] = [
|
|
289
|
+
{ check_type: 'build', status: 'pass', command: 'npm run build', exit_code: 0, duration_ms: 1000, timestamp: '' },
|
|
290
|
+
{ check_type: 'test', status: 'pass', command: 'npm test', exit_code: 0, duration_ms: 2000, timestamp: '' },
|
|
291
|
+
{ check_type: 'lint', status: 'pass', command: 'npm run lint', exit_code: 0, duration_ms: 500, timestamp: '' },
|
|
292
|
+
{ check_type: 'typecheck', status: 'pass', command: 'tsc', exit_code: 0, duration_ms: 300, timestamp: '' },
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
const result2 = engine.evaluateGate('PRODUCTION_GATE', pipeline);
|
|
296
|
+
expect(result2.pass).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('callbacks', () => {
|
|
301
|
+
it('should track phase transitions in log', () => {
|
|
302
|
+
const outcomes = new Map<PipelinePhase, boolean>([
|
|
303
|
+
['INTAKE', true],
|
|
304
|
+
['CONSENSUS_MASTER_PLAN', true],
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
|
|
308
|
+
expect(result.phaseLog.length).toBeGreaterThan(1);
|
|
309
|
+
expect(result.phaseLog[0]).toBe('INTAKE');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── v1.1 Gap Fix Tests ────────────────────────────────
|
|
314
|
+
|
|
315
|
+
describe('v1.1: CR routing (Gap #1)', () => {
|
|
316
|
+
it('should route to consensus phase when pending CRs exist after REVIEW', () => {
|
|
317
|
+
// Simulate: REVIEW passes but has a pending CR targeting CONSENSUS_MASTER_PLAN
|
|
318
|
+
pipeline.pendingChangeRequests = [
|
|
319
|
+
{ cr_id: 'CR-001', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const engine = createGateEngine();
|
|
323
|
+
let phase: PipelinePhase = 'REVIEW';
|
|
324
|
+
const log: PipelinePhase[] = [phase];
|
|
325
|
+
|
|
326
|
+
// REVIEW gate passes
|
|
327
|
+
const gateResult: GateResult = {
|
|
328
|
+
phase: 'REVIEW', pass: true, blockers: [],
|
|
329
|
+
missingArtifacts: [], failedChecks: [], timestamp: '',
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Check for pending CRs (same logic as orchestrator)
|
|
333
|
+
const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
|
|
334
|
+
if (crCheckPhases.has(phase)) {
|
|
335
|
+
const pending = pipeline.pendingChangeRequests;
|
|
336
|
+
const nextCR = pending?.find((cr) => cr.status === 'proposed');
|
|
337
|
+
if (nextCR) {
|
|
338
|
+
nextCR.status = 'approved';
|
|
339
|
+
phase = nextCR.target_phase;
|
|
340
|
+
log.push(phase);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
expect(phase).toBe('CONSENSUS_MASTER_PLAN');
|
|
345
|
+
expect(pipeline.pendingChangeRequests![0].status).toBe('approved');
|
|
346
|
+
expect(log).toEqual(['REVIEW', 'CONSENSUS_MASTER_PLAN']);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should route to consensus phase when pending CRs exist after AUDIT', () => {
|
|
350
|
+
pipeline.pendingChangeRequests = [
|
|
351
|
+
{ cr_id: 'CR-002', change_type: 'architecture', target_phase: 'CONSENSUS_ARCHITECTURE', status: 'proposed' },
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
let phase: PipelinePhase = 'AUDIT';
|
|
355
|
+
const log: PipelinePhase[] = [phase];
|
|
356
|
+
|
|
357
|
+
const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
|
|
358
|
+
if (crCheckPhases.has(phase)) {
|
|
359
|
+
const nextCR = pipeline.pendingChangeRequests?.find((cr) => cr.status === 'proposed');
|
|
360
|
+
if (nextCR) {
|
|
361
|
+
nextCR.status = 'approved';
|
|
362
|
+
phase = nextCR.target_phase;
|
|
363
|
+
log.push(phase);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
expect(phase).toBe('CONSENSUS_ARCHITECTURE');
|
|
368
|
+
expect(pipeline.pendingChangeRequests![0].status).toBe('approved');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should not re-route already approved CRs', () => {
|
|
372
|
+
pipeline.pendingChangeRequests = [
|
|
373
|
+
{ cr_id: 'CR-003', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'approved' },
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
let phase: PipelinePhase = 'REVIEW';
|
|
377
|
+
const nextCR = pipeline.pendingChangeRequests?.find((cr) => cr.status === 'proposed');
|
|
378
|
+
expect(nextCR).toBeUndefined();
|
|
379
|
+
// Phase should continue normally (no routing)
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should process multiple CRs one at a time', () => {
|
|
383
|
+
pipeline.pendingChangeRequests = [
|
|
384
|
+
{ cr_id: 'CR-A', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
|
|
385
|
+
{ cr_id: 'CR-B', change_type: 'architecture', target_phase: 'CONSENSUS_ARCHITECTURE', status: 'proposed' },
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
// First routing: picks CR-A
|
|
389
|
+
const firstCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
|
|
390
|
+
expect(firstCR?.cr_id).toBe('CR-A');
|
|
391
|
+
firstCR!.status = 'approved';
|
|
392
|
+
|
|
393
|
+
// Second routing: picks CR-B
|
|
394
|
+
const secondCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
|
|
395
|
+
expect(secondCR?.cr_id).toBe('CR-B');
|
|
396
|
+
secondCR!.status = 'approved';
|
|
397
|
+
|
|
398
|
+
// No more pending CRs
|
|
399
|
+
const thirdCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
|
|
400
|
+
expect(thirdCR).toBeUndefined();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should not route CRs after non-review/audit phases', () => {
|
|
404
|
+
pipeline.pendingChangeRequests = [
|
|
405
|
+
{ cr_id: 'CR-X', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
|
|
409
|
+
// IMPLEMENTATION is not a CR-check phase
|
|
410
|
+
expect(crCheckPhases.has('IMPLEMENTATION')).toBe(false);
|
|
411
|
+
expect(crCheckPhases.has('QA_VALIDATION')).toBe(false);
|
|
412
|
+
expect(crCheckPhases.has('PRODUCTION_GATE')).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('v1.1: Constitution verification (Gap #2)', () => {
|
|
417
|
+
it('should block gate progression when constitution is invalid', () => {
|
|
418
|
+
const engine = createGateEngine();
|
|
419
|
+
|
|
420
|
+
// Provide all required artifacts for INTAKE
|
|
421
|
+
pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
|
|
422
|
+
pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
|
|
423
|
+
pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
|
|
424
|
+
|
|
425
|
+
// Without constitution check — passes
|
|
426
|
+
const passResult = engine.evaluateGate('INTAKE', pipeline);
|
|
427
|
+
expect(passResult.pass).toBe(true);
|
|
428
|
+
|
|
429
|
+
// With invalid constitution — fails
|
|
430
|
+
const failResult = engine.evaluateGate('INTAKE', pipeline, {
|
|
431
|
+
constitutionValid: false,
|
|
432
|
+
constitutionReason: 'Hash mismatch: constitution modified after INTAKE',
|
|
433
|
+
});
|
|
434
|
+
expect(failResult.pass).toBe(false);
|
|
435
|
+
expect(failResult.blockers).toContain('Hash mismatch: constitution modified after INTAKE');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should pass gate when constitution is valid', () => {
|
|
439
|
+
const engine = createGateEngine();
|
|
440
|
+
pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
|
|
441
|
+
pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
|
|
442
|
+
pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
|
|
443
|
+
|
|
444
|
+
const result = engine.evaluateGate('INTAKE', pipeline, {
|
|
445
|
+
constitutionValid: true,
|
|
446
|
+
});
|
|
447
|
+
expect(result.pass).toBe(true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should apply constitution check to non-INTAKE phases too', () => {
|
|
451
|
+
const engine = createGateEngine();
|
|
452
|
+
|
|
453
|
+
// Setup for ARCHITECTURE phase
|
|
454
|
+
pipeline.artifacts.push(makeArtifact('architecture', 'ARCHITECTURE'));
|
|
455
|
+
|
|
456
|
+
const result = engine.evaluateGate('ARCHITECTURE', pipeline, {
|
|
457
|
+
constitutionValid: false,
|
|
458
|
+
constitutionReason: 'Constitution tampered',
|
|
459
|
+
});
|
|
460
|
+
expect(result.pass).toBe(false);
|
|
461
|
+
expect(result.blockers.some((b: string) => b.includes('Constitution'))).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('v1.1: gateResult merge (Gap #3)', () => {
|
|
466
|
+
it('should preserve consensus score when merging gate result', () => {
|
|
467
|
+
// Simulate: consensus phase stored a score in gateResults
|
|
468
|
+
pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = {
|
|
469
|
+
phase: 'CONSENSUS_MASTER_PLAN',
|
|
470
|
+
pass: true,
|
|
471
|
+
score: 0.85,
|
|
472
|
+
consensusScore: 0.92,
|
|
473
|
+
blockers: [],
|
|
474
|
+
missingArtifacts: [],
|
|
475
|
+
failedChecks: [],
|
|
476
|
+
timestamp: new Date().toISOString(),
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Gate engine produces a new result (without score)
|
|
480
|
+
const newGateResult: GateResult = {
|
|
481
|
+
phase: 'CONSENSUS_MASTER_PLAN',
|
|
482
|
+
pass: true,
|
|
483
|
+
blockers: [],
|
|
484
|
+
missingArtifacts: [],
|
|
485
|
+
failedChecks: [],
|
|
486
|
+
timestamp: new Date().toISOString(),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Merge logic (same as orchestrator's mergeGateResult)
|
|
490
|
+
const existing = pipeline.gateResults['CONSENSUS_MASTER_PLAN'];
|
|
491
|
+
if (existing?.score !== undefined || existing?.consensusScore !== undefined) {
|
|
492
|
+
pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = {
|
|
493
|
+
...newGateResult,
|
|
494
|
+
score: existing.score ?? newGateResult.score,
|
|
495
|
+
consensusScore: existing.consensusScore ?? newGateResult.consensusScore,
|
|
496
|
+
};
|
|
497
|
+
} else {
|
|
498
|
+
pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = newGateResult;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Scores should be preserved
|
|
502
|
+
const merged = pipeline.gateResults['CONSENSUS_MASTER_PLAN'];
|
|
503
|
+
expect(merged.score).toBe(0.85);
|
|
504
|
+
expect(merged.consensusScore).toBe(0.92);
|
|
505
|
+
// But pass/blockers come from the new gate result
|
|
506
|
+
expect(merged.pass).toBe(true);
|
|
507
|
+
expect(merged.blockers).toEqual([]);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should overwrite when no prior scores exist', () => {
|
|
511
|
+
const newGateResult: GateResult = {
|
|
512
|
+
phase: 'INTAKE',
|
|
513
|
+
pass: true,
|
|
514
|
+
blockers: [],
|
|
515
|
+
missingArtifacts: [],
|
|
516
|
+
failedChecks: [],
|
|
517
|
+
timestamp: new Date().toISOString(),
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// No existing entry
|
|
521
|
+
const existing = pipeline.gateResults['INTAKE'];
|
|
522
|
+
expect(existing).toBeUndefined();
|
|
523
|
+
|
|
524
|
+
// Merge should just set the new result
|
|
525
|
+
pipeline.gateResults['INTAKE'] = newGateResult;
|
|
526
|
+
expect(pipeline.gateResults['INTAKE']).toBe(newGateResult);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('should preserve score even when gate fails', () => {
|
|
530
|
+
pipeline.gateResults['CONSENSUS_ARCHITECTURE'] = {
|
|
531
|
+
phase: 'CONSENSUS_ARCHITECTURE',
|
|
532
|
+
pass: true,
|
|
533
|
+
score: 0.7,
|
|
534
|
+
blockers: [],
|
|
535
|
+
missingArtifacts: [],
|
|
536
|
+
failedChecks: [],
|
|
537
|
+
timestamp: '',
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const failingGateResult: GateResult = {
|
|
541
|
+
phase: 'CONSENSUS_ARCHITECTURE',
|
|
542
|
+
pass: false,
|
|
543
|
+
blockers: ['Missing required artifact'],
|
|
544
|
+
missingArtifacts: [],
|
|
545
|
+
failedChecks: [],
|
|
546
|
+
timestamp: '',
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const existing = pipeline.gateResults['CONSENSUS_ARCHITECTURE'];
|
|
550
|
+
if (existing?.score !== undefined || existing?.consensusScore !== undefined) {
|
|
551
|
+
pipeline.gateResults['CONSENSUS_ARCHITECTURE'] = {
|
|
552
|
+
...failingGateResult,
|
|
553
|
+
score: existing.score ?? failingGateResult.score,
|
|
554
|
+
consensusScore: existing.consensusScore ?? failingGateResult.consensusScore,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const merged = pipeline.gateResults['CONSENSUS_ARCHITECTURE'];
|
|
559
|
+
expect(merged.pass).toBe(false); // gate engine says fail
|
|
560
|
+
expect(merged.score).toBe(0.7); // consensus score preserved
|
|
561
|
+
expect(merged.blockers).toEqual(['Missing required artifact']);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe('v1.1: RCA rewind (Gap #4)', () => {
|
|
566
|
+
it('should return to failed phase after recovery when no RCA rewind target', () => {
|
|
567
|
+
// Manual simulation: phase fails -> recovery -> back to failed phase
|
|
568
|
+
let phase: PipelinePhase = 'QA_VALIDATION';
|
|
569
|
+
let failedPhase: PipelinePhase | null = null;
|
|
570
|
+
|
|
571
|
+
// QA fails
|
|
572
|
+
failedPhase = phase;
|
|
573
|
+
phase = 'RECOVERY_LOOP';
|
|
574
|
+
pipeline.recoveryCount++;
|
|
575
|
+
|
|
576
|
+
// Recovery succeeds, no RCA rewind target
|
|
577
|
+
// Without RCA, should return to failedPhase
|
|
578
|
+
phase = failedPhase ?? 'QA_VALIDATION';
|
|
579
|
+
failedPhase = null;
|
|
580
|
+
|
|
581
|
+
expect(phase).toBe('QA_VALIDATION');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should use RCA rewind target when available', () => {
|
|
585
|
+
// Simulate: RCA says rewind to ARCHITECTURE
|
|
586
|
+
let phase: PipelinePhase = 'QA_VALIDATION';
|
|
587
|
+
let failedPhase: PipelinePhase | null = 'QA_VALIDATION';
|
|
588
|
+
|
|
589
|
+
phase = 'RECOVERY_LOOP';
|
|
590
|
+
pipeline.recoveryCount++;
|
|
591
|
+
|
|
592
|
+
// Recovery succeeds, RCA has rewind target
|
|
593
|
+
const rca = { requires_phase_rewind_to: 'ARCHITECTURE' as PipelinePhase };
|
|
594
|
+
if (rca.requires_phase_rewind_to) {
|
|
595
|
+
phase = rca.requires_phase_rewind_to;
|
|
596
|
+
} else {
|
|
597
|
+
phase = failedPhase ?? 'QA_VALIDATION';
|
|
598
|
+
}
|
|
599
|
+
failedPhase = null;
|
|
600
|
+
|
|
601
|
+
expect(phase).toBe('ARCHITECTURE');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should default to QA_VALIDATION when no failed phase tracked', () => {
|
|
605
|
+
let phase: PipelinePhase = 'RECOVERY_LOOP';
|
|
606
|
+
let failedPhase: PipelinePhase | null = null;
|
|
607
|
+
|
|
608
|
+
// Recovery succeeds, no RCA, no failedPhase
|
|
609
|
+
phase = failedPhase ?? 'QA_VALIDATION';
|
|
610
|
+
|
|
611
|
+
expect(phase).toBe('QA_VALIDATION');
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|