import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; export const digest = value => crypto.createHash('sha256').update(value).digest('hex'); export const fileHash = file => digest(fs.readFileSync(file)); export const readJSON = file => JSON.parse(fs.readFileSync(file, 'utf8')); export function validateSourceReview(review, hashFile) { object(review, 'source review'); assert.equal(review.schemaVersion, 1); assert.equal(review.status, 'REVIEWED_SCOPE'); text(review.batchId, 'source review batchId'); assert.equal(review.verifiedUnitDelta, 0); assert.ok(Array.isArray(review.bindings) && review.bindings.length > 0, 'source review bindings required'); for (const ref of review.bindings) { object(ref, 'source binding'); text(ref.path, 'source binding path'); assert.match(text(ref.sha256, 'source binding hash'), /^[a-f0-9]{64}$/); assert.equal(hashFile(ref.path), ref.sha256, `SOURCE_REVIEW_DRIFT: ${ref.path}`); } assert.ok(Array.isArray(review.contracts) && review.contracts.length > 0, 'source contracts required'); for (const contract of review.contracts) { object(contract, 'source contract'); text(contract.id, 'contract id'); text(contract.scope, 'contract scope'); assert.ok(Array.isArray(contract.sourcePaths) && contract.sourcePaths.length > 0 && contract.sourcePaths.every(p => typeof p === 'string' && review.bindings.some(ref => ref.path === p)), 'contract sources must be bound'); assert.ok(Array.isArray(contract.proofStages) && contract.proofStages.length > 0 && contract.proofStages.every(p => typeof p === 'string' && p.length > 0), 'contract proof stages required'); } assert.equal(new Set(review.contracts.map(c => c.id)).size, review.contracts.length, 'duplicate source contracts'); return review; } export function blendFormat(file) { const fd = fs.openSync(file, 'r'), header = Buffer.alloc(7); try { fs.readSync(fd, header, 0, 7, 0); } finally { fs.closeSync(fd); } if (header.toString('ascii') === 'BLENDER') return 'blend'; if (header.subarray(0, 2).equals(Buffer.from([0x1f, 0x8b]))) return 'gzip'; if (header.subarray(0, 4).equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))) return 'zstd'; throw new Error(`${file}: unsupported blend header`); } export function filesUnder(directory) { return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap(e => { const file = path.join(directory, e.name); return e.isDirectory() ? filesUnder(file) : e.isFile() ? [file] : []; }); } export const records = files => [...new Set(files)].sort().map(file => ({ path: file, sha256: fileHash(file) })); export function candidateInputs(directory) { const manifestPath = path.join(directory, 'candidate.json'), manifest = object(readJSON(manifestPath), 'candidate'); const expected = object(manifest.sha256, 'candidate.sha256'); const native = ['web_engine.js', 'web_engine.wasm'].map(name => path.join(directory, 'public/vendor/blender', name)); for (const file of native) { const hash = text(expected[path.basename(file)], 'native hash'); assert.match(hash, /^[a-f0-9]{64}$/); assert.equal(fileHash(file), hash, `${file}: candidate drift`); } assert.equal(fs.readFileSync(native[1]).subarray(0, 4).toString('hex'), '0061736d', 'invalid WASM magic'); return [manifestPath, ...filesUnder(path.join(directory, 'public'))]; } export function reportOutcome(report) { object(report, 'Playwright report'); object(report.stats, 'stats'); for (const key of ['expected', 'unexpected', 'flaky', 'skipped']) assert.ok(Number.isSafeInteger(report.stats[key]) && report.stats[key] >= 0, `stats.${key}: expected nonnegative integer`); assert.ok(Array.isArray(report.suites) && Array.isArray(report.errors), 'report arrays required'); const tests = []; const visit = suites => { for (const suite of suites) { object(suite, 'suite'); assert.ok(Array.isArray(suite.specs)); for (const spec of suite.specs) { assert.ok(Array.isArray(spec.tests)); tests.push(...spec.tests); } if (suite.suites !== undefined) { assert.ok(Array.isArray(suite.suites)); visit(suite.suites); } } }; visit(report.suites); for (const test of tests) { object(test, 'test'); text(test.expectedStatus, 'expectedStatus'); text(test.status, 'test.status'); assert.ok(Array.isArray(test.results), 'test.results must be array'); for (const result of test.results) { object(result, 'result'); text(result.status, 'result.status'); } } const passed = tests.length > 0 && tests.every(t => t.expectedStatus === 'passed' && t.status === 'expected' && t.results.length === 1 && t.results[0].status === 'passed'); return { passed: passed && report.errors.length === 0 && report.stats.expected === tests.length && ['unexpected', 'flaky', 'skipped'].every(k => report.stats[k] === 0), stats: report.stats, tests: tests.length }; } export function object(value, label) { assert.ok(value !== null && typeof value === 'object' && !Array.isArray(value), `${label}: expected object`); return value; } export function text(value, label) { assert.equal(typeof value, 'string', `${label}: expected string`); assert.ok(value.length > 0, `${label}: empty string`); return value; } export function atomicJSON(file, value) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n'); fs.renameSync(temp, file); } export function identity(pid) { try { return fs.readFileSync(`/proc/${pid}/stat`, 'utf8').split(') ').at(-1).split(' ')[19]; } catch { return null; } } export function reusable(receipt, inputHash) { if (!receipt || receipt.status !== 'PASS' || receipt.inputHash !== inputHash || !Array.isArray(receipt.artifacts) || receipt.artifacts.length === 0) return false; try { return receipt.artifacts.every(a => typeof a.path === 'string' && typeof a.sha256 === 'string' && /^[a-f0-9]{64}$/.test(a.sha256) && fileHash(a.path) === a.sha256); } catch { return false; } } export function sameInputs(before, after) { assert.deepEqual(after, before, 'inputs changed during the case; evidence remains stale'); } export function validatePlan(plan) { object(plan, 'plan'); assert.equal(plan.schemaVersion, 1); assert.equal(typeof plan.nativeSession, 'boolean', 'nativeSession must be boolean'); assert.ok(Array.isArray(plan.remainingGates) && plan.remainingGates.every(x => typeof x === 'string'), 'remainingGates must be strings'); assert.match(text(plan.batchId, 'batchId'), /^B[1-6]$/); text(plan.workflow, 'workflow'); assert.ok(Array.isArray(plan.sourcePaths) && plan.sourcePaths.length > 0, 'source paths required'); assert.ok(Array.isArray(plan.fixtures) && plan.fixtures.length === (plan.sourceSuite ? 1 : 2), 'source suite or minimal + production required'); assert.deepEqual(plan.fixtures.map(f => object(f, 'fixture').kind), plan.sourceSuite ? ['source'] : ['minimal', 'production']); if (plan.sourceSuite !== undefined) { assert.match(text(plan.sourceSuite, 'sourceSuite'), /^blender-5\.2\.0\/tests\/python\/[\w-]+\.py$/); assert.ok(plan.sourcePaths.includes(plan.sourceSuite), 'source suite must be hash-bound'); } for (const f of plan.fixtures) { text(f.path, 'fixture.path'); text(f.grep, 'fixture.grep'); assert.match(text(f.spec, 'fixture.spec'), /^tests\/e2e\/[\w-]+\.spec\.ts$/); assert.ok(Array.isArray(f.scope) && f.scope.every(x => typeof x === 'string'), 'scope must be strings'); } assert.ok(Number.isSafeInteger(plan.timeoutMs) && plan.timeoutMs >= 1000 && plan.timeoutMs <= 900000, 'timeoutMs must be integer milliseconds'); assert.ok(plan.sourcePaths.every(x => typeof x === 'string' && x.length > 0), 'sourcePaths must be strings'); if (plan.sourceReview !== undefined) text(plan.sourceReview, 'sourceReview'); return plan; }