feat: prepare FreeCAD private naming worker linkage
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-11 23:16:22 -04:00
parent aa607451ad
commit 97967041e2
30 changed files with 505 additions and 84 deletions

View File

@@ -5,7 +5,7 @@ const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/chrome-opfs-migration-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.browserId !== 'chrome' || report.status !== 'pass' || report.crossOriginIsolated !== true) throw new Error('Chrome REL-02 OPFS migration evidence is not passing.')
const migration = report.migration
if (!migration || migration.sourceSchemaVersion !== 1 || migration.migratedSchemaVersion !== 6 || migration.migratedDocumentVersion !== 3 || migration.migratedObjectCount !== 1 || migration.recomputeDefaulted !== true || migration.reopenedSchemaVersion !== 6 || migration.reopenedVersion !== 4 || migration.checkpointVersion !== 4 || migration.recoveryIntegrity !== 'ok' || migration.reopenedRecoveryIntegrity !== 'ok' || migration.resourceRoundTrip !== true || migration.resourceReleased !== true) throw new Error('Chrome REL-02 forward migration, reopen, checkpoint, or resource evidence is invalid.')
if (!migration || migration.sourceSchemaVersion !== 1 || migration.migratedSchemaVersion !== 7 || migration.migratedDocumentVersion !== 3 || migration.migratedObjectCount !== 1 || migration.migratedNativeObjectTag !== 1 || migration.recomputeDefaulted !== true || migration.reopenedSchemaVersion !== 7 || migration.reopenedVersion !== 4 || migration.reopenedNativeObjectTag !== 17 || migration.checkpointNativeObjectTag !== 17 || migration.checkpointVersion !== 4 || migration.recoveryIntegrity !== 'ok' || migration.reopenedRecoveryIntegrity !== 'ok' || migration.resourceRoundTrip !== true || migration.resourceReleased !== true) throw new Error('Chrome REL-02 forward migration, native object tag, reopen, checkpoint, or resource evidence is invalid.')
if (!report.rollback || report.rollback.rejected !== true || report.rollback.interruptedAfterMarker !== true || !/interruption after schema version 3/.test(report.rollback.error || '') || JSON.stringify(report.rollback.appliedVersionsAfterFailure) !== '[1]') throw new Error('Chrome REL-02 SQLite interruption rollback evidence is invalid.')
if (!report.cleanup || !Array.isArray(report.cleanup.removedFiles) || report.cleanup.removedFiles.length === 0 || report.cleanup.removedFiles.some((name) => typeof name !== 'string' || !name.startsWith(report.cleanup.databasePath.slice(1)))) throw new Error('Chrome REL-02 OPFS database cleanup evidence is invalid.')
console.log(JSON.stringify({ status: 'chrome-opfs-migration-pass', browserId: report.browserId, migration, cleanup: report.cleanup }, null, 2))

View File

@@ -0,0 +1,60 @@
import { access, readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { resolve, isAbsolute } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sdkRoot = process.env.FREECAD_WASM_SDK_DIR ? resolve(root, process.env.FREECAD_WASM_SDK_DIR) : ''
const required = (value, name) => {
if (typeof value !== 'string' || !value.trim()) throw new Error(`FreeCAD WASM SDK manifest requires ${name}.`)
return value.trim()
}
const exists = async (path) => access(path).then(() => true).catch(() => false)
const sha256 = async (path) => createHash('sha256').update(await readFile(path)).digest('hex')
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) }
if (!sdkRoot) {
console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the shipped Worker remains OCCT-only.' }, null, 2))
process.exit(0)
}
const manifestPath = resolve(sdkRoot, 'manifest.json')
if (!await exists(manifestPath)) fail(`missing manifest: ${manifestPath}`)
let manifest
try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')) } catch (error) { fail(`manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`) }
if (manifest.schemaVersion !== 1) fail(`unsupported manifest schema ${String(manifest.schemaVersion)}.`)
if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freecadVersion must be 1.1.1.')
if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.')
if (required(manifest.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.')
if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.')
const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.')
const resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir))
const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.')
const libraryNames = new Set(libraries.map((library) => library?.name))
for (const name of ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`)
const namingBridge = manifest.namingBridge && typeof manifest.namingBridge === 'object' ? manifest.namingBridge : fail('namingBridge is required.')
const bridgePath = required(namingBridge.source, 'namingBridge.source')
const bridge = isAbsolute(bridgePath) ? bridgePath : resolve(sdkRoot, bridgePath)
if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`)
if (!Array.isArray(namingBridge.exports) || !['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.')
for (const path of resolvedIncludeDirs) {
if (!await exists(path)) fail(`missing include directory: ${path}`)
}
for (const library of libraries) {
if (!library || typeof library !== 'object') fail('libraries entries must be objects.')
const name = required(library.name, 'libraries[].name')
const pathValue = required(library.path, `libraries[${name}].path`)
const path = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue)
if (!path.endsWith('.a')) fail(`library ${name} must be a static .a archive.`)
if (!await exists(path)) fail(`missing static library ${name}: ${path}`)
if ((await readFile(path)).byteLength === 0) fail(`static library ${name} is empty.`)
if (typeof library.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(library.sha256)) fail(`library ${name} requires a lowercase SHA-256.`)
const actualHash = await sha256(path)
if (actualHash !== library.sha256) fail(`library ${name} hash mismatch: expected ${library.sha256}, got ${actualHash}.`)
}
for (const header of ['App/StringHasher.h', 'App/MappedName.h', 'App/ElementMap.h']) {
if (!(await Promise.all(resolvedIncludeDirs.map((includeDir) => exists(resolve(includeDir, header))))).some(Boolean)) fail(`missing locked FreeCAD private header in includeDirs: ${header}`)
}
const bridgeSha = await sha256(bridge)
if (typeof namingBridge.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.sha256)) fail('namingBridge.sha256 requires a lowercase SHA-256.')
if (namingBridge.sha256 !== bridgeSha) fail(`naming bridge hash mismatch: expected ${namingBridge.sha256}, got ${bridgeSha}.`)
console.log(JSON.stringify({ status: 'sdk-ready', availability: 'available', systemExact: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, exports: [...namingBridge.exports] } }, null, 2))

View File

@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto'
import { access, readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(new URL('..', import.meta.url).pathname)
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm']
@@ -27,4 +28,56 @@ const workerClient = await readFile(resolve(root, 'src/facade/nativeHistoryWorke
if (!workerClient.includes("'/native/occt-history/bitbybit-occt-history.js'")) throw new Error('Native history Worker client URL does not match the published artifact.')
const files = []
for (const name of artifactNames) files.push({ name, sha256: hash(await readFile(resolve(publicRoot, name))) })
console.log(JSON.stringify({ status: 'artifact-pass', files }, null, 2))
let namingAbi = { availability: 'unavailable', callbacks: [] }
const factoryModule = await import(pathToFileURL(resolve(distRoot, 'bitbybit-occt-history.js')).href)
const factory = factoryModule.default
if (typeof factory !== 'function') throw new TypeError('OCCT history artifact default export is not a factory.')
const nativeModule = await factory({ locateFile: (fileName) => resolve(distRoot, fileName.split('/').at(-1)) })
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].filter((name) => typeof nativeModule[name] === 'function')
if (callbacks.length !== 0 && callbacks.length !== 3) throw new Error(`FreeCAD naming ABI is partially exported: ${callbacks.join(', ')}.`)
if (callbacks.length === 3) {
const abiVersion = nativeModule.freecadNamingAbiVersion()
const descriptor = JSON.parse(nativeModule.freecadNamingCapabilitiesJson())
if (abiVersion !== 1 || descriptor.schemaVersion !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || descriptor.mappedNameRef !== true || descriptor.stringHasher !== true || descriptor.elementMap2 !== true || !Array.isArray(descriptor.operations) || !descriptor.operations.includes('cut')) throw new Error('FreeCAD naming ABI descriptor is not the locked v1 contract with Cut support.')
let object
let tool
let evidence
try {
object = nativeModule.makeBox(10, 10, 10)
tool = nativeModule.makeBox(5, 5, 5)
const objectStep = nativeModule.shapeToStep(object)
const toolStep = nativeModule.shapeToStep(tool)
const history = nativeModule.booleanHistoryFromStep(objectStep, toolStep, 'cut')
const request = {
schemaVersion: 1,
requestId: 'artifact-private-naming-probe',
documentId: 'artifact-probe-document',
documentVersion: 1,
operationId: 'artifact-probe-cut',
operation: 'cut',
stageId: 'artifact-probe:stage:0',
resultObjectId: 'artifact-probe:result',
resultObjectTag: 3,
inputs: [
{ inputId: 'object', objectId: 'artifact-probe:object', role: 'object', step: objectStep, objectTag: 1 },
{ inputId: 'tool', objectId: 'artifact-probe:tool', role: 'tool', step: toolStep, objectTag: 2 },
],
stages: [{ stageId: 'artifact-probe:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
resultStep: history.resultStep,
resultBrep: history.resultBrep,
history,
}
evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request)))
} finally {
object?.delete?.()
tool?.delete?.()
}
if (!evidence || evidence.schemaVersion !== 1 || evidence.stageId !== 'artifact-probe:stage:0' || evidence.resultObjectId !== 'artifact-probe:result' || !['native-evidence', 'ambiguous'].includes(evidence.status) || !Array.isArray(evidence.mappedNames) || evidence.mappedNames.length === 0 || !evidence.stringHasher || !evidence.elementMap2) throw new Error('FreeCAD naming ABI did not return complete stage-bound Cut evidence.')
namingAbi = { availability: 'available', callbacks, abiVersion, descriptor, evidenceProbe: { operation: 'cut', status: evidence.status, mappedNames: evidence.mappedNames.length, stringHasher: true, elementMap2: true } }
}
const abiContract = JSON.parse(await readFile(resolve(root, 'config/freecad-sketcher-partdesign-abi-contract.json'), 'utf8'))
const expectedImplementation = abiContract.privateNamingAbi?.shippedWorkerImplementation
if (expectedImplementation === 'not-linked' && namingAbi.availability !== 'unavailable') throw new Error('OCCT history artifact exports FreeCAD naming callbacks but the shipped-worker contract still says not-linked.')
if (expectedImplementation === 'freecad-linked' && namingAbi.availability !== 'available') throw new Error('Shipped-worker contract claims FreeCAD linkage but the artifact ABI probe is unavailable.')
if (!['not-linked', 'freecad-linked'].includes(expectedImplementation)) throw new Error('Shipped-worker FreeCAD naming implementation status is invalid.')
console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'probe-only; FreeCAD evidence still requires locked descriptor and response validation' : 'occt-only; EX-TSN-02 remains in_progress and systemExact=false' }, null, 2))

View File

@@ -59,7 +59,7 @@ if (sketchFuzz.status !== 'sketch-solver-fuzz-pass' || sketchFuzz.models !== 200
if (performance.status !== 'pass' || performance.benchmark?.objects !== 1000 || performance.benchmark.triangles !== 1_000_000 || performance.benchmark.tableCells !== 100_000 || performance.benchmark.pass !== true || performance.afterRelease?.shapeCount !== 0) throw new Error('QA-05 performance and ownership evidence is incomplete.')
if (fault.status !== 'pass' || fault.lifecycle?.completed !== 'completed' || fault.lifecycle.cancelled !== 'cancelled' || fault.lifecycle.crashInjected !== true || fault.lifecycle.recovered !== 'completed' || fault.lifecycle.stale !== 'stale' || fault.opfs?.markerRemoved !== true) throw new Error('QA-06 Worker/WASM fault evidence is incomplete.')
if (opfsMigration.status !== 'pass' || opfsMigration.migration?.migratedSchemaVersion !== 6 || opfsMigration.migration.recoveryIntegrity !== 'ok' || opfsMigration.rollback?.rejected !== true) throw new Error('QA-06 OPFS recovery evidence is incomplete.')
if (opfsMigration.status !== 'pass' || opfsMigration.migration?.migratedSchemaVersion !== 7 || opfsMigration.migration.reopenedNativeObjectTag !== 17 || opfsMigration.migration.recoveryIntegrity !== 'ok' || opfsMigration.rollback?.rejected !== true) throw new Error('QA-06 OPFS recovery evidence is incomplete.')
if (security.status !== 'pass' || security.checks?.rejectedCases !== 4 || security.checks.pass !== true || addon.security?.forgedRejected !== true || addon.security.permissionRejected !== true || script.security?.noDynamicExecution !== true || script.security.deniedCapabilities?.length !== 4) throw new Error('QA-07 security evidence is incomplete.')
if (qa08.status !== 'pass' || qa08.locales?.supported !== 3 || qa08.accessibility?.pass !== true || app.keyboard?.named !== 8 || app.screenReader?.namedControls !== app.screenReader?.controls || app.screenReader?.unnamedControls !== 0 || app.mobile?.bodyHorizontalOverflow > 1) throw new Error('QA-08 accessibility/i18n evidence is incomplete.')

View File

@@ -30,6 +30,6 @@ await access(resolve(root, 'docs/release-runbook.zh-CN.md')).catch(() => fail('r
if (release.schemaVersion !== 1 || release.signature?.status !== 'signed' || release.signature.algorithm !== 'Ed25519' || !release.signature.keyId || !/^[0-9a-f]{64}$/.test(release.signature.payloadSha256 || '') || release.build?.files?.length === 0) fail('release artifact manifest is not signed and complete.')
if (sbom.bomFormat !== 'CycloneDX' || sbom.components?.length !== audit.metadata?.dependencies?.total) fail('SBOM is missing, stale, or inconsistent with the locked audit inventory.')
if (offline.status !== 'pass' || offline.serviceWorker?.offlineFallback !== true || offline.serviceWorker?.staleCacheRemoved !== true) fail('offline deployment evidence is incomplete.')
if (migration.status !== 'pass' || migration.currentSchemaVersion !== 6 || migration.forward?.length !== 6 || migration.rollback?.length !== 6) fail('migration/rollback evidence is incomplete.')
if (migration.status !== 'pass' || migration.currentSchemaVersion !== 7 || migration.forward?.length !== 7 || migration.rollback?.length !== 7) fail('migration/rollback evidence is incomplete.')
if (performance.status !== 'pass' || performance.benchmark?.pass !== true || security.status !== 'pass' || security.checks?.pass !== true || quality.status !== 'quality-closure-pass') fail('quality evidence is incomplete.')
console.log(JSON.stringify({ status: 'release-closure-pass', gates: matrix.gates.length, modules: coverage.modules.length, moduleCounts: matrix.moduleCounts, signed: true, sbomComponents: sbom.components.length, migrationVersions: migration.currentSchemaVersion }, null, 2))