Setting the file. One moment. Benchmark Extraction · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
Next
Script Browser Extraction Preflight
scripts/benchmark-extraction.mjs
JavaScript·163 lines·9 KB
Object.
freeze
({
9 minimumFixtures: 20,
10 segmentationPrecision: 0.90,
11 segmentationRecall: 0.95,
12 componentPrecision: 0.95,
13 componentRecall: 0.85,
14 behaviorRecall: 0.90,
15 ownership: 1,
16 qaDefectRecall: 0.95,
17 qaFalsePositiveRate: 0.05,
18 p95ExtractionMs: 10 * 60 * 1000,
19 rawEvidenceBytes: 250 * 1024 * 1024,
20 frozenSpecBytes: 10 * 1024 * 1024,
21 agentDecisions: 30,
22 agentInputTokens: 50_000,
23});
24
25export async function benchmarkExtractionCorpus({ corpusPath }) {
26 const raw = await readFile(corpusPath);
27 const corpus = JSON.parse(raw);
28 const humanApproved = corpus.status === "human-approved"
29 && corpus.approval?.human === true
30 && Boolean(corpus.approval?.reviewerId)
31 && Boolean(corpus.approval?.reviewedAt);
32 const failures = [];
33 const measurements = [];
34 let boundaryTp = 0;
35 let boundaryFp = 0;
36 let boundaryFn = 0;
37 let componentTp = 0;
38 let componentFp = 0;
39 let componentFn = 0;
40 let behaviorExpected = 0;
41 let behaviorFound = 0;
42 let ownershipExpected = 0;
43 let ownershipAccounted = 0;
44 let qaExpected = 0;
45 let qaFound = 0;
46 let qaClean = 0;
47 let qaFalsePositive = 0;
48 let agentDecisions = 0;
49
50 for (const fixture of corpus.fixtures || []) {
51 const started = performance.now();
52 const sections = fixture.sections || [];
53 const plan = planRecursiveUnitJobs(sections.map((section) => ({ id: section.id, ...section.observed })));
54 ownershipExpected += plan.jobs.length;
55 ownershipAccounted += plan.ownership.length;
56 if (plan.conflicts.length) failures.push({ fixture: fixture.id, metric: "ownership", details: plan.conflicts });
57 const expectedQa = new Set(fixture.expectedQaDefects || []);
58 const actualQa = new Set();
59 for (const section of sections) {
60 const classification = classifyObservedUnit(section.observed, { unitKind: "section", evidenceRef: `${fixture.id}#${section.id}` });
61 const predictedBoundary = classification.confidence !== "low";
62 if (predictedBoundary) boundaryTp += 1;
63 else boundaryFn += 1;
64 if (classification.confidence === "medium") agentDecisions += 1;
65 if (section.knownComponent) {
66 if (classification.confidence === "low") componentFn += 1;
67 else if (classification.kind === section.expectedKind) componentTp += 1;
68 else {
69 componentFp += 1;
70 componentFn += 1;
71 failures.push({ fixture: fixture.id, unit: section.id, metric: "component-kind", expected: section.expectedKind, actual: classification.kind });
72 }
73 } else if (classification.kind !== section.expectedKind) {
74 failures.push({ fixture: fixture.id, unit: section.id, metric: "classification", expected: section.expectedKind, actual: classification.kind });
75 }
76 const expectedStates = new Set(section.expectedStates || []);
77 const observedStates = new Set(section.observed?.behavior?.states || []);
78 behaviorExpected += expectedStates.size;
79 for (const state of expectedStates) if (observedStates.has(state)) behaviorFound += 1;
80 for (const defect of detectQaDefects(section.id, section.observed)) actualQa.add(defect);
81 }
82 qaExpected += expectedQa.size;
83 for (const defect of expectedQa) {
84 if (actualQa.has(defect)) qaFound += 1;
85 else failures.push({ fixture: fixture.id, metric: "qa-missed-defect", defect });
86 }
87 const cleanAssertions = Math.max(1, sections.length * 6 - expectedQa.size);
88 qaClean += cleanAssertions;
89 for (const defect of actualQa) if (!expectedQa.has(defect)) {
90 qaFalsePositive += 1;
91 failures.push({ fixture: fixture.id, metric: "qa-false-positive", defect });
92 }
93 measurements.push({ fixture: fixture.id, extractionMs: Number((performance.now() - started).toFixed(3)), rawEvidenceBytes: Buffer.byteLength(JSON.stringify(fixture)), frozenSpecBytes: Buffer.byteLength(JSON.stringify(plan)) });
94 }
95 const metrics = {
96 fixtureCount: corpus.fixtures?.length || 0,
97 coverage: [...new Set((corpus.fixtures || []).flatMap((fixture) => fixture.features || []))].sort(),
98 segmentationPrecision: ratio(boundaryTp, boundaryTp + boundaryFp),
99 segmentationRecall: ratio(boundaryTp, boundaryTp + boundaryFn),
100 componentPrecision: ratio(componentTp, componentTp + componentFp),
101 componentRecall: ratio(componentTp, componentTp + componentFn),
102 behaviorRecall: ratio(behaviorFound, behaviorExpected),
103 ownership: ratio(ownershipAccounted, ownershipExpected),
104 qaDefectRecall: ratio(qaFound, qaExpected),
105 qaFalsePositiveRate: ratio(qaFalsePositive, qaClean),
106 p95ExtractionMs: percentile(measurements.map((entry) => entry.extractionMs), 0.95),
107 maxRawEvidenceBytes: Math.max(0, ...measurements.map((entry) => entry.rawEvidenceBytes)),
108 maxFrozenSpecBytes: Math.max(0, ...measurements.map((entry) => entry.frozenSpecBytes)),
109 agentDecisions,
110 agentInputTokens: 0,
111 };
112 const gateResults = [
113 gate("minimum-fixtures", metrics.fixtureCount >= RELEASE_GATES.minimumFixtures, metrics.fixtureCount, RELEASE_GATES.minimumFixtures),
114 gate("segmentation-precision", metrics.segmentationPrecision >= RELEASE_GATES.segmentationPrecision, metrics.segmentationPrecision, RELEASE_GATES.segmentationPrecision),
115 gate("segmentation-recall", metrics.segmentationRecall >= RELEASE_GATES.segmentationRecall, metrics.segmentationRecall, RELEASE_GATES.segmentationRecall),
116 gate("component-precision", metrics.componentPrecision >= RELEASE_GATES.componentPrecision, metrics.componentPrecision, RELEASE_GATES.componentPrecision),
117 gate("component-recall", metrics.componentRecall >= RELEASE_GATES.componentRecall, metrics.componentRecall, RELEASE_GATES.componentRecall),
118 gate("behavior-recall", metrics.behaviorRecall >= RELEASE_GATES.behaviorRecall, metrics.behaviorRecall, RELEASE_GATES.behaviorRecall),
119 gate("ownership", metrics.ownership === RELEASE_GATES.ownership, metrics.ownership, RELEASE_GATES.ownership),
120 gate("qa-defect-recall", metrics.qaDefectRecall >= RELEASE_GATES.qaDefectRecall, metrics.qaDefectRecall, RELEASE_GATES.qaDefectRecall),
121 gate("qa-false-positive-rate", metrics.qaFalsePositiveRate <= RELEASE_GATES.qaFalsePositiveRate, metrics.qaFalsePositiveRate, RELEASE_GATES.qaFalsePositiveRate),
122 gate("p95-extraction-ms", metrics.p95ExtractionMs <= RELEASE_GATES.p95ExtractionMs, metrics.p95ExtractionMs, RELEASE_GATES.p95ExtractionMs),
123 gate("raw-evidence-bytes", metrics.maxRawEvidenceBytes <= RELEASE_GATES.rawEvidenceBytes, metrics.maxRawEvidenceBytes, RELEASE_GATES.rawEvidenceBytes),
124 gate("frozen-spec-bytes", metrics.maxFrozenSpecBytes <= RELEASE_GATES.frozenSpecBytes, metrics.maxFrozenSpecBytes, RELEASE_GATES.frozenSpecBytes),
125 gate("agent-decisions", metrics.agentDecisions <= RELEASE_GATES.agentDecisions, metrics.agentDecisions, RELEASE_GATES.agentDecisions),
126 gate("agent-input-tokens", metrics.agentInputTokens <= RELEASE_GATES.agentInputTokens, metrics.agentInputTokens, RELEASE_GATES.agentInputTokens),
127 ];
128 return {
129 schemaVersion: "0083-benchmark.1",
130 corpus: { path: corpusPath, status: corpus.status, license: corpus.license, provenance: corpus.provenance, approval: corpus.approval || null },
131 status: gateResults.every((entry) => entry.passed) && failures.length === 0
132 ? (humanApproved ? "metrics-passed-human-approved-model-regression-corpus" : "metrics-passed-awaiting-human-model-corpus-approval")
133 : "failed",
134 metrics,
135 gates: gateResults,
136 failures,
137 measurements,
138 };
139}
140
141function detectQaDefects(id, observed = {}) {
142 const defects = [];
143 if ((observed.images || []).some((image) => !String(image.alt || "").trim())) defects.push(`${id}:missing-alt`);
144 if (observed.behavior?.interactive && observed.behavior?.keyboard === false) defects.push(`${id}:keyboard-unreachable`);
145 if (observed.layout?.scrollWidth > observed.layout?.viewportWidth) defects.push(`${id}:horizontal-overflow`);
146 if (observed.behavior?.motion && observed.behavior?.reducedMotion === false) defects.push(`${id}:reduced-motion-missing`);
147 return defects;
148}
149
150function ratio(numerator, denominator) { return denominator ? Number((numerator / denominator).toFixed(4)) : 1; }
151function percentile(values, quantile) { const sorted = [...values].sort((a, b) => a - b); return sorted.length ? sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * quantile) - 1)] : 0; }
152function gate(name, passed, actual, threshold) { return { name, passed, actual, threshold }; }
153
154async function main() {
155 const args = parseArgs();
156 const corpusPath = path.resolve(String(args.corpus || "tests/fixtures/headless/0083-model-regression-corpus.json"));
157 const report = await benchmarkExtractionCorpus({ corpusPath });
158 if (args.out) await writeJson(path.resolve(String(args.out)), report);
159 console.log(JSON.stringify(report, null, 2));
160 if (report.status === "failed") process.exitCode = 1;
161}
162
163if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });