172 lines
9.5 KiB
JavaScript
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`);
|