Setting the file. One moment. Finalize Extraction Report · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
scripts/finalize-extraction-report.mjs
JavaScript·126 lines·6 KB
=
await
readJson
(path.
join
(docs,
"extraction"
,
"latest.json"
));
9 const extractionDir = path.join(docs, "extraction", latest.captureId);
10 const manifest = await readJson(path.join(extractionDir, "extraction-manifest.json"));
11 const gapArtifact = await readJson(path.join(extractionDir, "extraction-gaps.json"));
12 const decisionArtifact = await optionalJson(path.join(docs, "gap-decisions.json"), { manifestHash: latest.manifestHash, decisions: [] });
13 if (decisionArtifact.manifestHash !== latest.manifestHash) throw new Error("Gap decisions refer to a stale frozen manifest");
14 const decisions = new Map((decisionArtifact.decisions || []).map((decision) => [decision.gapId, decision]));
15 const ledger = await optionalJson(path.join(docs, "build", "section-implementation.json"), { units: [] });
16 const qa = await optionalJson(path.join(docs, "qa", "visual-qa.json"), null);
17 const gapReview = await optionalJson(path.join(docs, "gap-analysis", "latest.json"), null);
18 const gaps = (gapArtifact.gaps || []).filter((gap) => gap.status !== "resolved").map((gap) => {
19 const implementation = ledger.units?.find((unit) => unit.id === gap.ownerUnitId);
20 return {
21 id: gap.id,
22 unitId: gap.ownerUnitId,
23 scope: gap.scope,
24 status: gap.status,
25 reason: gap.reason,
26 missingFields: gap.missingFields,
27 confidence: gap.confidence,
28 evidenceRefs: gap.evidenceRefs,
29 recoveryAttempts: gap.attempts,
30 remainingAttempts: gap.remainingAttempts,
31 assumptions: gap.assumptions,
32 omissions: gap.omissions,
33 affectedAcceptance: gap.affectedAcceptance,
34 dependencyClosure: gap.dependencyClosure,
35 implementation: implementation ? { status: implementation.status, cloneRoot: implementation.cloneRoot } : null,
36 userDecision: decisions.get(gap.id) || gap.userDecision,
37 choices: ["accept-as-is", "retry-or-fix", "provide-material", "replace", "omit", "leave-unresolved"],
38 unblockAction: gap.unblockAction,
39 };
40 });
41 const unresolvedGlobal = gaps.filter((gap) => gap.scope === "global");
42 const provisional = gaps.filter((gap) => gap.scope === "local" && !["accept-as-is", "replace", "omit"].includes(gap.userDecision?.decision));
43 const browserVerified = Boolean(gapReview?.acceptance?.passed && ["reviewed", "not-applicable"].includes(gapReview?.visualReview?.status));
44 const implementationExists = Boolean(ledger.units?.length);
45 const status = unresolvedGlobal.length ? "blocked" : provisional.length ? "done_with_gaps" : browserVerified ? "done" : qa || implementationExists ? "verification_pending" : "implementation_pending";
46 const report = {
47 schemaVersion: EXTRACTION_SCHEMA_VERSION,
48 kind: "final-report",
49 captureId: latest.captureId,
50 manifestHash: manifest.manifestHash,
51 status,
52 summary: {
53 totalGaps: gaps.length,
54 provisionalGaps: provisional.length,
55 globalBlockers: unresolvedGlobal.length,
56 acceptedUnits: manifest.specs.filter((spec) => spec.status === "accepted").length,
57 provisionalUnits: manifest.specs.filter((spec) => spec.status === "provisional").length,
58 },
59 gaps,
60 qa: qa || gapReview ? {
61 preflightScore: qa?.score ?? null,
62 warnings: qa?.warnings || [],
63 provisionalUnits: qa?.provisionalUnits || [],
64 browserReview: gapReview ? { iteration: gapReview.iteration, passed: Boolean(gapReview.acceptance?.passed), status: gapReview.visualReview?.status, blockingCount: gapReview.summary?.blockingCount } : null,
65 } : null,
66 };
67 assertValidArtifact(report, "final-report");
68 await writeJson(path.join(docs, "final-report.json"), report);
69 await writeText(path.join(docs, "final-report.md"), renderFinalReport(report));
70 return report;
71}
72
73export function renderFinalReport(report) {
74 const lines = [
75 "# Home-page reconstruction report",
76 "",
77 `- Status: \`${report.status}\``,
78 `- Frozen manifest: \`${report.manifestHash}\``,
79 `- Accepted units: ${report.summary.acceptedUnits}`,
80 `- Provisional units: ${report.summary.provisionalUnits}`,
81 `- Unresolved local gaps: ${report.summary.provisionalGaps}`,
82 `- Global blockers: ${report.summary.globalBlockers}`,
83 "",
84 ];
85 if (!report.gaps.length) {
86 lines.push("No extraction gaps remain. Browser-backed verification is still required before final clone acceptance.", "");
87 } else {
88 lines.push("## Gaps requiring review", "");
89 for (const gap of report.gaps) {
90 lines.push(
91 `### ${gap.unitId}`,
92 "",
93 `This unit could not be imported reliably because ${lowerFirst(gap.reason)}`,
94 "",
95 `- Gap: \`${gap.id}\` (${gap.scope}, ${gap.status}, confidence: ${gap.confidence})`,
96 `- Missing: ${gap.missingFields.join(", ") || "unspecified evidence"}`,
97 `- Attempts: ${gap.recoveryAttempts.length}; remaining budget: ${gap.remainingAttempts}`,
98 `- Provisional implementation: ${gap.implementation?.status || "not implemented"}${gap.implementation?.cloneRoot ? ` at \`${gap.implementation.cloneRoot}\`` : ""}`,
99 `- Assumptions: ${gap.assumptions.join("; ") || "none"}`,
100 `- Omissions: ${gap.omissions.join("; ") || "none"}`,
101 `- Affected checks: ${gap.affectedAcceptance.join(", ") || "none recorded"}`,
102 `- User decision: ${gap.userDecision ? `\`${gap.userDecision.decision}\`${gap.userDecision.note ? ` — ${gap.userDecision.note}` : ""}` : "pending"}`,
103 `- Available decisions: ${gap.choices.map((choice) => `\`${choice}\``).join(", ")}`,
104 "",
105 );
106 }
107 }
108 return `${lines.join("\n").trim()}\n`;
109}
110
111function lowerFirst(value) {
112 const text = String(value || "evidence was incomplete");
113 return `${text.charAt(0).toLowerCase()}${text.slice(1)}`;
114}
115
116async function optionalJson(filePath, fallback) {
117 try { return await readJson(filePath); } catch { return fallback; }
118}
119
120async function main() {
121 const args = parseArgs();
122 if (!args.out) throw new Error("--out is required");
123 console.log(JSON.stringify(await finalizeExtractionReport({ outputDir: path.resolve(String(args.out)) }), null, 2));
124}
125
126if (import.meta.url === `file://${process.argv[1]}`) main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });