Setting the file. One moment.
Finalize Gap Review · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
scripts/ finalize-gap-review.mjs
JavaScript · 180 lines · 12 KB
();
9 const sourceUrl = normalizeUrl (args._[ 0 ] || args.url). toString ();
10 const outputDir = resolveOutputDir (sourceUrl, args.out);
11 if (args.reviewed === true || args.reviewed === "true" ) throw new Error ( "--reviewed is no longer supported; write the required visual-review.json evidence beside the canonical iteration report" );
12 const report = await finalizeGapReview ({ outputDir });
13 if (args.json) process.stdout. write ( `${ JSON . stringify ( report , null , 2 ) } \n ` );
14 else console. log ( `[gap-analysis] visual review ${ report . visualReview . status }; final acceptance ${ report . acceptance . passed ? "passed" : "not yet passed"}` );
15 }
16
17 function requireNonEmptyString ( value , label ) {
18 if ( typeof value !== "string" || ! value. trim ()) throw new Error ( `${ label } must be a non-empty string` );
19 return value. trim ();
20 }
21
22 function validateVisualReview ({ review , report , reviewPath }) {
23 if ( ! review || typeof review !== "object" || Array. isArray (review)) throw new Error ( `Visual review must be a JSON object: ${ reviewPath }` );
24 if ( ! Array. isArray (review.pairs)) throw new Error ( `Visual review must include a pairs array: ${ reviewPath }` );
25 const expectedPairs = new Map ((report.screenshotPairs || []). map (( pair ) => [pair.id, pair]));
26 const findingIds = new Set ((report.findings || []). map (( finding ) => finding.id));
27 const seen = new Set ();
28 const normalized = review.pairs. map (( entry , index ) => {
29 const prefix = `visual review pair ${ index + 1 }` ;
30 if ( ! entry || typeof entry !== "object" || Array. isArray (entry)) throw new Error ( `${ prefix } must be an object` );
31 const pairId = requireNonEmptyString (entry.pairId, `${ prefix }.pairId` );
32 const pair = expectedPairs. get (pairId);
33 if ( ! pair) throw new Error ( `${ prefix } references unknown screenshot pair: ${ pairId }` );
34 if (seen. has (pairId)) throw new Error ( `Visual review has duplicate screenshot pair: ${ pairId }` );
35 seen. add (pairId);
36 const viewport = requireNonEmptyString (entry.viewport, `${ prefix }.viewport` );
37 if (viewport !== pair.viewport) throw new Error ( `${ prefix }.viewport must match ${ pairId } (${ pair . viewport })` );
38 const observation = requireNonEmptyString (entry.observation, `${ prefix }.observation` );
39 const verdict = requireNonEmptyString (entry.verdict, `${ prefix }.verdict` );
40 const entryFindingIds = Array. isArray (entry.findingIds) ? entry.findingIds. map (( id ) => requireNonEmptyString (id, `${ prefix }.findingIds` )) : [];
41 if ( new Set (entryFindingIds).size !== entryFindingIds. length ) throw new Error ( `${ prefix }.findingIds contains duplicates` );
42 for ( const id of entryFindingIds) if ( ! findingIds. has (id)) throw new Error ( `${ prefix } links unknown finding: ${ id }` );
43 const rationale = typeof entry.rationale === "string" ? entry.rationale. trim () : "" ;
44 if (verdict === "no-identity-gap" ) {
45 if (entryFindingIds. length ) throw new Error ( `${ prefix } cannot link findings for no-identity-gap` );
46 if ( ! rationale) throw new Error ( `${ prefix }.rationale is required for no-identity-gap` );
47 } else if (verdict === "findings-recorded" ) {
48 if ( ! entryFindingIds. length ) throw new Error ( `${ prefix }.findingIds is required for findings-recorded` );
49 } else {
50 throw new Error ( `${ prefix }.verdict must be findings-recorded or no-identity-gap` );
51 }
52 return { pairId, viewport, observation, verdict, findingIds: entryFindingIds, rationale: rationale || null };
53 });
54 const missing = [ ... expectedPairs. keys ()]. filter (( pairId ) => ! seen. has (pairId));
55 if (missing. length ) throw new Error ( `Visual review is incomplete: ${ missing . join ( ", " ) }` );
56 return normalized;
57 }
58
59 async function validateVisualProgress ({ progress , report , progressPath }) {
60 if ( ! progress || typeof progress !== "object" || Array. isArray (progress)) throw new Error ( `Visual progress proof must be a JSON object: ${ progressPath }` );
61 if ( ! Array. isArray (progress.entries)) throw new Error ( `Visual progress proof must include an entries array: ${ progressPath }` );
62 const expectedFindingIds = new Set (report.visualProgress?.previousBlockingFindingIds || []);
63 const currentPairs = new Map ((report.screenshotPairs || []). map (( pair ) => [pair.id, pair]));
64 const previousReportPath = report.visualProgress?.previousReportPath;
65 if ( ! previousReportPath) throw new Error ( "Visual progress proof is missing the prior canonical report path" );
66 const previousReport = await readJson (previousReportPath);
67 const previousPairs = new Map ((previousReport.screenshotPairs || []). map (( pair ) => [pair.id, pair]));
68 const seen = new Set ();
69 const records = progress.entries. map (( entry , index ) => {
70 const prefix = `visual progress entry ${ index + 1 }` ;
71 if ( ! entry || typeof entry !== "object" || Array. isArray (entry)) throw new Error ( `${ prefix } must be an object` );
72 const priorFindingId = requireNonEmptyString (entry.priorFindingId, `${ prefix }.priorFindingId` );
73 if ( ! expectedFindingIds. has (priorFindingId)) throw new Error ( `${ prefix } references unknown prior blocking finding: ${ priorFindingId }` );
74 if (seen. has (priorFindingId)) throw new Error ( `Visual progress proof has duplicate prior finding: ${ priorFindingId }` );
75 seen. add (priorFindingId);
76 const target = requireNonEmptyString (entry.target, `${ prefix }.target` );
77 const pairId = requireNonEmptyString (entry.pairId, `${ prefix }.pairId` );
78 const currentPair = currentPairs. get (pairId);
79 const previousPair = previousPairs. get (pairId);
80 if ( ! currentPair || ! previousPair) throw new Error ( `${ prefix }.pairId must exist in both the prior and current screenshot queues` );
81 const sourceScreenshotPath = requireNonEmptyString (entry.sourceScreenshotPath, `${ prefix }.sourceScreenshotPath` );
82 const beforeResultScreenshotPath = requireNonEmptyString (entry.beforeResultScreenshotPath, `${ prefix }.beforeResultScreenshotPath` );
83 const afterResultScreenshotPath = requireNonEmptyString (entry.afterResultScreenshotPath, `${ prefix }.afterResultScreenshotPath` );
84 if (sourceScreenshotPath !== currentPair.source.path) throw new Error ( `${ prefix }.sourceScreenshotPath must match the canonical source screenshot for ${ pairId }` );
85 if (beforeResultScreenshotPath !== previousPair.result.path) throw new Error ( `${ prefix }.beforeResultScreenshotPath must match the prior iteration result screenshot for ${ pairId }` );
86 if (afterResultScreenshotPath !== currentPair.result.path) throw new Error ( `${ prefix }.afterResultScreenshotPath must match the current iteration result screenshot for ${ pairId }` );
87 const beforeObservation = requireNonEmptyString (entry.beforeObservation, `${ prefix }.beforeObservation` );
88 const afterObservation = requireNonEmptyString (entry.afterObservation, `${ prefix }.afterObservation` );
89 const evidence = requireNonEmptyString (entry.evidence, `${ prefix }.evidence` );
90 const verdict = requireNonEmptyString (entry.verdict, `${ prefix }.verdict` );
91 if ( ! [ "improved" , "no-visible-improvement" , "regressed" ]. includes (verdict)) throw new Error ( `${ prefix }.verdict must be improved, no-visible-improvement, or regressed` );
92 return { priorFindingId, target, pairId, sourceScreenshotPath, beforeResultScreenshotPath, afterResultScreenshotPath, beforeObservation, afterObservation, verdict, evidence };
93 });
94 const missing = [ ... expectedFindingIds]. filter (( findingId ) => ! seen. has (findingId));
95 if (missing. length ) throw new Error ( `Visual progress proof is incomplete: ${ missing . join ( ", " ) }` );
96 return records;
97 }
98
99 export async function finalizeGapReview ({ outputDir } = {}) {
100 const gapRoot = path. join ( docsDir (outputDir), "gap-analysis" );
101 const latest = await readJson (path. join (gapRoot, "latest.json" ));
102 const reportPath = latest.paths?.reportJson;
103 if ( ! reportPath) throw new Error ( "Latest gap report does not include its canonical report path" );
104 const report = await readJson (reportPath);
105 const reviewPath = path. join (path. dirname (reportPath), "visual-review.json" );
106 const reviewRequired = Boolean (report.visualReview?.required || report.screenshotPairs?. length );
107 let reviewPairs = [];
108 let progressRecords = [];
109 if (reviewRequired) {
110 let review;
111 try {
112 review = await readJson (reviewPath);
113 } catch (error) {
114 throw new Error ( `Missing required visual review evidence at ${ reviewPath }. Open every source/result pair and write visual-review.json before finalizing. (${ error . message })` );
115 }
116 reviewPairs = validateVisualReview ({ review, report, reviewPath });
117 for ( const pair of report.screenshotPairs || []) pair.reviewStatus = "reviewed" ;
118 report.visualReview.status = "reviewed" ;
119 report.visualReview.reviewedPairs = reviewPairs. map (( entry ) => entry.pairId);
120 report.visualReview.records = reviewPairs;
121 report.visualReview.artifactPath = reviewPath;
122 report.visualReview.reviewedAt = new Date (). toISOString ();
123 } else {
124 report.visualReview.status = "not-applicable" ;
125 report.visualReview.reviewedPairs = [];
126 report.visualReview.records = [];
127 }
128 if (report.visualProgress?.required) {
129 const progressPath = report.paths?.visualProgress || path. join (path. dirname (reportPath), "visual-progress.json" );
130 let progress;
131 try {
132 progress = await readJson (progressPath);
133 } catch (error) {
134 throw new Error ( `Missing required visual progress proof at ${ progressPath }. Compare the prior and current result screenshots before finalizing. (${ error . message })` );
135 }
136 progressRecords = await validateVisualProgress ({ progress, report, progressPath });
137 report.visualProgress.status = "reviewed" ;
138 report.visualProgress.records = progressRecords;
139 report.visualProgress.artifactPath = progressPath;
140 report.visualProgress.reviewedAt = new Date (). toISOString ();
141 }
142 const open = (report.findings || []). filter (( finding ) => finding.status !== "resolved" && finding.status !== "accepted" );
143 const counts = { critical: 0 , high: 0 , medium: 0 , low: 0 };
144 for ( const finding of open) counts[finding.severity] = (counts[finding.severity] || 0 ) + 1 ;
145 report.summary.counts = counts;
146 report.summary.blockingCount = counts.critical + counts.high;
147 report.summary.findingCount = open. length ;
148 report.summary.score = Math. max ( 0 , Math. round ( 100 - counts.critical * 25 - counts.high * 10 - counts.medium * 3 - counts.low));
149 report.summary.finalAcceptance = report.summary.blockingCount === 0 && ( ! report.visualReview.required || report.visualReview.status === "reviewed" );
150 if (report.summary.finalAcceptance && report.visualProgress?.required && progressRecords. some (( record ) => record.verdict !== "improved" )) {
151 throw new Error ( "Cannot accept the iteration: a prior blocking finding was not proven improved by its before/after screenshot evidence" );
152 }
153 report.acceptance.passed = report.summary.finalAcceptance;
154 report.acceptance.finalizedAt = new Date (). toISOString ();
155 await writeJson (reportPath, report);
156 await writeText (report.paths.reportMarkdown, renderGapAnalysisMarkdown (report));
157 await writeText (report.paths.fixPlan, renderGapFixPlan (report));
158 await writeJson (path. join (gapRoot, "latest.json" ), report);
159 await writeText (path. join (gapRoot, "latest.md" ), renderGapAnalysisMarkdown (report));
160 const manifestPath = path. join (path. dirname (reportPath), "iteration-manifest.json" );
161 const manifest = await readJson (manifestPath);
162 manifest.status = "visual-review-complete" ;
163 manifest.visualReview = { artifactPath: reviewRequired ? reviewPath : null , reviewedPairs: reviewPairs. map (( entry ) => entry.pairId), reviewedAt: report.visualReview.reviewedAt || null };
164 manifest.visualProgress = { artifactPath: report.visualProgress?.artifactPath || null , reviewedAt: report.visualProgress?.reviewedAt || null , verdicts: progressRecords. map (( entry ) => ({ priorFindingId: entry.priorFindingId, verdict: entry.verdict })) };
165 await writeJson (manifestPath, manifest);
166 try {
167 await readJson (path. join ( docsDir (outputDir), "extraction" , "latest.json" ));
168 await finalizeExtractionReport ({ outputDir });
169 } catch (error) {
170 if (error?.code !== "ENOENT" ) throw error;
171 }
172 return report;
173 }
174
175 if ( import . meta .url === `file://${ process . argv [ 1 ] }` ) {
176 main (). catch (( error ) => {
177 console. error (error.stack || error.message);
178 process. exit ( 1 );
179 });
180 }