popeye-cli 2.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/CONTRIBUTING.md +23 -2
  3. package/README.md +47 -18
  4. package/dist/adapters/gemini.js +3 -3
  5. package/dist/adapters/openai.js +2 -2
  6. package/dist/adapters/openai.js.map +1 -1
  7. package/dist/auth/gemini.js +1 -1
  8. package/dist/cli/commands/create.d.ts.map +1 -1
  9. package/dist/cli/commands/create.js +11 -5
  10. package/dist/cli/commands/create.js.map +1 -1
  11. package/dist/cli/commands/resume.d.ts.map +1 -1
  12. package/dist/cli/commands/resume.js +9 -1
  13. package/dist/cli/commands/resume.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +29 -3
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/config/defaults.d.ts.map +1 -1
  18. package/dist/config/defaults.js +7 -2
  19. package/dist/config/defaults.js.map +1 -1
  20. package/dist/config/index.d.ts +1 -7
  21. package/dist/config/index.d.ts.map +1 -1
  22. package/dist/config/popeye-md.d.ts +32 -0
  23. package/dist/config/popeye-md.d.ts.map +1 -0
  24. package/dist/config/popeye-md.js +111 -0
  25. package/dist/config/popeye-md.js.map +1 -0
  26. package/dist/config/schema.d.ts +3 -21
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +21 -8
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
  31. package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
  32. package/dist/pipeline/bridges/review-bridge.js +266 -0
  33. package/dist/pipeline/bridges/review-bridge.js.map +1 -0
  34. package/dist/pipeline/consensus/consensus-runner.js +3 -3
  35. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  36. package/dist/pipeline/orchestrator.d.ts +2 -0
  37. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  38. package/dist/pipeline/orchestrator.js +5 -1
  39. package/dist/pipeline/orchestrator.js.map +1 -1
  40. package/dist/pipeline/phases/implementation.d.ts.map +1 -1
  41. package/dist/pipeline/phases/implementation.js +5 -2
  42. package/dist/pipeline/phases/implementation.js.map +1 -1
  43. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  44. package/dist/pipeline/phases/intake.js +13 -4
  45. package/dist/pipeline/phases/intake.js.map +1 -1
  46. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  47. package/dist/pipeline/phases/recovery-loop.js +2 -0
  48. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  49. package/dist/pipeline/type-defs/artifacts.d.ts +5 -0
  50. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  51. package/dist/pipeline/type-defs/artifacts.js +1 -0
  52. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  53. package/dist/pipeline/type-defs/audit.d.ts +3 -0
  54. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  55. package/dist/pipeline/type-defs/checks.d.ts +1 -0
  56. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  57. package/dist/pipeline/type-defs/packets.d.ts +15 -0
  58. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  59. package/dist/pipeline/type-defs/state.d.ts +6 -0
  60. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  61. package/dist/pipeline/type-defs/state.js +2 -0
  62. package/dist/pipeline/type-defs/state.js.map +1 -1
  63. package/dist/types/consensus.d.ts +5 -1
  64. package/dist/types/consensus.d.ts.map +1 -1
  65. package/dist/types/consensus.js +15 -4
  66. package/dist/types/consensus.js.map +1 -1
  67. package/dist/types/index.d.ts +1 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/types/index.js +1 -1
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/types/project.d.ts +1 -1
  72. package/dist/types/project.d.ts.map +1 -1
  73. package/dist/types/project.js +39 -10
  74. package/dist/types/project.js.map +1 -1
  75. package/dist/types/workflow.d.ts +1 -7
  76. package/dist/types/workflow.d.ts.map +1 -1
  77. package/dist/types/workflow.js +1 -1
  78. package/dist/types/workflow.js.map +1 -1
  79. package/dist/upgrade/handlers.js +5 -5
  80. package/dist/upgrade/handlers.js.map +1 -1
  81. package/dist/workflow/index.d.ts.map +1 -1
  82. package/dist/workflow/index.js +18 -14
  83. package/dist/workflow/index.js.map +1 -1
  84. package/dist/workflow/website-strategy.js +1 -1
  85. package/dist/workflow/website-strategy.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/adapters/gemini.ts +3 -3
  88. package/src/adapters/openai.ts +2 -2
  89. package/src/auth/gemini.ts +1 -1
  90. package/src/cli/commands/create.ts +12 -6
  91. package/src/cli/commands/resume.ts +9 -1
  92. package/src/cli/interactive.ts +32 -3
  93. package/src/config/defaults.ts +7 -2
  94. package/src/config/popeye-md.ts +139 -0
  95. package/src/config/schema.ts +21 -8
  96. package/src/pipeline/bridges/review-bridge.ts +371 -0
  97. package/src/pipeline/consensus/consensus-runner.ts +3 -3
  98. package/src/pipeline/orchestrator.ts +8 -0
  99. package/src/pipeline/phases/implementation.ts +6 -2
  100. package/src/pipeline/phases/intake.ts +18 -4
  101. package/src/pipeline/phases/recovery-loop.ts +2 -0
  102. package/src/pipeline/type-defs/artifacts.ts +1 -0
  103. package/src/pipeline/type-defs/state.ts +2 -0
  104. package/src/types/consensus.ts +16 -4
  105. package/src/types/index.ts +1 -0
  106. package/src/types/project.ts +39 -10
  107. package/src/types/workflow.ts +1 -1
  108. package/src/upgrade/handlers.ts +5 -5
  109. package/src/workflow/index.ts +18 -14
  110. package/src/workflow/website-strategy.ts +1 -1
  111. package/tests/cli/model-command.test.ts +19 -9
  112. package/tests/config/config.test.ts +3 -3
  113. package/tests/config/popeye-md.test.ts +168 -0
  114. package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
  115. package/tests/pipeline/session-guidance.test.ts +205 -0
  116. package/tests/types/consensus.test.ts +1 -1
  117. package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
@@ -349,7 +349,7 @@ export async function upgradeFullstackToAll(
349
349
  idea: 'Marketing website',
350
350
  name: projectName,
351
351
  language: 'all',
352
- openaiModel: 'gpt-4o',
352
+ openaiModel: 'gpt-4.1',
353
353
  };
354
354
 
355
355
  // Build content context from user docs, brand assets, and strategy
@@ -421,7 +421,7 @@ export async function upgradeSingleToFullstack(
421
421
  if (!(await pathExists(frontendDir))) {
422
422
  const spec: ProjectSpec = {
423
423
  idea: 'Frontend application', name: projectName,
424
- language: 'fullstack', openaiModel: 'gpt-4o',
424
+ language: 'fullstack', openaiModel: 'gpt-4.1',
425
425
  };
426
426
  const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
427
427
  baseDir: frontendDir,
@@ -439,7 +439,7 @@ export async function upgradeSingleToFullstack(
439
439
  if (!(await pathExists(backendDir))) {
440
440
  const spec: ProjectSpec = {
441
441
  idea: 'Backend API', name: projectName,
442
- language: 'fullstack', openaiModel: 'gpt-4o',
442
+ language: 'fullstack', openaiModel: 'gpt-4.1',
443
443
  };
444
444
  const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
445
445
  baseDir: backendDir,
@@ -508,7 +508,7 @@ export async function upgradeWebsiteToAll(
508
508
  if (!(await pathExists(frontendDir))) {
509
509
  const spec: ProjectSpec = {
510
510
  idea: 'Frontend application', name: projectName,
511
- language: 'all', openaiModel: 'gpt-4o',
511
+ language: 'all', openaiModel: 'gpt-4.1',
512
512
  };
513
513
  const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
514
514
  baseDir: frontendDir,
@@ -520,7 +520,7 @@ export async function upgradeWebsiteToAll(
520
520
  if (!(await pathExists(backendDir))) {
521
521
  const spec: ProjectSpec = {
522
522
  idea: 'Backend API', name: projectName,
523
- language: 'all', openaiModel: 'gpt-4o',
523
+ language: 'all', openaiModel: 'gpt-4.1',
524
524
  };
525
525
  const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
526
526
  baseDir: backendDir,
@@ -104,21 +104,24 @@ export async function runWorkflow(
104
104
  const useLegacy = process.env.POPEYE_LEGACY_WORKFLOW === '1' || process.env.POPEYE_LEGACY_WORKFLOW === 'true';
105
105
  if (!useLegacy) {
106
106
  try {
107
- const state = await loadProject(projectDir).catch(() => null);
108
- if (state) {
109
- const result = await runPipeline({
110
- projectDir,
111
- state,
112
- consensusConfig,
113
- onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
114
- onProgress: (msg) => onProgress?.('pipeline', msg),
115
- });
116
- return {
117
- success: result.success,
118
- state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
119
- error: result.error,
120
- };
107
+ // Bootstrap state if it doesn't exist yet (new projects)
108
+ let state = await loadProject(projectDir).catch(() => null);
109
+ if (!state) {
110
+ const { createProject } = await import('../state/index.js');
111
+ state = await createProject(spec, projectDir);
121
112
  }
113
+ const result = await runPipeline({
114
+ projectDir,
115
+ state,
116
+ consensusConfig,
117
+ onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
118
+ onProgress: (msg) => onProgress?.('pipeline', msg),
119
+ });
120
+ return {
121
+ success: result.success,
122
+ state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
123
+ error: result.error,
124
+ };
122
125
  } catch {
123
126
  // Fall through to legacy workflow on pipeline error
124
127
  onProgress?.('workflow', 'Pipeline mode failed, falling back to legacy workflow...');
@@ -241,6 +244,7 @@ export async function resumeWorkflow(
241
244
  projectDir,
242
245
  state,
243
246
  consensusConfig,
247
+ additionalContext,
244
248
  onPhaseStart: (phase) => onProgress?.('pipeline', `Resuming phase: ${phase}`),
245
249
  onProgress: (msg) => onProgress?.('pipeline', msg),
246
250
  });
@@ -142,7 +142,7 @@ Respond with ONLY valid JSON, no markdown code fences or explanation.`;
142
142
  onProgress?.('Generating website strategy via AI...');
143
143
 
144
144
  const completion = await client.chat.completions.create({
145
- model: 'gpt-4o',
145
+ model: 'gpt-4.1',
146
146
  messages: [{ role: 'user', content: prompt }],
147
147
  temperature: 0.4,
148
148
  max_tokens: 4096,
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, it, expect } from 'vitest';
7
7
  import { OpenAIModelSchema, KNOWN_OPENAI_MODELS } from '../../src/types/project.js';
8
- import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS } from '../../src/types/consensus.js';
8
+ import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS, KNOWN_GROK_MODELS } from '../../src/types/consensus.js';
9
9
 
10
10
  describe('OpenAI model validation', () => {
11
11
  it('should accept known OpenAI models', () => {
@@ -17,7 +17,7 @@ describe('OpenAI model validation', () => {
17
17
  it('should accept unknown/new OpenAI models (flexible)', () => {
18
18
  expect(OpenAIModelSchema.safeParse('gpt-5').success).toBe(true);
19
19
  expect(OpenAIModelSchema.safeParse('gpt-5.2-turbo').success).toBe(true);
20
- expect(OpenAIModelSchema.safeParse('o3-mini').success).toBe(true);
20
+ expect(OpenAIModelSchema.safeParse('some-future-model').success).toBe(true);
21
21
  });
22
22
 
23
23
  it('should reject empty string', () => {
@@ -33,8 +33,8 @@ describe('Gemini model validation', () => {
33
33
  });
34
34
 
35
35
  it('should accept unknown/new Gemini models (flexible)', () => {
36
- expect(GeminiModelSchema.safeParse('gemini-2.5-pro').success).toBe(true);
37
36
  expect(GeminiModelSchema.safeParse('gemini-3.0-ultra').success).toBe(true);
37
+ expect(GeminiModelSchema.safeParse('gemini-4.0-flash').success).toBe(true);
38
38
  });
39
39
 
40
40
  it('should reject empty string', () => {
@@ -44,9 +44,9 @@ describe('Gemini model validation', () => {
44
44
 
45
45
  describe('Grok model validation', () => {
46
46
  it('should accept any non-empty string as Grok model', () => {
47
+ expect(GrokModelSchema.safeParse('grok-4-0709').success).toBe(true);
47
48
  expect(GrokModelSchema.safeParse('grok-3').success).toBe(true);
48
49
  expect(GrokModelSchema.safeParse('grok-3-mini').success).toBe(true);
49
- expect(GrokModelSchema.safeParse('grok-2').success).toBe(true);
50
50
  expect(GrokModelSchema.safeParse('some-future-model').success).toBe(true);
51
51
  });
52
52
 
@@ -62,15 +62,25 @@ describe('Grok model validation', () => {
62
62
 
63
63
  describe('known models lists', () => {
64
64
  it('should have known OpenAI models', () => {
65
+ expect(KNOWN_OPENAI_MODELS).toContain('gpt-4.1');
65
66
  expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o');
66
- expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o-mini');
67
- expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(5);
67
+ expect(KNOWN_OPENAI_MODELS).toContain('o3');
68
+ expect(KNOWN_OPENAI_MODELS).toContain('o4-mini');
69
+ expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(8);
68
70
  });
69
71
 
70
72
  it('should have known Gemini models', () => {
73
+ expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-flash');
74
+ expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-pro');
71
75
  expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.0-flash');
72
- expect(KNOWN_GEMINI_MODELS).toContain('gemini-1.5-pro');
73
- expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(3);
76
+ expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(5);
77
+ });
78
+
79
+ it('should have known Grok models', () => {
80
+ expect(KNOWN_GROK_MODELS).toContain('grok-4-0709');
81
+ expect(KNOWN_GROK_MODELS).toContain('grok-3');
82
+ expect(KNOWN_GROK_MODELS).toContain('grok-3-mini');
83
+ expect(KNOWN_GROK_MODELS.length).toBeGreaterThanOrEqual(4);
74
84
  });
75
85
  });
76
86
 
@@ -84,7 +94,7 @@ describe('backward compatibility', () => {
84
94
  });
85
95
 
86
96
  it('should not auto-detect non-OpenAI models as known OpenAI', () => {
87
- const nonOpenAI = ['gemini-2.0-flash', 'grok-3'];
97
+ const nonOpenAI = ['gemini-2.5-flash', 'grok-3', 'grok-4-0709'];
88
98
  for (const model of nonOpenAI) {
89
99
  const isKnown = (KNOWN_OPENAI_MODELS as readonly string[]).includes(model);
90
100
  expect(isKnown).toBe(false);
@@ -23,7 +23,7 @@ describe('DEFAULT_CONFIG', () => {
23
23
  });
24
24
 
25
25
  it('should have valid API defaults', () => {
26
- expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-4o');
26
+ expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-4.1');
27
27
  expect(DEFAULT_CONFIG.apis.openai.temperature).toBe(0.3);
28
28
  expect(DEFAULT_CONFIG.apis.openai.max_tokens).toBe(4096);
29
29
  });
@@ -122,11 +122,11 @@ describe('ConfigSchema', () => {
122
122
  expect(result.success).toBe(false);
123
123
  });
124
124
 
125
- it('should reject invalid model', () => {
125
+ it('should reject empty model string', () => {
126
126
  const config = {
127
127
  apis: {
128
128
  openai: {
129
- model: 'gpt-5',
129
+ model: '',
130
130
  },
131
131
  },
132
132
  };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Fix C tests — readPopeyeMdConfig shared config reader.
3
+ * Verifies popeye.md parsing for CLI commands.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { readPopeyeMdConfig } from '../../src/config/popeye-md.js';
11
+
12
+ describe('Fix C: readPopeyeMdConfig', () => {
13
+ let tmpDir: string;
14
+
15
+ beforeEach(async () => {
16
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-md-test-'));
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it('should return null when popeye.md does not exist', async () => {
24
+ const config = await readPopeyeMdConfig(tmpDir);
25
+ expect(config).toBeNull();
26
+ });
27
+
28
+ it('should parse basic config with reviewer and language', async () => {
29
+ await fs.writeFile(
30
+ path.join(tmpDir, 'popeye.md'),
31
+ [
32
+ '---',
33
+ 'language: python',
34
+ 'reviewer: openai',
35
+ 'arbitrator: gemini',
36
+ '---',
37
+ '',
38
+ '# Project Config',
39
+ ].join('\n'),
40
+ );
41
+
42
+ const config = await readPopeyeMdConfig(tmpDir);
43
+ expect(config).not.toBeNull();
44
+ expect(config!.language).toBe('python');
45
+ expect(config!.reviewer).toBe('openai');
46
+ expect(config!.arbitrator).toBe('gemini');
47
+ expect(config!.enableArbitration).toBe(true);
48
+ });
49
+
50
+ it('should parse model fields from popeye.md', async () => {
51
+ await fs.writeFile(
52
+ path.join(tmpDir, 'popeye.md'),
53
+ [
54
+ '---',
55
+ 'language: typescript',
56
+ 'reviewer: gemini',
57
+ 'arbitrator: grok',
58
+ 'openaiModel: gpt-4o-mini',
59
+ 'geminiModel: gemini-2.0-flash',
60
+ 'grokModel: grok-3',
61
+ '---',
62
+ ].join('\n'),
63
+ );
64
+
65
+ const config = await readPopeyeMdConfig(tmpDir);
66
+ expect(config).not.toBeNull();
67
+ expect(config!.openaiModel).toBe('gpt-4o-mini');
68
+ expect(config!.geminiModel).toBe('gemini-2.0-flash');
69
+ expect(config!.grokModel).toBe('grok-3');
70
+ });
71
+
72
+ it('should handle arbitrator: off', async () => {
73
+ await fs.writeFile(
74
+ path.join(tmpDir, 'popeye.md'),
75
+ [
76
+ '---',
77
+ 'language: fullstack',
78
+ 'reviewer: openai',
79
+ 'arbitrator: off',
80
+ '---',
81
+ ].join('\n'),
82
+ );
83
+
84
+ const config = await readPopeyeMdConfig(tmpDir);
85
+ expect(config).not.toBeNull();
86
+ expect(config!.enableArbitration).toBe(false);
87
+ });
88
+
89
+ it('should return null when frontmatter is missing', async () => {
90
+ await fs.writeFile(
91
+ path.join(tmpDir, 'popeye.md'),
92
+ '# Just a markdown file\n\nNo frontmatter here.',
93
+ );
94
+
95
+ const config = await readPopeyeMdConfig(tmpDir);
96
+ expect(config).toBeNull();
97
+ });
98
+
99
+ it('should return null when essential fields are missing', async () => {
100
+ await fs.writeFile(
101
+ path.join(tmpDir, 'popeye.md'),
102
+ [
103
+ '---',
104
+ 'projectName: my-app',
105
+ 'created: 2024-01-01',
106
+ '---',
107
+ ].join('\n'),
108
+ );
109
+
110
+ const config = await readPopeyeMdConfig(tmpDir);
111
+ // Missing language and reviewer -> null
112
+ expect(config).toBeNull();
113
+ });
114
+
115
+ it('should extract notes section', async () => {
116
+ await fs.writeFile(
117
+ path.join(tmpDir, 'popeye.md'),
118
+ [
119
+ '---',
120
+ 'language: website',
121
+ 'reviewer: openai',
122
+ '---',
123
+ '',
124
+ '## Notes',
125
+ 'This project uses Tailwind CSS.',
126
+ 'Deploy to Vercel.',
127
+ ].join('\n'),
128
+ );
129
+
130
+ const config = await readPopeyeMdConfig(tmpDir);
131
+ expect(config).not.toBeNull();
132
+ expect(config!.notes).toContain('Tailwind CSS');
133
+ });
134
+
135
+ it('should reject invalid language values', async () => {
136
+ await fs.writeFile(
137
+ path.join(tmpDir, 'popeye.md'),
138
+ [
139
+ '---',
140
+ 'language: rust',
141
+ 'reviewer: openai',
142
+ '---',
143
+ ].join('\n'),
144
+ );
145
+
146
+ const config = await readPopeyeMdConfig(tmpDir);
147
+ // Invalid language means essential field is missing
148
+ expect(config).toBeNull();
149
+ });
150
+
151
+ it('should return model fields as undefined when not specified', async () => {
152
+ await fs.writeFile(
153
+ path.join(tmpDir, 'popeye.md'),
154
+ [
155
+ '---',
156
+ 'language: python',
157
+ 'reviewer: openai',
158
+ '---',
159
+ ].join('\n'),
160
+ );
161
+
162
+ const config = await readPopeyeMdConfig(tmpDir);
163
+ expect(config).not.toBeNull();
164
+ expect(config!.openaiModel).toBeUndefined();
165
+ expect(config!.geminiModel).toBeUndefined();
166
+ expect(config!.grokModel).toBeUndefined();
167
+ });
168
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Review Bridge tests — severity mapping, category mapping, CR routing,
3
+ * pipeline detection, finding conversion.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ mapSeverity,
9
+ mapCategory,
10
+ categoryToChangeType,
11
+ convertFinding,
12
+ isPipelineManaged,
13
+ extractPipelineState,
14
+ } from '../../../src/pipeline/bridges/review-bridge.js';
15
+ import { createDefaultPipelineState } from '../../../src/pipeline/types.js';
16
+ import type { AuditFinding as WorkflowFinding } from '../../../src/types/audit.js';
17
+ import type { ArtifactRef } from '../../../src/pipeline/types.js';
18
+ import type { ProjectState } from '../../../src/types/workflow.js';
19
+
20
+ // ─── Test Data ───────────────────────────────────────────
21
+
22
+ function makeSnapshotRef(): ArtifactRef {
23
+ return {
24
+ artifact_id: 'snap-001',
25
+ path: 'docs/snapshots/repo_snapshot_001.json',
26
+ sha256: 'abc123',
27
+ version: 1,
28
+ type: 'repo_snapshot',
29
+ };
30
+ }
31
+
32
+ function makeWorkflowFinding(overrides: Partial<WorkflowFinding> = {}): WorkflowFinding {
33
+ return {
34
+ id: 'finding-1',
35
+ category: 'integration-wiring',
36
+ severity: 'critical',
37
+ title: 'API endpoint mismatch',
38
+ description: 'Frontend calls /api/users but backend serves /api/v1/users',
39
+ evidence: [{ file: 'src/api.ts', line: 42, snippet: 'fetch("/api/users")' }],
40
+ recommendation: 'Update frontend API base URL',
41
+ autoFixable: true,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ function makeProjectState(withPipeline = false): ProjectState {
47
+ const base: ProjectState = {
48
+ id: 'test-id',
49
+ name: 'test-project',
50
+ idea: 'Build something',
51
+ language: 'python',
52
+ phase: 'execution',
53
+ status: 'in-progress',
54
+ milestones: [],
55
+ currentMilestone: null,
56
+ currentTask: null,
57
+ consensusHistory: [],
58
+ createdAt: new Date().toISOString(),
59
+ updatedAt: new Date().toISOString(),
60
+ } as ProjectState;
61
+
62
+ if (withPipeline) {
63
+ (base as unknown as { pipeline: unknown }).pipeline = createDefaultPipelineState();
64
+ }
65
+
66
+ return base;
67
+ }
68
+
69
+ // ─── Tests ───────────────────────────────────────────────
70
+
71
+ describe('Review Bridge', () => {
72
+ describe('severity mapping', () => {
73
+ it('should map critical to P0', () => {
74
+ expect(mapSeverity('critical')).toBe('P0');
75
+ });
76
+
77
+ it('should map major to P1', () => {
78
+ expect(mapSeverity('major')).toBe('P1');
79
+ });
80
+
81
+ it('should map minor to P2', () => {
82
+ expect(mapSeverity('minor')).toBe('P2');
83
+ });
84
+
85
+ it('should map info to P3', () => {
86
+ expect(mapSeverity('info')).toBe('P3');
87
+ });
88
+ });
89
+
90
+ describe('category mapping', () => {
91
+ it('should map integration-wiring to integration', () => {
92
+ expect(mapCategory('integration-wiring')).toBe('integration');
93
+ });
94
+
95
+ it('should map feature-completeness to integration', () => {
96
+ expect(mapCategory('feature-completeness')).toBe('integration');
97
+ });
98
+
99
+ it('should map test-coverage to tests', () => {
100
+ expect(mapCategory('test-coverage')).toBe('tests');
101
+ });
102
+
103
+ it('should map config-deployment to config', () => {
104
+ expect(mapCategory('config-deployment')).toBe('config');
105
+ });
106
+
107
+ it('should map security to security', () => {
108
+ expect(mapCategory('security')).toBe('security');
109
+ });
110
+
111
+ it('should map dependency-sanity to deployment', () => {
112
+ expect(mapCategory('dependency-sanity')).toBe('deployment');
113
+ });
114
+
115
+ it('should map consistency to schema', () => {
116
+ expect(mapCategory('consistency')).toBe('schema');
117
+ });
118
+
119
+ it('should map documentation to deployment', () => {
120
+ expect(mapCategory('documentation')).toBe('deployment');
121
+ });
122
+ });
123
+
124
+ describe('CR change type routing', () => {
125
+ it('should route integration to architecture CR', () => {
126
+ expect(categoryToChangeType('integration')).toBe('architecture');
127
+ });
128
+
129
+ it('should route schema to architecture CR', () => {
130
+ expect(categoryToChangeType('schema')).toBe('architecture');
131
+ });
132
+
133
+ it('should route security to requirement CR', () => {
134
+ expect(categoryToChangeType('security')).toBe('requirement');
135
+ });
136
+
137
+ it('should route tests to config CR', () => {
138
+ expect(categoryToChangeType('tests')).toBe('config');
139
+ });
140
+
141
+ it('should route config to config CR', () => {
142
+ expect(categoryToChangeType('config')).toBe('config');
143
+ });
144
+
145
+ it('should route deployment to config CR', () => {
146
+ expect(categoryToChangeType('deployment')).toBe('config');
147
+ });
148
+ });
149
+
150
+ describe('finding conversion', () => {
151
+ it('should convert a critical workflow finding to P0 pipeline finding', () => {
152
+ const wf = makeWorkflowFinding({ severity: 'critical', category: 'integration-wiring' });
153
+ const ref = makeSnapshotRef();
154
+
155
+ const pf = convertFinding(wf, ref);
156
+
157
+ expect(pf.id).toBe('finding-1');
158
+ expect(pf.severity).toBe('P0');
159
+ expect(pf.category).toBe('integration');
160
+ expect(pf.blocking).toBe(true);
161
+ expect(pf.description).toContain('API endpoint mismatch');
162
+ expect(pf.file_path).toBe('src/api.ts');
163
+ expect(pf.line_number).toBe(42);
164
+ expect(pf.evidence).toEqual([ref]);
165
+ expect(pf.suggested_owner).toBe('AUDITOR');
166
+ });
167
+
168
+ it('should convert an info finding to P3 non-blocking', () => {
169
+ const wf = makeWorkflowFinding({ severity: 'info', category: 'documentation' });
170
+ const ref = makeSnapshotRef();
171
+
172
+ const pf = convertFinding(wf, ref);
173
+
174
+ expect(pf.severity).toBe('P3');
175
+ expect(pf.blocking).toBe(false);
176
+ });
177
+
178
+ it('should mark P0 and P1 as blocking, P2 and P3 as non-blocking', () => {
179
+ const ref = makeSnapshotRef();
180
+
181
+ expect(convertFinding(makeWorkflowFinding({ severity: 'critical' }), ref).blocking).toBe(true);
182
+ expect(convertFinding(makeWorkflowFinding({ severity: 'major' }), ref).blocking).toBe(true);
183
+ expect(convertFinding(makeWorkflowFinding({ severity: 'minor' }), ref).blocking).toBe(false);
184
+ expect(convertFinding(makeWorkflowFinding({ severity: 'info' }), ref).blocking).toBe(false);
185
+ });
186
+
187
+ it('should handle finding with no evidence', () => {
188
+ const wf = makeWorkflowFinding({ evidence: [] });
189
+ const ref = makeSnapshotRef();
190
+
191
+ const pf = convertFinding(wf, ref);
192
+
193
+ expect(pf.file_path).toBeUndefined();
194
+ expect(pf.line_number).toBeUndefined();
195
+ });
196
+ });
197
+
198
+ describe('pipeline detection', () => {
199
+ it('should detect pipeline-managed state', () => {
200
+ const state = makeProjectState(true);
201
+ expect(isPipelineManaged(state)).toBe(true);
202
+ });
203
+
204
+ it('should detect non-pipeline state', () => {
205
+ const state = makeProjectState(false);
206
+ expect(isPipelineManaged(state)).toBe(false);
207
+ });
208
+
209
+ it('should extract pipeline state when present', () => {
210
+ const state = makeProjectState(true);
211
+ const pipeline = extractPipelineState(state);
212
+
213
+ expect(pipeline).toBeDefined();
214
+ expect(pipeline!.pipelinePhase).toBe('INTAKE');
215
+ });
216
+
217
+ it('should return undefined when no pipeline state', () => {
218
+ const state = makeProjectState(false);
219
+ expect(extractPipelineState(state)).toBeUndefined();
220
+ });
221
+ });
222
+
223
+ describe('CR routing determinism', () => {
224
+ it('should route integration findings to CONSENSUS_ARCHITECTURE', () => {
225
+ // Integration → architecture CR → CONSENSUS_ARCHITECTURE (via change-request.ts routing)
226
+ const changeType = categoryToChangeType('integration');
227
+ expect(changeType).toBe('architecture');
228
+ // architecture routes to CONSENSUS_ARCHITECTURE per CHANGE_TYPE_ROUTING
229
+ });
230
+
231
+ it('should route security findings to CONSENSUS_MASTER_PLAN', () => {
232
+ const changeType = categoryToChangeType('security');
233
+ expect(changeType).toBe('requirement');
234
+ // requirement routes to CONSENSUS_MASTER_PLAN per CHANGE_TYPE_ROUTING
235
+ });
236
+
237
+ it('should route test findings to QA_VALIDATION', () => {
238
+ const changeType = categoryToChangeType('tests');
239
+ expect(changeType).toBe('config');
240
+ // config routes to QA_VALIDATION per CHANGE_TYPE_ROUTING
241
+ });
242
+ });
243
+ });