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.
- package/CHANGELOG.md +54 -0
- package/README.md +184 -31
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +90 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +36 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +18 -3
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +81 -10
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts.map +1 -1
- package/dist/generators/templates/website-components.js +36 -11
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +15 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +155 -13
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -50
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -788
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +38 -2
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +179 -19
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +2 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +66 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +8 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +11 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/tester.d.ts +138 -0
- package/dist/types/tester.d.ts.map +1 -0
- package/dist/types/tester.js +110 -0
- package/dist/types/tester.js.map +1 -0
- package/dist/types/workflow.d.ts +151 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +14 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/execution-mode.js +2 -2
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +2 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +13 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts.map +1 -1
- package/dist/workflow/overview.js +4 -0
- package/dist/workflow/overview.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +4 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +69 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/task-workflow.d.ts +5 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +172 -6
- package/dist/workflow/task-workflow.js.map +1 -1
- package/dist/workflow/tester.d.ts +120 -0
- package/dist/workflow/tester.d.ts.map +1 -0
- package/dist/workflow/tester.js +589 -0
- package/dist/workflow/tester.js.map +1 -0
- package/dist/workflow/website-strategy.d.ts +9 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +73 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +15 -4
- package/dist/workflow/website-updater.js.map +1 -1
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +96 -7
- package/src/generators/all.ts +44 -332
- package/src/generators/doc-parser.ts +87 -10
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +36 -11
- package/src/generators/templates/website-config.ts +166 -13
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website.ts +38 -851
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +248 -20
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +71 -3
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +15 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/index.ts +21 -0
- package/src/types/tester.ts +136 -0
- package/src/types/workflow.ts +32 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/execution-mode.ts +2 -2
- package/src/workflow/index.ts +13 -0
- package/src/workflow/overview.ts +6 -0
- package/src/workflow/plan-mode.ts +81 -7
- package/src/workflow/task-workflow.ts +227 -5
- package/src/workflow/tester.ts +723 -0
- package/src/workflow/website-strategy.ts +75 -1
- package/src/workflow/website-updater.ts +17 -6
- package/src/workflow/workflow-logger.ts +2 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +1 -1
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +109 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/types/tester.test.ts +174 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- package/tests/workflow/tester.test.ts +401 -0
- 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
|
+
}
|
package/src/state/index.ts
CHANGED
|
@@ -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
|
*
|
package/src/types/consensus.ts
CHANGED
|
@@ -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
|
/**
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/types/workflow.ts
CHANGED
|
@@ -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
|
/**
|
package/src/upgrade/handlers.ts
CHANGED
|
@@ -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) {
|