#!/usr/bin/env node
import path from "node:path";
import { copyFile, readdir, readFile, stat } from "node:fs/promises";
import { docsDir, ensureDir, parseArgs, readJson, writeJson } from "./lib/common.mjs";
import {
EXTRACTION_BUDGETS,
EXTRACTION_SCHEMA_VERSION,
applyDecisionPatch,
assertValidArtifact,
createExtractionManifest,
createDecisionRequest,
createGap,
freezeSpec,
semanticHash,
sha256,
writeFrozenExtraction,
} from "./lib/extraction-contract.mjs";
import { loadApprovedRegistry, selectRegistryItem } from "./lib/component-registry.mjs";
import { classifyObservedUnit, planRecursiveUnitJobs } from "./lib/extraction-detectors.mjs";
import {
CONTENT_EXTRACTION_SEMANTICS_VERSION,
inspectContentObject,
removeContaminatedContent,
} from "./lib/content-boundary.mjs";
const LEGACY_OBSERVATION_FILES = [
"assets.json",
"control-state-contract.json",
"interaction-map.json",
"layout-blueprint.json",
"scene-contract.json",
"seo.json",
"tokens.json",
"ui-normalization.json",
"visual-assets.json",
];
export async function assembleExtraction({
outputDir,
requestedUrl,
resolvedUrl,
canonicalUrl,
page,
assets = {},
seo = {},
tokens = {},
interactionMap = {},
sceneContract = {},
layoutBlueprint = {},
uiNormalization = {},
controlStateContract = {},
visualAssets = {},
sourceFingerprint,
sourceFingerprintIncludesContentSemantics = false,
captureId: suppliedCaptureId,
decisionPatches = [],
pageResolutionArtifact = null,
}) {
const docs = docsDir(outputDir);
const observedSourceFingerprint = sourceFingerprint || sha256({
resolvedUrl,
title: page?.title || "",
sections: (page?.sections || []).map((section) => ({ id: section.id, heading: section.heading, text: section.text })),
});
const fingerprint = sourceFingerprintIncludesContentSemantics
? observedSourceFingerprint
: sha256({
sourceFingerprint: observedSourceFingerprint,
contentExtractionSemantics: CONTENT_EXTRACTION_SEMANTICS_VERSION,
});
const captureId = suppliedCaptureId || `home-${fingerprint.slice(0, 12)}`;
const extractionDir = path.join(docs, "extraction", captureId);
const gaps = [];
const decisionRequests = [];
const appliedDecisions = [];
const patchesByRequest = new Map(decisionPatches.map((patch) => [patch.requestId, patch]));
const rawEvidenceEstimate = Buffer.byteLength(JSON.stringify({ page, assets, seo, tokens, interactionMap, sceneContract, layoutBlueprint, uiNormalization, controlStateContract, visualAssets }));
const recursiveJobs = planRecursiveUnitJobs(page?.sections || []);
const pageCaptureGapRefs = [];
if (rawEvidenceEstimate > EXTRACTION_BUDGETS.maxRawEvidenceBytes) {
page = { ...page, rawDomSnapshots: {}, normalizedDomSnapshots: {} };
const gap = createGap({
id: "gap:page-capture:evidence-budget",
ownerUnitId: "page-capture:home",
missingFields: ["rawDomSnapshots", "normalizedDomSnapshots"],
reason: `Raw evidence was estimated at ${rawEvidenceEstimate} bytes and exceeded the ${EXTRACTION_BUDGETS.maxRawEvidenceBytes}-byte retention budget. DOM snapshots were omitted by priority while structured observations were retained.`,
evidenceRefs: [],
dependencyClosure: (page?.sections || []).map((section, index) => section.id || `section-${String(index + 1).padStart(3, "0")}`),
affectedAcceptance: ["packet-replay"],
assumptions: ["Structured page/unit evidence remains sufficient to assemble a viable home-page shell."],
omissions: ["DOM-only replay until a targeted recapture is authorized."],
});
gaps.push(gap);
pageCaptureGapRefs.push(gap.id);
}
const pageResolution = pageResolutionArtifact || freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "page-resolution",
id: "page-resolution:home",
pageKey: "home",
destinationRoute: "/",
status: "accepted",
source: {
requestedUrl,
resolvedUrl,
canonicalUrl: canonicalUrl || resolvedUrl,
},
evidence: [{ kind: "navigation", ref: "observations/page.json" }],
gapRefs: [],
});
const pageCapture = freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "page-capture",
id: "page-capture:home",
pageKey: "home",
status: pageCaptureGapRefs.length ? "provisional" : "accepted",
sourceFingerprint: fingerprint,
viewportSet: Object.entries(page?.screenshots || {}).map(([name, screenshotRef]) => ({ name, screenshotRef: `observations/screenshots/${path.basename(String(screenshotRef))}` })),
sectionOrder: recursiveJobs.jobs.filter((job) => job.unitKind === "section").map((job) => job.id),
ignoredSurfaces: page?.ignoredSurfaces || [],
normalizationActions: page?.normalizationActions || [],
observationRefs: LEGACY_OBSERVATION_FILES.map((name) => `observations/${name}`),
extensions: {
"wix.replatform.content-boundary": { semanticsVersion: CONTENT_EXTRACTION_SEMANTICS_VERSION },
},
gapRefs: pageCaptureGapRefs,
}, { dependencyHashes: [pageResolution.hash], gapIds: pageCaptureGapRefs });
const foundationGapRefs = [];
if (!hasFoundationEvidence(tokens)) {
const gap = createGap({
id: "gap:foundation:tokens",
ownerUnitId: "foundation:home",
missingFields: ["tokens"],
reason: "No reliable page-wide token evidence was projected from the capture.",
evidenceRefs: ["observations/tokens.json"],
dependencyClosure: ["shared-chrome:home", ...(page?.sections || []).map((section, index) => section.id || `section-${String(index + 1).padStart(3, "0")}`)],
assumptions: ["Use only directly observed computed values local to each unit."],
omissions: ["Unobserved global token aliases."],
});
gaps.push(gap);
foundationGapRefs.push(gap.id);
}
const foundation = freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "foundation",
id: "foundation:home",
pageKey: "home",
status: foundationGapRefs.length ? "provisional" : "accepted",
tokens: sanitizeEvidence(tokens),
fonts: sanitizeEvidence(assets?.fonts || {}),
primitives: collectFoundationPrimitives(page),
globalCssIntent: collectGlobalCssIntent(tokens),
evidenceRefs: ["observations/tokens.json", "observations/assets.json", "observations/page.json"],
acceptance: { required: ["fonts-load", "direction-matches", "page-background-matches", "token-references-resolve"] },
gapRefs: foundationGapRefs,
}, { dependencyHashes: [pageCapture.hash], gapIds: foundationGapRefs });
const metadata = freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "metadata",
id: "metadata:home",
pageKey: "home",
status: "accepted",
document: sanitizeEvidence(seo),
evidenceRefs: ["observations/seo.json", "observations/page.json"],
acceptance: { required: ["title", "description-when-observed", "canonical-when-observed", "lang-and-dir"] },
gapRefs: [],
}, { dependencyHashes: [pageCapture.hash] });
const sharedChromeGapRefs = [];
if (!page?.chrome && !page?.footer) {
const gap = createGap({
id: "gap:shared-chrome:unobserved",
ownerUnitId: "shared-chrome:home",
missingFields: ["header", "footer"],
reason: "The capture did not identify a reliable header or footer boundary.",
evidenceRefs: ["observations/page.json"],
assumptions: ["Do not invent navigation or footer content."],
omissions: ["Unobserved shared chrome."],
});
gaps.push(gap);
sharedChromeGapRefs.push(gap.id);
}
const sharedChromeContamination = inspectContentObject({ header: page?.chrome || null, footer: page?.footer || null });
if (sharedChromeContamination.contaminated) {
const gap = contentContaminationGap("shared-chrome:home", "observations/page.json", sharedChromeContamination);
gaps.push(gap);
sharedChromeGapRefs.push(gap.id);
}
const safeSharedChrome = removeContaminatedContent({ header: page?.chrome || null, footer: page?.footer || null });
const sharedChrome = freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "shared-chrome",
id: "shared-chrome:home",
pageKey: "home",
status: sharedChromeGapRefs.length ? "provisional" : "accepted",
header: sanitizeEvidence(safeSharedChrome.header),
footer: sanitizeEvidence(safeSharedChrome.footer),
controls: sanitizeEvidence(controlStateContract?.controls || []),
visualAssets: selectChromeAssets(visualAssets),
behavior: selectOwnedInteractions(interactionMap, "shared-chrome"),
evidenceRefs: ["observations/page.json", "observations/control-state-contract.json", "observations/visual-assets.json"],
acceptance: { required: ["visible-content", "navigation-hierarchy", "logo-identity", "responsive-chrome", "keyboard-navigation"] },
gapRefs: sharedChromeGapRefs,
}, { dependencyHashes: [foundation.hash, metadata.hash], gapIds: sharedChromeGapRefs });
const specs = [foundation, metadata, sharedChrome];
const specPaths = [
{ spec: foundation, relativePath: "foundation.spec.json" },
{ spec: metadata, relativePath: "metadata.spec.json" },
{ spec: sharedChrome, relativePath: "shared-chrome.spec.json" },
];
const layouts = new Map((layoutBlueprint?.pages || []).flatMap((entry) => entry.sections || []).map((entry) => [entry.sectionId, entry]));
const normalizations = new Map((uiNormalization?.sections || []).map((entry) => [entry.sectionId, entry]));
const scenes = new Map((sceneContract?.scenes || []).map((entry) => [entry.sectionId, entry]));
const recursiveSpecById = new Map();
const jobsByDepth = [...recursiveJobs.jobs].sort((a, b) => b.depth - a.depth || a.id.localeCompare(b.id));
for (const job of jobsByDepth) {
const decision = resolveClassification({
value: job.value,
unitId: job.id,
evidenceRef: job.evidenceRef,
patchesByRequest,
decisionRequests,
appliedDecisions,
});
const diagnosticGaps = job.unitKind === "section"
? gapsForSection(job.value, job.id, page, decision.classification)
: [];
const contentOptions = { allowVisibleCode: Array.isArray(job.value?.visibleCodeTexts) && job.value.visibleCodeTexts.length > 0 };
const contamination = inspectContentObject(job.value, contentOptions);
const contaminationGaps = contamination.contaminated
? [contentContaminationGap(job.id, job.evidenceRef, contamination)]
: [];
const unitGaps = [...decision.gaps, ...diagnosticGaps, ...contaminationGaps];
gaps.push(...unitGaps);
const gapRefs = unitGaps.map((gap) => gap.id);
const isSection = job.unitKind === "section";
const behavior = isSection
? scenes.get(job.id) || selectOwnedInteractions(interactionMap, job.id)
: job.value.behavior || selectOwnedInteractions(interactionMap, job.id);
const unit = freezeSpec({
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "unit",
id: job.id,
unitKind: job.unitKind,
pageKey: "home",
parentUnitId: job.parentUnitId,
order: job.order,
status: gapRefs.length ? "provisional" : "accepted",
classification: {
...decision.classification,
capabilityRequirements: capabilityRequirementsFor(job.value, decision.classification),
},
scope: {
evidenceRef: job.evidenceRef,
...(isSection ? { screenshotRefs: Object.values(page?.screenshots || {}).map((item) => `observations/screenshots/${path.basename(String(item))}`) } : {}),
},
content: contentFor(job.value, contentOptions),
assets: sanitizeEvidence(job.value.images || job.value.assets || []),
layers: sanitizeEvidence(job.value.layers || (isSection ? layouts.get(job.id)?.background?.layers : []) || []),
layout: sanitizeEvidence((isSection ? layouts.get(job.id) : null) || job.value.layout || {}),
styleables: sanitizeEvidence(job.value.styleables || (job.value.layoutEvidence?.regions || []).map((region, index) => ({
id: `${job.id}:styleable-${String(index + 1).padStart(3, "0")}`,
role: region.role,
categories: region.styleableCategories || [],
computedByViewport: { desktop: region.computedStyle || {} },
authoredCss: region.authoredCss || [],
}))),
behavior: sanitizeEvidence(behavior || {}),
children: job.childIds,
reconstruction: isSection ? {
strategy: knownClassification(job.value) ? "registry-or-deterministic" : "bounded-custom",
foundationRef: foundation.id,
sharedChromeRef: sharedChrome.id,
normalization: sanitizeEvidence(normalizations.get(job.id) || null),
} : {
strategy: "registry-or-custom",
foundationRef: foundation.id,
},
acceptance: acceptanceFor(job.value),
diagnostics: unitGaps.map((gap) => ({ gapRef: gap.id, reason: gap.reason })),
provenance: {
sourceRef: job.evidenceRef,
sourceUrl: resolvedUrl,
captureFingerprint: fingerprint,
detectorVersions: decision.classification.detectorVersions,
recursiveJobVersion: recursiveJobs.version,
},
extensions: {
"wix.replatform.content-boundary": {
semanticsVersion: CONTENT_EXTRACTION_SEMANTICS_VERSION,
visibleCode: contentOptions.allowVisibleCode,
},
},
gapRefs,
}, {
dependencyHashes: [foundation.hash, ...job.childIds.map((id) => recursiveSpecById.get(id)?.hash).filter(Boolean)],
gapIds: gapRefs,
});
recursiveSpecById.set(job.id, unit);
specs.push(unit);
const sectionRoot = job.id.split(":component-")[0];
specPaths.push({
spec: unit,
relativePath: isSection
? `sections/${job.id}/section.spec.json`
: `sections/${sectionRoot}/components/${pathSafe(job.id)}.spec.json`,
});
}
const observationRefs = await writeObservations(extractionDir, {
page,
assets,
seo,
tokens,
interactionMap,
sceneContract,
layoutBlueprint,
uiNormalization,
controlStateContract,
visualAssets,
recursiveJobs: {
version: recursiveJobs.version,
jobs: recursiveJobs.jobs.map(({ value, ...job }) => job),
ownership: recursiveJobs.ownership,
conflicts: recursiveJobs.conflicts,
hash: recursiveJobs.hash,
},
}, outputDir, fingerprint);
if (decisionRequests.length > EXTRACTION_BUDGETS.maxAgentDecisions) {
throw new Error(`Typed decision budget exceeded: ${decisionRequests.length} > ${EXTRACTION_BUDGETS.maxAgentDecisions}`);
}
const decisionRequestsArtifact = {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
requests: decisionRequests,
};
const appliedDecisionsArtifact = {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
decisions: appliedDecisions,
};
await writeJson(path.join(extractionDir, "decisions", "requests.json"), decisionRequestsArtifact);
await writeJson(path.join(extractionDir, "decisions", "applied.json"), appliedDecisionsArtifact);
observationRefs.push(
{ ref: "decisions/requests.json", hash: semanticHash(decisionRequestsArtifact) },
{ ref: "decisions/applied.json", hash: semanticHash(appliedDecisionsArtifact) },
);
const manifest = createExtractionManifest({ captureId, pageResolution, pageCapture, specs, gaps, observationRefs });
await writeFrozenExtraction({ extractionDir, pageResolution, pageCapture, specs, gaps, manifest, specPaths });
const buildPlan = createBuildPlan(manifest, specs);
await ensureDir(path.join(docs, "build"));
await writeJson(path.join(docs, "build", "build-plan.json"), buildPlan);
const approvedRegistry = await loadApprovedRegistry();
const componentSelection = {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
manifestHash: manifest.manifestHash,
selections: specs.filter((spec) => spec.kind === "unit").map((spec) => ({
unitId: spec.id,
contract: spec.classification,
...selectRegistryItem({
kind: spec.classification.kind,
requiresClientRuntime: requiresClientRuntime(spec),
hardRequirements: ["license", "accessibility", "responsive", "ssr", "rtl", "reducedMotion"],
capabilityRequirements: spec.classification.capabilityRequirements,
}, approvedRegistry),
})),
rejectedRegistryItems: approvedRegistry.rejected,
};
await writeJson(path.join(docs, "build", "component-selection.json"), componentSelection);
await ensureDir(path.join(docs, "qa"));
await writeJson(path.join(docs, "qa", "registry-opportunities.json"), {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
manifestHash: manifest.manifestHash,
opportunities: componentSelection.selections
.filter((selection) => selection.strategy === "bounded-custom")
.map((selection) => ({
unitId: selection.unitId,
contractKind: selection.contract.kind,
variant: selection.contract.variant,
reason: "No human-approved local registry item met every hard requirement; bounded custom building remains available.",
requiredCapabilities: ["license", "accessibility", "responsive", "ssr", "rtl", "reducedMotion"],
capabilityRequirements: selection.contract.capabilityRequirements || {},
registryRejections: selection.rejected || [],
sourceNeutral: true,
})),
});
await writeJson(path.join(docs, "extraction", "latest.json"), { captureId, manifestRef: `${captureId}/extraction-manifest.json`, manifestHash: manifest.manifestHash });
await enforceArtifactBudgets(extractionDir, manifest);
return { captureId, extractionDir, pageResolution, pageCapture, specs, gaps, manifest, buildPlan };
}
export function createBuildPlan(manifest, specs) {
const byId = new Map(specs.map((spec) => [spec.id, spec]));
const ordered = [...specs].sort((a, b) => buildRank(a) - buildRank(b) || (a.order || 0) - (b.order || 0) || a.id.localeCompare(b.id));
const plan = {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "build-plan",
pageKey: "home",
route: "/",
manifestHash: manifest.manifestHash,
status: manifest.status,
units: ordered.map((spec) => ({
id: spec.id,
kind: spec.kind,
unitKind: spec.unitKind || spec.kind,
status: spec.status,
hash: spec.hash,
gapRefs: spec.gapRefs || [],
dependencies: dependencyIds(spec, byId),
})),
};
assertValidArtifact(plan, "build-plan");
return plan;
}
function buildRank(spec) {
if (spec.kind === "foundation") return 0;
if (spec.kind === "metadata") return 1;
if (spec.kind === "shared-chrome") return 2;
if (spec.unitKind === "component") return 3;
return 4;
}
function dependencyIds(spec, byId) {
const ids = [spec.reconstruction?.foundationRef, spec.reconstruction?.sharedChromeRef, ...(spec.children || [])].filter(Boolean);
return ids.filter((id) => byId.has(id));
}
async function writeObservations(extractionDir, values, outputDir, sourceFingerprint) {
const observationsDir = path.join(extractionDir, "observations");
await ensureDir(observationsDir);
const page = structuredClone(values.page || {});
const screenshotDir = path.join(observationsDir, "screenshots");
for (const [name, sourceRef] of Object.entries(page.screenshots || {})) {
const sourcePath = path.isAbsolute(sourceRef) ? sourceRef : path.resolve(outputDir, sourceRef);
const filename = path.basename(sourceRef);
try {
await ensureDir(screenshotDir);
await copyFile(sourcePath, path.join(screenshotDir, filename));
page.screenshots[name] = `screenshots/${filename}`;
} catch {
page.screenshots[name] = `screenshots/${filename}`;
}
}
const mapping = {
"page.json": page,
"assets.json": values.assets,
"seo.json": values.seo,
"tokens.json": values.tokens,
"interaction-map.json": values.interactionMap,
"scene-contract.json": values.sceneContract,
"layout-blueprint.json": values.layoutBlueprint,
"ui-normalization.json": values.uiNormalization,
"control-state-contract.json": values.controlStateContract,
"visual-assets.json": values.visualAssets,
"recursive-jobs.json": values.recursiveJobs,
};
const refs = [];
const packetDir = path.join(observationsDir, "packets");
await ensureDir(packetDir);
const writePacket = async (packetName, packet) => {
assertValidArtifact(packet, "observation-packet");
await writeJson(path.join(packetDir, packetName), packet);
refs.push({ ref: `observations/packets/${packetName}`, hash: semanticHash(packet) });
};
for (const [name, value] of Object.entries(mapping)) {
const sanitized = sanitizeEvidence(value);
await writeJson(path.join(observationsDir, name), sanitized);
const packetName = name.replace(/\.json$/, ".packet.json");
const packet = {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "observation-packet",
packetType: name.replace(/\.json$/, ""),
sourceFingerprint,
viewport: null,
state: "settled",
payload: sanitized,
};
await writePacket(packetName, packet);
}
for (const [name, screenshotRef] of Object.entries(page.screenshots || {})) {
const packetName = `page.viewport-${pathSafe(name)}.packet.json`;
await writePacket(packetName, {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "observation-packet",
packetType: "page-viewport",
sourceFingerprint,
viewport: { name, screenshotRef },
state: "settled",
payload: sanitizeEvidence({
responsiveTextGeometry: page.responsiveTextGeometry?.[name] || null,
viewportEvidence: page.viewportEvidence?.[name] || null,
}),
});
}
let statePacketIndex = 0;
for (const scene of values.sceneContract?.scenes || []) {
for (const interaction of scene.states || []) {
const observedStates = Array.isArray(interaction.states) && interaction.states.length ? interaction.states : [null];
for (const observedState of observedStates) {
statePacketIndex += 1;
const stateName = typeof observedState === "string"
? observedState
: observedState?.id || observedState?.name || observedState?.phase || interaction.kind || "observed";
await writePacket(`behavior-state-${String(statePacketIndex).padStart(4, "0")}.packet.json`, {
schemaVersion: EXTRACTION_SCHEMA_VERSION,
kind: "observation-packet",
packetType: "behavior-state",
sourceFingerprint,
viewport: null,
state: String(stateName),
payload: sanitizeEvidence({
sceneId: scene.id,
sectionId: scene.sectionId,
interactionId: interaction.id,
interactionKind: interaction.kind,
observation: observedState,
}),
});
}
}
}
return refs;
}
export function sanitizeEvidence(value, key = "") {
if (Array.isArray(value)) return value.map((item) => sanitizeEvidence(item, key));
if (!value || typeof value !== "object") {
if (typeof value === "string" && /authorization|cookie|set-cookie|password|token|secret|formvalue/i.test(key)) return "[redacted]";
if (typeof value === "string" && /domsnapshots?/i.test(key)) return redactDomSecrets(value);
if (typeof value === "string" && /url$/i.test(key)) return redactUrl(value);
return value;
}
const result = {};
for (const [childKey, childValue] of Object.entries(value)) {
if (/^(authorization|cookie|set-cookie|password|token|secret|formValue)$/i.test(childKey)) {
result[childKey] = "[redacted]";
continue;
}
result[childKey] = sanitizeEvidence(childValue, childKey);
}
return result;
}
function redactDomSecrets(html) {
return String(html)
.replace(/(]*\bvalue\s*=\s*["'])[^"']*(["'][^>]*>)/gi, "$1[redacted]$2")
.replace(/(["']?(?:authorization|access[_-]?token|refresh[_-]?token|api[_-]?key|client[_-]?secret|password)["']?\s*[:=]\s*["'])[^"']+(["'])/gi, "$1[redacted]$2")
.replace(/(]*(?:csrf|token|secret)[^>]*\bcontent\s*=\s*["'])[^"']*(["'][^>]*>)/gi, "$1[redacted]$2");
}
function redactUrl(value) {
try {
const url = new URL(value);
for (const key of [...url.searchParams.keys()]) {
if (/token|key|secret|auth|password|session|email|code/i.test(key)) url.searchParams.set(key, "[redacted]");
}
return url.toString();
} catch {
return value;
}
}
function hasFoundationEvidence(tokens) {
return Boolean(tokens && typeof tokens === "object" && Object.keys(tokens).some((key) => {
const value = tokens[key];
return Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : Boolean(value);
}));
}
function collectFoundationPrimitives(page) {
const kinds = new Set();
for (const section of page?.sections || []) {
const classification = classifyObservedUnit(section, { unitKind: "section", evidenceRef: "observations/page.json" });
if (["button-group", "card", "accordion", "tabs", "gallery", "carousel", "navigation"].includes(classification.kind)) kinds.add(classification.kind);
}
return [...kinds].sort().map((kind) => ({ kind, evidence: "observations/page.json" }));
}
function collectGlobalCssIntent(tokens) {
return {
direction: tokens?.direction || tokens?.typography?.direction || "unknown",
colorRoles: tokens?.colors || tokens?.colorRoles || {},
typography: tokens?.typography || {},
spacing: tokens?.spacing || {},
breakpoints: tokens?.breakpoints || {},
};
}
function selectChromeAssets(visualAssets) {
return sanitizeEvidence({ logos: visualAssets?.logos || [], icons: visualAssets?.icons || [] });
}
function selectOwnedInteractions(interactionMap, owner) {
const interactions = interactionMap?.interactions || [];
if (owner === "shared-chrome") return interactions.filter((item) => /header|footer|nav|chrome/i.test(`${item.owner || ""} ${item.scope || ""} ${item.target?.selector || ""}`));
return interactions.filter((item) => item.sectionId === owner || item.ownerUnitId === owner);
}
function resolveClassification({ value, unitId, evidenceRef, patchesByRequest, decisionRequests, appliedDecisions }) {
const classification = classifyObservedUnit(value, { unitKind: unitId.includes(":component-") ? "component" : "section", evidenceRef });
if (classification.confidence === "high") return { classification, gaps: [] };
if (classification.confidence === "low") {
return {
classification,
gaps: [createGap({
id: `gap:${unitId}:classification`,
ownerUnitId: unitId,
missingFields: ["classification"],
reason: "Classification confidence is below the calibrated decision band.",
evidenceRefs: [evidenceRef],
confidence: "low",
affectedAcceptance: ["classification-specific-behavior", "visual-geometry"],
assumptions: ["Use the bounded custom builder and preserve observed content/order/layout evidence."],
omissions: ["Unverified classification-specific behavior."],
})],
};
}
const choices = [...new Set([classification.kind, value?.variant, "static-content", "bounded-custom"].filter(Boolean))];
const request = createDecisionRequest({
id: `decision:${unitId}:classification`,
decisionType: "classification",
allowedChoices: choices,
evidenceRefs: [evidenceRef],
detectorVersions: { sectionClassification: "0083.1" },
responseSchema: { type: "string", enum: choices },
});
decisionRequests.push(request);
const patch = patchesByRequest.get(request.id);
if (patch) {
try {
const applied = applyDecisionPatch(request, patch);
appliedDecisions.push(applied);
return { classification: { ...classification, kind: applied.choice, confidence: applied.confidence }, gaps: [] };
} catch (error) {
return {
classification,
gaps: [createGap({
id: `gap:${unitId}:classification`,
ownerUnitId: unitId,
missingFields: ["classification"],
reason: `The typed classification patch was invalid: ${error.message}`,
evidenceRefs: [evidenceRef, `decisions/requests.json#/${request.id}`],
confidence: "medium",
affectedAcceptance: ["classification-specific-behavior", "visual-geometry"],
assumptions: ["Use the bounded custom builder until a valid typed decision is supplied."],
omissions: ["Unverified classification-specific behavior."],
})],
};
}
}
return {
classification,
gaps: [createGap({
id: `gap:${unitId}:classification`,
ownerUnitId: unitId,
missingFields: ["classification"],
reason: "Classification requires a bounded typed orchestrating-agent decision.",
evidenceRefs: [evidenceRef, `decisions/requests.json#/${request.id}`],
confidence: "medium",
affectedAcceptance: ["classification-specific-behavior", "visual-geometry"],
assumptions: ["Use the bounded custom builder if the decision is not supplied before assembly."],
omissions: ["Unverified classification-specific behavior."],
})],
};
}
function gapsForSection(section, sectionId, page, classification) {
const gaps = [];
const warnings = [
...(section?.diagnostics?.warnings || []),
...(page?.sectionDiagnostics?.warnings || []).filter((warning) => String(warning).includes(sectionId)),
];
if (warnings.length) {
gaps.push(createGap({
id: `gap:${sectionId}:diagnostics`,
ownerUnitId: sectionId,
missingFields: ["diagnostics"],
reason: warnings.join(" "),
evidenceRefs: [`observations/page.json#/${sectionId}`],
confidence: classification.confidence,
affectedAcceptance: ["classification-specific-behavior", "visual-geometry"],
assumptions: ["Use the bounded custom builder and preserve observed content/order/layout evidence."],
omissions: ["Unverified classification-specific behavior."],
}));
}
if (!section?.layoutEvidence && !section?.layout) {
gaps.push(createGap({
id: `gap:${sectionId}:layout-evidence`,
ownerUnitId: sectionId,
missingFields: ["layout", "styleables"],
reason: "Viewport layout and styleable-category evidence was not captured for this visible section.",
evidenceRefs: [`observations/page.json#/${sectionId}`],
confidence: classification.confidence,
affectedAcceptance: ["responsive-layout", "visible-layers", "visual-geometry"],
assumptions: ["Preserve observed content and order in a bounded fluid layout."],
omissions: ["Unverified authored CSS context and viewport-specific property unions."],
}));
}
return gaps;
}
function contentFor(value, options = {}) {
return sanitizeEvidence(removeContaminatedContent({
heading: value?.heading || "",
text: value?.text || "",
label: value?.label || "",
links: value?.links || [],
items: value?.items || [],
accessibleName: value?.accessibleName || value?.ariaLabel || value?.a11y?.accessibleName || "",
icon: value?.icon || null,
}, options));
}
function contentContaminationGap(unitId, evidenceRef, contamination) {
const fields = contamination.findings.map((finding) => finding.path).filter(Boolean);
return createGap({
id: `gap:${unitId}:content-contamination`,
ownerUnitId: unitId,
missingFields: fields.length ? fields : ["content.text"],
reason: `Content-bearing DOM evidence matched non-rendered source-code signals (${contamination.reasons.join(", ")}). The affected strings were omitted instead of being emitted as page copy.`,
evidenceRefs: [evidenceRef],
affectedAcceptance: ["visible-content", "content-fidelity"],
assumptions: ["Reliable structure, layout, assets, and uncontaminated copy can still be implemented for this unit."],
omissions: ["Strings that appear to originate from executable script, stylesheet, template, or embedded source markup."],
});
}
function capabilityRequirementsFor(value, classification) {
if (value?.capabilityRequirements && typeof value.capabilityRequirements === "object") {
return sanitizeEvidence(value.capabilityRequirements);
}
if (classification.kind !== "button") return {};
const label = value?.label || value?.heading || value?.text || "";
const href = (value?.links || []).find((link) => link?.href)?.href || value?.href || "";
const icon = value?.icon || (value?.images || value?.assets || []).find((asset) => /icon/i.test(`${asset?.role || ""} ${asset?.type || ""}`));
const accessibleName = value?.accessibleName || value?.ariaLabel || value?.a11y?.accessibleName || "";
const availableSlots = [
label ? "label" : null,
href ? "href" : null,
icon ? "icon" : null,
accessibleName ? "accessibleName" : null,
].filter(Boolean);
const composition = href
? "as-link"
: icon && !label
? "icon-only"
: icon && value?.iconPosition === "end"
? "trailing-icon"
: icon
? "leading-icon"
: "text";
const axes = {};
if (value?.componentVariant || value?.visualVariant) axes.variant = value.componentVariant || value.visualVariant;
if (value?.componentSize || value?.controlSize) axes.size = value.componentSize || value.controlSize;
return {
axes,
composition,
availableSlots,
requiredSlots: availableSlots,
requiredStates: ["focus-visible", ...(value?.disabled ? ["disabled"] : [])],
};
}
function requiresClientRuntime(spec) {
const behavior = JSON.stringify(spec.behavior || {}).toLowerCase();
return /accordion|disclosure|tablist|tabpanel|carousel|menuitem|dropdown|toggle|clientstate|client-state|hydration/.test(behavior);
}
function acceptanceFor(value) {
return {
required: ["content-order", "responsive-layout", "visible-layers", "keyboard-when-interactive"],
sourceAssertions: sanitizeEvidence(value?.acceptance || value?.assertions || []),
};
}
function knownClassification(section) {
return /hero|navigation|menu|button|accordion|tabs|gallery|carousel|card|repeater|form|banner|footer|content|cta|promo|reviews/i.test(`${section?.kind || ""} ${section?.variant || ""}`);
}
function pathSafe(value) {
return value.replace(/[^a-z0-9._-]+/gi, "-");
}
async function enforceArtifactBudgets(extractionDir, manifest) {
const total = await directorySize(extractionDir);
if (total > EXTRACTION_BUDGETS.maxRawEvidenceBytes) {
const error = new Error(`Extraction evidence exceeds ${EXTRACTION_BUDGETS.maxRawEvidenceBytes} bytes (${total})`);
error.code = "EXTRACTION_EVIDENCE_BUDGET";
throw error;
}
const specsBytes = Buffer.byteLength(JSON.stringify(manifest));
if (specsBytes > EXTRACTION_BUDGETS.maxFrozenSpecBytes) {
const error = new Error(`Frozen manifest exceeds ${EXTRACTION_BUDGETS.maxFrozenSpecBytes} bytes (${specsBytes})`);
error.code = "EXTRACTION_SPEC_BUDGET";
throw error;
}
}
async function directorySize(dir) {
let total = 0;
for (const entry of await readdir(dir, { withFileTypes: true })) {
const target = path.join(dir, entry.name);
total += entry.isDirectory() ? await directorySize(target) : (await stat(target)).size;
}
return total;
}
async function main() {
const args = parseArgs();
if (!args.out) throw new Error("--out is required");
const outputDir = path.resolve(String(args.out));
const docs = docsDir(outputDir);
const pagePath = args.page || path.join(docs, "pages", "home-home.json");
const readOptional = async (name, fallback) => {
try { return await readJson(path.join(docs, name)); } catch { return fallback; }
};
const page = await readJson(pagePath);
const decisionPatchArtifact = args["decision-patches"] ? await readJson(path.resolve(String(args["decision-patches"]))) : [];
const result = await assembleExtraction({
outputDir,
requestedUrl: args.url || page.url,
resolvedUrl: page.url,
canonicalUrl: page.seo?.canonical || page.url,
page,
assets: await readOptional("assets.json", {}),
seo: await readOptional("seo.json", page.seo || {}),
tokens: await readOptional("tokens.json", {}),
interactionMap: await readOptional("interaction-map.json", {}),
sceneContract: await readOptional("scene-contract.json", {}),
layoutBlueprint: await readOptional("layout-blueprint.json", {}),
uiNormalization: await readOptional("ui-normalization.json", {}),
controlStateContract: await readOptional("control-state-contract.json", {}),
visualAssets: await readOptional("visual-assets.json", {}),
decisionPatches: Array.isArray(decisionPatchArtifact) ? decisionPatchArtifact : decisionPatchArtifact.patches || [],
});
console.log(JSON.stringify({ captureId: result.captureId, manifestHash: result.manifest.manifestHash, status: result.manifest.status }, null, 2));
}
if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });