Files
Web_FreeCAD_Bitbybit/scripts/check-chrome-fcstd-golden-verification.mjs

90 lines
11 KiB
JavaScript

import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { loadGoldenManifest } from './freecad-golden-contract.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/chrome-fcstd-golden-verification.json'), 'utf8'))
const manifest = await loadGoldenManifest(resolve(root, 'fixtures/freecad-golden/manifest.json'))
const fail = (message) => { throw new Error(`Chrome FCStd golden verification: ${message}`) }
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1' || report.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('FreeCAD baseline is not locked.')
if (report.bitbybitVersion !== '1.1.1' || report.geometryProvider !== 'Bitbybit OCCT') fail('Bitbybit 1.1.1 is not the sole geometry provider.')
if (report.browserId !== 'chrome' || !/^Chrome\//.test(report.browser?.product || '')) fail('the report must contain Chrome-only browser evidence.')
if (report.unknownDifferencesFail !== true || report.status !== 'pass') fail('unknown differences are not release-blocking.')
if (report.scenarioCount !== 100 || !Array.isArray(report.reports) || report.reports.length !== 100 || manifest.scenarios.length !== 100) fail('exactly 100 scenarios are required.')
if (!Array.isArray(report.pageErrors) || report.pageErrors.length !== 0) fail('Chrome page errors are present.')
if (report.productRecompute?.status !== 'pass') fail('the real product recompute probe did not pass.')
for (const [name, result] of Object.entries({ placedBox: report.productRecompute.placedBox, cut: report.productRecompute.cut, sphereTrim: report.productRecompute.sphereTrim, torus: report.productRecompute.torus, prism: report.productRecompute.prism, wedge: report.productRecompute.wedge, ellipsoid: report.productRecompute.ellipsoid })) {
if (!result?.quality?.structuralValid || result.quality.isNull || result.quality.structuralErrors !== 0 || result.quality.solids !== 1 || !(result.massProperties?.volume > 0)) fail(`product recompute ${name} Shape is invalid.`)
if (result.comparison?.status !== 'pass' || result.comparison.differences?.length !== 0) fail(`product recompute ${name} differs from FreeCAD.`)
}
if (report.productRecompute.placedBox.quality.boundingBox.min.some((value, index) => Math.abs(value - [3.9999999, 3.9999999, -1e-7][index]) > 1e-12) || report.productRecompute.placedBox.quality.boundingBox.max.some((value, index) => Math.abs(value - [6.0000001, 14.0000001, 5.0000001][index]) > 1e-12)) fail('the product Part::Box coordinate/translation mapping drifted.')
if (report.productRecompute.cut.comparison.knownDifferences?.length !== 1 || report.productRecompute.cut.comparison.knownDifferences[0].classification !== 'kernel-container-normalization') fail('the product Part::Cut container normalization is not explicitly classified.')
if (report.productRecompute.sphereTrim.comparison.knownDifferences?.length !== 1 || report.productRecompute.sphereTrim.comparison.knownDifferences[0].classification !== 'kernel-container-normalization' || report.productRecompute.sphereTrim.comparison.knownDifferences[0].expected !== 'solid' || report.productRecompute.sphereTrim.comparison.knownDifferences[0].actual !== 'compound') fail('the product Part::Sphere trim container normalization is not explicitly classified.')
const chromeTorusBound = 12.988706503508727
if (report.productRecompute.torus.comparison.knownDifferences?.length !== 0 || Math.abs(report.productRecompute.torus.massProperties.volume - 60 * Math.PI ** 2) > 1e-6 || report.productRecompute.torus.quality.boundingBox.min.some((value, index) => Math.abs(value - [-chromeTorusBound, -chromeTorusBound, -2.0000001][index]) > 1e-9) || report.productRecompute.torus.quality.boundingBox.max.some((value, index) => Math.abs(value - [chromeTorusBound, chromeTorusBound, 2.0000001][index]) > 1e-9)) fail('the product Part::Torus result differs from FreeCAD.')
const prism = report.productRecompute.prism
if (prism.comparison.knownDifferences?.length !== 0 || prism.quality.faces !== 8 || prism.quality.edges !== 18 || prism.quality.vertices !== 12 || Math.abs(prism.massProperties.volume - 60 * Math.sqrt(3)) > 1e-6 || prism.quality.boundingBox.min.some((value, index) => Math.abs(value - [-2.0000001, -2.606937542828117, -1e-7][index]) > 1e-9) || prism.quality.boundingBox.max.some((value, index) => Math.abs(value - [3.76326990708465, 1.7320509075688775, 10.0000001][index]) > 1e-9)) fail('the product Part::Prism result differs from FreeCAD.')
const wedge = report.productRecompute.wedge
if (wedge.comparison.knownDifferences?.length !== 0 || wedge.quality.faces !== 6 || wedge.quality.edges !== 12 || wedge.quality.vertices !== 8 || Math.abs(wedge.massProperties.volume - 2440 / 3) > 1e-6 || wedge.quality.boundingBox.min.some((value, index) => Math.abs(value - [-1e-7, -1e-7, -1e-7][index]) > 1e-9) || wedge.quality.boundingBox.max.some((value, index) => Math.abs(value - [10.0000001, 10.0000001, 10.0000001][index]) > 1e-9)) fail('the product Part::Wedge result differs from FreeCAD.')
const ellipsoid = report.productRecompute.ellipsoid
if (ellipsoid.comparison.knownDifferences?.length !== 0 || ellipsoid.quality.faces !== 1 || ellipsoid.quality.edges !== 3 || ellipsoid.quality.vertices !== 2 || Math.abs(ellipsoid.massProperties.volume - 133.9826640573845) > 1e-6 || ellipsoid.quality.boundingBox.min.some((value, index) => Math.abs(value - [-8.0000001, -6.92820333027551, -2.0000001][index]) > 1e-9) || ellipsoid.quality.boundingBox.max.some((value, index) => Math.abs(value - [4.0000001, 6.928203330275507, 2.0000001][index]) > 1e-9)) fail('the product Part::Ellipsoid result differs from FreeCAD.')
if (report.productRecompute.afterRelease?.shapeCount !== 0 || report.productRecompute.afterRelease?.kernelReferenceCount !== 0) fail('the product recompute probe leaked Shape ownership.')
const expectedIds = new Set(manifest.scenarios.map((entry) => entry.id))
const seen = new Set()
let knownDifferences = 0
let totalArchiveBytes = 0
let totalBrepBytes = 0
const formatSummary = { step: { passed: 0, totalBytes: 0, maximumVolumeDelta: 0, topologyNormalizations: 0, importedShapeTypes: {} }, iges: { passed: 0, totalBytes: 0, maximumVolumeDelta: 0, topologyNormalizations: 0, importedShapeTypes: {} }, brep: { passed: 0, totalBytes: 0, maximumVolumeDelta: 0, topologyNormalizations: 0, importedShapeTypes: {} } }
for (const item of report.reports) {
if (!expectedIds.has(item.id) || seen.has(item.id)) fail(`unexpected or duplicate scenario '${item.id}'.`)
seen.add(item.id)
if (item.status !== 'pass') fail(`${item.id} did not pass.`)
if (!item.source?.quality?.structuralValid || item.source.quality.isNull || item.source.quality.structuralErrors !== 0 || item.source.quality.solids !== 1) fail(`${item.id} source Shape is invalid.`)
if (!item.imported?.quality?.structuralValid || item.imported.quality.isNull || item.imported.quality.structuralErrors !== 0 || item.imported.quality.solids !== 1) fail(`${item.id} imported Shape is invalid.`)
for (const format of ['step', 'iges', 'brep']) {
const exchange = item.formatRoundTrips?.[format]
const expectedUnit = format === 'brep' ? 'unknown' : 'millimeter'
if (!exchange || exchange.status !== 'pass' || exchange.declaredUnit !== expectedUnit || !Number.isSafeInteger(exchange.sourceBytes) || exchange.sourceBytes <= 0 || !Number.isSafeInteger(exchange.importedBytes) || exchange.importedBytes <= 0 || !['solid', 'compound', 'shell', 'face'].includes(exchange.importedShapeType) || exchange.importedSolids > exchange.sourceSolids || !['none', 'format-topology-normalization'].includes(exchange.structuralNormalization) || Math.abs(exchange.importedVolume - exchange.sourceVolume) > 1e-6) fail(`${item.id}.${format} format round-trip evidence is invalid.`)
const aggregate = formatSummary[format]
aggregate.passed += 1
aggregate.totalBytes += exchange.sourceBytes
aggregate.maximumVolumeDelta = Math.max(aggregate.maximumVolumeDelta, Math.abs(exchange.importedVolume - exchange.sourceVolume))
if (exchange.structuralNormalization === 'format-topology-normalization') aggregate.topologyNormalizations += 1
aggregate.importedShapeTypes[exchange.importedShapeType] = (aggregate.importedShapeTypes[exchange.importedShapeType] || 0) + 1
}
if (!Number.isFinite(item.source.massProperties?.volume) || item.source.massProperties.volume <= 0 || !Number.isFinite(item.imported.massProperties?.volume) || item.imported.massProperties.volume <= 0) fail(`${item.id} has invalid mass properties.`)
if (!Number.isSafeInteger(item.fcstd?.archiveBytes) || item.fcstd.archiveBytes <= 0 || !Number.isSafeInteger(item.fcstd.brepBytes) || item.fcstd.brepBytes <= 0 || !/^[a-f0-9]{8}$/.test(item.fcstd.brepHash) || item.fcstd.shapeResources !== 1 || item.fcstd.references !== 1) fail(`${item.id} has invalid FCStd/BRep evidence.`)
totalArchiveBytes += item.fcstd.archiveBytes
totalBrepBytes += item.fcstd.brepBytes
for (const [name, comparison] of Object.entries(item.comparisons || {})) {
if (comparison.status !== 'pass' || !Array.isArray(comparison.differences) || comparison.differences.length !== 0 || !Array.isArray(comparison.knownDifferences)) fail(`${item.id}.${name} contains an unknown difference.`)
for (const difference of comparison.knownDifferences) {
const booleanOperation = ['cut', 'fuse', 'common'].includes(item.operation)
if (!booleanOperation || difference.classification !== 'kernel-container-normalization' || difference.domain !== 'identity' || difference.path !== 'shapeType' || difference.expected !== 'compound' || difference.actual !== 'solid') fail(`${item.id}.${name} contains an unclassified known difference.`)
knownDifferences += 1
}
}
}
if (seen.size !== expectedIds.size) fail('one or more locked scenarios are missing.')
const expectedOperationCounts = { box: 21, common: 8, cone: 16, cut: 10, cylinder: 21, fuse: 8, sphere: 16 }
if (report.summary?.passed !== 100 || report.summary.failed !== 0 || report.summary.unknownDifferences !== 0 || report.summary.knownDifferences !== knownDifferences || JSON.stringify(report.summary.operationCounts) !== JSON.stringify(expectedOperationCounts)) fail('summary counts do not match the locked corpus.')
if (report.summary.totalArchiveBytes !== totalArchiveBytes || report.summary.totalBrepBytes !== totalBrepBytes || totalArchiveBytes <= 0 || totalBrepBytes <= 0) fail('aggregate FCStd resource sizes are inconsistent.')
const canonicalFormatSummary = (summary) => Object.fromEntries(Object.entries(summary).map(([format, value]) => [format, { ...value, importedShapeTypes: Object.fromEntries(Object.entries(value.importedShapeTypes).sort(([left], [right]) => left.localeCompare(right))) }]))
if (JSON.stringify(canonicalFormatSummary(report.summary.formatRoundTrips)) !== JSON.stringify(canonicalFormatSummary(formatSummary))) fail('aggregate STEP/IGES/BREP tolerance evidence is inconsistent.')
if (report.afterRelease?.shapeCount !== 0 || report.afterRelease?.kernelReferenceCount !== 0) fail('ShapeHandle or kernel references leaked.')
console.log(JSON.stringify({
status: 'chrome-fcstd-golden-verification-pass',
scenarioCount: report.scenarioCount,
operationCounts: report.summary.operationCounts,
knownDifferences,
unknownDifferences: 0,
totalArchiveBytes,
totalBrepBytes,
productRecompute: { status: report.productRecompute.status, afterRelease: report.productRecompute.afterRelease },
afterRelease: report.afterRelease,
}, null, 2))