171 lines
9.7 KiB
JavaScript
171 lines
9.7 KiB
JavaScript
const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
|
|
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
|
|
|
|
export const recoveredNamingDecisions = [
|
|
{
|
|
taskId: 'TSN-DRIFT-000',
|
|
caseId: 'partdesign-plain',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD changes the Body and Pad naming history after the first Length edit/restore while restoring geometry exactly; the evolved names repeat deterministically and remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-001',
|
|
caseId: 'partdesign-midplane',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies the same deterministic history evolution to a Midplane Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-002',
|
|
caseId: 'partdesign-reverse',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies the same deterministic history evolution to a Reversed Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-003',
|
|
caseId: 'partdesign-taper',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies the same deterministic history evolution to a Tapered Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-004',
|
|
caseId: 'partdesign-twoside',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies the same deterministic history evolution to a Two Lengths Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-005',
|
|
caseId: 'partdesign-pocket',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution to an upstream Pad Length edit/restore: Body, Pad, and downstream Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-006',
|
|
caseId: 'partdesign-pocket-through',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution through a Through All Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-007',
|
|
caseId: 'partdesign-pocket-midplane',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution through a Midplane Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-008',
|
|
caseId: 'partdesign-pocket-twoside',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution through a Two Lengths Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-009',
|
|
caseId: 'partdesign-pocket-taper',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution through a Tapered Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
{
|
|
taskId: 'TSN-DRIFT-010',
|
|
caseId: 'partdesign-pocket-up-to-face',
|
|
classification: 'allowed_evolution',
|
|
reasonCode: 'native-history-stabilizes-after-edit-restore',
|
|
rationale: 'FreeCAD applies deterministic history evolution through an Up To Face Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
|
|
},
|
|
]
|
|
|
|
export const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
|
|
|
|
const namingFingerprint = (record) => ({
|
|
relationDigest: record.relationDigest,
|
|
semanticNameDigest: record.semanticNameDigest,
|
|
})
|
|
|
|
const phaseFingerprint = (record) => ({
|
|
geometryDigest: record.shape.geometryDigest,
|
|
relationDigest: record.relationDigest,
|
|
semanticNameDigest: record.semanticNameDigest,
|
|
})
|
|
|
|
export const buildRecoveredNamingSnapshot = (fixture, resaveFixture) => {
|
|
const driftOrdinals = fixture?.mutation?.metrics?.namingRestorationDriftOrdinals
|
|
if (!Array.isArray(driftOrdinals) || driftOrdinals.length === 0) throw new Error(`${fixture?.id ?? 'unknown case'} has no recovered naming drift to classify.`)
|
|
if (!resaveFixture || !['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(resaveFixture.stageCorrelations?.[phase]))) throw new Error(`${fixture.id} lacks native save/reopen/resave evidence.`)
|
|
|
|
const stages = driftOrdinals.map((ordinal) => {
|
|
const nominal = fixture.stages?.[ordinal]
|
|
const before = fixture.mutation.phases?.before?.[ordinal]
|
|
const edited = fixture.mutation.phases?.edited?.[ordinal]
|
|
const restored = fixture.mutation.phases?.restored?.[ordinal]
|
|
if (![nominal, before, edited, restored].every(Boolean)) throw new Error(`${fixture.id}/${ordinal} lacks a complete mutation phase.`)
|
|
const persistence = Object.fromEntries(['initial', 'reopened', 'resaved'].map((phase) => {
|
|
const record = resaveFixture.stageCorrelations[phase][ordinal]
|
|
if (!record) throw new Error(`${fixture.id}/${ordinal} lacks ${phase} persistence evidence.`)
|
|
return [phase, namingFingerprint(record)]
|
|
}))
|
|
return {
|
|
ordinal,
|
|
name: nominal.name,
|
|
typeId: nominal.typeId,
|
|
nominal: phaseFingerprint(nominal),
|
|
before: phaseFingerprint(before),
|
|
edited: phaseFingerprint(edited),
|
|
restored: phaseFingerprint(restored),
|
|
persistence,
|
|
}
|
|
})
|
|
|
|
return {
|
|
caseId: fixture.id,
|
|
contract: {
|
|
targetObject: fixture.mutation.contract.targetObject,
|
|
targetTypeId: fixture.mutation.contract.targetTypeId,
|
|
propertyPath: fixture.mutation.contract.propertyPath,
|
|
propertyType: fixture.mutation.contract.propertyType,
|
|
finalObject: fixture.mutation.contract.finalObject,
|
|
},
|
|
values: fixture.mutation.values,
|
|
driftOrdinals,
|
|
stages,
|
|
checks: {
|
|
propertyRestored: fixture.mutation.metrics.propertyRestored === true,
|
|
geometryRestored: stages.every(({ before, restored }) => before.geometryDigest === restored.geometryDigest),
|
|
namingEvolved: stages.every(({ before, restored }) => !same({ relationDigest: before.relationDigest, semanticNameDigest: before.semanticNameDigest }, { relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest })),
|
|
restoredMatchesNominal: stages.every(({ nominal, restored }) => same(nominal, restored)),
|
|
persistenceStable: resaveFixture.roundtripNameDrift === 0
|
|
&& resaveFixture.resaveNameDrift === 0
|
|
&& resaveFixture.nativeDesktopResaveCovered === true
|
|
&& stages.every(({ restored, persistence }) => ['initial', 'reopened', 'resaved'].every((phase) => same({ relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest }, persistence[phase]))),
|
|
},
|
|
}
|
|
}
|
|
|
|
export const validateRecoveredNamingSnapshot = (snapshot) => {
|
|
if (!snapshot || typeof snapshot.caseId !== 'string' || !snapshot.caseId) throw new Error('classification snapshot has no case identity.')
|
|
if (!Array.isArray(snapshot.driftOrdinals) || snapshot.driftOrdinals.length === 0 || snapshot.stages?.length !== snapshot.driftOrdinals.length) throw new Error(`${snapshot.caseId} has an invalid drift-stage set.`)
|
|
if (!same(snapshot.stages.map(({ ordinal }) => ordinal), snapshot.driftOrdinals)) throw new Error(`${snapshot.caseId} drift-stage identities are inconsistent.`)
|
|
for (const stage of snapshot.stages) {
|
|
if (!Number.isInteger(stage.ordinal) || !stage.name || !stage.typeId) throw new Error(`${snapshot.caseId} has an invalid stage identity.`)
|
|
for (const phase of ['nominal', 'before', 'edited', 'restored']) {
|
|
const value = stage[phase]
|
|
if (!sha256(value?.geometryDigest) || !sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has an invalid fingerprint.`)
|
|
}
|
|
for (const phase of ['initial', 'reopened', 'resaved']) {
|
|
const value = stage.persistence?.[phase]
|
|
if (!sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has invalid persistence evidence.`)
|
|
}
|
|
}
|
|
if (!Object.values(snapshot.checks ?? {}).every((value) => value === true)) throw new Error(`${snapshot.caseId} does not satisfy the allowed-evolution evidence contract.`)
|
|
}
|