const finite = (value) => typeof value === 'number' && Number.isFinite(value) const toleranceFor = (scenario, path) => path.includes('boundingBox') ? scenario.tolerance.linear : scenario.tolerance.scalar const domainFor = (path) => path.includes('boundingBox') ? 'shape' : ['solids', 'faces', 'edges', 'vertices'].some((name) => path.endsWith(`.${name}`)) ? 'topology' : ['volume', 'area'].some((name) => path.endsWith(`.${name}`)) ? 'shape' : path.includes('objects') || path.includes('tree') ? 'objectTree' : path.includes('properties') ? 'properties' : 'identity' export function compareSemanticResult(scenario, actual, { source = 'FreeCAD', target = 'Web' } = {}) { const differences = [] const comparedDomains = new Set() const compare = (expected, received, path) => { const domain = domainFor(path) comparedDomains.add(domain) if (typeof expected === 'number') { if (!finite(received) || Math.abs(expected - received) > toleranceFor(scenario, path)) differences.push({ kind: 'numeric', classification: 'unknown', domain, path, expected, actual: received, tolerance: toleranceFor(scenario, path) }) return } if (Array.isArray(expected)) { if (!Array.isArray(received) || received.length !== expected.length) differences.push({ kind: 'structure', classification: 'unknown', domain, path, expected: `array(${expected.length})`, actual: Array.isArray(received) ? `array(${received.length})` : typeof received }) else expected.forEach((value, index) => compare(value, received[index], `${path}[${index}]`)) return } if (expected && typeof expected === 'object') { if (!received || typeof received !== 'object' || Array.isArray(received)) { differences.push({ kind: 'structure', classification: 'unknown', domain, path, expected: 'object', actual: typeof received }); return } for (const [key, value] of Object.entries(expected)) compare(value, received[key], `${path}.${key}`) return } if (received !== expected) differences.push({ kind: 'value', classification: 'unknown', domain, path, expected, actual: received }) } compare(scenario.expected, actual, scenario.id) const allowedRuntimeFields = new Set(['schemaVersion', 'fixtureId', 'freecadVersion', 'freecadVersion', 'runtimeStatus']) for (const key of Object.keys(actual || {})) if (!Object.prototype.hasOwnProperty.call(scenario.expected || {}, key) && !allowedRuntimeFields.has(key)) differences.push({ kind: 'field', classification: 'unknown', domain: domainFor(key), path: `${scenario.id}.${key}`, expected: 'absent', actual: actual[key] }) return { schemaVersion: 1, fixtureId: scenario.id, source, target, status: differences.length === 0 ? 'pass' : 'fail-unknown-difference', domains: Object.fromEntries(['identity', 'shape', 'topology', 'objectTree', 'properties'].map((domain) => [domain, comparedDomains.has(domain) ? 'compared' : 'no-fixture-field'])), differences, } }