@nexus-ai-nexoralabs/cli 0.1.3 → 0.1.4
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/.env.example +16 -16
- package/LICENSE +21 -21
- package/README.md +135 -106
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +9 -3
- package/dist/api/index.js.map +1 -1
- package/dist/chat/chatService.d.ts.map +1 -1
- package/dist/chat/chatService.js +22 -3
- package/dist/chat/chatService.js.map +1 -1
- package/dist/chat/conversationService.d.ts +7 -1
- package/dist/chat/conversationService.d.ts.map +1 -1
- package/dist/chat/conversationService.js +48 -15
- package/dist/chat/conversationService.js.map +1 -1
- package/dist/cli/agentRunner.d.ts.map +1 -1
- package/dist/cli/agentRunner.js +88 -8
- package/dist/cli/agentRunner.js.map +1 -1
- package/dist/cli/chatRunner.d.ts +65 -0
- package/dist/cli/chatRunner.d.ts.map +1 -0
- package/dist/cli/chatRunner.js +289 -0
- package/dist/cli/chatRunner.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inputEngine.d.ts +10 -0
- package/dist/cli/inputEngine.d.ts.map +1 -1
- package/dist/cli/inputEngine.js +28 -1
- package/dist/cli/inputEngine.js.map +1 -1
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +89 -25
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/uiFormatters.d.ts +11 -1
- package/dist/cli/uiFormatters.d.ts.map +1 -1
- package/dist/cli/uiFormatters.js +253 -15
- package/dist/cli/uiFormatters.js.map +1 -1
- package/dist/intelligence/modelGateway.d.ts +1 -0
- package/dist/intelligence/modelGateway.d.ts.map +1 -1
- package/dist/intelligence/modelGateway.js +63 -6
- package/dist/intelligence/modelGateway.js.map +1 -1
- package/dist/intelligence/modelRegistry.d.ts +2 -2
- package/dist/intelligence/modelRegistry.d.ts.map +1 -1
- package/dist/intelligence/modelRegistry.js +72 -31
- package/dist/intelligence/modelRegistry.js.map +1 -1
- package/dist/intelligence/modelRouter.d.ts.map +1 -1
- package/dist/intelligence/modelRouter.js +62 -16
- package/dist/intelligence/modelRouter.js.map +1 -1
- package/dist/intelligence/providers/embeddedInferenceProvider.d.ts.map +1 -1
- package/dist/intelligence/providers/embeddedInferenceProvider.js +2224 -6
- package/dist/intelligence/providers/embeddedInferenceProvider.js.map +1 -1
- package/dist/intelligence/runtime/runtimeManager.d.ts +5 -0
- package/dist/intelligence/runtime/runtimeManager.d.ts.map +1 -1
- package/dist/intelligence/runtime/runtimeManager.js +64 -4
- package/dist/intelligence/runtime/runtimeManager.js.map +1 -1
- package/dist/orchestration/agentExecutionService.d.ts.map +1 -1
- package/dist/orchestration/agentExecutionService.js +22 -0
- package/dist/orchestration/agentExecutionService.js.map +1 -1
- package/dist/orchestration/agentTaskService.d.ts +24 -10
- package/dist/orchestration/agentTaskService.d.ts.map +1 -1
- package/dist/orchestration/agentTaskService.js +76 -13
- package/dist/orchestration/agentTaskService.js.map +1 -1
- package/dist/orchestration/codingAgentService.d.ts.map +1 -1
- package/dist/orchestration/codingAgentService.js +2 -0
- package/dist/orchestration/codingAgentService.js.map +1 -1
- package/dist/orchestration/goalCompletionVerifier.d.ts +9 -0
- package/dist/orchestration/goalCompletionVerifier.d.ts.map +1 -1
- package/dist/orchestration/goalCompletionVerifier.js +131 -9
- package/dist/orchestration/goalCompletionVerifier.js.map +1 -1
- package/dist/orchestration/index.d.ts +16 -2
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +20 -4
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/placeholderDetector.d.ts +74 -0
- package/dist/orchestration/placeholderDetector.d.ts.map +1 -0
- package/dist/orchestration/placeholderDetector.js +741 -0
- package/dist/orchestration/placeholderDetector.js.map +1 -0
- package/dist/orchestration/planner.d.ts.map +1 -1
- package/dist/orchestration/planner.js +94 -55
- package/dist/orchestration/planner.js.map +1 -1
- package/dist/orchestration/projectOrchestrator.d.ts +63 -0
- package/dist/orchestration/projectOrchestrator.d.ts.map +1 -0
- package/dist/orchestration/projectOrchestrator.js +555 -0
- package/dist/orchestration/projectOrchestrator.js.map +1 -0
- package/dist/orchestration/taskDecompositionService.d.ts +36 -0
- package/dist/orchestration/taskDecompositionService.d.ts.map +1 -0
- package/dist/orchestration/taskDecompositionService.js +367 -0
- package/dist/orchestration/taskDecompositionService.js.map +1 -0
- package/dist/orchestration/taskDecompositionTypes.d.ts +62 -0
- package/dist/orchestration/taskDecompositionTypes.d.ts.map +1 -0
- package/dist/orchestration/taskDecompositionTypes.js +5 -0
- package/dist/orchestration/taskDecompositionTypes.js.map +1 -0
- package/dist/preview/previewServer.d.ts.map +1 -1
- package/dist/preview/previewServer.js +28 -7
- package/dist/preview/previewServer.js.map +1 -1
- package/dist/preview/previewService.d.ts +32 -3
- package/dist/preview/previewService.d.ts.map +1 -1
- package/dist/preview/previewService.js +370 -108
- package/dist/preview/previewService.js.map +1 -1
- package/dist/preview/previewTypes.d.ts +1 -1
- package/dist/preview/previewTypes.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/migration.d.ts.map +1 -1
- package/dist/storage/migration.js +79 -0
- package/dist/storage/migration.js.map +1 -1
- package/dist/storage/repositories/agentTaskRepository.d.ts +13 -0
- package/dist/storage/repositories/agentTaskRepository.d.ts.map +1 -1
- package/dist/storage/repositories/agentTaskRepository.js +87 -25
- package/dist/storage/repositories/agentTaskRepository.js.map +1 -1
- package/dist/storage/repositories/conversationRepository.d.ts +5 -1
- package/dist/storage/repositories/conversationRepository.d.ts.map +1 -1
- package/dist/storage/repositories/conversationRepository.js +69 -0
- package/dist/storage/repositories/conversationRepository.js.map +1 -1
- package/dist/storage/repositories/messageRepository.d.ts +3 -0
- package/dist/storage/repositories/messageRepository.d.ts.map +1 -1
- package/dist/storage/repositories/messageRepository.js +29 -0
- package/dist/storage/repositories/messageRepository.js.map +1 -1
- package/dist/storage/repositories/taskArtifactRepository.d.ts +19 -0
- package/dist/storage/repositories/taskArtifactRepository.d.ts.map +1 -0
- package/dist/storage/repositories/taskArtifactRepository.js +82 -0
- package/dist/storage/repositories/taskArtifactRepository.js.map +1 -0
- package/dist/storage/repositories/types.d.ts +32 -0
- package/dist/storage/repositories/types.d.ts.map +1 -1
- package/dist/tools/adapters/filesystemAdapter.d.ts.map +1 -1
- package/dist/tools/adapters/filesystemAdapter.js +8 -1
- package/dist/tools/adapters/filesystemAdapter.js.map +1 -1
- package/dist/ui/public/app.js +777 -777
- package/dist/ui/public/index.html +117 -117
- package/dist/ui/public/styles.css +413 -413
- package/package.json +64 -63
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEXUS AI — Production-Grade Content Substance & Placeholder Detector
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic, reusable AST & textual analysis over generated web artifacts.
|
|
5
|
+
* Prevents empty skeletons, comment-only containers, unresolved generation tokens,
|
|
6
|
+
* and generic fallback templates from masquerading as successful task completions.
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// 1. Text & HTML Sanitization Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Extracts pure visible text rendered to the user from HTML source.
|
|
13
|
+
* Strips scripts, styles, noscript, svg, comments, and all HTML tags.
|
|
14
|
+
*/
|
|
15
|
+
export function extractVisibleText(htmlContent) {
|
|
16
|
+
if (!htmlContent)
|
|
17
|
+
return '';
|
|
18
|
+
return htmlContent
|
|
19
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ' ')
|
|
20
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ' ')
|
|
21
|
+
.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, ' ')
|
|
22
|
+
.replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, ' ')
|
|
23
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
24
|
+
.replace(/<[^>]+>/g, ' ')
|
|
25
|
+
.replace(/ /gi, ' ')
|
|
26
|
+
.replace(/&/gi, '&')
|
|
27
|
+
.replace(/</gi, '<')
|
|
28
|
+
.replace(/>/gi, '>')
|
|
29
|
+
.replace(/"/gi, '"')
|
|
30
|
+
.replace(/'/gi, "'")
|
|
31
|
+
.replace(/©/gi, '©')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// 2. Unresolved Placeholder & Token Detector
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Detects unresolved placeholder comments, TODO/TBD markers, and generic templates.
|
|
40
|
+
*/
|
|
41
|
+
export function detectPlaceholders(htmlContent, taskGoal) {
|
|
42
|
+
const reasons = [];
|
|
43
|
+
const matches = [];
|
|
44
|
+
const lowerGoal = (taskGoal || '').toLowerCase();
|
|
45
|
+
// Pattern 1: Placeholder comments like <!-- Hero Content -->, <!-- Features Content -->, <!-- Content -->, <!-- TODO -->
|
|
46
|
+
const commentPlaceholderRegex = /<!--\s*([a-zA-Z0-9_\s-]*?(?:\bcontent\b|\btodo\b|\btbd\b|\bplaceholder\b|insert\s+\w+\s+here|your\s+content)[a-zA-Z0-9_\s-]*?)\s*-->/gi;
|
|
47
|
+
let commentMatch;
|
|
48
|
+
while ((commentMatch = commentPlaceholderRegex.exec(htmlContent)) !== null) {
|
|
49
|
+
const matchedText = commentMatch[0].trim();
|
|
50
|
+
matches.push(matchedText);
|
|
51
|
+
reasons.push(`Unresolved placeholder comment detected: "${matchedText}"`);
|
|
52
|
+
}
|
|
53
|
+
// Pattern 2: Explicit placeholder text tokens inside visible body text
|
|
54
|
+
const bodyMatch = htmlContent.match(/<body\b[^>]*>([\s\S]*?)<\/body>/i);
|
|
55
|
+
const bodyHtml = bodyMatch ? bodyMatch[1] : htmlContent;
|
|
56
|
+
const visibleBodyText = extractVisibleText(bodyHtml);
|
|
57
|
+
const textTokenPatterns = [
|
|
58
|
+
{ regex: /\bTODO\b/i, name: 'TODO token', target: 'visible' },
|
|
59
|
+
{ regex: /\bTBD\b/i, name: 'TBD token', target: 'visible' },
|
|
60
|
+
{ regex: /\bPLACEHOLDER\b/i, name: 'PLACEHOLDER token', target: 'visible' },
|
|
61
|
+
{ regex: /\bYOUR\s+CONTENT\s+HERE\b/i, name: 'YOUR CONTENT HERE token', target: 'visible' },
|
|
62
|
+
{ regex: /\bINSERT\s+[\w\s]+\s+HERE\b/i, name: 'INSERT ... HERE token', target: 'visible' },
|
|
63
|
+
{ regex: /\bREPLACE\s+WITH\s+[\w\s]+\b/i, name: 'REPLACE WITH ... token', target: 'visible' },
|
|
64
|
+
{ regex: /\bLorem\s+ipsum\s+dolor\b/i, name: 'Lorem ipsum placeholder text', target: 'visible' },
|
|
65
|
+
{
|
|
66
|
+
regex: /Generated\s+by\s+NEXUS\s+AI\s+Agent/i,
|
|
67
|
+
name: 'Generic fallback marker "Generated by NEXUS AI Agent"',
|
|
68
|
+
target: 'raw',
|
|
69
|
+
allowIfInGoal: lowerGoal.includes('nexus ai agent') || lowerGoal.includes('nexus web application'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
regex: /NEXUS\s+Web\s+Application/i,
|
|
73
|
+
name: 'Generic fallback title "NEXUS Web Application"',
|
|
74
|
+
target: 'raw',
|
|
75
|
+
allowIfInGoal: lowerGoal.includes('nexus web application'),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
for (const { regex, name, target, allowIfInGoal } of textTokenPatterns) {
|
|
79
|
+
if (allowIfInGoal)
|
|
80
|
+
continue;
|
|
81
|
+
const searchTarget = target === 'visible' ? visibleBodyText : htmlContent;
|
|
82
|
+
const match = searchTarget.match(regex);
|
|
83
|
+
if (match) {
|
|
84
|
+
matches.push(match[0]);
|
|
85
|
+
reasons.push(`Unresolved generation marker detected: ${name} ("${match[0]}")`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
hasPlaceholders: matches.length > 0,
|
|
90
|
+
reasons,
|
|
91
|
+
placeholderMatches: matches,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// 3. Structural Skeleton & Empty Container Detector
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Analyzes semantic container elements (<section>, <header>, <nav>, <footer>, <main>, <article>, and major section <div>s)
|
|
99
|
+
* to detect empty shells or comment-only skeletons.
|
|
100
|
+
*/
|
|
101
|
+
export function detectSkeletonContainers(htmlContent) {
|
|
102
|
+
const reasons = [];
|
|
103
|
+
const skeletonSectionNames = [];
|
|
104
|
+
// Decorative element patterns that are legitimately empty in modern CSS (glows, overlays, bars, etc.)
|
|
105
|
+
const decorativePattern = /(?:bg-glow|bg-grid|overlay|backdrop|particle|spacer|divider|\bbar\b|toggle-bar|separator|decoration|blur|halo|beam|mesh|glow|circle|dot|blob|star|spinner)/i;
|
|
106
|
+
// 1. Check all <section ...>...</section> tags (sections should never be empty or comment-only in complete projects)
|
|
107
|
+
const sectionRegex = /<section\b([^>]*)>([\s\S]*?)<\/section>/gi;
|
|
108
|
+
let totalSections = 0;
|
|
109
|
+
let emptyOrCommentOnlySections = 0;
|
|
110
|
+
let sMatch;
|
|
111
|
+
while ((sMatch = sectionRegex.exec(htmlContent)) !== null) {
|
|
112
|
+
totalSections++;
|
|
113
|
+
const attrs = sMatch[1];
|
|
114
|
+
const innerContent = sMatch[2] || '';
|
|
115
|
+
const idMatch = attrs.match(/id=["']([^"']+)["']/i);
|
|
116
|
+
const classMatch = attrs.match(/class=["']([^"']+)["']/i);
|
|
117
|
+
const identifier = idMatch
|
|
118
|
+
? `#${idMatch[1]}`
|
|
119
|
+
: classMatch
|
|
120
|
+
? `.${classMatch[1].split(/\s+/)[0]}`
|
|
121
|
+
: `section[${totalSections}]`;
|
|
122
|
+
const trimmedInner = innerContent.trim();
|
|
123
|
+
const visibleText = extractVisibleText(innerContent);
|
|
124
|
+
const isCommentOnly = trimmedInner.length > 0 && /^(\s*<!--[\s\S]*?-->\s*)+$/i.test(trimmedInner);
|
|
125
|
+
const isEmpty = trimmedInner.length === 0;
|
|
126
|
+
const hasVisualElements = /<(?:img|svg|input|button|canvas|video|audio|iframe|form|ul|ol|table|h[1-6]|p)\b/i.test(innerContent);
|
|
127
|
+
if (isCommentOnly || (isEmpty && !decorativePattern.test(identifier)) || (visibleText.length === 0 && !hasVisualElements && !decorativePattern.test(identifier))) {
|
|
128
|
+
emptyOrCommentOnlySections++;
|
|
129
|
+
skeletonSectionNames.push(identifier);
|
|
130
|
+
if (isCommentOnly) {
|
|
131
|
+
reasons.push(`Section <${identifier}> contains only HTML comments without real implementation.`);
|
|
132
|
+
}
|
|
133
|
+
else if (isEmpty) {
|
|
134
|
+
reasons.push(`Section <${identifier}> is completely empty.`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
reasons.push(`Section <${identifier}> contains no visible text or UI elements.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 2. Check major content <div id="..." or class="..."> containing comment-only placeholders like <!-- Hero Content -->
|
|
142
|
+
const divCommentRegex = /<div\b[^>]*?(?:id|class)=["']([^"']+)["'][^>]*>\s*<!--\s*([a-zA-Z0-9_\s-]+?)\s*(?:Content|Section|Placeholder)?\s*-->\s*<\/div>/gi;
|
|
143
|
+
let dMatch;
|
|
144
|
+
while ((dMatch = divCommentRegex.exec(htmlContent)) !== null) {
|
|
145
|
+
const divIdOrClass = dMatch[1];
|
|
146
|
+
const placeholderText = dMatch[2];
|
|
147
|
+
const identifier = `div.${divIdOrClass}`;
|
|
148
|
+
if (!skeletonSectionNames.includes(identifier)) {
|
|
149
|
+
skeletonSectionNames.push(identifier);
|
|
150
|
+
emptyOrCommentOnlySections++;
|
|
151
|
+
reasons.push(`Container <${identifier}> contains only placeholder comment '<!-- ${placeholderText} -->'`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 3. Determine if document is an incomplete skeleton
|
|
155
|
+
const totalContainers = totalSections + (skeletonSectionNames.length - emptyOrCommentOnlySections);
|
|
156
|
+
const isSkeleton = skeletonSectionNames.length > 0 &&
|
|
157
|
+
(emptyOrCommentOnlySections >= 2 ||
|
|
158
|
+
(totalSections > 0 && emptyOrCommentOnlySections / totalSections >= 0.3) ||
|
|
159
|
+
reasons.some((r) => r.includes('contains only HTML comments') || r.includes('placeholder comment')));
|
|
160
|
+
return {
|
|
161
|
+
isSkeleton,
|
|
162
|
+
totalContainers: Math.max(totalContainers, totalSections),
|
|
163
|
+
emptyOrCommentOnlyContainers: emptyOrCommentOnlySections,
|
|
164
|
+
skeletonSectionNames,
|
|
165
|
+
reasons,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// 4. Request-Aware Section & Brand Substance Verifier
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
/**
|
|
172
|
+
* Extracts candidate brand / proper project names from the task goal.
|
|
173
|
+
*/
|
|
174
|
+
export function extractBrandCandidates(taskGoal) {
|
|
175
|
+
const stopWords = new Set([
|
|
176
|
+
'a', 'an', 'the', 'and', 'or', 'for', 'with', 'to', 'of', 'in', 'on', 'at', 'by', 'as', 'is', 'it',
|
|
177
|
+
'build', 'create', 'make', 'generate', 'implement', 'write', 'complete', 'premium',
|
|
178
|
+
'modern', 'multi-section', 'landing', 'website', 'webpage', 'page', 'fictional',
|
|
179
|
+
'technology', 'company', 'saas', 'app', 'application', 'site', 'dark', 'aesthetic',
|
|
180
|
+
'responsive', 'include', 'including', 'navbar', 'hero', 'features', 'solutions',
|
|
181
|
+
'dashboard', 'testimonials', 'pricing', 'faq', 'cta', 'footer', 'using', 'html',
|
|
182
|
+
'css', 'js', 'javascript', 'typescript', 'folder', 'directory', 'new', 'task', 'test',
|
|
183
|
+
'project', 'demo', 'sample', 'simple', 'basic', 'web', 'file', 'files', 'frontend',
|
|
184
|
+
'backend', 'local', 'preview', 'server', 'code', 'data', 'database', 'system', 'user',
|
|
185
|
+
'run', 'first', 'second', 'third', 'interface', 'ui', 'ux', 'card', 'cards', 'menu',
|
|
186
|
+
'about', 'contact', 'services', 'service', 'home', 'main', 'header', 'section', 'sections',
|
|
187
|
+
'start building', 'explore platform', 'get started', 'learn more', 'click here',
|
|
188
|
+
'sign up', 'sign in', 'log in', 'login', 'read more', 'buy now', 'contact us',
|
|
189
|
+
'submit', 'subscribe', 'try for free', 'book a demo', 'request demo', 'view all',
|
|
190
|
+
'see more', 'explore more', 'get in touch',
|
|
191
|
+
'generated by nexus ai', 'generated by nexus ai agent', 'nexus ai', 'nexus ai agent',
|
|
192
|
+
'nexus web application', 'nexus'
|
|
193
|
+
]);
|
|
194
|
+
const candidates = [];
|
|
195
|
+
// 0. Explicit Project Brand directive in prompt
|
|
196
|
+
const explicitDirectiveMatch = taskGoal.match(/Project Brand \/ Identity:\s*([^\n]+)/i);
|
|
197
|
+
if (explicitDirectiveMatch && explicitDirectiveMatch[1]) {
|
|
198
|
+
const brand = explicitDirectiveMatch[1].trim();
|
|
199
|
+
if (brand && !stopWords.has(brand.toLowerCase())) {
|
|
200
|
+
candidates.push(brand);
|
|
201
|
+
return candidates;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// 1. Explicit brand declaration patterns (e.g. called NOVARA AI, company called Bean & Brew, for DevForge)
|
|
205
|
+
const namePatterns = [
|
|
206
|
+
/(?:called|named|company called|brand called|brand named|for a fictional technology company called|for a fictional ai company called|for an ai company called|for a cafe called|for a company called|for a startup called)\s+[`"']?([a-zA-Z0-9_& -]+?)[`"']?(?:\.|\s+with|\s+that|\s+including|\s+requirements|\s+featuring|\s+and|\s+in\s+a|\s+to|$)/i,
|
|
207
|
+
/(?:portfolio|landing page|website|web app|app|application)\s+for\s+[`"']?([a-zA-Z0-9_& -]+?)[`"']?(?:\.|\s+with|\s+that|\s+including|\s+requirements|\s+featuring|\s+and|\s+in\s+a|\s+to|$)/i,
|
|
208
|
+
/\bfor\s+[`"']?([A-Z][a-zA-Z0-9_&]+(?:\s+[A-Z][a-zA-Z0-9_&]+)*)[`"']?(?:\.|\s+with|\s+that|\s+including|\s+requirements|\s+featuring|\s+and|\s+in\s+a|\s+to|$)/,
|
|
209
|
+
];
|
|
210
|
+
for (const pat of namePatterns) {
|
|
211
|
+
const match = taskGoal.match(pat);
|
|
212
|
+
if (match && match[1]) {
|
|
213
|
+
let val = match[1].trim();
|
|
214
|
+
val = val.replace(/^(?:a|an|the)\s+/i, '').trim();
|
|
215
|
+
if (val.length >= 2 && !stopWords.has(val.toLowerCase()) && !candidates.includes(val)) {
|
|
216
|
+
candidates.push(val);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// If explicit brand names were identified, return them
|
|
221
|
+
if (candidates.length > 0) {
|
|
222
|
+
return candidates;
|
|
223
|
+
}
|
|
224
|
+
// 2. Match quoted strings e.g. "NEXORA", "Bean & Brew"
|
|
225
|
+
const quotesMatch = taskGoal.match(/"([^"]+)"|'([^']+)'|“([^”]+)”/g);
|
|
226
|
+
if (quotesMatch) {
|
|
227
|
+
for (const q of quotesMatch) {
|
|
228
|
+
const clean = q.replace(/["'“”]/g, '').trim();
|
|
229
|
+
if (clean.length >= 2 && !stopWords.has(clean.toLowerCase()) && !candidates.includes(clean)) {
|
|
230
|
+
candidates.push(clean);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (candidates.length > 0) {
|
|
235
|
+
return candidates;
|
|
236
|
+
}
|
|
237
|
+
// 3. Match multi-word capitalized brands or acronyms (e.g., NOVARA AI, NEXORA)
|
|
238
|
+
const multiCapMatch = taskGoal.match(/\b([A-Z][A-Z0-9_-]+(?:\s+[A-Z][A-Z0-9_-]+)+)\b/g);
|
|
239
|
+
if (multiCapMatch) {
|
|
240
|
+
for (const phrase of multiCapMatch) {
|
|
241
|
+
const words = phrase.toLowerCase().split(/\s+/);
|
|
242
|
+
const isAllStopWords = words.every((w) => stopWords.has(w) || /^(html|css|http|url|json|api|cta|faq|todo|tbd|dom|nexus|app|ui|ux|js|ts|task)$/i.test(w));
|
|
243
|
+
if (!isAllStopWords && !candidates.includes(phrase)) {
|
|
244
|
+
candidates.push(phrase);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (candidates.length > 0) {
|
|
249
|
+
return candidates;
|
|
250
|
+
}
|
|
251
|
+
// 4. Match single ALL-CAPS acronyms / distinct brand names (e.g., NEXORA, NOVARA)
|
|
252
|
+
const allCapsWords = taskGoal.match(/\b[A-Z0-9_-]{3,}\b/g);
|
|
253
|
+
if (allCapsWords) {
|
|
254
|
+
for (const w of allCapsWords) {
|
|
255
|
+
if (!stopWords.has(w.toLowerCase()) && !candidates.includes(w) && !/^(HTML|CSS|HTTP|URL|JSON|API|CTA|FAQ|TODO|TBD|DOM|NEXUS)$/i.test(w)) {
|
|
256
|
+
candidates.push(w);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return candidates;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Extracts requested hero CTA/button or headline terms from parentGoal or unit.
|
|
264
|
+
*/
|
|
265
|
+
export function extractHeroRequestedTerms(parentGoal, unit) {
|
|
266
|
+
const terms = [];
|
|
267
|
+
const unitText = unit ? `${unit.title} ${unit.description} ${unit.expectedOutput} ${unit.verificationCriteria.join(' ')}` : '';
|
|
268
|
+
const combined = `${parentGoal} ${unitText}`;
|
|
269
|
+
// Find quotes near hero / CTA keywords
|
|
270
|
+
const heroMatch = combined.match(/(?:hero|headline|cta\s+buttons?|banner)[^.]*?(?:buttons?|titled|with|saying)?\s*([“"'][^"']+?[”"'](?:\s+and\s+[“"'][^"']+?[”"'])*)/i);
|
|
271
|
+
if (heroMatch && heroMatch[1]) {
|
|
272
|
+
const qMatches = heroMatch[1].match(/["'“”]([^"'“”]+)["'“”]/g);
|
|
273
|
+
if (qMatches) {
|
|
274
|
+
for (const q of qMatches) {
|
|
275
|
+
const clean = q.replace(/["'“”]/g, '').trim();
|
|
276
|
+
if (clean.length >= 2 && !terms.includes(clean)) {
|
|
277
|
+
terms.push(clean);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Also check if unit verification criteria explicitly specifies a quoted term
|
|
283
|
+
if (unit) {
|
|
284
|
+
for (const crit of unit.verificationCriteria) {
|
|
285
|
+
const critQuotes = crit.match(/["'“]([^"'“”]+)["'”]/g);
|
|
286
|
+
if (critQuotes) {
|
|
287
|
+
for (const q of critQuotes) {
|
|
288
|
+
const clean = q.replace(/["'“”]/g, '').trim();
|
|
289
|
+
if (clean.length >= 2 && !terms.includes(clean)) {
|
|
290
|
+
terms.push(clean);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return terms;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Extracts requested section keys from user task goal using regex boundary matching.
|
|
300
|
+
*/
|
|
301
|
+
export function extractRequestedSections(taskGoal) {
|
|
302
|
+
const knownSections = [
|
|
303
|
+
{ key: 'navbar', label: 'Navbar / Navigation', regex: /\b(?:navbar|navigation\s+bar|nav\s+bar)\b/i },
|
|
304
|
+
{ key: 'hero', label: 'Hero Section', regex: /\b(?:hero|headline)\b/i },
|
|
305
|
+
{ key: 'feature', label: 'Features Section', regex: /\b(?:features?|capabilities)\b/i },
|
|
306
|
+
{ key: 'solution', label: 'Solutions Section', regex: /\b(?:solutions?)\b/i },
|
|
307
|
+
{ key: 'about', label: 'About Section', regex: /\b(?:about\s+us|about\s+section|\babout\b)/i },
|
|
308
|
+
{ key: 'dashboard', label: 'Product Dashboard', regex: /\b(?:dashboard|platform\s+mockup|showcase)\b/i },
|
|
309
|
+
{ key: 'process', label: 'Process / How It Works', regex: /\b(?:how\s+it\s+works|process|3-step)\b/i },
|
|
310
|
+
{ key: 'stat', label: 'Statistics / Metrics', regex: /\b(?:statistics|stats|metrics)\b/i },
|
|
311
|
+
{ key: 'testimonial', label: 'Testimonials / Reviews', regex: /\b(?:testimonials?|reviews?)\b/i },
|
|
312
|
+
{ key: 'pricing', label: 'Pricing Section', regex: /\b(?:pricing|subscription\s+tier|plans?)\b/i },
|
|
313
|
+
{ key: 'faq', label: 'FAQ Accordion', regex: /\b(?:faq|frequently\s+asked|accordion)\b/i },
|
|
314
|
+
{ key: 'cta', label: 'Call To Action', regex: /\b(?:cta|call\s+to\s+action)\b/i },
|
|
315
|
+
{ key: 'footer', label: 'Footer', regex: /\b(?:footer)\b/i },
|
|
316
|
+
{ key: 'menu', label: 'Menu Section', regex: /\b(?:menu\s+grid|food\s+menu|drink\s+menu|restaurant\s+menu|our\s+specialties|\bmenu\b(?!.*?\b(?:toggle|mobile|bar)\b))\b/i },
|
|
317
|
+
{ key: 'project', label: 'Projects Section', regex: /\b(?:projects?|featured\s+projects?)\b/i },
|
|
318
|
+
{ key: 'skill', label: 'Skills Section', regex: /\b(?:skills?)\b/i },
|
|
319
|
+
{ key: 'contact', label: 'Contact Section', regex: /\b(?:contact\s+us|contact\s+section|\bcontact\b)/i },
|
|
320
|
+
];
|
|
321
|
+
return knownSections.filter((s) => s.regex.test(taskGoal));
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Performs complete, multi-stage content substance and integrity verification
|
|
325
|
+
* on generated HTML content given the original user task goal.
|
|
326
|
+
*/
|
|
327
|
+
export function verifyHtmlContentSubstance(htmlContent, taskGoal) {
|
|
328
|
+
if (!htmlContent || !htmlContent.trim()) {
|
|
329
|
+
return {
|
|
330
|
+
valid: false,
|
|
331
|
+
errorCategory: 'EMPTY_ARTIFACT',
|
|
332
|
+
reason: 'HTML artifact is empty (0 bytes).',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const lowerHtml = htmlContent.toLowerCase();
|
|
336
|
+
const lowerGoal = taskGoal.toLowerCase();
|
|
337
|
+
// ── CHECK 1: Non-Empty HTML Body ──────────────────────────────────────────
|
|
338
|
+
const bodyMatch = htmlContent.match(/<body\b[^>]*>([\s\S]*?)<\/body>/i);
|
|
339
|
+
const bodyHtml = bodyMatch ? bodyMatch[1] : htmlContent;
|
|
340
|
+
if (!bodyHtml || !bodyHtml.trim()) {
|
|
341
|
+
return {
|
|
342
|
+
valid: false,
|
|
343
|
+
errorCategory: 'EMPTY_HTML_BODY',
|
|
344
|
+
reason: 'HTML artifact does not contain a non-empty <body> element.',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// ── CHECK 2: Unresolved Generation Placeholders ───────────────────────────
|
|
348
|
+
const placeholders = detectPlaceholders(htmlContent, taskGoal);
|
|
349
|
+
if (placeholders.hasPlaceholders) {
|
|
350
|
+
return {
|
|
351
|
+
valid: false,
|
|
352
|
+
errorCategory: 'PLACEHOLDER_ARTIFACT_REJECTED',
|
|
353
|
+
reason: `HTML artifact contains unresolved placeholders: ${placeholders.reasons.join('; ')}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// ── CHECK 3: Empty Containers & Comment-Only Skeletons ─────────────────────
|
|
357
|
+
const skeletonCheck = detectSkeletonContainers(htmlContent);
|
|
358
|
+
if (skeletonCheck.isSkeleton) {
|
|
359
|
+
return {
|
|
360
|
+
valid: false,
|
|
361
|
+
errorCategory: 'INCOMPLETE_SKELETON_REJECTED',
|
|
362
|
+
skeletonSections: skeletonCheck.skeletonSectionNames,
|
|
363
|
+
reason: `HTML artifact contains incomplete skeleton containers: ${skeletonCheck.skeletonSectionNames.join(', ')}. ${skeletonCheck.reasons.join(' ')}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// ── CHECK 4: Visible Text Check (Non-Blank Body) ──────────────────────────
|
|
367
|
+
const visibleText = extractVisibleText(bodyHtml);
|
|
368
|
+
const hasVisualElements = /<(?:img|svg|input|button|canvas|form|video|audio|iframe)\b/i.test(bodyHtml);
|
|
369
|
+
if (visibleText.length === 0 && !hasVisualElements) {
|
|
370
|
+
return {
|
|
371
|
+
valid: false,
|
|
372
|
+
errorCategory: 'INSUFFICIENT_PROJECT_CONTENT',
|
|
373
|
+
visibleTextLength: 0,
|
|
374
|
+
reason: 'HTML artifact contains zero visible text. Page renders completely blank.',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// ── CHECK 5: Brand / Project Identity Evidence ────────────────────────────
|
|
378
|
+
const brandCandidates = extractBrandCandidates(taskGoal);
|
|
379
|
+
let detectedBrand;
|
|
380
|
+
let foundBrand = false;
|
|
381
|
+
const isCalculator = lowerGoal.includes('calculator');
|
|
382
|
+
if (brandCandidates.length > 0) {
|
|
383
|
+
for (const b of brandCandidates) {
|
|
384
|
+
const lowerB = b.toLowerCase();
|
|
385
|
+
const words = lowerB.split(/\s+/).filter((w) => w.length >= 3 && !['ai', 'inc', 'corp', 'app', 'ltd', 'co'].includes(w));
|
|
386
|
+
const hasWordMatch = words.length > 0 && words.every((w) => lowerHtml.includes(w));
|
|
387
|
+
if (lowerHtml.includes(lowerB) || hasWordMatch) {
|
|
388
|
+
foundBrand = true;
|
|
389
|
+
detectedBrand = b;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!foundBrand && !isCalculator) {
|
|
394
|
+
return {
|
|
395
|
+
valid: false,
|
|
396
|
+
errorCategory: 'CONTENT_VERIFICATION_FAILED',
|
|
397
|
+
reason: `HTML artifact is missing requested project brand/title terms corresponding to the task goal (${brandCandidates.join(', ')}).`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// ── CHECK 6: Request-Aware Section Content Substance ──────────────────────
|
|
402
|
+
const requestedSections = extractRequestedSections(taskGoal);
|
|
403
|
+
if (requestedSections.length >= 3) {
|
|
404
|
+
const missingSections = [];
|
|
405
|
+
const emptyRequestedSections = [];
|
|
406
|
+
for (const sec of requestedSections) {
|
|
407
|
+
let inner = '';
|
|
408
|
+
let foundContainer = false;
|
|
409
|
+
const idOrClassRegex = new RegExp(`<(section|footer|header|nav|main|article|div)\\b[^>]*?(?:id|class)=["'][^"']*?${sec.key}[^"']*?["'][^>]*>([\\s\\S]*?)<\\/\\1>`, 'i');
|
|
410
|
+
const match = idOrClassRegex.exec(htmlContent);
|
|
411
|
+
if (match) {
|
|
412
|
+
foundContainer = true;
|
|
413
|
+
inner = match[2] || '';
|
|
414
|
+
}
|
|
415
|
+
else if (sec.key === 'footer' || sec.key === 'navbar' || sec.key === 'nav') {
|
|
416
|
+
const tag = sec.key === 'footer' ? 'footer' : '(?:header|nav)';
|
|
417
|
+
const tagMatch = new RegExp(`<(${tag})\\b[^>]*>([\\s\\S]*?)<\\/\\1>`, 'i').exec(htmlContent);
|
|
418
|
+
if (tagMatch) {
|
|
419
|
+
foundContainer = true;
|
|
420
|
+
inner = tagMatch[2] || '';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (foundContainer) {
|
|
424
|
+
const innerVisible = extractVisibleText(inner);
|
|
425
|
+
const hasVisual = /<(?:img|svg|input|button|canvas|form|ul|ol|table)\b/i.test(inner);
|
|
426
|
+
if (innerVisible.length === 0 && !hasVisual) {
|
|
427
|
+
emptyRequestedSections.push(`${sec.label} (empty/placeholder)`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (!lowerHtml.includes(sec.key)) {
|
|
431
|
+
missingSections.push(sec.label);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (emptyRequestedSections.length > 0) {
|
|
435
|
+
return {
|
|
436
|
+
valid: false,
|
|
437
|
+
errorCategory: 'INCOMPLETE_SKELETON_REJECTED',
|
|
438
|
+
skeletonSections: emptyRequestedSections,
|
|
439
|
+
reason: `HTML artifact contains empty requested sections without real content: ${emptyRequestedSections.join(', ')}.`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (missingSections.length > Math.floor(requestedSections.length * 0.4)) {
|
|
443
|
+
return {
|
|
444
|
+
valid: false,
|
|
445
|
+
errorCategory: 'INSUFFICIENT_PROJECT_CONTENT',
|
|
446
|
+
missingSections,
|
|
447
|
+
reason: `HTML artifact is missing critical requested sections: ${missingSections.join(', ')}.`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// ── CHECK 7: Stale Artifact / Mismatched Project Detection ────────────────
|
|
452
|
+
if (!isCalculator && lowerGoal.includes('landing') && (brandCandidates.length > 0 || lowerGoal.includes('nexora') || lowerGoal.includes('novara'))) {
|
|
453
|
+
if (lowerHtml.includes('class="calculator"') && !foundBrand) {
|
|
454
|
+
return {
|
|
455
|
+
valid: false,
|
|
456
|
+
errorCategory: 'STALE_PROJECT_MISMATCH',
|
|
457
|
+
reason: 'HTML artifact appears to be an unrelated calculator project rather than the requested landing page.',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (isCalculator) {
|
|
462
|
+
const hasButtons = lowerHtml.includes('<button') || lowerHtml.includes('class="btn');
|
|
463
|
+
const hasDisplay = lowerHtml.includes('display') || lowerHtml.includes('screen') || lowerHtml.includes('result');
|
|
464
|
+
const hasCalcTerm = lowerHtml.includes('calculator') || lowerHtml.includes('calc');
|
|
465
|
+
if (!hasButtons && !hasDisplay && !hasCalcTerm) {
|
|
466
|
+
return {
|
|
467
|
+
valid: false,
|
|
468
|
+
errorCategory: 'CONTENT_VERIFICATION_FAILED',
|
|
469
|
+
reason: 'Calculator project is missing required calculator content or UI elements (buttons/display/calculator heading).',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
valid: true,
|
|
475
|
+
visibleTextLength: visibleText.length,
|
|
476
|
+
detectedBrand,
|
|
477
|
+
foundBrand,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Performs child-task-aware verification for an incremental unit of work.
|
|
482
|
+
* Validates current child requirements and project brand identity without
|
|
483
|
+
* prematurely demanding future child-task sections.
|
|
484
|
+
*/
|
|
485
|
+
export function verifyChildUnitContentSubstance(htmlContent, options) {
|
|
486
|
+
const { unit, parentGoal } = options;
|
|
487
|
+
if (!htmlContent || !htmlContent.trim()) {
|
|
488
|
+
return {
|
|
489
|
+
valid: false,
|
|
490
|
+
errorCategory: 'EMPTY_ARTIFACT',
|
|
491
|
+
reason: `HTML artifact for unit '${unit.title}' is empty (0 bytes).`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const lowerHtml = htmlContent.toLowerCase();
|
|
495
|
+
const lowerUnitTitle = unit.title.toLowerCase();
|
|
496
|
+
const lowerParentGoal = parentGoal.toLowerCase();
|
|
497
|
+
// ── CHECK 1: Non-Empty HTML Body ──────────────────────────────────────────
|
|
498
|
+
const bodyMatch = htmlContent.match(/<body\b[^>]*>([\s\S]*?)<\/body>/i);
|
|
499
|
+
const bodyHtml = bodyMatch ? bodyMatch[1] : htmlContent;
|
|
500
|
+
if (!bodyHtml || !bodyHtml.trim()) {
|
|
501
|
+
return {
|
|
502
|
+
valid: false,
|
|
503
|
+
errorCategory: 'EMPTY_HTML_BODY',
|
|
504
|
+
reason: `HTML artifact for unit '${unit.title}' does not contain a non-empty <body> element.`,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// ── CHECK 2: Unresolved Generation Placeholders ───────────────────────────
|
|
508
|
+
const placeholders = detectPlaceholders(htmlContent, parentGoal);
|
|
509
|
+
if (placeholders.hasPlaceholders) {
|
|
510
|
+
return {
|
|
511
|
+
valid: false,
|
|
512
|
+
errorCategory: 'PLACEHOLDER_ARTIFACT_REJECTED',
|
|
513
|
+
reason: `HTML artifact contains unresolved placeholders: ${placeholders.reasons.join('; ')}`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
// ── CHECK 3: Empty Containers & Comment-Only Skeletons ─────────────────────
|
|
517
|
+
const skeletonCheck = detectSkeletonContainers(htmlContent);
|
|
518
|
+
if (skeletonCheck.isSkeleton) {
|
|
519
|
+
return {
|
|
520
|
+
valid: false,
|
|
521
|
+
errorCategory: 'INCOMPLETE_SKELETON_REJECTED',
|
|
522
|
+
skeletonSections: skeletonCheck.skeletonSectionNames,
|
|
523
|
+
reason: `HTML artifact contains incomplete skeleton containers: ${skeletonCheck.skeletonSectionNames.join(', ')}. ${skeletonCheck.reasons.join(' ')}`,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
// ── CHECK 4: Visible Text Check (Non-Blank Body) ──────────────────────────
|
|
527
|
+
const visibleText = extractVisibleText(bodyHtml);
|
|
528
|
+
const hasVisualElements = /<(?:img|svg|input|button|canvas|form|video|audio|iframe)\b/i.test(bodyHtml);
|
|
529
|
+
if (visibleText.length === 0 && !hasVisualElements) {
|
|
530
|
+
return {
|
|
531
|
+
valid: false,
|
|
532
|
+
errorCategory: 'INSUFFICIENT_PROJECT_CONTENT',
|
|
533
|
+
visibleTextLength: 0,
|
|
534
|
+
reason: `HTML artifact for unit '${unit.title}' contains zero visible text. Page renders completely blank.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// ── CHECK 5: Brand / Project Identity Evidence ────────────────────────────
|
|
538
|
+
const brandCandidates = extractBrandCandidates(parentGoal);
|
|
539
|
+
let detectedBrand;
|
|
540
|
+
let foundBrand = false;
|
|
541
|
+
const isCalculator = lowerParentGoal.includes('calculator') || lowerUnitTitle.includes('calculator');
|
|
542
|
+
if (brandCandidates.length > 0) {
|
|
543
|
+
for (const b of brandCandidates) {
|
|
544
|
+
const lowerB = b.toLowerCase();
|
|
545
|
+
const words = lowerB.split(/\s+/).filter((w) => w.length >= 3 && !['ai', 'inc', 'corp', 'app', 'ltd', 'co'].includes(w));
|
|
546
|
+
const hasWordMatch = words.length > 0 && words.every((w) => lowerHtml.includes(w));
|
|
547
|
+
if (lowerHtml.includes(lowerB) || hasWordMatch) {
|
|
548
|
+
foundBrand = true;
|
|
549
|
+
detectedBrand = b;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (!foundBrand && !isCalculator) {
|
|
554
|
+
return {
|
|
555
|
+
valid: false,
|
|
556
|
+
errorCategory: 'CONTENT_VERIFICATION_FAILED',
|
|
557
|
+
reason: `HTML artifact is missing requested project brand/title terms corresponding to the task goal (${brandCandidates.join(', ')}).`,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ── CHECK 6: Stale Artifact / Mismatched Project Detection ────────────────
|
|
562
|
+
if (!isCalculator && lowerParentGoal.includes('landing') && (brandCandidates.length > 0 || lowerParentGoal.includes('nexora') || lowerParentGoal.includes('novara'))) {
|
|
563
|
+
if (lowerHtml.includes('class="calculator"') && !foundBrand) {
|
|
564
|
+
return {
|
|
565
|
+
valid: false,
|
|
566
|
+
errorCategory: 'STALE_PROJECT_MISMATCH',
|
|
567
|
+
reason: 'HTML artifact appears to be an unrelated calculator project rather than the requested landing page.',
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (isCalculator && (unit.id.includes('foundation') || unit.id.includes('integration') || unit.category === 'foundation')) {
|
|
572
|
+
const hasButtons = lowerHtml.includes('<button') || lowerHtml.includes('class="btn');
|
|
573
|
+
const hasDisplay = lowerHtml.includes('display') || lowerHtml.includes('screen') || lowerHtml.includes('result');
|
|
574
|
+
const hasCalcTerm = lowerHtml.includes('calculator') || lowerHtml.includes('calc');
|
|
575
|
+
if (!hasButtons && !hasDisplay && !hasCalcTerm) {
|
|
576
|
+
return {
|
|
577
|
+
valid: false,
|
|
578
|
+
errorCategory: 'CONTENT_VERIFICATION_FAILED',
|
|
579
|
+
reason: 'Calculator project is missing required calculator content or UI elements (buttons/display/calculator heading).',
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ── CHECK 7: Child-Unit Focused Requirement Verification ───────────────────
|
|
584
|
+
// A. Foundation Unit:
|
|
585
|
+
if (unit.id.includes('foundation') || unit.category === 'foundation') {
|
|
586
|
+
const hasDoctypeOrHtml = /<!doctype\s+html|<html\b/i.test(htmlContent);
|
|
587
|
+
const hasHead = /<head\b/i.test(htmlContent);
|
|
588
|
+
const hasTitle = /<title\b/i.test(htmlContent);
|
|
589
|
+
const hasBody = /<body\b/i.test(htmlContent);
|
|
590
|
+
if (!hasDoctypeOrHtml || !hasHead || !hasTitle || !hasBody) {
|
|
591
|
+
return {
|
|
592
|
+
valid: false,
|
|
593
|
+
errorCategory: 'INVALID_FOUNDATION_STRUCTURE',
|
|
594
|
+
reason: `HTML artifact for foundation unit '${unit.title}' is missing standard HTML5 structure elements (DOCTYPE, head, title, body).`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
valid: true,
|
|
599
|
+
visibleTextLength: visibleText.length,
|
|
600
|
+
detectedBrand,
|
|
601
|
+
foundBrand,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// B. Navbar / Header Unit:
|
|
605
|
+
if (unit.id.includes('navbar') || lowerUnitTitle.includes('navbar') || lowerUnitTitle.includes('navigation header')) {
|
|
606
|
+
const hasNav = /<(?:header|nav)\b/i.test(htmlContent) || /(?:id|class)=["'][^"']*(?:navbar|nav-container|site-header|navigation)[^"']*["']/i.test(htmlContent);
|
|
607
|
+
if (!hasNav) {
|
|
608
|
+
return {
|
|
609
|
+
valid: false,
|
|
610
|
+
errorCategory: 'MISSING_NAVBAR_SECTION',
|
|
611
|
+
reason: `HTML artifact is missing the navigation header element (<header>, <nav>, or navbar container) required by unit '${unit.title}'.`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// C. Hero Unit:
|
|
616
|
+
if (unit.id.includes('hero') || lowerUnitTitle.includes('hero')) {
|
|
617
|
+
const hasHero = /<section\b[^>]*?(?:id|class)=["'][^"']*?hero[^"']*?["']/i.test(htmlContent) || /<h1\b/i.test(htmlContent);
|
|
618
|
+
if (!hasHero) {
|
|
619
|
+
return {
|
|
620
|
+
valid: false,
|
|
621
|
+
errorCategory: 'MISSING_HERO_SECTION',
|
|
622
|
+
reason: `HTML artifact is missing the hero section (<section class="hero"> or <h1> heading) required by unit '${unit.title}'.`,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const heroTermsToCheck = extractHeroRequestedTerms(parentGoal, unit);
|
|
626
|
+
const missingHeroTerms = [];
|
|
627
|
+
for (const term of heroTermsToCheck) {
|
|
628
|
+
if (!lowerHtml.includes(term.toLowerCase())) {
|
|
629
|
+
missingHeroTerms.push(term);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (missingHeroTerms.length > 0) {
|
|
633
|
+
return {
|
|
634
|
+
valid: false,
|
|
635
|
+
errorCategory: 'MISSING_HERO_CONTENT',
|
|
636
|
+
reason: `Hero section is missing requested hero content/CTA terms: ${missingHeroTerms.join(', ')}.`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// D. Features Unit:
|
|
641
|
+
if (unit.id.includes('features') || lowerUnitTitle.includes('feature')) {
|
|
642
|
+
const hasFeatures = /<(?:section|div)\b[^>]*?(?:id|class)=["'][^"']*?feature[^"']*?["']/i.test(htmlContent) || lowerHtml.includes('feature');
|
|
643
|
+
if (!hasFeatures) {
|
|
644
|
+
return {
|
|
645
|
+
valid: false,
|
|
646
|
+
errorCategory: 'MISSING_FEATURES_SECTION',
|
|
647
|
+
reason: `HTML artifact is missing the features section required by unit '${unit.title}'.`,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// E. Solutions / Menu Unit:
|
|
652
|
+
if (unit.id.includes('solutions') || unit.id.includes('menu') || lowerUnitTitle.includes('solution') || lowerUnitTitle.includes('menu')) {
|
|
653
|
+
const key = unit.id.includes('menu') ? 'menu' : 'solution';
|
|
654
|
+
const hasSection = new RegExp(`<(?:section|div)\\b[^>]*?(?:id|class)=["'][^"']*?${key}[^"']*?["']`, 'i').test(htmlContent) || lowerHtml.includes(key);
|
|
655
|
+
if (!hasSection) {
|
|
656
|
+
return {
|
|
657
|
+
valid: false,
|
|
658
|
+
errorCategory: `MISSING_${key.toUpperCase()}_SECTION`,
|
|
659
|
+
reason: `HTML artifact is missing the ${key} section required by unit '${unit.title}'.`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// F. Dashboard Unit:
|
|
664
|
+
if (unit.id.includes('dashboard') || lowerUnitTitle.includes('dashboard')) {
|
|
665
|
+
const hasDashboard = /<(?:section|div)\b[^>]*?(?:id|class)=["'][^"']*?dashboard[^"']*?["']/i.test(htmlContent) || lowerHtml.includes('dashboard') || lowerHtml.includes('metric');
|
|
666
|
+
if (!hasDashboard) {
|
|
667
|
+
return {
|
|
668
|
+
valid: false,
|
|
669
|
+
errorCategory: 'MISSING_DASHBOARD_SECTION',
|
|
670
|
+
reason: `HTML artifact is missing the product dashboard section required by unit '${unit.title}'.`,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// G. Testimonials Unit:
|
|
675
|
+
if (unit.id.includes('testimonials') || lowerUnitTitle.includes('testimonial')) {
|
|
676
|
+
const hasTestimonials = /<(?:section|div)\b[^>]*?(?:id|class)=["'][^"']*?testimonial[^"']*?["']/i.test(htmlContent) || lowerHtml.includes('testimonial') || lowerHtml.includes('review');
|
|
677
|
+
if (!hasTestimonials) {
|
|
678
|
+
return {
|
|
679
|
+
valid: false,
|
|
680
|
+
errorCategory: 'MISSING_TESTIMONIALS_SECTION',
|
|
681
|
+
reason: `HTML artifact is missing the testimonials section required by unit '${unit.title}'.`,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// H. Pricing Unit:
|
|
686
|
+
if (unit.id.includes('pricing') || lowerUnitTitle.includes('pricing')) {
|
|
687
|
+
const hasPricing = /<(?:section|div)\b[^>]*?(?:id|class)=["'][^"']*?pricing[^"']*?["']/i.test(htmlContent) || lowerHtml.includes('pricing') || lowerHtml.includes('plan');
|
|
688
|
+
if (!hasPricing) {
|
|
689
|
+
return {
|
|
690
|
+
valid: false,
|
|
691
|
+
errorCategory: 'MISSING_PRICING_SECTION',
|
|
692
|
+
reason: `HTML artifact is missing the pricing section required by unit '${unit.title}'.`,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const hasPricingContent = /\$\d+|\/mo|\/month|tier|starter|pro|enterprise|free/i.test(htmlContent);
|
|
696
|
+
if (!hasPricingContent) {
|
|
697
|
+
return {
|
|
698
|
+
valid: false,
|
|
699
|
+
errorCategory: 'INSUFFICIENT_PRICING_CONTENT',
|
|
700
|
+
reason: `Pricing section is missing pricing tiers, amounts, or plan details.`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// I. FAQ Unit:
|
|
705
|
+
if (unit.id.includes('faq') || lowerUnitTitle.includes('faq') || lowerUnitTitle.includes('frequently asked')) {
|
|
706
|
+
const hasFaq = /<(?:section|div)\b[^>]*?(?:id|class)=["'][^"']*?faq[^"']*?["']/i.test(htmlContent) || lowerHtml.includes('faq') || lowerHtml.includes('accordion') || lowerHtml.includes('frequently asked');
|
|
707
|
+
if (!hasFaq) {
|
|
708
|
+
return {
|
|
709
|
+
valid: false,
|
|
710
|
+
errorCategory: 'MISSING_FAQ_SECTION',
|
|
711
|
+
reason: `HTML artifact is missing the FAQ section required by unit '${unit.title}'.`,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const hasQuestion = /\?|question|accordion/i.test(htmlContent);
|
|
715
|
+
if (!hasQuestion) {
|
|
716
|
+
return {
|
|
717
|
+
valid: false,
|
|
718
|
+
errorCategory: 'INSUFFICIENT_FAQ_CONTENT',
|
|
719
|
+
reason: `FAQ section is missing question and answer content.`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// J. CTA / Footer Unit:
|
|
724
|
+
if (unit.id.includes('cta') || unit.id.includes('footer') || lowerUnitTitle.includes('footer')) {
|
|
725
|
+
const hasFooter = /<footer\b/i.test(htmlContent) || /(?:id|class)=["'][^"']*?footer[^"']*?["']/i.test(htmlContent);
|
|
726
|
+
if (!hasFooter) {
|
|
727
|
+
return {
|
|
728
|
+
valid: false,
|
|
729
|
+
errorCategory: 'MISSING_FOOTER_SECTION',
|
|
730
|
+
reason: `HTML artifact is missing the footer element required by unit '${unit.title}'.`,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
valid: true,
|
|
736
|
+
visibleTextLength: visibleText.length,
|
|
737
|
+
detectedBrand,
|
|
738
|
+
foundBrand,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
//# sourceMappingURL=placeholderDetector.js.map
|