Setting the file. One moment.
Visual QA · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
This file
Number 28.65
Position 65 of 89
Type JavaScript
Size 21 KB
Lines 370 scripts/ visual-qa.mjs
JavaScript · 370 lines · 21 KB
0
]
||
args.url).
toString
();
9 const outputDir = resolveOutputDir (url, args.out);
10 const report = await visualQa ({ outputDir });
11 await writeJson (path. join ( docsDir (outputDir), "qa" , "visual-qa.json" ), report);
12 if (args.json) process.stdout. write ( `${ JSON . stringify ( report , null , 2 ) } \n ` );
13 else console. log ( `Visual QA heuristic score: ${ report . score }/100` );
14 }
15
16 export async function visualQa ({ outputDir }) {
17 const dir = docsDir (outputDir);
18 const latest = await safeRead (path. join (dir, "extraction" , "latest.json" ), null );
19 if ( ! latest?.captureId) return missingManifestReport ();
20 const extractionDir = path. join (dir, "extraction" , latest.captureId);
21 const integrity = await verifyFrozenManifest (extractionDir);
22 const observations = path. join (extractionDir, "observations" );
23 const page = await safeRead (path. join (observations, "page.json" ), {});
24 const seo = await safeRead (path. join (observations, "seo.json" ), {});
25 const sourceMap = { pages: [page], seo };
26 const interactionMap = await safeRead (path. join (observations, "interaction-map.json" ), { interactions: [], structural: {} });
27 const sceneContract = await safeRead (path. join (observations, "scene-contract.json" ), { scenes: [] });
28 const controlStateContract = await safeRead (path. join (observations, "control-state-contract.json" ), { controls: [] });
29 const visualAssets = await safeRead (path. join (observations, "visual-assets.json" ), { logos: [], icons: [] });
30 const layoutBlueprint = await safeRead (path. join (observations, "layout-blueprint.json" ), { pages: [] });
31 const buildPlan = await safeRead (path. join (dir, "build" , "build-plan.json" ), { units: [] });
32 const ledger = await safeRead (path. join (dir, "build" , "section-implementation.json" ), { units: [] });
33 const specIndex = await safeRead (path. join (extractionDir, "spec-index.json" ), {});
34 const extractionGaps = await safeRead (path. join (extractionDir, "extraction-gaps.json" ), { gaps: [] });
35 const chromePages = (sourceMap.pages || []). filter (( page ) => page.chrome);
36 const implementationText = await readImplementationText (outputDir);
37 const chromeWarnings = chromeReviewWarnings ({ chromePages, implementationText });
38 const repeaterWarnings = repeaterReviewWarnings ({ pages: sourceMap.pages || [], implementationText });
39 const interactionWarnings = interactionReviewWarnings ({ pages: sourceMap.pages || [], interactionMap, implementationText });
40 const checks = [];
41 checks. push ( check ( "frozen manifest integrity" , integrity.ok && integrity.manifest.manifestHash === latest.manifestHash, 20 , integrity.failures));
42 checks. push ( check ( "home build plan matches manifest" , buildPlan.manifestHash === latest.manifestHash, 10 ));
43 checks. push ( check ( "every planned unit has a build ledger entry" , buildPlan.units?. every (( unit ) => ledger.units?. some (( entry ) => entry.id === unit.id && entry.specHash === unit.hash)), 10 ));
44 checks. push ( check ( "source pages extracted" , sourceMap.pages?. length > 0 , 10 ));
45 checks. push ( check ( "text content captured" , sourceMap.pages?. some (( p ) => p.sections?. some (( s ) => s.text?. length > 80 )), 15 ));
46 checks. push ( check ( "seo captured" , Boolean (sourceMap.seo?.title || sourceMap.pages?. some (( p ) => p.seo?.title)), 10 ));
47 checks. push ( check ( "screenshots captured or fallback recorded" , sourceMap.pages?. some (( p ) => Object. keys (p.screenshots || {}). length > 0 || p.extractionWarnings?. length ), 5 ));
48 checks. push ( check ( "stateful scene contract generated" , Array. isArray (sceneContract.scenes) && sceneContract.scenes. length > 0 , 10 ));
49 checks. push ( check ( "control state contract generated" , Array. isArray (controlStateContract.controls) && controlStateContract.controls. length > 0 , 10 ));
50 checks. push ( check ( "source logo assets materialized" , (visualAssets.logos || []). length > 0 && (visualAssets.logos || []). every (( logo ) => Boolean (logo.localPath || logo.sourceUrl)), 10 ));
51 checks. push ( check ( "important text geometry captured" , (layoutBlueprint.pages || []). some (( page ) => page.sections?. some (( section ) => section.regions?. some (( region ) => region.textGeometry?.lineCount))), 10 ));
52 checks. push ( check ( "chrome evidence captured or fallback warning recorded" , chromeEvidenceAvailable (sourceMap.pages), 10 ));
53 checks. push ( check ( "repeaters preserved or manually flagged" , ! repeaterWarnings. some (( warning ) => warning.kind === "collapsed-repeater" ), 5 , repeaterWarnings. filter (( warning ) => warning.kind === "collapsed-repeater" ). map (( warning ) => warning.message)));
54 checks. push ( check ( "repeater headings preserved or manually flagged" , ! repeaterWarnings. some (( warning ) => warning.kind === "missing-repeater-headings" ), 5 , repeaterWarnings. filter (( warning ) => warning.kind === "missing-repeater-headings" ). map (( warning ) => warning.message)));
55 checks. push ( check ( "multi-image repeaters preserved or manually flagged" , ! repeaterWarnings. some (( warning ) => warning.kind === "collapsed-multi-image-repeater" ), 5 , repeaterWarnings. filter (( warning ) => warning.kind === "collapsed-multi-image-repeater" ). map (( warning ) => warning.message)));
56 checks. push ( check ( "dropdown hierarchy preserved or manually flagged" , ! chromeWarnings. some (( warning ) => warning.kind === "flattened-dropdown" ), 10 , chromeWarnings. filter (( warning ) => warning.kind === "flattened-dropdown" ). map (( warning ) => warning.message)));
57 checks. push ( check ( "sticky header footprint reviewed" , ! chromeWarnings. some (( warning ) => warning.kind === "sticky-header-footprint" ), 5 , chromeWarnings. filter (( warning ) => warning.kind === "sticky-header-footprint" ). map (( warning ) => warning.message)));
58 checks. push ( check ( "seo-only hero text reviewed" , ! chromeWarnings. some (( warning ) => warning.kind === "seo-only-hero-text" ), 5 , chromeWarnings. filter (( warning ) => warning.kind === "seo-only-hero-text" ). map (( warning ) => warning.message)));
59 checks. push ( check ( "core interaction evidence consumed" , ! interactionWarnings. some (( warning ) => warning.kind !== "manual-interaction-review" ), 20 , interactionWarnings. filter (( warning ) => warning.kind !== "manual-interaction-review" ). map (( warning ) => warning.message)));
60 checks. push ( check ( "source footer content preserved" , ! interactionWarnings. some (( warning ) => warning.kind === "missing-footer-content" || warning.kind === "clone-process-copy" ), 10 , interactionWarnings. filter (( warning ) => warning.kind === "missing-footer-content" || warning.kind === "clone-process-copy" ). map (( warning ) => warning.message)));
61 const rawScore = checks. reduce (( sum , item ) => sum + (item.pass ? item.points : 0 ), 0 );
62 const possiblePoints = checks. reduce (( sum , item ) => sum + item.points, 0 );
63 const unitAcceptance = await acceptanceResults ({
64 extractionDir,
65 buildPlan,
66 ledger,
67 specIndex,
68 gaps: extractionGaps.gaps || [],
69 });
70 return {
71 score: possiblePoints ? Math. round ((rawScore / possiblePoints) * 100 ) : 0 ,
72 rawScore,
73 possiblePoints,
74 checks,
75 unitAcceptance,
76 pageAcceptance: checks. map (({ name , pass , warnings = [] }) => ({
77 criterion: name,
78 status: pass ? "verified" : "failed-or-pending" ,
79 warnings,
80 })),
81 warnings: [ ... chromeWarnings, ... repeaterWarnings, ... interactionWarnings]. map (( warning ) => warning.message),
82 manifestHash: latest.manifestHash,
83 provisionalUnits: buildPlan.units?. filter (( unit ) => unit.status === "provisional" ). map (( unit ) => unit.id) || [],
84 note: "Frozen-spec preflight only. Run browser-backed result comparison and report unresolved local gaps without blocking unrelated units." ,
85 generatedAt: new Date (). toISOString (),
86 };
87 }
88
89 async function acceptanceResults ({ extractionDir , buildPlan , ledger , specIndex , gaps }) {
90 const gapById = new Map (gaps. map (( gap ) => [gap.id, gap]));
91 const ledgerById = new Map ((ledger.units || []). map (( entry ) => [entry.id, entry]));
92 const results = [];
93 for ( const unit of buildPlan.units || []) {
94 const relativePath = specIndex[unit.id];
95 const spec = relativePath ? await safeRead (path. join (extractionDir, relativePath), {}) : {};
96 const ledgerEntry = ledgerById. get (unit.id);
97 const ownedGaps = (unit.gapRefs || []). map (( id ) => gapById. get (id)). filter (Boolean);
98 const unresolvedGapRefs = ownedGaps
99 . filter (( gap ) => ! [ "resolved" , "user-accepted" ]. includes (gap.status))
100 . map (( gap ) => gap.id);
101 const viewports = Object. values (ledgerEntry?.verification || {});
102 const browserVerified = viewports. length > 0 && viewports. every (( status ) => status === "passed" );
103 const implementationMatches = Boolean (ledgerEntry && ledgerEntry.specHash === unit.hash);
104 const required = Array. isArray (spec.acceptance?.required) ? spec.acceptance.required : [];
105 const criteria = required. map (( criterion ) => ({
106 criterion,
107 status: ! implementationMatches
108 ? "not-implemented"
109 : unresolvedGapRefs. length
110 ? "provisional-gap-backed"
111 : browserVerified
112 ? "verified"
113 : "pending-browser-verification" ,
114 ... (unresolvedGapRefs. length ? { gapRefs: unresolvedGapRefs } : {}),
115 }));
116 results. push ({
117 unitId: unit.id,
118 specHash: unit.hash,
119 implementationMatches,
120 status: criteria. length && criteria. every (( entry ) => entry.status === "verified" )
121 ? "verified"
122 : unresolvedGapRefs. length
123 ? "provisional"
124 : implementationMatches
125 ? "verification-pending"
126 : "not-implemented" ,
127 criteria,
128 sourceAssertions: spec.acceptance?.sourceAssertions || [],
129 gapRefs: unresolvedGapRefs,
130 });
131 }
132 return results;
133 }
134
135 function missingManifestReport () {
136 return {
137 score: 0 ,
138 rawScore: 0 ,
139 possiblePoints: 20 ,
140 checks: [{ name: "frozen manifest integrity" , pass: false , points: 20 , warnings: [ "No frozen extraction manifest exists." ] }],
141 warnings: [ "No frozen extraction manifest exists." ],
142 provisionalUnits: [],
143 note: "Extraction must freeze before build QA." ,
144 generatedAt: new Date (). toISOString (),
145 };
146 }
147
148 function interactionReviewWarnings ({ pages , interactionMap , implementationText }) {
149 const warnings = [];
150 if ( ! implementationText) return [{ kind: "manual-interaction-review" , message: "No implementation source exists yet; verify every core interaction after UI code is written." }];
151 const required = new Set ((pages || []). flatMap (( page ) => page.interactionDiscovery?.requiredBehaviors || []));
152 const media = interactionMap.structural?.media || (pages || []). flatMap (( page ) => page.interactionDiscovery?.media || []);
153 for ( const item of media. filter (( entry ) => entry.src && entry.role === "background" )) {
154 if ( ! mediaReferenced (implementationText, item.src)) {
155 warnings. push ({ kind: "missing-embedded-media" , message: `Required ${ item . provider || item . tag } background media is not referenced by the implementation: ${ item . src }` });
156 }
157 }
158 if (required. has ( "preserve-carousel-card-expansion" ) && ! ( /carousel | story-rail | rail-track | case-card/ i . test (implementationText) && /overflow-x | overflow \s * : \s * auto | scrollLeft | scrollWidth | translateX/ i . test (implementationText) && /active | expanded/ i . test (implementationText))) {
159 warnings. push ({ kind: "flattened-carousel" , message: "Source carousel has an active-card expansion state, but implementation does not show an overflowing rail with active/expanded state handling." });
160 }
161 if (required. has ( "preserve-scroll-expanded-section-state" ) && ! ( /position \s * : \s * (sticky | fixed) | scrolltrigger | intersectionobserver/ i . test (implementationText) && /scroll/ i . test (implementationText))) {
162 warnings. push ({ kind: "flattened-scroll-state" , message: "Source scroll-state section is required, but implementation does not show pinned/sticky scroll-state handling." });
163 }
164 if ( /this cloned homepage | development reconstruction | first-pass clone | video background unavailable/ i . test (implementationText)) {
165 warnings. push ({ kind: "clone-process-copy" , message: "Implementation contains clone/migration-process copy that must not appear on a migrated source page." });
166 }
167 for ( const page of pages || []) {
168 const legalText = String (page.footer?.legalText || "" ). replace ( / \s + / g , " " ). trim ();
169 const meaningful = legalText. split ( /(?<= [.!?] ) \s + / ). find (( sentence ) => sentence. length >= 80 );
170 if (meaningful && ! implementationText. includes (meaningful. slice ( 0 , 80 ))) {
171 warnings. push ({ kind: "missing-footer-content" , message: "Source footer legal text was extracted but is not present in implementation source." });
172 }
173 }
174 return warnings;
175 }
176
177 function mediaReferenced ( implementationText , source ) {
178 const canonicalSource = canonicalMediaUrl (source);
179 if (implementationText. includes (source) || implementationText. includes (canonicalSource)) return true ;
180 return canonicalSource ? implementationText. includes (canonicalSource. replace ( /&/ g , "&" )) : false ;
181 }
182
183 function canonicalMediaUrl ( value ) {
184 const raw = String (value || "" ). trim ();
185 if ( ! raw) return raw;
186 try {
187 const url = new URL (raw);
188 url.search = url.search. replace ( / \? ( [ ^ ?] * ) \? / g , "?$1&" );
189 return url. toString ();
190 } catch {
191 return raw. replace ( / \? ( [ ^ ?] * ) \? / g , "?$1&" );
192 }
193 }
194
195 function repeaterReviewWarnings ({ pages , implementationText }) {
196 const warnings = [];
197 for ( const page of pages || []) {
198 for ( const warning of page.repeaterDiagnostics?.warnings || []) {
199 warnings. push ({
200 kind: "manual-repeater-review" ,
201 message: `Source page ${ page . path || page . url }: ${ warning }` ,
202 });
203 }
204 for ( const repeater of page.repeaters || []) {
205 if ( ! implementationText) {
206 warnings. push ({
207 kind: "manual-repeater-review" ,
208 message: `Source page ${ page . path || page . url } has structured repeater content (${ repeater . itemCount || repeater . items ?. length || 0 } items); verify the implementation consumes repeaters instead of flattening the content.` ,
209 });
210 continue ;
211 }
212 const headings = (repeater.items || []). map (( item ) => item.heading). filter (Boolean);
213 const matchedHeadings = headings. filter (( heading ) => implementationText. includes (heading)). length ;
214 const hasRepeaterStructure = / \b repeaters \b|\b stories \b|\b items \b|\b map \s * \( / . test (implementationText);
215 if (headings. length && matchedHeadings < Math. min ( 3 , headings. length )) {
216 warnings. push ({
217 kind: "missing-repeater-headings" ,
218 message: `Source page ${ page . path || page . url } has ${ headings . length } repeater heading(s), but fewer than ${ Math . min ( 3 , headings . length ) } appear in implementation files.` ,
219 });
220 }
221 if (matchedHeadings >= Math. min ( 3 , headings. length ) && ! hasRepeaterStructure) {
222 warnings. push ({
223 kind: "collapsed-repeater" ,
224 message: `Source page ${ page . path || page . url } has structured repeater items, but implementation files contain multiple repeater headings without an obvious repeated-content structure.` ,
225 });
226 }
227 if ((repeater.schema?.maxImagesPerItem || 0 ) > 1 ) {
228 const supportsMultipleImages = / \b images \b|\b gallery \b|\b carousel \b| map \s * \( \s * \( ? image \b| image [s2-9] / . test (implementationText);
229 if ( ! supportsMultipleImages) {
230 warnings. push ({
231 kind: "collapsed-multi-image-repeater" ,
232 message: `Source page ${ page . path || page . url } has repeater items with up to ${ repeater . schema . maxImagesPerItem } images each, but implementation files do not show an obvious multi-image item structure.` ,
233 });
234 }
235 }
236 }
237 }
238 return warnings;
239 }
240
241 function check ( name , pass , points , warnings = []) {
242 return { name, pass: Boolean (pass), points, ... (warnings. length ? { warnings } : {}) };
243 }
244
245 async function existsJsonOrText ( filePath ) {
246 try {
247 await import ( "node:fs/promises" ). then (({ access }) => access (filePath));
248 return true ;
249 } catch {
250 return false ;
251 }
252 }
253
254 async function safeRead ( filePath , fallback ) {
255 try {
256 return await readJson (filePath);
257 } catch {
258 return fallback;
259 }
260 }
261
262 function chromeEvidenceAvailable ( pages = []) {
263 return pages. some (( page ) => {
264 if ( ! page.chrome) return false ;
265 if (page.chrome.warnings?. length ) return true ;
266 return Boolean (page.chrome.header?.variants?. length || page.chrome.navigation?. length || page.chrome.heroEvidence);
267 });
268 }
269
270 function chromeReviewWarnings ({ chromePages , implementationText }) {
271 const warnings = [];
272 if ( ! chromePages. length ) {
273 warnings. push ({
274 kind: "manual-chrome-review" ,
275 message: "No chrome evidence is available; manually review header behavior, menu hierarchy, logo size, and hero text before completion." ,
276 });
277 return warnings;
278 }
279 for ( const page of chromePages) {
280 for ( const warning of page.chrome?.warnings || []) {
281 warnings. push ({ kind: "manual-chrome-review" , message: warning });
282 }
283 const dropdowns = (page.chrome?.navigation || []). filter (( item ) => item.children?. length );
284 if (dropdowns. length && implementationText) {
285 for ( const dropdown of dropdowns) {
286 const childLabels = dropdown.children. map (( child ) => child.label). filter (Boolean);
287 const hasStructuredChildren = /children \s * [:=] |\b submenu \b|\b dropdown \b| <details \b| aria-haspopup/ . test (implementationText);
288 const flattenedCount = childLabels. filter (( label ) => implementationText. includes (label)). length ;
289 if ( ! hasStructuredChildren && flattenedCount >= Math. min ( 3 , childLabels. length )) {
290 warnings. push ({
291 kind: "flattened-dropdown" ,
292 message: `Navigation dropdown "${ dropdown . label || dropdown . href }" has ${ childLabels . length } source child item(s), but implementation files contain child labels without an obvious submenu structure.` ,
293 });
294 }
295 }
296 } else if (dropdowns. length && ! implementationText) {
297 warnings. push ({
298 kind: "manual-chrome-review" ,
299 message: `Source navigation has dropdowns (${ dropdowns . map (( item ) => item . label || item . href ). join ( ", " ) }); verify implementation preserves them as submenus after UI code exists.` ,
300 });
301 }
302
303 const variants = page.chrome?.header?.variants || [];
304 const initial = variants. find (( variant ) => variant.name === "initial" );
305 const scrolled = variants. find (( variant ) => variant.name === "scrolled" );
306 const viewportHeight = page.chrome?.viewport?.height || 1200 ;
307 const limit = page.chrome?.header?.maxStickyViewportRatio || 0.16 ;
308 if (implementationText && initial?.height && initial.height / viewportHeight > limit && /position \s * : \s * sticky | position \s * : \s * fixed/ . test (implementationText)) {
309 const hasCompactState = /scrolled | compact | is-sticky | shrink | small-header | sticky-header/ . test (implementationText);
310 if ( ! hasCompactState && ( ! scrolled?.height || scrolled.height < initial.height * 0.75 )) {
311 warnings. push ({
312 kind: "sticky-header-footprint" ,
313 message: `Source initial header is large (${ initial . height }px); implementation appears to use sticky/fixed positioning without an obvious compact scrolled state.` ,
314 });
315 }
316 }
317
318 const seoOnly = page.chrome?.heroEvidence?.forbiddenSeoOnlyText || [];
319 if (implementationText && seoOnly. some (( text ) => text. length > 12 && implementationText. includes (text))) {
320 warnings. push ({
321 kind: "seo-only-hero-text" ,
322 message: "Implementation contains SEO-only title/tagline text that was not confirmed as visible first-viewport hero content." ,
323 });
324 } else if ( ! implementationText && seoOnly. length ) {
325 warnings. push ({
326 kind: "manual-chrome-review" ,
327 message: "SEO-only title/tagline text was detected; verify it is not rendered as hero copy without visual source evidence." ,
328 });
329 }
330 }
331 return warnings;
332 }
333
334 async function readImplementationText ( outputDir ) {
335 const srcDir = path. join (outputDir, "src" );
336 const files = await listSourceFiles (srcDir);
337 const chunks = [];
338 for ( const file of files. slice ( 0 , 80 )) {
339 try {
340 const { readFile } = await import ( "node:fs/promises" );
341 chunks. push ( await readFile (file, "utf8" ));
342 } catch {
343 // Ignore unreadable generated files during heuristic QA.
344 }
345 }
346 return chunks. join ( " \n " ). slice ( 0 , 500000 );
347 }
348
349 async function listSourceFiles ( dir ) {
350 try {
351 const { readdir } = await import ( "node:fs/promises" );
352 const entries = await readdir (dir, { withFileTypes: true });
353 const files = [];
354 for ( const entry of entries) {
355 const fullPath = path. join (dir, entry.name);
356 if (entry. isDirectory ()) files. push ( ...await listSourceFiles (fullPath));
357 else if ( / \. (astro | tsx ?| jsx ?| css | scss) $ / . test (entry.name)) files. push (fullPath);
358 }
359 return files;
360 } catch {
361 return [];
362 }
363 }
364
365 if ( import . meta .url === `file://${ process . argv [ 1 ] }` ) {
366 main (). catch (( error ) => {
367 console. error (error.stack || error.message);
368 process. exit ( 1 );
369 });
370 }