Files
workinf_Blender_Wasm/tools/web/check-wbv2-inventory.mjs
mes123456 c2c65d3a09
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Rebuild local Blender WASM environment and recovery evidence
2026-08-27 20:22:15 +08:00

172 lines
9.5 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const task = "WBV2-P0-002";
const outputDir = path.join(root, "tests/golden", task);
const inventoryPath = path.join(outputDir, "inventory.json");
const parityPath = path.join(outputDir, "parity-map.json");
const sourceHashesPath = path.join(outputDir, "source-hashes.json");
const reportPath = path.join(outputDir, "inventory-check-report.json");
const manifestPath = path.join(outputDir, "manifest.json");
const generatorPath = path.join(root, "tools/web/generate-wbv2-inventory.mjs");
const checkerPath = path.join(root, "tools/web/check-wbv2-inventory.mjs");
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const fileSha256 = (file) => sha256(fs.readFileSync(file));
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const read = (file) => fs.readFileSync(file, "utf8");
const json = (file) => JSON.parse(read(file));
const sourcePlanPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json");
const sourceMapPath = path.join(root, "tests/golden/M15-02A/blender-parity-map.json");
const sourcePlan = json(sourcePlanPath);
const sourceMap = json(sourceMapPath);
const inventory = json(inventoryPath);
const parity = json(parityPath);
const sourceHashes = json(sourceHashesPath);
const taskByGapId = new Map(sourcePlan.tasks.map((entry) => [entry.gapId, entry]));
assert.equal(inventory.schemaVersion, 1);
assert.equal(inventory.task, task);
assert.equal(inventory.operation, "BLENDER_52_INVENTORY");
assert.equal(inventory.status, "INVENTORY_ONLY");
assert.equal(parity.schemaVersion, 1);
assert.equal(parity.task, task);
assert.equal(parity.operation, "BLENDER_52_CAPABILITY_PARITY_MAP");
assert.equal(parity.statusAxis, "INVENTORY_ONLY");
assert.deepEqual(parity.implementationClasses, ["METADATA_ONLY"]);
assert.equal(sourceHashes.schemaVersion, 1);
assert.equal(sourceHashes.task, task);
for (const [relativePath, expected] of Object.entries(sourceHashes.sources)) {
const file = path.join(root, relativePath);
assert.ok(fs.existsSync(file), `missing source ${relativePath}`);
assert.equal(fileSha256(file), expected, `source hash drift: ${relativePath}`);
}
assert.deepEqual(inventory.sourceHashes, sourceHashes.sources);
for (const [relativePath, reference] of Object.entries(parity.sourceInventories)) {
assert.equal(reference.path, relativePath);
assert.equal(reference.sha256, sourceHashes.sources[relativePath], `parity source reference drift: ${relativePath}`);
}
const ids = parity.entries.map((entry) => entry.id);
assert.deepEqual(ids, [...ids].sort((a, b) => a < b ? -1 : a > b ? 1 : 0), "parity map IDs are not sorted");
assert.equal(new Set(ids).size, ids.length, "parity map IDs are not unique");
assert.equal(ids.length, sourceMap.entries.length, "inventory entry count drifted from source map");
assert.deepEqual(inventory.entries.map((entry) => entry.id), ids, "inventory and parity map IDs differ");
assert.equal(parity.summary.inventoryEntries, ids.length);
assert.equal(inventory.summary.inventoryEntries, ids.length);
assert.equal(parity.summary.featureParityClaims, 0, "inventory must not claim feature parity");
for (const entry of parity.entries) {
assert.match(entry.id, /^[\x20-\x7e]+$/u);
assert.ok(taskByGapId.has(entry.id), `missing legacy task for ${entry.id}`);
assert.match(entry.capabilityId, /^CAP-[A-Z0-9]+(?:-[A-Z0-9]+)*$/u);
assert.ok(["CORE", "FEATURE", "UI", "UNCLASSIFIED"].includes(entry.layer));
assert.ok(["MAPPED", "UNKNOWN_EXPLICIT"].includes(entry.classificationStatus));
assert.equal(entry.completionClass, "METADATA_ONLY");
assert.equal(entry.parityLevel, "L0");
assert.equal(entry.parityClaim, "NONE");
assert.equal(entry.evidenceStatus, "NOT_RUN");
assert.equal(entry.evidence.legacyTask, taskByGapId.get(entry.id).id);
assert.equal(entry.evidence.representativeOnly, true);
}
const expectedCompleted = sourcePlan.tasks.filter((entry) => entry.state === "completed").length;
const expectedPending = sourcePlan.tasks.filter((entry) => entry.state === "pending").length;
const expectedActive = sourcePlan.tasks.filter((entry) => entry.state === "active").length;
assert.equal(parity.summary.completedLegacyEntries, expectedCompleted);
assert.equal(parity.summary.pendingLegacyEntries, expectedPending);
assert.equal(parity.summary.activeLegacyEntries, expectedActive);
assert.equal(parity.summary.completedLegacyEntries + parity.summary.pendingLegacyEntries + parity.summary.activeLegacyEntries, ids.length);
assert.equal(parity.summary.unknownEntries, parity.entries.filter((entry) => entry.family === "UNKNOWN").length);
assert.equal(parity.capabilities.reduce((sum, entry) => sum + entry.entryCount, 0), ids.length);
assert.equal(new Set(parity.capabilities.map((entry) => entry.capabilityId)).size, parity.capabilities.length);
for (const capability of parity.capabilities) {
assert.equal(capability.completionClass, "METADATA_ONLY");
assert.equal(capability.parityLevel, "L0");
assert.equal(capability.evidenceStatus, "NOT_RUN");
assert.equal(capability.claim, "inventory_only_no_feature_parity");
}
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "wbv2-inventory-"));
try {
execFileSync(process.execPath, [generatorPath, temporaryRoot], { cwd: root, stdio: "pipe" });
for (const name of ["inventory.json", "parity-map.json", "source-hashes.json"]) {
assert.deepEqual(fs.readFileSync(path.join(temporaryRoot, name)), fs.readFileSync(path.join(outputDir, name)), `${name} is not deterministic`);
}
} finally {
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
const report = {
schemaVersion: 1,
task,
operation: "BLENDER_52_INVENTORY_CHECK",
status: "PASS",
summary: parity.summary,
sourceHashes: sourceHashes.sources,
inventorySha256: fileSha256(inventoryPath),
parityMapSha256: fileSha256(parityPath),
deterministic: true,
featureParityClaims: 0,
nextTask: "WBV2-P0-003",
};
if (process.env.UPDATE_WBV2_P0_002_MANIFEST === "1") {
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
const statusPath = path.join(root, "docs/status", `${task}.md`);
const status = `# ${task} Status\n\nstatus: done\ntask: ${task}\ngate: G0\nupdated: 2026-08-24 America/New_York\n\nThe Blender 5.2 baseline is now represented by one deterministic capability map.\nThe map covers ${parity.summary.inventoryEntries} immutable legacy entries in ${parity.summary.capabilityCount} capability families, with ${parity.summary.completedLegacyEntries} completed legacy receipts and ${parity.summary.pendingLegacyEntries} pending coverage records. Every entry remains L0 and METADATA_ONLY; no inventory row claims browser feature parity. Unknown entries are explicit and cannot advance a capability.\n\nSource inventories and legacy states are hash-bound. The old M15/M16 manifests remain unchanged, and the capability lane only consumes them as read-only evidence.\n\nCommands passed:\n- node tools/web/generate-wbv2-inventory.mjs\n- node tools/web/check-wbv2-inventory.mjs\n- node tools/web/check-execution-control-plane.mjs\n- node tools/web/check-web-parity-v2-plan.mjs\n- node tools/web/check-corrective-plan.mjs\n- npm --prefix web run test:context-governance\n\nnextTask: WBV2-P0-003\n`;
fs.mkdirSync(path.dirname(statusPath), { recursive: true });
fs.writeFileSync(statusPath, status);
execFileSync(process.execPath, [path.join(root, "tools/web/check-task-context.mjs"), "--task", task, "--write"], { cwd: root, stdio: "pipe" });
const artifacts = {};
const artifactPaths = {
parentManifest: path.join(root, "tests/golden/WBV2-P0-001/manifest.json"),
generator: generatorPath,
checker: checkerPath,
inventory: inventoryPath,
parityMap: parityPath,
sourceHashes: sourceHashesPath,
report: reportPath,
status: statusPath,
taskContext: path.join(outputDir, "task-context.json"),
package: path.join(root, "web/package.json"),
};
for (const [name, file] of Object.entries(artifactPaths)) artifacts[name] = { path: relative(file), sha256: fileSha256(file) };
const manifest = {
schemaVersion: 1,
task,
parentTask: "WBV2-P0-001",
enablingTask: true,
parityStateChange: false,
runtime: "BLENDER_5_2_INVENTORY",
operation: "BLENDER_52_CAPABILITY_PARITY_MAP",
status: "done",
artifacts,
nextTask: "WBV2-P0-003",
};
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}
if (fs.existsSync(manifestPath)) {
const manifest = json(manifestPath);
assert.equal(manifest.task, task);
assert.equal(manifest.parentTask, "WBV2-P0-001");
assert.equal(manifest.status, "done");
assert.equal(manifest.nextTask, "WBV2-P0-003");
for (const artifact of Object.values(manifest.artifacts ?? {})) {
assert.ok(fs.existsSync(path.join(root, artifact.path)), `missing artifact ${artifact.path}`);
assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, `artifact hash drift: ${artifact.path}`);
}
}
fs.mkdirSync(outputDir, { recursive: true });
if (!fs.existsSync(reportPath)) fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`wbv2-inventory-ok entries=${parity.summary.inventoryEntries} capabilities=${parity.summary.capabilityCount} unknown=${parity.summary.unknownEntries} parityClaims=0 inventory=${fileSha256(inventoryPath)} map=${fileSha256(parityPath)} next=WBV2-P0-003\n`);