import assert from "node:assert/strict"; import crypto from "node:crypto"; 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 ledgerPath = path.join(root, "docs/status/parity-ledger.json"); const outputPath = path.join(root, "docs/status/release-evidence.json"); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); function executeEvidenceCommand(id, fields, command, artifacts = []) { const started = Date.now(); const result = spawnSync("bash", ["-lc", command], { cwd: root, encoding: "utf8", env: { ...process.env, WEB_TEST_PORT: process.env.WEB_TEST_PORT ?? "5326" }, maxBuffer: 16 * 1024 * 1024 }); const combined = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim(); if (result.status !== 0) { process.stderr.write(`${combined}\n`); throw new Error(`${id} failed with exit code ${result.status}`); } const record = { id, fields, command, exitCode: 0, durationMs: Date.now() - started, output: combined.slice(-4096) || `${id} passed`, artifactSha256: artifacts.filter((file) => fs.existsSync(path.join(root, file))).map((file) => sha256(path.join(root, file))) }; process.stdout.write(`${id}-ok durationMs=${record.durationMs}\n`); return record; } if (process.argv.includes("--sync-ledger")) { const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const current = JSON.parse(fs.readFileSync(outputPath, "utf8")); const manifest = { ...current, schemaVersion: 4, source: "docs/status/parity-ledger.json", sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, }; fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-ledger-sync-ok families=${manifest.families.length} sha256=${sha256(outputPath)}\n`); process.exit(0); } if (process.argv.includes("--record-v1-user-loop")) { const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const current = JSON.parse(fs.readFileSync(outputPath, "utf8")); const record = executeEvidenceCommand( "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"], ); current.evidence.records = [...current.evidence.records.filter((item) => item.id !== record.id), record]; const manifest = { ...current, schemaVersion: 4, sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, }; fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-v1-ok records=${manifest.evidence.records.length} sha256=${sha256(outputPath)}\n`); process.exit(0); } if (process.argv.includes("--record-geometry-10m")) { const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const current = JSON.parse(fs.readFileSync(outputPath, "utf8")); const record = executeEvidenceCommand( "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", ], ); current.evidence.performance.geometry10M = true; current.evidence.records = [...current.evidence.records.filter((item) => item.id !== record.id), record]; const manifest = { ...current, schemaVersion: 4, sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, }; fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-geometry-10m-ok records=${manifest.evidence.records.length} sha256=${sha256(outputPath)}\n`); process.exit(0); } if (process.argv.includes("--record-long-media")) { const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const current = JSON.parse(fs.readFileSync(outputPath, "utf8")); const record = executeEvidenceCommand( "long-media", ["performance.longMedia"], "npm --prefix web run test:long-media-performance", [ "web/app/src/sequencer/LongMediaTimeline.ts", "web/app/src/workers/long-media.worker.ts", "web/tests/e2e/long-media-performance.spec.ts", "tests/files/web/media/sequencer-frame.png", "tests/files/web/media/sequencer-silence.wav", ], ); current.evidence.performance.longMedia = true; current.evidence.records = [...current.evidence.records.filter((item) => item.id !== record.id), record]; const manifest = { ...current, schemaVersion: 4, sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, }; fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-long-media-ok records=${manifest.evidence.records.length} sha256=${sha256(outputPath)}\n`); process.exit(0); } if (process.argv.includes("--record-oom-recovery")) { const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const current = JSON.parse(fs.readFileSync(outputPath, "utf8")); const record = executeEvidenceCommand( "oom-recovery", ["faults.oom"], "npm --prefix web run test:oom-recovery", [ "web/protocol/oom-recovery.ts", "web/app/src/testing/oom-fault-session.ts", "web/app/src/testing/oom-recovery-scenarios.ts", "web/app/src/storage/opfs-files.ts", "web/app/src/render/nanovdb-volume-renderer.ts", "web/tests/e2e/oom-recovery.spec.ts", "web/app/src/vendor/blender/web_engine.wasm", ], ); current.evidence.faults.oom = true; current.evidence.records = [...current.evidence.records.filter((item) => item.id !== record.id), record]; const manifest = { ...current, schemaVersion: 4, sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, }; fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-oom-ok records=${manifest.evidence.records.length} sha256=${sha256(outputPath)}\n`); process.exit(0); } const evidence = { browser: { chromium: false }, runtime: { offline: false, workerRestart: false, opfsRecovery: false }, performance: { geometry1M: false, geometry10M: false, texture4K: false, texture8K: false, longMedia: false, simulationCache: false }, faults: { oom: false, deviceLoss: false, networkInterrupt: false, malformedBlend: false, zipBomb: false }, provenance: { license: false, sbom: false, sourceOffer: false, deterministicPackage: false }, records: [], }; function run(id, fields, command, artifacts = []) { for (const field of fields) { const [group, key] = field.split("."); evidence[group][key] = true; } evidence.records.push(executeEvidenceCommand(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 && 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"]); run("long-media", ["performance.longMedia"], "npm --prefix web run test:long-media-performance", ["web/app/src/sequencer/LongMediaTimeline.ts", "web/app/src/workers/long-media.worker.ts", "web/tests/e2e/long-media-performance.spec.ts", "tests/files/web/media/sequencer-frame.png", "tests/files/web/media/sequencer-silence.wav"]); run("simulation-cache", ["performance.simulationCache"], "npm --prefix web run test:simulation-cache-performance", ["web/protocol/physics-cache-playback.ts", "web/protocol/simulation-cache.ts", "web/app/src/workers/storage.worker.ts"]); run("malformed-blend", ["faults.malformedBlend"], "npm --prefix web run test:malicious-blends", ["tests/files/web/basic_scene.blend"]); run("network-interruption", ["faults.networkInterrupt"], "npm --prefix web run test:network-interruption", ["web/tests/e2e/network-interruption.spec.ts", "web/app/src/vendor/blender/web_engine.wasm"]); run("device-loss", ["faults.deviceLoss"], "npm --prefix web run test:device-loss", ["web/app/src/three-adapter/viewport.ts", "web/tests/e2e/device-loss.spec.ts"]); run("oom-recovery", ["faults.oom"], "npm --prefix web run test:oom-recovery", ["web/protocol/oom-recovery.ts", "web/app/src/testing/oom-fault-session.ts", "web/app/src/testing/oom-recovery-scenarios.ts", "web/app/src/storage/opfs-files.ts", "web/app/src/render/nanovdb-volume-renderer.ts", "web/tests/e2e/oom-recovery.spec.ts", "web/app/src/vendor/blender/web_engine.wasm"]); run("texture-4k", ["performance.texture4K"], "npm --prefix web run test:texture-4k-performance", ["web/protocol/render-assets.ts", "web/app/src/three-adapter/texture-assets.ts"]); run("texture-8k", ["performance.texture8K"], "npm --prefix web run test:texture-8k-performance", ["web/protocol/render-assets.ts", "web/app/src/three-adapter/texture-assets.ts"]); run("zip-bomb", ["faults.zipBomb"], "WEB_TEST_PORT=5323 npm --prefix web run test:asset-library", ["web/protocol/asset-library-io.ts"]); run("release-package", [], "npm --prefix web run test:release-package", ["docs/web/sbom.spdx.json", "web/app/src/vendor/blender/web_engine.wasm"]); run("offline-reproducibility", ["provenance.sourceOffer", "provenance.deterministicPackage"], "npm --prefix web run release:offline", ["release/blender-web-offline.tar.gz", "release/blender-web-corresponding-source.tar.gz", "release/SHA256SUMS.txt"]); const ledgerBytes = fs.readFileSync(ledgerPath); const ledger = JSON.parse(ledgerBytes); const manifest = { schemaVersion: 4, source: "docs/status/parity-ledger.json", sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, evidence }; assert.ok(manifest.families.every((family) => family.releaseStatus === "READY")); assert.equal(manifest.evidence.performance.geometry10M, true); assert.equal(manifest.evidence.performance.longMedia, true); assert.equal(manifest.evidence.faults.oom, true); assert.equal(manifest.evidence.performance.simulationCache, true); assert.equal(manifest.evidence.performance.texture4K, true); assert.equal(manifest.evidence.performance.texture8K, true); assert.equal(manifest.evidence.faults.deviceLoss, true); assert.equal(manifest.evidence.faults.networkInterrupt, true); fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`); process.stdout.write(`release-evidence-ok records=${evidence.records.length} status=READY sha256=${sha256(outputPath)}\n`);