feat: add candidate FreeCAD naming SDK and attachment oracles
Build and verify the candidate-only FreeCAD naming bridge and OCCT worker path, including three-stage StringHasher restoration. Add Datum, ShapeBinder, attachment-mode, and PartDesign structure oracles plus offline SDK build plans and CI boundary checks.
This commit is contained in:
103
scripts/check-freecad-naming-sdk-readiness.mjs
Normal file
103
scripts/check-freecad-naming-sdk-readiness.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { execFile } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
REQUIRED_FREECAD_NAMING_CALLBACKS,
|
||||
REQUIRED_FREECAD_NAMING_DEFINITIONS,
|
||||
REQUIRED_FREECAD_NAMING_LIBRARIES,
|
||||
exists,
|
||||
inspectIncludeDirectory,
|
||||
inspectPlannedLibrary,
|
||||
inspectPlannedRuntimeAsset,
|
||||
loadSdkPlan,
|
||||
resolveFrom,
|
||||
sha256File,
|
||||
} from './freecad-naming-sdk-lib.mjs'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const execFileAsync = promisify(execFile)
|
||||
const plan = await loadSdkPlan(root)
|
||||
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK readiness: ${message}`) }
|
||||
if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.')
|
||||
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.')
|
||||
if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.')
|
||||
if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.')
|
||||
if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.')
|
||||
if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')
|
||||
|
||||
const libraries = []
|
||||
for (const library of plan.libraries) libraries.push(await inspectPlannedLibrary(root, library))
|
||||
const linkDependencies = []
|
||||
for (const library of plan.linkDependencies ?? []) linkDependencies.push(await inspectPlannedLibrary(root, library))
|
||||
const includeDirs = []
|
||||
for (const includeDir of plan.includeDirs ?? []) includeDirs.push(await inspectIncludeDirectory(root, includeDir))
|
||||
const runtimeAssets = []
|
||||
for (const asset of plan.runtimeAssets ?? []) runtimeAssets.push(await inspectPlannedRuntimeAsset(root, asset))
|
||||
const freeCadInclude = plan.includeDirs.find(({ name }) => name === 'FreeCAD')
|
||||
let sourceCommit = null
|
||||
if (freeCadInclude && await exists(resolveFrom(root, freeCadInclude.path))) {
|
||||
try {
|
||||
sourceCommit = (await execFileAsync('git', ['-C', resolveFrom(root, freeCadInclude.path), 'rev-parse', 'HEAD'])).stdout.trim()
|
||||
} catch (error) {
|
||||
fail(`cannot inspect FreeCAD source commit: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
if (sourceCommit !== plan.baseline.sourceCommit) fail(`FreeCAD source commit mismatch: expected ${plan.baseline.sourceCommit}, got ${sourceCommit}.`)
|
||||
}
|
||||
const bridgePath = resolveFrom(root, plan.namingBridge.path)
|
||||
const hostAdapterPath = resolveFrom(root, plan.namingBridge.hostAdapter)
|
||||
const forceIncludePath = resolveFrom(root, plan.compileOptions.forceInclude)
|
||||
const bridgePresent = await exists(bridgePath)
|
||||
const hostAdapterPresent = await exists(hostAdapterPath)
|
||||
const forceIncludePresent = await exists(forceIncludePath)
|
||||
let bridgeExports = []
|
||||
if (bridgePresent) {
|
||||
const source = await readFile(bridgePath, 'utf8')
|
||||
bridgeExports = REQUIRED_FREECAD_NAMING_CALLBACKS.filter((callback) => source.includes(callback))
|
||||
if (bridgeExports.length !== REQUIRED_FREECAD_NAMING_CALLBACKS.length) fail('present naming bridge source omits required callback names.')
|
||||
}
|
||||
if (hostAdapterPresent) {
|
||||
const source = await readFile(hostAdapterPath, 'utf8')
|
||||
if (!source.includes('Module.preRun') || !source.includes('FREECAD_USER_HOME')) fail('host adapter does not initialize the FreeCAD virtual user environment.')
|
||||
}
|
||||
const missingLibraries = libraries.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingLinkDependencies = linkDependencies.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingIncludes = includeDirs.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const missingRuntimeAssets = runtimeAssets.filter(({ status }) => status !== 'verified').map(({ name }) => name)
|
||||
const blockers = [
|
||||
...missingLibraries.map((name) => `${name} wasm static library`),
|
||||
...missingLinkDependencies.map((name) => `${name} wasm link dependency`),
|
||||
...missingIncludes.map((name) => `${name} headers`),
|
||||
...missingRuntimeAssets.map((name) => `${name} runtime asset`),
|
||||
...(!bridgePresent ? ['FreeCAD private naming Worker bridge'] : []),
|
||||
...(!hostAdapterPresent ? ['FreeCAD naming host adapter'] : []),
|
||||
...(!forceIncludePresent ? ['FreeCAD wasm force-include header'] : []),
|
||||
]
|
||||
console.log(JSON.stringify({
|
||||
status: blockers.length === 0 ? 'sdk-readiness-complete' : 'sdk-readiness-incomplete',
|
||||
availability: blockers.length === 0 ? 'candidate-complete' : 'unavailable',
|
||||
baseline: plan.baseline,
|
||||
libraries,
|
||||
linkDependencies,
|
||||
includeDirs,
|
||||
runtimeAssets,
|
||||
sourceCommit,
|
||||
namingBridge: {
|
||||
path: plan.namingBridge.path,
|
||||
status: bridgePresent ? 'verified' : 'missing',
|
||||
sha256: bridgePresent ? await sha256File(bridgePath) : null,
|
||||
hostAdapter: plan.namingBridge.hostAdapter,
|
||||
hostAdapterSha256: hostAdapterPresent ? await sha256File(hostAdapterPath) : null,
|
||||
exports: bridgeExports,
|
||||
},
|
||||
compileOptions: {
|
||||
...plan.compileOptions,
|
||||
forceIncludeSha256: forceIncludePresent ? await sha256File(forceIncludePath) : null,
|
||||
},
|
||||
blockers,
|
||||
readyLibraries: libraries.length - missingLibraries.length,
|
||||
requiredLibraries: libraries.length,
|
||||
publishToWorker: false,
|
||||
boundary: plan.boundary,
|
||||
}, null, 2))
|
||||
Reference in New Issue
Block a user