Files
workinf_Blender_Wasm/tools/web/classify-corrective-legacy.mjs
mes123456 a081c68c87
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Reorganize Blender Web execution around capabilities
2026-08-24 18:37:46 -04:00

107 lines
10 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const stagedRoot = path.resolve(root, process.argv[process.argv.indexOf("--staged-root") + 1] ?? "tests/golden/corrective/C0-002/staged");
const outputRoot = path.resolve(root, process.argv[process.argv.indexOf("--output-root") + 1] ?? "tests/golden/corrective/C0-003");
const read = (file) => fs.readFileSync(file, "utf8");
const json = (file) => JSON.parse(read(file));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const arg = (name) => { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; };
const outputPath = (name) => path.join(outputRoot, name);
const registry = json(path.join(stagedRoot, "completed-gap-tasks.candidate.json"));
const plan = json(path.join(stagedRoot, "next-task-plan.candidate.json"));
const planById = new Map(plan.tasks.map((task) => [task.id, task]));
const taskDirs = fs.readdirSync(path.join(stagedRoot, "resigned-manifests")).filter((entry) => /^M16-GAP-\d{5}$/u.test(entry)).filter((entry) => json(path.join(stagedRoot, "resigned-manifests", entry, "manifest.json")).status === "done").sort();
const positiveToken = (value) => typeof value === "string" && value.length > 0 && !/(?:^|_)(?:NONE|CANCELLED|UNSUPPORTED|ALREADY|EMPTY|NOOP)(?:$|_)/u.test(value);
const reportPathFor = (manifest, name) => manifest.artifacts?.[name]?.path ? path.join(root, manifest.artifacts[name].path) : null;
const productionKeys = ["reader", "nativeStub", "api", "protocol", "sceneIrProtocol", "wasm", "wasmJs", "wasmPublic", "wasmJsPublic"];
const capabilityFor = (gapId, ownerFamily, operation) => {
if (gapId?.startsWith("operator:asset.")) return "CAP:asset.catalog.operator";
if (gapId?.startsWith("operator:anim.")) return "CAP:animation.operator";
if (gapId?.startsWith("operator:armature.")) return "CAP:armature.operator";
if (gapId?.startsWith("modifier:")) return "CAP:modifier.snapshot";
if (gapId?.startsWith("datablock:")) return "CAP:datablock.read";
return `CAP:${String(ownerFamily || operation || "legacy").toLowerCase()}`;
};
const classify = ({ manifest, desktop, web, task }) => {
const desktopMutation = desktop?.mainMutation;
const desktopPositive = Boolean(desktop?.poll === true && desktop?.operatorStatus === "FINISHED" && positiveToken(desktopMutation));
const wasmMutation = web?.wasm?.mainMutation ?? web?.wasm?.mutation ?? web?.wasm?.commandResult?.mainMutation ?? null;
const beforeAfterChanged = web?.wasm?.before !== undefined && web?.wasm?.after !== undefined && JSON.stringify(web.wasm.before) !== JSON.stringify(web.wasm.after);
const explicitAfterMutation = Boolean(web?.wasm?.after && Object.keys(web.wasm.after).some((key) => /(?:inserted|deleted|cleared|changed|added|removed|moved|duplicated|replaced|updated|set|assigned|unassigned|created|selected)/iu.test(key)));
const wasmPositiveMutation = positiveToken(wasmMutation) || beforeAfterChanged || explicitAfterMutation || (Number.isFinite(web?.wasm?.revisionAfter) && Number.isFinite(web?.wasm?.revisionBefore) && web.wasm.revisionAfter !== web.wasm.revisionBefore);
const saveReopen = web?.saveReopen ?? desktop?.saveReopen ?? "NOT_REPORTED";
const negative = web?.negative && Object.keys(web.negative).length > 0;
const exact = (web?.status === "EXACT" || web?.desktop?.status === "EXACT" || web?.wasm?.status === "EXACT") && saveReopen === "EXACT";
let completionClass = "BLOCKED";
let evidenceLevel = "L0";
let mappingState = "BLOCKED";
let reason = "missing or non-authoritative mutation evidence";
if (desktopPositive && wasmPositiveMutation && exact) {
completionClass = "FEATURE_PARITY";
evidenceLevel = saveReopen === "EXACT" ? "L3" : "L2";
mappingState = "CANDIDATE";
reason = "desktop positive oracle and explicit WASM/Main mutation with persistence evidence";
} else if (negative && !desktopPositive && !wasmPositiveMutation) {
completionClass = "NEGATIVE_BOUNDARY";
evidenceLevel = exact ? "L1" : "L0";
mappingState = "RETAINED_NEGATIVE";
reason = "evidence is cancellation, unsupported, resource, or malformed-input boundary";
} else if (exact && !desktopPositive && !wasmPositiveMutation) {
completionClass = web?.wasm && Object.keys(web.wasm).length > 1 ? "METADATA_ONLY" : "READ_COMPATIBILITY";
evidenceLevel = Object.keys(web?.wasm ?? {}).length > 1 ? "L1" : "L0";
mappingState = "CANDIDATE";
reason = completionClass === "METADATA_ONLY" ? "exact snapshot fields without equivalent mutation" : "read/observe evidence without mutation";
}
if (manifest.status !== "done") {
completionClass = "BLOCKED";
mappingState = "BLOCKED";
reason = "legacy manifest is not done";
}
return { completionClass, evidenceLevel, mappingState, reason, desktopPositive, wasmPositiveMutation, readerChanged: Boolean(manifest.artifacts?.reader || manifest.artifacts?.nativeStub || manifest.artifacts?.api), wasmChanged: Boolean(manifest.artifacts?.wasm || manifest.artifacts?.wasmJs || manifest.artifacts?.wasmPublic || manifest.artifacts?.wasmJsPublic), saveReopen };
};
const records = [];
for (const task of taskDirs) {
const manifestPath = path.join(stagedRoot, "resigned-manifests", task, "manifest.json");
const manifest = json(manifestPath);
const taskPlan = planById.get(task);
const desktopPath = reportPathFor(manifest, "desktopReport");
const webPath = reportPathFor(manifest, "webReport");
const desktop = desktopPath && fs.existsSync(desktopPath) ? json(desktopPath) : null;
const web = webPath && fs.existsSync(webPath) ? json(webPath) : null;
const classification = classify({ manifest, desktop, web, task });
const artifactPaths = [relative(manifestPath), desktopPath && relative(desktopPath), webPath && relative(webPath)].filter(Boolean);
for (const key of productionKeys) {
const pathValue = manifest.artifacts?.[key]?.path;
if (pathValue && fs.existsSync(path.join(root, pathValue))) artifactPaths.push(pathValue);
}
const sourceHashes = Object.fromEntries([...new Set(artifactPaths)].map((file) => [file, sha256(path.join(root, file))]));
const gapId = taskPlan?.gapId ?? null;
const candidateCapabilityId = capabilityFor(gapId, taskPlan?.ownerFamily, manifest.operation);
records.push({ task, gapId, evidenceLevel: classification.evidenceLevel, completionClass: classification.completionClass, desktopPositive: classification.desktopPositive, wasmPositiveMutation: classification.wasmPositiveMutation, readerChanged: classification.readerChanged, wasmChanged: classification.wasmChanged, saveReopen: classification.saveReopen, reason: classification.reason, candidateCapabilityId, coverageCapabilityIds: [candidateCapabilityId], mappingState: classification.mappingState, evidenceRefs: { manifest: relative(manifestPath), desktopReport: desktopPath ? relative(desktopPath) : null, webReport: webPath ? relative(webPath) : null, command: `node tools/web/check-generated-gap.mjs --task ${task}` }, sourceHashes });
}
const clusters = new Map();
for (const record of records) {
const key = `${record.completionClass}:${record.candidateCapabilityId}`;
const value = clusters.get(key) ?? { clusterId: key, completionClass: record.completionClass, capabilityId: record.candidateCapabilityId, taskCount: 0, tasks: [] };
value.taskCount += 1;
value.tasks.push(record.task);
clusters.set(key, value);
}
const classificationSource = { stagedRoot: relative(stagedRoot), registry: relative(path.join(stagedRoot, "completed-gap-tasks.candidate.json")), registrySha256: sha256(path.join(stagedRoot, "completed-gap-tasks.candidate.json")), plan: relative(path.join(stagedRoot, "next-task-plan.candidate.json")), planSha256: sha256(path.join(stagedRoot, "next-task-plan.candidate.json")) };
const classification = { schemaVersion: 1, operation: "CORRECTIVE_LEGACY_273_CLASSIFICATION", status: records.length === 273 && records.every((record) => Object.values(record.sourceHashes).every(Boolean)) ? "PASS" : "BLOCKED", queueMutation: false, source: classificationSource, sourceHashes: { [classificationSource.registry]: classificationSource.registrySha256, [classificationSource.plan]: classificationSource.planSha256 }, inputDigest: crypto.createHash("sha256").update(JSON.stringify({ source: classificationSource, records })).digest("hex"), taskCount: records.length, records };
const clusterSummary = { schemaVersion: 1, operation: "CORRECTIVE_LEGACY_CLUSTER_SUMMARY", status: "PASS", queueMutation: false, clusters: [...clusters.values()].map((cluster) => ({ ...cluster, tasks: cluster.tasks.sort() })).sort((a, b) => a.clusterId.localeCompare(b.clusterId)), counts: Object.fromEntries([...new Set(records.map((record) => record.completionClass))].sort().map((key) => [key, records.filter((record) => record.completionClass === key).length])) };
const reportLines = ["# C0-003 Legacy Classification Report", "", `status: ${classification.status}`, `taskCount: ${records.length}`, "queueMutation: false", "", "Classification is fail-closed: only explicit desktop positive, explicit WASM/Main mutation, and persistence evidence can produce FEATURE_PARITY.", "", "| Class | Count |", "| --- | ---: |", ...Object.entries(clusterSummary.counts).map(([key, value]) => `| ${key} | ${value} |`), "", "| Task | Gap | Level | Class | Desktop | WASM mutation | Save/reopen | Mapping |", "| --- | --- | --- | --- | --- | --- | --- | --- |", ...records.map((record) => `| ${record.task} | ${record.gapId} | ${record.evidenceLevel} | ${record.completionClass} | ${record.desktopPositive} | ${record.wasmPositiveMutation} | ${record.saveReopen} | ${record.mappingState} |`), ""];
fs.mkdirSync(outputRoot, { recursive: true });
fs.writeFileSync(outputPath("legacy-gap-classification.json"), `${JSON.stringify(classification, null, 2)}\n`);
fs.writeFileSync(outputPath("cluster-summary.json"), `${JSON.stringify(clusterSummary, null, 2)}\n`);
fs.writeFileSync(outputPath("classification-report.md"), reportLines.join("\n"));
process.stdout.write(`corrective-classification-${classification.status.toLowerCase()} tasks=${records.length} clusters=${clusterSummary.clusters.length} output=${relative(outputRoot)}\n`);