Setting the file. One moment.
Site Clone · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
— line 526
This file
Number 28.62
Position 62 of 89
Type JavaScript
Size 35 KB
Lines 658 scripts/ site-clone.mjs
JavaScript · 658 lines · 35 KB
11 writeJson,
12 writeText,
13 } from "./lib/common.mjs" ;
14 import {
15 createFrontendAutomationState,
16 renderScopeSummary,
17 resolveFrontendContext,
18 updateScopeSummaryCheckpoint,
19 writeFrontendAutomationState,
20 } from "./lib/frontend-automation-state.mjs" ;
21 import { discover } from "./discover-urls.mjs" ;
22 import { ensureBrowserExtractionReady } from "./lib/browser-tooling.mjs" ;
23 import { extractPage } from "./extract-page.mjs" ;
24 import { extractAssets } from "./extract-assets.mjs" ;
25 import { extractInteractions } from "./extract-interactions.mjs" ;
26 import { extractSeoForUrl } from "./extract-seo.mjs" ;
27 import { extractDesignSystem } from "./extract-design-system.mjs" ;
28 import { generateDesignMd } from "./generate-design-md.mjs" ;
29 import { generateSceneContract } from "./generate-scene-contract.mjs" ;
30 import { generateLayoutBlueprint } from "./generate-layout-blueprint.mjs" ;
31 import { generateUiNormalization } from "./generate-ui-normalization.mjs" ;
32 import { generateControlStateContract } from "./generate-control-state-contract.mjs" ;
33 import { generateVisualAssets } from "./generate-visual-assets.mjs" ;
34 import { createWixProject, ensureYarnProjectBoundary, verifyWixRuntimeProject } from "./create-wix-project.mjs" ;
35 import { validateProjectFontFaces } from "./lib/font-contract.mjs" ;
36 import { visualQa } from "./visual-qa.mjs" ;
37 import { buildExtractedHomepage } from "./build-extracted-homepage.mjs" ;
38 import { resolveHomePage } from "./resolve-home-page.mjs" ;
39 import { assembleExtraction } from "./assemble-extraction.mjs" ;
40 import { finalizeExtractionReport } from "./finalize-extraction-report.mjs" ;
41
42 const execFileAsync = promisify (execFile);
43
44 async function main () {
45 const args = parseArgs ();
46 const context = await resolveFrontendContext ({ args });
47 const requestedSourceUrl = context.sourceUrl;
48 let sourceUrl = requestedSourceUrl;
49 const scope = context.scope;
50 const outputDir = context.outputDir;
51 const projectName = path. basename (outputDir);
52 const docs = docsDir (outputDir);
53 const runId = `site-clone-${ Date . now (). toString ( 36 ) }` ;
54 const automationState = createFrontendAutomationState (context);
55
56 if (scope !== "home" || context.explicitUrls. length ) {
57 throw new Error ( "Spec 0083 currently implements the home-page workflow only. Remove --urls and use --scope home; additional-page resolution requires a future approved workflow." );
58 }
59
60 // Standalone runs must own one runnable project directory. Create it before
61 // writing extraction artifacts: the Wix CLI will not scaffold into a folder
62 // that already contains docs/site-clone or downloaded assets.
63 const shouldProvision = context.mode === "standalone" && scope === "home" && ! context.explicitUrls. length ;
64 let wixProject = null ;
65 if (shouldProvision) {
66 wixProject = await ensureStandaloneWixProject ({ sourceUrl, outputDir, projectName, args });
67 } else if (context.mode === "migration_phase" && context.frontendPhase === "build" ) {
68 await ensureYarnProjectBoundary (outputDir);
69 const runtime = await verifyWixRuntimeProject (outputDir);
70 if ( ! runtime.ok) {
71 throw new Error ( `Migration phase build requires the existing frontend project to be runnable: ${ runtime . summary }` );
72 }
73 wixProject = { executed: true , reused: true , migrationManaged: true , ... runtime };
74 }
75 await ensureDir (docs);
76 await assertNoLegacyResume (docs, args);
77 await prepareCanonicalDocsDir (docs);
78 console. log ( "[site-clone] resolving home page" );
79 const resolution = await resolveHomePage (requestedSourceUrl);
80 if ( ! resolution.artifact) {
81 await writeJson (path. join (docs, "page-resolution.spec.json" ), {
82 schemaVersion: "0083.1" ,
83 kind: "page-resolution" ,
84 gaps: resolution.gaps,
85 });
86 const error = new Error (resolution.gaps[ 0 ]?.reason || "Home-page resolution failed" );
87 error.code = "PAGE_RESOLUTION_BLOCKED" ;
88 throw error;
89 }
90 sourceUrl = resolution.artifact.source.resolvedUrl;
91 await writeJson (path. join (docs, "page-resolution.spec.json" ), resolution.artifact);
92 await writeJson (path. join (docs, "run-manifest.json" ), {
93 runId,
94 sourceUrl,
95 outputDir,
96 mode: context.mode,
97 automationMode: context.automationMode,
98 frontendPhase: context.frontendPhase,
99 handoffPath: context.handoffPath,
100 startedAt: new Date (). toISOString (),
101 });
102 await writeFrontendAutomationState (outputDir, automationState);
103
104 const explicitUrls = context.explicitUrls;
105 console. log ( `[site-clone] discovery started (${ scope }${ explicitUrls . length ? `, explicit URLs: ${ explicitUrls . length }` : ""})` );
106 const discovery = await discover ({ sourceUrl, scope, explicitUrls });
107 await writeJson (path. join (docs, "discovery.json" ), discovery);
108 const scopeSummaryPath = path. join (docs, "scope-summary.md" );
109 await writeText (scopeSummaryPath, renderScopeSummary (context, discovery));
110 console. log ( `[site-clone] discovery complete: ${ discovery . totalDiscovered } page(s), ${ discovery . representativePages . length } representative page(s)` );
111
112 if (discovery.requiresConfirmation && ! args.yes && ! context.autoApprove) {
113 updateScopeSummaryCheckpoint (automationState, {
114 status: "pending" ,
115 discovery,
116 artifactRefs: [ "docs/site-clone/discovery.json" , "docs/site-clone/scope-summary.md" ],
117 notes: "Non-homepage scope requires confirmation before extraction/building in manual mode." ,
118 });
119 await writeFrontendAutomationState (outputDir, automationState);
120 printConfirmationSummary (discovery, outputDir);
121 console. log ( "" );
122 console. log ( "Review the scope above, then rerun with --yes to continue or pass --urls/--scope to adjust." );
123 return ;
124 }
125 updateScopeSummaryCheckpoint (automationState, {
126 status: "approved" ,
127 discovery,
128 artifactRefs: [ "docs/site-clone/discovery.json" , "docs/site-clone/scope-summary.md" ],
129 decidedBy: context.autoApprove ? "agent" : discovery.requiresConfirmation ? "user" : "system" ,
130 notes: context.autoApprove
131 ? "Migration phase 1-click mode auto-approved the frontend scope summary."
132 : discovery.requiresConfirmation
133 ? "Scope summary confirmed by explicit --yes continuation."
134 : "Scope confirmation was not required for this run." ,
135 });
136 await writeFrontendAutomationState (outputDir, automationState);
137
138 console. log ( "[site-clone] browser preflight started" );
139 const browserTooling = await ensureBrowserExtractionReady ({ startDir: process. cwd () });
140 console. log ( "[site-clone] browser preflight complete" );
141 const extractionWarnings = [];
142
143 const pages = [];
144 const pagesDir = path. join (docs, "pages" );
145 await ensureDir (pagesDir);
146 for ( const page of discovery.representativePages) {
147 console. log ( `[site-clone] extracting page ${ page . path || page . url }` );
148 const extracted = await extractPage (page.url, { outputDir, screenshots: args.screenshots !== "false" , browserTooling });
149 pages. push (extracted);
150 await writeJson (path. join (pagesDir, `${ safePageName ( page ) }.json` ), extracted);
151 }
152
153 console. log ( "[site-clone] extracting SEO and assets" );
154 const seo = await extractSeoForUrl (sourceUrl);
155 const assets = await extractAssets (sourceUrl, { outputDir, download: args.download !== "false" });
156 await writeJson (path. join (docs, "seo.json" ), seo);
157 await writeJson (path. join (docs, "assets.json" ), assets);
158 await writeJson (path. join (docs, "fonts.json" ), assets.fonts);
159
160 console. log ( "[site-clone] extracting interactions" );
161 const interactionMap = await loadOrExtractInteractions ({ sourceUrl, outputDir, browserTooling });
162 await writeJson (path. join (docs, "interaction-map.json" ), interactionMap);
163 const enrichedPages = interactionMap.pages?. length ? interactionMap.pages : pages;
164 pages. length = 0 ;
165 pages. push ( ... enrichedPages);
166 for ( const page of pages) {
167 await writeJson (path. join (pagesDir, `${ safePageName ( page ) }.json` ), page);
168 }
169
170 const sourceMap = {
171 sourceUrl,
172 normalizedAt: new Date (). toISOString (),
173 scope: discovery.scope,
174 project: {
175 name: projectName,
176 outputDir,
177 businessName: args[ "business-name" ] || businessNameFromProject (projectName),
178 },
179 discovery,
180 pages,
181 assets: assets.assets,
182 fonts: {
183 families: assets.fonts?.families || [],
184 faceCount: assets.fonts?.faces?. length || 0 ,
185 contentScripts: assets.fonts?.contentScripts || [],
186 },
187 interactions: {
188 interactionCount: interactionMap.summary?.interactionCount || 0 ,
189 kinds: interactionMap.summary?.kinds || {},
190 targetCount: interactionMap.summary?.targetCount || 0 ,
191 meaningfulCaptureCount: interactionMap.summary?.meaningfulCaptureCount || 0 ,
192 },
193 seo,
194 tokens: {},
195 extractionWarnings,
196 };
197 await writeJson (path. join (docs, "source-map.json" ), sourceMap);
198
199 console. log ( "[site-clone] extracting design system" );
200 const designSystem = await extractDesignSystem ({
201 sourceUrl,
202 outputDir,
203 extractor: args[ "design-extractor" ],
204 browserTooling,
205 });
206 const tokens = designSystem.tokens;
207 sourceMap.tokens = tokens;
208 sourceMap.designExtraction = {
209 selectedExtractor: designSystem.selectedExtractor,
210 actualExtractor: designSystem.actualExtractor || designSystem.extractor,
211 fallbackReason: designSystem.fallbackReason,
212 attempts: designSystem.attempts || [],
213 };
214 await writeJson (path. join (docs, "tokens.json" ), tokens);
215 await writeJson (path. join (docs, "source-map.json" ), sourceMap);
216
217 console. log ( "[site-clone] projecting interaction, visual, and layout observations" );
218 const sceneContract = await generateSceneContract ({ outputDir, pages, interactionMap });
219 const controlStateContract = await generateControlStateContract ({ outputDir, interactionMap });
220 const visualAssets = await generateVisualAssets ({ outputDir, pages, assets });
221 const layoutBlueprint = await generateLayoutBlueprint ({ outputDir, pages, interactionMap, sceneContract });
222 const uiNormalization = await generateUiNormalization ({ outputDir, pages, interactionMap, sceneContract, layoutBlueprint });
223 const designMd = designSystem.designMd?. trim ()
224 ? designSystem.designMd
225 : await generateDesignMd ({ outputDir, sourceUrl });
226 await writeText (path. join (docs, "design.md" ), `${ designMd . trim () } \n\n ${ renderVisualAssetsDesignSummary ( visualAssets ) } \n\n ${ renderControlStateDesignSummary ( controlStateContract ) } \n\n ${ renderLayoutBlueprintDesignSummary ( layoutBlueprint ) } \n\n ${ renderUiNormalizationDesignSummary ( uiNormalization ) } \n ` );
227 console. log ( "[site-clone] assembling and freezing extraction specs" );
228 const decisionPatches = args[ "decision-patches" ]
229 ? JSON . parse ( await readFile (path. resolve ( String (args[ "decision-patches" ])), "utf8" ))
230 : [];
231 const extraction = await assembleExtraction ({
232 outputDir,
233 requestedUrl: requestedSourceUrl,
234 resolvedUrl: sourceUrl,
235 canonicalUrl: resolution.artifact.source.canonicalUrl,
236 page: pages[ 0 ],
237 assets,
238 seo,
239 tokens,
240 interactionMap,
241 sceneContract,
242 layoutBlueprint,
243 uiNormalization,
244 controlStateContract,
245 visualAssets,
246 sourceFingerprint: resolution.artifact.source.fingerprint,
247 decisionPatches: Array. isArray (decisionPatches) ? decisionPatches : decisionPatches.patches || [],
248 pageResolutionArtifact: resolution.artifact,
249 });
250 await removeLegacyExtractionHandoffs (docs);
251 if (context.frontendPhase === "build" && scope === "home" ) {
252 console. log ( `[site-clone] building home page from frozen manifest ${ extraction . manifest . manifestHash }` );
253 await installInteractionRuntime (outputDir);
254 await buildExtractedHomepage ({ outputDir });
255 }
256
257 if ( ! wixProject && context.mode === "standalone" ) {
258 console. log ( "[site-clone] preparing Wix project metadata" );
259 wixProject = await createWixProject ({
260 sourceUrl,
261 outputDir,
262 projectName,
263 businessName: args[ "business-name" ] || businessNameFromProject (projectName),
264 template: args.template,
265 execute: Boolean (args[ "execute-create" ]),
266 });
267 }
268 if (wixProject?.executed) {
269 const runtime = await verifyWixRuntimeProject (outputDir);
270 if ( ! runtime.ok) throw new Error ( `Wix project contract failed: ${ runtime . summary }` );
271 }
272
273 let qa = null ;
274 let fontValidation = null ;
275 if (context.frontendPhase === "build" ) {
276 console. log ( "[site-clone] running visual QA" );
277 qa = await visualQa ({ outputDir });
278 fontValidation = await validateProjectFontFaces ({ projectRoot: outputDir, fontManifest: assets.fonts });
279 await writeJson (path. join (docs, "qa" , "visual-qa.json" ), qa);
280 await writeJson (path. join (docs, "qa" , "font-validation.json" ), fontValidation);
281 const finalReport = await finalizeExtractionReport ({ outputDir });
282 console. log ( `[site-clone] reconstruction status: ${ finalReport . status }` );
283 }
284 await writeJson (path. join (docs, "run-manifest.json" ), {
285 runId,
286 sourceUrl,
287 outputDir,
288 scope: discovery.scope,
289 mode: context.mode,
290 automationMode: context.automationMode,
291 frontendPhase: context.frontendPhase,
292 handoffPath: context.handoffPath,
293 representativePages: discovery.representativePages. map (( page ) => page.url),
294 actualDesignExtractor: designSystem.actualExtractor || designSystem.extractor,
295 completedAt: new Date (). toISOString (),
296 });
297
298 console. log ( `Site clone artifacts written to ${ docs }` );
299 console. log ( `Output project: ${ outputDir }` );
300 if (context.frontendPhase === "plan" ) {
301 console. log ( "Migration phase plan mode completed: extraction and planning artifacts were refreshed, and build-only project verification/QA were intentionally skipped." );
302 } else if ( ! wixProject?.executed) {
303 console. log ( "Wix project creation command prepared:" );
304 console. log (wixProject.command);
305 console. log ( "Run with --execute-create when ready to create the Wix Headless project." );
306 }
307 if (wixProject?.localOutputRename) {
308 console. log ( `Wix CLI folder name normalized to "${ wixProject . wixFolderName }" and should be renamed to "${ projectName }" after creation.` );
309 }
310 if (qa) {
311 console. log ( `Heuristic QA score: ${ qa . score }/100` );
312 console. log ( "After implementation: start the clone, run post-build-gap.mjs with --result-url, review every screenshot pair, then complete the bounded gap-fix loop." );
313 }
314 }
315
316 async function ensureStandaloneWixProject ({ sourceUrl , outputDir , projectName , args }) {
317 const packagePath = path. join (outputDir, "package.json" );
318 try {
319 await readFile (packagePath, "utf8" );
320 await ensureYarnProjectBoundary (outputDir);
321 const existing = await verifyWixRuntimeProject (outputDir);
322 if ( ! existing.ok) throw new Error ( `Existing standalone output is not a runnable Wix project: ${ existing . summary }` );
323 console. log ( "[site-clone] reusing existing Wix project" );
324 return { executed: true , reused: true , ... existing };
325 } catch (error) {
326 if (error?.code !== "ENOENT" ) throw error;
327 }
328 console. log ( "[site-clone] provisioning Wix project before extraction" );
329 return createWixProject ({
330 sourceUrl,
331 outputDir,
332 projectName,
333 businessName: args[ "business-name" ] || businessNameFromProject (projectName),
334 template: args.template,
335 execute: true ,
336 });
337 }
338
339 async function loadOrExtractInteractions ({ sourceUrl , outputDir , browserTooling }) {
340 const artifact = path. join ( docsDir (outputDir), "interaction-map.json" );
341 try {
342 const existing = JSON . parse ( await readFile (artifact, "utf8" ));
343 if (existing.sourceUrl === sourceUrl && Array. isArray (existing.interactions)) {
344 console. log ( "[site-clone] reusing completed interaction map" );
345 return existing;
346 }
347 } catch {
348 // No compatible checkpoint; perform a fresh extraction below.
349 }
350 return extractInteractionsIsolated ({ sourceUrl, outputDir, browserTooling });
351 }
352
353 async function extractInteractionsIsolated ({ sourceUrl , outputDir , browserTooling }) {
354 const script = new URL ( "./extract-interactions.mjs" , import . meta .url);
355 try {
356 await execFileAsync (process.execPath, [
357 script.pathname,
358 sourceUrl,
359 "--out" ,
360 outputDir,
361 "--project-root" ,
362 browserTooling.projectRoot,
363 ], { timeout: 240000 , maxBuffer: 1024 * 1024 });
364 } catch (error) {
365 throw new Error ( `interaction extraction failed: ${ error . stderr || error . message }` );
366 }
367 const artifact = path. join ( docsDir (outputDir), "interaction-map.json" );
368 try { return JSON . parse ( await readFile (artifact, "utf8" )); }
369 catch { throw new Error ( "interaction extraction exited without interaction-map.json" ); }
370 }
371
372 async function prepareCanonicalDocsDir ( docs ) {
373 const generatedPaths = [
374 "pages" ,
375 "components" ,
376 "screenshots" ,
377 "design-md-generator" ,
378 ".design-md-generator-run" ,
379 "design.md" ,
380 "discovery.json" ,
381 "routes.json" ,
382 "repeater-cms.json" ,
383 "repeater-cms.md" ,
384 "source-map.json" ,
385 "seo.json" ,
386 "assets.json" ,
387 "interaction-qa.json" ,
388 "scene-contract.json" ,
389 "control-state-contract.json" ,
390 "control-state-contract.md" ,
391 "visual-assets.json" ,
392 "visual-assets.md" ,
393 "layout-blueprint.json" ,
394 "layout-blueprint.md" ,
395 "ui-normalization.json" ,
396 "ui-normalization.md" ,
397 "fonts.json" ,
398 "font-validation.json" ,
399 "tokens.json" ,
400 "visual-qa.json" ,
401 "scope-summary.md" ,
402 "frontend-automation-state.json" ,
403 "run-manifest.json" ,
404 "page-resolution.spec.json" ,
405 "extraction" ,
406 "build" ,
407 "qa" ,
408 "gap-analysis" ,
409 "final-report.json" ,
410 "final-report.md" ,
411 ];
412 for ( const relativePath of generatedPaths) {
413 await rm (path. join (docs, relativePath), { recursive: true , force: true });
414 }
415 }
416
417 async function assertNoLegacyResume ( docs , args ) {
418 try {
419 await readFile (path. join (docs, "run-manifest.json" ), "utf8" );
420 } catch (error) {
421 if (error?.code === "ENOENT" ) return ;
422 throw error;
423 }
424 try {
425 await readFile (path. join (docs, "extraction" , "latest.json" ), "utf8" );
426 return ;
427 } catch (error) {
428 if (error?.code !== "ENOENT" ) throw error;
429 }
430 if (args[ "restart-0083" ] === true || args[ "restart-0083" ] === "true" ) return ;
431 throw new Error ( "This output contains a pre-0083 headless run and cannot be resumed. Rerun with --restart-0083 to discard its generated frontend evidence and restart from home-page resolution plus fresh capture." );
432 }
433
434 async function removeLegacyExtractionHandoffs ( docs ) {
435 const obsolete = [
436 "pages" ,
437 "components" ,
438 "source-map.json" ,
439 "seo.json" ,
440 "assets.json" ,
441 "fonts.json" ,
442 "tokens.json" ,
443 "interaction-map.json" ,
444 "scene-contract.json" ,
445 "control-state-contract.json" ,
446 "control-state-contract.md" ,
447 "visual-assets.json" ,
448 "visual-assets.md" ,
449 "layout-blueprint.json" ,
450 "layout-blueprint.md" ,
451 "ui-normalization.json" ,
452 "ui-normalization.md" ,
453 "routes.json" ,
454 "repeater-cms.json" ,
455 "repeater-cms.md" ,
456 "design.md" ,
457 "page-resolution.spec.json" ,
458 ];
459 for ( const relativePath of obsolete) await rm (path. join (docs, relativePath), { recursive: true , force: true });
460 }
461
462 export async function installInteractionRuntime ( outputDir ) {
463 const source = new URL ( "./lib/interaction-runtime.mjs" , import . meta .url);
464 const sourceText = await readFile (source, "utf8" );
465 const target = path. join (outputDir, "src" , "lib" , "rp-interactions.mjs" );
466 let existing = "" ;
467 try {
468 existing = await readFile (target, "utf8" );
469 } catch (error) {
470 if (error?.code !== "ENOENT" ) throw error;
471 }
472 if ( ! existing || existing. includes ( "@generated-source wix-headless-replatform" )) {
473 await ensureDir (path. dirname (target));
474 await writeText (target, sourceText);
475 }
476 const publicDir = path. join (outputDir, "public" , "site-clone" );
477 await ensureDir (publicDir);
478 await writeText (path. join (publicDir, "rp-interactions.mjs" ), sourceText);
479 await writeText (
480 path. join (publicDir, "rp-interactions-bootstrap.mjs" ),
481 await readFile ( new URL ( "./lib/interaction-bootstrap.mjs" , import . meta .url), "utf8" ),
482 );
483 const normalizationCss = await readFile ( new URL ( "./lib/ui-normalization.css" , import . meta .url), "utf8" );
484 await writeText (path. join (publicDir, "rp-ui-normalize.css" ), normalizationCss);
485 const sourceStyleDir = path. join (outputDir, "src" , "styles" );
486 await ensureDir (sourceStyleDir);
487 const sourceStyleTarget = path. join (sourceStyleDir, "rp-ui-normalize.css" );
488 let existingStyle = "" ;
489 try {
490 existingStyle = await readFile (sourceStyleTarget, "utf8" );
491 } catch (error) {
492 if (error?.code !== "ENOENT" ) throw error;
493 }
494 if ( ! existingStyle || existingStyle. includes ( "@generated-source wix-headless-replatform" )) {
495 await writeText (sourceStyleTarget, normalizationCss);
496 }
497 }
498
499 function printConfirmationSummary ( discovery , outputDir ) {
500 console. log ( `Output directory: ${ outputDir }` );
501 console. log ( `Discovered ${ discovery . totalDiscovered } same-origin URL(s) for scope "${ discovery . scope }".` );
502 if (discovery.inScopePages) console. log ( `In-scope implementation URL(s): ${ discovery . inScopePages . length }` );
503 if (discovery.preservedPages) console. log ( `Preserved fallback URL(s): ${ discovery . preservedPages . length }` );
504 for ( const [ area , count ] of Object. entries (discovery.countsByArea)) {
505 console. log ( `- ${ area }: ${ count }` );
506 }
507 const dynamicAreas = [ "product" , "product-category" , "blog-post" , "blog-index" , "cms-content" , "bookings" , "events" , "pricing" ]
508 . filter (( area ) => discovery.countsByArea[area]);
509 if (dynamicAreas. length ) {
510 console. log ( "" );
511 console. log ( `Dynamic Wix SDK-backed templates: ${ dynamicAreas . join ( ", " ) }` );
512 }
513 if (discovery.excluded. length ) {
514 console. log ( "" );
515 console. log ( `Excluded URL records: ${ discovery . excluded . length }` );
516 }
517 }
518
519 function parseUrlList ( value , sourceUrl ) {
520 if ( ! value) return [];
521 return String (value)
522 . split ( "," )
523 . map (( item ) => new URL (item. trim (), sourceUrl). toString ());
524 }
525
526 function safePageName ( page ) {
527 return `${ page . area }-${ String ( page . path || "home" ). replace ( / [ ^ a-z0-9] + / gi , "-" ). replace ( / ^ - +| - +$ / g , "" ) || "home"}` ;
528 }
529
530 function renderUiNormalizationDesignSummary ( contract ) {
531 return `## Identity-Preserving UI Normalization \n\n Identity locks outrank polish. Preserve content, section order/archetype, brand tokens, media, CTA hierarchy, and interaction model. Do not select a new design style. \n\n Load once: \` <link rel="stylesheet" href="/site-clone/rp-ui-normalize.css"> \`\n\n Motion: hover/focus is subtle and non-structural; click/activation may change major geometry only when required by the scene. Captured timing overrides defaults. Use easing and respect \` prefers-reduced-motion \` . \n\n Section recipes: \n ${ ( contract . sections || []). map (( section ) => `- \` ${ section . sectionId } \` / \` ${ section . layout } \` : ${ section . recipe . join ( " " ) }` ). join ( " \n " ) || "- none"}` ;
532 }
533
534 function renderLayoutBlueprintDesignSummary ( contract ) {
535 return `## Section Layout Blueprints \n\n Build each section independently in this order: canvas, background layers, container, semantic regions, relationships, responsive reflow, then UI normalization. Composition and background-media role are identity locks. \n\n ${ ( contract . pages || []). flatMap (( page ) => page . sections || []). map (( section ) => `- \` ${ section . sectionId } \` / \` ${ section . composition } \` / \` ${ section . canvas . widthMode }-${ section . canvas . heightMode } \` : ${ section . implementation . steps . join ( " " ) }` ). join ( " \n " ) || "- none"}` ;
536 }
537
538 function renderControlStateDesignSummary ( contract ) {
539 return `## Control State Fidelity \n\n Implement source-observed rest, hover, focus-visible, pressed, activated, current, and disabled deltas on their measured owner. Nested icon motion must not be promoted to the whole control. \n\n ${ ( contract . controls || []). map (( control ) => `- \` ${ control . scope }/${ control . role } \` ${ ( control . members || [ control . label ]). join ( ", " ) }: ${ Object . keys ( control . states ). map (( state ) => ` \` ${ state } \` ` ). join ( ", " ) || "rest only"}` ). join ( " \n " ) || "- no representative controls captured"}` ;
540 }
541
542 function renderVisualAssetsDesignSummary ( contract ) {
543 return `## Source Visual Assets \n\n Logos are identity locks: use exact source/materialized files, preserve every observed variant and target size, and never recreate a logo as text. Icons follow source asset → source library/sprite → style-matched established library fallback. \n\n ${ ( contract . logos || []). map (( logo ) => `- \` ${ logo . id } \` : ${ logo . localPath || logo . sourceUrl || logo . useHref || "captured markup"}` ). join ( " \n " ) || "- BLOCKING: no logo candidate captured; inspect the source before implementing brand chrome."}` ;
544 }
545
546 async function writeComponentSpecs ({ docs , pages , routes , sceneContract , layoutBlueprint , uiNormalization , controlStateContract , visualAssets }) {
547 const dir = path. join (docs, "components" );
548 await ensureDir (dir);
549 for ( const page of pages) {
550 const relatedRoutes = routes.routes. filter (( route ) => route.representativeUrls?. includes (page.url));
551 const pageScenes = (sceneContract?.scenes || []). filter (( scene ) => scene.id. startsWith ( `${ page . area || "page"}-` ));
552 const pageLayouts = (layoutBlueprint?.pages || []). find (( candidate ) => candidate.url === page.url)?.sections || [];
553 const pageNormalization = (uiNormalization?.sections || []). filter (( section ) => section.pageUrl === page.url);
554 const pageControls = (controlStateContract?.controls || []). filter (( control ) => control.scope !== "content" || page.url === pages[ 0 ]?.url);
555 const pageLogos = (visualAssets?.logos || []). filter (( asset ) => asset.usages. some (( usage ) => usage.pageUrl === page.url));
556 const pageIcons = (visualAssets?.icons || []). filter (( asset ) => asset.usages. some (( usage ) => usage.pageUrl === page.url));
557 const title = page.title || page.seo?.title || page.url;
558 const body = `# ${ title }
559
560 Source URL: ${ page . url }
561
562 Area: ${ page . area }
563
564 Route strategy:
565
566 ${ relatedRoutes . map (( route ) => `- ${ route . kind }: \` ${ route . targetRoute } \` ${ route . dataSource ? ` via ${ route . dataSource }` : ""}` ). join ( " \n " ) || "- static or not yet classified"}
567
568 ## Blocking Interaction Checklist
569
570 Do this before visual polish. The page is incomplete until every item passes.
571
572 1. Add this exact tag once near the end of the document: \` <script type="module" src="/site-clone/rp-interactions-bootstrap.mjs"></script> \` .
573 2. Add this exact stylesheet once in the document head: \` <link rel="stylesheet" href="/site-clone/rp-ui-normalize.css"> \` .
574 3. Do not use a raw \` type="module" \` script to import a relative file from \` src/ \` ; that browser URL will not resolve. The public bootstrap auto-binds marker-complete scenes.
575 4. Implement the following ${ pageScenes . length } core scene(s) and their exact markers:
576 ${ pageScenes . map (( scene ) => ` - \` ${ scene . id } \` / \` ${ scene . implementation . primitive } \` : ${ ( scene . implementation . recipe ?. steps || []). join ( " " ) }` ). join ( " \n " ) || " - none"}
577 5. Use the exact logos/icons in the visual asset inventory. A text recreation of a logo is a blocking failure.
578 6. Implement the dedicated control states below before generic UI normalization.
579 7. Implement each section's layout blueprint below before applying UI normalization. Composition, text geometry, and background-media role are identity locks.
580 8. Implement each identity-preserving UI normalization recipe below. Identity locks outrank polish.
581 9. Start the clone and run \` node skills/wix-headless-replatform/scripts/verify-interactions.mjs --out <project-dir> --clone-url <local-url> --project-root <host-project-root> \` .
582 10. Fix every failed scene or normalization check. A missing \` interaction-qa.json \` or \` pass: false \` is a blocking failure, even if the static page looks close.
583
584 Ignored source infrastructure:
585 ${ ( page . ignoredSurfaces || []). map (( surface ) => `- \` ${ surface . kind } \` / provider \` ${ surface . provider || "unknown"} \` : ${ surface . creationPolicy === "ignore" ? "identify only; do not recreate as page UI" : surface . creationPolicy }` ). join ( " \n " ) || "- none"}
586
587 ## Source Visual Assets
588
589 Logos must use these exact source/materialized files and recorded usage sizes. Do not recreate them as text. Icons use source-first resolution; a matching established library is fallback-only.
590
591 Logos:
592 ${ pageLogos . map (( asset ) => `- \` ${ asset . id } \` : ${ asset . localPath || asset . sourceUrl || asset . useHref || "captured source markup"}; ${ asset . usages . filter (( usage ) => usage . pageUrl === page . url ). map (( usage ) => `${ usage . context }/${ usage . variant } ${ usage . renderedSize ?. width || 0 }×${ usage . renderedSize ?. height || 0 }` ). join ( ", " ) }` ). join ( " \n " ) || "- BLOCKING: none captured; inspect source chrome before implementation"}
593
594 Icons:
595 ${ pageIcons . map (( asset ) => `- \` ${ asset . id } \` : ${ asset . localPath || asset . sourceUrl || asset . useHref || "captured source markup"}; ${ asset . usages . filter (( usage ) => usage . pageUrl === page . url ). map (( usage ) => usage . context ). join ( ", " ) }` ). join ( " \n " ) || "- no standalone icon evidence captured"}
596
597 ## Dedicated Control States
598
599 Implement state deltas on the recorded owner. \` focus-visible \` remains required for accessibility; nested icon motion must remain nested.
600
601 ${ pageControls . map (( control ) => `- \` ${ control . scope }/${ control . role } \` ${ ( control . members || [ control . label ]). join ( ", " ) }: ${ Object . keys ( control . states ). map (( state ) => ` \` ${ state } \` ` ). join ( ", " ) || "rest only"}; icon motion: ${ control . iconMotion . join ( ", " ) || "none"}` ). join ( " \n " ) || "- no representative controls captured"}
602
603 ## Section Layout Blueprints
604
605 Use the controlled terms and normalized rectangles as the source of truth. Build canvas and ordered background layers first. A background media layer is not a peer column.
606
607 ${ pageLayouts . map (( section ) => `### ${ section . sectionId }: ${ section . composition } \n\n - Canvas: \` ${ section . canvas . widthMode }/${ section . canvas . heightMode } \` ${ section . canvas . pinned ? "; pinned" : ""} \n - Background: \` ${ section . background . kind } \` — ${ section . background . layers . map (( layer ) => ` \` ${ layer . role }:${ layer . kind } \` ` ). join ( " -> " ) || "none"} \n - Container: \` ${ section . container . widthMode } \`\n - Relationships: ${ section . relationships . map (( item ) => ` \` ${ item } \` ` ). join ( ", " ) || "none measured"} \n - Forbidden conversions: ${ section . identityLocks . forbiddenConversions . map (( item ) => ` \` ${ item } \` ` ). join ( ", " ) || "none"} \n\n Regions: \n ${ section . regions . map (( region ) => `- \` ${ region . role } \` / \` ${ region . placement } \` / \` ${ JSON . stringify ( region . normalizedRect ) } \` ${ region . textGeometry ? ` / text geometry \` ${ JSON . stringify ({ ... region.textGeometry , responsive: region.responsiveTextGeometry || [] }) } \` ` : ""}${ region . text ? `: ${ region . text }` : ""}` ). join ( " \n " ) || "- no measured regions; follow scene evidence and source screenshot"} \n\n Build order: \n ${ section . implementation . steps . map (( step , index ) => `${ index + 1 }. ${ step }` ). join ( " \n " ) }` ). join ( " \n\n " ) || "_No section layout blueprint captured._"}
608
609 ## Identity-Preserving UI Normalization
610
611 Allowed: spacing/alignment cleanup, consistent repeated-item geometry, responsive reflow within the same archetype, subtle hover/focus feedback, eased state transitions, accessibility, and reduced motion.
612
613 Forbidden: new visual style/palette/type, section reordering, archetype conversion, new CTA hierarchy, replaced media/interaction intent, or hiding source content to simplify layout.
614
615 ${ pageNormalization . map (( section ) => `### ${ section . sectionId }: ${ section . kind }/${ section . variant } \n\n Markers: \` ${ section . markerContract . section } \` ; \` ${ section . markerContract . layout } \`\n\n Recipe: \n ${ section . recipe . map (( step , index ) => `${ index + 1 }. ${ step }` ). join ( " \n " ) } \n\n Rules: \n ${ section . rules . map (( rule ) => `- ${ rule . id }: ${ rule . requirement }` ). join ( " \n " ) } \n\n Assertions: \n ${ section . assertions . map (( assertion ) => `- \` ${ assertion . kind } \` : ${ JSON . stringify ( assertion ) }` ). join ( " \n " ) }` ). join ( " \n\n " ) || "_No section normalization contract captured._"}
616
617 ## Sections
618
619 ${ ( page . sections || []). map (( section , index ) => `### ${ index + 1 }. ${ section . heading || section . tag || "Section"} \n\n ${ section . text || "_No text captured._"}` ). join ( " \n\n " ) || "_No sections captured._"}
620
621 ## Repeaters
622
623 ${ ( page . repeaters || []). map (( repeater , index ) => `### ${ index + 1 }. ${ repeater . label || repeater . kind || "Repeater"} \n\n - kind: \` ${ repeater . kind } \`\n - source: \` ${ repeater . source } \`\n - items: ${ repeater . itemCount || repeater . items ?. length || 0 } \n - schema: headings=${ Boolean ( repeater . schema ?. hasHeading ) }, paragraphs=${ Boolean ( repeater . schema ?. hasParagraphs ) }, images=${ Boolean ( repeater . schema ?. hasImages ) }, maxImagesPerItem=${ repeater . schema ?. maxImagesPerItem ?? 0 } \n\n ${ ( repeater . items || []). slice ( 0 , 12 ). map (( item , itemIndex ) => `#### ${ itemIndex + 1 }. ${ item . heading || "Item"} \n\n ${ ( item . paragraphs || []). join ( " \n\n " ) || "_No text captured._"} \n\n Images: \n ${ ( item . images || []). map (( image ) => `- ${ image . src }` ). join ( " \n " ) || "_No images captured._"}` ). join ( " \n\n " ) }` ). join ( " \n\n " ) || "_No repeaters captured._"}
624
625 ## Repeater Diagnostics
626
627 ${ page . repeaterDiagnostics ? `- source: \` ${ page . repeaterDiagnostics . source || "unknown"} \`\n - accessibility snapshot available: ${ Boolean ( page . repeaterDiagnostics . accessibilitySnapshotAvailable ) } \n - signal count: ${ page . repeaterDiagnostics . signalCount ?? 0 } \n - accepted repeaters: ${ page . repeaterDiagnostics . acceptedRepeaterCount ?? 0 } \n ${ ( page . repeaterDiagnostics . warnings || []). length ? `- warnings: \n ${ page . repeaterDiagnostics . warnings . map (( warning ) => ` - ${ warning }` ). join ( " \n " ) }` : "- warnings: none"}` : "_No repeater diagnostics captured._"}
628
629 ## Interactions
630
631 ${ page . interactionDiscovery ? `${ ( page . interactionDiscovery . requiredBehaviors || []). length ? `Required behaviors: \n ${ page . interactionDiscovery . requiredBehaviors . map (( behavior ) => `- \` ${ behavior } \` ` ). join ( " \n " ) }` : "Required behaviors: none"} \n\n ${ ( page . interactionDiscovery . sectionInteractions || []). filter (( section ) => section . interactions ?. length ). map (( section ) => `### ${ section . sectionId } \n\n ${ section . interactions . map (( interaction ) => `- ${ interaction . kind } / ${ interaction . importance } / ${ interaction . trigger ?. type || interaction . trigger }${ interaction . textChanged ? " / text-changes" : ""}` ). join ( " \n " ) }` ). join ( " \n\n " ) || "_No section interactions captured._"}` : "_No interaction discovery captured._"}
632
633 ## Scene Implementation Contract
634
635 ${ pageScenes . map (( scene ) => `### ${ scene . sectionId }: ${ scene . implementation . primitive || scene . implementation . model } \n\n Runtime adapter: \` ${ scene . implementation . runtimeAdapter || "custom"} \` ; preferred startup: \` ${ scene . implementation . runtimeModule || "/site-clone/rp-interactions-bootstrap.mjs"} \`\n\n Scene marker: \` data-rp-scene="${ scene . id }" \`\n\n Markers: \n ${ Object . entries ( scene . implementation . markerContract || {}). map (([ role , marker ]) => `- ${ role }: \` ${ marker } \` ` ). join ( " \n " ) || "- none"} \n\n Recipe: \n ${ ( scene . implementation . recipe ?. steps || []). map (( step , index ) => `${ index + 1 }. ${ step }` ). join ( " \n " ) || "- custom"} \n\n Assertions: \n ${ ( scene . implementation . assertions || []). map (( assertion ) => `- \` ${ assertion . kind } \` : ${ JSON . stringify ( assertion ) }` ). join ( " \n " ) || "- none"} \n\n Requirements: \n ${ scene . implementation . requirements . map (( requirement ) => `- ${ requirement }` ). join ( " \n " ) } \n\n Acceptance: \n ${ scene . implementation . acceptance . map (( criterion ) => `- ${ criterion }` ). join ( " \n " ) } \n\n Evidence: \n ${ scene . states . map (( state ) => `- ${ state . kind }: ${ state . screenshot || "state data in scene-contract.json"}` ). join ( " \n " ) } \n ${ scene . manualContentInventory ? " \n - Manual content inventory required: this interactive collection was not captured as a structured repeater; preserve all source items before coding. \n " : ""}` ). join ( " \n\n " ) || "_No stateful scene contract captured._"}
636
637 ## Media
638
639 ${ ( page . interactionDiscovery ?. media || []). map (( media ) => `- ${ media . role } ${ media . provider || media . tag }: ${ media . src || "deferred source"} (autoplay: ${ Boolean ( media . playback ?. autoplay ) }, loop: ${ Boolean ( media . playback ?. loop ) }, muted: ${ Boolean ( media . playback ?. muted ) })` ). join ( " \n " ) || "_No embedded media captured._"}
640
641 ## Footer
642
643 ${ page . footer ?. legalText || "_No structured footer content captured._"}
644
645 ## Links
646
647 ${ ( page . links || []). slice ( 0 , 80 ). map (( link ) => `- [${ link . text || link . url }](${ link . url })` ). join ( " \n " ) || "_No links captured._"}
648 ` ;
649 await writeText (path. join (dir, `${ safePageName ( page ) }.spec.md` ), body);
650 }
651 }
652
653 if ( import . meta .url === `file://${ process . argv [ 1 ] }` ) {
654 main (). catch (( error ) => {
655 console. error (error.stack || error.message);
656 process. exit ( 1 );
657 });
658 }