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:
126
scripts/freecad-naming-sdk-lib.mjs
Normal file
126
scripts/freecad-naming-sdk-lib.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
import { access, open, readFile } from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { isAbsolute, resolve } from 'node:path'
|
||||
|
||||
export const REQUIRED_FREECAD_NAMING_LIBRARIES = ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']
|
||||
export const REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES = ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3']
|
||||
export const REQUIRED_FREECAD_NAMING_CALLBACKS = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
|
||||
export const REQUIRED_FREECAD_NAMING_DEFINITIONS = ['__linux__=1', 'QT_NO_KEYWORDS', 'HAVE_CONFIG_H', 'PYCXX_6_2_COMPATIBILITY']
|
||||
|
||||
export const exists = async (path) => access(path).then(() => true).catch(() => false)
|
||||
|
||||
export const resolveFrom = (base, path) => isAbsolute(path) ? path : resolve(base, path)
|
||||
|
||||
export const sha256File = async (path) => new Promise((resolveHash, reject) => {
|
||||
const hash = createHash('sha256')
|
||||
const stream = createReadStream(path)
|
||||
stream.on('error', reject)
|
||||
stream.on('data', (chunk) => hash.update(chunk))
|
||||
stream.on('end', () => resolveHash(hash.digest('hex')))
|
||||
})
|
||||
|
||||
const readAt = async (handle, length, position) => {
|
||||
const buffer = Buffer.alloc(length)
|
||||
const { bytesRead } = await handle.read(buffer, 0, length, position)
|
||||
return buffer.subarray(0, bytesRead)
|
||||
}
|
||||
|
||||
const isWasmObject = (bytes) => bytes.length >= 8
|
||||
&& bytes.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d]))
|
||||
&& bytes.subarray(4, 8).equals(Buffer.from([0x01, 0x00, 0x00, 0x00]))
|
||||
const isElfObject = (bytes) => bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
|
||||
const isLlvmBitcode = (bytes) => bytes.length >= 4
|
||||
&& (bytes.subarray(0, 4).equals(Buffer.from([0x42, 0x43, 0xc0, 0xde]))
|
||||
|| bytes.subarray(0, 4).equals(Buffer.from([0xde, 0xc0, 0x17, 0x0b])))
|
||||
|
||||
export async function inspectWasmStaticArchive(path) {
|
||||
const handle = await open(path, 'r')
|
||||
try {
|
||||
const stat = await handle.stat()
|
||||
if (stat.size < 68) throw new Error('archive is empty or truncated')
|
||||
const magic = await readAt(handle, 8, 0)
|
||||
if (magic.toString('ascii') !== '!<arch>\n') throw new Error('file is not a regular static ar archive')
|
||||
let offset = 8
|
||||
let memberCount = 0
|
||||
let wasmObjectCount = 0
|
||||
let llvmBitcodeCount = 0
|
||||
let hostObjectCount = 0
|
||||
let unsupportedMemberCount = 0
|
||||
while (offset + 60 <= stat.size) {
|
||||
const header = await readAt(handle, 60, offset)
|
||||
if (header.length !== 60 || header.subarray(58, 60).toString('ascii') !== '`\n') throw new Error(`invalid ar member header at byte ${offset}`)
|
||||
const rawName = header.subarray(0, 16).toString('ascii').trim()
|
||||
const sizeText = header.subarray(48, 58).toString('ascii').trim()
|
||||
if (!/^\d+$/.test(sizeText)) throw new Error(`invalid ar member size at byte ${offset}`)
|
||||
const memberSize = Number(sizeText)
|
||||
const dataOffset = offset + 60
|
||||
if (!Number.isSafeInteger(memberSize) || memberSize < 0 || dataOffset + memberSize > stat.size) throw new Error(`ar member exceeds archive size at byte ${offset}`)
|
||||
const special = rawName === '/' || rawName === '//' || rawName === '/SYM64/'
|
||||
if (!special) {
|
||||
memberCount += 1
|
||||
const extendedName = rawName.startsWith('#1/') ? Number(rawName.slice(3)) : 0
|
||||
if (!Number.isSafeInteger(extendedName) || extendedName < 0 || extendedName > memberSize) throw new Error(`invalid BSD ar filename at byte ${offset}`)
|
||||
const prefix = await readAt(handle, Math.min(8, memberSize - extendedName), dataOffset + extendedName)
|
||||
if (isWasmObject(prefix)) wasmObjectCount += 1
|
||||
else if (isLlvmBitcode(prefix)) llvmBitcodeCount += 1
|
||||
else if (isElfObject(prefix)) hostObjectCount += 1
|
||||
else unsupportedMemberCount += 1
|
||||
}
|
||||
offset = dataOffset + memberSize + (memberSize % 2)
|
||||
}
|
||||
if (memberCount === 0) throw new Error('archive has no object members')
|
||||
if (hostObjectCount > 0) throw new Error(`archive contains ${hostObjectCount} host ELF object member(s)`)
|
||||
if (unsupportedMemberCount > 0) throw new Error(`archive contains ${unsupportedMemberCount} unsupported member(s)`)
|
||||
if (wasmObjectCount + llvmBitcodeCount === 0) throw new Error('archive has no wasm or LLVM bitcode object members')
|
||||
return { target: 'wasm32-emscripten', bytes: stat.size, memberCount, wasmObjectCount, llvmBitcodeCount }
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSdkPlan(projectRoot) {
|
||||
const path = resolve(projectRoot, 'config/freecad-naming-sdk-plan.json')
|
||||
return JSON.parse(await readFile(path, 'utf8'))
|
||||
}
|
||||
|
||||
export async function inspectPlannedLibrary(projectRoot, library) {
|
||||
const path = resolveFrom(projectRoot, library.path)
|
||||
if (!await exists(path)) return { name: library.name, path: library.path, status: 'missing' }
|
||||
const archive = await inspectWasmStaticArchive(path)
|
||||
const sha256 = await sha256File(path)
|
||||
if (library.expectedSha256 && sha256 !== library.expectedSha256) throw new Error(`${library.name} hash mismatch: expected ${library.expectedSha256}, got ${sha256}`)
|
||||
return { name: library.name, path: library.path, status: 'verified', sha256, ...archive }
|
||||
}
|
||||
|
||||
export async function inspectIncludeDirectory(projectRoot, entry) {
|
||||
const path = resolveFrom(projectRoot, entry.path)
|
||||
const missingHeaders = []
|
||||
for (const header of entry.requiredHeaders ?? []) if (!await exists(resolve(path, header))) missingHeaders.push(header)
|
||||
return { name: entry.name, path: entry.path, status: missingHeaders.length === 0 ? 'verified' : 'missing', missingHeaders }
|
||||
}
|
||||
|
||||
export async function inspectPlannedRuntimeAsset(projectRoot, asset) {
|
||||
const root = resolveFrom(projectRoot, asset.path)
|
||||
const files = []
|
||||
const missingFiles = []
|
||||
for (const relativePath of asset.requiredFiles ?? []) {
|
||||
const path = resolve(root, relativePath)
|
||||
if (!await exists(path)) {
|
||||
missingFiles.push(relativePath)
|
||||
continue
|
||||
}
|
||||
const sha256 = await sha256File(path)
|
||||
const expectedSha256 = asset.expectedSha256?.[relativePath]
|
||||
if (expectedSha256 && sha256 !== expectedSha256) throw new Error(`${asset.name}/${relativePath} hash mismatch: expected ${expectedSha256}, got ${sha256}`)
|
||||
files.push({ path: relativePath, sha256 })
|
||||
}
|
||||
return {
|
||||
name: asset.name,
|
||||
path: asset.path,
|
||||
preloadTo: asset.preloadTo,
|
||||
status: missingFiles.length === 0 ? 'verified' : 'missing',
|
||||
files,
|
||||
missingFiles,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user