95 lines
9.4 KiB
JavaScript
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 }
|
|
}
|