Setting the file. One moment. Replay Extraction · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
scripts/replay-extraction.mjs
scripts/replay-extraction.mjs
JavaScript·107 lines·5 KB
;
8
9const REQUIRED_PACKET_TYPES = new Set([
10 "page", "assets", "seo", "tokens", "interaction-map", "scene-contract",
11 "layout-blueprint", "ui-normalization", "control-state-contract", "visual-assets",
12]);
13
14export async function replayExtraction({ outputDir, captureId, decisionPatches = [] }) {
15 const docs = docsDir(outputDir);
16 const latest = captureId ? null : await readJson(path.join(docs, "extraction", "latest.json"));
17 const sourceCaptureId = captureId || latest.captureId;
18 const sourceDir = path.join(docs, "extraction", sourceCaptureId);
19 const integrity = await verifyFrozenManifest(sourceDir);
20 if (!integrity.ok) throw new Error(`Cannot replay corrupt extraction: ${integrity.failures.join("; ")}`);
21 const pageResolution = await readJson(path.join(sourceDir, "page-resolution.spec.json"));
22 const pageCapture = await readJson(path.join(sourceDir, "page-capture.spec.json"));
23 const capturedContentSemantics = pageCapture.extensions?.["wix.replatform.content-boundary"]?.semanticsVersion;
24 if (capturedContentSemantics !== CONTENT_EXTRACTION_SEMANTICS_VERSION) {
25 throw new Error(`Capture ${sourceCaptureId} uses obsolete content-extraction semantics (${capturedContentSemantics || "unversioned"}); recapture the source page before replay.`);
26 }
27 const packetDir = path.join(sourceDir, "observations", "packets");
28 const packetFiles = (await readdir(packetDir)).filter((name) => name.endsWith(".packet.json")).sort();
29 const packets = new Map();
30 for (const file of packetFiles) {
31 const packet = await readJson(path.join(packetDir, file));
32 if (packet.schemaVersion !== EXTRACTION_SCHEMA_VERSION || packet.kind !== "observation-packet") {
33 throw new Error(`Invalid observation packet: ${file}`);
34 }
35 if (packet.sourceFingerprint !== pageCapture.sourceFingerprint) {
36 throw new Error(`Packet source fingerprint changed: ${file}`);
37 }
38 packets.set(packet.packetType, packet.payload);
39 }
40 const missing = [...REQUIRED_PACKET_TYPES].filter((type) => !packets.has(type));
41 if (missing.length) throw new Error(`Replay requires missing packet type(s): ${missing.join(", ")}`);
42 const page = structuredClone(packets.get("page"));
43 for (const [name, screenshotRef] of Object.entries(page.screenshots || {})) {
44 page.screenshots[name] = path.join(sourceDir, "observations", screenshotRef);
45 }
46 const packetHashes = integrity.manifest.observationRefs
47 .filter((entry) => entry.ref.startsWith("observations/packets/"))
48 .map((entry) => ({ ref: entry.ref, hash: entry.hash }));
49 const replayKey = semanticHash({
50 sourceCaptureId,
51 sourceFingerprint: pageCapture.sourceFingerprint,
52 packetHashes,
53 decisionPatches,
54 }).slice(0, 12);
55 const replayCaptureId = `${sourceCaptureId}-replay-${replayKey}`;
56 const result = await assembleExtraction({
57 outputDir,
58 requestedUrl: pageResolution.source.requestedUrl,
59 resolvedUrl: pageResolution.source.resolvedUrl,
60 canonicalUrl: pageResolution.source.canonicalUrl,
61 page,
62 assets: packets.get("assets"),
63 seo: packets.get("seo"),
64 tokens: packets.get("tokens"),
65 interactionMap: packets.get("interaction-map"),
66 sceneContract: packets.get("scene-contract"),
67 layoutBlueprint: packets.get("layout-blueprint"),
68 uiNormalization: packets.get("ui-normalization"),
69 controlStateContract: packets.get("control-state-contract"),
70 visualAssets: packets.get("visual-assets"),
71 sourceFingerprint: pageCapture.sourceFingerprint,
72 sourceFingerprintIncludesContentSemantics: true,
73 captureId: replayCaptureId,
74 decisionPatches,
75 pageResolutionArtifact: pageResolution,
76 });
77 const sourceHashes = new Map(integrity.manifest.specs.map((entry) => [entry.id, entry.hash]));
78 const changedSpecs = result.manifest.specs
79 .filter((entry) => sourceHashes.get(entry.id) !== entry.hash)
80 .map((entry) => ({ id: entry.id, before: sourceHashes.get(entry.id) || null, after: entry.hash }));
81 const report = {
82 schemaVersion: EXTRACTION_SCHEMA_VERSION,
83 sourceCaptureId,
84 replayCaptureId,
85 sourceFingerprint: pageCapture.sourceFingerprint,
86 reopenedSource: false,
87 packetCount: packetFiles.length,
88 changedSpecs,
89 manifestHash: result.manifest.manifestHash,
90 };
91 await writeJson(path.join(result.extractionDir, "replay-report.json"), report);
92 return { ...result, replayReport: report };
93}
94
95async function main() {
96 const args = parseArgs();
97 if (!args.out) throw new Error("--out is required");
98 const patches = args["decision-patches"] ? await readJson(path.resolve(String(args["decision-patches"]))) : [];
99 const result = await replayExtraction({
100 outputDir: path.resolve(String(args.out)),
101 captureId: args.capture ? String(args.capture) : undefined,
102 decisionPatches: Array.isArray(patches) ? patches : patches.patches || [],
103 });
104 console.log(JSON.stringify(result.replayReport, null, 2));
105}
106
107if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });