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`);