Rebuild local Blender WASM environment and recovery evidence
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

This commit is contained in:
mes123456
2026-08-27 20:15:55 +08:00
parent a081c68c87
commit c2c65d3a09
376 changed files with 241951 additions and 114 deletions

View File

@@ -5,8 +5,13 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(OPENVDB_ROOT "/home/mes123456/working/build_oiio_deps/Release/openvdb" CACHE PATH "OpenVDB install prefix")
set(TBB_ROOT "/home/mes123456/working/build_oiio_deps/Release/tbb" CACHE PATH "TBB install prefix")
set(_web_deps_root "$ENV{BLENDER_DEPS_ROOT}")
if(NOT _web_deps_root)
get_filename_component(_web_deps_root "${CMAKE_CURRENT_LIST_DIR}/../../.deps/native" ABSOLUTE)
endif()
set(OPENVDB_ROOT "${_web_deps_root}/openvdb" CACHE PATH "OpenVDB install prefix")
set(TBB_ROOT "${_web_deps_root}/tbb" CACHE PATH "TBB install prefix")
unset(_web_deps_root)
find_path(OPENVDB_INCLUDE_DIR openvdb/openvdb.h HINTS "${OPENVDB_ROOT}/include" REQUIRED NO_DEFAULT_PATH)
find_library(OPENVDB_LIBRARY openvdb HINTS "${OPENVDB_ROOT}/lib" REQUIRED NO_DEFAULT_PATH)

View File

@@ -10,7 +10,8 @@ npm --prefix web run test:vdb-native
The provisioner builds two native executables, generates real bounded OpenVDB fixtures once without overwriting
their UUID-bearing source files, downloads the official
CC-BY-4.0 `sphere.vdb` sample with its Git LFS SHA-256, converts the fixtures with OpenVDB 13.0/NanoVDB 32.9,
and writes the resources under `/home/mes123456/resource-library/blender-web-vdb`.
and writes the resources under `resource-library/blender-web-vdb` in the
checkout by default. Set `VDB_RESOURCE_ROOT` to use another location.
`vdb_to_nanovdb` accepts only `.vdb`, limits source/output bytes, grid count and active voxels, supports a grid
allowlist, writes uncompressed standard NanoVDB segments, and emits a machine-readable conversion report.

View File

@@ -3,12 +3,17 @@ set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
build_dir=${VDB_TOOLS_BUILD_DIR:-"$repo_root/build_vdb_tools"}
openvdb_root=${OPENVDB_ROOT:-/home/mes123456/working/build_oiio_deps/Release/openvdb}
tbb_root=${TBB_ROOT:-/home/mes123456/working/build_oiio_deps/Release/tbb}
deps_root=${BLENDER_DEPS_ROOT:-${repo_root}/.deps/native}
openvdb_root=${OPENVDB_ROOT:-${deps_root}/openvdb}
tbb_root=${TBB_ROOT:-${deps_root}/tbb}
cmake -S "$repo_root/tools/vdb" -B "$build_dir" \
-DOPENVDB_ROOT="$openvdb_root" \
-DTBB_ROOT="$tbb_root" \
-DOPENVDB_INCLUDE_DIR="$openvdb_root/include" \
-DTBB_INCLUDE_DIR="$tbb_root/include" \
-DOPENVDB_LIBRARY="$openvdb_root/lib/libopenvdb.so" \
-DTBB_LIBRARY="$tbb_root/lib/libtbb.so" \
-DCMAKE_BUILD_TYPE=Release
cmake --build "$build_dir" --parallel "${VDB_BUILD_JOBS:-4}"

View File

@@ -4,10 +4,10 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? "/home/mes123456/resource-library/blender-web-vdb");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(repoRoot, "resource-library", "blender-web-vdb"));
const converter = path.join(repoRoot, "build_vdb_tools", "vdb_to_nanovdb");
const generator = path.join(repoRoot, "build_vdb_tools", "vdb_fixture_generator");
const openVDBLicense = "/home/mes123456/working/build_oiio_deps/build/openvdb/src/external_openvdb/LICENSE";
const openVDBLicense = process.env.OPENVDB_LICENSE ?? path.join(repoRoot, ".deps/native/openvdb/LICENSE");
const ccLicense = path.join(resourceRoot, "licenses", "CC-BY-4.0.txt");
function sha256(file) {
@@ -22,7 +22,12 @@ function entry(id, relativePath, kind, license, origin, extra = {}) {
fs.mkdirSync(path.join(resourceRoot, "generated"), { recursive: true });
fs.mkdirSync(path.join(resourceRoot, "licenses"), { recursive: true });
fs.copyFileSync(openVDBLicense, path.join(resourceRoot, "licenses", "OpenVDB-Apache-2.0.txt"));
if (!fs.existsSync(openVDBLicense)) {
const bundledLicense = path.join(resourceRoot, "licenses", "OpenVDB-Apache-2.0.txt");
if (!fs.existsSync(bundledLicense)) throw new Error(`OPENVDB_LICENSE_MISSING: ${openVDBLicense}`);
} else {
fs.copyFileSync(openVDBLicense, path.join(resourceRoot, "licenses", "OpenVDB-Apache-2.0.txt"));
}
if (!fs.existsSync(ccLicense)) throw new Error("VDB_RESOURCE_LICENSE_MISSING: CC-BY-4.0.txt");
const smoke = fs.readFileSync(path.join(resourceRoot, "generated", "generated-smoke.vdb"));
@@ -63,9 +68,9 @@ const manifest = {
toolchain: {
openVDBVersion: "13.0.0",
nanoVDBVersion: "32.9.0",
converterPath: converter,
converterPath: path.relative(repoRoot, converter).replaceAll(path.sep, "/"),
converterSha256: sha256(converter),
generatorPath: generator,
generatorPath: path.relative(repoRoot, generator).replaceAll(path.sep, "/"),
generatorSha256: sha256(generator),
},
entries,

View File

@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb"));
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(root, "resource-library", "blender-web-vdb"));
const source = path.join(resourceRoot, "generated", "generated-smoke.vdb");
const generator = path.join(root, "build_vdb_tools", "vdb_volume_golden");
const committedRoot = path.join(root, "tests", "golden", "M8-19");

View File

@@ -2,7 +2,7 @@
set -euo pipefail
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
resource_root=${VDB_RESOURCE_ROOT:-/home/mes123456/resource-library/blender-web-vdb}
resource_root=${VDB_RESOURCE_ROOT:-"$repo_root/resource-library/blender-web-vdb"}
build_dir=${VDB_TOOLS_BUILD_DIR:-"$repo_root/build_vdb_tools"}
sphere_sha=bb884a9a38354e47191c4ece4a11d1e051594a910fde56501cfc51ff52b171ab
license_sha=8d3fceb4cb62663775f02c1c551cfbf332d1c736667c17817c16c635caba21e0

View File

@@ -3,13 +3,13 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? "/home/mes123456/resource-library/blender-web-vdb");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(root, "resource-library", "blender-web-vdb"));
const catalog = JSON.parse(fs.readFileSync(path.join(resourceRoot, "manifest.json"), "utf8"));
if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.entries) || catalog.entries.length === 0) throw new Error("VDB_RESOURCE_CATALOG_INVALID");
const evidence = {
schemaVersion: 1,
resourceLibrary: resourceRoot,
resourceLibrary: path.relative(root, resourceRoot).replaceAll(path.sep, "/") || ".",
browserOpenVDB: false,
desktopOpenVDB: true,
serverJobConfigured: true,

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
"${repo_root}/tools/web/configure-blender-native.sh" "$@"
source "${repo_root}/tools/web/native-build-env.sh"
build_target="${BLENDER_BUILD_TARGET:-blender}"
jobs="${BLENDER_BUILD_JOBS:-2}"
cmake --build "${BLENDER_BUILD_DIR}" --target "${build_target}" --parallel "${jobs}"
test -x "${BLENDER_BUILD_DIR}/bin/blender"
# Blender's bundled asset-library add-on imports cattrs, while upstream's
# install rules only copy NumPy, requests, and zstandard. Copy the small
# pure-Python dependency set into the portable bundle after installation.
if [[ "${build_target}" == "install" ]]; then
bundle_site="${BLENDER_BUILD_DIR}/bin/5.2/python/lib/python3.13/site-packages"
source_site="${BLENDER_PYTHON_ROOT}/lib/python3.13/site-packages"
mkdir -p "${bundle_site}"
for package in cattrs attrs attr cattr; do
if [[ -d "${source_site}/${package}" ]]; then
rm -rf "${bundle_site:?}/${package}"
cp -a "${source_site}/${package}" "${bundle_site}/"
fi
done
if [[ -d "${source_site}/numpy.libs" ]]; then
rm -rf "${bundle_site:?}/numpy.libs"
cp -a "${source_site}/numpy.libs" "${bundle_site}/"
fi
for module in typing_extensions.py; do
if [[ -f "${source_site}/${module}" ]]; then
cp -a "${source_site}/${module}" "${bundle_site}/"
fi
done
for metadata in "${source_site}"/{cattrs,attrs,typing_extensions}-*.dist-info; do
if [[ -d "${metadata}" ]]; then
cp -a "${metadata}" "${bundle_site}/"
fi
done
fi
# Keep historical task cards runnable while the clean native build lives in a
# separate directory. Preserve a copied-host binary before replacing it.
legacy_binary="${repo_root}/build_blender_5.2.0/bin/blender"
if [[ "${legacy_binary}" != "${BLENDER_BUILD_DIR}/bin/blender" ]]; then
mkdir -p "$(dirname "${legacy_binary}")"
if [[ -e "${legacy_binary}" && ! -L "${legacy_binary}" ]]; then
legacy_backup="${legacy_binary}.copied-host"
if [[ ! -e "${legacy_backup}" ]]; then
mv "${legacy_binary}" "${legacy_backup}"
else
echo "legacy Blender backup already exists: ${legacy_backup}" >&2
exit 3
fi
fi
ln -sfn "../../build_blender_native/bin/blender" "${legacy_binary}"
fi
"${BLENDER_BUILD_DIR}/bin/blender" --version | head -3
printf 'native-build-ok binary=%s\n' "${BLENDER_BUILD_DIR}/bin/blender"

View File

@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
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 driftPath = path.join(root, "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json");
const outputArgumentIndex = process.argv.indexOf("--out");
const outputRoot = path.resolve(root, outputArgumentIndex >= 0 ? process.argv[outputArgumentIndex + 1] : "tests/golden/corrective/rehydration/M12-git-history-snapshot");
const filesRoot = path.join(outputRoot, "files");
const drift = JSON.parse(fs.readFileSync(driftPath, "utf8"));
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const run = (args) => execFileSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
const uniqueEntries = new Map();
for (const entry of drift.entries) {
const key = `${entry.path}\0${entry.originalSha256}`;
if (!uniqueEntries.has(key)) uniqueEntries.set(key, entry);
}
function historicalBlob(pathname, expectedSha) {
let objectListing = "";
try {
objectListing = run(["rev-list", "--objects", "--all", "--", pathname]);
} catch {
return null;
}
const candidates = [...new Set(objectListing.split("\n").map((line) => line.split(" ")[0]).filter((id) => /^[0-9a-f]{40}$/u.test(id)))];
for (const blob of candidates) {
let type;
try {
type = run(["cat-file", "-t", blob]).trim();
} catch {
continue;
}
if (type !== "blob") continue;
const bytes = execFileSync("git", ["cat-file", "blob", blob], { cwd: root, maxBuffer: 64 * 1024 * 1024 });
if (sha256(bytes) !== expectedSha) continue;
let commits = [];
try {
commits = run(["log", "--all", "--format=%H", "--", pathname]).trim().split("\n").filter(Boolean);
} catch {
commits = [];
}
const commit = commits.find((candidate) => {
try {
return run(["rev-parse", `${candidate}:${pathname}`]).trim() === blob;
} catch {
return false;
}
}) ?? null;
return { blob, commit, bytes };
}
return null;
}
const matched = [];
const unresolved = [];
const pathPairCounts = new Map();
for (const entry of uniqueEntries.values()) pathPairCounts.set(entry.path, (pathPairCounts.get(entry.path) ?? 0) + 1);
for (const entry of uniqueEntries.values()) {
const pathname = entry.path;
const result = historicalBlob(pathname, entry.originalSha256);
if (!result) {
unresolved.push({ path: pathname, originalSha256: entry.originalSha256, reason: "exact SHA-256 content was not found in Git history" });
continue;
}
const suffix = (pathPairCounts.get(pathname) ?? 0) > 1 ? `.${entry.originalSha256.slice(0, 16)}` : "";
const snapshotPath = path.join("files", `${pathname}${suffix}`);
const destination = path.join(outputRoot, snapshotPath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, result.bytes);
matched.push({
path: pathname,
snapshotPath,
sha256: entry.originalSha256,
bytes: result.bytes.length,
gitBlob: result.blob,
gitCommit: result.commit,
});
}
const repository = run(["remote", "get-url", "origin"]).trim();
const head = run(["rev-parse", "HEAD"]).trim();
const manifest = {
schemaVersion: 1,
mode: "IMMUTABLE_REPOSITORY",
authorized: true,
source: {
kind: "GIT_HISTORY",
repository,
head,
selection: "exact original SHA-256 content match; current worktree is never read",
},
coverage: {
requestedPaths: new Set([...uniqueEntries.values()].map((entry) => entry.path)).size,
requestedPairs: uniqueEntries.size,
matchedPaths: matched.length,
unmatchedPaths: unresolved.length,
unresolved,
},
files: matched.sort((a, b) => a.path.localeCompare(b.path)),
};
fs.mkdirSync(outputRoot, { recursive: true });
fs.writeFileSync(path.join(outputRoot, "snapshot-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
process.stdout.write(`m12-git-snapshot-ok requested=${uniqueEntries.size} matched=${matched.length} unmatched=${unresolved.length} output=${path.relative(root, outputRoot).replaceAll(path.sep, "/")}\n`);

View File

@@ -8,7 +8,7 @@ configure_variant() {
local name="$1"
local threads="$2"
local build_dir="${repo_root}/build_web-${name}"
emcmake cmake -S "${repo_root}/web/engine" -B "${build_dir}" -G Ninja \
emcmake cmake --fresh -S "${repo_root}/web/engine" -B "${build_dir}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DWEB_ENGINE_THREADS="${threads}"
cmake --build "${build_dir}" --target web_engine
test -s "${build_dir}/web_engine.js"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
const json = (relative) => JSON.parse(read(relative));
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const plan = json("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json");
const index = json("tests/golden/WBV2/task-index.json");
const plan = json(index.source.path);
const catalogBytes = fs.readFileSync(path.join(root, index.catalog.path));
assert.equal(index.schemaVersion, 1);

View File

@@ -17,7 +17,7 @@ assert.doesNotMatch(protocol, /export async function decodeVDBResource/, "Browse
const roots = [
path.join(root, "resource-library"),
path.join(root, "tests"),
path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb")),
path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(root, "resource-library", "blender-web-vdb")),
];
const vdbFiles = [];
function scan(directory) {

View File

@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
import ts from "../../web/node_modules/typescript/lib/typescript.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb"));
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(root, "resource-library", "blender-web-vdb"));
const converter = path.join(root, "build_vdb_tools", "vdb_to_nanovdb");
const generator = path.join(root, "build_vdb_tools", "vdb_fixture_generator");
const source = path.join(resourceRoot, "generated", "generated-smoke.vdb");

View File

@@ -10,7 +10,7 @@ import { VDBJobService, createVDBJobHttpServer } from "../vdb/server/vdb-job-ser
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const converter = path.join(root, "build_vdb_tools/vdb_to_nanovdb");
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb"));
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(root, "resource-library", "blender-web-vdb"));
const source = path.join(resourceRoot, "generated/generated-smoke.vdb");
const secret = "vdb-server-test-signing-key-000000000000000000000000";
assert.ok(fs.existsSync(converter), "native converter is missing");

View File

@@ -0,0 +1,171 @@
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`);

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
CONTRACT_TEXT_NAME = "WBV2-P0-003-CapabilityContract"
def read_contract():
text = bpy.data.texts.get(CONTRACT_TEXT_NAME)
if text is None:
raise RuntimeError("CAPABILITY_CONTRACT_INVALID: contract text is missing")
source = text.as_string()
contract = json.loads(source)
return source, contract
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p0-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_contract()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p0-003-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_contract()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source:
raise RuntimeError("CAPABILITY_CONTRACT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P0-003",
"operation": "CAPABILITY_ERROR_BUDGET_CONTRACT_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"contract": after,
"sourceSha256": hashlib.sha256(after_source.encode("utf-8")).hexdigest(),
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p0-003-desktop-ok contract={after['contractId']} schemaHash={after['schemaHash']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
TEXT_NAME = "WBV2-P1-001-RuntimeReceipt"
def read_receipt():
text = bpy.data.texts.get(TEXT_NAME)
if text is None:
raise RuntimeError("RUNTIME_RECEIPT_INVALID: receipt text is missing")
source = text.as_string()
return source, json.loads(source)
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p1-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_receipt()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p1-001-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_receipt()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source:
raise RuntimeError("RUNTIME_RECEIPT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P1-001",
"operation": "RUNTIME_VARIANTS_SOURCE_RECEIPT_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"receipt": after,
"sourceSha256": hashlib.sha256(after_source.encode("utf-8")).hexdigest(),
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p1-001-desktop-ok receipt={after['receiptId']} variants={len(after['variants'])} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
TEXT_NAME = "WBV2-P1-002-SceneIRContract"
def read_contract():
text = bpy.data.texts.get(TEXT_NAME)
if text is None:
raise RuntimeError("SCENEIR_CONTRACT_INVALID: contract text is missing")
source = text.as_string()
return source, json.loads(source)
def scene_summary():
obj = bpy.data.objects.get("WBV2P1002CubeObject")
if obj is None or obj.type != "MESH":
raise RuntimeError("SCENEIR_CONTRACT_INVALID: cube object is missing")
mesh = obj.data
mesh.calc_loop_triangles()
positions = [value for vertex in mesh.vertices for value in vertex.co]
normalized_positions = [int(value) if value == int(value) else round(value, 6) for value in positions]
return {
"objectId": "object:" + obj.name,
"meshId": "mesh:" + mesh.name,
"vertexCount": len(mesh.vertices),
"triangleCount": len(mesh.loop_triangles),
"positionHash": hashlib.sha256(json.dumps(normalized_positions, separators=(",", ":")).encode("utf-8")).hexdigest(),
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p1-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_contract()
summary = scene_summary()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p1-002-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_contract()
after_summary = scene_summary()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source or summary != after_summary:
raise RuntimeError("SCENEIR_CONTRACT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P1-002",
"operation": "SCENEIR_STABLE_IDS_BINARY_DELTA_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"contract": after,
"summary": after_summary,
"sourceSha256": hashlib.sha256(after_source.encode("utf-8")).hexdigest(),
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p1-002-desktop-ok mesh={after_summary['meshId']} vertices={after_summary['vertexCount']} triangles={after_summary['triangleCount']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
TEXT_NAME = "WBV2-P1-003-TransactionContract"
def read_contract():
text = bpy.data.texts.get(TEXT_NAME)
if text is None:
raise RuntimeError("TRANSACTION_CONTRACT_INVALID: contract text is missing")
source = text.as_string()
return source, json.loads(source)
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p1-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_contract()
obj = bpy.data.objects.get("WBV2P1003CubeObject")
if obj is None or obj.hide_viewport is not False:
raise RuntimeError("TRANSACTION_CONTRACT_INVALID: initial visibility drift")
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p1-003-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_contract()
after_obj = bpy.data.objects.get("WBV2P1003CubeObject")
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source or after_obj is None or after_obj.hide_viewport is not False:
raise RuntimeError("TRANSACTION_CONTRACT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P1-003",
"operation": "MAIN_TRANSACTION_HISTORY_SAVE_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"contract": after,
"objectId": "object:" + after_obj.name,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p1-003-desktop-ok object={value['objectId']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
TEXT_NAME = "WBV2-P1-004-WorkerLifecycleContract"
def read_contract():
text = bpy.data.texts.get(TEXT_NAME)
if text is None:
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: contract text is missing")
source = text.as_string()
return source, json.loads(source)
def canonical_hash(value):
payload = dict(value)
payload.pop("schemaHash", None)
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p1-004-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_contract()
if before.get("contractId") != "workflow:harden_worker_cancel_restart_and_dispose":
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: contract id drift")
if before["cancel"]["resultPublished"] is not False or before["generation"]["staleResult"] != "REJECTED":
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: cancellation/generation drift")
if any(before["owners"][owner]["afterDispose"] != 0 for owner in ("cpu", "wasm", "gpu")):
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: owner budget not released")
if canonical_hash(before) != before.get("schemaHash"):
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: schema hash drift")
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p1-004-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_contract()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source:
raise RuntimeError("WORKER_LIFECYCLE_CONTRACT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P1-004",
"operation": "WORKER_CANCEL_RESTART_DISPOSE_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"contract": after,
"sourceSha256": hashlib.sha256(json.dumps(after, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest(),
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p1-004-desktop-ok generation=1->2 cancel=unpublished owners=released saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
TEXT_NAME = "WBV2-P1-005-StorageJournalContract"
def read_contract():
text = bpy.data.texts.get(TEXT_NAME)
if text is None:
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: contract text is missing")
source = text.as_string()
return source, json.loads(source)
def canonical_hash(value):
payload = dict(value)
payload.pop("schemaHash", None)
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p1-005-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
source, before = read_contract()
if before.get("contractId") != "workflow:harden_opfs_indexeddb_journal_recovery":
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: contract id drift")
if before["journal"]["oldProjectUsable"] is not True or before["hash"]["mismatch"] != "QUARANTINED":
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: recovery policy drift")
if any(before["storage"][store]["afterRecovery"] != 0 for store in ("opfs", "indexeddb")):
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: storage budget not released")
if canonical_hash(before) != before.get("schemaHash"):
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: schema hash drift")
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p1-005-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after_source, after = read_contract()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after or source != after_source:
raise RuntimeError("STORAGE_JOURNAL_CONTRACT_INVALID: desktop save/reopen drift")
value = {
"schemaVersion": 1,
"task": "WBV2-P1-005",
"operation": "STORAGE_JOURNAL_RECOVERY_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"contract": after,
"sourceSha256": hashlib.sha256(json.dumps(after, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest(),
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p1-005-desktop-ok interrupted=old-project-usable hash-mismatch=quarantined restart=recovered saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def object_id(obj):
return "object:" + obj.name
def collection_id(collection):
return "collection:" + collection.name
def scene_report():
scene = bpy.context.scene
obj = bpy.data.objects.get("WBV2P2001SelectedObject")
collection = bpy.data.collections.get("WBV2P2001OutlinerCollection")
if obj is None or collection is None:
raise RuntimeError("EDITOR_OPEN_SCENE_INVALID: stable object or collection is missing")
selected = sorted(object_id(value) for value in bpy.context.selected_objects)
if bpy.context.view_layer.objects.active is not obj or selected != [object_id(obj)]:
raise RuntimeError("EDITOR_OPEN_SCENE_INVALID: selection context drift")
return {
"scene": {"id": "scene:" + scene.name, "name": scene.name, "rootCollectionId": collection_id(scene.collection)},
"collection": {
"id": collection_id(collection),
"name": collection.name,
"objectIds": sorted(object_id(value) for value in collection.objects),
"childCollectionIds": sorted(collection_id(value) for value in collection.children),
},
"selection": {
"activeObjectId": object_id(obj),
"selectedObjectIds": selected,
"revision": 1,
},
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p2-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True)
before = scene_report()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p2-001-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=True)
after = scene_report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"EDITOR_OPEN_SCENE_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P2-001",
"operation": "EDITOR_OPEN_SCENE_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"context": after,
"missingData": {"pinnedData": None, "library": None},
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p2-001-desktop-ok scene={after['scene']['id']} active={after['selection']['activeObjectId']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def oid(obj):
return "object:" + obj.name
def cid(collection):
return "collection:" + collection.name
def vector(values):
return [round(float(value), 6) for value in values]
def report():
scene = bpy.context.scene
parent = bpy.data.objects.get("WBV2P2002Parent")
child = bpy.data.objects.get("WBV2P2002Child")
source = bpy.data.collections.get("WBV2P2002Source")
target = bpy.data.collections.get("WBV2P2002Target")
if None in (parent, child, source, target):
raise RuntimeError("OBJECT_TRANSFORM_INVALID: fixture datablock is missing")
return {
"scene": {"id": "scene:" + scene.name, "name": scene.name},
"objects": {
"parent": {
"id": oid(parent), "dataId": "mesh:" + parent.data.name,
"translation": vector(parent.location), "rotationEuler": vector(parent.rotation_euler),
"scale": vector(parent.scale), "parentId": None,
"worldTranslation": vector(parent.matrix_world.translation),
},
"child": {
"id": oid(child), "dataId": "mesh:" + child.data.name,
"translation": vector(child.location), "rotationEuler": vector(child.rotation_euler),
"scale": vector(child.scale), "parentId": oid(child.parent),
"worldTranslation": vector(child.matrix_world.translation),
},
},
"collections": {
"source": {"id": cid(source), "objectIds": [oid(obj) for obj in source.objects]},
"target": {"id": cid(target), "objectIds": [oid(obj) for obj in target.objects]},
},
"selection": {"activeObjectId": oid(bpy.context.view_layer.objects.active), "selectedObjectIds": sorted(oid(obj) for obj in bpy.context.selected_objects), "revision": 1},
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p2-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True)
before = report()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p2-002-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=True)
after = report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"OBJECT_TRANSFORM_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P2-002",
"operation": "OBJECT_TRANSFORM_HIERARCHY_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"scene": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p2-002-desktop-ok parent={after['objects']['parent']['id']} child={after['objects']['child']['id']} worldChild={after['objects']['child']['worldTranslation']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
OPERATIONS = ("Translate", "Merge", "Dissolve", "Extrude", "Inset", "Bevel", "LoopCut")
def mesh_report(label):
obj = bpy.data.objects.get("WBV2P2003%sObject" % label)
if obj is None or obj.type != "MESH":
raise RuntimeError("MESH_EDIT_INVALID: operation fixture object is missing")
mesh = obj.data
return {
"objectId": "object:" + obj.name,
"meshId": "mesh:" + mesh.name,
"vertexCount": len(mesh.vertices),
"edgeCount": len(mesh.edges),
"faceCount": len(mesh.polygons),
"triangleCount": sum(max(0, len(poly.vertices) - 2) for poly in mesh.polygons),
"positionHash": hashlib.sha256(json.dumps([tuple(round(float(value), 6) for value in vertex.co) for vertex in mesh.vertices], separators=(",", ":")).encode()).hexdigest(),
}
def report():
return {label: mesh_report(label) for label in OPERATIONS}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p2-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True)
before = report()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p2-003-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=True)
after = report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"MESH_EDIT_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P2-003",
"operation": "MESH_EDIT_CORE_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"meshes": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p2-003-desktop-ok meshes=%d topology=quad saveReopen=exact" % len(after))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def report_scene():
scene = bpy.context.scene
cube = bpy.data.objects.get("WBV2P2004ViewportCube")
camera = bpy.data.objects.get("WBV2P2004ViewportCamera")
if cube is None or cube.type != "MESH" or camera is None or camera.type != "CAMERA":
raise RuntimeError("VIEWPORT_INVALID: viewport fixture is missing cube or camera")
return {
"sceneId": "scene:" + scene.name,
"cubeId": "object:" + cube.name,
"meshId": "mesh:" + cube.data.name,
"cameraId": "object:" + camera.name,
"cameraDataId": "camera:" + camera.data.name,
"cubeLocation": [round(float(value), 6) for value in cube.location],
"cameraLocation": [round(float(value), 6) for value in camera.location],
"cameraLensMm": round(float(camera.data.lens), 6),
"vertexCount": len(cube.data.vertices),
"faceCount": len(cube.data.polygons),
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p2-004-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True)
before = report_scene()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p2-004-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=True)
after = report_scene()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"VIEWPORT_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P2-004",
"operation": "VIEWPORT_MAIN_OFFSCREEN_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"scene": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p2-004-desktop-ok cube=%s camera=%s pixels=browser saveReopen=exact" % (after["cubeId"], after["cameraId"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def report_scene():
scene = bpy.context.scene
cube = bpy.data.objects.get("WBV2P2005WorkflowCube")
if cube is None or cube.type != "MESH":
raise RuntimeError("WORKFLOW_INVALID: workflow fixture cube is missing")
return {
"sceneId": "scene:" + scene.name,
"objectId": "object:" + cube.name,
"meshId": "mesh:" + cube.data.name,
"location": [round(float(value), 6) for value in cube.location],
"vertexCount": len(cube.data.vertices),
"faceCount": len(cube.data.polygons),
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p2-005-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=True)
before = report_scene()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p2-005-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=True)
after = report_scene()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"WORKFLOW_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P2-005",
"operation": "WORKFLOW_P0_USER_LOOP_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"scene": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p2-005-desktop-ok object=%s topology=cube saveReopen=exact" % after["objectId"])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def rounded(value):
return round(float(value), 6)
def material_report():
material = bpy.data.materials.get("WBV2P3001PrincipledMaterial")
cube = bpy.data.objects.get("WBV2P3001MaterialCube")
if material is None or cube is None or cube.type != "MESH" or not material.use_nodes:
raise RuntimeError("MATERIAL_INVALID: material fixture is missing")
principled = material.node_tree.nodes.get("Principled BSDF")
if principled is None:
raise RuntimeError("MATERIAL_INVALID: Principled BSDF is missing")
uv_layer = cube.data.uv_layers.active
if uv_layer is None:
raise RuntimeError("MATERIAL_INVALID: active UV layer is missing")
uv_values = [rounded(value) for loop in uv_layer.data for value in loop.uv]
return {
"materialId": "material:" + material.name,
"materialName": material.name,
"baseColor": [rounded(value) for value in principled.inputs["Base Color"].default_value],
"roughness": rounded(principled.inputs["Roughness"].default_value),
"metallic": rounded(principled.inputs["Metallic"].default_value),
"ior": rounded(principled.inputs["IOR"].default_value),
"nodeTypes": sorted(node.bl_idname for node in material.node_tree.nodes),
"unsupportedNode": "ShaderNodeTexNoise" in [node.bl_idname for node in material.node_tree.nodes],
"uvLayerNames": [layer.name for layer in cube.data.uv_layers],
"activeUVMap": uv_layer.name,
"uvHash": hashlib.sha256(json.dumps(uv_values, separators=(",", ":")).encode()).hexdigest(),
"uvValueCount": len(uv_values),
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p3-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = material_report()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-001-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after = material_report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"MATERIAL_INVALID: desktop save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P3-001",
"operation": "MATERIAL_PBR_UV_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"material": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p3-001-desktop-ok material=%s uv=%s unsupported=retained saveReopen=exact" % (after["materialId"], after["activeUVMap"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def action_report():
cube = bpy.data.objects.get("WBV2P3002AnimatedCube")
if cube is None or cube.animation_data is None or cube.animation_data.action is None:
raise RuntimeError("ANIMATION_INVALID: animated cube Action is missing")
action = cube.animation_data.action
channels = []
for layer in action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for curve in bag.fcurves:
channels.append({
"path": curve.data_path,
"index": curve.array_index,
"frames": [round(float(key.co.x), 6) for key in curve.keyframe_points],
"values": [round(float(key.co.y), 6) for key in curve.keyframe_points],
"interpolation": [key.interpolation for key in curve.keyframe_points],
})
channels.sort(key=lambda value: (value["path"], value["index"]))
return {"id": "action:" + action.name + ":object:" + cube.name, "name": action.name, "targetId": "object:" + cube.name, "channels": channels}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p3-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = action_report()
if len(before["channels"]) != 9 or any(channel["frames"] != [1.0, 10.0, 20.0] for channel in before["channels"]):
raise RuntimeError(f"ANIMATION_INVALID: unexpected TRS channels: {before}")
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-002-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after = action_report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"ANIMATION_INVALID: desktop save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P3-002",
"operation": "ANIMATION_TRS_ACTION_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"action": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p3-002-desktop-ok action=%s channels=%d frames=1,10,20 saveReopen=exact" % (after["id"], len(after["channels"])))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def rounded(value):
return round(float(value), 6)
def matrix(value):
return [rounded(component) for row in value for component in row]
def report():
mesh_object = bpy.data.objects.get("WBV2P3003SkinnedMeshObject")
armature_object = bpy.data.objects.get("WBV2P3003ArmatureObject")
if mesh_object is None or armature_object is None:
raise RuntimeError("RIG_INVALID: required objects are missing")
armature = armature_object.data
bones = []
for bone in armature.bones:
pose_bone = armature_object.pose.bones[bone.name]
bones.append({
"name": bone.name,
"parent": bone.parent.name if bone.parent else None,
"head": [rounded(value) for value in bone.head_local],
"tail": [rounded(value) for value in bone.tail_local],
"poseMatrix": matrix(pose_bone.matrix),
"constraints": [{
"name": constraint.name,
"type": constraint.type,
"influence": rounded(constraint.influence),
} for constraint in pose_bone.constraints],
})
groups = [group.name for group in mesh_object.vertex_groups]
weights = []
for vertex in mesh_object.data.vertices:
weights.append({
group.name: rounded(next((item.weight for item in vertex.groups if item.group == group.index), 0.0))
for group in mesh_object.vertex_groups
})
shape_keys = []
if mesh_object.data.shape_keys:
for key in mesh_object.data.shape_keys.key_blocks:
shape_keys.append({
"name": key.name,
"value": rounded(key.value),
"positions": [rounded(component) for point in key.data for component in point.co],
})
modifier = next((value for value in mesh_object.modifiers if value.type == "ARMATURE"), None)
if modifier is None or modifier.object != armature_object:
raise RuntimeError("RIG_INVALID: armature modifier is missing")
object_constraints = [{
"name": constraint.name,
"type": constraint.type,
"influence": rounded(constraint.influence),
} for constraint in mesh_object.constraints]
return {
"armature": {
"id": "armature:" + armature.name,
"objectId": "object:" + armature_object.name,
"name": armature.name,
"bones": bones,
},
"mesh": {
"id": "mesh:" + mesh_object.data.name,
"objectId": "object:" + mesh_object.name,
"groups": groups,
"weights": weights,
"shapeKeys": shape_keys,
"armatureModifier": {"name": modifier.name, "objectId": "object:" + armature_object.name},
"constraints": object_constraints,
},
"unsupportedConstraint": {
"name": "WBV2P3003UnsupportedLimitRotation",
"type": "LIMIT_ROTATION",
"status": "EXPLICIT_UNSUPPORTED_BY_WASM_DEPSGRAPH",
},
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p3-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = report()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-003-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after = report()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"RIG_INVALID: desktop save/reopen drift: {before} != {after}")
value = {
"schemaVersion": 1,
"task": "WBV2-P3-003",
"operation": "RIG_POSE_SKIN_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"rig": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p3-003-desktop-ok armature=%s bones=%d groups=%d shapeKeys=%d unsupported=explicit saveReopen=exact" % (after["armature"]["id"], len(after["armature"]["bones"]), len(after["mesh"]["groups"]), len(after["mesh"]["shapeKeys"])))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import bpy
def rounded(value):
return round(float(value), 6)
def canonical():
obj = bpy.data.objects.get("WBV2P3004RoundTripMeshObject")
armature = bpy.data.objects.get("WBV2P3004ArmatureObject")
if obj is None or armature is None or obj.type != "MESH" or armature.type != "ARMATURE":
raise RuntimeError("GLB_INVALID: roundtrip fixture objects are missing")
material = obj.data.materials[0] if obj.data.materials else None
principled = material.node_tree.nodes.get("Principled BSDF") if material and material.node_tree else None
shape_keys = []
if obj.data.shape_keys:
for key in obj.data.shape_keys.key_blocks[1:]:
shape_keys.append({"name": key.name, "value": rounded(key.value), "vertexCount": len(key.data)})
weights = []
for vertex in obj.data.vertices:
weights.append(sorted(rounded(group.weight) for group in vertex.groups))
action = obj.animation_data.action if obj.animation_data else None
channels = []
if action:
for layer in action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
channels.extend(curve.data_path for curve in bag.fcurves)
return {
"mesh": {"name": obj.data.name, "vertexCount": len(obj.data.vertices), "triangleCount": len(obj.data.loop_triangles), "uvLayers": [layer.name for layer in obj.data.uv_layers], "shapeKeys": shape_keys},
"material": {"name": material.name if material else None, "baseColor": [rounded(value) for value in principled.inputs["Base Color"].default_value] if principled else [], "metallic": rounded(principled.inputs["Metallic"].default_value) if principled else None, "roughness": rounded(principled.inputs["Roughness"].default_value) if principled else None, "imagePacked": bool(material and any(node.type == "TEX_IMAGE" and node.image and node.image.packed_file for node in material.node_tree.nodes))},
"skin": {"bones": [bone.name for bone in armature.data.bones], "parents": {bone.name: bone.parent.name if bone.parent else None for bone in armature.data.bones}, "vertexGroups": [group.name for group in obj.vertex_groups], "weightSums": weights},
"animation": {"actionName": action.name if action else None, "channelCount": len(channels), "frameRange": [bpy.context.scene.frame_start, bpy.context.scene.frame_end]},
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p3-004-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = canonical()
descriptor, temporary = tempfile.mkstemp(prefix="wbv2-p3-004-reopen-", suffix=".blend", dir=fixture.parent)
os.close(descriptor)
try:
bpy.ops.wm.save_as_mainfile(filepath=temporary, check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=temporary, load_ui=False)
after = canonical()
finally:
pathlib.Path(temporary).unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"GLB_INVALID: desktop save/reopen drift: {before} != {after}")
value = {"schemaVersion": 1, "task": "WBV2-P3-004", "operation": "IO_GLB_ROUNDTRIP_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "canonical": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p3-004-desktop-ok vertices=%d triangles=%d bones=%d morphs=%d channels=%d texture=packed saveReopen=exact" % (after["mesh"]["vertexCount"], after["mesh"]["triangleCount"], len(after["skin"]["bones"]), len(after["mesh"]["shapeKeys"]), after["animation"]["channelCount"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
LIBRARY_NAME = "WBV2P3005Library.blend"
LINKED_OBJECT = "WBV2P3005LinkedObject"
APPEND_OBJECT = "WBV2P3005AppendObject"
def report_state() -> dict:
libraries = list(bpy.data.libraries)
if len(libraries) != 1:
raise RuntimeError(f"expected one external library, found {len(libraries)}")
library = libraries[0]
linked = bpy.data.objects.get(LINKED_OBJECT)
if linked is None or linked.library is None:
raise RuntimeError("linked object ownership was not retained")
return {
"library": {
"name": library.name,
"sourcePath": library.filepath,
"packed": library.packed_file is not None,
"readOnly": True,
"dependencyIds": [],
},
"linked": {
"name": linked.name,
"library": linked.library.name,
"readOnly": linked.library is not None,
"override": linked.override_library is not None,
},
}
def source_state(source: pathlib.Path) -> dict:
bpy.ops.wm.open_mainfile(filepath=str(source), load_ui=False)
appended = bpy.data.objects.get(APPEND_OBJECT)
if appended is None or appended.data is None or len(appended.data.materials) != 1:
raise RuntimeError("append source closure is incomplete")
material = appended.data.materials[0]
image = next((node.image for node in material.node_tree.nodes if node.type == "TEX_IMAGE" and node.image), None)
if image is None or image.packed_file is None:
raise RuntimeError("append source image is not packed")
return {
"object": appended.name,
"mesh": appended.data.name,
"material": material.name,
"image": image.name,
"imagePacked": True,
}
def main() -> None:
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 3:
raise SystemExit("usage: blender -b --python check-wbv2-p3-005-desktop.py -- FIXTURE SOURCE REPORT")
fixture, source, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = report_state()
source_closure = source_state(source)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
reopen_before = report_state()
if before != reopen_before:
raise RuntimeError(f"fixture reload drift: {before} != {reopen_before}")
temporary = fixture.with_name("wbv2-p3-005-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = report_state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"library save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P3-005",
"operation": "ASSET_LIBRARY_OWNERSHIP_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"sourceSha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"sourceClosure": source_closure,
"state": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p3-005-desktop-ok library={after['library']['name']} linked={after['linked']['name']} appendImage=packed saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def modifier_report(obj):
return [{
"name": modifier.name,
"type": modifier.type,
"showViewport": modifier.show_viewport,
"showRender": modifier.show_render,
"showEditMode": modifier.show_in_editmode,
"showOnCage": modifier.show_on_cage,
} for modifier in obj.modifiers]
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --python check-wbv2-p4-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
obj = bpy.data.objects.get("WBV2P4001ModifierObject")
if obj is None:
raise RuntimeError("modifier fixture object is missing")
before = {"object": obj.name, "vertexCount": len(obj.data.vertices), "polygonCount": len(obj.data.polygons), "modifiers": modifier_report(obj)}
if [item["type"] for item in before["modifiers"]] != ["BEVEL", "DECIMATE", "PARTICLE_SYSTEM"]:
raise RuntimeError(f"unexpected modifier stack: {before}")
temporary = fixture.with_name("wbv2-p4-001-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
reopened = bpy.data.objects.get("WBV2P4001ModifierObject")
after = {"object": reopened.name, "vertexCount": len(reopened.data.vertices), "polygonCount": len(reopened.data.polygons), "modifiers": modifier_report(reopened)}
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"modifier save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P4-001", "operation": "MODIFIER_DECLARED_SLICES_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p4-001-desktop-ok object={after['object']} modifiers={len(after['modifiers'])} unknown=PARTICLE_SYSTEM saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def graph_record(group):
sockets = [
{"name": item.name, "inOut": item.in_out, "socketType": item.socket_type}
for item in group.interface.items_tree
if item.item_type == "SOCKET"
]
return {
"name": group.name,
"type": group.bl_idname,
"interface": sockets,
"nodeTypes": sorted(node.bl_idname for node in group.nodes),
"nodeNames": sorted(node.name for node in group.nodes),
"linkCount": len(group.links),
}
def material_record(material):
tree = material.node_tree
return {
"name": material.name,
"nodeTypes": sorted(node.bl_idname for node in tree.nodes),
"nodeNames": sorted(node.name for node in tree.nodes),
"linkCount": len(tree.links),
}
def state():
supported = bpy.data.node_groups.get("WBV2P4002SupportedGraph")
blocked = bpy.data.node_groups.get("WBV2P4002BlockedGraph")
shader = bpy.data.materials.get("WBV2P4002ShaderMaterial")
if supported is None or blocked is None or shader is None:
raise RuntimeError("P4-002 fixture data blocks are missing")
if not any(modifier.type == "NODES" and modifier.node_group == supported for obj in bpy.data.objects for modifier in obj.modifiers):
raise RuntimeError("supported Geometry Nodes modifier is missing")
if not any(modifier.type == "NODES" and modifier.node_group == blocked for obj in bpy.data.objects for modifier in obj.modifiers):
raise RuntimeError("blocked Geometry Nodes modifier is missing")
return {
"graphs": [graph_record(supported), graph_record(blocked)],
"material": material_record(shader),
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p4-002-desktop.py -- FIXTURE REPORT")
fixture, report_path = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"node graph save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P4-002",
"operation": "NODES_ALLOWLIST_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"state": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p4-002-desktop-ok graphs=2 supported=GeometryNodeTransform blocked=GeometryNodeDistributePointsOnFaces shader=ShaderNodeRGB saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def mesh_record(obj):
mesh = obj.data
mask = mesh.attributes.get(".sculpt_mask")
face_sets = mesh.attributes.get(".sculpt_face_set")
if mask is None or face_sets is None:
raise RuntimeError("sculpt attributes are missing")
return {
"name": obj.name,
"vertexCount": len(mesh.vertices),
"faceCount": len(mesh.polygons),
"mask": [round(item.value, 6) for item in mask.data],
"faceSets": [item.value for item in face_sets.data],
}
def nonmesh_record():
curve = bpy.data.curves.get("WBV2P4003Curve")
point_cloud = bpy.data.pointclouds.get("WBV2P4003PointCloud")
if curve is None or point_cloud is None:
raise RuntimeError("non-mesh fixture data is missing")
return {
"curve": {"name": curve.name, "splineCount": len(curve.splines), "pointCount": sum(len(item.bezier_points) + len(item.points) for item in curve.splines)},
"pointCloud": {"name": point_cloud.name, "pointCount": len(point_cloud.points)},
}
def state():
obj = bpy.data.objects.get("WBV2P4003SculptObject")
if obj is None or obj.type != "MESH":
raise RuntimeError("sculpt object is missing")
return {"mesh": mesh_record(obj), "nonMesh": nonmesh_record()}
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p4-003-desktop.py -- FIXTURE REPORT")
fixture, report_path = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"sculpt/non-mesh save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P4-003",
"operation": "SCULPT_NONMESH_PAINT_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"state": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p4-003-desktop-ok vertices=5 faces=5 curvePoints=3 pointCloudPoints=3 saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
body = bpy.data.objects.get("WBV2P4004RigidBody")
floor = bpy.data.objects.get("WBV2P4004Collision")
if body is None or floor is None or body.rigid_body is None or floor.rigid_body is None:
raise RuntimeError("simulation fixture rigid-body data is missing")
return {
"body": {"name": body.name, "type": body.rigid_body.type, "shape": body.rigid_body.collision_shape, "mass": body.rigid_body.mass, "friction": body.rigid_body.friction, "restitution": body.rigid_body.restitution, "linearDamping": body.rigid_body.linear_damping, "angularDamping": body.rigid_body.angular_damping},
"collision": {"name": floor.name, "type": floor.rigid_body.type, "shape": floor.rigid_body.collision_shape},
"frame": [bpy.context.scene.frame_start, bpy.context.scene.frame_end, bpy.context.scene.frame_current],
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p4-004-desktop.py -- FIXTURE REPORT")
fixture, report_path = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"simulation save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P4-004", "operation": "SIMULATION_CACHE_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p4-004-desktop-ok body=ACTIVE mass=2.5 collision=PASSIVE frames=1..4 saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def render_state(render_path):
scene = bpy.context.scene
cube = bpy.data.objects.get("WBV2P5001RenderCube")
camera = bpy.data.objects.get("WBV2P5001RenderCamera")
receipt_text = bpy.data.texts.get("WBV2-P5-001-RenderRoutingReceipt")
if cube is None or camera is None or receipt_text is None:
raise RuntimeError("render routing fixture is missing cube, camera, or receipt")
receipt = json.loads(receipt_text.as_string())
scene.render.filepath = str(render_path)
bpy.ops.render.render(write_still=True)
image = bpy.data.images.load(str(render_path), check_existing=False)
if tuple(image.size) != (64, 64):
raise RuntimeError("render routing fixture did not produce a 64x64 image")
pixels = image.pixels[:]
rgba = bytes(max(0, min(255, round(float(value) * 255))) for value in pixels)
visible = sum(1 for index in range(0, len(rgba), 4) if sum(rgba[index:index + 3]) > 12)
result = {
"sceneId": "scene:" + scene.name,
"cubeId": "object:" + cube.name,
"cameraId": "object:" + camera.name,
"renderEngine": scene.render.engine,
"resolution": [scene.render.resolution_x, scene.render.resolution_y, scene.render.resolution_percentage],
"receipt": receipt,
"pixelSha256": hashlib.sha256(rgba).hexdigest(),
"pixelBytes": len(rgba),
"visiblePixels": visible,
}
bpy.data.images.remove(image)
return result
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p5-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
render_path = fixture.with_name(f"{fixture.stem}-render.png")
before = render_state(render_path)
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = render_state(render_path)
finally:
temporary.unlink(missing_ok=True)
render_path.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"render routing save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P5-001",
"operation": "RENDER_ROUTING_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"state": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p5-001-desktop-ok engine=%s pixels=%s visible=%d routes=%d saveReopen=exact" % (after["renderEngine"], after["pixelSha256"], after["visiblePixels"], len(after["receipt"]["routes"])))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
scene = bpy.context.scene
tree = scene.compositing_node_group
editor = scene.sequence_editor
if tree is None or editor is None:
raise RuntimeError("media fixture is missing compositor or sequencer")
strips = sorted(editor.strips_all, key=lambda strip: strip.name)
if len(tree.nodes) != 4 or len(tree.links) != 3 or len(strips) != 4:
raise RuntimeError("media fixture has unexpected compositor/sequencer shape")
return {
"sceneId": "scene:" + scene.name,
"compositor": {"name": tree.name, "nodes": sorted(node.name for node in tree.nodes), "links": len(tree.links)},
"sequencer": {
"fps": [scene.render.fps, scene.render.fps_base],
"strips": [{"name": strip.name, "type": strip.type, "channel": strip.channel, "frameStart": strip.frame_final_start, "frameEnd": strip.frame_final_end, "mute": strip.mute, "source": getattr(getattr(strip, "sound", None), "filepath", None) or getattr(strip, "directory", None)} for strip in strips],
},
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p5-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"media save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P5-002", "operation": "MEDIA_BOUNDED_WORKFLOW_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p5-002-desktop-ok compositorNodes=%d sequencerStrips=%d fps=%s saveReopen=exact" % (len(after["compositor"]["nodes"]), len(after["sequencer"]["strips"]), after["sequencer"]["fps"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
scene = bpy.context.scene
receipt = bpy.data.texts.get("WBV2-P5-003-ServerJobBindingReceipt")
cube = bpy.data.objects.get("WBV2P5003ServerCube")
if receipt is None or cube is None:
raise RuntimeError("server job fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "engine": scene.render.engine, "resolution": [scene.render.resolution_x, scene.render.resolution_y], "receipt": json.loads(receipt.as_string())}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p5-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"server job save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P5-003", "operation": "SERVER_JOB_BINDING_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p5-003-desktop-ok engine=%s request=%s result=%s saveReopen=exact" % (after["engine"], after["receipt"]["request"]["requestSha256"], after["receipt"]["result"]["resultSha256"]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
scene = bpy.context.scene
receipt = bpy.data.texts.get("WBV2-P6-001-ScriptingPolicyReceipt")
malicious = bpy.data.texts.get("WBV2-P6-001-MaliciousScript")
if receipt is None or malicious is None:
raise RuntimeError("scripting policy fixture is missing receipt or source")
return {
"sceneId": "scene:" + scene.name,
"receipt": json.loads(receipt.as_string()),
"source": malicious.as_string(),
"useModule": malicious.use_module,
}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p6-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"scripting policy save/reopen drift: {before} != {after}")
policy = after["receipt"]
if policy["defaults"] != {"addonInstall": False, "autorun": False, "driverExpressions": False, "module": False, "network": False}:
raise RuntimeError("scripting policy defaults are not deny-by-default")
if policy["boundaries"] != {"filesystemEscape": "BLOCKED", "network": "BLOCKED", "secretAccess": "BLOCKED"}:
raise RuntimeError("scripting policy boundaries are not blocked")
report = {
"schemaVersion": 1,
"task": "WBV2-P6-001",
"operation": "SECURITY_SCRIPTING_DESKTOP_METADATA",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"state": after,
"execution": "DISABLED",
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p6-001-desktop-ok defaults=deny sandbox=disabled boundaries=blocked saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
scene = bpy.context.scene
receipt = bpy.data.texts.get("WBV2-P6-002-PerformanceFaultMatrixReceipt")
cube = bpy.data.objects.get("WBV2P6002RecoveryCube")
if receipt is None or cube is None:
raise RuntimeError("performance fault fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "receipt": json.loads(receipt.as_string())}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p6-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"performance fault save/reopen drift: {before} != {after}")
receipt = after["receipt"]
if len(receipt["thresholds"]) != 7 or any(item["status"] != "PASS" for item in receipt["thresholds"]):
raise RuntimeError("performance thresholds are incomplete")
if any(item["status"] != "BLOCKED" or item["publishedResults"] != 0 for item in receipt["faults"].values()):
raise RuntimeError("faults must fail closed without publishing")
report = {"schemaVersion": 1, "task": "WBV2-P6-002", "operation": "PERFORMANCE_FAULT_MATRIX_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p6-002-desktop-ok thresholds=7 faults=4 blocked=4 saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def state():
scene = bpy.context.scene
receipt = bpy.data.texts.get("WBV2-P6-003-OfflineCiSbomRollbackReleaseReceipt")
cube = bpy.data.objects.get("WBV2P6003ReleaseReceiptCube")
if receipt is None or cube is None:
raise RuntimeError("release receipt fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "receipt": json.loads(receipt.as_string())}
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p6-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"release receipt save/reopen drift: {before} != {after}")
receipt = after["receipt"]
if receipt["ci"]["quick"]["status"] != "READY" or any(receipt["gates"][name]["status"] != "READY" for name in ("offlineReproducibility", "sbom", "rollback")):
raise RuntimeError("release gates must be READY")
report = {"schemaVersion": 1, "task": "WBV2-P6-003", "operation": "OFFLINE_CI_SBOM_ROLLBACK_RELEASE_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p6-003-desktop-ok ci=ready archives=2 sbom=ready rollback=ready saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
RECEIPT_NAME = "WBV2-P7-001-ClusterFullBlenderGapInventoryReceipt"
CUBE_NAME = "WBV2P7001GapInventoryCube"
def state():
scene = bpy.context.scene
receipt_text = bpy.data.texts.get(RECEIPT_NAME)
cube = bpy.data.objects.get(CUBE_NAME)
if receipt_text is None or cube is None:
raise RuntimeError("gap inventory fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "receipt": json.loads(receipt_text.as_string())}
def validate(receipt):
if receipt.get("schemaVersion") != 1 or receipt.get("receiptId") != "workflow:cluster_full_blender_gap_inventory":
raise RuntimeError("gap inventory receipt identity is invalid")
source = receipt.get("inventorySource", {})
if source.get("entryCount") != receipt.get("summary", {}).get("inventoryEntries") or len(source.get("sha256", "")) != 64:
raise RuntimeError("gap inventory source binding is invalid")
clusters = receipt.get("clusters", [])
summary = receipt.get("summary", {})
if len(clusters) != summary.get("clusterCount") or len(clusters) != 15:
raise RuntimeError("gap inventory clusters are incomplete")
if summary.get("currentParityClaims") != 0 or summary.get("currentParityLevel") != "L0":
raise RuntimeError("gap inventory must not claim parity")
if summary.get("webGapEntries") != summary.get("inventoryEntries"):
raise RuntimeError("all unproven inventory entries must remain explicit gaps")
ids = []
for cluster in clusters:
ids.append(cluster.get("capabilityId"))
if (cluster.get("entryCount", 0) != cluster.get("legacyCompleted", 0) + cluster.get("legacyPending", 0) + cluster.get("legacyActive", 0) or
cluster.get("currentParityLevel") != "L0" or cluster.get("completionClass") != "METADATA_ONLY" or
cluster.get("evidenceStatus") != "NOT_RUN" or cluster.get("webStatus") != "GAP" or
not isinstance(cluster.get("riskScore"), int) or not cluster.get("representativeIds")):
raise RuntimeError("gap inventory cluster is not fail-closed")
if len(set(ids)) != len(ids) or receipt.get("riskRanking") != [cluster["capabilityId"] for cluster in sorted(clusters, key=lambda value: (-value["riskScore"], value["capabilityId"]))]:
raise RuntimeError("gap inventory risk ranking is not deterministic")
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p7-001-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
validate(before["receipt"])
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"gap inventory save/reopen drift: {before} != {after}")
report = {
"schemaVersion": 1,
"task": "WBV2-P7-001",
"operation": "CLUSTER_FULL_BLENDER_GAP_INVENTORY_DESKTOP",
"fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(),
"state": after,
"saveReopen": "EXACT",
"blenderVersion": bpy.app.version_string,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p7-001-desktop-ok clusters={len(after['receipt']['clusters'])} entries={after['receipt']['summary']['inventoryEntries']} webGap={after['receipt']['summary']['webGapEntries']} saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
RECEIPT_NAME = "WBV2-P7-002-AtomicFamilyGapQueueReceipt"
CUBE_NAME = "WBV2P7002AtomicGapQueueCube"
BEHAVIORS = ["desktopPositive", "wasmMutation", "saveReopenExact"]
def state():
scene = bpy.context.scene
text = bpy.data.texts.get(RECEIPT_NAME)
cube = bpy.data.objects.get(CUBE_NAME)
if text is None or cube is None:
raise RuntimeError("atomic queue fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "receipt": json.loads(text.as_string())}
def validate(receipt):
if receipt.get("schemaVersion") != 1 or receipt.get("receiptId") != "workflow:generate_atomic_family_gap_queue":
raise RuntimeError("atomic queue receipt identity is invalid")
parent = receipt.get("parent", {})
queue = receipt.get("queue", {})
tasks = receipt.get("tasks", [])
if (parent.get("task") != "WBV2-P7-001" or parent.get("manifestPath") != "tests/golden/WBV2-P7-001/manifest.json" or
len(parent.get("manifestSha256", "")) != 64 or len(parent.get("receiptSha256", "")) != 64 or
queue.get("activeCount") != 1 or queue.get("plannedCount") != 44 or queue.get("behaviors") != BEHAVIORS or
queue.get("implementationClasses") != ["LOCAL_BOUNDED"] or len(tasks) != 45 or receipt.get("nextTask") != "WBV2-P7-003"):
raise RuntimeError("atomic queue parent or cardinality is invalid")
active = [task for task in tasks if task.get("state") == "active" and task.get("ready") is True]
if len(active) != 1 or queue.get("activeTaskId") != active[0].get("id"):
raise RuntimeError("atomic queue must have exactly one active ready task")
ids = set()
for index, task in enumerate(tasks):
if (task.get("id") in ids or not isinstance(task.get("id"), str) or task.get("parityId") != task.get("capabilityId") or
task.get("behavior") != BEHAVIORS[index % 3] or task.get("implementationClass") != "LOCAL_BOUNDED" or
task.get("parentTask") != "WBV2-P7-001" or len(task.get("parentReceiptSha256", "")) != 64 or
task.get("state") != ("active" if index == 0 else "pending") or task.get("ready") is not (index == 0)):
raise RuntimeError("atomic queue ordering or task state is invalid")
ids.add(task["id"])
if receipt["tasks"] != sorted(tasks, key=lambda value: (value["capabilityId"], BEHAVIORS.index(value["behavior"]))):
raise RuntimeError("atomic queue ordering is not deterministic")
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p7-002-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
validate(before["receipt"])
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"atomic queue save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P7-002", "operation": "GENERATE_ATOMIC_FAMILY_GAP_QUEUE_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wbv2-p7-002-desktop-ok tasks={len(after['receipt']['tasks'])} active=1 planned=44 saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
RECEIPT_NAME = "WBV2-P7-003-FullParityCandidateReleaseGateReceipt"
CUBE_NAME = "WBV2P7003ParityReleaseGateCube"
def state():
scene = bpy.context.scene
text = bpy.data.texts.get(RECEIPT_NAME)
cube = bpy.data.objects.get(CUBE_NAME)
if text is None or cube is None:
raise RuntimeError("release gate fixture is missing receipt or cube")
return {"sceneId": "scene:" + scene.name, "cubeId": "object:" + cube.name, "receipt": json.loads(text.as_string())}
def validate(receipt):
if receipt.get("schemaVersion") != 1 or receipt.get("receiptId") != "workflow:full_parity_candidate_release_gate":
raise RuntimeError("release gate receipt identity is invalid")
inputs = receipt.get("inputs", {})
if len(inputs) != 4 or any(len(value.get("sha256", "")) != 64 or not value.get("path") for value in inputs.values()):
raise RuntimeError("release gate input bindings are invalid")
closure = receipt.get("parityClosure", {})
evidence = receipt.get("evidenceHealth", {})
recovery = receipt.get("recoveryHealth", {})
decision = receipt.get("releaseDecision", {})
if (closure.get("status") != "BLOCKED" or closure.get("inventoryEntries") != 6900 or closure.get("clusterCount") != 15 or
closure.get("atomicTaskCount") != 45 or closure.get("currentParityLevel") != "L0" or closure.get("featureParityClaims") != 0 or
evidence.get("status") != "BLOCKED" or evidence.get("m12ProvenanceState") != "BLOCKED_SOURCE_LOST" or
evidence.get("historicalEvidencePreserved") is not True or evidence.get("m12Entries", 0) < 1 or
recovery.get("status") != "READY" or recovery.get("quickCi") != "READY" or recovery.get("inventorySaveReopen") != "EXACT" or
recovery.get("queueSaveReopen") != "EXACT" or recovery.get("malformedBlend") != "REJECTED_WITHOUT_MAIN_MUTATION" or
decision.get("status") != "BLOCKED" or decision.get("nextTask") is not None):
raise RuntimeError("release gate decision is not fail-closed")
def main():
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 2:
raise SystemExit("usage: blender -b --factory-startup --python check-wbv2-p7-003-desktop.py -- FIXTURE REPORT")
fixture, output = (pathlib.Path(value).resolve() for value in arguments)
bpy.ops.wm.open_mainfile(filepath=str(fixture), load_ui=False)
before = state()
validate(before["receipt"])
temporary = fixture.with_name(f"{fixture.stem}-reopen.blend")
try:
bpy.ops.wm.save_as_mainfile(filepath=str(temporary), check_existing=False, compress=True)
bpy.ops.wm.open_mainfile(filepath=str(temporary), load_ui=False)
after = state()
finally:
temporary.unlink(missing_ok=True)
if before != after:
raise RuntimeError(f"release gate save/reopen drift: {before} != {after}")
report = {"schemaVersion": 1, "task": "WBV2-P7-003", "operation": "FULL_PARITY_CANDIDATE_RELEASE_GATE_DESKTOP", "fixtureSha256": hashlib.sha256(fixture.read_bytes()).hexdigest(), "state": after, "saveReopen": "EXACT", "blenderVersion": bpy.app.version_string}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print("wbv2-p7-003-desktop-ok parity=blocked evidence=blocked recovery=ready decision=blocked saveReopen=exact")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,96 @@
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 write = process.argv.includes("--write");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const same = (left, right, label) => assert.deepEqual(left, right, label);
const outputRoot = "tests/golden/WBV2-P8-001";
const currentDependencyPath = `${outputRoot}/dependency-inventory.current.json`;
const reportPath = `${outputRoot}/recovery-baseline-report.json`;
const statusPath = "docs/status/WBV2-P8-001.md";
const planPath = `${outputRoot}/recovery-task-plan.json`;
const manifestPath = `${outputRoot}/manifest.json`;
const parentManifest = readJson("tests/golden/WBV2-P8-000/manifest.json");
assert.equal(parentManifest.task, "WBV2-P8-000");
assert.equal(parentManifest.nextTask, "WBV2-P8-001");
for (const artifact of Object.values(parentManifest.artifacts ?? {})) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
const sourceHashes = readJson("tests/golden/WBV2-P0-002/source-hashes.json");
const wbv2Drift = Object.entries(sourceHashes.sources).map(([file, expected]) => ({ file, expected, actual: sha256(file) })).filter((entry) => entry.expected !== entry.actual);
assert.deepEqual(wbv2Drift.map((entry) => entry.file), ["docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json"]);
const recoveryPlan = readJson("tests/golden/WBV2-P8-000/recovery-task-plan.json");
assert.equal(recoveryPlan.parentPlan.sha256, sha256("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json"));
for (const task of recoveryPlan.tasks.filter((task) => task.id.startsWith("WBV2-P") && !task.id.startsWith("WBV2-P8-"))) {
const manifest = readJson(`tests/golden/${task.id}/manifest.json`);
assert.equal(manifest.task, task.id);
assert.equal(task.status, manifest.status === "blocked" ? "blocked" : "done", `${task.id} recovery status drift`);
}
fs.mkdirSync(absolute(outputRoot), { recursive: true });
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "wbv2-dependency-current-"));
const generatedDependency = path.join(temporary, "dependency-inventory.json");
try {
execFileSync(process.execPath, [absolute("tools/web/generate-dependency-inventory.mjs"), generatedDependency], { cwd: root, stdio: "pipe" });
const historical = readJson("tests/golden/M13-05C/dependency-inventory.json");
const current = JSON.parse(fs.readFileSync(generatedDependency, "utf8"));
same(current.roots, historical.roots, "dependency roots drifted");
same(current.categoryCounts, historical.categoryCounts, "dependency category counts drifted");
same(current.sharedCount, historical.sharedCount, "dependency sharing drifted");
same(current.packages, historical.packages, "dependency closure drifted");
assert.equal(current.lockfile.sha256, historical.lockfile.sha256, "package-lock drifted");
fs.copyFileSync(generatedDependency, absolute(currentDependencyPath));
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
const currentDependency = readJson(currentDependencyPath);
const historicalDependency = readJson("tests/golden/M13-05C/dependency-inventory.json");
const report = {
schemaVersion: 1,
task: "WBV2-P8-001",
operation: "REBASELINE_MUTABLE_INVENTORIES",
status: "PASS",
wbv2: { historicalInventory: "tests/golden/WBV2-P0-002/manifest.json", expectedMutableDriftOnly: true, drift: wbv2Drift },
dependency: { historicalInventory: "tests/golden/M13-05C/manifest.json", packageHashChanged: currentDependency.package.sha256 !== historicalDependency.package.sha256, lockfileHashStable: currentDependency.lockfile.sha256 === historicalDependency.lockfile.sha256, rootsStable: true, closureStable: true, packageCount: currentDependency.packages.length },
invariants: { historicalManifestsRewritten: false, capabilityClaimsAdded: 0, dependencyPackagesAdded: 0 },
nextTask: "WBV2-P8-002",
};
if (!write) {
process.stdout.write(`wbv2-recovery-baseline-ok wbv2Drift=${wbv2Drift.length} dependencyPackages=${currentDependency.packages.length} closureStable=1 next=WBV2-P8-002\n`);
process.exit(0);
}
fs.writeFileSync(absolute(reportPath), `${JSON.stringify(report, null, 2)}\n`);
const status = `# WBV2-P8-001 status\n\n- task: \`WBV2-P8-001\`; parent: \`WBV2-P8-000\`; status: \`done\`\n- nextTask: \`WBV2-P8-002\`\n\nThe WBV2 drift is limited to the mutable execution plan. The npm dependency graph and lockfile are unchanged; only package scripts changed. Historical P0 and M13 manifests remain untouched.\n`;
fs.mkdirSync(absolute("docs/status"), { recursive: true });
fs.writeFileSync(absolute(statusPath), status);
const nextPlan = structuredClone(recoveryPlan);
nextPlan.parentRecoveryPlan = { path: "tests/golden/WBV2-P8-000/recovery-task-plan.json", sha256: sha256("tests/golden/WBV2-P8-000/recovery-task-plan.json") };
nextPlan.tasks = nextPlan.tasks.map((task) => task.id === "WBV2-P8-001" ? { ...task, status: "done", ready: false } : task.id === "WBV2-P8-002" ? { ...task, status: "in_progress", ready: true } : { ...task, ready: false });
fs.writeFileSync(absolute(planPath), `${JSON.stringify(nextPlan, null, 2)}\n`);
execFileSync(process.execPath, [absolute("tools/web/check-task-context.mjs"), "--task", "WBV2-P8-001", "--write"], { cwd: root, stdio: "pipe" });
const checkerPath = "tools/web/check-wbv2-recovery-baseline.mjs";
const artifactPaths = { parentManifest: "tests/golden/WBV2-P8-000/manifest.json", historicalWbv2Manifest: "tests/golden/WBV2-P0-002/manifest.json", historicalDependencyManifest: "tests/golden/M13-05C/manifest.json", sourceHashes: "tests/golden/WBV2-P0-002/source-hashes.json", sourcePlan: "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json", parentRecoveryPlan: "tests/golden/WBV2-P8-000/recovery-task-plan.json", recoveryPlan: planPath, dependencyInventory: currentDependencyPath, checker: checkerPath, package: "web/package.json", report: reportPath, status: statusPath, taskContext: `${outputRoot}/task-context.json` };
const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: file, sha256: sha256(file) }]));
const manifest = { schemaVersion: 1, task: "WBV2-P8-001", parentTask: "WBV2-P8-000", operation: "REBASELINE_MUTABLE_INVENTORIES", status: "done", enablingTask: true, parityStateChange: false, artifacts, nextTask: "WBV2-P8-002" };
fs.writeFileSync(absolute(manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
const nextCard = `# WBV2-P8-002: close atomic Chromium receipts\n\n- task: WBV2-P8-002\n- parent: WBV2-P8-001\n- status: in_progress\n- gap: workflow:close_atomic_chromium_receipts\n- ownerFamily: PARITY\n- targetImplementationClass: LOCAL_BOUNDED\n\n## 目标\n\nProduce manifest-bound Chromium receipts for all 45 family behaviors by reusing verified WBV2 desktop/WASM/save-reopen evidence and running the shared production protocol in Chromium.\n\n## 输入与范围\n\n- Queue: \`tests/golden/WBV2-P7-002/atomic-queue-desktop-report.json\`\n- Existing receipts: \`tests/golden/WBV2-P7-003/atomic-chromium-receipt-index.json\`\n- Chromium runner: \`web/tests/e2e/wbv2-atomic-recovery.spec.ts\`\n\n## 验收\n\nnpm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-atomic-recovery.spec.ts\nnode tools/web/generate-wbv2-recovery-receipts.mjs --write\nnode tools/web/check-task-context.mjs --task WBV2-P8-002 --write\n\nMissing Chromium output, unsupported family mapping, source hash drift, or non-exact save/reopen must keep in_progress and must not advance nextTask.\n\n## 交付与回滚\n\n- Reports: \`tests/golden/WBV2-P8-002/\`\n- Status: \`docs/status/WBV2-P8-002.md\`\n- Manifest: \`tests/golden/WBV2-P8-002/manifest.json\`\n- Rollback: restore P8-001 as queue parent without modifying earlier evidence.\n`;
fs.writeFileSync(absolute("docs/tasks/WBV2-P8-002.md"), nextCard);
execFileSync(process.execPath, [absolute("tools/web/generate-v2-task-index.mjs"), "--plan-path", planPath], { cwd: root, stdio: "inherit" });
let queue = fs.readFileSync(absolute("docs/EXECUTION_QUEUE.md"), "utf8");
queue = queue.replace(/\| 当前任务 \| `[^`]+` \|/u, "| 当前任务 | `WBV2-P8-002` |")
.replace(/\| parent manifest \| `[^`]+` \|/u, "| parent manifest | `tests/golden/WBV2-P8-001/manifest.json` |")
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, "| 任务卡 | [`tasks/WBV2-P8-002.md`](tasks/WBV2-P8-002.md) |");
fs.writeFileSync(absolute("docs/EXECUTION_QUEUE.md"), queue);
process.stdout.write(`wbv2-recovery-baseline-written task=WBV2-P8-001 artifacts=${Object.keys(artifacts).length} next=WBV2-P8-002\n`);

View File

@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
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 write = process.argv.includes("--write");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const outputRoot = "tests/golden/WBV2-P8-004";
const reportPath = `${outputRoot}/recovery-release-gate-report.json`;
const statusPath = "docs/status/WBV2-P8-004.md";
const planPath = `${outputRoot}/recovery-task-plan.json`;
const manifestPath = `${outputRoot}/manifest.json`;
const receiptIndexPath = "tests/golden/WBV2-P8-002/atomic-receipt-index.json";
const epochPath = "tests/golden/WBV2-P8-003/m12-provenance-epoch.json";
const p7GatePath = "tests/golden/WBV2-P7-003/release-gate-report.json";
const parent = readJson("tests/golden/WBV2-P8-003/manifest.json");
assert.equal(parent.task, "WBV2-P8-003"); assert.equal(parent.status, "done"); assert.equal(parent.nextTask, "WBV2-P8-004");
for (const artifact of Object.values(parent.artifacts ?? {})) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
const receipts = readJson(receiptIndexPath); const epoch = readJson(epochPath); const oldGate = readJson(p7GatePath);
assert.equal(receipts.summary.total, 45); assert.equal(receipts.summary.manifestBound, 45); assert.equal(receipts.summary.remaining, 0); assert.equal(receipts.browser, "chromium"); assert.equal(receipts.queueMutation, false);
assert.equal(epoch.policy.restoredOriginals, 22); assert.equal(epoch.policy.authorizedReplacements, 62); assert.equal(epoch.policy.replacementsAreNotHistoricalRestorations, true); assert.equal(epoch.entries.length, 84);
for (const entry of epoch.entries) {
if (entry.replacement) assert.equal(entry.sourceSha256, sha256(entry.sourcePath ?? entry.path), `${entry.task}:${entry.artifact}`);
else {
assert.ok(entry.snapshotPath && fs.existsSync(absolute(entry.snapshotPath)), `${entry.task}:${entry.artifact} snapshot missing`);
assert.equal(sha256(entry.snapshotPath), entry.originalSha256, `${entry.task}:${entry.artifact}`);
assert.equal(entry.sourceSha256, entry.originalSha256, `${entry.task}:${entry.artifact}`);
}
}
assert.equal(oldGate.task, "WBV2-P7-003"); assert.equal(oldGate.desktop.decision, "BLOCKED");
const decision = { status: "BLOCKED", code: "HISTORICAL_PROVENANCE_REBASELINE_REQUIRED", reason: "62 M12 entries use an authorized current-worktree replacement; original historical bytes are unavailable", featureParityClaims: 0, nextTask: null };
const report = { schemaVersion: 1, task: "WBV2-P8-004", operation: "RECOMPUTE_RECOVERY_RELEASE_GATE", status: "PASS", inputs: { atomicReceipts: { path: receiptIndexPath, sha256: sha256(receiptIndexPath), total: receipts.summary.total, manifestBound: receipts.summary.manifestBound }, provenanceEpoch: { path: epochPath, sha256: sha256(epochPath), entries: epoch.entries.length, restoredOriginals: epoch.policy.restoredOriginals, authorizedReplacements: epoch.policy.authorizedReplacements }, previousGate: { path: p7GatePath, sha256: sha256(p7GatePath), decision: oldGate.desktop.decision } }, closure: { atomicReceiptStatus: "CLOSED", chromiumFamilyCount: receipts.summary.families, atomicBehaviorCount: receipts.summary.total, provenanceStatus: "AUTHORIZED_REBASELINE", historicalOriginalStatus: "PARTIAL" }, decision, negative: { missingReceipt: "REJECTED", receiptHashDrift: "REJECTED", provenanceHashDrift: "REJECTED", falseHistoricalRestoration: "REJECTED", unsupportedBrowser: "REJECTED", queueAdvance: "BLOCKED" }, nextTask: null };
if (!write) { process.stdout.write(`wbv2-recovery-release-gate-ok receipts=45 provenance=22-restored+62-rebaseline decision=BLOCKED next=NONE\n`); process.exit(0); }
fs.mkdirSync(absolute(outputRoot), { recursive: true }); fs.writeFileSync(absolute(reportPath), `${JSON.stringify(report, null, 2)}\n`);
const status = `# WBV2-P8-004 status\n\n- task: \`WBV2-P8-004\`; parent: \`WBV2-P8-003\`; status: \`blocked\`\n- atomic Chromium receipts: \`45/45\`\n- M12 provenance: \`22 restored + 62 authorized replacements\`\n- release decision: \`BLOCKED\`\n- nextTask: \`NONE\`\n\nThe recovery gate is internally consistent and fail-closed. The 62 authorized replacements are current-worktree baselines, not historical restoration, so full release parity cannot be claimed until authoritative M12 source bytes are supplied or a separately approved provenance epoch supersedes that requirement.\n`;
fs.mkdirSync(absolute("docs/status"), { recursive: true }); fs.writeFileSync(absolute(statusPath), status);
const previousPlan = readJson("tests/golden/WBV2-P8-003/recovery-task-plan.json"); const finalPlan = structuredClone(previousPlan); finalPlan.parentRecoveryPlan = { path: "tests/golden/WBV2-P8-003/recovery-task-plan.json", sha256: sha256("tests/golden/WBV2-P8-003/recovery-task-plan.json") }; finalPlan.tasks = finalPlan.tasks.map((task) => task.id === "WBV2-P8-004" ? { ...task, status: "in_progress", ready: true } : { ...task, ready: false }); fs.writeFileSync(absolute(planPath), `${JSON.stringify(finalPlan, null, 2)}\n`);
execFileSync(process.execPath, [absolute("tools/web/check-task-context.mjs"), "--task", "WBV2-P8-004", "--write"], { cwd: root, stdio: "pipe" });
const artifactPaths = { parentManifest: "tests/golden/WBV2-P8-003/manifest.json", receipts: receiptIndexPath, provenanceEpoch: epochPath, previousGate: p7GatePath, generator: "tools/web/generate-m12-provenance-epoch.mjs", checker: "tools/web/check-wbv2-recovery-release-gate.mjs", report: reportPath, status: statusPath, recoveryPlan: planPath, taskContext: `${outputRoot}/task-context.json` }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: file, sha256: sha256(file) }]));
const manifest = { schemaVersion: 1, task: "WBV2-P8-004", parentTask: "WBV2-P8-003", operation: "RECOMPUTE_RECOVERY_RELEASE_GATE", status: "blocked", enablingTask: true, parityStateChange: false, artifacts, assertions: { atomicReceipts: 45, restoredOriginals: 22, authorizedReplacements: 62, historicalProvenanceClosed: false, featureParityClaims: 0 }, nextTask: null }; fs.writeFileSync(absolute(manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
process.stdout.write(`wbv2-recovery-release-gate-written task=WBV2-P8-004 receipts=45 restored=22 replacements=62 decision=BLOCKED next=NONE\n`);

View File

@@ -0,0 +1,25 @@
# Minimal relocatable OpenColorIO package for native Blender builds.
# The distro package on some systems ships an unusable imported-target prefix.
find_path(OpenColorIO_INCLUDE_DIR
NAMES OpenColorIO/OpenColorIO.h
)
find_library(OpenColorIO_LIBRARY
NAMES OpenColorIO
)
if(OpenColorIO_INCLUDE_DIR AND OpenColorIO_LIBRARY)
if(NOT TARGET OpenColorIO::OpenColorIO)
add_library(OpenColorIO::OpenColorIO SHARED IMPORTED)
set_target_properties(OpenColorIO::OpenColorIO PROPERTIES
IMPORTED_LOCATION "${OpenColorIO_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${OpenColorIO_INCLUDE_DIR}"
)
endif()
set(OpenColorIO_FOUND TRUE)
set(OpenColorIO_VERSION "2.5.0")
else()
set(OpenColorIO_FOUND FALSE)
endif()
mark_as_advanced(OpenColorIO_INCLUDE_DIR OpenColorIO_LIBRARY)

View File

@@ -0,0 +1,7 @@
set(PACKAGE_VERSION "2.5.0")
if(PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION)
set(PACKAGE_VERSION_COMPATIBLE TRUE)
else()
set(PACKAGE_VERSION_COMPATIBLE FALSE)
endif()
set(PACKAGE_VERSION_EXACT (PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION))

View File

@@ -0,0 +1,9 @@
prefix=${pcfiledir}/../../../../.deps/native/shaderc
libdir=${prefix}/lib
includedir=${prefix}/include
Name: shaderc
Description: Shaderc compiler used by Blender Vulkan
Version: 2025.4
Libs: -L${libdir} -lshaderc_combined -lSPIRV-Tools-opt -lSPIRV-Tools
Cflags: -I${includedir}

View File

@@ -0,0 +1,10 @@
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib/x86_64-linux-gnu
includedir=${pcfiledir}/../../../../.deps/native/vulkan_headers/include
Name: Vulkan
Description: Vulkan headers and loader for the Blender native build
Version: 1.4.341
Libs: -L${libdir} -lvulkan
Cflags: -I${includedir}

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${repo_root}/tools/web/native-build-env.sh"
export PKG_CONFIG_PATH="${repo_root}/tools/web/cmake/pkgconfig:${PKG_CONFIG_PATH:-}"
: "${CC:=gcc-14}"
: "${CXX:=g++-14}"
command -v "${CC}" >/dev/null || { echo "missing ${CC}; install GCC 14 or set CC" >&2; exit 1; }
command -v "${CXX}" >/dev/null || { echo "missing ${CXX}; install G++ 14 or set CXX" >&2; exit 1; }
command -v cmake >/dev/null || { echo "missing cmake" >&2; exit 1; }
command -v ninja >/dev/null || { echo "missing ninja" >&2; exit 1; }
runtime_rpath="${BLENDER_PYTHON_ROOT}/lib"
for dependency in openvdb openimageio opencolorio openexr imath openjph tbb zstd fmt pugixml; do
if [[ -d "${BLENDER_DEPS_ROOT}/${dependency}/lib" ]]; then
runtime_rpath+=";${BLENDER_DEPS_ROOT}/${dependency}/lib"
fi
done
cmake_args=(
-S "${BLENDER_SOURCE_DIR}"
-B "${BLENDER_BUILD_DIR}"
-G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER="$(command -v "${CC}")"
-DCMAKE_CXX_COMPILER="$(command -v "${CXX}")"
-DCMAKE_BUILD_RPATH="${runtime_rpath}"
-DCMAKE_INSTALL_RPATH="${runtime_rpath}"
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON
-DWITH_LIBS_PRECOMPILED=OFF
-DCMAKE_PREFIX_PATH="${BLENDER_DEPS_ROOT}/openimageio;${BLENDER_DEPS_ROOT}/opencolorio;${BLENDER_DEPS_ROOT}/openexr;${BLENDER_DEPS_ROOT}/imath;${BLENDER_DEPS_ROOT}/openjph;${BLENDER_DEPS_ROOT}/fmt;${BLENDER_DEPS_ROOT}/tbb;${BLENDER_DEPS_ROOT}/zstd;${BLENDER_DEPS_ROOT}/pugixml;${BLENDER_DEPS_ROOT}/openvdb"
-DWITH_INSTALL_PORTABLE=ON
-DWITH_PYTHON=ON
-DWITH_PYTHON_NUMPY=ON
-DWITH_PYTHON_INSTALL_NUMPY=ON
-DWITH_PYTHON_INSTALL_REQUESTS=ON
-DWITH_PYTHON_INSTALL_ZSTANDARD=ON
-DWITH_SYSTEM_GFLAGS=ON
-DWITH_SYSTEM_GLOG=ON
-DWITH_CODEC_FFMPEG=ON
-DOpenColorIO_DIR="${BLENDER_DEPS_ROOT}/opencolorio/lib/cmake/OpenColorIO"
-DOpenImageIO_DIR="${BLENDER_DEPS_ROOT}/openimageio/lib/cmake/OpenImageIO"
-DOpenEXR_DIR="${BLENDER_DEPS_ROOT}/openexr/lib/cmake/OpenEXR"
-Dfmt_DIR="${BLENDER_DEPS_ROOT}/fmt/lib/cmake/fmt"
-DVULKAN_INCLUDE_DIRS="${BLENDER_DEPS_ROOT}/vulkan_headers/include"
-DSHADERC_ROOT_DIR="${BLENDER_DEPS_ROOT}/shaderc"
-DSHADERC_INCLUDE_DIRS:PATH="${BLENDER_DEPS_ROOT}/shaderc/include"
-DSHADERC_LIBRARIES:STRING="${BLENDER_DEPS_ROOT}/shaderc/lib/libshaderc_combined.a;${BLENDER_DEPS_ROOT}/shaderc/lib/libSPIRV-Tools-opt.a;${BLENDER_DEPS_ROOT}/shaderc/lib/libSPIRV-Tools.a"
)
# Python 3.13 is required by the Blender 5.2 source tree. A source-built
# interpreter can be supplied through BLENDER_PYTHON_ROOT; otherwise CMake
# reports the missing development files without silently selecting Python 3.12.
if [[ -x "${BLENDER_PYTHON_ROOT}/bin/python3.13" && -f "${BLENDER_PYTHON_ROOT}/lib/libpython3.13.so" ]]; then
cmake_args+=(
-DPYTHON_VERSION=3.13
-DPYTHON_EXECUTABLE="${BLENDER_PYTHON_ROOT}/bin/python3.13"
-DPYTHON_LIBRARY="${BLENDER_PYTHON_ROOT}/lib/libpython3.13.so"
-DPYTHON_LIBPATH="${BLENDER_PYTHON_ROOT}/lib"
-DPYTHON_INCLUDE_DIR="${BLENDER_PYTHON_ROOT}/include/python3.13"
-DPYTHON_INCLUDE_CONFIG_DIR="${BLENDER_PYTHON_ROOT}/include/python3.13"
)
else
echo "BLENDER_PYTHON_ROOT must contain a source-built Python 3.13 installation:" >&2
echo " ${BLENDER_PYTHON_ROOT}/bin/python3.13" >&2
echo " ${BLENDER_PYTHON_ROOT}/lib/libpython3.13.so" >&2
exit 2
fi
cmake "${cmake_args[@]}" "$@"
printf 'native-configured source=%s build=%s compiler=%s\n' \
"${BLENDER_SOURCE_DIR}" "${BLENDER_BUILD_DIR}" "${CXX}"

View File

@@ -5,7 +5,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${repo_root}/tools/web/emscripten-env.sh"
build_dir="${repo_root}/build_web"
emcmake cmake -S "${repo_root}/web/engine" -B "${build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release -DWEB_ENGINE_THREADS=OFF
emcmake cmake --fresh -S "${repo_root}/web/engine" -B "${build_dir}" -G Ninja -DCMAKE_BUILD_TYPE=Release -DWEB_ENGINE_THREADS=OFF
cmake --build "${build_dir}" --target web_engine
test -s "${build_dir}/web_engine.js"
printf 'web-cmake-ok build=%s\n' "${build_dir}"

View File

@@ -4,6 +4,18 @@ set -euo pipefail
WEB_EMSCRIPTEN_VERSION="3.1.69"
web_workspace_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Prefer the repository-local SDK prepared by tools/web/prepare-build-env.sh.
if [[ -x "${web_workspace_root}/.emsdk/upstream/emscripten/emcc" ]]; then
export EMSDK="${web_workspace_root}/.emsdk"
export PATH="${EMSDK}/upstream/emscripten:${EMSDK}/node/24.19.0_64bit/bin:${EMSDK}:${PATH}"
# Override copied-host settings in .emscripten-web with this checkout's SDK.
export EM_LLVM_ROOT="${EM_LLVM_ROOT-${EMSDK}/upstream/bin}"
export EM_BINARYEN_ROOT="${EM_BINARYEN_ROOT-${EMSDK}/upstream}"
export EM_NODE_JS="${EM_NODE_JS-${EMSDK}/node/24.19.0_64bit/bin/node}"
export EM_LLVM_ADD_VERSION="${EM_LLVM_ADD_VERSION-}"
export EM_CLANG_ADD_VERSION="${EM_CLANG_ADD_VERSION-}"
fi
if command -v emcc >/dev/null 2>&1; then
detected_version="$(emcc --version | awk '{for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+$/) {print $i; exit}}')"
if [[ "${detected_version}" != "${WEB_EMSCRIPTEN_VERSION}" ]]; then

View File

@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
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 write = process.argv.includes("--write");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const outputRoot = "tests/golden/WBV2-P8-003";
const epochPath = `${outputRoot}/m12-provenance-epoch.json`;
const reportPath = `${outputRoot}/provenance-epoch-report.json`;
const statusPath = "docs/status/WBV2-P8-003.md";
const planPath = `${outputRoot}/recovery-task-plan.json`;
const manifestPath = `${outputRoot}/manifest.json`;
const driftPath = "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json";
const intakePath = "tests/golden/corrective/rehydration/M12-provenance-intake.json";
const snapshotPath = "tests/golden/corrective/rehydration/M12-git-history-snapshot/snapshot-manifest.json";
const parent = readJson("tests/golden/WBV2-P8-002/manifest.json");
assert.equal(parent.task, "WBV2-P8-002"); assert.equal(parent.status, "done"); assert.equal(parent.nextTask, "WBV2-P8-003");
for (const artifact of Object.values(parent.artifacts ?? {})) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
const drift = readJson(driftPath); const intake = readJson(intakePath); const snapshot = readJson(snapshotPath);
assert.equal(drift.entries.length, 84); assert.equal(intake.driftEntries, 84); assert.equal(intake.restoredEntries, 22); assert.equal(intake.remainingEntries, 62); assert.equal(intake.immutableSourceSnapshotAvailable, true);
assert.equal(snapshot.schemaVersion, 1); assert.equal(snapshot.mode, "IMMUTABLE_REPOSITORY");
const restored = new Set(intake.entries.filter((entry) => entry.provenanceState === "PROVENANCE_RESTORED").map((entry) => `${entry.task}:${entry.artifact}:${entry.path}:${entry.originalSha256}`));
const entries = intake.entries.map((entry) => {
const key = `${entry.task}:${entry.artifact}:${entry.path}:${entry.originalSha256}`;
const actual = sha256(entry.path);
if (entry.provenanceState === "PROVENANCE_RESTORED") {
assert.equal(actual, entry.observedSha256, entry.path);
return { ...entry, epoch: 1, binding: "IMMUTABLE_SOURCE_SNAPSHOT", sourceSha256: entry.originalSha256, replacement: false, snapshotPath: entry.snapshotPath };
}
assert.equal(fs.existsSync(absolute(entry.path)), true, entry.path);
return { ...entry, epoch: 1, provenanceState: "AUTHORIZED_REBASELINE", binding: "CURRENT_WORKTREE_REPLACEMENT", sourceSha256: actual, replacement: true, sourcePath: entry.path, originalPreserved: entry.originalSha256 };
});
assert.equal(entries.filter((entry) => entry.provenanceState === "PROVENANCE_RESTORED").length, 22);
assert.equal(entries.filter((entry) => entry.provenanceState === "AUTHORIZED_REBASELINE").length, 62);
const epoch = { schemaVersion: 1, epoch: 1, task: "WBV2-P8-003", operation: "M12_AUTHORIZED_PROVENANCE_REBASELINE", historicalEvidencePreserved: true, sourceSnapshot: snapshotPath, sourceSnapshotSha256: sha256(snapshotPath), originalDriftReport: driftPath, originalDriftReportSha256: sha256(driftPath), originalIntake: intakePath, originalIntakeSha256: sha256(intakePath), policy: { restoredOriginals: 22, authorizedReplacements: 62, replacementsAreNotHistoricalRestorations: true, historicalManifestsRewritten: false }, entries };
const report = { schemaVersion: 1, task: "WBV2-P8-003", operation: "AUTHORIZE_M12_PROVENANCE_EPOCH", status: "PASS", epoch: 1, counts: { total: entries.length, restoredOriginals: 22, authorizedReplacements: 62, unresolvedHistoricalOriginals: 62 }, sourceSnapshot: { path: snapshotPath, sha256: sha256(snapshotPath), mode: snapshot.mode }, invariants: { historicalEvidencePreserved: true, falseRestoredClaims: 0, historicalManifestsRewritten: false, allCurrentBindingsHashVerified: entries.every((entry) => entry.sourceSha256 === sha256(entry.sourcePath ?? entry.path)) }, nextTask: "WBV2-P8-004" };
if (!write) { process.stdout.write(`m12-provenance-epoch-ok restored=22 replacements=62 total=84 historical=preserved next=WBV2-P8-004\n`); process.exit(0); }
fs.mkdirSync(absolute(outputRoot), { recursive: true }); fs.writeFileSync(absolute(epochPath), `${JSON.stringify(epoch, null, 2)}\n`); fs.writeFileSync(absolute(reportPath), `${JSON.stringify(report, null, 2)}\n`);
const status = `# WBV2-P8-003 status\n\n- task: \`WBV2-P8-003\`; parent: \`WBV2-P8-002\`; status: \`done\`\n- provenance epoch: \`1\`\n- historical originals restored: \`22/84\`\n- authorized replacements: \`62/84\`\n- nextTask: \`WBV2-P8-004\`\n\nThe 22 exact Git-history matches remain immutable restorations. The other 62 entries are explicitly authorized current-worktree replacements; they are not represented as recovery of the unavailable historical bytes.\n`;
fs.mkdirSync(absolute("docs/status"), { recursive: true }); fs.writeFileSync(absolute(statusPath), status);
const previousPlan = readJson("tests/golden/WBV2-P8-002/recovery-task-plan.json"); const nextPlan = structuredClone(previousPlan); nextPlan.parentRecoveryPlan = { path: "tests/golden/WBV2-P8-002/recovery-task-plan.json", sha256: sha256("tests/golden/WBV2-P8-002/recovery-task-plan.json") }; nextPlan.tasks = nextPlan.tasks.map((task) => task.id === "WBV2-P8-003" ? { ...task, status: "done", ready: false } : task.id === "WBV2-P8-004" ? { ...task, status: "in_progress", ready: true } : { ...task, ready: false }); fs.writeFileSync(absolute(planPath), `${JSON.stringify(nextPlan, null, 2)}\n`);
execFileSync(process.execPath, [absolute("tools/web/check-task-context.mjs"), "--task", "WBV2-P8-003", "--write"], { cwd: root, stdio: "pipe" });
const artifactPaths = { parentManifest: "tests/golden/WBV2-P8-002/manifest.json", drift: driftPath, intake: intakePath, snapshot: snapshotPath, epoch: epochPath, report: reportPath, generator: "tools/web/generate-m12-provenance-epoch.mjs", status: statusPath, recoveryPlan: planPath, taskContext: `${outputRoot}/task-context.json` }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: file, sha256: sha256(file) }]));
const manifest = { schemaVersion: 1, task: "WBV2-P8-003", parentTask: "WBV2-P8-002", operation: "AUTHORIZE_M12_PROVENANCE_EPOCH", status: "done", enablingTask: true, parityStateChange: false, artifacts, assertions: { total: 84, restoredOriginals: 22, authorizedReplacements: 62, falseRestoredClaims: 0, historicalManifestsRewritten: false }, nextTask: "WBV2-P8-004" }; fs.writeFileSync(absolute(manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
const nextCard = `# WBV2-P8-004: recompute recovery release gate\n\n- task: WBV2-P8-004\n- parent: WBV2-P8-003\n- status: in_progress\n- gap: workflow:recompute_recovery_release_gate\n- ownerFamily: RELEASE\n- targetImplementationClass: LOCAL_BOUNDED\n\n## 目标\n\nRecompute the final release decision from 45 atomic Chromium receipts and the explicit M12 provenance epoch.\n\n## 输入与范围\n\n- Atomic receipts: \`tests/golden/WBV2-P8-002/atomic-receipt-index.json\`\n- M12 epoch: \`tests/golden/WBV2-P8-003/m12-provenance-epoch.json\`\n- Release gate: \`tools/web/check-wbv2-recovery-release-gate.mjs\`\n\n## 验收\n\nnode tools/web/check-wbv2-recovery-release-gate.mjs --write\nnode tools/web/check-task-context.mjs --task WBV2-P8-004 --write\n\nMissing receipt, provenance ambiguity, hash drift, unsupported release claim, or governance failure must keep in_progress and must not advance nextTask.\n\n## 交付与回滚\n\n- Reports: \`tests/golden/WBV2-P8-004/\`\n- Status: \`docs/status/WBV2-P8-004.md\`\n- Manifest: \`tests/golden/WBV2-P8-004/manifest.json\`\n- Rollback: restore P8-003 as queue parent and preserve all historical evidence.\n`;
fs.writeFileSync(absolute("docs/tasks/WBV2-P8-004.md"), nextCard); execFileSync(process.execPath, [absolute("tools/web/generate-v2-task-index.mjs"), "--plan-path", planPath], { cwd: root, stdio: "inherit" });
let queue = fs.readFileSync(absolute("docs/EXECUTION_QUEUE.md"), "utf8"); queue = queue.replace(/\| 当前任务 \| `[^`]+` \|/u, "| 当前任务 | `WBV2-P8-004` |").replace(/\| parent manifest \| `[^`]+` \|/u, "| parent manifest | `tests/golden/WBV2-P8-003/manifest.json` |").replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, "| 任务卡 | [`tasks/WBV2-P8-004.md`](tasks/WBV2-P8-004.md) |"); fs.writeFileSync(absolute("docs/EXECUTION_QUEUE.md"), queue);
process.stdout.write(`m12-provenance-epoch-written task=WBV2-P8-003 restored=22 replacements=62 next=WBV2-P8-004\n`);

View File

@@ -4,7 +4,8 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const planPath = path.join(root, "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json");
const planArgument = process.argv.indexOf("--plan-path");
const planPath = path.resolve(root, planArgument >= 0 ? process.argv[planArgument + 1] : "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json");
const outputDir = path.join(root, "tests/golden/WBV2");
const catalogPath = path.join(outputDir, "task-catalog.jsonl");
const indexPath = path.join(outputDir, "task-index.json");

View File

@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
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 outputPath = path.join(root, "tests/golden/WBV2-P7-003/atomic-chromium-receipt-index.json");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const sources = [
{
family: "ASSET_CATALOG",
capabilityId: "CAP-ASSET-CATALOG-MUTATION",
sourceTask: "C3-003",
report: "tests/golden/corrective/C3-003/receipt-report.json",
secondReport: "tests/golden/corrective/C3-003/receipt-report.second.json",
manifest: "tests/golden/corrective/C3-003/manifest.json",
proof: { desktopPositive: "positive.desktop", wasmMutation: "positive.wasm", saveReopenExact: "persistence" },
},
{
family: "MODIFIER",
capabilityId: "CAP-MODIFIER-STATE-MUTATION",
sourceTask: "C4-001",
report: "tests/golden/corrective/C4-001/receipt-report.json",
secondReport: "tests/golden/corrective/C4-001/receipt-report.second.json",
manifest: "tests/golden/corrective/C4-001/manifest.json",
proof: { desktopPositive: "positive.desktop", wasmMutation: "positive.wasm", saveReopenExact: "persistence.reopened" },
},
{
family: "MESH",
capabilityId: "CAP-MESH-POSITIVE-EDIT",
sourceTask: "C4-002",
report: "tests/golden/corrective/C4-002/receipt-report.json",
secondReport: "tests/golden/corrective/C4-002/receipt-report.second.json",
manifest: "tests/golden/corrective/C4-002/manifest.json",
proof: { desktopPositive: "positive.desktop", wasmMutation: "positive.wasm", saveReopenExact: "persistence.reopened" },
},
];
const behaviors = ["desktopPositive", "wasmMutation", "saveReopenExact"];
const get = (value, selector) => selector.split(".").reduce((current, key) => current?.[key], value);
const entries = [];
for (const source of sources) {
const report = readJson(source.report);
const manifest = readJson(source.manifest);
assert.equal(manifest.status, "done", `${source.sourceTask} manifest is not done`);
assert.equal(report.browser, "chromium", `${source.sourceTask} is not Chromium evidence`);
assert.equal(report.queueMutation, false, `${source.sourceTask} mutated the queue`);
assert.equal(report.parity, true, `${source.sourceTask} did not prove parity`);
assert.equal(sha256(source.secondReport), sha256(source.report), `${source.sourceTask} report is not deterministic`);
const bound = Object.values(manifest.artifacts ?? {}).find((artifact) => artifact.path === source.report);
assert.ok(bound, `${source.sourceTask} report is not manifest-bound`);
assert.equal(bound.sha256, sha256(source.report), `${source.sourceTask} report hash drift`);
for (const behavior of behaviors) {
const proofPath = source.proof[behavior];
const proof = get(report, proofPath);
assert.ok(proof, `${source.sourceTask} missing ${behavior} proof`);
entries.push({
id: `WBV2-P7-003-${source.family}-${behavior.toUpperCase()}`,
family: source.family,
capabilityId: source.capabilityId,
behavior,
browser: "chromium",
status: "EXACT_REUSED_FROM_CORRECTIVE_CHROMIUM",
manifestBound: true,
queueMutation: false,
source: {
task: source.sourceTask,
manifest: source.manifest,
manifestSha256: sha256(source.manifest),
report: source.report,
reportSha256: sha256(source.report),
secondReport: source.secondReport,
secondReportSha256: sha256(source.secondReport),
proofPath,
},
proof,
});
}
}
const report = {
schemaVersion: 1,
task: "WBV2-P7-003",
operation: "ATOMIC_CHROMIUM_RECEIPT_INDEX",
browser: "chromium",
queueMutation: false,
policy: {
sourceReportsReadOnly: true,
exactReuseOnly: true,
featureParityClaims: 0,
canonicalOrder: behaviors,
},
summary: { total: entries.length, manifestBound: entries.filter((entry) => entry.manifestBound).length, families: [...new Set(entries.map((entry) => entry.family))] },
entries,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
process.stdout.write(`atomic-chromium-receipts-ok task=${report.task} receipts=${report.summary.total} families=${report.summary.families.length} queueMutation=false output=${relative(outputPath)}\n`);

View File

@@ -0,0 +1,175 @@
import assert from "node:assert/strict";
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 outputPath = path.join(root, "tests/golden/WBV2-P7-003/c5-family-evidence-report.json");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const sources = [
{
family: "MESH",
capabilityIds: ["CAP-MESH-POSITIVE-EDIT", "CAP-MESH-EDITING", "MESH.EDIT_CORE"],
task: "WBV2-P2-003",
report: "tests/golden/WBV2-P2-003/mesh-edit-report.json",
desktopReport: "tests/golden/WBV2-P2-003/mesh-edit-desktop-report.json",
protocol: "web/protocol/corrective-mesh-command.ts",
c5ReceiptId: "mesh-desktop",
c5MutationReceiptId: "mesh-wasm",
},
{
family: "MODIFIER",
capabilityIds: ["CAP-MODIFIER-STATE-MUTATION", "CAP-MODIFIER-SYSTEM", "MODIFIER.DECLARED_SLICES"],
task: "WBV2-P4-001",
report: "tests/golden/WBV2-P4-001/modifier-report.json",
desktopReport: "tests/golden/WBV2-P4-001/modifier-desktop-report.json",
protocol: "web/protocol/corrective-modifier-command.ts",
c5ReceiptId: "modifier-desktop",
c5MutationReceiptId: "modifier-wasm",
},
{
family: "ASSET_CATALOG",
capabilityIds: ["CAP-ASSET-CATALOG-MUTATION", "ASSET_LIBRARY_APPEND_LINK_OVERRIDE", "ASSET.LIBRARY_OPERATIONS"],
task: "WBV2-P3-005",
report: "tests/golden/WBV2-P3-005/library-report.json",
desktopReport: "tests/golden/WBV2-P3-005/library-desktop-report.json",
protocol: "web/protocol/corrective-asset-catalog-command.ts",
c5ReceiptId: "asset_catalog-desktop",
c5MutationReceiptId: "asset_catalog-wasm",
},
];
const c5ReportPath = "tests/golden/corrective/C5-002/chromium-family-report.json";
const c5ManifestPath = "tests/golden/corrective/C5-002/manifest.json";
const c5SecondReportPath = "tests/golden/corrective/C5-002/chromium-family-report.second.json";
const c5Manifest = readJson(c5ManifestPath);
const c5 = readJson(c5ReportPath);
assert.equal(c5Manifest.status, "done");
assert.equal(c5Manifest.artifacts?.find((artifact) => artifact.path === c5ReportPath)?.sha256, sha256(c5ReportPath));
assert.equal(sha256(c5SecondReportPath), sha256(c5ReportPath), "C5 family report is not deterministic");
assert.equal(c5.browser, "chromium");
assert.equal(c5.queueMutation, false);
function exactSource(source) {
const manifest = readJson(`tests/golden/${source.task}/manifest.json`);
const report = readJson(source.report);
const desktop = readJson(source.desktopReport);
assert.equal(manifest.status, "done", `${source.task} is not complete`);
assert.equal(report.schemaVersion, 1);
assert.equal(report.desktop?.status, "EXACT");
assert.equal(report.wasm?.status, "EXACT");
assert.equal(report.saveReopen ?? report.wasm?.saveReopen ?? report.desktop?.saveReopen, "EXACT");
assert.equal(desktop.saveReopen, "EXACT");
assert.ok(report.negative && Object.keys(report.negative).length > 0, `${source.task} has no negative evidence`);
const boundReport = manifest.artifacts?.report;
assert.ok(boundReport && boundReport.path === source.report);
assert.equal(sha256(source.report), boundReport.sha256, `${source.task} report hash drift`);
const boundDesktop = manifest.artifacts?.desktopReport;
assert.ok(boundDesktop && boundDesktop.path === source.desktopReport);
assert.equal(sha256(source.desktopReport), boundDesktop.sha256, `${source.task} desktop report hash drift`);
return {
sourceTask: source.task,
sourceManifest: {
path: `tests/golden/${source.task}/manifest.json`,
sha256: sha256(`tests/golden/${source.task}/manifest.json`),
},
fixture: report.fixture ?? null,
desktopPositive: {
status: report.desktop.status,
report: source.desktopReport,
reportSha256: sha256(source.desktopReport),
semanticOperation: report.operation,
},
wasmMutation: {
status: report.wasm.status,
report: source.report,
reportSha256: sha256(source.report),
revision: {
initial: report.wasm.initialRevision ?? report.wasm.revision ?? null,
final: report.wasm.finalRevision ?? report.wasm.mutatedRevision ?? null,
},
},
saveReopenExact: {
status: "EXACT",
desktop: report.desktop.saveReopen ?? "EXACT",
wasm: report.wasm.saveReopen ?? report.saveReopen ?? "EXACT",
},
negativeMatrix: report.negative,
};
}
const families = sources.map((source) => ({
family: source.family,
capabilityIds: source.capabilityIds,
protocol: {
path: source.protocol,
sha256: sha256(source.protocol),
},
c5Candidate: {
desktopReceiptId: source.c5ReceiptId,
wasmReceiptId: source.c5MutationReceiptId,
desktop: c5.results.find((result) => result.id === source.c5ReceiptId)?.receipt?.result ?? null,
wasm: c5.results.find((result) => result.id === source.c5MutationReceiptId)?.receipt?.result ?? null,
browser: c5.browser,
queueMutation: c5.queueMutation,
status: "PROTOCOL_CANDIDATE",
},
evidence: exactSource(source),
evidenceState: "EXACT_REUSED_FROM_WBV2",
featureParityEligible: false,
featureParityReason: "C5 Chromium receipt is protocol-only; production family interaction and atomic manifest are still required.",
}));
for (const family of families) {
assert.equal(family.evidence.desktopPositive.status, "EXACT");
assert.equal(family.evidence.wasmMutation.status, "EXACT");
assert.equal(family.evidence.saveReopenExact.status, "EXACT");
}
const report = {
schemaVersion: 1,
task: "WBV2-P7-003",
operation: "C5_FAMILY_EVIDENCE_INTEGRATION",
queueMutation: false,
browser: "chromium",
evidencePolicy: {
historicalC5ReadOnly: true,
sourceReportsReadOnly: true,
featureParityClaims: 0,
canonicalOrder: ["desktopPositive", "wasmMutation", "saveReopenExact"],
},
families,
meshProgression: {
family: "MESH",
order: ["desktopPositive", "wasmMutation", "saveReopenExact"],
allThreeExact: true,
sourceTask: "WBV2-P2-003",
nextAction: "manifest-backed atomic successor required before queue activation",
},
summary: {
familyCount: families.length,
exactReused: families.filter((family) => family.evidenceState === "EXACT_REUSED_FROM_WBV2").length,
featureParityEligible: families.filter((family) => family.featureParityEligible).length,
productionChromiumReady: 0,
},
sourceHashes: Object.fromEntries([
[c5ManifestPath, sha256(c5ManifestPath)],
[c5ReportPath, sha256(c5ReportPath)],
...sources.flatMap((source) => [
[`tests/golden/${source.task}/manifest.json`, sha256(`tests/golden/${source.task}/manifest.json`)],
[source.report, sha256(source.report)],
[source.desktopReport, sha256(source.desktopReport)],
[source.protocol, sha256(source.protocol)],
]),
]),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
process.stdout.write(`family-evidence-ok task=${report.task} families=${families.length} exactReused=${report.summary.exactReused} meshSequence=desktopPositive,wasmMutation,saveReopenExact queueMutation=false output=${relative(outputPath)}\n`);

View File

@@ -0,0 +1,146 @@
import assert from "node:assert/strict";
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 relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const filePath = (value) => path.join(root, value);
const readJson = (value) => JSON.parse(fs.readFileSync(filePath(value), "utf8"));
const sha256 = (value) => crypto.createHash("sha256").update(fs.readFileSync(filePath(value))).digest("hex");
const planPath = "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json";
const inventoryPath = "tests/golden/WBV2-P7-001/gap-inventory-report.json";
const atomicPath = "tests/golden/WBV2-P7-002/atomic-queue-report.json";
const gatePath = "tests/golden/WBV2-P7-003/release-gate-report.json";
const quickPath = "release/ci-reports/quick.json";
const driftPath = "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json";
const provenanceIntakePath = "tests/golden/corrective/rehydration/M12-provenance-intake.json";
const m12SnapshotPath = "tests/golden/corrective/rehydration/M12-git-history-snapshot/snapshot-manifest.json";
const c5Path = "tests/golden/corrective/C5-002/chromium-family-report.json";
const familyEvidencePath = "tests/golden/WBV2-P7-003/c5-family-evidence-report.json";
const atomicReceiptPath = "tests/golden/WBV2-P7-003/atomic-chromium-receipt-index.json";
const outputPath = process.argv.includes("--out")
? path.resolve(root, process.argv[process.argv.indexOf("--out") + 1])
: filePath("tests/golden/WBV2-P7-003/integration-readiness-report.json");
const plan = readJson(planPath);
const inventory = readJson(inventoryPath);
const atomic = readJson(atomicPath);
const gate = readJson(gatePath);
const quick = readJson(quickPath);
const drift = readJson(driftPath);
const provenanceIntake = readJson(provenanceIntakePath);
const c5 = readJson(c5Path);
const familyEvidence = readJson(familyEvidencePath);
const atomicReceipts = readJson(atomicReceiptPath);
assert.equal(plan.planId, "BLENDER_WEB_PARITY_V2");
assert.equal(inventory.task, "WBV2-P7-001");
assert.equal(atomic.task, "WBV2-P7-002");
assert.equal(gate.task, "WBV2-P7-003");
assert.equal(quick.lane, "quick");
assert.equal(c5.browser, "chromium");
assert.equal(c5.queueMutation, false);
assert.equal(familyEvidence.task, "WBV2-P7-003");
assert.equal(familyEvidence.queueMutation, false);
assert.equal(familyEvidence.families.length, 3);
assert.equal(atomicReceipts.browser, "chromium");
assert.equal(atomicReceipts.queueMutation, false);
assert.ok(Array.isArray(atomicReceipts.entries));
assert.equal(atomicReceipts.entries.every((entry) => entry.manifestBound === true), true);
assert.equal(provenanceIntake.historicalEvidencePreserved, true);
assert.equal(provenanceIntake.driftEntries, drift.entries.length);
const taskCounts = plan.tasks.reduce((groups, task) => {
(groups[task.status] ??= []).push(task);
return groups;
}, {});
const manifestRows = plan.tasks.map((task) => {
const manifest = readJson(`tests/golden/${task.id}/manifest.json`);
return { id: task.id, status: manifest.status, nextTask: manifest.nextTask ?? null };
});
assert.equal(manifestRows.length, plan.tasks.length);
assert.ok(manifestRows.every((row) => row.status === "done" || row.status === "blocked"));
const pilotFamilies = familyEvidence.families.map((family) => ({
family: family.family,
capabilityIds: family.capabilityIds,
evidenceState: family.evidenceState,
desktopPositive: family.evidence.desktopPositive.status,
wasmMutation: family.evidence.wasmMutation.status,
saveReopenExact: family.evidence.saveReopenExact.status,
candidateEvidence: family.c5Candidate.status,
featureParityEligible: family.featureParityEligible,
missing: ["production_chromium_family_receipt", "manifested_atomic_successor"],
sourceTask: family.evidence.sourceTask,
}));
const queueText = fs.readFileSync(filePath("docs/EXECUTION_QUEUE.md"), "utf8");
const queueTask = queueText.match(/\|\s*当前任务\s*\|\s*`([^`]+)`/u)?.[1] ?? null;
const parentManifest = queueText.match(/\|\s*parent manifest\s*\|\s*`([^`]+)`/iu)?.[1] ?? null;
assert.equal(queueTask, "WBV2-P7-003");
assert.equal(parentManifest, "tests/golden/WBV2-P7-002/manifest.json");
const report = {
schemaVersion: 1,
task: "WBV2-P7-003",
operation: "INTEGRATION_READINESS_CHECKPOINT",
queue: { currentTask: queueTask, parentManifest, queueMutation: false, nextTask: null },
completedWork: {
wbv2Tasks: { total: plan.tasks.length, done: (taskCounts.done ?? []).length, inProgress: (taskCounts.in_progress ?? []).length, blocked: (taskCounts.blocked ?? []).length },
manifestRows,
runtimeFoundation: ["SceneIR", "Main transaction/history", "Worker lifecycle", "OPFS/IndexedDB recovery"],
productSlices: ["open/context", "transform/hierarchy", "mesh edit", "viewport", "material/UV", "animation", "rig/skin", "GLB", "asset library", "modifier", "nodes/shader", "sculpt/paint", "simulation", "render", "media", "server", "security", "CI/SBOM/rollback"],
},
gapInventory: {
entries: inventory.desktop.entries,
families: inventory.desktop.clusters,
currentParityLevel: "L0",
featureParityClaims: 0,
},
atomicQueue: {
tasks: atomic.desktop.tasks,
active: atomic.desktop.active,
planned: atomic.desktop.planned,
currentTaskGate: gate.desktop.decision,
c5CandidateReceiptCount: familyEvidence.families.length * 3,
manifestBoundReceiptCount: atomicReceipts.entries.length,
remainingManifestBoundReceipts: atomic.desktop.tasks - atomicReceipts.entries.length,
atomicReceiptIndex: atomicReceiptPath,
},
reusablePilots: pilotFamilies,
immediateWork: {
firstFamily: "MESH",
firstCapabilityId: "CAP-MESH-POSITIVE-EDIT",
firstBehavior: "desktopPositive",
order: ["desktopPositive", "wasmMutation", "saveReopenExact"],
queueState: "PROPOSED_OVERLAY_NOT_QUEUED",
reason: "P7-003 has no successor; activate only after a manifest-backed queue successor is emitted.",
},
releaseInputs: {
quickCi: quick.status,
m12Provenance: drift.provenanceState,
m12Intake: provenanceIntake.provenanceState,
m12SnapshotAvailable: provenanceIntake.immutableSourceSnapshotAvailable,
m12Entries: drift.entries.length,
m12RestoredEntries: provenanceIntake.restoredEntries,
historicalEvidencePreserved: drift.historicalEvidencePreserved,
parityClosure: gate.desktop.parity,
releaseDecision: gate.desktop.decision,
},
blockers: [
{ code: "PARITY_CLOSURE_BLOCKED", detail: `${atomic.desktop.tasks} atomic behavior tasks are not yet backed by manifest-bound family receipts; ${atomicReceipts.entries.length} are reusable Chromium receipts and ${atomic.desktop.tasks - atomicReceipts.entries.length} remain.` },
{
code: "HISTORICAL_EVIDENCE_PROVENANCE_BLOCKED",
detail: provenanceIntake.immutableSourceSnapshotAvailable
? `M12 immutable snapshot matched ${provenanceIntake.restoredEntries}/${provenanceIntake.driftEntries} entries; ${provenanceIntake.remainingEntries} SHA drift entries remain fail-closed.`
: `M12 immutable snapshot is unavailable; ${provenanceIntake.remainingEntries} SHA drift entries remain fail-closed.`,
},
],
sourceHashes: Object.fromEntries([planPath, inventoryPath, atomicPath, gatePath, quickPath, driftPath, provenanceIntakePath, c5Path, familyEvidencePath, atomicReceiptPath, m12SnapshotPath].filter((file) => fs.existsSync(filePath(file))).map((file) => [relative(filePath(file)), sha256(file)])),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
process.stdout.write(`integration-readiness-ok task=${report.task} completed=${report.completedWork.wbv2Tasks.done} inventory=${report.gapInventory.entries} families=${report.gapInventory.families} atomic=${report.atomicQueue.tasks} first=${report.immediateWork.firstFamily} parity=${report.releaseInputs.parityClosure} decision=${report.releaseInputs.releaseDecision} output=${relative(outputPath)}\n`);

View File

@@ -0,0 +1,209 @@
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 defaultOutput = path.join(root, "tests/golden/WBV2-P0-002");
const outputDir = path.resolve(process.argv[2] ?? defaultOutput);
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const readJson = (relativePath) => {
const file = path.join(root, relativePath);
return { path: relativePath, file, bytes: fs.readFileSync(file), value: JSON.parse(fs.readFileSync(file, "utf8")) };
};
const sources = [
readJson("tests/golden/M15-02A/blender-parity-map.json"),
readJson("tests/golden/M15-03A/next-task-plan.json"),
readJson("tests/golden/M15-03A/completed-gap-tasks.json"),
readJson("docs/BLENDER_WASM_CAPABILITY_MATRIX_TEMPLATE.json"),
readJson("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json"),
readJson("tests/golden/WBV2-P0-001/manifest.json"),
];
const sourceHashes = Object.fromEntries(sources.map((source) => [source.path, sha256(source.bytes)]));
const sourceRefs = Object.fromEntries(sources.map((source) => [source.path, { path: source.path, sha256: sourceHashes[source.path] }]));
const parityMap = sources[0].value;
const legacyPlan = sources[1].value;
const capabilityMatrix = sources[3].value;
const taskByGapId = new Map(legacyPlan.tasks.map((task) => [task.gapId, task]));
const matrixByFamily = new Map((capabilityMatrix.entries ?? []).map((entry) => [entry.family, entry]));
const evidenceByTask = new Map((parityMap.sourceInventories ? Object.keys(parityMap.sourceInventories) : []).map((task) => [
task,
`tests/golden/${task}/manifest.json`,
]));
const coreSceneNames = new Set(["Scene", "Collection", "Object", "ViewLayer", "LayerCollection", "Window", "WindowManager", "Screen", "RenderSettings", "World", "Camera", "Light", "Material", "NodeTree"]);
const coreMeshNames = new Set(["Mesh", "Attribute", "AttributeGroupMesh", "BoolAttribute", "ByteColorAttribute", "FloatAttribute", "IntAttribute"]);
const familySpec = {
CORE_DNA_RNA: { layer: "CORE", targetParityLevel: "L2" },
CORE_MESH: { layer: "CORE", targetParityLevel: "L3" },
CORE_OBJECT: { layer: "CORE", targetParityLevel: "L3" },
CORE_SCENE_GRAPH: { layer: "CORE", targetParityLevel: "L3" },
MESH_EDITING: { layer: "FEATURE", targetParityLevel: "L3" },
OBJECT_OPERATIONS: { layer: "FEATURE", targetParityLevel: "L3" },
MATERIAL_RENDER: { layer: "FEATURE", targetParityLevel: "L3" },
MODIFIER_SYSTEM: { layer: "FEATURE", targetParityLevel: "L3" },
NODES_GRAPH: { layer: "FEATURE", targetParityLevel: "L3" },
RIG_ANIMATION: { layer: "FEATURE", targetParityLevel: "L3" },
SIMULATION: { layer: "FEATURE", targetParityLevel: "L2" },
SCULPT_PAINT: { layer: "FEATURE", targetParityLevel: "L2" },
MEDIA_IO: { layer: "FEATURE", targetParityLevel: "L3" },
UI_EDITOR: { layer: "UI", targetParityLevel: "L2" },
OTHER_OPERATOR: { layer: "FEATURE", targetParityLevel: "L1" },
UNKNOWN: { layer: "UNCLASSIFIED", targetParityLevel: "L0" },
};
function classify(entry) {
const id = entry.id;
const lower = id.toLowerCase();
const suffix = id.includes(":") ? id.slice(id.indexOf(":") + 1) : id;
const name = suffix.split(":").at(-1) ?? suffix;
if (id.startsWith("datablock:")) {
if (name === "Mesh") return "CORE_MESH";
if (name === "Object") return "CORE_OBJECT";
if (coreSceneNames.has(name)) return "CORE_SCENE_GRAPH";
return "CORE_DNA_RNA";
}
if (id.startsWith("rna:")) {
if (entry.ownerFamily === "Mesh" || coreMeshNames.has(name)) return "CORE_MESH";
if (name === "Object") return "CORE_OBJECT";
if (entry.ownerFamily === "Scene" || entry.ownerFamily === "Main" || entry.ownerFamily === "Depsgraph" || coreSceneNames.has(name)) return "CORE_SCENE_GRAPH";
return "CORE_DNA_RNA";
}
if (id.startsWith("modifier:")) return "MODIFIER_SYSTEM";
if (id.startsWith("constraint:")) return "RIG_ANIMATION";
if (id.startsWith("node:")) return lower.includes("shader") ? "MATERIAL_RENDER" : "NODES_GRAPH";
if (id.startsWith("physics:")) return "SIMULATION";
if (id.startsWith("strip:")) return "MEDIA_IO";
if (id.startsWith("io:")) return "MEDIA_IO";
if (/^(area|region|space|space-class|workspace|keymap|keymap-item):/u.test(id)) return "UI_EDITOR";
if (id.startsWith("operator:")) {
const module = id.slice("operator:".length).split(".")[0];
if (["node", "geometry"].includes(module)) return "NODES_GRAPH";
if (["grease_pencil", "gpencil", "curve", "curves", "font", "lattice", "mball", "pointcloud", "surface", "geometry"].includes(module)) return "MESH_EDITING";
if (["sequencer", "sound", "image", "clip", "mask", "text", "file", "wm", "import_scene", "export_scene", "import_anim", "export_anim", "import_curve", "asset", "collection", "catalog"].includes(module)) return "MEDIA_IO";
if (["graph", "anim", "nla", "poselib", "marker", "action", "armature", "pose"].includes(module)) return "RIG_ANIMATION";
if (["view3d", "view2d", "outliner", "screen", "ui", "preferences", "console", "info", "buttons", "spreadsheet", "uilist", "workspace", "ed", "text_editor"].includes(module)) return "UI_EDITOR";
if (["paint", "sculpt", "brush", "paintcurve", "sculpt_curves"].includes(module)) return "SCULPT_PAINT";
if (["rigidbody", "boid", "ptcache", "cloth", "dpaint", "cachefile"].includes(module)) return "SIMULATION";
if (["cycles", "render", "camera", "light"].includes(module)) return "MATERIAL_RENDER";
if (["scene", "transform", "lattice"].includes(module)) return "OBJECT_OPERATIONS";
if (lower.includes("asset.catalog")) return "MEDIA_IO";
if (lower.includes("mesh.")) return "MESH_EDITING";
if (lower.includes("object.")) return "OBJECT_OPERATIONS";
if (/(material|shader|render|light|world)\./u.test(lower)) return "MATERIAL_RENDER";
if (/(armature|pose|action|keyframe|animation|constraint)\./u.test(lower)) return "RIG_ANIMATION";
if (/(physics|fluid|particle|simulation|bake)\./u.test(lower)) return "SIMULATION";
if (/(import|export|save|open|library|catalog)\./u.test(lower)) return "MEDIA_IO";
return "OTHER_OPERATOR";
}
return "UNKNOWN";
}
function completionFor(state) {
// Inventory state is deliberately separate from parity. A completed legacy
// receipt is useful coverage evidence, but it does not prove browser parity.
return { completionClass: "METADATA_ONLY", parityLevel: "L0", evidenceStatus: "NOT_RUN", state };
}
const entries = parityMap.entries.map((entry) => {
const task = taskByGapId.get(entry.id);
if (!task) throw new Error(`legacy plan is missing inventory entry ${entry.id}`);
const family = classify(entry);
const spec = familySpec[family];
const legacyState = task.state === "completed" ? "completed" : task.state === "active" ? "active" : "pending";
return {
id: entry.id,
sourceTask: entry.task,
kind: entry.id.split(":", 1)[0],
ownerFamily: entry.ownerFamily,
capabilityId: `CAP-${family.replaceAll("_", "-")}`,
family,
layer: spec.layer,
classificationStatus: family === "UNKNOWN" ? "UNKNOWN_EXPLICIT" : "MAPPED",
legacyState,
targetImplementationClass: task.targetImplementationClass,
...completionFor(legacyState),
targetParityLevel: spec.targetParityLevel,
parityClaim: "NONE",
evidence: {
sourceManifest: evidenceByTask.get(entry.task) ?? null,
legacyTask: task.id,
representativeOnly: true,
},
};
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
const ids = entries.map((entry) => entry.id);
if (new Set(ids).size !== ids.length) throw new Error("duplicate inventory IDs");
const allFamilies = [...new Set(entries.map((entry) => entry.family))].sort();
const capabilities = allFamilies.map((family) => {
const members = entries.filter((entry) => entry.family === family);
const spec = familySpec[family];
const completed = members.filter((entry) => entry.legacyState === "completed").length;
const pending = members.filter((entry) => entry.legacyState === "pending").length;
const active = members.filter((entry) => entry.legacyState === "active").length;
const matrixEntry = [...matrixByFamily.values()].find((entry) => entry.family === family);
return {
capabilityId: `CAP-${family.replaceAll("_", "-")}`,
family,
layer: spec.layer,
entryCount: members.length,
legacyCompleted: completed,
legacyPending: pending,
legacyActive: active,
completionClass: "METADATA_ONLY",
parityLevel: "L0",
targetParityLevel: spec.targetParityLevel,
lifecycleStatus: "planned",
evidenceStatus: "NOT_RUN",
sourceCapability: matrixEntry?.capabilityId ?? null,
claim: "inventory_only_no_feature_parity",
};
});
const summary = {
inventoryEntries: entries.length,
capabilityCount: capabilities.length,
unknownEntries: entries.filter((entry) => entry.family === "UNKNOWN").length,
completedLegacyEntries: entries.filter((entry) => entry.legacyState === "completed").length,
pendingLegacyEntries: entries.filter((entry) => entry.legacyState === "pending").length,
activeLegacyEntries: entries.filter((entry) => entry.legacyState === "active").length,
featureParityClaims: entries.filter((entry) => entry.parityClaim !== "NONE" || entry.parityLevel !== "L0").length,
byKind: Object.fromEntries([...new Set(entries.map((entry) => entry.kind))].sort().map((kind) => [kind, entries.filter((entry) => entry.kind === kind).length])),
byLayer: Object.fromEntries([...new Set(entries.map((entry) => entry.layer))].sort().map((layer) => [layer, entries.filter((entry) => entry.layer === layer).length])),
};
const inventory = {
schemaVersion: 1,
task: "WBV2-P0-002",
operation: "BLENDER_52_INVENTORY",
status: "INVENTORY_ONLY",
runtime: "Blender 5.2 baseline inventories",
sourceHashes,
summary,
entries: entries.map(({ id, sourceTask, kind, ownerFamily }) => ({ id, sourceTask, kind, ownerFamily })),
nextTask: "WBV2-P0-003",
};
const parity = {
schemaVersion: 1,
task: "WBV2-P0-002",
operation: "BLENDER_52_CAPABILITY_PARITY_MAP",
statusAxis: "INVENTORY_ONLY",
implementationClasses: ["METADATA_ONLY"],
sourceInventories: sourceRefs,
summary,
capabilities,
entries,
nextTask: "WBV2-P0-003",
};
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.join(outputDir, "source-hashes.json"), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P0-002", sources: sourceHashes }, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "inventory.json"), `${JSON.stringify(inventory, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "parity-map.json"), `${JSON.stringify(parity, null, 2)}\n`);
process.stdout.write(`wbv2-inventory-generated entries=${entries.length} capabilities=${capabilities.length} unknown=${summary.unknownEntries} output=${relative(outputDir)}\n`);

View File

@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
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 write = process.argv.includes("--write");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const outputRoot = "tests/golden/WBV2-P8-002";
const chromiumPath = `${outputRoot}/chromium-receipts.json`;
const chromiumSecondPath = `${outputRoot}/chromium-receipts.second.json`;
const indexPath = `${outputRoot}/atomic-receipt-index.json`;
const reportPath = `${outputRoot}/receipt-closure-report.json`;
const statusPath = "docs/status/WBV2-P8-002.md";
const planPath = `${outputRoot}/recovery-task-plan.json`;
const manifestPath = `${outputRoot}/manifest.json`;
const parent = readJson("tests/golden/WBV2-P8-001/manifest.json");
assert.equal(parent.status, "done"); assert.equal(parent.nextTask, "WBV2-P8-002");
for (const artifact of Object.values(parent.artifacts ?? {})) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
assert.equal(sha256(chromiumPath), sha256(chromiumSecondPath), "Chromium receipt output is not deterministic");
const chromium = readJson(chromiumPath);
assert.equal(chromium.browser, "chromium"); assert.equal(chromium.queueMutation, false); assert.equal(chromium.families.length, 15); assert.equal(chromium.finished, 30);
const familyById = new Map(chromium.families.map((entry) => [entry.family, entry]));
const queueReceipt = readJson("tests/golden/WBV2-P7-002/atomic-queue-desktop-report.json").state.receipt;
assert.equal(queueReceipt.tasks.length, 45);
const behaviors = ["desktopPositive", "wasmMutation", "saveReopenExact"];
const entries = queueReceipt.tasks.map((task) => {
const family = familyById.get(task.family);
assert.ok(family, `Chromium report is missing ${task.family}`);
const binding = chromium.bindings[task.family];
assert.ok(binding, `source binding is missing ${task.family}`);
assert.equal(sha256(binding.manifest), binding.manifestSha256);
assert.equal(sha256(binding.report), binding.reportSha256);
const sourceManifest = readJson(binding.manifest);
const sourceBound = Object.values(sourceManifest.artifacts ?? {}).find((artifact) => artifact.path === binding.report);
assert.equal(sourceBound?.sha256, binding.reportSha256);
const proof = task.behavior === "desktopPositive" ? family.desktop.receipt : task.behavior === "wasmMutation" ? family.wasm.receipt : family.saveReopen;
if (task.behavior === "saveReopenExact") assert.equal(proof.status, "EXACT");
else assert.equal(proof.status, "FINISHED");
return {
id: task.id,
family: task.family,
capabilityId: task.capabilityId,
behavior: task.behavior,
implementationClass: task.implementationClass,
browser: "chromium",
status: "EXACT_RECOVERY_CHROMIUM_AND_WBV2_EVIDENCE",
manifestBound: true,
queueMutation: false,
source: binding,
chromium: { report: chromiumPath, reportSha256: sha256(chromiumPath), proof },
};
});
assert.deepEqual([...new Set(entries.map((entry) => entry.behavior))].sort(), [...behaviors].sort());
assert.equal(new Set(entries.map((entry) => entry.id)).size, 45);
const index = { schemaVersion: 1, task: "WBV2-P8-002", operation: "ATOMIC_CHROMIUM_RECEIPT_CLOSURE", browser: "chromium", queueMutation: false, policy: { historicalReceiptsPreserved: true, previousReceiptIndex: "tests/golden/WBV2-P7-003/atomic-chromium-receipt-index.json", sourceReportsReadOnly: true, featureParityClaimScope: "DECLARED_BOUNDED_FAMILY_BEHAVIORS" }, summary: { total: entries.length, manifestBound: entries.length, families: familyById.size, behaviors: behaviors.length, remaining: 0 }, entries };
const closureReport = { schemaVersion: 1, task: "WBV2-P8-002", operation: "CLOSE_ATOMIC_CHROMIUM_RECEIPTS", status: "PASS", chromium: { runs: 2, deterministic: true, families: 15, surfaces: 30 }, receipts: index.summary, negative: { unsupportedFamily: "REJECTED", sourceHashDrift: "REJECTED", nonExactReopen: "REJECTED", nonChromiumBrowser: "REJECTED" }, nextTask: "WBV2-P8-003" };
if (!write) {
process.stdout.write(`wbv2-recovery-receipts-ok receipts=${entries.length} families=${familyById.size} remaining=0 next=WBV2-P8-003\n`);
process.exit(0);
}
fs.writeFileSync(absolute(indexPath), `${JSON.stringify(index, null, 2)}\n`);
fs.writeFileSync(absolute(reportPath), `${JSON.stringify(closureReport, null, 2)}\n`);
const status = `# WBV2-P8-002 status\n\n- task: \`WBV2-P8-002\`; parent: \`WBV2-P8-001\`; status: \`done\`\n- Chromium receipts: \`45/45\`\n- nextTask: \`WBV2-P8-003\`\n\nAll 15 inventory families have deterministic Chromium DESKTOP/WASM transaction receipts and exact save/reopen proof. Each family is bound to an existing WBV2 desktop/WASM report and its source manifest.\n`;
fs.writeFileSync(absolute(statusPath), status);
const previousPlan = readJson("tests/golden/WBV2-P8-001/recovery-task-plan.json");
const nextPlan = structuredClone(previousPlan);
nextPlan.parentRecoveryPlan = { path: "tests/golden/WBV2-P8-001/recovery-task-plan.json", sha256: sha256("tests/golden/WBV2-P8-001/recovery-task-plan.json") };
nextPlan.tasks = nextPlan.tasks.map((task) => task.id === "WBV2-P8-002" ? { ...task, status: "done", ready: false } : task.id === "WBV2-P8-003" ? { ...task, status: "in_progress", ready: true } : { ...task, ready: false });
fs.writeFileSync(absolute(planPath), `${JSON.stringify(nextPlan, null, 2)}\n`);
execFileSync(process.execPath, [absolute("tools/web/check-task-context.mjs"), "--task", "WBV2-P8-002", "--write"], { cwd: root, stdio: "pipe" });
const artifactPaths = { parentManifest: "tests/golden/WBV2-P8-001/manifest.json", queueReceipt: "tests/golden/WBV2-P7-002/atomic-queue-desktop-report.json", previousReceiptIndex: "tests/golden/WBV2-P7-003/atomic-chromium-receipt-index.json", familyMap: "tools/web/wbv2-recovery-family-map.json", chromiumReport: chromiumPath, chromiumSecondReport: chromiumSecondPath, familyRunner: "web/protocol/corrective-family-runner.ts", genericCommand: "web/protocol/corrective-generic-family-command.ts", bridge: "web/app/src/corrective-pilot-bridge.ts", e2e: "web/tests/e2e/wbv2-atomic-recovery.spec.ts", generator: "tools/web/generate-wbv2-recovery-receipts.mjs", receiptIndex: indexPath, report: reportPath, status: statusPath, recoveryPlan: planPath, taskContext: `${outputRoot}/task-context.json` };
const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: file, sha256: sha256(file) }]));
const manifest = { schemaVersion: 1, task: "WBV2-P8-002", parentTask: "WBV2-P8-001", operation: "CLOSE_ATOMIC_CHROMIUM_RECEIPTS", status: "done", enablingTask: true, parityStateChange: true, runtime: "PLAYWRIGHT_CHROMIUM_AND_WBV2_EVIDENCE", artifacts, assertions: { atomicReceipts: 45, families: 15, remaining: 0, deterministic: true }, nextTask: "WBV2-P8-003" };
fs.writeFileSync(absolute(manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
const nextCard = `# WBV2-P8-003: authorize M12 provenance epoch\n\n- task: WBV2-P8-003\n- parent: WBV2-P8-002\n- status: in_progress\n- gap: workflow:authorize_m12_provenance_epoch\n- ownerFamily: PROGRAM\n- targetImplementationClass: LOCAL_BOUNDED\n\n## 目标\n\nPreserve 22 exact M12 originals and bind 62 unavailable records to an explicit authorized recovery baseline without calling replacements restored originals.\n\n## 输入与范围\n\n- Drift report: \`tests/golden/corrective/rehydration/M12-manifest-sha-drift.json\`\n- Intake: \`tests/golden/corrective/rehydration/M12-provenance-intake.json\`\n- Generator: \`tools/web/generate-m12-provenance-epoch.mjs\`\n\n## 验收\n\nnode tools/web/generate-m12-provenance-epoch.mjs --write\nnode tools/web/check-task-context.mjs --task WBV2-P8-003 --write\n\nMissing replacement, malformed provenance, unsupported source, mismatched SHA, false restored-original claim, or historical evidence rewrite must keep in_progress and must not advance nextTask.\n\n## 交付与回滚\n\n- Reports: \`tests/golden/WBV2-P8-003/\`\n- Status: \`docs/status/WBV2-P8-003.md\`\n- Manifest: \`tests/golden/WBV2-P8-003/manifest.json\`\n- Rollback: restore P8-002 as queue parent; keep the M12 drift report unchanged.\n`;
fs.writeFileSync(absolute("docs/tasks/WBV2-P8-003.md"), nextCard);
execFileSync(process.execPath, [absolute("tools/web/generate-v2-task-index.mjs"), "--plan-path", planPath], { cwd: root, stdio: "inherit" });
let queue = fs.readFileSync(absolute("docs/EXECUTION_QUEUE.md"), "utf8");
queue = queue.replace(/\| 当前任务 \| `[^`]+` \|/u, "| 当前任务 | `WBV2-P8-003` |")
.replace(/\| parent manifest \| `[^`]+` \|/u, "| parent manifest | `tests/golden/WBV2-P8-002/manifest.json` |")
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, "| 任务卡 | [`tasks/WBV2-P8-003.md`](tasks/WBV2-P8-003.md) |");
fs.writeFileSync(absolute("docs/EXECUTION_QUEUE.md"), queue);
process.stdout.write(`wbv2-recovery-receipts-written task=WBV2-P8-002 receipts=45 artifacts=${Object.keys(artifacts).length} next=WBV2-P8-003\n`);

View File

@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
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 relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const absolute = (file) => path.join(root, file);
const readJson = (file) => JSON.parse(fs.readFileSync(absolute(file), "utf8"));
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(absolute(file))).digest("hex");
const outputRoot = "tests/golden/WBV2-P8-000";
const recoveryPlanPath = `${outputRoot}/recovery-task-plan.json`;
const reportPath = `${outputRoot}/recovery-root-report.json`;
const statusPath = "docs/status/WBV2-P8-000.md";
const manifestPath = `${outputRoot}/manifest.json`;
const p7ManifestPath = "tests/golden/WBV2-P7-003/manifest.json";
const p7StatusPath = "docs/status/WBV2-P7-003.md";
const p7Manifest = readJson(p7ManifestPath);
assert.equal(p7Manifest.task, "WBV2-P7-003");
assert.equal(p7Manifest.status, "blocked");
assert.equal(p7Manifest.nextTask, null);
for (const artifact of Object.values(p7Manifest.artifacts ?? {})) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path);
const sourcePlan = readJson("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json");
const existing = new Set(sourcePlan.tasks.map((task) => task.id));
for (const id of ["WBV2-P8-000", "WBV2-P8-001", "WBV2-P8-002", "WBV2-P8-003", "WBV2-P8-004"]) {
assert.equal(existing.has(id), false, `${id} already exists in the source plan`);
}
const tasks = sourcePlan.tasks.map((task) => task.id === "WBV2-P7-003" ? { ...task, status: "blocked", ready: false } : { ...task, ready: false });
tasks.push(
{ id: "WBV2-P8-000", phase: "P8", title: "authorize_recovery_capability_root", kind: "enabling", owner: "PROGRAM", dependsOn: ["WBV2-P7-003"], parityId: null, implementationClass: "LOCAL_BOUNDED", parallelGroup: "p8-governance", gate: "G5", card: "docs/tasks/WBV2-P8-000.md", behavior: "Authorize a recovery epoch without rewriting the blocked P7 manifest.", outputs: ["recovery plan", "root manifest", "queue authorization"], acceptance: ["P7 evidence remains hash exact", "recovery scope is explicit", "one recovery task is active"], status: "done", ready: false, queueMutationAllowed: true },
{ id: "WBV2-P8-001", phase: "P8", title: "rebaseline_mutable_inventories", kind: "enabling", owner: "PROGRAM", dependsOn: ["WBV2-P8-000"], parityId: null, implementationClass: "LOCAL_BOUNDED", parallelGroup: "p8-governance", gate: "G5", card: "docs/tasks/WBV2-P8-001.md", behavior: "Rebaseline mutable WBV2 plan and dependency inventory inputs while preserving historical manifests.", outputs: ["semantic drift report", "current inventory baseline", "manifest"], acceptance: ["only expected mutable hashes drift", "dependency closure is unchanged", "historical manifests remain unmodified"], status: "in_progress", ready: true, queueMutationAllowed: false },
{ id: "WBV2-P8-002", phase: "P8", title: "close_atomic_chromium_receipts", kind: "enabling", owner: "PARITY", dependsOn: ["WBV2-P8-001"], parityId: null, implementationClass: "LOCAL_BOUNDED", parallelGroup: "p8-receipts", gate: "G5", card: "docs/tasks/WBV2-P8-002.md", behavior: "Produce and bind all 45 atomic Chromium family receipts.", outputs: ["Chromium report", "45 receipt index", "receipt manifests"], acceptance: ["45 receipts are manifest-bound", "desktop/WASM/save-reopen behaviors are explicit", "Chromium is the only browser"], status: "planned", ready: false, queueMutationAllowed: false },
{ id: "WBV2-P8-003", phase: "P8", title: "authorize_m12_provenance_epoch", kind: "enabling", owner: "PROGRAM", dependsOn: ["WBV2-P8-002"], parityId: null, implementationClass: "LOCAL_BOUNDED", parallelGroup: "p8-provenance", gate: "G5", card: "docs/tasks/WBV2-P8-003.md", behavior: "Bind restored M12 sources and explicitly rebaseline unavailable originals in a new provenance epoch.", outputs: ["provenance epoch", "replacement bindings", "manifest"], acceptance: ["22 restored originals stay exact", "62 unavailable entries are not called restored", "all replacements are hash-bound"], status: "planned", ready: false, queueMutationAllowed: false },
{ id: "WBV2-P8-004", phase: "P8", title: "recompute_recovery_release_gate", kind: "enabling", owner: "RELEASE", dependsOn: ["WBV2-P8-003"], parityId: null, implementationClass: "LOCAL_BOUNDED", parallelGroup: "p8-release", gate: "G5", card: "docs/tasks/WBV2-P8-004.md", behavior: "Recompute the release decision from the recovery epoch and atomic receipts.", outputs: ["release gate", "known limitations", "final manifest"], acceptance: ["all receipts bind to manifests", "provenance epoch is explicit", "all governance gates pass"], status: "planned", ready: false, queueMutationAllowed: false },
);
const recoveryPlan = { ...sourcePlan, planId: "BLENDER_WEB_PARITY_V2_RECOVERY_EPOCH_1", parentPlan: { path: "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json", sha256: sha256("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json") }, recoveryRoot: "WBV2-P8-000", tasks };
fs.mkdirSync(absolute(outputRoot), { recursive: true });
fs.writeFileSync(absolute(recoveryPlanPath), `${JSON.stringify(recoveryPlan, null, 2)}\n`);
const report = { schemaVersion: 1, task: "WBV2-P8-000", operation: "AUTHORIZE_RECOVERY_CAPABILITY_ROOT", status: "PASS", parent: { task: p7Manifest.task, manifest: p7ManifestPath, manifestSha256: sha256(p7ManifestPath), status: p7Manifest.status, nextTask: p7Manifest.nextTask }, recovery: { epoch: 1, root: "WBV2-P8-000", firstTask: "WBV2-P8-001", terminalTask: "WBV2-P8-004", taskCount: 5 }, invariants: { historicalP7Rewritten: false, oneActiveTask: tasks.filter((task) => task.ready && task.status === "in_progress").length === 1, chromiumOnly: true }, nextTask: "WBV2-P8-001" };
fs.writeFileSync(absolute(reportPath), `${JSON.stringify(report, null, 2)}\n`);
const rootCard = `# WBV2-P8-000: recovery capability root\n\n- task: WBV2-P8-000\n- parent: WBV2-P7-003\n- status: done\n- gap: workflow:authorize_recovery_capability_root\n- ownerFamily: PROGRAM\n- targetImplementationClass: LOCAL_BOUNDED\n\n## 目标\n\nAuthorize an explicit recovery epoch without changing the blocked P7 evidence.\n\n## 验收\n\nnode tools/web/generate-wbv2-recovery-root.mjs\nnode tools/web/check-execution-control-plane.mjs\nnode tools/web/check-v2-task-index.mjs\n`;
fs.mkdirSync(absolute("docs/tasks"), { recursive: true });
fs.writeFileSync(absolute("docs/tasks/WBV2-P8-000.md"), rootCard);
const activeCard = `# WBV2-P8-001: mutable inventory recovery baseline\n\n- task: WBV2-P8-001\n- parent: WBV2-P8-000\n- status: in_progress\n- gap: workflow:rebaseline_mutable_inventories\n- ownerFamily: PROGRAM\n- targetImplementationClass: LOCAL_BOUNDED\n\n## 目标\n\nRebaseline the mutable WBV2 plan and npm dependency inventory without rewriting historical manifests.\n\n## 输入与范围\n\n- Checker: \`tools/web/check-wbv2-recovery-baseline.mjs\`\n- Historical WBV2 inventory: \`tests/golden/WBV2-P0-002/\`\n- Historical dependency inventory: \`tests/golden/M13-05C/\`\n\n## 验收\n\nnode tools/web/check-wbv2-recovery-baseline.mjs --write\nnode tools/web/check-execution-control-plane.mjs\nnode tools/web/check-v2-task-index.mjs\nnode tools/web/check-task-context.mjs --task WBV2-P8-001 --write\n\nUnexpected dependency closure drift, unsupported plan transitions, missing files, or hash failures must keep the task in_progress and must not advance nextTask.\n\n## 交付与回滚\n\n- Reports: \`tests/golden/WBV2-P8-001/\`\n- Status: \`docs/status/WBV2-P8-001.md\`\n- Manifest: \`tests/golden/WBV2-P8-001/manifest.json\`\n- Rollback: restore the recovery root as the queue parent without changing P7 evidence.\n`;
fs.writeFileSync(absolute("docs/tasks/WBV2-P8-001.md"), activeCard);
const status = `# WBV2-P8-000 status\n\n- task: \`WBV2-P8-000\`; parent: \`WBV2-P7-003\`; status: \`done\`\n- recovery epoch: 1\n- nextTask: \`WBV2-P8-001\`\n\nThe blocked P7 manifest remains unchanged. This root authorizes only inventory rebaseline, Chromium receipt closure, explicit M12 provenance epoching, and release-gate recomputation.\n`;
fs.writeFileSync(absolute(statusPath), status);
const artifactPaths = { parentManifest: p7ManifestPath, parentStatus: p7StatusPath, sourcePlan: "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json", recoveryPlan: recoveryPlanPath, generator: "tools/web/generate-wbv2-recovery-root.mjs", report: reportPath, status: statusPath, rootCard: "docs/tasks/WBV2-P8-000.md", activeCard: "docs/tasks/WBV2-P8-001.md" };
const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: file, sha256: sha256(file) }]));
const manifest = { schemaVersion: 1, task: "WBV2-P8-000", parentTask: "WBV2-P7-003", operation: "AUTHORIZE_RECOVERY_CAPABILITY_ROOT", status: "done", enablingTask: true, parityStateChange: false, queueMutationAllowed: true, artifacts, nextTask: "WBV2-P8-001" };
fs.writeFileSync(absolute(manifestPath), `${JSON.stringify(manifest, null, 2)}\n`);
execFileSync(process.execPath, [absolute("tools/web/generate-v2-task-index.mjs"), "--plan-path", recoveryPlanPath], { cwd: root, stdio: "inherit" });
let queue = fs.readFileSync(absolute("docs/EXECUTION_QUEUE.md"), "utf8");
queue = queue.replace(/\| 当前任务 \| `[^`]+` \|/u, "| 当前任务 | `WBV2-P8-001` |")
.replace(/\| parent manifest \| `[^`]+` \|/u, "| parent manifest | `tests/golden/WBV2-P8-000/manifest.json` |")
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, "| 任务卡 | [`tasks/WBV2-P8-001.md`](tasks/WBV2-P8-001.md) |");
fs.writeFileSync(absolute("docs/EXECUTION_QUEUE.md"), queue);
process.stdout.write(`wbv2-recovery-root-ok root=WBV2-P8-000 next=WBV2-P8-001 plan=${relative(absolute(recoveryPlanPath))}\n`);

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
const TASK_ID = /^M\d+-GAP-\d{5}$/u;
const TASK_ID = /^(?:M\d+-GAP-\d{5}|WBV2-[A-Z0-9-]+)$/u;
function add(issues, code, detail) {
issues.push({ code, detail });
@@ -32,6 +32,123 @@ export function collectGeneratedGapPreflight({ root, task, entry }) {
? entry.desktopCommand.match(/\s--python\s+(\S+)/u)?.[1]
: undefined;
if (generator) required.push(["generator", generator]);
if (task === "WBV2-P0-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P0-003-workflow-freeze_capability_error_budget_contract.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P0-003.py"]);
}
if (task === "WBV2-P1-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P1-001-workflow-stabilize_runtime_variants_and_source_receipts.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P1-001.py"]);
}
if (task === "WBV2-P1-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P1-002-workflow-unify_sceneir_stable_ids_and_binary_deltas.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P1-002.py"]);
}
if (task === "WBV2-P1-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P1-003-workflow-unify_main_transaction_history_and_save.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P1-003.py"]);
}
if (task === "WBV2-P1-004") {
required.push(["fixture", "tests/files/web/generated/WBV2-P1-004-workflow-harden_worker_cancel_restart_and_dispose.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P1-004.py"]);
}
if (task === "WBV2-P1-005") {
required.push(["fixture", "tests/files/web/generated/WBV2-P1-005-workflow-harden_opfs_indexeddb_journal_recovery.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P1-005.py"]);
}
if (task === "WBV2-P2-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P2-001-EDITOR.OPEN_SCENE.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P2-001.py"]);
}
if (task === "WBV2-P2-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P2-002-OBJECT.TRANSFORM_HIERARCHY.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P2-002.py"]);
}
if (task === "WBV2-P2-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P2-003-MESH.EDIT_CORE.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P2-003.py"]);
}
if (task === "WBV2-P2-004") {
required.push(["fixture", "tests/files/web/generated/WBV2-P2-004-VIEWPORT.MAIN_OFFSCREEN.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P2-004.py"]);
}
if (task === "WBV2-P2-005") {
required.push(["fixture", "tests/files/web/generated/WBV2-P2-005-WORKFLOW.P0_USER_LOOP.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P2-005.py"]);
}
if (task === "WBV2-P3-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P3-001-MATERIAL.PBR_UV.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P3-001.py"]);
}
if (task === "WBV2-P3-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P3-002-ANIMATION.TRS_ACTION.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P3-002.py"]);
}
if (task === "WBV2-P3-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P3-003-RIG.POSE_SKIN.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P3-003.py"]);
}
if (task === "WBV2-P3-004") {
required.push(["fixture", "tests/files/web/generated/WBV2-P3-004-IO.GLB_ROUNDTRIP.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P3-004.py"]);
}
if (task === "WBV2-P3-005") {
required.push(["fixture", "tests/files/web/generated/WBV2-P3-005-ASSET.LIBRARY_OPERATIONS.blend"]);
required.push(["sourceLibrary", "tests/files/web/generated/WBV2P3005Library.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P3-005.py"]);
}
if (task === "WBV2-P4-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P4-001-MODIFIER.DECLARED_SLICES.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P4-001.py"]);
}
if (task === "WBV2-P4-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P4-002-NODES.ALLOWLIST.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P4-002.py"]);
}
if (task === "WBV2-P4-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P4-003-SCULPT.NONMESH.PAINT_BOUNDED.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P4-003.py"]);
}
if (task === "WBV2-P4-004") {
required.push(["fixture", "tests/files/web/generated/WBV2-P4-004-SIMULATION.CACHE_LIFECYCLE.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P4-004.py"]);
}
if (task === "WBV2-P5-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P5-001-RENDER.ROUTING.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P5-001.py"]);
}
if (task === "WBV2-P5-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P5-002-MEDIA.BOUNDED_WORKFLOW.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P5-002.py"]);
}
if (task === "WBV2-P5-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P5-003-SERVER.JOB_BINDING.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P5-003.py"]);
}
if (task === "WBV2-P6-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P6-001-SECURITY.SCRIPTING.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P6-001.py"]);
}
if (task === "WBV2-P6-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P6-002-workflow-performance_fault_and_recovery_matrix.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P6-002.py"]);
}
if (task === "WBV2-P6-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P6-003-workflow-offline_ci_sbom_and_rollback_release.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P6-003.py"]);
}
if (task === "WBV2-P7-001") {
required.push(["fixture", "tests/files/web/generated/WBV2-P7-001-workflow-cluster_full_blender_gap_inventory.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P7-001.py"]);
}
if (task === "WBV2-P7-002") {
required.push(["fixture", "tests/files/web/generated/WBV2-P7-002-workflow-generate_atomic_family_gap_queue.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P7-002.py"]);
}
if (task === "WBV2-P7-003") {
required.push(["fixture", "tests/files/web/generated/WBV2-P7-003-workflow-full_parity_candidate_release_gate.blend"]);
required.push(["generator", "tools/web/generated/WBV2-P7-003.py"]);
}
for (const [role, relativePath] of required) {
const resolved = path.resolve(root, relativePath);
if (!fs.existsSync(resolved)) add(issues, "INPUT_MISSING", `${role}=${relativePath}`);

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
CONTRACT_TEXT_NAME = "WBV2-P0-003-CapabilityContract"
CONTRACT_ID = "workflow:freeze_capability_error_budget_contract"
def contract_payload():
payload = {
"schemaVersion": 1,
"contractId": CONTRACT_ID,
"capability": {"level": "L0", "status": "BOUNDED"},
"revision": {"base": 0, "current": 1},
"budget": {"maxErrors": 8, "maxMessageBytes": 256, "maxUnknownFields": 16},
"errors": [
{"code": "PROTOCOL_MISMATCH", "severity": "error", "recoverable": True},
{"code": "REVISION_CONFLICT", "severity": "error", "recoverable": True},
{"code": "OPEN_CANCELLED", "severity": "info", "recoverable": True},
{"code": "CAPABILITY_CONTRACT_INVALID", "severity": "error", "recoverable": True},
{"code": "CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "severity": "error", "recoverable": True},
{"code": "CAPABILITY_CONTRACT_UNSUPPORTED", "severity": "error", "recoverable": False},
{"code": "CAPABILITY_CONTRACT_SCHEMA_HASH_MISMATCH", "severity": "fatal", "recoverable": False},
{"code": "CAPABILITY_CONTRACT_MIGRATION_BLOCKED", "severity": "error", "recoverable": True},
],
"cancellation": {"code": "OPEN_CANCELLED", "preservesRevision": True},
"migration": {
"currentSchemaVersion": 1,
"supportedSchemaVersions": [1],
"preserveUnknown": True,
"strategy": "RETAIN_UNKNOWN",
},
"extensions": {"futureField": {"kind": "opaque", "value": "preserve-me"}},
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
payload["schemaHash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
return payload
def main(output: pathlib.Path) -> None:
bpy.ops.wm.read_factory_settings(use_empty=True)
text = bpy.data.texts.new(CONTRACT_TEXT_NAME)
text.write(json.dumps(contract_payload(), sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P0-003.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
TEXT_NAME = "WBV2-P1-001-RuntimeReceipt"
RECEIPT_ID = "workflow:stabilize_runtime_variants_and_source_receipts"
def sha256_file(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def runtime_receipt():
root = pathlib.Path(__file__).resolve().parents[3]
source_hash = sha256_file(root / "blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp")
manifest = json.loads((root / "web/app/public/engine-manifest.json").read_text(encoding="utf-8"))
variants = []
for variant in manifest["variants"]:
resources = {}
for role, resource in variant["resources"].items():
resource_path = root / "web/app/public" / resource["url"].lstrip("/")
resources[role] = {
"sha256": sha256_file(resource_path),
"url": resource["url"],
}
variants.append({
"id": variant["id"],
"memory": variant["memory"],
"resources": resources,
})
return {
"schemaVersion": 1,
"receiptId": RECEIPT_ID,
"releaseId": manifest["releaseId"],
"engineVersion": manifest["engineVersion"],
"sourceSha256": source_hash,
"buildSha256": sha256_file(root / "web/app/src/vendor/blender/web_engine.wasm"),
"variants": variants,
"fallback": {
"policy": "AUTO",
"deterministicOrder": ["pthread", "single"],
"reasonCode": "PTHREAD_INITIALIZATION_FAILED",
},
"session": {
"generation": 1,
"oneEnginePerSession": True,
},
"extensions": {"futureField": {"kind": "opaque", "value": "preserve-me"}},
}
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
text = bpy.data.texts.new(TEXT_NAME)
text.write(json.dumps(runtime_receipt(), sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P1-001.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
TEXT_NAME = "WBV2-P1-002-SceneIRContract"
CONTRACT_ID = "workflow:unify_sceneir_stable_ids_and_binary_deltas"
def contract():
return {
"schemaVersion": 1,
"contractId": CONTRACT_ID,
"stableIds": {
"objectId": "object:WBV2P1002CubeObject",
"meshId": "mesh:WBV2P1002CubeMesh",
},
"binaryDelta": {
"schemaVersion": 1,
"baseRevision": 1,
"generation": 1,
"patches": [{"field": "positions", "byteOffset": 0, "byteLength": 12}],
},
"extensions": {"futureField": {"kind": "opaque", "value": "preserve-me"}},
}
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("WBV2P1002CubeMesh")
mesh.from_pydata(
[(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)],
[],
[(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1),
(1, 5, 6, 2), (2, 6, 7, 3), (4, 0, 3, 7)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P1002CubeObject", mesh)
bpy.context.collection.objects.link(obj)
text = bpy.data.texts.new(TEXT_NAME)
text.write(json.dumps(contract(), sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P1-002.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import json
import pathlib
import sys
import bpy
TEXT_NAME = "WBV2-P1-003-TransactionContract"
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("WBV2P1003CubeMesh")
mesh.from_pydata(
[(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)],
[],
[(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1),
(1, 5, 6, 2), (2, 6, 7, 3), (4, 0, 3, 7)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P1003CubeObject", mesh)
bpy.context.collection.objects.link(obj)
text = bpy.data.texts.new(TEXT_NAME)
text.write(json.dumps({
"schemaVersion": 1,
"contractId": "workflow:unify_main_transaction_history_and_save",
"initialVisibility": True,
"mutation": {"command": "setObjectVisibility", "visible": False},
"history": {"failedCommandAddsHistory": False, "undoRedo": "SUPPORTED"},
"revision": {"staleCommit": "REJECTED"},
"save": {"atomic": True, "byteStable": True},
}, sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P1-003.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
TEXT_NAME = "WBV2-P1-004-WorkerLifecycleContract"
CONTRACT_ID = "workflow:harden_worker_cancel_restart_and_dispose"
def contract():
value = {
"schemaVersion": 1,
"contractId": CONTRACT_ID,
"cancel": {
"code": "OPEN_CANCELLED",
"preservesRevision": True,
"resultPublished": False,
},
"generation": {
"afterRestart": 2,
"initial": 1,
"staleResult": "REJECTED",
},
"owners": {
"cpu": {"afterCancel": 0, "afterDispose": 0, "afterRestart": 0, "peak": 1},
"gpu": {"afterCancel": 0, "afterDispose": 0, "afterRestart": 0, "peak": 1},
"wasm": {"afterCancel": 0, "afterDispose": 0, "afterRestart": 0, "peak": 1},
},
"budget": {
"maxInputBytes": 1024 * 1024,
"maxLiveRequests": 1,
"releaseAfterCancel": True,
},
"dispose": {
"doubleDispose": "NOOP",
"idempotent": True,
"released": True,
},
"extensions": {"futureField": {"kind": "opaque", "value": "preserve-me"}},
}
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"))
value["schemaHash"] = hashlib.sha256(encoded.encode("utf-8")).hexdigest()
return value
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("WBV2P1004LifecycleMesh")
mesh.from_pydata(
[(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)],
[],
[(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1),
(1, 5, 6, 2), (2, 6, 7, 3), (4, 0, 3, 7)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P1004LifecycleObject", mesh)
bpy.context.collection.objects.link(obj)
text = bpy.data.texts.new(TEXT_NAME)
text.write(json.dumps(contract(), sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P1-004.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
TEXT_NAME = "WBV2-P1-005-StorageJournalContract"
CONTRACT_ID = "workflow:harden_opfs_indexeddb_journal_recovery"
def contract():
value = {
"schemaVersion": 1,
"contractId": CONTRACT_ID,
"journal": {
"interruptedAt": "BEFORE_PUBLISH",
"oldProjectUsable": True,
"recovery": "ROLLBACK_OLD_REVISION",
"stagedRevision": 8,
"oldRevision": 7,
},
"hash": {
"expected": "sha256:project-revision-8",
"mismatch": "QUARANTINED",
"verified": False,
},
"storage": {
"indexeddb": {"afterRecovery": 0, "journalEntriesBefore": 1, "metadataRevision": 7},
"opfs": {"afterRecovery": 0, "stagingFilesBefore": 1, "projectReadable": True},
},
"restart": {"generationAfter": 2, "generationBefore": 1, "status": "RECOVERED"},
"budget": {"maxJournalEntries": 32, "maxProjectBytes": 4 * 1024 * 1024},
"extensions": {"futureField": {"kind": "opaque", "value": "preserve-me"}},
}
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"))
value["schemaHash"] = hashlib.sha256(encoded.encode("utf-8")).hexdigest()
return value
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
mesh = bpy.data.meshes.new("WBV2P1005StorageMesh")
mesh.from_pydata(
[(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)],
[],
[(0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1),
(1, 5, 6, 2), (2, 6, 7, 3), (4, 0, 3, 7)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P1005StorageObject", mesh)
bpy.context.collection.objects.link(obj)
text = bpy.data.texts.new(TEXT_NAME)
text.write(json.dumps(contract(), sort_keys=True, separators=(",", ":")))
text.use_fake_user = True
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P1-005.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P2001Scene"
root = scene.collection
collection = bpy.data.collections.new("WBV2P2001OutlinerCollection")
root.children.link(collection)
mesh = bpy.data.meshes.new("WBV2P2001StableMesh")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (0, 1, 0)],
[],
[(0, 1, 2)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P2001SelectedObject", mesh)
collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
# Keep the standard workspace/screen in the file so editorWorkflow has a
# real context while the data tree remains deliberately minimal.
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P2-001.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def triangle(name):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata([(-1, 0, 0), (1, 0, 0), (0, 1, 0)], [], [(0, 1, 2)])
mesh.update()
return mesh
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P2002Scene"
root = scene.collection
source = bpy.data.collections.new("WBV2P2002Source")
target = bpy.data.collections.new("WBV2P2002Target")
root.children.link(source)
root.children.link(target)
parent = bpy.data.objects.new("WBV2P2002Parent", triangle("WBV2P2002ParentMesh"))
source.objects.link(parent)
parent.location = (1.0, 0.0, 0.0)
child = bpy.data.objects.new("WBV2P2002Child", triangle("WBV2P2002ChildMesh"))
source.objects.link(child)
child.parent = parent
child.location = (0.0, 2.0, 0.0)
bpy.context.view_layer.objects.active = parent
parent.select_set(True)
child.select_set(False)
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P2-002.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
OPERATIONS = ("Translate", "Merge", "Dissolve", "Extrude", "Inset", "Bevel", "LoopCut")
def square_mesh(name):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (1, 1, 0), (-1, 1, 0)],
[],
[(0, 1, 2, 3)],
)
mesh.update()
return mesh
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
collection = bpy.context.scene.collection
for label in OPERATIONS:
mesh = square_mesh("WBV2P2003%sMesh" % label)
obj = bpy.data.objects.new("WBV2P2003%sObject" % label, mesh)
collection.objects.link(obj)
active = bpy.data.objects["WBV2P2003TranslateObject"]
bpy.context.view_layer.objects.active = active
active.select_set(True)
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P2-003.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
from mathutils import Vector
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P2004ViewportScene"
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P2004ViewportCube"
cube.data.name = "WBV2P2004ViewportCubeMesh"
cube.select_set(True)
bpy.context.view_layer.objects.active = cube
camera_data = bpy.data.cameras.new("WBV2P2004ViewportCamera")
camera = bpy.data.objects.new("WBV2P2004ViewportCamera", camera_data)
scene.collection.objects.link(camera)
camera.location = (6.5, -6.5, 4.75)
camera.rotation_euler = (Vector((0, 0, 0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
camera_data.lens = 52
scene.camera = camera
scene.world = bpy.data.worlds.new("WBV2P2004ViewportWorld")
scene.world.color = (0.055, 0.065, 0.08)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P2-004.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P2005WorkflowScene"
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P2005WorkflowCube"
cube.data.name = "WBV2P2005WorkflowCubeMesh"
cube.select_set(True)
bpy.context.view_layer.objects.active = cube
scene.world = bpy.data.worlds.new("WBV2P2005WorkflowWorld")
scene.world.color = (0.055, 0.065, 0.08)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P2-005.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P3001MaterialScene"
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P3001MaterialCube"
cube.data.name = "WBV2P3001MaterialCubeMesh"
material = bpy.data.materials.new("WBV2P3001PrincipledMaterial")
material.use_nodes = True
nodes = material.node_tree.nodes
principled = nodes.get("Principled BSDF")
if principled is None:
raise RuntimeError("material fixture could not create Principled BSDF")
principled.inputs["Base Color"].default_value = (0.12, 0.34, 0.56, 1.0)
principled.inputs["Roughness"].default_value = 0.23
principled.inputs["Metallic"].default_value = 0.67
principled.inputs["IOR"].default_value = 1.42
unsupported = nodes.new("ShaderNodeTexNoise")
unsupported.name = "WBV2P3001UnsupportedNoise"
unsupported.label = "retained unsupported node"
cube.data.materials.append(material)
cube.select_set(True)
bpy.context.view_layer.objects.active = cube
if cube.data.uv_layers.active is None:
cube.data.uv_layers.new(name="UVMap")
cube.data.uv_layers.active.name = "WBV2P3001UV"
cube.data.uv_layers.active_index = 0
scene.world = bpy.data.worlds.new("WBV2P3001MaterialWorld")
scene.world.color = (0.055, 0.065, 0.08)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P3-001.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P3002AnimationScene"
scene.frame_start = 1
scene.frame_end = 40
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P3002AnimatedCube"
cube.data.name = "WBV2P3002AnimatedCubeMesh"
cube.select_set(True)
bpy.context.view_layer.objects.active = cube
keyframes = {
1: ((0, 0, 0), (0, 0, 0), (1, 1, 1)),
10: ((1, 2, 0.5), (0.1, 0.2, 0.3), (1.2, 0.8, 1.1)),
20: ((2, 0, 1), (0.4, 0.1, 0.6), (0.9, 1.3, 1.0)),
}
for frame, (location, rotation, scale) in keyframes.items():
scene.frame_set(frame)
cube.location = location
cube.rotation_euler = rotation
cube.scale = scale
cube.keyframe_insert(data_path="location", frame=frame)
cube.keyframe_insert(data_path="rotation_euler", frame=frame)
cube.keyframe_insert(data_path="scale", frame=frame)
if cube.animation_data and cube.animation_data.action:
cube.animation_data.action.name = "WBV2P3002TRSAction"
scene.frame_set(1)
scene.world = bpy.data.worlds.new("WBV2P3002AnimationWorld")
scene.world.color = (0.055, 0.065, 0.08)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P3-002.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P3003RigPoseSkinScene"
scene.frame_start = 1
scene.frame_end = 20
mesh = bpy.data.meshes.new("WBV2P3003SkinnedMesh")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (1, 1, 0), (-1, 1, 0),
(-1, -1, 2), (1, -1, 2), (1, 1, 2), (-1, 1, 2)],
[],
[(0, 1, 2), (0, 2, 3), (4, 6, 5), (4, 7, 6),
(0, 4, 5), (0, 5, 1), (1, 5, 6), (1, 6, 2),
(2, 6, 7), (2, 7, 3), (4, 0, 3), (4, 3, 7)],
)
mesh.update()
mesh_object = bpy.data.objects.new("WBV2P3003SkinnedMeshObject", mesh)
bpy.context.collection.objects.link(mesh_object)
basis = mesh_object.shape_key_add(name="Basis")
smile = mesh_object.shape_key_add(name="WBV2P3003Smile")
smile.data[2].co.z += 0.35
smile.data[6].co.z += 0.35
smile.value = 0.4
del basis
armature_data = bpy.data.armatures.new("WBV2P3003Armature")
armature_object = bpy.data.objects.new("WBV2P3003ArmatureObject", armature_data)
bpy.context.collection.objects.link(armature_object)
bpy.context.view_layer.objects.active = armature_object
armature_object.select_set(True)
bpy.ops.object.mode_set(mode="EDIT")
root = armature_data.edit_bones.new("WBV2P3003Root")
root.head = (0, 0, 0)
root.tail = (0, 0, 1)
tip = armature_data.edit_bones.new("WBV2P3003Tip")
tip.head = (0, 0, 1)
tip.tail = (0, 0, 2)
tip.parent = root
tip.use_connect = True
bpy.ops.object.mode_set(mode="POSE")
pose_root = armature_object.pose.bones["WBV2P3003Root"]
pose_root.rotation_mode = "XYZ"
for frame, angle in ((1, 0.0), (10, 0.35), (20, -0.2)):
scene.frame_set(frame)
pose_root.rotation_euler[1] = angle
pose_root.keyframe_insert(data_path="rotation_euler", index=1, frame=frame)
unsupported = armature_object.pose.bones["WBV2P3003Tip"].constraints.new("LIMIT_ROTATION")
unsupported.name = "WBV2P3003UnsupportedLimitRotation"
unsupported.use_limit_x = True
unsupported.min_x = -0.25
unsupported.max_x = 0.25
unsupported.influence = 0.65
bpy.ops.object.mode_set(mode="OBJECT")
root_group = mesh_object.vertex_groups.new(name="WBV2P3003Root")
tip_group = mesh_object.vertex_groups.new(name="WBV2P3003Tip")
root_group.add([0, 1, 2, 3], 1.0, "REPLACE")
root_group.add([4, 5, 6, 7], 0.2, "REPLACE")
tip_group.add([4, 5, 6, 7], 0.8, "REPLACE")
tip_group.add([0, 1, 2, 3], 0.0, "REPLACE")
armature_modifier = mesh_object.modifiers.new(name="WBV2P3003ArmatureDeform", type="ARMATURE")
armature_modifier.object = armature_object
bounded = mesh_object.constraints.new("LIMIT_LOCATION")
bounded.name = "WBV2P3003BoundedObjectConstraint"
bounded.use_min_x = True
bounded.use_max_x = True
bounded.min_x = -2.0
bounded.max_x = 2.0
bounded.influence = 0.8
scene.frame_set(1)
scene.world = bpy.data.worlds.new("WBV2P3003RigWorld")
scene.world.color = (0.03, 0.04, 0.05)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P3-003.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P3004GLBRoundTripScene"
scene.frame_start = 1
scene.frame_end = 20
scene.render.fps = 24
mesh = bpy.data.meshes.new("WBV2P3004RoundTripMesh")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (1, 1, 0), (-1, 1, 0),
(-1, -1, 2), (1, -1, 2), (1, 1, 2), (-1, 1, 2)],
[],
[(0, 1, 2), (0, 2, 3), (4, 6, 5), (4, 7, 6),
(0, 4, 5), (0, 5, 1), (1, 5, 6), (1, 6, 2),
(2, 6, 7), (2, 7, 3), (4, 0, 3), (4, 3, 7)],
)
mesh.update()
uv = mesh.uv_layers.new(name="WBV2P3004UV")
for loop_index, loop in enumerate(mesh.loops):
uv.data[loop_index].uv = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))[loop.vertex_index % 4]
mesh_object = bpy.data.objects.new("WBV2P3004RoundTripMeshObject", mesh)
bpy.context.collection.objects.link(mesh_object)
mesh_object.shape_key_add(name="Basis")
morph = mesh_object.shape_key_add(name="WBV2P3004Morph")
morph.data[2].co.z += 0.3
morph.data[6].co.z += 0.3
morph.value = 0.5
material = bpy.data.materials.new("WBV2P3004PBRMaterial")
material.use_nodes = True
material.diffuse_color = (0.22, 0.48, 0.82, 1.0)
principled = material.node_tree.nodes.get("Principled BSDF")
principled.inputs["Base Color"].default_value = (0.22, 0.48, 0.82, 1.0)
principled.inputs["Metallic"].default_value = 0.35
principled.inputs["Roughness"].default_value = 0.42
principled.inputs["Emission Color"].default_value = (0.0, 0.0, 0.0, 1.0)
principled.inputs["Emission Strength"].default_value = 1.0
image = bpy.data.images.load(str(pathlib.Path("tests/files/web/resources/udim_1001.png").resolve()), check_existing=False)
image.name = "WBV2P3004PackedTexture"
image.pack()
texture = material.node_tree.nodes.new("ShaderNodeTexImage")
texture.name = "WBV2P3004PackedTextureNode"
texture.image = image
material.node_tree.links.new(texture.outputs["Color"], principled.inputs["Base Color"])
mesh.materials.append(material)
armature_data = bpy.data.armatures.new("WBV2P3004Armature")
armature_object = bpy.data.objects.new("WBV2P3004ArmatureObject", armature_data)
bpy.context.collection.objects.link(armature_object)
bpy.context.view_layer.objects.active = armature_object
armature_object.select_set(True)
bpy.ops.object.mode_set(mode="EDIT")
root = armature_data.edit_bones.new("WBV2P3004Root")
root.head = (0, 0, 0)
root.tail = (0, 0, 1)
tip = armature_data.edit_bones.new("WBV2P3004Tip")
tip.head = (0, 0, 1)
tip.tail = (0, 0, 2)
tip.parent = root
tip.use_connect = True
bpy.ops.object.mode_set(mode="OBJECT")
root_group = mesh_object.vertex_groups.new(name="WBV2P3004Root")
tip_group = mesh_object.vertex_groups.new(name="WBV2P3004Tip")
root_group.add([0, 1, 2, 3], 1.0, "REPLACE")
root_group.add([4, 5, 6, 7], 0.25, "REPLACE")
tip_group.add([4, 5, 6, 7], 0.75, "REPLACE")
modifier = mesh_object.modifiers.new(name="WBV2P3004ArmatureDeform", type="ARMATURE")
modifier.object = armature_object
scene.frame_set(1)
mesh_object.location = (0, 0, 0)
mesh_object.keyframe_insert(data_path="location", frame=1)
mesh_object.location = (1.25, -0.5, 0.25)
mesh_object.keyframe_insert(data_path="location", frame=10)
mesh_object.location = (2.0, 0.0, 0.5)
mesh_object.keyframe_insert(data_path="location", frame=20)
if mesh_object.animation_data and mesh_object.animation_data.action:
mesh_object.animation_data.action.name = "WBV2P3004RoundTripAction"
scene.frame_set(1)
scene.world = bpy.data.worlds.new("WBV2P3004GLBWorld")
scene.world.color = (0.02, 0.03, 0.05)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P3-004.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
LIBRARY_NAME = "WBV2P3005Library.blend"
LINKED_OBJECT = "WBV2P3005LinkedObject"
APPEND_OBJECT = "WBV2P3005AppendObject"
def reset() -> None:
bpy.ops.wm.read_factory_settings(use_empty=True)
def mesh_object(name: str, mesh_name: str, location: tuple[float, float, float]) -> bpy.types.Object:
mesh = bpy.data.meshes.new(mesh_name)
mesh.from_pydata(
[(-0.75, -0.75, 0.0), (0.75, -0.75, 0.0), (0.75, 0.75, 0.0), (-0.75, 0.75, 0.0)],
[],
[(0, 1, 2), (0, 2, 3)],
)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
obj.location = location
bpy.context.collection.objects.link(obj)
return obj
def create_source(path: pathlib.Path) -> None:
reset()
linked = mesh_object(LINKED_OBJECT, "WBV2P3005LinkedMesh", (-1.5, 0.0, 0.0))
linked_material = bpy.data.materials.new("WBV2P3005LinkedMaterial")
linked_material.diffuse_color = (0.18, 0.38, 0.62, 1.0)
linked.data.materials.append(linked_material)
appended = mesh_object(APPEND_OBJECT, "WBV2P3005AppendMesh", (1.5, 0.0, 0.0))
material = bpy.data.materials.new("WBV2P3005AppendMaterial")
material.use_nodes = True
principled = material.node_tree.nodes.get("Principled BSDF")
principled.inputs["Base Color"].default_value = (0.76, 0.28, 0.12, 1.0)
principled.inputs["Metallic"].default_value = 0.2
principled.inputs["Roughness"].default_value = 0.5
principled.inputs["Emission Color"].default_value = (0.0, 0.0, 0.0, 1.0)
principled.inputs["Emission Strength"].default_value = 1.0
image_path = pathlib.Path("tests/files/web/resources/udim_1001.png").resolve()
image = bpy.data.images.load(str(image_path), check_existing=False)
image.name = "WBV2P3005AppendImage"
image.pack()
texture = material.node_tree.nodes.new("ShaderNodeTexImage")
texture.name = "WBV2P3005AppendImageNode"
texture.image = image
material.node_tree.links.new(texture.outputs["Color"], principled.inputs["Base Color"])
appended.data.materials.append(material)
bpy.ops.wm.save_as_mainfile(filepath=str(path.resolve()), check_existing=False, compress=True)
def create_fixture(output: pathlib.Path) -> None:
source = output.with_name(LIBRARY_NAME)
create_source(source)
reset()
with bpy.data.libraries.load(str(source.resolve()), link=True) as (data_from, data_to):
if LINKED_OBJECT not in data_from.objects:
raise RuntimeError("linked source object is missing")
data_to.objects = [LINKED_OBJECT]
linked = data_to.objects[0]
if linked is None:
raise RuntimeError("linked object could not be loaded")
bpy.context.collection.objects.link(linked)
bpy.context.view_layer.objects.active = linked
linked.select_set(True)
bpy.ops.wm.save_as_mainfile(filepath=str(output.resolve()), check_existing=False, compress=True)
def main(output: pathlib.Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
create_fixture(output.resolve())
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P3-005.py -- OUTPUT")
main(pathlib.Path(arguments[0]))

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P4001ModifierScene"
mesh = bpy.data.meshes.new("WBV2P4001ModifierMesh")
mesh.from_pydata(
[(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)],
[],
[(0, 1, 2), (0, 2, 3), (4, 6, 5), (4, 7, 6),
(0, 4, 5), (0, 5, 1), (1, 5, 6), (1, 6, 2),
(2, 6, 7), (2, 7, 3), (4, 0, 3), (4, 3, 7)],
)
mesh.update()
obj = bpy.data.objects.new("WBV2P4001ModifierObject", mesh)
bpy.context.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bevel = obj.modifiers.new("WBV2P4001Bevel", "BEVEL")
bevel.width = 0.12
bevel.segments = 2
bevel.profile = 0.5
bevel.show_viewport = True
bevel.show_render = True
decimate = obj.modifiers.new("WBV2P4001Decimate", "DECIMATE")
decimate.ratio = 0.75
decimate.iterations = 1
decimate.show_viewport = True
decimate.show_render = False
bpy.ops.object.particle_system_add()
particle_modifier = obj.modifiers[-1]
particle_modifier.name = "WBV2P4001UnsupportedParticle"
particle_modifier.show_viewport = True
particle_modifier.show_render = False
scene.frame_start = 1
scene.frame_end = 10
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P4-001.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def geometry_group(name, blocked=False):
group = bpy.data.node_groups.new(name, "GeometryNodeTree")
group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
group_input = group.nodes.new("NodeGroupInput")
group_input.name = "WBV2P4002 Group Input"
group_output = group.nodes.new("NodeGroupOutput")
group_output.name = "WBV2P4002 Group Output"
if blocked:
unsupported = group.nodes.new("GeometryNodeDistributePointsOnFaces")
unsupported.name = "WBV2P4002 Unsupported Distribute"
unsupported.distribute_method = "RANDOM"
group.links.new(group_input.outputs["Geometry"], unsupported.inputs["Mesh"])
group.links.new(unsupported.outputs["Points"], group_output.inputs["Geometry"])
else:
transform = group.nodes.new("GeometryNodeTransform")
transform.name = "WBV2P4002 Transform"
transform.inputs["Translation"].default_value = (0.25, -0.5, 0.75)
group.links.new(group_input.outputs["Geometry"], transform.inputs["Geometry"])
group.links.new(transform.outputs["Geometry"], group_output.inputs["Geometry"])
return group
def mesh_object(name, group):
mesh = bpy.data.meshes.new(f"{name}Mesh")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (1, 1, 0), (-1, 1, 0)],
[],
[(0, 1, 2, 3)],
)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(obj)
modifier = obj.modifiers.new("WBV2P4002 Geometry Nodes", "NODES")
modifier.node_group = group
return obj
def material():
value = bpy.data.materials.new("WBV2P4002ShaderMaterial")
value.use_nodes = True
tree = value.node_tree
nodes = tree.nodes
links = tree.links
principled = nodes.get("Principled BSDF")
output = nodes.get("Material Output")
if principled is None or output is None:
raise RuntimeError("default Principled shader nodes are missing")
rgb = nodes.new("ShaderNodeRGB")
rgb.name = "WBV2P4002 RGB"
rgb.outputs["Color"].default_value = (0.12, 0.34, 0.56, 1.0)
links.new(rgb.outputs["Color"], principled.inputs["Base Color"])
links.new(principled.outputs["BSDF"], output.inputs["Surface"])
return value
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P4002NodesScene"
scene.frame_start = 1
scene.frame_end = 12
scene.frame_set(1)
supported_group = geometry_group("WBV2P4002SupportedGraph")
blocked_group = geometry_group("WBV2P4002BlockedGraph", blocked=True)
supported_object = mesh_object("WBV2P4002SupportedObject", supported_group)
blocked_object = mesh_object("WBV2P4002BlockedObject", blocked_group)
shader = material()
supported_object.data.materials.append(shader)
bpy.context.view_layer.objects.active = supported_object
supported_object.select_set(True)
blocked_object.select_set(False)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P4-002.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def sculpt_mesh():
mesh = bpy.data.meshes.new("WBV2P4003SculptMesh")
mesh.from_pydata(
[(-1, -1, 0), (1, -1, 0), (1, 1, 0), (-1, 1, 0), (0, 0, 1)],
[],
[(0, 1, 2, 3), (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4)],
)
mesh.update()
mask = mesh.attributes.new(".sculpt_mask", "FLOAT", "POINT")
for index, item in enumerate(mask.data):
item.value = 0.0 if index == 0 else 0.25 * index
face_sets = mesh.attributes.new(".sculpt_face_set", "INT", "FACE")
for index, item in enumerate(face_sets.data):
item.value = index + 1
obj = bpy.data.objects.new("WBV2P4003SculptObject", mesh)
bpy.context.scene.collection.objects.link(obj)
return obj
def curve_object():
data = bpy.data.curves.new("WBV2P4003Curve", "CURVE")
data.dimensions = "3D"
data.resolution_u = 4
spline = data.splines.new("BEZIER")
spline.bezier_points.add(2)
for point, co in zip(spline.bezier_points, ((-1.5, 2, 0), (0, 2.5, 0.75), (1.5, 2, 0))):
point.co = co
point.handle_left_type = "AUTO"
point.handle_right_type = "AUTO"
obj = bpy.data.objects.new("WBV2P4003CurveObject", data)
bpy.context.scene.collection.objects.link(obj)
return obj
def point_cloud_object():
seed = bpy.data.meshes.new("WBV2P4003PointSeed")
seed.from_pydata([(-1, -2, 0), (0, -2, 0.5), (1, -2, 0)], [], [])
obj = bpy.data.objects.new("WBV2P4003PointCloudObject", seed)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.convert(target="POINTCLOUD")
obj.data.name = "WBV2P4003PointCloud"
radius = obj.data.attributes.new("radius", "FLOAT", "POINT")
for index, item in enumerate(radius.data):
item.value = 0.05 + index * 0.01
return obj
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P4003SculptNonmeshScene"
scene.frame_start = 1
scene.frame_end = 8
scene.frame_set(1)
sculpt_mesh()
curve_object()
point_cloud_object()
bpy.ops.object.select_all(action="DESELECT")
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P4-003.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python3
import pathlib
import sys
import bpy
def cube(name, location):
bpy.ops.mesh.primitive_cube_add(size=2, location=location)
obj = bpy.context.object
obj.name = name
obj.data.name = f"{name}Mesh"
return obj
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P4004SimulationScene"
scene.frame_start = 1
scene.frame_end = 4
scene.frame_set(1)
floor = cube("WBV2P4004Collision", (0, 0, -1.5))
bpy.context.view_layer.objects.active = floor
bpy.ops.rigidbody.object_add()
floor.rigid_body.type = "PASSIVE"
floor.rigid_body.collision_shape = "BOX"
body = cube("WBV2P4004RigidBody", (0, 0, 1.0))
bpy.context.view_layer.objects.active = body
bpy.ops.rigidbody.object_add()
body.rigid_body.type = "ACTIVE"
body.rigid_body.mass = 2.5
body.rigid_body.friction = 0.35
body.rigid_body.restitution = 0.2
body.rigid_body.linear_damping = 0.04
body.rigid_body.angular_damping = 0.1
body.rigid_body.collision_shape = "BOX"
bpy.context.scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P4-004.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
from mathutils import Vector
WASM_SHA256 = "25bc1e286fd3b72c2c7c082b85a29b2d0bb109588a6dc0de5be94fcdc37cc88c"
def digest(value):
if isinstance(value, bytes):
payload = value
else:
payload = value.encode("utf-8")
return hashlib.sha256(payload).hexdigest()
def settings():
return {
"backend": "WEBGL2",
"complexity": "BOUNDED",
"renderEngine": "BLENDER_EEVEE",
"resolutionPercentage": 100,
"resolutionX": 64,
"resolutionY": 64,
"samples": 1,
}
def route(name, target, status, capability, reason, code=None):
result = {
"name": name,
"target": target,
"status": status,
"capability": capability,
"reason": reason,
}
if code:
result["code"] = code
return result
def receipt():
render_settings = settings()
return {
"schemaVersion": 1,
"receiptId": "workflow:render_capability_routing",
"source": {
"kind": "blend",
"identifier": "WBV2-P5-001-RENDER.ROUTING",
"sha256": digest("blend:WBV2-P5-001-RENDER.ROUTING"),
},
"settings": {
**render_settings,
"sha256": digest(json.dumps(render_settings, sort_keys=True, separators=(",", ":"))),
},
"build": {"version": "5.2.0", "buildSha256": WASM_SHA256},
"routes": [
route("boundedEevee", "WEB_LOCAL_BOUNDED", "READY", "WEB_REALTIME_BOUNDED", "BOUNDED_EEVEE"),
route("cycles", "SERVER_JOB", "BLOCKED", "CYCLES_SERVER_RENDER", "CYCLES_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE"),
route("complexEevee", "SERVER_JOB", "BLOCKED", "COMPLEX_EEVEE_SERVER_RENDER", "COMPLEX_EEVEE_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE"),
route("hardware", "SERVER_JOB", "BLOCKED", "HARDWARE_SERVER_RENDER", "HARDWARE_BACKEND_REQUIRES_SERVER", "SERVER_JOB_UNAVAILABLE"),
route("webgpu", "WEB_LOCAL_BOUNDED", "BLOCKED", "WEB_REALTIME_BOUNDED", "WEBGPU_UNAVAILABLE", "WEBGPU_RENDERER_UNAVAILABLE"),
route("unknownEngine", "SERVER_JOB", "BLOCKED", "UNSUPPORTED_RENDER_ENGINE", "UNSUPPORTED_ENGINE", "PLATFORM_CAPABILITY_UNAVAILABLE"),
],
"recovery": {
"cancellation": "OPEN_CANCELLED",
"deviceLoss": "RELEASED",
"temporaryResourcesAfter": 0,
"publishedResults": 0,
},
}
def make_fixture(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P5001RenderRoutingScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 64
scene.render.resolution_y = 64
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = "PNG"
scene.render.image_settings.color_mode = "RGBA"
scene.render.image_settings.color_depth = "8"
scene.render.film_transparent = False
scene.render.dither_intensity = 0
scene.eevee.taa_render_samples = 1
scene.view_settings.look = "None"
scene.view_settings.exposure = 0
scene.view_settings.gamma = 1
world = bpy.data.worlds.new("WBV2P5001RenderWorld")
world.use_nodes = True
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.03, 0.04, 0.06, 1)
world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5
scene.world = world
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P5001RenderCube"
cube.data.name = "WBV2P5001RenderMesh"
material = bpy.data.materials.new("WBV2P5001RenderMaterial")
material.use_nodes = True
principled = material.node_tree.nodes.get("Principled BSDF")
principled.inputs["Base Color"].default_value = (0.12, 0.42, 0.8, 1)
principled.inputs["Roughness"].default_value = 0.6
cube.data.materials.append(material)
camera_data = bpy.data.cameras.new("WBV2P5001RenderCamera")
camera = bpy.data.objects.new("WBV2P5001RenderCamera", camera_data)
scene.collection.objects.link(camera)
camera.location = (4.5, -4.5, 3.2)
camera.rotation_euler = (Vector((0, 0, 0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
camera_data.lens = 52
scene.camera = camera
text = bpy.data.texts.new("WBV2-P5-001-RenderRoutingReceipt")
text.write(json.dumps(receipt(), sort_keys=True, separators=(",", ":")))
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P5-001.py -- OUTPUT")
make_fixture(arguments[0])

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import pathlib
import sys
import wave
import bpy
def write_silence(path):
path.parent.mkdir(parents=True, exist_ok=True)
with wave.open(str(path), "wb") as stream:
stream.setnchannels(1)
stream.setsampwidth(2)
stream.setframerate(8000)
stream.writeframes(b"\0\0" * 8000)
def main(output_path):
output = pathlib.Path(output_path).resolve()
media = output.parent / "media"
audio_path = media / "WBV2P5002-silence.wav"
image_path = media / "WBV2P5002-frame.png"
write_silence(audio_path)
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("WBV2P5002Frame", width=8, height=8, alpha=True)
image.generated_color = (0.12, 0.35, 0.8, 1.0)
image.filepath_raw = str(image_path)
image.file_format = "PNG"
image.save()
bpy.data.images.remove(image)
scene = bpy.context.scene
scene.name = "WBV2P5002MediaScene"
scene.frame_start = 1
scene.frame_end = 48
scene.frame_set(1)
scene.render.fps = 24
scene.render.fps_base = 1.0
tree = bpy.data.node_groups.new("WBV2P5002CompositorTree", "CompositorNodeTree")
scene.compositing_node_group = tree
tree.interface.new_socket(name="Image", in_out="OUTPUT", socket_type="NodeSocketColor")
color = tree.nodes.new("CompositorNodeRGB")
color.name = "WBV2P5002Constant"
color.outputs["Color"].default_value = (0.1, 0.2, 0.4, 1.0)
exposure = tree.nodes.new("CompositorNodeExposure")
exposure.name = "WBV2P5002Exposure"
exposure.inputs["Exposure"].default_value = 0.5
invert = tree.nodes.new("CompositorNodeInvert")
invert.name = "WBV2P5002Invert"
composite = tree.nodes.new("NodeGroupOutput")
composite.name = "WBV2P5002Composite"
tree.links.new(color.outputs["Color"], exposure.inputs["Image"])
tree.links.new(exposure.outputs["Image"], invert.inputs["Color"])
tree.links.new(invert.outputs["Color"], composite.inputs["Image"])
editor = scene.sequence_editor_create()
sound = editor.strips.new_sound("WBV2P5002Sound", str(audio_path), channel=1, frame_start=1)
sound.sound.filepath = "//media/WBV2P5002-silence.wav"
sound.mute = True
image_strip = editor.strips.new_image("WBV2P5002Image", str(image_path), channel=2, frame_start=1)
image_strip.directory = "//media/"
image_strip.elements[0].filename = "WBV2P5002-frame.png"
image_strip.frame_final_duration = 24
second = editor.strips.new_image("WBV2P5002ImageB", str(image_path), channel=3, frame_start=1)
second.directory = "//media/"
second.elements[0].filename = "WBV2P5002-frame.png"
second.frame_final_duration = 24
cross = editor.strips.new_effect("WBV2P5002Cross", "CROSS", channel=4, frame_start=1, length=24, input1=image_strip, input2=second)
cross.mute = True
output.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(output), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P5-002.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
WASM_SHA256 = "25bc1e286fd3b72c2c7c082b85a29b2d0bb109588a6dc0de5be94fcdc37cc88c"
def digest(value):
return hashlib.sha256(value.encode("utf-8") if isinstance(value, str) else value).hexdigest()
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P5003ServerJobScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = "PNG"
scene.render.image_settings.color_mode = "RGBA"
scene.render.image_settings.color_depth = "8"
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
cube = bpy.context.object
cube.name = "WBV2P5003ServerCube"
cube.data.name = "WBV2P5003ServerMesh"
material = bpy.data.materials.new("WBV2P5003ServerMaterial")
material.diffuse_color = (0.2, 0.55, 0.9, 1)
cube.data.materials.append(material)
settings = {"renderEngine": "BLENDER_EEVEE", "frameStart": 1, "frameEnd": 1, "resolutionX": 32, "resolutionY": 32, "resolutionPercentage": 100, "samples": 1, "outputMime": "image/png", "transparent": False}
request_hash = digest("request:WBV2-P5-003")
result_hash = digest("result:WBV2-P5-003")
receipt = {
"schemaVersion": 1,
"receiptId": "workflow:server_job_result_binding",
"source": {"kind": "blend", "identifier": "WBV2-P5-003-SERVER.JOB_BINDING", "sha256": digest("source:WBV2-P5-003")},
"settings": {**settings, "settingsSha256": digest(json.dumps(settings, sort_keys=True, separators=(",", ":")))},
"build": {"version": "5.2.0", "buildSha256": WASM_SHA256},
"request": {"jobId": "wbv2-p5-003", "sourceRevision": 1, "requestSha256": request_hash},
"result": {"status": "SUCCEEDED", "outputMime": "image/png", "outputByteLength": 512, "outputSha256": digest("output:WBV2-P5-003"), "resultSha256": result_hash},
"lifecycle": {"duplicate": "REUSED", "stale": "BLOCKED", "cancel": "CANCELLED", "timeout": "TIMEOUT", "oom": "OOM", "publishedOnFailure": False},
}
text = bpy.data.texts.new("WBV2-P5-003-ServerJobBindingReceipt")
text.write(json.dumps(receipt, sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P5-003.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def digest(value):
payload = value.encode("utf-8") if isinstance(value, str) else value
return hashlib.sha256(payload).hexdigest()
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P6001ScriptingSecurityScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
source = "import os\nimport socket\nopen('../escape.txt', 'w')\nprint('secret-token')"
malicious = bpy.data.texts.new("WBV2-P6-001-MaliciousScript")
malicious.write(source)
malicious.use_module = True
policy = {
"schemaVersion": 1,
"policyId": "workflow:scripting_default_deny_audit",
"defaults": {
"autorun": False,
"network": False,
"addonInstall": False,
"driverExpressions": False,
"module": False,
},
"scope": {
"dom": False,
"hostWorker": False,
"opfs": False,
"indexedDB": False,
"network": False,
},
"sandbox": {
"execution": "DISABLED",
"cancellation": "SCRIPT_SANDBOX_CANCELLED",
"dispose": "RELEASED",
"mainRevisionUnchanged": True,
"publishedResults": 0,
"temporaryBytes": 0,
},
"audit": {
"decision": "DENY",
"approvedKey": False,
"reason": "SCRIPT_SIGNATURE_INVALID",
"requestId": "script-audit:WBV2-P6-001",
},
"boundaries": {
"secretAccess": "BLOCKED",
"filesystemEscape": "BLOCKED",
"network": "BLOCKED",
},
"budget": {
"maxSourceBytes": 1024 * 1024,
"maxScripts": 64,
"maxAuditEntries": 256,
},
}
policy["policyHash"] = digest(json.dumps(policy, sort_keys=True, separators=(",", ":")))
receipt = bpy.data.texts.new("WBV2-P6-001-ScriptingPolicyReceipt")
receipt.write(json.dumps(policy, sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P6-001.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
def digest(value):
payload = value.encode("utf-8") if isinstance(value, str) else value
return hashlib.sha256(payload).hexdigest()
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P6002PerformanceFaultScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
bpy.ops.mesh.primitive_cube_add(size=2)
cube = bpy.context.object
cube.name = "WBV2P6002RecoveryCube"
cube.data.name = "WBV2P6002RecoveryMesh"
thresholds = [
{"id": "geometry-100k", "domain": "geometry", "limit": 100000, "unit": "triangles", "status": "PASS"},
{"id": "geometry-1m", "domain": "geometry", "limit": 1000000, "unit": "triangles", "status": "PASS"},
{"id": "geometry-10m", "domain": "geometry", "limit": 10000000, "unit": "triangles", "status": "PASS"},
{"id": "texture-4k", "domain": "texture", "limit": 4096, "unit": "pixels", "status": "PASS"},
{"id": "texture-8k", "domain": "texture", "limit": 8192, "unit": "pixels", "status": "PASS"},
{"id": "long-media", "domain": "media", "limit": 600, "unit": "seconds", "status": "PASS"},
{"id": "simulation-cache", "domain": "simulation", "limit": 17179869184, "unit": "bytes", "status": "PASS"},
]
faults = {
"oom": {"status": "BLOCKED", "code": "WASM_OUT_OF_MEMORY", "publishedResults": 0},
"quota": {"status": "BLOCKED", "code": "STORAGE_QUOTA", "publishedResults": 0},
"deviceLoss": {"status": "BLOCKED", "code": "GPU_DEVICE_LOSS_BLOCKED", "publishedResults": 0},
"networkInterrupt": {"status": "BLOCKED", "code": "NETWORK_INTERRUPT_BLOCKED", "publishedResults": 0},
}
matrix = {
"schemaVersion": 1,
"receiptId": "workflow:performance_fault_recovery_matrix",
"thresholds": thresholds,
"faults": faults,
"recovery": {"oldRevisionUsable": True, "revisionBefore": 1, "revisionAfter": 1, "resourcesAfter": 0, "cache": "QUARANTINED", "workerRestart": "RECOVERED"},
"budget": {"maxGeometryTriangles": 10000000, "maxTextureBytes": 536870912, "maxMediaBytes": 536870912, "maxCacheBytes": 17179869184},
}
matrix["matrixHash"] = digest(json.dumps(matrix, sort_keys=True, separators=(",", ":")))
text = bpy.data.texts.new("WBV2-P6-002-PerformanceFaultMatrixReceipt")
text.write(json.dumps(matrix, sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P6-002.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
ROOT = pathlib.Path(__file__).resolve().parents[3]
def digest_file(relative):
return hashlib.sha256((ROOT / relative).read_bytes()).hexdigest()
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P6003OfflineReleaseScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
bpy.ops.mesh.primitive_cube_add(size=2)
cube = bpy.context.object
cube.name = "WBV2P6003ReleaseReceiptCube"
cube.data.name = "WBV2P6003ReleaseReceiptMesh"
package = json.loads((ROOT / "web/package.json").read_text(encoding="utf-8"))
engine = json.loads((ROOT / "web/app/public/engine-manifest.json").read_text(encoding="utf-8"))
quick = json.loads((ROOT / "release/ci-reports/quick.json").read_text(encoding="utf-8"))
if quick.get("status") != "READY":
raise RuntimeError("quick CI report must be READY before creating the release receipt")
receipt = {
"schemaVersion": 1,
"receiptId": "workflow:offline_ci_sbom_and_rollback_release",
"releaseId": engine["releaseId"],
"ci": {
"quick": {"status": quick["status"], "reportSha256": digest_file("release/ci-reports/quick.json")},
},
"gates": {
"offlineReproducibility": {"status": "READY", "binarySha256": digest_file("release/blender-web-offline.tar.gz"), "sourceSha256": digest_file("release/blender-web-corresponding-source.tar.gz")},
"sbom": {"status": "READY", "sha256": digest_file("docs/web/sbom.spdx.json")},
"rollback": {"status": "READY", "reportSha256": digest_file("release/operations-reports/rollback.json")},
},
"archives": {
"binary": {"path": "release/blender-web-offline.tar.gz", "bytes": (ROOT / "release/blender-web-offline.tar.gz").stat().st_size, "sha256": digest_file("release/blender-web-offline.tar.gz")},
"source": {"path": "release/blender-web-corresponding-source.tar.gz", "bytes": (ROOT / "release/blender-web-corresponding-source.tar.gz").stat().st_size, "sha256": digest_file("release/blender-web-corresponding-source.tar.gz")},
},
"sbom": {"path": "docs/web/sbom.spdx.json", "sha256": digest_file("docs/web/sbom.spdx.json")},
"rollback": {"runbookPath": "docs/web/RELEASE_RECOVERY.md", "runbookSha256": digest_file("docs/web/RELEASE_RECOVERY.md"), "reportPath": "release/operations-reports/rollback.json"},
"bindings": {"packageJsonSha256": digest_file("web/package.json"), "packageLockSha256": digest_file("web/package-lock.json"), "rcManifestSha256": digest_file("release/RC_MANIFEST.json"), "version": package["version"]},
}
text = bpy.data.texts.new("WBV2-P6-003-OfflineCiSbomRollbackReleaseReceipt")
text.write(json.dumps(receipt, sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P6-003.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
ROOT = pathlib.Path(__file__).resolve().parents[3]
PARITY_MAP = ROOT / "tests/golden/WBV2-P0-002/parity-map.json"
def digest_file(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def risk_score(capability):
levels = {"L0": 0, "L1": 1, "L2": 2, "L3": 3}
return (capability["legacyPending"] * 100 + capability["legacyActive"] * 50 +
capability["entryCount"] * 10 + levels[capability["targetParityLevel"]] * 1000)
def build_receipt():
parity = json.loads(PARITY_MAP.read_text(encoding="utf-8"))
if parity.get("schemaVersion") != 1 or parity.get("operation") != "BLENDER_52_CAPABILITY_PARITY_MAP":
raise RuntimeError("P0-002 parity map schema is not supported")
if parity.get("summary", {}).get("featureParityClaims") != 0 or parity["summary"].get("unknownEntries") != 0:
raise RuntimeError("P0-002 parity map contains an unsafe claim")
entries_by_family = {}
for entry in parity["entries"]:
entries_by_family.setdefault(entry["family"], []).append(entry)
clusters = []
for capability in sorted(parity["capabilities"], key=lambda value: value["capabilityId"]):
members = sorted(entries_by_family.get(capability["family"], []), key=lambda value: value["id"])
score = risk_score(capability)
band = "CRITICAL" if score >= 300000 else "HIGH" if score >= 50000 else "MEDIUM" if score >= 10000 else "LOW"
clusters.append({
"capabilityId": capability["capabilityId"],
"family": capability["family"],
"layer": capability["layer"],
"entryCount": capability["entryCount"],
"legacyCompleted": capability["legacyCompleted"],
"legacyPending": capability["legacyPending"],
"legacyActive": capability["legacyActive"],
"targetParityLevel": capability["targetParityLevel"],
"currentParityLevel": "L0",
"completionClass": "METADATA_ONLY",
"evidenceStatus": "NOT_RUN",
"webStatus": "GAP",
"riskScore": score,
"riskBand": band,
"representativeIds": [entry["id"] for entry in members[:5]],
"blockedReasons": [
"no_desktop_positive_and_wasm_mutation_evidence",
"save_reopen_parity_not_run",
],
})
summary = {
"inventoryEntries": parity["summary"]["inventoryEntries"],
"clusterCount": len(clusters),
"unknownEntries": parity["summary"]["unknownEntries"],
"completedLegacyEntries": parity["summary"]["completedLegacyEntries"],
"pendingLegacyEntries": parity["summary"]["pendingLegacyEntries"],
"activeLegacyEntries": parity["summary"]["activeLegacyEntries"],
"webGapEntries": sum(cluster["entryCount"] for cluster in clusters if cluster["currentParityLevel"] == "L0"),
"currentParityClaims": 0,
"currentParityLevel": "L0",
"byLayer": parity["summary"]["byLayer"],
}
risk_ranking = [cluster["capabilityId"] for cluster in sorted(clusters, key=lambda value: (-value["riskScore"], value["capabilityId"]))]
return {
"schemaVersion": 1,
"receiptId": "workflow:cluster_full_blender_gap_inventory",
"runtime": "BLENDER_5_2_INVENTORY",
"inventorySource": {
"path": "tests/golden/WBV2-P0-002/parity-map.json",
"sha256": digest_file(PARITY_MAP),
"entryCount": parity["summary"]["inventoryEntries"],
},
"classificationPolicy": {
"featureParityRequires": ["desktopPositive", "wasmMutation", "saveReopenExact"],
"negativeOnlyClasses": ["NEGATIVE_BOUNDARY", "UNSUPPORTED_DECLARED", "BLOCKED"],
"unprovenClass": "METADATA_ONLY",
"parityLevel": "L0",
},
"clusters": clusters,
"summary": summary,
"riskRanking": risk_ranking,
"nextTask": "WBV2-P7-002",
}
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P7001GapInventoryScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
bpy.ops.mesh.primitive_cube_add(size=2)
cube = bpy.context.object
cube.name = "WBV2P7001GapInventoryCube"
cube.data.name = "WBV2P7001GapInventoryMesh"
receipt = build_receipt()
text = bpy.data.texts.new("WBV2-P7-001-ClusterFullBlenderGapInventoryReceipt")
text.write(json.dumps(receipt, sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P7-001.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
ROOT = pathlib.Path(__file__).resolve().parents[3]
PARENT_MANIFEST = ROOT / "tests/golden/WBV2-P7-001/manifest.json"
PARENT_REPORT = ROOT / "tests/golden/WBV2-P7-001/gap-inventory-desktop-report.json"
BEHAVIORS = ("desktopPositive", "wasmMutation", "saveReopenExact")
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def task_id(capability_id, behavior):
return "WBV2-P7-002-" + capability_id.removeprefix("CAP-") + "-" + behavior.upper()
def build_receipt():
parent_manifest = json.loads(PARENT_MANIFEST.read_text(encoding="utf-8"))
parent_report = json.loads(PARENT_REPORT.read_text(encoding="utf-8"))
inventory = parent_report["state"]["receipt"]
if inventory.get("receiptId") != "workflow:cluster_full_blender_gap_inventory":
raise RuntimeError("P7-001 parent receipt is missing")
tasks = []
for cluster in sorted(inventory["clusters"], key=lambda value: value["capabilityId"]):
for behavior in BEHAVIORS:
tasks.append({
"id": task_id(cluster["capabilityId"], behavior),
"parityId": cluster["capabilityId"],
"capabilityId": cluster["capabilityId"],
"family": cluster["family"],
"behavior": behavior,
"implementationClass": "LOCAL_BOUNDED",
"parentTask": "WBV2-P7-001",
"parentReceiptSha256": digest(PARENT_REPORT),
"state": "active" if not tasks else "pending",
"ready": not tasks,
})
return {
"schemaVersion": 1,
"receiptId": "workflow:generate_atomic_family_gap_queue",
"runtime": "BLENDER_5_2_INVENTORY",
"parent": {
"task": "WBV2-P7-001",
"manifestPath": "tests/golden/WBV2-P7-001/manifest.json",
"manifestSha256": digest(PARENT_MANIFEST),
"receiptPath": "tests/golden/WBV2-P7-001/gap-inventory-desktop-report.json",
"receiptSha256": digest(PARENT_REPORT),
},
"queue": {
"activeTaskId": tasks[0]["id"],
"activeCount": 1,
"plannedCount": len(tasks) - 1,
"ordering": "capabilityId_asc_then_behavior_order",
"behaviors": list(BEHAVIORS),
"implementationClasses": ["LOCAL_BOUNDED"],
},
"tasks": tasks,
"nextTask": "WBV2-P7-003",
}
def main(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P7002AtomicGapQueueScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
bpy.ops.mesh.primitive_cube_add(size=2)
cube = bpy.context.object
cube.name = "WBV2P7002AtomicGapQueueCube"
cube.data.name = "WBV2P7002AtomicGapQueueMesh"
text = bpy.data.texts.new("WBV2-P7-002-AtomicFamilyGapQueueReceipt")
text.write(json.dumps(build_receipt(), sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P7-002.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
import hashlib
import json
import pathlib
import sys
import bpy
ROOT = pathlib.Path(__file__).resolve().parents[3]
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def build_receipt():
inventory = json.loads((ROOT / "tests/golden/WBV2-P7-001/gap-inventory-report.json").read_text(encoding="utf-8"))
queue = json.loads((ROOT / "tests/golden/WBV2-P7-002/atomic-queue-report.json").read_text(encoding="utf-8"))
quick = json.loads((ROOT / "release/ci-reports/quick.json").read_text(encoding="utf-8"))
drift = json.loads((ROOT / "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json").read_text(encoding="utf-8"))
if quick.get("status") != "READY" or inventory["wasm"]["status"] != "EXACT" or queue["wasm"]["status"] != "EXACT":
raise RuntimeError("release gate inputs are not ready")
return {
"schemaVersion": 1,
"receiptId": "workflow:full_parity_candidate_release_gate",
"runtime": "BLENDER_DESKTOP_WASM_MAIN",
"inputs": {
"gapInventoryReport": {"path": "tests/golden/WBV2-P7-001/gap-inventory-report.json", "sha256": sha(ROOT / "tests/golden/WBV2-P7-001/gap-inventory-report.json")},
"atomicQueueReport": {"path": "tests/golden/WBV2-P7-002/atomic-queue-report.json", "sha256": sha(ROOT / "tests/golden/WBV2-P7-002/atomic-queue-report.json")},
"quickCiReport": {"path": "release/ci-reports/quick.json", "sha256": sha(ROOT / "release/ci-reports/quick.json")},
"m12DriftEvidence": {"path": "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json", "sha256": sha(ROOT / "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json")},
},
"parityClosure": {
"status": "BLOCKED",
"inventoryEntries": inventory["desktop"]["entries"],
"clusterCount": inventory["desktop"]["clusters"],
"atomicTaskCount": queue["desktop"]["tasks"],
"currentParityLevel": "L0",
"featureParityClaims": 0,
"reasonCodes": ["ATOMIC_QUEUE_PENDING", "NO_WASM_MUTATION_EVIDENCE"],
},
"evidenceHealth": {
"status": "BLOCKED",
"historicalEvidencePreserved": drift.get("historicalEvidencePreserved") is True,
"m12ProvenanceState": drift.get("provenanceState"),
"m12Entries": len(drift.get("entries", [])),
"reasonCodes": ["M12_SOURCE_LOST_SHA_DRIFT"],
},
"recoveryHealth": {
"status": "READY",
"quickCi": quick["status"],
"inventorySaveReopen": inventory["desktop"]["saveReopen"],
"queueSaveReopen": queue["desktop"]["saveReopen"],
"malformedBlend": "REJECTED_WITHOUT_MAIN_MUTATION",
},
"releaseDecision": {
"status": "BLOCKED",
"reasonCodes": ["PARITY_CLOSURE_BLOCKED", "HISTORICAL_EVIDENCE_PROVENANCE_BLOCKED"],
"nextTask": None,
},
}
def main(output):
output_path = pathlib.Path(output).resolve()
# Blender serializes process-local pointers into .blend files; never
# overwrite an existing evidence fixture during a verification rerun.
if output_path.exists():
if output_path.stat().st_size == 0:
raise RuntimeError(f"existing fixture is empty: {output_path}")
print(f"fixture-preserved path={output_path} bytes={output_path.stat().st_size} sha256={sha(output_path)}")
return
bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
scene.name = "WBV2P7003ParityReleaseGateScene"
scene.render.engine = "BLENDER_EEVEE"
scene.render.resolution_x = 32
scene.render.resolution_y = 32
scene.render.resolution_percentage = 100
bpy.ops.mesh.primitive_cube_add(size=2)
cube = bpy.context.object
cube.name = "WBV2P7003ParityReleaseGateCube"
cube.data.name = "WBV2P7003ParityReleaseGateMesh"
text = bpy.data.texts.new("WBV2-P7-003-FullParityCandidateReleaseGateReceipt")
text.write(json.dumps(build_receipt(), sort_keys=True, separators=(",", ":")))
scene.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=str(output_path), check_existing=False, compress=True)
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1 :]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --factory-startup --python WBV2-P7-003.py -- OUTPUT")
main(arguments[0])

View File

@@ -0,0 +1,108 @@
import assert from "node:assert/strict";
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 outputPath = path.join(root, "tests/golden/corrective/rehydration/M12-provenance-intake.json");
const driftPath = path.join(root, "tests/golden/corrective/rehydration/M12-manifest-sha-drift.json");
const snapshotArgument = process.argv.indexOf("--snapshot");
const requestedSnapshot = snapshotArgument >= 0 ? process.argv[snapshotArgument + 1] : null;
const write = process.argv.includes("--write");
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const drift = readJson(driftPath);
function emptyReport(reason) {
return {
schemaVersion: 1,
operation: "M12_IMMUTABLE_SNAPSHOT_PROVENANCE_INTAKE",
historicalEvidencePreserved: true,
immutableSourceSnapshotAvailable: false,
provenanceState: "BLOCKED_SOURCE_LOST",
sourceSnapshot: null,
sourceSnapshotManifestSha256: null,
driftEntries: drift.entries.length,
restoredEntries: 0,
remainingEntries: drift.entries.length,
reason,
entries: drift.entries.map((entry) => ({ ...entry, provenanceState: "BLOCKED_SOURCE_LOST" })),
};
}
function resolveSnapshot(value) {
if (!value) return null;
const candidate = path.resolve(root, value);
if (!fs.existsSync(candidate)) return null;
const manifestPath = fs.statSync(candidate).isDirectory() ? path.join(candidate, "snapshot-manifest.json") : candidate;
if (!fs.existsSync(manifestPath)) return null;
const manifest = readJson(manifestPath);
assert.equal(manifest.schemaVersion, 1, "snapshot manifest schema must be 1");
assert.ok(["IMMUTABLE_ARCHIVE", "IMMUTABLE_REPOSITORY", "SIGNED_ARTIFACT"].includes(manifest.mode), "snapshot mode is not immutable");
assert.equal(manifest.authorized, true, "snapshot must be authorized");
assert.ok(Array.isArray(manifest.files), "snapshot manifest files are missing");
const rootPath = path.dirname(manifestPath);
const files = new Map();
for (const entry of manifest.files) {
assert.equal(typeof entry.path, "string");
assert.match(entry.sha256 ?? "", /^[a-f0-9]{64}$/u);
const sourcePath = path.resolve(rootPath, entry.snapshotPath ?? entry.path);
assert.ok(sourcePath === rootPath || sourcePath.startsWith(`${rootPath}${path.sep}`), `snapshot path escapes root: ${entry.path}`);
assert.ok(fs.existsSync(sourcePath), `snapshot file is missing: ${entry.path}`);
assert.equal(sha256(sourcePath), entry.sha256, `snapshot hash drift: ${entry.path}`);
if (entry.bytes !== undefined) assert.equal(fs.statSync(sourcePath).size, entry.bytes, `snapshot byte drift: ${entry.path}`);
const key = `${entry.path}\0${entry.sha256}`;
if (files.has(key)) throw new Error(`duplicate snapshot path/SHA pair: ${entry.path}`);
files.set(key, { ...entry, sourcePath });
}
return { rootPath, manifestPath, manifest, files };
}
let report;
if (!requestedSnapshot) {
report = emptyReport("immutable snapshot was not supplied; no historical source may be inferred from the current worktree");
} else {
const snapshot = resolveSnapshot(requestedSnapshot);
if (!snapshot) {
report = emptyReport(`immutable snapshot is unavailable or invalid: ${requestedSnapshot}`);
} else {
const restored = [];
const remaining = [];
for (const entry of drift.entries) {
const source = snapshot.files.get(`${entry.path}\0${entry.originalSha256}`);
if (source) {
restored.push({ ...entry, snapshotPath: relative(source.sourcePath), provenanceState: "PROVENANCE_RESTORED" });
} else {
remaining.push({ ...entry, snapshotPath: source ? relative(source.sourcePath) : null, provenanceState: "BLOCKED_SOURCE_LOST" });
}
}
report = {
schemaVersion: 1,
operation: "M12_IMMUTABLE_SNAPSHOT_PROVENANCE_INTAKE",
historicalEvidencePreserved: true,
immutableSourceSnapshotAvailable: true,
provenanceState: remaining.length === 0 ? "PROVENANCE_RESTORED" : "BLOCKED_SOURCE_LOST",
sourceSnapshot: relative(snapshot.rootPath),
sourceSnapshotManifestSha256: sha256(snapshot.manifestPath),
snapshotMode: snapshot.manifest.mode,
driftEntries: drift.entries.length,
restoredEntries: restored.length,
remainingEntries: remaining.length,
reason: remaining.length === 0 ? "all observed M12 drift entries match the immutable source snapshot" : "snapshot supplied but some historical hashes remain unresolved",
entries: [...restored, ...remaining].sort((a, b) => `${a.task}:${a.artifact}:${a.path}`.localeCompare(`${b.task}:${b.artifact}:${b.path}`)),
};
}
}
if (write) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
} else {
assert.ok(fs.existsSync(outputPath), "provenance intake report is missing; run with --write");
assert.deepEqual(readJson(outputPath), report, "provenance intake report is stale or source changed");
}
process.stdout.write(`m12-provenance-intake-ok state=${report.provenanceState} drift=${report.driftEntries} restored=${report.restoredEntries} remaining=${report.remainingEntries} snapshot=${report.immutableSourceSnapshotAvailable} write=${write}\n`);

45
tools/web/native-build-env.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# Shared environment for native Blender builds and desktop fixture checkers.
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
export BLENDER_SOURCE_DIR="${BLENDER_SOURCE_DIR:-${repo_root}/blender-5.2.0}"
export BLENDER_BUILD_DIR="${BLENDER_BUILD_DIR:-${repo_root}/build_blender_native}"
export BLENDER_DEPS_ROOT="${BLENDER_DEPS_ROOT:-${repo_root}/.deps/native}"
export BLENDER_PYTHON_ROOT="${BLENDER_PYTHON_ROOT:-${BLENDER_DEPS_ROOT}/python}"
export BLENDER_CMAKE_HELPERS="${repo_root}/tools/web/cmake"
native_lib_dirs=()
if [[ -d "${BLENDER_DEPS_ROOT}" ]]; then
while IFS= read -r directory; do
native_lib_dirs+=("${directory}")
done < <(find "${BLENDER_DEPS_ROOT}" -type d \( -name lib -o -name lib64 \) -print | sort)
fi
if [[ -d "${BLENDER_PYTHON_ROOT}/lib" ]]; then
native_lib_dirs+=("${BLENDER_PYTHON_ROOT}/lib")
fi
for site_packages in \
"${BLENDER_PYTHON_ROOT}/lib/python3.13/site-packages" \
"${BLENDER_BUILD_DIR}/bin/5.2/python/lib/python3.13/site-packages"; do
for private_lib_dir in "${site_packages}"/*.libs; do
if [[ -d "${private_lib_dir}" ]]; then
native_lib_dirs+=("${private_lib_dir}")
fi
done
done
if [[ -n "${LD_LIBRARY_PATH:-}" ]]; then
native_lib_dirs+=("${LD_LIBRARY_PATH}")
fi
if ((${#native_lib_dirs[@]})); then
IFS=:; export LD_LIBRARY_PATH="${native_lib_dirs[*]}"
fi
export PATH="${BLENDER_PYTHON_ROOT}/bin:${PATH}"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
printf 'native-build-env-ok source=%s build=%s deps=%s\n' \
"${BLENDER_SOURCE_DIR}" "${BLENDER_BUILD_DIR}" "${BLENDER_DEPS_ROOT}"
printf 'compiler=%s\n' "$(command -v "${CC:-gcc-14}" || command -v gcc || printf 'missing')"
printf 'cmake=%s ninja=%s\n' "$(command -v cmake || printf 'missing')" "$(command -v ninja || printf 'missing')"
fi

20
tools/web/prepare-build-env.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "${repo_root}/tools/web/native-build-env.sh"
command -v gcc-14 >/dev/null || { echo "missing gcc-14; install GCC 14" >&2; exit 1; }
command -v g++-14 >/dev/null || { echo "missing g++-14; install G++ 14" >&2; exit 1; }
command -v cmake >/dev/null || { echo "missing cmake" >&2; exit 1; }
command -v ninja >/dev/null || { echo "missing ninja" >&2; exit 1; }
if [[ ! -x "${repo_root}/.emsdk/upstream/emscripten/emcc" ]]; then
echo "Emscripten 3.1.69 is not installed in ${repo_root}/.emsdk" >&2
echo "Run: ${repo_root}/tools/web/prepare-emscripten.sh" >&2
exit 2
fi
source "${repo_root}/tools/web/emscripten-env.sh"
emcc --version | head -1
printf 'build-env-ready native=%s wasm=%s\n' "$(${CC:-gcc-14} --version | head -1)" "$(emcc --version | head -1)"

21
tools/web/prepare-emscripten.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
emsdk_dir="${EMSDK_ROOT:-${repo_root}/.emsdk}"
emsdk_url="${EMSDK_URL:-https://gh-proxy.com/https://github.com/emscripten-core/emsdk/archive/refs/heads/main.tar.gz}"
if [[ ! -x "${emsdk_dir}/emsdk" ]]; then
archive="$(mktemp --suffix=.tar.gz)"
trap 'rm -f "${archive}"' EXIT
curl -L --fail --retry 3 --max-time 180 "${emsdk_url}" -o "${archive}"
mkdir -p "${emsdk_dir}"
tar -xzf "${archive}" --strip-components=1 -C "${emsdk_dir}"
fi
cd "${emsdk_dir}"
./emsdk install 3.1.69
./emsdk activate 3.1.69
source ./emsdk_env.sh
emcc --version | head -3
printf 'emscripten-ready root=%s version=%s\n' "${emsdk_dir}" "$(emcc --version | awk 'NR==1 {print $NF}')"

View File

@@ -18,7 +18,7 @@ const laneCommands = {
["lockfile-install", "npm", ["--prefix", "web", "ci", "--ignore-scripts"]],
["typecheck", "npm", ["--prefix", "web", "run", "typecheck"]],
["lint", "npm", ["--prefix", "web", "run", "lint"]],
["unit", "npm", ["--prefix", "web", "test"]],
["unit", "node", ["tools/web/run-unit-tests.mjs"]],
["status", "npm", ["--prefix", "web", "run", "test:status-consistency"]],
["evidence-schema", "npm", ["--prefix", "web", "run", "test:release-evidence"]],
["rc-docs", "npm", ["--prefix", "web", "run", "test:rc-docs"]],
@@ -138,6 +138,9 @@ const records = [];
for (const [index, definition] of commands.entries()) {
const [id, command, args, portMode] = definition;
const env = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
if (id === "unit" && !/(^|\s)--max-old-space-size(?:=|\s)/u.test(env.NODE_OPTIONS ?? "")) {
env.NODE_OPTIONS = `${env.NODE_OPTIONS ?? ""} --max-old-space-size=4096`.trim();
}
let port;
if (portMode === "browser") {
port = await freePortRange();

View File

@@ -0,0 +1,35 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const testsRoot = path.join(root, "web", "tests");
function collect(directory) {
const files = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...collect(file));
else if (entry.isFile() && entry.name.endsWith(".test.mjs")) files.push(file);
}
return files.sort();
}
const tests = collect(testsRoot);
for (const [index, file] of tests.entries()) {
const relative = path.relative(path.join(root, "web"), file).replaceAll(path.sep, "/");
process.stdout.write(`unit-file ${index + 1}/${tests.length} ${relative}\n`);
const result = spawnSync(process.execPath, ["--test", "--test-concurrency=1", relative], {
cwd: path.join(root, "web"),
stdio: "inherit",
env: process.env,
});
if (result.status !== 0) {
process.stderr.write(`unit-file-failed ${relative} exit=${result.status ?? 1}\n`);
process.exitCode = result.status ?? 1;
break;
}
}
if (process.exitCode === undefined) process.stdout.write(`unit-suite-ok files=${tests.length}\n`);

View File

@@ -0,0 +1,17 @@
{
"CORE_DNA_RNA": { "task": "WBV2-P2-001", "report": "tests/golden/WBV2-P2-001/editor-open-scene-report.json" },
"CORE_MESH": { "task": "WBV2-P2-003", "report": "tests/golden/WBV2-P2-003/mesh-edit-report.json" },
"CORE_OBJECT": { "task": "WBV2-P2-002", "report": "tests/golden/WBV2-P2-002/object-transform-report.json" },
"CORE_SCENE_GRAPH": { "task": "WBV2-P2-001", "report": "tests/golden/WBV2-P2-001/editor-open-scene-report.json" },
"MATERIAL_RENDER": { "task": "WBV2-P3-001", "report": "tests/golden/WBV2-P3-001/material-report.json" },
"MEDIA_IO": { "task": "WBV2-P3-004", "report": "tests/golden/WBV2-P3-004/glb-report.json" },
"MESH_EDITING": { "task": "WBV2-P2-003", "report": "tests/golden/WBV2-P2-003/mesh-edit-report.json" },
"MODIFIER_SYSTEM": { "task": "WBV2-P4-001", "report": "tests/golden/WBV2-P4-001/modifier-report.json" },
"NODES_GRAPH": { "task": "WBV2-P4-002", "report": "tests/golden/WBV2-P4-002/nodes-report.json" },
"OBJECT_OPERATIONS": { "task": "WBV2-P2-002", "report": "tests/golden/WBV2-P2-002/object-transform-report.json" },
"OTHER_OPERATOR": { "task": "WBV2-P2-005", "report": "tests/golden/WBV2-P2-005/workflow-report.json" },
"RIG_ANIMATION": { "task": "WBV2-P3-003", "report": "tests/golden/WBV2-P3-003/rig-report.json" },
"SCULPT_PAINT": { "task": "WBV2-P4-003", "report": "tests/golden/WBV2-P4-003/sculpt-report.json" },
"SIMULATION": { "task": "WBV2-P4-004", "report": "tests/golden/WBV2-P4-004/simulation-report.json" },
"UI_EDITOR": { "task": "WBV2-P2-004", "report": "tests/golden/WBV2-P2-004/viewport-report.json" }
}