Files
Web_FreeCAD_Bitbybit/scripts/freecad-composite-mutation-evidence.mjs
wangdequan 5bbd7b9d4f
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: advance FreeCAD exact parity evidence
2026-08-14 22:39:16 -04:00

95 lines
9.4 KiB
JavaScript

const CASES = [
...['fuse', 'cut', 'common'].flatMap((operation) => [1, 2, 3].map((variant) => [`boolean-${operation}-${variant}`, 'boolean'])),
...['plain', 'midplane', 'reverse', 'taper', 'twoside', 'pocket', 'pocket-through', 'pocket-midplane', 'pocket-twoside', 'pocket-taper', 'pocket-up-to-face'].map((mode) => [`partdesign-${mode}`, 'partdesign']),
...[1, 2, 3, 4, 5, 6].map((variant) => [`composite-${String(variant).padStart(2, '0')}`, 'composite-boolean']),
...['fillet', 'chamfer'].flatMap((operation) => [1, 2].map((variant) => [`dressup-${operation}-${variant}`, 'dress-up'])),
]
export const expectedCompositeMutationCases = new Map(CASES)
export const expectedCompositeMutationCategoryCases = {
boolean: 9,
partdesign: 11,
'composite-boolean': 6,
'dress-up': 4,
}
const fail = (message) => { throw new Error(`FreeCAD composite mutation evidence: ${message}`) }
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)])) : value
const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
const stageFingerprint = (stage) => [stage?.shape?.geometryDigest, stage?.relationDigest, stage?.semanticNameDigest]
const geometryFingerprint = (stage) => stage?.shape?.geometryDigest
const namingFingerprint = (stage) => [stage?.relationDigest, stage?.semanticNameDigest]
const validatePhaseRecord = (fixture, nominalStage, ordinal, phase, record) => {
const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${nominalStage.name}`
if (record?.key !== expectedKey || record.ordinal !== ordinal || record.name !== nominalStage.name || record.typeId !== nominalStage.typeId) fail(`${fixture.id}/${phase}/${ordinal} changed its locked stage identity.`)
if (record.shape?.valid !== true || !sha256(record.shape?.brepSha256) || !sha256(record.shape?.geometryDigest) || !sha256(record.relationDigest) || !sha256(record.semanticNameDigest)) fail(`${fixture.id}/${phase}/${nominalStage.name} lacks valid geometry or naming digests.`)
}
export const validateCompositeMutationEvidence = (report) => {
if (report?.mutationContractVersion !== 1 || !Array.isArray(report.cases) || report.cases.length !== CASES.length) fail('the report does not declare the locked 30-case mutation contract.')
if (!same(report.cases.map(({ id }) => id), CASES.map(([id]) => id))) fail('case identities or ordering differ from the locked constructor inventory.')
const categoryCases = Object.fromEntries(Object.keys(expectedCompositeMutationCategoryCases).map((category) => [category, 0]))
let mutationCases = 0
let mutationPassed = 0
let mutationStageRecords = 0
let mutationPhaseStageRecords = 0
let mutationFinalPropagationFailures = 0
let mutationStageRestoreFailures = 0
let mutationNamingRestoreDriftCases = 0
let mutationNamingRestoreDriftStages = 0
const cases = []
for (const fixture of report.cases) {
const expectedCategory = expectedCompositeMutationCases.get(fixture.id)
if (fixture.category !== expectedCategory || fixture.status !== 'pass') fail(`${fixture.id} is not a passing ${expectedCategory} case.`)
categoryCases[fixture.category] += 1
const mutation = fixture.mutation
const contract = mutation?.contract
const metrics = mutation?.metrics
const phases = mutation?.phases
if (mutation?.schemaVersion !== 1 || mutation.category !== fixture.category || mutation.status !== 'pass' || !contract || !metrics || !phases) fail(`${fixture.id} has no passing mutation transaction.`)
if (contract.nativeEditable !== true || !Array.isArray(contract.editorModes) || contract.editorModes.includes('ReadOnly') || contract.editorModes.includes('Hidden') || !/^App::Property/.test(contract.propertyType || '') || typeof contract.propertyPath !== 'string' || !contract.propertyPath) fail(`${fixture.id} did not mutate a native editable FreeCAD property.`)
if (contract.finalObject !== fixture.finalObject || contract.requiresFinalPropagation !== true || contract.requiresAllStageRestore !== true) fail(`${fixture.id} changed its propagation or restoration contract.`)
if (!Number.isInteger(contract.targetStageOrdinal) || !Number.isInteger(contract.finalStageOrdinal) || contract.targetStageOrdinal < 0 || contract.finalStageOrdinal < 0 || contract.targetStageOrdinal >= fixture.stages.length || contract.finalStageOrdinal >= fixture.stages.length) fail(`${fixture.id} has invalid target/final stage ordinals.`)
if (fixture.stages[contract.targetStageOrdinal]?.name !== contract.targetObject || fixture.stages[contract.targetStageOrdinal]?.typeId !== contract.targetTypeId || fixture.stages[contract.finalStageOrdinal]?.name !== fixture.finalObject) fail(`${fixture.id} mutation targets are not bound to captured stages.`)
if (same(mutation.values?.before, mutation.values?.edited) || !same(mutation.values?.before, mutation.values?.restored)) fail(`${fixture.id} property edit or restoration readback is invalid.`)
if (!['before', 'edited', 'restored'].every((phase) => Array.isArray(phases[phase]) && phases[phase].length === fixture.stages.length)) fail(`${fixture.id} lacks a full three-phase stage matrix.`)
for (let ordinal = 0; ordinal < fixture.stages.length; ordinal += 1) {
const nominal = fixture.stages[ordinal]
for (const phase of ['before', 'edited', 'restored']) validatePhaseRecord(fixture, nominal, ordinal, phase, phases[phase][ordinal])
if (!same(stageFingerprint(phases.restored[ordinal]), stageFingerprint(nominal))) fail(`${fixture.id}/${nominal.name} recovered state differs from the saved nominal stage.`)
}
const changedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.edited[ordinal]) ? [] : [ordinal])
const restorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.restored[ordinal]) ? [] : [ordinal])
const namingChangedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.edited[ordinal])) ? [] : [ordinal])
const namingRestorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.restored[ordinal])) ? [] : [ordinal])
if (!same(metrics.changedStageOrdinals, changedStageOrdinals) || metrics.changedStages !== changedStageOrdinals.length || !changedStageOrdinals.includes(contract.finalStageOrdinal)) fail(`${fixture.id} did not propagate its native edit to the final object.`)
if (!same(metrics.restorationDriftOrdinals, restorationDriftOrdinals) || metrics.restorationDriftStages !== restorationDriftOrdinals.length || restorationDriftOrdinals.length !== 0) fail(`${fixture.id} did not restore every stage exactly.`)
if (!same(metrics.namingChangedStageOrdinals, namingChangedStageOrdinals) || metrics.namingChangedStages !== namingChangedStageOrdinals.length || !same(metrics.namingRestorationDriftOrdinals, namingRestorationDriftOrdinals) || metrics.namingRestorationDriftStages !== namingRestorationDriftOrdinals.length || metrics.allStageNamingRestored !== (namingRestorationDriftOrdinals.length === 0)) fail(`${fixture.id} naming edit/restoration metrics do not match its native phase evidence.`)
if (metrics.propertyChanged !== true || metrics.finalShapeChanged !== true || metrics.finalBrepChanged !== true || metrics.propertyRestored !== true || metrics.allStagesRestored !== true || metrics.allStageGeometryRestored !== true || metrics.editedShapesValid !== true) fail(`${fixture.id} mutation acceptance metrics are incomplete.`)
if (metrics.stageRecords !== fixture.stages.length || metrics.phaseStageRecords !== fixture.stages.length * 3) fail(`${fixture.id} mutation stage record counts are inconsistent.`)
mutationCases += 1
mutationPassed += 1
mutationStageRecords += metrics.stageRecords
mutationPhaseStageRecords += metrics.phaseStageRecords
mutationFinalPropagationFailures += metrics.finalShapeChanged === true ? 0 : 1
mutationStageRestoreFailures += metrics.restorationDriftStages
mutationNamingRestoreDriftCases += metrics.namingRestorationDriftStages > 0 ? 1 : 0
mutationNamingRestoreDriftStages += metrics.namingRestorationDriftStages
cases.push({ id: fixture.id, category: fixture.category, stages: fixture.stages.length, changedStages: metrics.changedStages, restoredStages: metrics.stageRecords, namingRestoreDriftStages: metrics.namingRestorationDriftStages })
}
const summary = { mutationCases, mutationPassed, mutationStageRecords, mutationPhaseStageRecords, mutationFinalPropagationFailures, mutationStageRestoreFailures, mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages, categoryCases }
for (const [key, value] of Object.entries(summary)) {
if (!same(report.summary?.[key], value)) fail(`summary ${key} differs from per-case evidence.`)
}
if (!same(categoryCases, expectedCompositeMutationCategoryCases) || mutationCases !== 30 || mutationPassed !== 30 || mutationStageRecords !== 219 || mutationPhaseStageRecords !== 657 || mutationFinalPropagationFailures !== 0 || mutationStageRestoreFailures !== 0) fail(`locked totals are incomplete: ${JSON.stringify(summary)}`)
return { ...summary, cases }
}