Reorganize Blender Web execution around capabilities
This commit is contained in:
106
tools/web/classify-corrective-legacy.mjs
Normal file
106
tools/web/classify-corrective-legacy.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
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`);
|
||||
Reference in New Issue
Block a user