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.
107 lines
11 KiB
JavaScript
107 lines
11 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { createReadStream, existsSync } from 'node:fs'
|
|
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
import { createServer } from 'node:http'
|
|
import { extname, join, normalize, resolve } from 'node:path'
|
|
import { spawn } from 'node:child_process'
|
|
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
|
|
|
|
const root = resolve(new URL('..', import.meta.url).pathname)
|
|
const candidateRoot = resolve(root, '.cache/candidates/freecad-naming-worker')
|
|
const reportPath = resolve(root, '.cache/toolchains/freecad-naming-sdk/chrome-candidate-worker-report.json')
|
|
const chrome = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
|
|
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
|
|
for (const name of artifactNames) if (!existsSync(resolve(candidateRoot, name))) throw new Error(`Missing candidate Worker artifact: ${name}`)
|
|
if (!existsSync(chrome)) throw new Error(`Chrome executable is unavailable: ${chrome}`)
|
|
|
|
const artifacts = await Promise.all(artifactNames.map(async (name) => {
|
|
const path = resolve(candidateRoot, name)
|
|
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
|
|
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
|
|
}))
|
|
let resolveReport
|
|
let rejectReport
|
|
const reportPromise = new Promise((resolveValue, rejectValue) => { resolveReport = resolveValue; rejectReport = rejectValue })
|
|
const html = `<!doctype html><meta charset="utf-8"><title>FreeCAD naming candidate Worker</title><script type="module">
|
|
const send = async (report) => fetch('/__report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
|
|
try {
|
|
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Chrome candidate harness requires cross-origin isolation and SharedArrayBuffer.')
|
|
const createCandidate = (await import('/candidate/bitbybit-occt-history.js')).default
|
|
const candidate = await createCandidate({ locateFile: (path) => new URL('/candidate/' + path, location.href).href })
|
|
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
|
|
if (!callbacks.every((name) => typeof candidate[name] === 'function')) throw new Error('Candidate module omits a FreeCAD naming callback.')
|
|
const descriptor = JSON.parse(candidate.freecadNamingCapabilitiesJson())
|
|
if (candidate.freecadNamingAbiVersion() !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Candidate naming ABI descriptor is not locked.')
|
|
const object = candidate.makeBox(10, 10, 10)
|
|
const tool = candidate.makeBox(5, 5, 5)
|
|
const objectStep = candidate.shapeToStep(object)
|
|
const toolStep = candidate.shapeToStep(tool)
|
|
const history = candidate.booleanHistoryFromStep(objectStep, toolStep, 'cut')
|
|
const record = history.records.find((entry) => entry.relation !== 'deleted' && Number.isSafeInteger(entry.sourceIndex) && (Number.isSafeInteger(entry.resultIndex) || entry.resultIndexes?.some(Number.isSafeInteger)))
|
|
if (!record) throw new Error('Chrome OCCT cut returned no usable history record.')
|
|
const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex : record.resultIndexes.find(Number.isSafeInteger)
|
|
const firstRequest = { schemaVersion: 1, requestId: 'chrome-candidate-1', documentId: 'chrome-candidate-document', documentVersion: 1, operationId: 'chrome-cut-1', operation: 'cut', stageId: 'chrome:stage:1', resultObjectId: 'chrome:result:1', resultObjectTag: 99, inputs: [{ inputId: 'object', objectId: 'source-object', role: 'object', step: objectStep, objectTag: 42 }, { inputId: 'tool', objectId: 'source-tool', role: 'tool', step: toolStep, objectTag: 43 }], stages: [{ stageId: 'chrome:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [{ ...record, resultIndex, resultIndexes: undefined }] } }
|
|
const first = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(firstRequest)))
|
|
if (first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || first.elementMap2?.maps?.length !== 1 || !Array.isArray(first.stringHasher?.entries)) throw new Error('Chrome first-stage naming evidence is incomplete.')
|
|
const secondRequest = { ...firstRequest, requestId: 'chrome-candidate-2', documentVersion: 2, operationId: 'chrome-cut-2', stageId: 'chrome:stage:2', resultObjectId: 'chrome:result:2', resultObjectTag: 100, inputs: [{ inputId: 'object', objectId: 'chrome:result:1', role: 'object', stageId: 'chrome:stage:1', step: history.resultStep, objectTag: 99, namingEvidence: first }], stages: [{ stageId: 'chrome:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] } }
|
|
const second = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(secondRequest)))
|
|
const reference = second.mappedNames?.[0]?.reference
|
|
const hasherIds = new Set(second.stringHasher?.entries?.map((entry) => entry.id))
|
|
const tokens = second.elementMap2?.maps?.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens))) ?? []
|
|
if (second.status !== 'native-evidence' || !reference?.name?.startsWith('#') || !Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !hasherIds.has(reference.prefixStringId) || !tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error('Chrome chained MappedNameRef/StringHasher/ElementMap2 closure is invalid.')
|
|
const thirdRequest = { ...secondRequest, requestId: 'chrome-candidate-3', documentVersion: 3, operationId: 'chrome-cut-3', stageId: 'chrome:stage:3', resultObjectId: 'chrome:result:3', resultObjectTag: 101, inputs: [{ inputId: 'object', objectId: 'chrome:result:2', role: 'object', stageId: 'chrome:stage:2', step: history.resultStep, objectTag: 100, namingEvidence: second }], stages: [{ stageId: 'chrome:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] } }
|
|
const third = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(thirdRequest)))
|
|
const thirdIds = new Set(third.stringHasher?.entries?.map((entry) => entry.id))
|
|
const thirdReference = third.mappedNames?.[0]?.reference
|
|
if (third.status !== 'native-evidence' || !thirdReference?.name?.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdIds.has(id)) || third.stringHasher?.entries?.length <= second.stringHasher?.entries?.length) throw new Error('Chrome candidate did not restore non-empty StringHasher evidence across three stages.')
|
|
const invalid = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify({ ...firstRequest, history: { ...history, records: [] } })))
|
|
if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Chrome candidate did not fail closed for missing history.')
|
|
await send({ schemaVersion: 1, status: 'pass', browserId: 'chrome', crossOriginIsolated, occtVersion: history.occtVersion, callbacks, firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length }, chainedStage: { mappedNames: second.mappedNames.length, stringHasherEntries: second.stringHasher.entries.length }, thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length }, invalidHistoryRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
|
|
} catch (error) {
|
|
await send({ schemaVersion: 1, status: 'failed', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error), candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
|
|
}
|
|
</script>`
|
|
const contentTypes = { '.js': 'text/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream' }
|
|
const server = createServer((request, response) => {
|
|
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
|
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
|
|
if (request.method === 'POST' && request.url === '/__report') {
|
|
let body = ''
|
|
request.setEncoding('utf8')
|
|
request.on('data', (chunk) => { body += chunk })
|
|
request.on('end', () => {
|
|
try { resolveReport(JSON.parse(body)); response.writeHead(204); response.end() }
|
|
catch (error) { rejectReport(error); response.writeHead(400); response.end(String(error)) }
|
|
})
|
|
return
|
|
}
|
|
if (request.url === '/' || request.url === '/index.html') { response.setHeader('content-type', 'text/html'); response.end(html); return }
|
|
if (!request.url?.startsWith('/candidate/')) { response.writeHead(404); response.end('not found'); return }
|
|
const file = normalize(join(candidateRoot, decodeURIComponent(request.url.slice('/candidate/'.length))))
|
|
if (!file.startsWith(candidateRoot) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
|
|
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
|
|
createReadStream(file).pipe(response)
|
|
})
|
|
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
|
|
const port = server.address().port
|
|
const profile = await createChromeProfile('freecad-naming-candidate')
|
|
const child = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] })
|
|
let stderr = ''
|
|
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
|
|
const timeout = setTimeout(() => { rejectReport(new Error(`Chrome candidate Worker harness timed out. ${stderr.slice(-2000)}`)); child.kill('SIGTERM') }, 240_000)
|
|
let report
|
|
try {
|
|
report = await reportPromise
|
|
report = { ...report, artifacts, generatedAt: new Date().toISOString() }
|
|
await mkdir(resolve(reportPath, '..'), { recursive: true })
|
|
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
console.log(JSON.stringify(report, null, 2))
|
|
if (report.status !== 'pass') process.exitCode = 1
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
child.kill('SIGTERM')
|
|
await new Promise((resolveServer) => server.close(resolveServer))
|
|
await removeChromeProfile(profile)
|
|
}
|