popeye-cli 2.1.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/gemini.d.ts +14 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +41 -6
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +14 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +42 -6
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +44 -5
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +328 -21
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +25 -2
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +21 -6
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +55 -4
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/templates/fullstack.js +1 -1
- package/dist/generators/templates/website-components.js +1 -1
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +4 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +17 -11
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-conversion.js +1 -1
- package/dist/generators/templates/website-conversion.js.map +1 -1
- package/dist/generators/templates/website-landing.js +1 -1
- package/dist/generators/templates/website-landing.js.map +1 -1
- package/dist/generators/templates/website-layout.d.ts +36 -4
- package/dist/generators/templates/website-layout.d.ts.map +1 -1
- package/dist/generators/templates/website-layout.js +466 -23
- package/dist/generators/templates/website-layout.js.map +1 -1
- package/dist/generators/templates/website-pricing.js +1 -1
- package/dist/generators/templates/website-pricing.js.map +1 -1
- package/dist/generators/templates/website-sections.js +1 -1
- package/dist/generators/templates/website-sections.js.map +1 -1
- package/dist/generators/templates/website-seo.d.ts.map +1 -1
- package/dist/generators/templates/website-seo.js +4 -1
- package/dist/generators/templates/website-seo.js.map +1 -1
- package/dist/generators/templates/website.d.ts +1 -1
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +1 -1
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-ai.d.ts +52 -0
- package/dist/generators/website-content-ai.d.ts.map +1 -0
- package/dist/generators/website-content-ai.js +141 -0
- package/dist/generators/website-content-ai.js.map +1 -0
- package/dist/generators/website-content-scanner.d.ts +1 -1
- package/dist/generators/website-content-scanner.d.ts.map +1 -1
- package/dist/generators/website-content-scanner.js +98 -1
- package/dist/generators/website-content-scanner.js.map +1 -1
- package/dist/generators/website-context.d.ts +34 -1
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +131 -9
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +12 -0
- package/dist/generators/website-debug.d.ts.map +1 -1
- package/dist/generators/website-debug.js +16 -0
- package/dist/generators/website-debug.js.map +1 -1
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +26 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts.map +1 -1
- package/dist/pipeline/artifact-manager.js +3 -0
- package/dist/pipeline/artifact-manager.js.map +1 -1
- package/dist/pipeline/auto-recovery.d.ts +56 -0
- package/dist/pipeline/auto-recovery.d.ts.map +1 -0
- package/dist/pipeline/auto-recovery.js +185 -0
- package/dist/pipeline/auto-recovery.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +39 -0
- package/dist/pipeline/change-request.d.ts.map +1 -1
- package/dist/pipeline/change-request.js +40 -1
- package/dist/pipeline/change-request.js.map +1 -1
- package/dist/pipeline/check-runner.d.ts +30 -1
- package/dist/pipeline/check-runner.d.ts.map +1 -1
- package/dist/pipeline/check-runner.js +122 -1
- package/dist/pipeline/check-runner.js.map +1 -1
- package/dist/pipeline/command-resolver.d.ts.map +1 -1
- package/dist/pipeline/command-resolver.js +33 -2
- package/dist/pipeline/command-resolver.js.map +1 -1
- package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
- package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
- package/dist/pipeline/consensus/arbitrator-query.js +70 -0
- package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
- package/dist/pipeline/consensus/consensus-runner.js +809 -35
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/cr-lifecycle.d.ts +42 -0
- package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
- package/dist/pipeline/cr-lifecycle.js +89 -0
- package/dist/pipeline/cr-lifecycle.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +1 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -1
- package/dist/pipeline/gate-engine.js +27 -8
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/migration.d.ts.map +1 -1
- package/dist/pipeline/migration.js +3 -26
- package/dist/pipeline/migration.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +311 -16
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
- package/dist/pipeline/phases/architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/architecture.js +5 -3
- package/dist/pipeline/phases/architecture.js.map +1 -1
- package/dist/pipeline/phases/audit.d.ts.map +1 -1
- package/dist/pipeline/phases/audit.js +5 -3
- package/dist/pipeline/phases/audit.js.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.js +10 -1
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.js +10 -3
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.js +10 -1
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
- package/dist/pipeline/phases/done.d.ts.map +1 -1
- package/dist/pipeline/phases/done.js +9 -4
- package/dist/pipeline/phases/done.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts +1 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +56 -13
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/phase-context.d.ts +2 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
- package/dist/pipeline/phases/phase-context.js +3 -1
- package/dist/pipeline/phases/phase-context.js.map +1 -1
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
- package/dist/pipeline/phases/production-gate.js +28 -3
- package/dist/pipeline/phases/production-gate.js.map +1 -1
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
- package/dist/pipeline/phases/qa-validation.js +38 -5
- package/dist/pipeline/phases/qa-validation.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +200 -6
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- package/dist/pipeline/phases/review.d.ts.map +1 -1
- package/dist/pipeline/phases/review.js +58 -28
- package/dist/pipeline/phases/review.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +20 -5
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/phases/stuck.d.ts.map +1 -1
- package/dist/pipeline/phases/stuck.js +10 -0
- package/dist/pipeline/phases/stuck.js.map +1 -1
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
- package/dist/pipeline/repo-snapshot.js +3 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -1
- package/dist/pipeline/role-execution-adapter.d.ts +2 -1
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
- package/dist/pipeline/role-execution-adapter.js +22 -7
- package/dist/pipeline/role-execution-adapter.js.map +1 -1
- package/dist/pipeline/skill-loader.d.ts +19 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -1
- package/dist/pipeline/skill-loader.js +22 -0
- package/dist/pipeline/skill-loader.js.map +1 -1
- package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
- package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
- package/dist/pipeline/skills/constitution-generator.js +210 -0
- package/dist/pipeline/skills/constitution-generator.js.map +1 -0
- package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
- package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
- package/dist/pipeline/skills/coverage-gate.js +143 -0
- package/dist/pipeline/skills/coverage-gate.js.map +1 -0
- package/dist/pipeline/skills/generator.d.ts +65 -0
- package/dist/pipeline/skills/generator.d.ts.map +1 -0
- package/dist/pipeline/skills/generator.js +221 -0
- package/dist/pipeline/skills/generator.js.map +1 -0
- package/dist/pipeline/skills/role-map.d.ts +38 -0
- package/dist/pipeline/skills/role-map.d.ts.map +1 -0
- package/dist/pipeline/skills/role-map.js +234 -0
- package/dist/pipeline/skills/role-map.js.map +1 -0
- package/dist/pipeline/skills/types.d.ts +47 -0
- package/dist/pipeline/skills/types.d.ts.map +1 -0
- package/dist/pipeline/skills/types.js +5 -0
- package/dist/pipeline/skills/types.js.map +1 -0
- package/dist/pipeline/skills/usage-registry.d.ts +48 -0
- package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
- package/dist/pipeline/skills/usage-registry.js +55 -0
- package/dist/pipeline/skills/usage-registry.js.map +1 -0
- package/dist/pipeline/strategy-context.d.ts +20 -0
- package/dist/pipeline/strategy-context.d.ts.map +1 -0
- package/dist/pipeline/strategy-context.js +55 -0
- package/dist/pipeline/strategy-context.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +30 -5
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +5 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +28 -13
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +19 -8
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.js +4 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +119 -18
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.js +17 -1
- package/dist/pipeline/type-defs/packets.js.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +165 -16
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +26 -1
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/shared/text-utils.d.ts +23 -0
- package/dist/shared/text-utils.d.ts.map +1 -0
- package/dist/shared/text-utils.js +66 -0
- package/dist/shared/text-utils.js.map +1 -0
- package/dist/shared/website-strategy-format.d.ts +18 -0
- package/dist/shared/website-strategy-format.d.ts.map +1 -0
- package/dist/shared/website-strategy-format.js +47 -0
- package/dist/shared/website-strategy-format.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +57 -8
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +1 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +1 -1
- package/dist/types/workflow.d.ts +447 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +3 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +6 -3
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +1 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +2 -29
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +3 -2
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +51 -6
- package/src/adapters/grok.ts +51 -6
- package/src/adapters/openai.ts +53 -5
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/interactive.ts +337 -20
- package/src/generators/all.ts +25 -2
- package/src/generators/doc-parser.ts +75 -15
- package/src/generators/templates/fullstack.ts +1 -1
- package/src/generators/templates/website-components.ts +1 -1
- package/src/generators/templates/website-config.ts +23 -11
- package/src/generators/templates/website-conversion.ts +1 -1
- package/src/generators/templates/website-landing.ts +1 -1
- package/src/generators/templates/website-layout.ts +491 -23
- package/src/generators/templates/website-pricing.ts +1 -1
- package/src/generators/templates/website-sections.ts +1 -1
- package/src/generators/templates/website-seo.ts +4 -1
- package/src/generators/templates/website.ts +3 -0
- package/src/generators/website-content-ai.ts +186 -0
- package/src/generators/website-content-scanner.ts +113 -1
- package/src/generators/website-context.ts +151 -12
- package/src/generators/website-debug.ts +26 -0
- package/src/generators/website.ts +28 -3
- package/src/pipeline/artifact-manager.ts +3 -0
- package/src/pipeline/auto-recovery.ts +283 -0
- package/src/pipeline/change-request.ts +63 -1
- package/src/pipeline/check-runner.ts +141 -2
- package/src/pipeline/command-resolver.ts +34 -2
- package/src/pipeline/consensus/arbitrator-query.ts +101 -0
- package/src/pipeline/consensus/consensus-runner.ts +1099 -42
- package/src/pipeline/cr-lifecycle.ts +103 -0
- package/src/pipeline/gate-engine.ts +36 -8
- package/src/pipeline/migration.ts +5 -30
- package/src/pipeline/orchestrator.ts +367 -16
- package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
- package/src/pipeline/phases/architecture.ts +6 -3
- package/src/pipeline/phases/audit.ts +6 -3
- package/src/pipeline/phases/consensus-architecture.ts +10 -1
- package/src/pipeline/phases/consensus-master-plan.ts +10 -3
- package/src/pipeline/phases/consensus-role-plans.ts +10 -1
- package/src/pipeline/phases/done.ts +15 -4
- package/src/pipeline/phases/intake.ts +67 -14
- package/src/pipeline/phases/phase-context.ts +6 -1
- package/src/pipeline/phases/production-gate.ts +41 -3
- package/src/pipeline/phases/qa-validation.ts +51 -5
- package/src/pipeline/phases/recovery-loop.ts +229 -7
- package/src/pipeline/phases/review.ts +73 -30
- package/src/pipeline/phases/role-planning.ts +23 -5
- package/src/pipeline/phases/stuck.ts +10 -0
- package/src/pipeline/repo-snapshot.ts +3 -0
- package/src/pipeline/role-execution-adapter.ts +30 -4
- package/src/pipeline/skill-loader.ts +33 -0
- package/src/pipeline/skills/constitution-generator.ts +236 -0
- package/src/pipeline/skills/coverage-gate.ts +199 -0
- package/src/pipeline/skills/generator.ts +287 -0
- package/src/pipeline/skills/role-map.ts +248 -0
- package/src/pipeline/skills/types.ts +53 -0
- package/src/pipeline/skills/usage-registry.ts +87 -0
- package/src/pipeline/strategy-context.ts +60 -0
- package/src/pipeline/type-defs/artifacts.ts +5 -0
- package/src/pipeline/type-defs/checks.ts +4 -0
- package/src/pipeline/type-defs/packets.ts +18 -1
- package/src/pipeline/type-defs/state.ts +26 -1
- package/src/shared/text-utils.ts +70 -0
- package/src/shared/website-strategy-format.ts +56 -0
- package/src/state/index.ts +60 -8
- package/src/types/consensus.ts +1 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +9 -3
- package/src/workflow/consensus.ts +1 -0
- package/src/workflow/website-strategy.ts +2 -36
- package/src/workflow/website-updater.ts +4 -2
- package/tests/adapters/gemini.test.ts +165 -0
- package/tests/adapters/grok.test.ts +137 -0
- package/tests/adapters/openai.test.ts +128 -0
- package/tests/generators/doc-parser.test.ts +88 -9
- package/tests/generators/quality-gate.test.ts +19 -3
- package/tests/generators/website-components.test.ts +34 -0
- package/tests/generators/website-content-ai.test.ts +308 -0
- package/tests/generators/website-content-scanner.test.ts +86 -0
- package/tests/generators/website-context.test.ts +3 -2
- package/tests/integration/smokestack-scaffold.test.ts +385 -0
- package/tests/pipeline/auto-recovery.test.ts +337 -0
- package/tests/pipeline/change-request.test.ts +70 -0
- package/tests/pipeline/command-resolver.test.ts +42 -0
- package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
- package/tests/pipeline/consensus-runner.test.ts +1333 -10
- package/tests/pipeline/consensus-scoring.test.ts +602 -18
- package/tests/pipeline/gate-engine.test.ts +34 -0
- package/tests/pipeline/install-check.test.ts +261 -0
- package/tests/pipeline/migration.test.ts +4 -3
- package/tests/pipeline/orchestrator.test.ts +1506 -15
- package/tests/pipeline/packets/builders.test.ts +29 -6
- package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
- package/tests/pipeline/pipeline-persistence.test.ts +230 -0
- package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
- package/tests/pipeline/role-execution-adapter.test.ts +88 -0
- package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
- package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
- package/tests/pipeline/skills/generator.test.ts +213 -0
- package/tests/pipeline/skills/role-map.test.ts +198 -0
- package/tests/pipeline/skills/usage-registry.test.ts +114 -0
- package/tests/pipeline/strategy-context.test.ts +148 -0
- package/tests/shared/text-utils.test.ts +155 -0
- package/tests/state/progress-analysis.test.ts +375 -0
- package/tests/upgrade/handlers.test.ts +33 -2
- package/tests/workflow/consensus.test.ts +6 -0
- package/tsconfig.json +1 -1
|
@@ -15,7 +15,7 @@ function escapeJsx(str: string): string {
|
|
|
15
15
|
.replace(/\\/g, '\\\\')
|
|
16
16
|
.replace(/'/g, "\\'")
|
|
17
17
|
.replace(/`/g, '\\`')
|
|
18
|
-
.replace(
|
|
18
|
+
.replace(/\$(?=\{)/g, '\\$');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -111,6 +111,9 @@ export function generateEnhancedSitemap(
|
|
|
111
111
|
{ path: '/pricing', pageType: 'pricing' },
|
|
112
112
|
{ path: '/docs', pageType: 'docs' },
|
|
113
113
|
{ path: '/blog', pageType: 'blog' },
|
|
114
|
+
{ path: '/contact', pageType: 'contact' },
|
|
115
|
+
{ path: '/privacy', pageType: 'legal' },
|
|
116
|
+
{ path: '/terms', pageType: 'legal' },
|
|
114
117
|
];
|
|
115
118
|
|
|
116
119
|
const priorityMap: Record<string, number> = {
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Website Content Generator — fills missing website content
|
|
3
|
+
* when doc parser fails to extract tagline/description/features/pricing.
|
|
4
|
+
*
|
|
5
|
+
* Uses OpenAI gpt-4.1 with Zod-validated JSON response.
|
|
6
|
+
* Includes pricing hallucination guard to prevent inventing prices
|
|
7
|
+
* without evidence in source docs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
// ─── Types ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface AIContentResult {
|
|
15
|
+
tagline?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
features?: Array<{ title: string; description: string; icon?: string }>;
|
|
18
|
+
pricing?: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
price: string;
|
|
21
|
+
period?: string;
|
|
22
|
+
description: string;
|
|
23
|
+
features: string[];
|
|
24
|
+
cta: string;
|
|
25
|
+
featured?: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Response Schema ─────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const AIContentResponseSchema = z.object({
|
|
32
|
+
tagline: z.string().optional(),
|
|
33
|
+
description: z.string().optional(),
|
|
34
|
+
features: z.array(z.object({
|
|
35
|
+
title: z.string(),
|
|
36
|
+
description: z.string(),
|
|
37
|
+
icon: z.string().optional(),
|
|
38
|
+
})).optional(),
|
|
39
|
+
pricing: z.array(z.object({
|
|
40
|
+
name: z.string(),
|
|
41
|
+
price: z.string(),
|
|
42
|
+
period: z.string().optional(),
|
|
43
|
+
description: z.string(),
|
|
44
|
+
features: z.array(z.string()),
|
|
45
|
+
cta: z.string().default('Get Started'),
|
|
46
|
+
featured: z.boolean().optional(),
|
|
47
|
+
})).optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Pricing Hallucination Guard ─────────────────────────
|
|
51
|
+
|
|
52
|
+
/** Broad pricing evidence detection */
|
|
53
|
+
const PRICING_EVIDENCE = /\$\d|€\d|£\d|USD\s*\d|\d+\s*\/\s*mo|per\s*month|monthly|yearly|\/yr|per\s*year|pricing\s*(?:plan|tier|model)|subscription|free\s*(?:plan|tier)/i;
|
|
54
|
+
|
|
55
|
+
/** Pattern that detects numeric pricing in AI output */
|
|
56
|
+
const NUMERIC_PRICING_PATTERN = /[\$€£]\d|USD\s*\d|\d+\s*\/\s*mo/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if source docs contain evidence of pricing information.
|
|
60
|
+
*
|
|
61
|
+
* Args:
|
|
62
|
+
* rawDocs: Combined documentation content.
|
|
63
|
+
* specification: Expanded specification text.
|
|
64
|
+
*
|
|
65
|
+
* Returns:
|
|
66
|
+
* true if pricing evidence is found in either source.
|
|
67
|
+
*/
|
|
68
|
+
export function hasPricingEvidence(rawDocs: string, specification: string): boolean {
|
|
69
|
+
return PRICING_EVIDENCE.test(rawDocs) || PRICING_EVIDENCE.test(specification);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Post-validate AI-generated pricing to prevent hallucinated prices.
|
|
74
|
+
* If no pricing evidence exists in source docs, reject any numeric prices.
|
|
75
|
+
* "Free" is always allowed (low risk).
|
|
76
|
+
*
|
|
77
|
+
* Args:
|
|
78
|
+
* pricing: AI-generated pricing array.
|
|
79
|
+
* hasEvidence: Whether pricing evidence was found in source docs.
|
|
80
|
+
*
|
|
81
|
+
* Returns:
|
|
82
|
+
* Sanitized pricing array (empty if hallucinated prices detected).
|
|
83
|
+
*/
|
|
84
|
+
function sanitizePricing(
|
|
85
|
+
pricing: NonNullable<AIContentResult['pricing']>,
|
|
86
|
+
hasEvidence: boolean,
|
|
87
|
+
): AIContentResult['pricing'] {
|
|
88
|
+
if (hasEvidence) return pricing;
|
|
89
|
+
|
|
90
|
+
// No evidence — check if AI invented numeric prices
|
|
91
|
+
const hasNumericPrices = pricing.some(
|
|
92
|
+
(tier) => NUMERIC_PRICING_PATTERN.test(tier.price),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (hasNumericPrices) {
|
|
96
|
+
// Reject entire pricing array — AI hallucinated prices
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Allow "Free", "TBD", "Contact us" etc.
|
|
101
|
+
return pricing;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── AI Content Generation ──────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate missing website content using AI.
|
|
108
|
+
* Single OpenAI gpt-4.1 call with Zod-validated JSON response.
|
|
109
|
+
* Non-blocking: returns empty {} on any failure.
|
|
110
|
+
*
|
|
111
|
+
* Args:
|
|
112
|
+
* productName: The product name.
|
|
113
|
+
* specification: Expanded specification text.
|
|
114
|
+
* rawDocs: Combined documentation content.
|
|
115
|
+
*
|
|
116
|
+
* Returns:
|
|
117
|
+
* AIContentResult with any fields the AI could generate.
|
|
118
|
+
*/
|
|
119
|
+
export async function generateMissingWebsiteContent(
|
|
120
|
+
productName: string,
|
|
121
|
+
specification: string,
|
|
122
|
+
rawDocs: string,
|
|
123
|
+
): Promise<AIContentResult> {
|
|
124
|
+
try {
|
|
125
|
+
const hasEvidence = hasPricingEvidence(rawDocs, specification);
|
|
126
|
+
|
|
127
|
+
const pricingInstruction = hasEvidence
|
|
128
|
+
? 'Infer pricing tiers from the documentation. Use exact prices if mentioned.'
|
|
129
|
+
: 'Set all prices to "Contact us" or "TBD". Do not invent numeric pricing.';
|
|
130
|
+
|
|
131
|
+
const systemPrompt = [
|
|
132
|
+
'You are a marketing content expert. Generate website content for a software product.',
|
|
133
|
+
'Return a JSON object with these fields (all optional):',
|
|
134
|
+
'- tagline: short marketing tagline (max 80 chars)',
|
|
135
|
+
'- description: product description (max 200 chars)',
|
|
136
|
+
'- features: array of {title, description, icon?} (3-6 features, icon is a lucide icon name)',
|
|
137
|
+
'- pricing: array of {name, price, period?, description, features[], cta, featured?} (2-4 tiers, cta is the button text like "Get Started")',
|
|
138
|
+
'',
|
|
139
|
+
`PRICING RULES: ${pricingInstruction}`,
|
|
140
|
+
'',
|
|
141
|
+
'Base all content on the product documentation provided. Do not invent facts.',
|
|
142
|
+
'Return ONLY valid JSON, no markdown or explanation.',
|
|
143
|
+
].join('\n');
|
|
144
|
+
|
|
145
|
+
const userPrompt = [
|
|
146
|
+
`Product: ${productName}`,
|
|
147
|
+
'',
|
|
148
|
+
specification ? `Specification:\n${specification.slice(0, 3000)}` : '',
|
|
149
|
+
'',
|
|
150
|
+
rawDocs ? `Documentation:\n${rawDocs.slice(0, 5000)}` : '',
|
|
151
|
+
].filter(Boolean).join('\n');
|
|
152
|
+
|
|
153
|
+
const { createClient } = await import('../adapters/openai.js');
|
|
154
|
+
const client = await createClient();
|
|
155
|
+
const completion = await client.chat.completions.create({
|
|
156
|
+
model: 'gpt-4.1',
|
|
157
|
+
messages: [
|
|
158
|
+
{ role: 'system', content: systemPrompt },
|
|
159
|
+
{ role: 'user', content: userPrompt },
|
|
160
|
+
],
|
|
161
|
+
temperature: 0.3,
|
|
162
|
+
response_format: { type: 'json_object' },
|
|
163
|
+
});
|
|
164
|
+
const response = completion.choices[0]?.message?.content ?? '{}';
|
|
165
|
+
|
|
166
|
+
// Parse and validate response
|
|
167
|
+
const parsed = JSON.parse(response);
|
|
168
|
+
const validated = AIContentResponseSchema.safeParse(parsed);
|
|
169
|
+
|
|
170
|
+
if (!validated.success) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result: AIContentResult = { ...validated.data };
|
|
175
|
+
|
|
176
|
+
// Apply pricing hallucination guard
|
|
177
|
+
if (result.pricing && result.pricing.length > 0) {
|
|
178
|
+
result.pricing = sanitizePricing(result.pricing, hasEvidence);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
} catch {
|
|
183
|
+
// Non-blocking: return empty on any failure
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -76,6 +76,16 @@ const PLACEHOLDER_PATTERNS: Array<{
|
|
|
76
76
|
message: 'Default pricing amount ($29/mo)',
|
|
77
77
|
severity: 'warning',
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
pattern: /coming soon/i,
|
|
81
|
+
message: 'Contains "coming soon" placeholder text',
|
|
82
|
+
severity: 'error',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
pattern: /placeholder/i,
|
|
86
|
+
message: 'Contains "placeholder" text',
|
|
87
|
+
severity: 'error',
|
|
88
|
+
},
|
|
79
89
|
];
|
|
80
90
|
|
|
81
91
|
/**
|
|
@@ -156,7 +166,100 @@ function findLineNumber(content: string, pattern: RegExp): number | undefined {
|
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
|
-
*
|
|
169
|
+
* Collect all page routes from the src/app directory structure.
|
|
170
|
+
* A route exists if there is a page.tsx (or page.jsx/page.ts/page.js) in a directory.
|
|
171
|
+
*
|
|
172
|
+
* @param appDir - The src/app directory path
|
|
173
|
+
* @returns Set of route paths (e.g., '/', '/pricing', '/blog')
|
|
174
|
+
*/
|
|
175
|
+
async function collectPageRoutes(appDir: string): Promise<Set<string>> {
|
|
176
|
+
const routes = new Set<string>();
|
|
177
|
+
|
|
178
|
+
async function walk(dir: string, routePrefix: string): Promise<void> {
|
|
179
|
+
try {
|
|
180
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
181
|
+
|
|
182
|
+
// Check if this directory has a page file
|
|
183
|
+
const hasPage = entries.some(
|
|
184
|
+
(e) => e.isFile() && /^page\.(tsx|jsx|ts|js)$/.test(e.name)
|
|
185
|
+
);
|
|
186
|
+
if (hasPage) {
|
|
187
|
+
routes.add(routePrefix || '/');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Recurse into subdirectories (skip special dirs)
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (!entry.isDirectory()) continue;
|
|
193
|
+
if (entry.name.startsWith('_') || entry.name === 'api' || SKIP_DIRS.has(entry.name)) continue;
|
|
194
|
+
await walk(path.join(dir, entry.name), `${routePrefix}/${entry.name}`);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Directory not accessible
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await walk(appDir, '');
|
|
202
|
+
return routes;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Scan for internal links that point to pages that do not exist.
|
|
207
|
+
*
|
|
208
|
+
* @param websiteDir - The website project directory
|
|
209
|
+
* @param files - Already-collected source file paths
|
|
210
|
+
* @returns Array of scan issues for broken internal links
|
|
211
|
+
*/
|
|
212
|
+
async function scanInternalLinks(
|
|
213
|
+
websiteDir: string,
|
|
214
|
+
files: string[]
|
|
215
|
+
): Promise<ScanIssue[]> {
|
|
216
|
+
const issues: ScanIssue[] = [];
|
|
217
|
+
const appDir = path.join(websiteDir, 'src', 'app');
|
|
218
|
+
|
|
219
|
+
// Collect existing page routes
|
|
220
|
+
const routes = await collectPageRoutes(appDir);
|
|
221
|
+
|
|
222
|
+
// Regex to find href="/..." values in source files
|
|
223
|
+
const hrefPattern = /href=["'](\/([\w-/]*)?)["']/g;
|
|
224
|
+
|
|
225
|
+
for (const filePath of files) {
|
|
226
|
+
try {
|
|
227
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
228
|
+
const relativePath = path.relative(websiteDir, filePath);
|
|
229
|
+
|
|
230
|
+
let match;
|
|
231
|
+
while ((match = hrefPattern.exec(content)) !== null) {
|
|
232
|
+
const href = match[1];
|
|
233
|
+
|
|
234
|
+
// Skip anchor links (/#section), API routes, and external/protocol links
|
|
235
|
+
if (href.startsWith('/#') || href.startsWith('/api/') || href.startsWith('/api')) continue;
|
|
236
|
+
|
|
237
|
+
// Normalize: strip trailing slash for comparison
|
|
238
|
+
const normalizedHref = href === '/' ? '/' : href.replace(/\/$/, '');
|
|
239
|
+
|
|
240
|
+
if (!routes.has(normalizedHref)) {
|
|
241
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
242
|
+
issues.push({
|
|
243
|
+
file: relativePath,
|
|
244
|
+
message: `Internal link "${href}" points to a page that does not exist`,
|
|
245
|
+
severity: 'error',
|
|
246
|
+
line: lineNum,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Reset regex lastIndex for next file
|
|
252
|
+
hrefPattern.lastIndex = 0;
|
|
253
|
+
} catch {
|
|
254
|
+
// Skip unreadable files
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return issues;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Scan generated website files for placeholder fingerprints and broken internal links
|
|
160
263
|
*
|
|
161
264
|
* @param websiteDir - The website project directory to scan
|
|
162
265
|
* @returns Scan result with issues and quality score
|
|
@@ -200,6 +303,15 @@ export async function scanGeneratedContent(websiteDir: string): Promise<ScanResu
|
|
|
200
303
|
}
|
|
201
304
|
}
|
|
202
305
|
|
|
306
|
+
// Scan for broken internal links
|
|
307
|
+
try {
|
|
308
|
+
const linkIssues = await scanInternalLinks(websiteDir, files);
|
|
309
|
+
issues.push(...linkIssues);
|
|
310
|
+
score -= linkIssues.length * 15;
|
|
311
|
+
} catch {
|
|
312
|
+
// Non-blocking: link scan failures should not stop the overall scan
|
|
313
|
+
}
|
|
314
|
+
|
|
203
315
|
return {
|
|
204
316
|
issues,
|
|
205
317
|
filesScanned: files.length,
|
|
@@ -17,10 +17,17 @@ import {
|
|
|
17
17
|
} from './doc-parser.js';
|
|
18
18
|
import { getScanDirectories } from './workspace-root.js';
|
|
19
19
|
import type { WebsiteStrategyDocument, BrandAssetsContract } from '../types/website-strategy.js';
|
|
20
|
+
import { generateMissingWebsiteContent } from './website-content-ai.js';
|
|
20
21
|
|
|
21
22
|
/** Per-file character cap to prevent a single large doc from consuming the budget */
|
|
22
23
|
const PER_FILE_CAP = 8000;
|
|
23
24
|
|
|
25
|
+
/** Allowed extensions for user-provided doc paths */
|
|
26
|
+
const ALLOWED_DOC_EXTENSIONS = ['.md', '.mdx', '.txt'];
|
|
27
|
+
|
|
28
|
+
/** Maximum file size for user-provided docs (2MB) */
|
|
29
|
+
const MAX_DOC_SIZE = 2 * 1024 * 1024;
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Structured content context for website generation
|
|
26
33
|
*/
|
|
@@ -48,6 +55,15 @@ export interface WebsiteContentContext {
|
|
|
48
55
|
strategy?: WebsiteStrategyDocument;
|
|
49
56
|
/** Resolved brand assets contract for deterministic logo/favicon placement */
|
|
50
57
|
brandAssets?: BrandAssetsContract;
|
|
58
|
+
/** Diagnostics for pricing extraction pipeline */
|
|
59
|
+
pricingDiagnostics?: {
|
|
60
|
+
charsScanned: number;
|
|
61
|
+
foundPricingHeader: boolean;
|
|
62
|
+
extractionMethod: 'known_plan_names' | 'table_fallback' | 'ai' | 'none';
|
|
63
|
+
extractedTiers?: Array<{ name: string; price: string }>;
|
|
64
|
+
reasonIfEmpty?: string;
|
|
65
|
+
source: 'docs' | 'ai' | 'default' | 'none';
|
|
66
|
+
};
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
/**
|
|
@@ -279,20 +295,104 @@ export async function readProjectDocs(
|
|
|
279
295
|
return sections.join('\n\n');
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Extract absolute file paths from user-provided text (idea/specification).
|
|
300
|
+
* Supports:
|
|
301
|
+
* - Unix paths: '/path/to/file.md'
|
|
302
|
+
* - Windows paths: "C:\Users\me\file.md"
|
|
303
|
+
* - Paths with spaces: '/Users/me/My Docs/pricing.md'
|
|
304
|
+
* - Extensions: .md, .mdx, .txt
|
|
305
|
+
*
|
|
306
|
+
* @param text - User-provided idea or specification text
|
|
307
|
+
* @returns Array of absolute file paths found in the text
|
|
308
|
+
*/
|
|
309
|
+
export function extractDocPathsFromText(text: string): string[] {
|
|
310
|
+
const paths: string[] = [];
|
|
311
|
+
|
|
312
|
+
// 1. Quoted paths (most reliable — handles spaces, cross-platform)
|
|
313
|
+
const quotedPattern = /['"]([A-Za-z]:[\\\/].+?\.(?:md|mdx|txt)|\/[^'"]+\.(?:md|mdx|txt))['"]/gi;
|
|
314
|
+
let match;
|
|
315
|
+
while ((match = quotedPattern.exec(text)) !== null) {
|
|
316
|
+
paths.push(match[1]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 2. Unquoted Unix absolute paths (no spaces — spaces break word boundaries)
|
|
320
|
+
const unquotedUnix = /(?:^|\s)(\/\S+\.(?:md|mdx|txt))(?=[\s,;)]|$)/gm;
|
|
321
|
+
while ((match = unquotedUnix.exec(text)) !== null) {
|
|
322
|
+
if (!paths.includes(match[1])) paths.push(match[1]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 3. Unquoted Windows paths (drive letter)
|
|
326
|
+
const unquotedWin = /(?:^|\s)([A-Za-z]:[\\\/]\S+\.(?:md|mdx|txt))(?=[\s,;)]|$)/gm;
|
|
327
|
+
while ((match = unquotedWin.exec(text)) !== null) {
|
|
328
|
+
if (!paths.includes(match[1])) paths.push(match[1]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return [...new Set(paths)];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Validate and filter doc paths for safe ingestion.
|
|
336
|
+
* Checks: isAbsolute, file exists, size < 2MB, extension allowed.
|
|
337
|
+
*
|
|
338
|
+
* @param paths - Array of absolute paths to validate
|
|
339
|
+
* @returns Array of valid, readable paths
|
|
340
|
+
*/
|
|
341
|
+
export async function validateAndFilterDocPaths(paths: string[]): Promise<string[]> {
|
|
342
|
+
const valid: string[] = [];
|
|
343
|
+
for (const p of paths) {
|
|
344
|
+
const ext = path.extname(p).toLowerCase();
|
|
345
|
+
if (!ALLOWED_DOC_EXTENSIONS.includes(ext)) {
|
|
346
|
+
console.log(`[doc-ingest] Skipped: ${p} (unsupported format ${ext})`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const isAbs = path.isAbsolute(p) || /^[A-Za-z]:[\\\/]/.test(p);
|
|
350
|
+
if (!isAbs) continue;
|
|
351
|
+
try {
|
|
352
|
+
const stat = await fs.stat(p);
|
|
353
|
+
if (stat.size > MAX_DOC_SIZE) {
|
|
354
|
+
console.log(`[doc-ingest] Skipped: ${p} (${(stat.size / 1024).toFixed(0)}KB exceeds 2MB limit)`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
console.log(`[doc-ingest] Read: ${p} (${(stat.size / 1024).toFixed(1)}KB)`);
|
|
358
|
+
valid.push(p);
|
|
359
|
+
} catch {
|
|
360
|
+
console.log(`[doc-ingest] Skipped: ${p} (file not found)`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return valid;
|
|
364
|
+
}
|
|
365
|
+
|
|
282
366
|
/**
|
|
283
367
|
* Build a structured website content context from discovered docs
|
|
284
368
|
*
|
|
285
369
|
* @param cwd - Working directory to scan for docs
|
|
286
370
|
* @param projectName - The project name (folder name fallback)
|
|
287
371
|
* @param specification - Optional expanded specification text
|
|
372
|
+
* @param extraDocPaths - Optional extra doc paths extracted from user idea text
|
|
288
373
|
* @returns Structured content context for website templates
|
|
289
374
|
*/
|
|
290
375
|
export async function buildWebsiteContext(
|
|
291
376
|
cwd: string,
|
|
292
377
|
projectName: string,
|
|
293
|
-
specification?: string
|
|
378
|
+
specification?: string,
|
|
379
|
+
extraDocPaths?: string[],
|
|
294
380
|
): Promise<WebsiteContentContext> {
|
|
295
381
|
const docPaths = await discoverProjectDocs(cwd);
|
|
382
|
+
|
|
383
|
+
// Merge validated extra doc paths (from idea text)
|
|
384
|
+
if (extraDocPaths?.length) {
|
|
385
|
+
const validPaths = await validateAndFilterDocPaths(extraDocPaths);
|
|
386
|
+
const seen = new Set(docPaths.map((p) => path.resolve(p)));
|
|
387
|
+
for (const p of validPaths) {
|
|
388
|
+
const resolved = path.resolve(p);
|
|
389
|
+
if (!seen.has(resolved)) {
|
|
390
|
+
docPaths.push(resolved);
|
|
391
|
+
seen.add(resolved);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
296
396
|
const rawDocs = docPaths.length > 0 ? await readProjectDocs(docPaths) : '';
|
|
297
397
|
const brandAssets = await findBrandAssets(cwd);
|
|
298
398
|
|
|
@@ -307,7 +407,20 @@ export async function buildWebsiteContext(
|
|
|
307
407
|
|
|
308
408
|
context.tagline = extractTagline(cleanDocs, context.productName);
|
|
309
409
|
context.description = extractDescription(cleanDocs, specification);
|
|
310
|
-
|
|
410
|
+
|
|
411
|
+
// Extract pricing with provenance tracking
|
|
412
|
+
const pricingResult = extractPricing(cleanDocs);
|
|
413
|
+
context.pricing = pricingResult.tiers.length > 0 ? pricingResult.tiers : undefined;
|
|
414
|
+
context.pricingDiagnostics = {
|
|
415
|
+
charsScanned: cleanDocs.length,
|
|
416
|
+
foundPricingHeader: !!cleanDocs.match(/##\s+(?:[\d.]*\s*)?Pricing\b/i),
|
|
417
|
+
extractionMethod: pricingResult.evidence?.extractionMethod ?? 'none',
|
|
418
|
+
extractedTiers: pricingResult.tiers.map((t) => ({ name: t.name, price: t.price })),
|
|
419
|
+
reasonIfEmpty: pricingResult.tiers.length === 0
|
|
420
|
+
? 'No pricing table rows matched in docs'
|
|
421
|
+
: undefined,
|
|
422
|
+
source: pricingResult.source,
|
|
423
|
+
};
|
|
311
424
|
|
|
312
425
|
// Extract brand info
|
|
313
426
|
if (brandAssets.logoPath) {
|
|
@@ -319,6 +432,36 @@ export async function buildWebsiteContext(
|
|
|
319
432
|
context.brand = { ...context.brand, primaryColor };
|
|
320
433
|
}
|
|
321
434
|
|
|
435
|
+
// v2.2.2: Per-field AI content generation fallback
|
|
436
|
+
// Reason: triggers when ANY field is missing (not all-or-nothing), but only fills missing fields
|
|
437
|
+
const pricingMissing = context.pricing == null || context.pricing.length === 0;
|
|
438
|
+
const hasMissingContent = !context.tagline || !context.description || context.features.length === 0 || pricingMissing;
|
|
439
|
+
if (hasMissingContent && (rawDocs.length > 50 || specification)) {
|
|
440
|
+
try {
|
|
441
|
+
const aiContent = await generateMissingWebsiteContent(
|
|
442
|
+
context.productName,
|
|
443
|
+
specification ?? '',
|
|
444
|
+
rawDocs,
|
|
445
|
+
);
|
|
446
|
+
// Only fill fields that are actually missing (don't overwrite extracted truth)
|
|
447
|
+
if (!context.tagline && aiContent.tagline) context.tagline = aiContent.tagline;
|
|
448
|
+
if (!context.description && aiContent.description) context.description = aiContent.description;
|
|
449
|
+
if (context.features.length === 0 && aiContent.features?.length) context.features = aiContent.features;
|
|
450
|
+
if (pricingMissing && aiContent.pricing?.length) {
|
|
451
|
+
context.pricing = aiContent.pricing;
|
|
452
|
+
context.pricingDiagnostics = {
|
|
453
|
+
...context.pricingDiagnostics!,
|
|
454
|
+
extractionMethod: 'ai',
|
|
455
|
+
source: 'ai',
|
|
456
|
+
extractedTiers: aiContent.pricing.map((t) => ({ name: t.name, price: t.price })),
|
|
457
|
+
reasonIfEmpty: undefined,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
// Non-blocking: continue with doc-parser results
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
322
465
|
return context;
|
|
323
466
|
}
|
|
324
467
|
|
|
@@ -388,14 +531,6 @@ export function validateWebsiteContext(
|
|
|
388
531
|
contentScore -= 20;
|
|
389
532
|
}
|
|
390
533
|
|
|
391
|
-
// Strategy validation
|
|
392
|
-
if (!context.strategy) {
|
|
393
|
-
issues.push(
|
|
394
|
-
'Website strategy missing. Strategy generation may have failed or been skipped.'
|
|
395
|
-
);
|
|
396
|
-
contentScore -= 15;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
534
|
// Brand color validation: brand/color docs exist but no color extracted
|
|
400
535
|
if (!context.brand?.primaryColor && context.rawDocs && /color|brand/i.test(context.rawDocs)) {
|
|
401
536
|
issues.push(
|
|
@@ -421,8 +556,12 @@ export function validateWebsiteContext(
|
|
|
421
556
|
const nameMatches = tierNames.filter((n) => DEFAULT_PRICING_NAMES.includes(n));
|
|
422
557
|
const priceMatches = tierPrices.filter((p) => DEFAULT_PRICING_AMOUNTS.includes(p));
|
|
423
558
|
|
|
424
|
-
// Reason:
|
|
425
|
-
|
|
559
|
+
// Reason: only warn when pricing did NOT come from docs or AI (provenance-based check)
|
|
560
|
+
const pricingSource = context.pricingDiagnostics?.source;
|
|
561
|
+
if (
|
|
562
|
+
nameMatches.length >= 2 && priceMatches.length >= 2
|
|
563
|
+
&& pricingSource !== 'docs' && pricingSource !== 'ai'
|
|
564
|
+
) {
|
|
426
565
|
warnings.push(
|
|
427
566
|
'Pricing tiers appear to use default values (Starter/Pro/Enterprise at $0/$29/Custom). ' +
|
|
428
567
|
'Add a pricing section to your docs for accurate tiers.'
|
|
@@ -23,6 +23,15 @@ export interface WebsiteDebugTrace {
|
|
|
23
23
|
dataSource: 'strategy' | 'docs' | 'defaults' | 'skipped';
|
|
24
24
|
itemCount: number;
|
|
25
25
|
}>;
|
|
26
|
+
/** Pricing extraction diagnostics */
|
|
27
|
+
pricingDiagnostics?: {
|
|
28
|
+
charsScanned: number;
|
|
29
|
+
foundPricingHeader: boolean;
|
|
30
|
+
extractionMethod: 'known_plan_names' | 'table_fallback' | 'ai' | 'none';
|
|
31
|
+
extractedTiers?: Array<{ name: string; price: string }>;
|
|
32
|
+
reasonIfEmpty?: string;
|
|
33
|
+
source: 'docs' | 'ai' | 'default' | 'none';
|
|
34
|
+
};
|
|
26
35
|
/** Validation result from quality gate */
|
|
27
36
|
validationPassed: boolean;
|
|
28
37
|
validationIssues: string[];
|
|
@@ -104,6 +113,23 @@ export function formatDebugTrace(trace: WebsiteDebugTrace): string {
|
|
|
104
113
|
}
|
|
105
114
|
lines.push('');
|
|
106
115
|
|
|
116
|
+
if (trace.pricingDiagnostics) {
|
|
117
|
+
lines.push('Pricing Diagnostics:');
|
|
118
|
+
lines.push(` Source: ${trace.pricingDiagnostics.source}`);
|
|
119
|
+
lines.push(` Method: ${trace.pricingDiagnostics.extractionMethod}`);
|
|
120
|
+
lines.push(` Chars Scanned: ${trace.pricingDiagnostics.charsScanned}`);
|
|
121
|
+
lines.push(` Found Header: ${trace.pricingDiagnostics.foundPricingHeader}`);
|
|
122
|
+
if (trace.pricingDiagnostics.extractedTiers?.length) {
|
|
123
|
+
for (const t of trace.pricingDiagnostics.extractedTiers) {
|
|
124
|
+
lines.push(` - ${t.name}: ${t.price}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (trace.pricingDiagnostics.reasonIfEmpty) {
|
|
128
|
+
lines.push(` Reason empty: ${trace.pricingDiagnostics.reasonIfEmpty}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
lines.push(`Validation: ${trace.validationPassed ? 'PASSED' : 'FAILED'}`);
|
|
108
134
|
if (trace.validationIssues.length > 0) {
|
|
109
135
|
for (const issue of trace.validationIssues) {
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
generateWebsiteTest,
|
|
17
17
|
generateWebsiteDocsPage,
|
|
18
18
|
generateWebsiteBlogPage,
|
|
19
|
+
generateWebsiteContactPage,
|
|
20
|
+
generateWebsitePrivacyPage,
|
|
21
|
+
generateWebsiteTermsPage,
|
|
19
22
|
} from './templates/website.js';
|
|
20
23
|
import {
|
|
21
24
|
generateWebsitePackageJson,
|
|
@@ -134,6 +137,9 @@ export async function generateWebsiteProject(
|
|
|
134
137
|
await ensureDir(path.join(projectDir, 'src', 'app', 'pricing'));
|
|
135
138
|
await ensureDir(path.join(projectDir, 'src', 'app', 'docs'));
|
|
136
139
|
await ensureDir(path.join(projectDir, 'src', 'app', 'blog'));
|
|
140
|
+
await ensureDir(path.join(projectDir, 'src', 'app', 'contact'));
|
|
141
|
+
await ensureDir(path.join(projectDir, 'src', 'app', 'privacy'));
|
|
142
|
+
await ensureDir(path.join(projectDir, 'src', 'app', 'terms'));
|
|
137
143
|
await ensureDir(path.join(projectDir, 'src', 'app', 'api', 'lead'));
|
|
138
144
|
await ensureDir(path.join(projectDir, 'src', 'components'));
|
|
139
145
|
await ensureDir(path.join(projectDir, 'src', 'lib'));
|
|
@@ -187,6 +193,7 @@ export async function generateWebsiteProject(
|
|
|
187
193
|
pricingTiers: contentContext.pricing?.length || 0,
|
|
188
194
|
},
|
|
189
195
|
sectionsRendered: [],
|
|
196
|
+
pricingDiagnostics: contentContext.pricingDiagnostics,
|
|
190
197
|
validationPassed: true,
|
|
191
198
|
validationIssues: [],
|
|
192
199
|
};
|
|
@@ -206,7 +213,10 @@ export async function generateWebsiteProject(
|
|
|
206
213
|
},
|
|
207
214
|
{
|
|
208
215
|
path: path.join(projectDir, 'tsconfig.json'),
|
|
209
|
-
content: generateWebsiteTsconfig(
|
|
216
|
+
content: generateWebsiteTsconfig({
|
|
217
|
+
workspaceMode,
|
|
218
|
+
projectName: workspaceMode ? projectName : undefined,
|
|
219
|
+
}),
|
|
210
220
|
},
|
|
211
221
|
{
|
|
212
222
|
path: path.join(projectDir, 'tailwind.config.ts'),
|
|
@@ -248,11 +258,23 @@ export async function generateWebsiteProject(
|
|
|
248
258
|
},
|
|
249
259
|
{
|
|
250
260
|
path: path.join(projectDir, 'src', 'app', 'docs', 'page.tsx'),
|
|
251
|
-
content: generateWebsiteDocsPage(),
|
|
261
|
+
content: generateWebsiteDocsPage(projectName, contentContext),
|
|
252
262
|
},
|
|
253
263
|
{
|
|
254
264
|
path: path.join(projectDir, 'src', 'app', 'blog', 'page.tsx'),
|
|
255
|
-
content: generateWebsiteBlogPage(),
|
|
265
|
+
content: generateWebsiteBlogPage(projectName, contentContext),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
path: path.join(projectDir, 'src', 'app', 'contact', 'page.tsx'),
|
|
269
|
+
content: generateWebsiteContactPage(projectName, contentContext),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
path: path.join(projectDir, 'src', 'app', 'privacy', 'page.tsx'),
|
|
273
|
+
content: generateWebsitePrivacyPage(projectName, contentContext),
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
path: path.join(projectDir, 'src', 'app', 'terms', 'page.tsx'),
|
|
277
|
+
content: generateWebsiteTermsPage(projectName, contentContext),
|
|
256
278
|
},
|
|
257
279
|
|
|
258
280
|
// Shared components
|
|
@@ -462,6 +484,9 @@ export function getWebsiteProjectFiles(_projectName: string): string[] {
|
|
|
462
484
|
'src/app/pricing/page.tsx',
|
|
463
485
|
'src/app/docs/page.tsx',
|
|
464
486
|
'src/app/blog/page.tsx',
|
|
487
|
+
'src/app/contact/page.tsx',
|
|
488
|
+
'src/app/privacy/page.tsx',
|
|
489
|
+
'src/app/terms/page.tsx',
|
|
465
490
|
'src/app/sitemap.ts',
|
|
466
491
|
'src/app/robots.ts',
|
|
467
492
|
// Tests
|