popeye-cli 1.5.0 → 1.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.
Files changed (195) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +184 -31
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +11 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/consensus.d.ts +3 -0
  82. package/dist/types/consensus.d.ts.map +1 -1
  83. package/dist/types/consensus.js +1 -0
  84. package/dist/types/consensus.js.map +1 -1
  85. package/dist/types/index.d.ts +1 -0
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/dist/types/index.js +2 -0
  88. package/dist/types/index.js.map +1 -1
  89. package/dist/types/tester.d.ts +138 -0
  90. package/dist/types/tester.d.ts.map +1 -0
  91. package/dist/types/tester.js +110 -0
  92. package/dist/types/tester.js.map +1 -0
  93. package/dist/types/workflow.d.ts +151 -0
  94. package/dist/types/workflow.d.ts.map +1 -1
  95. package/dist/types/workflow.js +14 -0
  96. package/dist/types/workflow.js.map +1 -1
  97. package/dist/upgrade/handlers.d.ts +15 -0
  98. package/dist/upgrade/handlers.d.ts.map +1 -1
  99. package/dist/upgrade/handlers.js +52 -0
  100. package/dist/upgrade/handlers.js.map +1 -1
  101. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  102. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  103. package/dist/workflow/auto-fix-bundler.js +320 -0
  104. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  105. package/dist/workflow/auto-fix.d.ts.map +1 -1
  106. package/dist/workflow/auto-fix.js +10 -3
  107. package/dist/workflow/auto-fix.js.map +1 -1
  108. package/dist/workflow/execution-mode.js +2 -2
  109. package/dist/workflow/execution-mode.js.map +1 -1
  110. package/dist/workflow/index.d.ts +2 -0
  111. package/dist/workflow/index.d.ts.map +1 -1
  112. package/dist/workflow/index.js +13 -0
  113. package/dist/workflow/index.js.map +1 -1
  114. package/dist/workflow/overview.d.ts.map +1 -1
  115. package/dist/workflow/overview.js +4 -0
  116. package/dist/workflow/overview.js.map +1 -1
  117. package/dist/workflow/plan-mode.d.ts +4 -3
  118. package/dist/workflow/plan-mode.d.ts.map +1 -1
  119. package/dist/workflow/plan-mode.js +69 -5
  120. package/dist/workflow/plan-mode.js.map +1 -1
  121. package/dist/workflow/task-workflow.d.ts +5 -0
  122. package/dist/workflow/task-workflow.d.ts.map +1 -1
  123. package/dist/workflow/task-workflow.js +172 -6
  124. package/dist/workflow/task-workflow.js.map +1 -1
  125. package/dist/workflow/tester.d.ts +120 -0
  126. package/dist/workflow/tester.d.ts.map +1 -0
  127. package/dist/workflow/tester.js +589 -0
  128. package/dist/workflow/tester.js.map +1 -0
  129. package/dist/workflow/website-strategy.d.ts +9 -0
  130. package/dist/workflow/website-strategy.d.ts.map +1 -1
  131. package/dist/workflow/website-strategy.js +73 -1
  132. package/dist/workflow/website-strategy.js.map +1 -1
  133. package/dist/workflow/website-updater.d.ts.map +1 -1
  134. package/dist/workflow/website-updater.js +15 -4
  135. package/dist/workflow/website-updater.js.map +1 -1
  136. package/dist/workflow/workflow-logger.d.ts +1 -1
  137. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  138. package/dist/workflow/workflow-logger.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/cli/commands/create.ts +58 -4
  141. package/src/cli/interactive.ts +96 -7
  142. package/src/generators/all.ts +44 -332
  143. package/src/generators/doc-parser.ts +87 -10
  144. package/src/generators/frontend-design-analyzer.ts +261 -0
  145. package/src/generators/shared-packages.ts +500 -0
  146. package/src/generators/templates/index.ts +4 -0
  147. package/src/generators/templates/website-components.ts +36 -11
  148. package/src/generators/templates/website-config.ts +166 -13
  149. package/src/generators/templates/website-landing.ts +331 -0
  150. package/src/generators/templates/website-layout.ts +443 -0
  151. package/src/generators/templates/website-pricing.ts +330 -0
  152. package/src/generators/templates/website-sections.ts +541 -0
  153. package/src/generators/templates/website.ts +38 -851
  154. package/src/generators/website-content-scanner.ts +208 -0
  155. package/src/generators/website-context.ts +248 -20
  156. package/src/generators/website-debug.ts +130 -0
  157. package/src/generators/website.ts +71 -3
  158. package/src/generators/workspace-root.ts +113 -0
  159. package/src/state/index.ts +15 -0
  160. package/src/types/consensus.ts +3 -0
  161. package/src/types/index.ts +21 -0
  162. package/src/types/tester.ts +136 -0
  163. package/src/types/workflow.ts +32 -0
  164. package/src/upgrade/handlers.ts +65 -0
  165. package/src/workflow/auto-fix-bundler.ts +392 -0
  166. package/src/workflow/auto-fix.ts +11 -3
  167. package/src/workflow/execution-mode.ts +2 -2
  168. package/src/workflow/index.ts +13 -0
  169. package/src/workflow/overview.ts +6 -0
  170. package/src/workflow/plan-mode.ts +81 -7
  171. package/src/workflow/task-workflow.ts +227 -5
  172. package/src/workflow/tester.ts +723 -0
  173. package/src/workflow/website-strategy.ts +75 -1
  174. package/src/workflow/website-updater.ts +17 -6
  175. package/src/workflow/workflow-logger.ts +2 -0
  176. package/tests/cli/project-naming.test.ts +136 -0
  177. package/tests/generators/doc-parser.test.ts +121 -0
  178. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  179. package/tests/generators/quality-gate.test.ts +183 -0
  180. package/tests/generators/shared-packages.test.ts +83 -0
  181. package/tests/generators/website-components.test.ts +1 -1
  182. package/tests/generators/website-config.test.ts +84 -0
  183. package/tests/generators/website-content-scanner.test.ts +181 -0
  184. package/tests/generators/website-context.test.ts +109 -0
  185. package/tests/generators/website-debug.test.ts +77 -0
  186. package/tests/generators/website-landing.test.ts +188 -0
  187. package/tests/generators/website-pricing.test.ts +98 -0
  188. package/tests/generators/website-sections.test.ts +245 -0
  189. package/tests/generators/workspace-root.test.ts +105 -0
  190. package/tests/types/tester.test.ts +174 -0
  191. package/tests/upgrade/handlers.test.ts +162 -0
  192. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  193. package/tests/workflow/plan-mode.test.ts +111 -1
  194. package/tests/workflow/tester.test.ts +401 -0
  195. package/tests/workflow/website-strategy.test.ts +55 -0
@@ -48,6 +48,10 @@ import {
48
48
  generateLeadCaptureEnvExample,
49
49
  } from './templates/website-conversion.js';
50
50
  import type { WebsiteContentContext } from './website-context.js';
51
+ import { validateWebsiteContextOrThrow } from './website-context.js';
52
+ import { scanGeneratedContent } from './website-content-scanner.js';
53
+ import { printDebugTrace, isDebugEnabled } from './website-debug.js';
54
+ import type { WebsiteDebugTrace } from './website-debug.js';
51
55
 
52
56
  /**
53
57
  * Project generation result
@@ -75,6 +79,8 @@ export interface WebsiteGeneratorOptions {
75
79
  skipReadme?: boolean;
76
80
  /** Content context from user docs for populating templates */
77
81
  contentContext?: WebsiteContentContext;
82
+ /** Skip content validation (scaffold-only use) */
83
+ skipValidation?: boolean;
78
84
  }
79
85
 
80
86
  /**
@@ -111,6 +117,7 @@ export async function generateWebsiteProject(
111
117
  skipDocker = false,
112
118
  skipReadme = false,
113
119
  contentContext,
120
+ skipValidation = false,
114
121
  } = options;
115
122
 
116
123
  const projectName = spec.name || 'my-project';
@@ -140,6 +147,51 @@ export async function generateWebsiteProject(
140
147
  await ensureDir(path.join(projectDir, '.popeye'));
141
148
  }
142
149
 
150
+ // Validate content context quality gate
151
+ if (!skipValidation) {
152
+ const validationContext = contentContext || {
153
+ productName: projectName,
154
+ features: [],
155
+ rawDocs: '',
156
+ };
157
+ validateWebsiteContextOrThrow(validationContext, projectName);
158
+ }
159
+
160
+ // Debug trace
161
+ if (isDebugEnabled() && contentContext) {
162
+ const trace: WebsiteDebugTrace = {
163
+ workspaceRoot: projectDir,
164
+ docsFound: contentContext.rawDocs
165
+ ? contentContext.rawDocs.split(/^--- .+ ---$/m).filter(Boolean).map((s, i) => ({
166
+ path: `doc-${i}`,
167
+ size: s.length,
168
+ }))
169
+ : [],
170
+ brandAssets: {
171
+ logoPath: contentContext.brand?.logoPath,
172
+ logoOutputPath: contentContext.brandAssets?.logoOutputPath || 'public/brand/logo.svg',
173
+ },
174
+ productName: {
175
+ value: contentContext.productName,
176
+ source: contentContext.rawDocs ? 'docs' : 'directory',
177
+ },
178
+ primaryColor: {
179
+ value: contentContext.brand?.primaryColor,
180
+ source: contentContext.brand?.primaryColor ? 'brand-docs' : 'defaults',
181
+ },
182
+ strategyStatus: contentContext.strategy ? 'success' : 'skipped',
183
+ templateValues: {
184
+ headline: contentContext.strategy?.messaging.headline,
185
+ features: contentContext.features.length,
186
+ pricingTiers: contentContext.pricing?.length || 0,
187
+ },
188
+ sectionsRendered: [],
189
+ validationPassed: true,
190
+ validationIssues: [],
191
+ };
192
+ printDebugTrace(trace);
193
+ }
194
+
143
195
  // Generate and write files
144
196
  const files: Array<{ path: string; content: string }> = [
145
197
  // Config files
@@ -157,7 +209,11 @@ export async function generateWebsiteProject(
157
209
  },
158
210
  {
159
211
  path: path.join(projectDir, 'tailwind.config.ts'),
160
- content: generateWebsiteTailwindConfig(),
212
+ content: generateWebsiteTailwindConfig({
213
+ primaryColor: contentContext?.brand?.primaryColor,
214
+ workspaceMode,
215
+ projectName: workspaceMode ? projectName : undefined,
216
+ }),
161
217
  },
162
218
  {
163
219
  path: path.join(projectDir, 'postcss.config.js'),
@@ -313,11 +369,11 @@ export async function generateWebsiteProject(
313
369
  });
314
370
  }
315
371
 
316
- // Copy logo to public/ if brand context has one
372
+ // Copy logo to public/brand/ if brand context has one
317
373
  if (contentContext?.brand?.logoPath) {
318
374
  try {
319
375
  const logoExt = path.extname(contentContext.brand.logoPath);
320
- const destPath = path.join(projectDir, 'public', `logo${logoExt}`);
376
+ const destPath = path.join(projectDir, 'public', 'brand', `logo${logoExt}`);
321
377
  await fs.copyFile(contentContext.brand.logoPath, destPath);
322
378
  filesCreated.push(destPath);
323
379
  } catch {
@@ -347,6 +403,18 @@ export async function generateWebsiteProject(
347
403
  filesCreated.push(file.path);
348
404
  }
349
405
 
406
+ // Post-generation content scan for placeholder fingerprints
407
+ try {
408
+ const scanResult = await scanGeneratedContent(projectDir);
409
+ if (scanResult.issues.length > 0) {
410
+ for (const issue of scanResult.issues) {
411
+ console.warn(`[content-scan] ${issue.severity}: ${issue.message} in ${issue.file}`);
412
+ }
413
+ }
414
+ } catch {
415
+ // Non-blocking: scan failures should not stop generation
416
+ }
417
+
350
418
  return {
351
419
  success: true,
352
420
  projectDir,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Workspace root detection
3
+ * Resolves the workspace root directory by walking up the directory tree,
4
+ * looking for Popeye config, monorepo indicators, or package.json with workspaces
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ /**
11
+ * Resolve the workspace root directory from a given working directory
12
+ *
13
+ * Heuristic priority:
14
+ * 1. Walk ancestors: first dir containing `.popeye/` -> workspace root
15
+ * 2. First dir containing `package.json` with "workspaces" field
16
+ * 3. First dir containing `pnpm-workspace.yaml` or `turbo.json`
17
+ * 4. `cwd` (fallback)
18
+ *
19
+ * @param cwd - The current working directory
20
+ * @returns The resolved workspace root path
21
+ */
22
+ export async function resolveWorkspaceRoot(cwd: string): Promise<string> {
23
+ let current = path.resolve(cwd);
24
+ const root = path.parse(current).root;
25
+
26
+ while (current !== root) {
27
+ // Check for .popeye/ directory
28
+ if (await dirExists(path.join(current, '.popeye'))) {
29
+ return current;
30
+ }
31
+
32
+ // Check for package.json with "workspaces" field
33
+ const pkgJsonPath = path.join(current, 'package.json');
34
+ if (await fileExists(pkgJsonPath)) {
35
+ try {
36
+ const content = await fs.readFile(pkgJsonPath, 'utf-8');
37
+ const pkg = JSON.parse(content);
38
+ if (pkg.workspaces) {
39
+ return current;
40
+ }
41
+ } catch {
42
+ // Invalid JSON, skip
43
+ }
44
+ }
45
+
46
+ // Check for pnpm-workspace.yaml or turbo.json
47
+ if (
48
+ (await fileExists(path.join(current, 'pnpm-workspace.yaml'))) ||
49
+ (await fileExists(path.join(current, 'turbo.json')))
50
+ ) {
51
+ return current;
52
+ }
53
+
54
+ current = path.dirname(current);
55
+ }
56
+
57
+ return cwd;
58
+ }
59
+
60
+ /**
61
+ * Build a list of directories to scan for docs and brand assets
62
+ * Includes workspace root, its parent, and relevant subdirectories
63
+ *
64
+ * @param cwd - The current working directory
65
+ * @returns Array of directories to scan (deduplicated)
66
+ */
67
+ export async function getScanDirectories(cwd: string): Promise<string[]> {
68
+ const workspaceRoot = await resolveWorkspaceRoot(cwd);
69
+ const parentDir = path.dirname(workspaceRoot);
70
+
71
+ const candidates = [workspaceRoot, parentDir];
72
+ const subdirs = ['docs', 'brand', 'assets'];
73
+
74
+ for (const base of [workspaceRoot, parentDir]) {
75
+ for (const sub of subdirs) {
76
+ candidates.push(path.join(base, sub));
77
+ }
78
+ }
79
+
80
+ // Deduplicate by resolved path and filter to existing directories
81
+ const seen = new Set<string>();
82
+ const result: string[] = [];
83
+
84
+ for (const dir of candidates) {
85
+ const resolved = path.resolve(dir);
86
+ if (seen.has(resolved)) continue;
87
+ seen.add(resolved);
88
+
89
+ if (await dirExists(resolved)) {
90
+ result.push(resolved);
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ async function fileExists(filePath: string): Promise<boolean> {
98
+ try {
99
+ const stat = await fs.stat(filePath);
100
+ return stat.isFile();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ async function dirExists(dirPath: string): Promise<boolean> {
107
+ try {
108
+ const stat = await fs.stat(dirPath);
109
+ return stat.isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -60,6 +60,7 @@ export async function createProject(
60
60
  consensusHistory: [],
61
61
  createdAt: now,
62
62
  updatedAt: now,
63
+ qaEnabled: true,
63
64
  };
64
65
 
65
66
  await saveState(projectDir, state);
@@ -352,6 +353,20 @@ export async function storeUserDocs(
352
353
  return updateState(projectDir, { userDocs });
353
354
  }
354
355
 
356
+ /**
357
+ * Store discovered source document paths in project state
358
+ *
359
+ * @param projectDir - The project root directory
360
+ * @param sourceDocPaths - Array of absolute paths to doc files
361
+ * @returns The updated state
362
+ */
363
+ export async function storeSourceDocPaths(
364
+ projectDir: string,
365
+ sourceDocPaths: string[]
366
+ ): Promise<ProjectState> {
367
+ return updateState(projectDir, { sourceDocPaths });
368
+ }
369
+
355
370
  /**
356
371
  * Store brand context in project state
357
372
  *
@@ -91,6 +91,8 @@ export interface ConsensusConfig {
91
91
  additionalReviewers?: AIProvider[];
92
92
  /** Custom reviewer persona for domain-specific reviews (e.g., marketing strategist for website projects) */
93
93
  reviewerPersona?: string;
94
+ /** Consensus threshold for test plans (default: 90, lower than code plan threshold) */
95
+ testPlanThreshold?: number;
94
96
  }
95
97
 
96
98
  /**
@@ -153,6 +155,7 @@ export const ConsensusConfigSchema = z.object({
153
155
  temperature: z.number().min(0).max(2).default(0.3),
154
156
  maxTokens: z.number().min(100).max(32000).default(4096),
155
157
  reviewerPersona: z.string().optional(),
158
+ testPlanThreshold: z.number().min(0).max(100).optional(),
156
159
  });
157
160
 
158
161
  /**
@@ -80,6 +80,27 @@ export {
80
80
  type ConsensusTrackingRecord,
81
81
  } from './consensus.js';
82
82
 
83
+ // Tester (QA) types
84
+ export {
85
+ TestVerdictSchema,
86
+ TestScopeSchema,
87
+ TestCommandSchema,
88
+ TestCaseSchema,
89
+ TestPlanOutputSchema,
90
+ TestRunReviewSchema,
91
+ FixStepSchema,
92
+ TestFixPlanSchema,
93
+ type TestVerdict,
94
+ type TestScope,
95
+ type TestCommand,
96
+ type TestCase,
97
+ type TestPlanOutput,
98
+ type TestRunReview,
99
+ type FixStep,
100
+ type TestFixPlan,
101
+ type DiscoveredTestCommands,
102
+ } from './tester.js';
103
+
83
104
  // CLI types
84
105
  export {
85
106
  EXIT_CODES,
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Tester (QA) skill type definitions
3
+ * Defines test planning, review, and fix plan structures
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Test verdict from the Tester's review
10
+ */
11
+ export const TestVerdictSchema = z.enum(['PASS', 'PASS_WITH_NOTES', 'FAIL']);
12
+ export type TestVerdict = z.infer<typeof TestVerdictSchema>;
13
+
14
+ /**
15
+ * Scope components that a test plan can cover
16
+ */
17
+ export const TestScopeSchema = z.enum(['frontend', 'backend', 'db', 'infra']);
18
+ export type TestScope = z.infer<typeof TestScopeSchema>;
19
+
20
+ /**
21
+ * A structured test command to execute
22
+ */
23
+ export const TestCommandSchema = z.object({
24
+ /** The shell command to run */
25
+ command: z.string().min(1),
26
+ /** Working directory (relative to project root) */
27
+ cwd: z.string().optional(),
28
+ /** Human-readable purpose of this command */
29
+ purpose: z.string().min(1),
30
+ /** Whether this command must pass for the test run to succeed */
31
+ required: z.boolean(),
32
+ });
33
+ export type TestCommand = z.infer<typeof TestCommandSchema>;
34
+
35
+ /**
36
+ * Individual test case in the test matrix
37
+ */
38
+ export const TestCaseSchema = z.object({
39
+ /** Unique identifier within the test plan */
40
+ id: z.string().min(1),
41
+ /** Category: unit, integration, e2e, smoke, lint, build */
42
+ category: z.string().min(1),
43
+ /** Human-readable description of what is being tested */
44
+ description: z.string().min(1),
45
+ /** What must be true for this test to pass */
46
+ acceptanceCriteria: z.string().min(1),
47
+ /** What evidence (log output, report) is needed to verify */
48
+ evidenceRequired: z.string().min(1),
49
+ /** Priority: critical, high, medium, low */
50
+ priority: z.enum(['critical', 'high', 'medium', 'low']),
51
+ });
52
+ export type TestCase = z.infer<typeof TestCaseSchema>;
53
+
54
+ /**
55
+ * Structured test plan output from the Tester
56
+ */
57
+ export const TestPlanOutputSchema = z.object({
58
+ /** What risks this test plan targets */
59
+ summary: z.string().min(1),
60
+ /** Components covered by this plan */
61
+ scope: z.array(TestScopeSchema).min(1),
62
+ /** Matrix of test cases with acceptance criteria */
63
+ testMatrix: z.array(TestCaseSchema).min(1),
64
+ /** Exact commands to execute (with cwd, purpose, required flag) */
65
+ commands: z.array(TestCommandSchema).min(1),
66
+ /** Top risks this test plan focuses on (3-7 items) */
67
+ riskFocus: z.array(z.string().min(1)).min(1),
68
+ /** What evidence (logs, reports) to capture */
69
+ evidenceRequired: z.array(z.string().min(1)).min(1),
70
+ /** Minimum verification always present: build, lint, smoke */
71
+ minimumVerification: z.array(z.string().min(1)).min(1),
72
+ /** Rationale if tester decides no custom tests are needed (min verification still applies) */
73
+ noTestsRationale: z.string().optional(),
74
+ });
75
+ export type TestPlanOutput = z.infer<typeof TestPlanOutputSchema>;
76
+
77
+ /**
78
+ * Post-run review from the Tester
79
+ */
80
+ export const TestRunReviewSchema = z.object({
81
+ /** Overall verdict */
82
+ verdict: TestVerdictSchema,
83
+ /** Summary of the review */
84
+ summary: z.string().min(1),
85
+ /** List of evidence that was checked */
86
+ evidenceReviewed: z.array(z.string().min(1)).min(1),
87
+ /** Specific failures found (empty array if PASS) */
88
+ failures: z.array(z.string()),
89
+ /** Missing evidence or coverage gaps */
90
+ gaps: z.array(z.string()),
91
+ /** Recommendations for improvement */
92
+ recommendations: z.array(z.string()),
93
+ /** Whether this verdict requires consensus (true if FAIL) */
94
+ requiresConsensus: z.boolean(),
95
+ });
96
+ export type TestRunReview = z.infer<typeof TestRunReviewSchema>;
97
+
98
+ /**
99
+ * Individual fix step in a TestFixPlan
100
+ */
101
+ export const FixStepSchema = z.object({
102
+ /** File to modify */
103
+ file: z.string().min(1),
104
+ /** Description of the change */
105
+ change: z.string().min(1),
106
+ /** Why this change is needed */
107
+ reason: z.string().min(1),
108
+ });
109
+ export type FixStep = z.infer<typeof FixStepSchema>;
110
+
111
+ /**
112
+ * Fix plan proposed by the Tester when tests fail
113
+ */
114
+ export const TestFixPlanSchema = z.object({
115
+ /** Which acceptance criteria failed */
116
+ failedCriteria: z.array(z.string().min(1)).min(1),
117
+ /** Root cause analysis from the Tester */
118
+ rootCauseAnalysis: z.string().min(1),
119
+ /** Ordered steps to fix the failures */
120
+ fixSteps: z.array(FixStepSchema).min(1),
121
+ /** Risks of introducing regressions */
122
+ regressionRisks: z.array(z.string()),
123
+ /** Strategy for re-testing after fix */
124
+ retestStrategy: z.string().min(1),
125
+ });
126
+ export type TestFixPlan = z.infer<typeof TestFixPlanSchema>;
127
+
128
+ /**
129
+ * Discovered test infrastructure for a project
130
+ */
131
+ export interface DiscoveredTestCommands {
132
+ testCmd: string | null;
133
+ lintCmd: string | null;
134
+ buildCmd: string | null;
135
+ typecheckCmd: string | null;
136
+ }
@@ -7,6 +7,8 @@ import { z } from 'zod';
7
7
  import { OutputLanguageSchema } from './project.js';
8
8
  import type { OutputLanguage, OpenAIModel } from './project.js';
9
9
  import type { ConsensusIteration } from './consensus.js';
10
+ import type { TestPlanOutput } from './tester.js';
11
+ import { TestPlanOutputSchema, TestVerdictSchema } from './tester.js';
10
12
 
11
13
  /**
12
14
  * Workflow phases
@@ -68,6 +70,17 @@ export interface Task {
68
70
 
69
71
  // App target (which app this task affects)
70
72
  appTarget?: 'frontend' | 'backend' | 'unified';
73
+
74
+ // Tester (QA) tracking
75
+ qaTestPlanText?: string; // Approved test plan (markdown, for humans)
76
+ qaTestPlanParsed?: TestPlanOutput; // Parsed structured plan (for machine flow)
77
+ qaTestPlanScore?: number; // Consensus score (0-100)
78
+ qaTestPlanIterations?: number; // Iterations to reach consensus
79
+ qaTestPlanApproved?: boolean; // Whether consensus was reached
80
+ qaTestPlanDoc?: string; // Path to docs/qa/test-plans/...
81
+ qaVerdict?: 'PASS' | 'PASS_WITH_NOTES' | 'FAIL';
82
+ qaReviewNotes?: string; // Tester's review notes
83
+ qaReviewDoc?: string; // Path to docs/qa/test-runs/...
71
84
  }
72
85
 
73
86
  /**
@@ -107,6 +120,16 @@ export const TaskSchema = z.object({
107
120
  backendConsensus: AppConsensusTrackingSchema.optional(),
108
121
  unifiedConsensus: AppConsensusTrackingSchema.optional(),
109
122
  appTarget: z.enum(['frontend', 'backend', 'unified']).optional(),
123
+ // Tester (QA) tracking
124
+ qaTestPlanText: z.string().optional(),
125
+ qaTestPlanParsed: TestPlanOutputSchema.optional(),
126
+ qaTestPlanScore: z.number().optional(),
127
+ qaTestPlanIterations: z.number().optional(),
128
+ qaTestPlanApproved: z.boolean().optional(),
129
+ qaTestPlanDoc: z.string().optional(),
130
+ qaVerdict: TestVerdictSchema.optional(),
131
+ qaReviewNotes: z.string().optional(),
132
+ qaReviewDoc: z.string().optional(),
110
133
  });
111
134
 
112
135
  /**
@@ -201,6 +224,12 @@ export interface ProjectState {
201
224
  };
202
225
  /** Path to website strategy JSON file (relative to .popeye/) */
203
226
  websiteStrategy?: string;
227
+ /** Error message from website strategy generation (for visibility) */
228
+ strategyError?: string;
229
+ /** Absolute paths to discovered source documentation files */
230
+ sourceDocPaths?: string[];
231
+ /** Whether QA Tester skill is active (default: true for new projects, undefined/false for existing) */
232
+ qaEnabled?: boolean;
204
233
  }
205
234
 
206
235
  /**
@@ -244,6 +273,9 @@ export const ProjectStateSchema = z.object({
244
273
  primaryColor: z.string().optional(),
245
274
  }).optional(),
246
275
  websiteStrategy: z.string().optional(),
276
+ strategyError: z.string().optional(),
277
+ sourceDocPaths: z.array(z.string()).optional(),
278
+ qaEnabled: z.boolean().optional(),
247
279
  });
248
280
 
249
281
  /**
@@ -21,6 +21,10 @@ import {
21
21
  generateRootDockerCompose,
22
22
  } from '../generators/templates/fullstack.js';
23
23
  import { loadState, saveState } from '../state/persistence.js';
24
+ import { buildWebsiteContext, resolveBrandAssets, validateWebsiteContext } from '../generators/website-context.js';
25
+ import type { WebsiteContentContext } from '../generators/website-context.js';
26
+ import { resolveWorkspaceRoot } from '../generators/workspace-root.js';
27
+ import { loadWebsiteStrategy } from '../workflow/website-strategy.js';
24
28
  import type { UpgradeResult } from './index.js';
25
29
 
26
30
  /**
@@ -42,6 +46,57 @@ async function pathExists(filePath: string): Promise<boolean> {
42
46
  }
43
47
  }
44
48
 
49
+ /**
50
+ * Build website content context from user docs, brand assets, and strategy
51
+ *
52
+ * Replicates the context-building pattern from website-updater.ts so that
53
+ * upgrade-generated websites get real content instead of TODO placeholders.
54
+ *
55
+ * @param projectDir - Project directory (workspace root)
56
+ * @param projectName - Project name
57
+ * @returns Content context and optional warning
58
+ */
59
+ export async function buildUpgradeContentContext(
60
+ projectDir: string,
61
+ projectName: string,
62
+ ): Promise<{ context?: WebsiteContentContext; warning?: string }> {
63
+ try {
64
+ // Build context from user docs (scans projectDir + parent via getScanDirectories)
65
+ const context = await buildWebsiteContext(projectDir, projectName);
66
+
67
+ // Apply brand context from state if available
68
+ const state = await loadState(projectDir);
69
+ if (state?.brandContext?.primaryColor) {
70
+ context.brand = { ...context.brand, primaryColor: state.brandContext.primaryColor };
71
+ }
72
+ if (state?.brandContext?.logoPath) {
73
+ context.brand = { ...context.brand, logoPath: state.brandContext.logoPath };
74
+ }
75
+
76
+ // Resolve brand assets using workspace root
77
+ const workspaceRoot = await resolveWorkspaceRoot(projectDir);
78
+ context.brandAssets = await resolveBrandAssets(workspaceRoot, context.brand);
79
+
80
+ // Load website strategy if available
81
+ const strategyData = await loadWebsiteStrategy(projectDir);
82
+ if (strategyData) {
83
+ context.strategy = strategyData.strategy;
84
+ }
85
+
86
+ // Soft validation: include quality warnings in the return value
87
+ const validation = validateWebsiteContext(context, projectName);
88
+ const validationWarnings = [...validation.issues, ...validation.warnings]
89
+ .map((w) => `[quality-gate] ${w}`);
90
+
91
+ return {
92
+ context,
93
+ warning: validationWarnings.length > 0 ? validationWarnings.join('; ') : undefined,
94
+ };
95
+ } catch (e) {
96
+ return { warning: e instanceof Error ? e.message : 'Unknown error building website context' };
97
+ }
98
+ }
99
+
45
100
  /**
46
101
  * Update state.json language field
47
102
  *
@@ -297,11 +352,21 @@ export async function upgradeFullstackToAll(
297
352
  openaiModel: 'gpt-4o',
298
353
  };
299
354
 
355
+ // Build content context from user docs, brand assets, and strategy
356
+ const { context: contentContext, warning } = await buildUpgradeContentContext(
357
+ projectDir,
358
+ projectName,
359
+ );
360
+ if (warning) {
361
+ console.warn(`[upgrade] Website context warning: ${warning}`);
362
+ }
363
+
300
364
  const result = await generateWebsiteProject(spec, projectDir, {
301
365
  baseDir: websiteDir,
302
366
  workspaceMode: true,
303
367
  skipDocker: true,
304
368
  skipReadme: true,
369
+ contentContext,
305
370
  });
306
371
 
307
372
  if (!result.success) {