Rebuild local Blender WASM environment and recovery evidence
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
59
tools/web/build-blender-native.sh
Executable file
59
tools/web/build-blender-native.sh
Executable 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"
|
||||
107
tools/web/build-m12-git-snapshot.mjs
Normal file
107
tools/web/build-m12-git-snapshot.mjs
Normal 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`);
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
171
tools/web/check-wbv2-inventory.mjs
Normal file
171
tools/web/check-wbv2-inventory.mjs
Normal 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`);
|
||||
57
tools/web/check-wbv2-p0-003-desktop.py
Normal file
57
tools/web/check-wbv2-p0-003-desktop.py
Normal 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()
|
||||
56
tools/web/check-wbv2-p1-001-desktop.py
Normal file
56
tools/web/check-wbv2-p1-001-desktop.py
Normal 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()
|
||||
76
tools/web/check-wbv2-p1-002-desktop.py
Normal file
76
tools/web/check-wbv2-p1-002-desktop.py
Normal 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()
|
||||
60
tools/web/check-wbv2-p1-003-desktop.py
Normal file
60
tools/web/check-wbv2-p1-003-desktop.py
Normal 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()
|
||||
71
tools/web/check-wbv2-p1-004-desktop.py
Normal file
71
tools/web/check-wbv2-p1-004-desktop.py
Normal 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()
|
||||
71
tools/web/check-wbv2-p1-005-desktop.py
Normal file
71
tools/web/check-wbv2-p1-005-desktop.py
Normal 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()
|
||||
78
tools/web/check-wbv2-p2-001-desktop.py
Normal file
78
tools/web/check-wbv2-p2-001-desktop.py
Normal 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()
|
||||
88
tools/web/check-wbv2-p2-002-desktop.py
Normal file
88
tools/web/check-wbv2-p2-002-desktop.py
Normal 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()
|
||||
67
tools/web/check-wbv2-p2-003-desktop.py
Normal file
67
tools/web/check-wbv2-p2-003-desktop.py
Normal 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()
|
||||
64
tools/web/check-wbv2-p2-004-desktop.py
Normal file
64
tools/web/check-wbv2-p2-004-desktop.py
Normal 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()
|
||||
59
tools/web/check-wbv2-p2-005-desktop.py
Normal file
59
tools/web/check-wbv2-p2-005-desktop.py
Normal 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()
|
||||
76
tools/web/check-wbv2-p3-001-desktop.py
Normal file
76
tools/web/check-wbv2-p3-001-desktop.py
Normal 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()
|
||||
67
tools/web/check-wbv2-p3-002-desktop.py
Normal file
67
tools/web/check-wbv2-p3-002-desktop.py
Normal 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()
|
||||
120
tools/web/check-wbv2-p3-003-desktop.py
Normal file
120
tools/web/check-wbv2-p3-003-desktop.py
Normal 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()
|
||||
69
tools/web/check-wbv2-p3-004-desktop.py
Normal file
69
tools/web/check-wbv2-p3-004-desktop.py
Normal 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()
|
||||
96
tools/web/check-wbv2-p3-005-desktop.py
Normal file
96
tools/web/check-wbv2-p3-005-desktop.py
Normal 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()
|
||||
50
tools/web/check-wbv2-p4-001-desktop.py
Normal file
50
tools/web/check-wbv2-p4-001-desktop.py
Normal 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()
|
||||
83
tools/web/check-wbv2-p4-002-desktop.py
Normal file
83
tools/web/check-wbv2-p4-002-desktop.py
Normal 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()
|
||||
74
tools/web/check-wbv2-p4-003-desktop.py
Normal file
74
tools/web/check-wbv2-p4-003-desktop.py
Normal 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()
|
||||
45
tools/web/check-wbv2-p4-004-desktop.py
Normal file
45
tools/web/check-wbv2-p4-004-desktop.py
Normal 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()
|
||||
74
tools/web/check-wbv2-p5-001-desktop.py
Normal file
74
tools/web/check-wbv2-p5-001-desktop.py
Normal 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()
|
||||
52
tools/web/check-wbv2-p5-002-desktop.py
Normal file
52
tools/web/check-wbv2-p5-002-desktop.py
Normal 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()
|
||||
42
tools/web/check-wbv2-p5-003-desktop.py
Normal file
42
tools/web/check-wbv2-p5-003-desktop.py
Normal 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()
|
||||
61
tools/web/check-wbv2-p6-001-desktop.py
Normal file
61
tools/web/check-wbv2-p6-001-desktop.py
Normal 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()
|
||||
47
tools/web/check-wbv2-p6-002-desktop.py
Normal file
47
tools/web/check-wbv2-p6-002-desktop.py
Normal 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()
|
||||
45
tools/web/check-wbv2-p6-003-desktop.py
Normal file
45
tools/web/check-wbv2-p6-003-desktop.py
Normal 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()
|
||||
81
tools/web/check-wbv2-p7-001-desktop.py
Normal file
81
tools/web/check-wbv2-p7-001-desktop.py
Normal 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()
|
||||
74
tools/web/check-wbv2-p7-002-desktop.py
Normal file
74
tools/web/check-wbv2-p7-002-desktop.py
Normal 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()
|
||||
67
tools/web/check-wbv2-p7-003-desktop.py
Normal file
67
tools/web/check-wbv2-p7-003-desktop.py
Normal 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()
|
||||
96
tools/web/check-wbv2-recovery-baseline.mjs
Normal file
96
tools/web/check-wbv2-recovery-baseline.mjs
Normal 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`);
|
||||
47
tools/web/check-wbv2-recovery-release-gate.mjs
Normal file
47
tools/web/check-wbv2-recovery-release-gate.mjs
Normal 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`);
|
||||
25
tools/web/cmake/OpenColorIOConfig.cmake
Normal file
25
tools/web/cmake/OpenColorIOConfig.cmake
Normal 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)
|
||||
7
tools/web/cmake/OpenColorIOConfigVersion.cmake
Normal file
7
tools/web/cmake/OpenColorIOConfigVersion.cmake
Normal 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))
|
||||
9
tools/web/cmake/pkgconfig/shaderc.pc
Normal file
9
tools/web/cmake/pkgconfig/shaderc.pc
Normal 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}
|
||||
10
tools/web/cmake/pkgconfig/vulkan.pc
Normal file
10
tools/web/cmake/pkgconfig/vulkan.pc
Normal 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}
|
||||
74
tools/web/configure-blender-native.sh
Executable file
74
tools/web/configure-blender-native.sh
Executable 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}"
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
56
tools/web/generate-m12-provenance-epoch.mjs
Normal file
56
tools/web/generate-m12-provenance-epoch.mjs
Normal 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`);
|
||||
@@ -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");
|
||||
|
||||
103
tools/web/generate-wbv2-atomic-chromium-receipts.mjs
Normal file
103
tools/web/generate-wbv2-atomic-chromium-receipts.mjs
Normal 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`);
|
||||
175
tools/web/generate-wbv2-family-evidence.mjs
Normal file
175
tools/web/generate-wbv2-family-evidence.mjs
Normal 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`);
|
||||
146
tools/web/generate-wbv2-integration-readiness.mjs
Normal file
146
tools/web/generate-wbv2-integration-readiness.mjs
Normal 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`);
|
||||
209
tools/web/generate-wbv2-inventory.mjs
Normal file
209
tools/web/generate-wbv2-inventory.mjs
Normal 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`);
|
||||
89
tools/web/generate-wbv2-recovery-receipts.mjs
Normal file
89
tools/web/generate-wbv2-recovery-receipts.mjs
Normal 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`);
|
||||
67
tools/web/generate-wbv2-recovery-root.mjs
Normal file
67
tools/web/generate-wbv2-recovery-root.mjs
Normal 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`);
|
||||
@@ -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}`);
|
||||
|
||||
57
tools/web/generated/WBV2-P0-003.py
Normal file
57
tools/web/generated/WBV2-P0-003.py
Normal 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]))
|
||||
69
tools/web/generated/WBV2-P1-001.py
Normal file
69
tools/web/generated/WBV2-P1-001.py
Normal 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]))
|
||||
55
tools/web/generated/WBV2-P1-002.py
Normal file
55
tools/web/generated/WBV2-P1-002.py
Normal 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]))
|
||||
43
tools/web/generated/WBV2-P1-003.py
Normal file
43
tools/web/generated/WBV2-P1-003.py
Normal 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]))
|
||||
73
tools/web/generated/WBV2-P1-004.py
Normal file
73
tools/web/generated/WBV2-P1-004.py
Normal 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]))
|
||||
66
tools/web/generated/WBV2-P1-005.py
Normal file
66
tools/web/generated/WBV2-P1-005.py
Normal 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]))
|
||||
37
tools/web/generated/WBV2-P2-001.py
Normal file
37
tools/web/generated/WBV2-P2-001.py
Normal 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]))
|
||||
44
tools/web/generated/WBV2-P2-002.py
Normal file
44
tools/web/generated/WBV2-P2-002.py
Normal 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]))
|
||||
39
tools/web/generated/WBV2-P2-003.py
Normal file
39
tools/web/generated/WBV2-P2-003.py
Normal 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]))
|
||||
36
tools/web/generated/WBV2-P2-004.py
Normal file
36
tools/web/generated/WBV2-P2-004.py
Normal 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])
|
||||
27
tools/web/generated/WBV2-P2-005.py
Normal file
27
tools/web/generated/WBV2-P2-005.py
Normal 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])
|
||||
45
tools/web/generated/WBV2-P3-001.py
Normal file
45
tools/web/generated/WBV2-P3-001.py
Normal 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])
|
||||
45
tools/web/generated/WBV2-P3-002.py
Normal file
45
tools/web/generated/WBV2-P3-002.py
Normal 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])
|
||||
91
tools/web/generated/WBV2-P3-003.py
Normal file
91
tools/web/generated/WBV2-P3-003.py
Normal 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])
|
||||
98
tools/web/generated/WBV2-P3-004.py
Normal file
98
tools/web/generated/WBV2-P3-004.py
Normal 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])
|
||||
85
tools/web/generated/WBV2-P3-005.py
Normal file
85
tools/web/generated/WBV2-P3-005.py
Normal 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]))
|
||||
53
tools/web/generated/WBV2-P4-001.py
Normal file
53
tools/web/generated/WBV2-P4-001.py
Normal 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])
|
||||
89
tools/web/generated/WBV2-P4-002.py
Normal file
89
tools/web/generated/WBV2-P4-002.py
Normal 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])
|
||||
75
tools/web/generated/WBV2-P4-003.py
Normal file
75
tools/web/generated/WBV2-P4-003.py
Normal 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])
|
||||
46
tools/web/generated/WBV2-P4-004.py
Normal file
46
tools/web/generated/WBV2-P4-004.py
Normal 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])
|
||||
131
tools/web/generated/WBV2-P5-001.py
Normal file
131
tools/web/generated/WBV2-P5-001.py
Normal 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])
|
||||
81
tools/web/generated/WBV2-P5-002.py
Normal file
81
tools/web/generated/WBV2-P5-002.py
Normal 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])
|
||||
59
tools/web/generated/WBV2-P5-003.py
Normal file
59
tools/web/generated/WBV2-P5-003.py
Normal 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])
|
||||
82
tools/web/generated/WBV2-P6-001.py
Normal file
82
tools/web/generated/WBV2-P6-001.py
Normal 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])
|
||||
62
tools/web/generated/WBV2-P6-002.py
Normal file
62
tools/web/generated/WBV2-P6-002.py
Normal 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])
|
||||
65
tools/web/generated/WBV2-P6-003.py
Normal file
65
tools/web/generated/WBV2-P6-003.py
Normal 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])
|
||||
117
tools/web/generated/WBV2-P7-001.py
Normal file
117
tools/web/generated/WBV2-P7-001.py
Normal 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])
|
||||
91
tools/web/generated/WBV2-P7-002.py
Normal file
91
tools/web/generated/WBV2-P7-002.py
Normal 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])
|
||||
95
tools/web/generated/WBV2-P7-003.py
Normal file
95
tools/web/generated/WBV2-P7-003.py
Normal 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])
|
||||
108
tools/web/intake-m12-immutable-snapshot.mjs
Normal file
108
tools/web/intake-m12-immutable-snapshot.mjs
Normal 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
45
tools/web/native-build-env.sh
Executable 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
20
tools/web/prepare-build-env.sh
Executable 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
21
tools/web/prepare-emscripten.sh
Executable 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}')"
|
||||
@@ -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();
|
||||
|
||||
35
tools/web/run-unit-tests.mjs
Normal file
35
tools/web/run-unit-tests.mjs
Normal 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`);
|
||||
17
tools/web/wbv2-recovery-family-map.json
Normal file
17
tools/web/wbv2-recovery-family-map.json
Normal 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" }
|
||||
}
|
||||
Reference in New Issue
Block a user