Complete V1 RC deployment capability gates

This commit is contained in:
mes123456
2026-08-15 01:01:23 -04:00
parent a3f3071c03
commit 17ab961485
37 changed files with 2031 additions and 184 deletions

View File

@@ -0,0 +1,63 @@
import assert from "node:assert/strict";
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 contractPath = path.join(root, "docs/web/deployment-contract.json");
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
const guide = fs.readFileSync(path.join(root, "docs/web/DEPLOYMENT.md"), "utf8");
const vite = fs.readFileSync(path.join(root, "web/app/vite.config.ts"), "utf8");
const releaseCreator = fs.readFileSync(path.join(root, "tools/web/create-offline-release.mjs"), "utf8");
assert.equal(contract.schemaVersion, 1);
assert.equal(contract.product, "Web Blender Modeler V1");
assert.equal(contract.browserScope, "Chromium");
assert.deepEqual(contract.methods, ["GET", "HEAD"]);
assert.equal(contract.transport.production, "https");
assert.equal(contract.transport.localDevelopment, "http://127.0.0.1");
assert.equal(contract.transport.fileProtocolSupported, false);
assert.equal(contract.transport.sameOriginRuntimeAssets, true);
assert.deepEqual(contract.responseHeaders.allResponses, {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Resource-Policy": "same-origin",
});
const routes = Object.fromEntries(contract.responseHeaders.routes.map((route) => [route.id, route]));
assert.deepEqual(routes["entry-document"], { id: "entry-document", patterns: ["/", "/index.html"], cacheControl: "no-cache" });
assert.deepEqual(routes["runtime-manifest"], { id: "runtime-manifest", patterns: ["/engine-manifest.json"], cacheControl: "no-cache" });
assert.deepEqual(routes["content-hashed-assets"], { id: "content-hashed-assets", patterns: ["/assets/*"], cacheControl: "public, max-age=31536000, immutable" });
assert.deepEqual(routes["stable-engine-assets"], { id: "stable-engine-assets", patterns: ["/vendor/blender/*"], cacheControl: "no-cache" });
assert.deepEqual(routes.fallback, { id: "fallback", patterns: ["*"], cacheControl: "no-cache" });
for (const [extension, mime] of Object.entries({
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".wasm": "application/wasm",
".json": "application/json; charset=utf-8",
".png": "image/png",
".wav": "audio/wav",
".blend": "application/octet-stream",
".nvdb": "application/x-nanovdb",
})) assert.equal(contract.mimeTypes[extension], mime, `${extension} MIME drifted`);
assert.deepEqual(contract.rangeRequests.extensions, [".wasm", ".blend", ".nvdb"]);
assert.equal(contract.rangeRequests.unit, "bytes");
assert.equal(contract.rangeRequests.singleRangeOnly, true);
assert.equal(contract.rangeRequests.etag, "strong-sha256");
assert.equal(contract.rangeRequests.satisfiedStatus, 206);
assert.equal(contract.rangeRequests.unsatisfiedStatus, 416);
assert.deepEqual(contract.rangeRequests.requiredSatisfiedHeaders, ["Accept-Ranges", "Content-Range", "Content-Length", "ETag"]);
assert.equal(contract.rangeRequests.ifRangeMismatch, "return-full-200");
for (const value of Object.values(contract.responseHeaders.allResponses)) assert.ok(guide.includes(`\`${value}\``));
for (const status of ["206", "416", "200"]) assert.ok(guide.includes(`\`${status}\``));
assert.match(vite, /deployment-contract\.json/);
assert.match(vite, /responseHeaders\.allResponses/);
assert.match(releaseCreator, /deployment-contract\.json/);
assert.match(releaseCreator, /DEPLOYMENT\.md/);
assert.doesNotMatch(releaseCreator, /Serve app\/ from any local static HTTP server/);
process.stdout.write(`deployment-contract-ok headers=3 routes=${contract.responseHeaders.routes.length} mime=${Object.keys(contract.mimeTypes).length} rangeExtensions=${contract.rangeRequests.extensions.length}\n`);

View File

@@ -5,7 +5,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
native_dir="${repo_root}/build_web/native"
mkdir -p "${native_dir}"
c++ -std=c++17 -O2 \
c++ -std=c++20 -O2 \
-I"${repo_root}/blender-5.2.0/source/blender/web_engine" \
-I"${repo_root}/blender-5.2.0/extern/json/include" \
"${repo_root}/blender-5.2.0/source/blender/web_engine/web_engine_api.cpp" \

View File

@@ -48,6 +48,7 @@ for (const target of [100_000, 1_000_000]) {
const engine = await factory({ wasmBinary });
const source = grid(target);
const pointers = [];
let report;
try {
const positions = alloc(engine, source.positions); pointers.push(positions);
const indices = alloc(engine, source.indices); pointers.push(indices);
@@ -71,11 +72,15 @@ for (const target of [100_000, 1_000_000]) {
const heapBytes = engine.HEAPU8.byteLength;
assert.ok(elapsedMs <= limits[target], `${target} triangle gate exceeded ${limits[target]}ms: ${elapsedMs.toFixed(1)}ms`);
assert.ok(heapBytes <= limits.heapBytes, `${target} triangle gate exceeded WASM heap limit: ${heapBytes}`);
results.push({ target, ratio, outputTriangles, elapsedMs: Math.round(elapsedMs), heapBytes });
report = { target, ratio, inputTriangles: source.triangleCount, outputTriangles, elapsedMs: Math.round(elapsedMs), peakHeapBytes: heapBytes };
}
finally {
for (const pointer of pointers.reverse()) if (pointer) engine._free(pointer);
}
const recoveryPointer = engine._malloc(16);
assert.ok(recoveryPointer > 0, `${target} triangle gate did not recover a small allocation`);
engine._free(recoveryPointer);
results.push({ ...report, releasedPointerCount: pointers.length, recoveryAllocation: true });
}
process.stdout.write(`release-performance-ok ${JSON.stringify(results)}\n`);

View File

@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { buildAcceptancePlan, readAcceptanceInputs } from "./v1-acceptance-lib.mjs";
const { ledger, packageManifest } = readAcceptanceInputs();
const plan = buildAcceptancePlan(ledger, packageManifest);
assert.equal(plan.entries.some((entry) => entry.token === "web:test:nonmesh-usd-blender-roundtrip"), true);
assert.equal(plan.entries.some((entry) => entry.token === "web:test:v1-user-loop"), true);
assert.equal(plan.entries.some((entry) => entry.token === "web:release:offline"), true);
assert.ok(plan.e2eCount > 0);
assert.ok(plan.reusedCount > 0);
process.stdout.write(
`v1-acceptance-coverage-ok families=${plan.familyCount} declarations=${plan.declarationCount} unique=${plan.uniqueCount} scripts=${plan.scriptCount} e2e=${plan.e2eCount} reused=${plan.reusedCount}\n`,
);

View File

@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import { evidencePath, expectedEvidenceIdentity, sha256 } from "./v1-acceptance-lib.mjs";
const { ledgerSha256, packageSha256, plan, toolSourceSha256 } = expectedEvidenceIdentity();
assert.ok(fs.existsSync(evidencePath), "V1 acceptance evidence is missing; run release:v1-acceptance");
const manifest = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.sourceSha256, ledgerSha256, "V1 acceptance evidence is stale relative to parity ledger");
assert.equal(manifest.packageSha256, packageSha256, "V1 acceptance evidence is stale relative to web package scripts");
assert.deepEqual(manifest.toolSourceSha256, toolSourceSha256, "V1 acceptance evidence is stale relative to acceptance tools");
assert.ok(Number.isFinite(Date.parse(manifest.generatedAt)), "V1 acceptance generatedAt is invalid");
assert.equal(manifest.summary.families, plan.familyCount);
assert.equal(manifest.summary.declarations, plan.declarationCount);
assert.equal(manifest.summary.unique, plan.uniqueCount);
assert.equal(manifest.summary.passed, plan.uniqueCount);
assert.equal(manifest.summary.failed, 0);
assert.equal(manifest.environment.blender.includes("USD=True"), true, "acceptance Blender must provide USD");
assert.match(manifest.environment.blenderExecutableSha256, /^[a-f0-9]{64}$/, "acceptance Blender executable hash is missing");
assert.match(manifest.environment.blenderArchiveSha256, /^[a-f0-9]{64}$/, "acceptance Blender archive hash is missing");
assert.equal(manifest.results.length, plan.uniqueCount);
assert.deepEqual(manifest.results.map((result) => result.token), plan.entries.map((entry) => entry.token));
for (const [index, result] of manifest.results.entries()) {
const expected = plan.entries[index];
assert.deepEqual(result.familyIds, expected.familyIds, `${result.token}: family mapping drifted`);
assert.equal(result.command, expected.command, `${result.token}: command drifted`);
assert.equal(result.exitCode, 0, `${result.token}: command failed`);
assert.ok(result.durationMs >= 0, `${result.token}: duration is invalid`);
assert.match(result.outputSha256, /^[a-f0-9]{64}$/, `${result.token}: output hash is missing`);
assert.equal(result.outputSha256, sha256(result.output), `${result.token}: stored output does not match its hash`);
assert.ok(result.output.trim().length > 0, `${result.token}: output is empty`);
}
process.stdout.write(
`v1-acceptance-evidence-ok families=${plan.familyCount} declarations=${plan.declarationCount} unique=${plan.uniqueCount} sha256=${sha256(fs.readFileSync(evidencePath))}\n`,
);

View File

@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { evidencePath, root, sha256 } from "./v1-acceptance-lib.mjs";
assert.ok(fs.existsSync(evidencePath), "V1 acceptance evidence is missing");
const acceptanceSha256 = sha256(fs.readFileSync(evidencePath));
const releaseEvidence = JSON.parse(fs.readFileSync(path.join(root, "docs/status/release-evidence.json"), "utf8"));
const userLoopRecord = releaseEvidence.evidence?.records?.find((record) => record.id === "v1-user-loop");
assert.ok(userLoopRecord, "release evidence has no v1-user-loop record");
assert.equal(userLoopRecord.exitCode, 0, "release v1-user-loop record failed");
assert.ok(userLoopRecord.output?.trim().length > 0, "release v1-user-loop output is empty");
assert.ok(
userLoopRecord.artifactSha256?.includes(acceptanceSha256),
"current V1 acceptance evidence is not bound to release evidence",
);
process.stdout.write(
`v1-acceptance-release-binding-ok acceptanceSha256=${acceptanceSha256} releaseGeneratedAt=${releaseEvidence.generatedAt}\n`,
);

View File

@@ -47,8 +47,8 @@ if (process.argv.includes("--record-v1-user-loop")) {
const record = executeEvidenceCommand(
"v1-user-loop",
[],
"npm --prefix web run test:v1-user-loop",
["web/tests/e2e/v1-user-loop.spec.ts", "web/app/src/vendor/blender/web_engine.wasm"],
"npm --prefix web run test:v1-user-loop && npm --prefix web run test:v1-acceptance-evidence",
["web/tests/e2e/v1-user-loop.spec.ts", "web/app/src/vendor/blender/web_engine.wasm", "docs/status/v1-acceptance-evidence.json"],
);
current.evidence.records = [...current.evidence.records.filter((item) => item.id !== record.id), record];
const manifest = {
@@ -173,7 +173,7 @@ function run(id, fields, command, artifacts = []) {
run("sbom", ["provenance.license", "provenance.sbom"], "npm --prefix web run release:sbom", ["docs/web/sbom.spdx.json", "docs/web/third-party-notices.json", "web/package-lock.json"]);
run("vdb-boundary", [], "npm --prefix web run test:vdb-availability && npm --prefix web run test:vdb-native && npm --prefix web run test:vdb", ["web/protocol/volume-vdb.ts", "web/app/src/volume/nanovdb-stream.ts", "tools/vdb/vdb_to_nanovdb.cc", "docs/status/vdb-native-evidence.json", "docs/VDB_NANOVDB_WEBGPU_IMPLEMENTATION_PLAN.md"]);
run("v1-user-loop", [], "npm --prefix web run test:v1-user-loop", ["web/tests/e2e/v1-user-loop.spec.ts", "web/app/src/vendor/blender/web_engine.wasm"]);
run("v1-user-loop", [], "npm --prefix web run test:v1-user-loop && npm --prefix web run test:v1-acceptance-evidence", ["web/tests/e2e/v1-user-loop.spec.ts", "web/app/src/vendor/blender/web_engine.wasm", "docs/status/v1-acceptance-evidence.json"]);
run("chromium", ["browser.chromium", "runtime.offline", "runtime.workerRestart", "runtime.opfsRecovery"], "npm --prefix web run test:e2e -- --workers=1 && npm --prefix web run test:browser", ["web/app/src/vendor/blender/web_engine.wasm"]);
run("geometry-1m", ["performance.geometry1M"], "npm --prefix web run test:release-performance", ["web/app/src/vendor/blender/web_engine.wasm"]);
run("geometry-10m", ["performance.geometry10M"], "npm --prefix web run test:geometry-10m-performance", ["web/protocol/geometry-stream.ts", "web/app/src/performance/large-geometry.ts", "web/app/src/workers/geometry-stream.worker.ts", "web/tests/e2e/geometry-10m-performance.spec.ts"]);

View File

@@ -15,12 +15,16 @@ fs.cpSync(path.join(root, "web/dist"), path.join(bundle, "app"), { recursive: tr
fs.copyFileSync(path.join(root, "blender-5.2.0/COPYING"), path.join(bundle, "COPYING"));
fs.copyFileSync(path.join(root, "docs/web/third-party-notices.json"), path.join(bundle, "third-party-notices.json"));
fs.copyFileSync(path.join(root, "docs/web/sbom.spdx.json"), path.join(bundle, "sbom.spdx.json"));
fs.copyFileSync(path.join(root, "docs/web/deployment-contract.json"), path.join(bundle, "deployment-contract.json"));
fs.copyFileSync(path.join(root, "docs/web/DEPLOYMENT.md"), path.join(bundle, "DEPLOYMENT.md"));
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/opensubdiv-source/LICENSE.txt"), path.join(bundle, "LICENSE-OpenSubdiv.txt"));
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/gmp-source/COPYING.LESSERv3"), path.join(bundle, "LICENSE-GMP-LGPLv3.txt"));
fs.writeFileSync(path.join(bundle, "README.txt"), [
"Blender Web offline release",
"Web Blender Modeler V1 offline release",
"",
"Serve app/ from any local static HTTP server. The application has no runtime CDN dependency.",
"Serve app/ from a static HTTP server that implements deployment-contract.json.",
"See DEPLOYMENT.md for the required isolation, cache, MIME, ETag and byte-range behavior.",
"The application has no runtime CDN dependency.",
"Opening index.html directly is unsupported because browsers restrict module Workers and WASM under file://.",
"See SOURCE_OFFER.txt and third-party-notices.json for licensing and corresponding source.",
"",

View File

@@ -0,0 +1,103 @@
import fs from "node:fs";
import { spawnSync } from "node:child_process";
import {
evidencePath,
expectedEvidenceIdentity,
root,
sha256,
} from "./v1-acceptance-lib.mjs";
const { ledgerSha256, packageSha256, plan, toolSourceSha256 } = expectedEvidenceIdentity();
const basePort = Number.parseInt(process.env.WEB_ACCEPTANCE_PORT ?? "5400", 10);
if (!Number.isInteger(basePort) || basePort < 1024 || basePort > 65000 - plan.entries.length) {
throw new Error(`WEB_ACCEPTANCE_PORT must leave ${plan.entries.length} sequential ports available`);
}
function capture(command, args) {
const result = spawnSync(command, args, {
cwd: root,
encoding: "utf8",
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
maxBuffer: 16 * 1024 * 1024,
});
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
}
const blenderBin = process.env.BLENDER_BIN ?? "build_blender_5.2.0/bin/blender";
const blenderArchiveSha256 = process.env.BLENDER_ARCHIVE_SHA256 ?? "";
if (blenderArchiveSha256 && !/^[a-f0-9]{64}$/.test(blenderArchiveSha256)) {
throw new Error("BLENDER_ARCHIVE_SHA256 must be a lowercase SHA-256");
}
const environment = {
node: process.version,
npm: capture("npm", ["--version"]).split(/\r?\n/)[0],
chromium: capture(process.env.CHROME_PATH ?? "/home/mes123456/.local/bin/google-chrome", ["--version"]).split(/\r?\n/)[0],
blenderBin: process.env.BLENDER_BIN ? "BLENDER_BIN" : blenderBin,
blenderExecutableSha256: fs.existsSync(blenderBin) ? sha256(fs.readFileSync(blenderBin)) : "unavailable",
blenderArchiveSha256: blenderArchiveSha256 || "unavailable",
blender: capture(blenderBin, [
"--background",
"--factory-startup",
"--python-expr",
"import bpy; print('ACCEPTANCE_BLENDER=' + bpy.app.version_string + ';USD=' + str(bpy.app.build_options.usd))",
]).split(/\r?\n/).filter((line) => line.includes("ACCEPTANCE_BLENDER=")).at(-1) ?? "unavailable",
};
const results = [];
for (const [index, entry] of plan.entries.entries()) {
const started = Date.now();
process.stdout.write(`[${index + 1}/${plan.entries.length}] ${entry.token}\n`);
const result = spawnSync("npm", ["--prefix", "web", "run", entry.script, ...entry.args], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
FORCE_COLOR: "0",
NO_COLOR: "1",
WEB_TEST_PORT: String(basePort + index),
},
maxBuffer: 32 * 1024 * 1024,
});
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
const storedOutput = output.slice(-4096) || `${entry.token} produced no output`;
const exitCode = result.status ?? 1;
results.push({
token: entry.token,
familyIds: entry.familyIds,
command: entry.command,
exitCode,
durationMs: Date.now() - started,
outputSha256: sha256(storedOutput),
output: storedOutput,
});
process.stdout.write(` exit=${exitCode} durationMs=${results.at(-1).durationMs}\n`);
}
const manifest = {
schemaVersion: 1,
source: "docs/status/parity-ledger.json",
sourceSha256: ledgerSha256,
package: "web/package.json",
packageSha256,
toolSourceSha256,
generatedAt: new Date().toISOString(),
environment,
summary: {
families: plan.familyCount,
declarations: plan.declarationCount,
unique: plan.uniqueCount,
passed: results.filter((result) => result.exitCode === 0).length,
failed: results.filter((result) => result.exitCode !== 0).length,
},
results,
};
fs.writeFileSync(evidencePath, `${JSON.stringify(manifest, null, 2)}\n`);
const failed = results.filter((result) => result.exitCode !== 0);
process.stdout.write(
`v1-acceptance-run-complete passed=${manifest.summary.passed} failed=${manifest.summary.failed} evidence=${evidencePath}\n`,
);
if (failed.length > 0) {
process.stderr.write(`failed acceptance: ${failed.map((result) => result.token).join(", ")}\n`);
process.exitCode = 1;
}

View File

@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export const ledgerPath = path.join(root, "docs/status/parity-ledger.json");
export const packagePath = path.join(root, "web/package.json");
export const evidencePath = path.join(root, "docs/status/v1-acceptance-evidence.json");
export const toolRelativePaths = [
"tools/web/v1-acceptance-lib.mjs",
"tools/web/check-v1-acceptance-coverage.mjs",
"tools/web/run-v1-acceptance.mjs",
"tools/web/check-v1-acceptance-evidence.mjs",
];
export function sha256(value) {
return crypto.createHash("sha256").update(value).digest("hex");
}
export function readAcceptanceInputs() {
const ledgerBytes = fs.readFileSync(ledgerPath);
const packageBytes = fs.readFileSync(packagePath);
return {
ledgerBytes,
packageBytes,
ledger: JSON.parse(ledgerBytes),
packageManifest: JSON.parse(packageBytes),
};
}
function resolveAcceptance(token, scripts) {
assert.equal(typeof token, "string", "acceptance token must be a string");
if (token.startsWith("web:e2e:")) {
const pattern = token.slice("web:e2e:".length);
assert.ok(pattern.trim().length > 0, `${token}: E2E pattern must not be empty`);
assert.equal(typeof scripts["test:e2e"], "string", `${token}: web test:e2e script is missing`);
return {
token,
kind: "e2e",
script: "test:e2e",
args: ["--", "--workers=1", "-g", pattern],
command: `npm --prefix web run test:e2e -- --workers=1 -g ${JSON.stringify(pattern)}`,
};
}
const match = /^web:(test|release):(.+)$/.exec(token);
assert.ok(match, `${token}: unsupported acceptance namespace`);
const script = `${match[1]}:${match[2]}`;
assert.equal(typeof scripts[script], "string", `${token}: web package script ${script} is missing`);
return {
token,
kind: "script",
script,
args: [],
command: `npm --prefix web run ${script}`,
};
}
export function buildAcceptancePlan(ledger, packageManifest) {
assert.equal(ledger.schemaVersion, 2, "unsupported parity ledger schema");
assert.ok(Array.isArray(ledger.families), "parity ledger families must be an array");
assert.equal(ledger.families.length, 12, "V1 acceptance expects N-015 through N-026");
assert.ok(packageManifest.scripts && typeof packageManifest.scripts === "object", "web package scripts are missing");
const byToken = new Map();
let declarationCount = 0;
for (const family of ledger.families) {
assert.match(family.id, /^N-0(?:1[5-9]|2[0-6])$/, `${family.id}: unexpected family id`);
assert.ok(Array.isArray(family.acceptance) && family.acceptance.length > 0, `${family.id}: acceptance is empty`);
assert.equal(new Set(family.acceptance).size, family.acceptance.length, `${family.id}: duplicate acceptance token`);
for (const token of family.acceptance) {
declarationCount += 1;
const existing = byToken.get(token);
if (existing) {
existing.familyIds.push(family.id);
continue;
}
byToken.set(token, { ...resolveAcceptance(token, packageManifest.scripts), familyIds: [family.id] });
}
}
const entries = [...byToken.values()];
return {
familyCount: ledger.families.length,
declarationCount,
uniqueCount: entries.length,
scriptCount: entries.filter((entry) => entry.kind === "script").length,
e2eCount: entries.filter((entry) => entry.kind === "e2e").length,
reusedCount: entries.filter((entry) => entry.familyIds.length > 1).length,
entries,
};
}
export function expectedEvidenceIdentity() {
const inputs = readAcceptanceInputs();
return {
...inputs,
plan: buildAcceptancePlan(inputs.ledger, inputs.packageManifest),
ledgerSha256: sha256(inputs.ledgerBytes),
packageSha256: sha256(inputs.packageBytes),
toolSourceSha256: Object.fromEntries(
toolRelativePaths.map((relativePath) => [relativePath, sha256(fs.readFileSync(path.join(root, relativePath)))]),
),
};
}