feat: close link sub property codecs
Some checks are pending
real-verification / chrome (push) Waiting to run
real-verification / freecad-oracle (push) Waiting to run
real-verification / wasm (push) Waiting to run

This commit is contained in:
2026-08-17 21:37:33 -04:00
parent 7bd7ff5055
commit 54649da16c
69 changed files with 6383 additions and 76 deletions

View File

@@ -0,0 +1,12 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname); const report = JSON.parse(await readFile(resolve(root, 'config/chrome-property-linksubhidden-verification.json'), 'utf8')); const fail = (message) => { throw new Error(`Chrome PropertyLinkSubHidden check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid')
const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']; if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete')
const initial = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] }; const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }; const same = (left, right) => isDeepStrictEqual(left, right)
if (report.ui?.visibleLabelInput !== true || report.ui.hiddenControlCountBefore !== 0 || report.ui.hiddenControlCountAfter !== 0 || report.ui.hiddenTextPresentBefore !== false || report.ui.hiddenTextPresentAfter !== false || report.ui.hiddenStatus !== true || !same(report.ui.beforeValue, initial) || !same(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.dependencyCount !== 0 || report.ui.errors?.length !== 0) fail('production hidden-property UI state is incomplete')
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !same(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyLinkSubHidden' || report.persistence.loadedDependencyCount !== 0 || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'LinkSub' || !same(report.persistence.fcstdValue, target) || report.persistence.fcstdNativeStatus !== 67108864) fail('OPFS/FCStd persistence evidence is incomplete')
if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete')
console.log(JSON.stringify({ status: 'chrome-property-linksubhidden-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -0,0 +1,4 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname); const report = JSON.parse(await readFile(resolve(root, 'config/chrome-property-linksublistglobal-verification.json'), 'utf8')); const fail = (message) => { throw new Error(`Chrome PropertyLinkSubListGlobal check failed: ${message}`) }; if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid'); const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']; if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete'); const initial = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] }; const target = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] }; if (report.ui?.visibleLabelInput !== true || report.ui.controlsBefore !== 3 || report.ui.controlsAfter !== 2 || !isDeepStrictEqual(report.ui.beforeValue, initial) || !isDeepStrictEqual(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.dependencyCount !== 2 || report.ui.errors?.length !== 0) fail('production Global property UI state is incomplete'); if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyLinkSubListGlobal' || report.persistence.loadedDependencyCount !== 2 || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'LinkSubList' || report.persistence.fcstdType !== 'App::PropertyLinkSubListGlobal' || !isDeepStrictEqual(report.persistence.fcstdValue, target) || report.persistence.fcstdDecoded !== true) fail('OPFS/FCStd persistence evidence is incomplete'); if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release?.kernelReferenceCount !== 0 || report.release?.workerTerminated !== true) fail('resource release evidence is incomplete'); console.log(JSON.stringify({ status: 'chrome-property-linksublistglobal-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -24,6 +24,8 @@ const allowWorkerMessaging = new Set([
'chromePropertyHeatFluxHarness.tsx',
'chromePropertyIntegerSetHarness.tsx',
'chromePropertyLinkListHiddenHarness.tsx',
'chromePropertyLinkSubHiddenHarness.tsx',
'chromePropertyLinkSubListGlobalHarness.tsx',
'facade/camPipeline.ts',
])

View File

@@ -5,10 +5,10 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-native-property-semantics.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD native property semantics check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '45 runtime property types and 455 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid')
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid')
if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed')
const support = report.supportSummary
if (support?.['native-editable-codec']?.typeCount !== 35 || support['native-editable-codec'].recordCount !== 4372 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 45 || support['opaque-fcstd-proxy'].recordCount !== 455) fail('property support partition is stale')
if (support?.['native-editable-codec']?.typeCount !== 37 || support['native-editable-codec'].recordCount !== 4377 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 43 || support['opaque-fcstd-proxy'].recordCount !== 450) fail('property support partition is stale')
if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid')
const acceleration = report.types.find(({ typeId }) => typeId === 'App::PropertyAcceleration')
if (acceleration?.support !== 'native-editable-codec' || acceleration.recordCount !== 1 || acceleration.objectTypeCount !== 1) fail('App::PropertyAcceleration promotion is missing')
@@ -36,6 +36,10 @@ const integerSet = report.types.find(({ typeId }) => typeId === 'App::PropertyIn
if (integerSet?.support !== 'native-editable-codec' || integerSet.recordCount !== 2 || integerSet.objectTypeCount !== 2 || integerSet.statusNames?.length !== 0) fail('App::PropertyIntegerSet promotion is missing')
const linkListHidden = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkListHidden')
if (linkListHidden?.support !== 'native-editable-codec' || linkListHidden.recordCount !== 2 || linkListHidden.objectTypeCount !== 2 || linkListHidden.statusNames?.join(',') !== 'PropHidden') fail('App::PropertyLinkListHidden promotion is missing')
const linkSubHidden = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkSubHidden')
if (linkSubHidden?.support !== 'native-editable-codec' || linkSubHidden.recordCount !== 4 || linkSubHidden.objectTypeCount !== 4 || linkSubHidden.statusNames?.join(',') !== 'LockDynamic,PropHidden') fail('App::PropertyLinkSubHidden promotion is missing')
const linkSubListGlobal = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkSubListGlobal')
if (linkSubListGlobal?.support !== 'native-editable-codec' || linkSubListGlobal.recordCount !== 1 || linkSubListGlobal.objectTypeCount !== 1 || linkSubListGlobal.statusNames?.length !== 0) fail('App::PropertyLinkSubListGlobal promotion is missing')
if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 0 || report.propertyStatus.proxyOnly?.length !== 0 || report.propertyStatus.facadeNative?.join(',') !== report.propertyStatus.observed?.join(',')) fail('property status representation coverage is stale')
if (report.propertyStatus.behaviorNative?.join(',') !== 'Hidden,Immutable,LockDynamic,NoModify,Ordered,Output,PartialTrigger,PropHidden,PropNoPersist,PropNoRecompute,PropOutput,PropReadOnly,PropTransient,ReadOnly,Transient' || report.propertyStatus.preservedOnly?.length !== 0 || report.propertyStatus.preservedOnlyRecordCount !== 0) fail('property status behavior boundary is stale')
for (const locked of [report.source, report.harness]) {
@@ -43,4 +47,4 @@ for (const locked of [report.source, report.harness]) {
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`)
}
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5055, opaqueProxyRecords: 455, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId, font.typeId, force.typeId, heatFlux.typeId, integerSet.typeId, linkListHidden.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2))
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5060, opaqueProxyRecords: 450, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId, font.typeId, force.typeId, heatFlux.typeId, integerSet.typeId, linkListHidden.typeId, linkSubHidden.typeId, linkSubListGlobal.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2))

View File

@@ -0,0 +1,32 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksubhidden-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksubhidden-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubHidden' || report.caseCount !== 4 || report.cases?.length !== 4) fail('baseline is invalid')
const expectedHosts = new Map([
['App::Link', { name: 'LinkProbe', ownerOutList: ['ColorSourceA'], targetInLists: { ColorSourceA: ['LinkProbe'], ColorSourceB: [] } }],
['App::LinkGroup', { name: 'LinkGroupProbe', ownerOutList: [], targetInLists: { ColorSourceA: [], ColorSourceB: [] } }],
['App::LinkGroupPython', { name: 'LinkGroupPythonProbe', ownerOutList: [], targetInLists: { ColorSourceA: [], ColorSourceB: [] } }],
['App::LinkPython', { name: 'LinkPythonProbe', ownerOutList: ['ColorSourceA'], targetInLists: { ColorSourceA: ['LinkPythonProbe'], ColorSourceB: [] } }],
])
const expectedObjects = (ownerName, ownerTypeId) => [{ name: ownerName, typeId: ownerTypeId }, { name: 'ColorSourceA', typeId: 'Part::Feature' }, { name: 'ColorSourceB', typeId: 'Part::Feature' }]
const baselineValue = { object: 'ColorSourceA', subElements: ['Face1', 'Face3'] }
const editedValue = { object: 'ColorSourceB', subElements: ['Face5'] }
for (const entry of report.cases) {
const host = expectedHosts.get(entry.objectTypeId)
if (!host || entry.objectName !== host.name || entry.propertyName !== 'ColoredElements' || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.valueRestored !== true || entry.semanticStateRestored !== true) fail(`${entry.objectTypeId} mutation identity or recovery changed`)
if (!isDeepStrictEqual(entry.before?.value, baselineValue) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, editedValue) || !isDeepStrictEqual(entry.edited?.value, editedValue) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, baselineValue) || !isDeepStrictEqual(entry.restored?.value, baselineValue)) fail(`${entry.objectTypeId} value mutation changed`)
if (!isDeepStrictEqual(entry.before.ownerState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.ownerState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched' || !isDeepStrictEqual(entry.edited.ownerState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterRestore.ownerState, ['Touched']) || entry.touchedAfterRestore.statusString !== 'Touched' || !isDeepStrictEqual(entry.restored.ownerState, ['Up-to-date']) || entry.restored.statusString !== 'Valid') fail(`${entry.objectTypeId} state transition changed`)
const baselineTargets = entry.before.targetShapes
for (const [phase, snapshot] of Object.entries({ before: entry.before, touchedAfterEdit: entry.touchedAfterEdit, edited: entry.edited, touchedAfterRestore: entry.touchedAfterRestore, restored: entry.restored })) {
if (snapshot.propertyTypeId !== 'App::PropertyLinkSubHidden' || snapshot.group !== ' Link' || !isDeepStrictEqual(snapshot.propertyStatus, ['LockDynamic', '26']) || !isDeepStrictEqual(snapshot.editorMode, ['Hidden']) || snapshot.shape?.applicable !== false) fail(`${entry.objectTypeId} ${phase} metadata or host Shape changed`)
if (!isDeepStrictEqual(snapshot.ownerOutList, host.ownerOutList) || !isDeepStrictEqual(snapshot.targetInLists, host.targetInLists) || !isDeepStrictEqual(snapshot.targetShapes, baselineTargets) || !isDeepStrictEqual(snapshot.objectSet, expectedObjects(host.name, entry.objectTypeId))) fail(`${entry.objectTypeId} ${phase} leaked dependencies or changed targets/objects`)
}
if (baselineTargets?.ColorSourceA?.applicable !== true || baselineTargets.ColorSourceA.isNull !== false || baselineTargets.ColorSourceA.solids !== 1 || baselineTargets.ColorSourceA.faces !== 6 || baselineTargets.ColorSourceA.edges !== 12 || baselineTargets.ColorSourceA.vertices !== 8 || baselineTargets.ColorSourceB?.applicable !== true || baselineTargets.ColorSourceB.isNull !== false || baselineTargets.ColorSourceB.solids !== 1 || baselineTargets.ColorSourceB.faces !== 3 || baselineTargets.ColorSourceB.edges !== 3 || baselineTargets.ColorSourceB.vertices !== 2) fail(`${entry.objectTypeId} target topology changed`)
}
console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-mutation-pass', cases: report.cases.map(({ objectTypeId, before, edited, restored, semanticStateRestored }) => ({ objectTypeId, before: before.value, edited: edited.value, restored: restored.value, semanticStateRestored })) }, null, 2))

View File

@@ -0,0 +1,27 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
load('config/freecad-property-linksubhidden-promotion.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
load('config/freecad-property-linksubhidden-roundtrip.json'),
load('config/chrome-property-linksubhidden-verification.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertylinksubhidden-I' || report.propertyType !== 'App::PropertyLinkSubHidden' || report.recordCount !== 4 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
for (const phase of requiredPhases) { const taskId = `PROP-app-propertylinksubhidden-${phase}`; if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`) }
const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
const promotion = report.promotion
if (promotion?.from !== 'opaque-fcstd-proxy' || promotion.to !== 'native-editable-codec' || propertyType?.support !== promotion.to || propertyType.recordCount !== 4 || propertyType.objectTypeCount !== 4 || propertyType.statusNames?.join(',') !== 'LockDynamic,PropHidden' || promotion.facadeValueModel !== 'same-document-object-and-ordered-sub-element-name-array-or-null' || promotion.writableRecords !== 4 || promotion.objectTypeCount !== 4 || promotion.propertyStatus !== 'LockDynamic,PropHidden' || promotion.linkScope !== 'Hidden' || promotion.runtimeDependencyEdges !== 0 || promotion.fcstdElement !== 'LinkSub' || !isDeepStrictEqual(promotion.nativeRoundTripValue, target) || !isDeepStrictEqual(promotion.browserRoundTripValue, target) || promotion.hiddenUiFiltered !== true || promotion.hiddenLinksPreserved !== true || promotion.nativeDependencyMetadataNormalized !== true || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 37 || sync.nativeEditableRecords !== 4377 || sync.opaqueTypes !== 43 || sync.opaqueRecords !== 450 || sync.exactPromotionReady !== false || sync.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
if (roundTrip.classification?.zeroUnknownDrift !== true || !isDeepStrictEqual(roundTrip.classification.resavedValue, target) || roundTrip.classification.hiddenLinksPreserved !== true || roundTrip.classification.nativeDependencyMetadataNormalized !== true || chrome.status !== 'pass' || !isDeepStrictEqual(chrome.persistence?.loadedValue, target) || chrome.persistence?.fcstdElement !== 'LinkSub' || chrome.ui?.hiddenControlCountBefore !== 0 || chrome.ui?.hiddenControlCountAfter !== 0 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -0,0 +1,22 @@
import { createHash } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksubhidden-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden roundtrip check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksubhidden-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubHidden') fail('baseline is invalid')
const initial = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] }
const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }
const objectSet = [{ name: 'LinkGroupProbe', typeId: 'App::LinkGroup' }, { name: 'ColorSourceA', typeId: 'Part::Feature' }, { name: 'ColorSourceB', typeId: 'Part::Feature' }]
for (const [phase, snapshot, value] of [['nativeInitial', report.nativeInitial, { object: 'ColorSourceA', subElements: ['Face1', 'Face3'] }], ['nativeReopened', report.nativeAfter?.reopened, { object: 'ColorSourceB', subElements: ['Face5'] }], ['nativeResaved', report.nativeAfter?.resaved, { object: 'ColorSourceB', subElements: ['Face5'] }]]) {
if (!isDeepStrictEqual(snapshot?.objectSet, objectSet) || snapshot.object?.name !== 'LinkGroupProbe' || snapshot.object.typeId !== 'App::LinkGroup' || snapshot.object.propertyName !== 'ColoredElements' || snapshot.object.propertyTypeId !== 'App::PropertyLinkSubHidden' || snapshot.object.group !== ' Link' || !isDeepStrictEqual(snapshot.object.value, value) || !isDeepStrictEqual(snapshot.object.propertyStatus, ['LockDynamic', '26']) || !isDeepStrictEqual(snapshot.object.editorMode, ['Hidden']) || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || snapshot.object.statusString !== 'Valid' || !isDeepStrictEqual(snapshot.object.outList, [])) fail(`${phase} native hidden sub-link semantics changed`)
if (snapshot.object.shape?.applicable !== false) fail(`${phase} owner Shape boundary changed`)
for (const targetName of ['ColorSourceA', 'ColorSourceB']) { const item = snapshot.targets?.[targetName]; if (item?.typeId !== 'Part::Feature' || !isDeepStrictEqual(item.inList, []) || item.shape?.applicable !== true || item.shape.isNull !== false || item.shape.valid !== true || item.shape.shapeType !== 'Solid' || item.shape.solids !== 1 || !/^[0-9a-f]{64}$/.test(item.shape.brepSha256)) fail(`${phase} ${targetName} changed`) }
}
if (report.web.before?.typeId !== 'App::PropertyLinkSubHidden' || report.web.before.element !== 'LinkSub' || report.web.after?.typeId !== 'App::PropertyLinkSubHidden' || report.web.after.element !== 'LinkSub' || report.web.targetMarkedTouched !== true || !isDeepStrictEqual(report.web.initialDependencyTargets, ['ColorSourceA']) || !isDeepStrictEqual(report.web.webDependencyTargets, ['ColorSourceA']) || report.web.hiddenDependencyMetadataPreserved !== true || report.web.webOutputMatches !== true || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve the native archive boundary')
if (!isDeepStrictEqual(report.classification?.requestedValue, target) || !isDeepStrictEqual(report.classification.webValue, target) || !isDeepStrictEqual(report.classification.reopenedValue, target) || !isDeepStrictEqual(report.classification.resavedValue, target) || report.classification.nativeOutputMatches !== true || report.classification.hiddenLinksPreserved !== true || !isDeepStrictEqual(report.classification.nativeResavedDependencyTargets, ['ColorSourceB']) || report.classification.nativeDependencyMetadataNormalized !== true || report.classification.targetShapeStructurePreserved !== true || report.classification.ownerShapePreserved !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true || report.classification.nativeTargetBrepHashes?.length !== 3) fail('roundtrip classification is incomplete')
for (const hashes of report.classification.nativeTargetBrepHashes) if (!/^[0-9a-f]{64}$/.test(hashes.ColorSourceA) || !/^[0-9a-f]{64}$/.test(hashes.ColorSourceB)) fail('native target BREP evidence is incomplete')
for (const archive of Object.values(report.archives ?? {})) { const path = resolve(root, archive.path ?? ''); const content = await readFile(path); if ((await stat(path)).size !== archive.bytes || createHash('sha256').update(content).digest('hex') !== archive.sha256) fail(`archive is stale: ${archive.path}`) }
console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-roundtrip-pass', values: { nativeInitial: initial, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, hiddenLinksPreserved: report.classification.hiddenLinksPreserved, targetShapeStructurePreserved: report.classification.targetShapeStructurePreserved, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))

View File

@@ -0,0 +1,34 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1) fail('baseline is invalid')
const entry = report.case
if (!entry || entry.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support') fail('fixture identity changed')
const initial = entry.initial
if (initial?.propertyTypeId !== report.propertyType || !isDeepStrictEqual(initial.propertyStatus, []) || !isDeepStrictEqual(initial.editorMode, []) || !isDeepStrictEqual(initial.value, [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }]) || initial.statusString !== 'Valid' || !isDeepStrictEqual(initial.ownerState, ['Up-to-date'])) fail('initial global link state is invalid')
const assertGeometryPreserved = (snapshot, label) => { if (!isDeepStrictEqual(snapshot.objectSet, initial.objectSet) || !isDeepStrictEqual(snapshot.targetShapeHashes, initial.targetShapeHashes)) fail(`${label} polluted objects or source Shapes`) }
const expectedInputs = new Map([
['scalar', { exception: ['TypeError', 'Expects sequence of items of type DocObj, (DocObj,SubName), or (DocObj, (SubName,...))'], value: initial.value, preserved: true }],
['singlePairShorthand', { exception: null, value: [{ object: 'SourceBox', subElements: ['Face1'] }], preserved: false }],
['stringObject', { exception: null, value: [], preserved: false }],
['integerObject', { exception: null, value: [], preserved: false }],
])
if (entry.inputs?.length !== expectedInputs.size) fail('invalid-input case count changed')
for (const input of entry.inputs) {
const expected = expectedInputs.get(input.id)
if (!expected || !isDeepStrictEqual(input.before, initial) || !isDeepStrictEqual(input.after.value, expected.value) || input.valuePreserved !== expected.preserved || (expected.exception === null ? input.exception !== null : input.exception?.type !== expected.exception[0] || input.exception.message !== expected.exception[1])) fail(`${input.id} classification changed`)
assertGeometryPreserved(input.after, input.id)
}
if (!entry.crossDocument?.exception || entry.crossDocument.valuePreserved !== true || entry.crossDocument.documentPreserved !== true || !isDeepStrictEqual(entry.crossDocument.before, initial) || !isDeepStrictEqual(entry.crossDocument.after, initial)) fail('cross-document boundary is not atomic')
assertGeometryPreserved(entry.crossDocument.after, 'cross-document')
if (entry.readOnly?.exception !== null || entry.readOnly.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(entry.readOnly.after.value, [{ object: 'SecondBox', subElements: ['Face2'] }]) || !isDeepStrictEqual(entry.readOnly.after.propertyStatus, ['ReadOnly'])) fail('ReadOnly Python boundary changed')
if (entry.immutable?.exception?.type !== 'AttributeError' || entry.immutable.exception.message !== "Object attribute 'Support' is read-only" || entry.immutable.pythonBypassesImmutable !== false || !isDeepStrictEqual(entry.immutable.after.value, initial.value) || !isDeepStrictEqual(entry.immutable.after.propertyStatus, ['Immutable'])) fail('Immutable Python boundary changed')
const transaction = entry.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-linksublistglobal-cancel' || transaction.activeAfterAbort?.id !== 0 || !isDeepStrictEqual(transaction.before, transaction.afterAbort) || transaction.restored !== true || transaction.recomputeAfterAbort !== true) fail('transaction cancel and recovery changed')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is incomplete')
if (entry.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(entry.documentIntegrity.initialObjects, entry.documentIntegrity.finalObjects)) fail('document object set was polluted')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-failure-pass', inputs: entry.inputs.map(({ id, exception, after }) => ({ id, exception, normalizedValue: after.value })), crossDocument: entry.crossDocument.exception, readOnlyPythonAccepted: entry.readOnly.pythonBypassesEditorReadOnly, immutablePythonAccepted: entry.immutable.pythonBypassesImmutable, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,45 @@
import { createHash } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const runtimePath = '.cache/freecad/reference-desktop.json'
const oraclePath = 'config/freecad-partdesign-structure-oracle.json'
const [runtimeContent, oracleContent, reportContent] = await Promise.all([
readFile(resolve(root, runtimePath)),
readFile(resolve(root, oraclePath)),
readFile(resolve(root, 'config/freecad-property-linksublistglobal-inventory.json')),
])
const runtime = JSON.parse(runtimeContent)
const oracle = JSON.parse(oracleContent)
const report = JSON.parse(reportContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal inventory check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
if (report.recordCount !== 1 || report.objectTypeCount !== 1 || report.writableRecordCount !== 1 || report.records?.length !== 1) fail('runtime record or host count changed')
const contract = report.typeContract
if (contract?.inheritedFrom !== 'App::PropertyLinkSubList' || contract.linkScope !== 'Global' || !isDeepStrictEqual(contract.propertyStatus, []) || contract.xmlElement !== 'LinkSubList' || contract.xmlChildElement !== 'Link' || contract.emptyListMeans !== 'no-references' || contract.crossDocumentBoundary !== 'reject-without-LinkAllowExternal') fail('type contract is incomplete')
if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale')
if (report.provenance?.oracle?.path !== oraclePath || report.provenance.oracle.bytes !== oracleContent.length || report.provenance.oracle.sha256 !== createHash('sha256').update(oracleContent).digest('hex')) fail('structure oracle provenance is stale')
for (const locked of report.provenance?.sources ?? []) { const content = await readFile(resolve(root, locked.path)); if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`) }
const nativeMatches = []
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyLinkSubListGlobal') nativeMatches.push({ objectTypeId: objectType.typeId, objectAvailable: objectType.available === true, probeStatus: objectType.probeStatus, propertyName: property.name, group: property.group, statusRaw: property.status, defaultRaw: property.default })
const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, statusRaw, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, statusRaw, defaultRaw })
const sorted = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
if (!isDeepStrictEqual(sorted(nativeMatches), sorted(report.records))) fail('report diverges from the locked runtime oracle')
const record = report.records[0]
if (record.objectTypeId !== 'PartDesign::ShapeBinder' || record.objectAvailable !== true || record.probeStatus !== 'available' || record.propertyName !== 'Support' || record.group !== '' || !isDeepStrictEqual(record.statusRaw, []) || !isDeepStrictEqual(record.statusNames, []) || !isDeepStrictEqual(record.defaultRaw, [])) fail('native ShapeBinder Support metadata changed')
const model = record.valueModel; const inputs = record.inputs; const dependencies = record.dependencies
if (model?.kind !== 'same-document-object-and-ordered-sub-element-pair-list' || model.inheritedFrom !== 'App::PropertyLinkSubList' || model.orderPreserved !== true || model.duplicatesPreserved !== true || model.writable !== true) fail('value model changed')
if (inputs?.nativeCppSetValues !== 'parallel DocumentObject and sub-element lists' || inputs.emptyListMeansNoReferences !== true || inputs.nullObjectClears !== true || inputs.detachedObjectRejected !== true || inputs.crossDocumentRejectedWithoutLinkAllowExternal !== true || inputs.invalidSubElementAcceptedAsReference !== true) fail('input preconditions changed')
if (dependencies?.linkScope !== 'Global' || dependencies.createsBacklinks !== true || dependencies.includedByDefaultGetLinks !== true || dependencies.includedByGetLinksAll !== true || dependencies.defaultDagDependency !== true) fail('global dependency scope changed')
if (record.applicability?.requiredObjectTypeId !== 'PartDesign::ShapeBinder' || record.applicability.role !== 'ShapeBinder source geometry support' || record.applicability.propertyWriteRequiresShape !== false) fail('applicability changed')
const evidence = report.nativeEvidence
if (evidence?.structureOracle?.path !== oraclePath || evidence.structureOracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || evidence.structureOracle.crossDocumentShapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || evidence.structureOracle.crossDocumentAccepted !== false || evidence.structureOracle.crossDocumentError !== 'PropertyLinkSubList does not support external object' || !isDeepStrictEqual(evidence.structureOracle.crossDocumentShapeBinderSupport, [])) fail('native ShapeBinder cross-document boundary evidence changed')
const linksHeader = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyLinks.h'), 'utf8')
const linksSource = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyLinks.cpp'), 'utf8')
const binderHeader = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.h'), 'utf8')
const binderSource = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.cpp'), 'utf8')
if (!linksHeader.includes('class AppExport PropertyLinkSubListGlobal: public PropertyLinkSubList') || !linksHeader.includes('_pcScope = LinkScope::Global;') || !linksSource.includes('TYPESYSTEM_SOURCE(App::PropertyLinkSubListGlobal, App::PropertyLinkSubList)') || !linksSource.includes('PropertyLinkSubList does not support external object') || !binderHeader.includes('App::PropertyLinkSubListGlobal Support') || !/ADD_PROPERTY_TYPE\(\s*Support\s*,/s.test(binderSource)) fail('locked type, scope, host, or cross-document source contract changed')
if (oracle.status !== 'pass' || oracle.crossDocument?.initial?.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || oracle.crossDocument.initial.shapeBinderExternalLink.accepted !== false) fail('partdesign structure oracle boundary changed')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId), propertyStatus: contract.propertyStatus, linkScope: contract.linkScope, crossDocument: evidence.structureOracle.crossDocumentError, classification: report.classification }, null, 2))

View File

@@ -0,0 +1,23 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1) fail('baseline is invalid')
const entry = report.case
const baselineValue = [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }]
const editedValue = [{ object: 'SecondBox', subElements: [] }]
if (entry?.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support' || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.valueRestored !== true || entry.semanticStateRestored !== true) fail('mutation identity or restoration changed')
if (!isDeepStrictEqual(entry.before?.value, baselineValue) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, editedValue) || !isDeepStrictEqual(entry.edited?.value, editedValue) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, baselineValue) || !isDeepStrictEqual(entry.restored?.value, baselineValue)) fail('mutation values changed')
if (!isDeepStrictEqual(entry.before.ownerState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.ownerState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched' || !isDeepStrictEqual(entry.edited.ownerState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterRestore.ownerState, ['Touched']) || entry.touchedAfterRestore.statusString !== 'Touched' || !isDeepStrictEqual(entry.restored.ownerState, ['Up-to-date']) || entry.restored.statusString !== 'Valid') fail('state transitions changed')
const objectSet = [{ name: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder' }, { name: 'SourceBox', typeId: 'Part::Feature' }, { name: 'SecondBox', typeId: 'Part::Feature' }]
for (const [phase, snapshot] of Object.entries({ before: entry.before, touchedAfterEdit: entry.touchedAfterEdit, edited: entry.edited, touchedAfterRestore: entry.touchedAfterRestore, restored: entry.restored })) {
if (snapshot.propertyTypeId !== report.propertyType || snapshot.group !== '' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectSet, objectSet) || !isDeepStrictEqual(snapshot.targetShapes, entry.before.targetShapes)) fail(`${phase} metadata, object set, or source Shapes changed`)
}
if (entry.before.shape?.isNull !== false || entry.before.shape.solids !== 0 || entry.before.shape.faces !== 2 || entry.before.shape.edges !== 8 || !isDeepStrictEqual(entry.before.ownerOutList, ['SourceBox', 'SourceBox', 'SecondBox']) || !isDeepStrictEqual(entry.before.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: ['ShapeBinderProbe'] })) fail('baseline Shape or backlinks changed')
if (entry.touchedAfterEdit.shape?.brepSha256 !== entry.before.shape.brepSha256 || !isDeepStrictEqual(entry.touchedAfterEdit.ownerOutList, ['SecondBox']) || !isDeepStrictEqual(entry.touchedAfterEdit.targetInLists, { SourceBox: [], SecondBox: ['ShapeBinderProbe'] })) fail('touched edit state changed')
if (entry.edited.shape?.isNull !== false || entry.edited.shape.solids !== 1 || entry.edited.shape.faces !== 6 || entry.edited.shape.edges !== 12 || entry.edited.shape.brepSha256 !== entry.before.targetShapes.SecondBox.brepSha256) fail('edited whole-object Shape changed')
if (!isDeepStrictEqual(entry.restored, entry.before)) fail('restored semantic state differs from baseline')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-mutation-pass', before: entry.before.value, edited: entry.edited.value, restored: entry.restored.value, baselineShape: entry.before.shape, editedShape: entry.edited.shape, semanticStateRestored: entry.semanticStateRestored }, null, 2))

View File

@@ -0,0 +1,26 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [report, semantics, progress, roundTrip, chrome] = await Promise.all([
load('config/freecad-property-linksublistglobal-promotion.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
load('config/freecad-property-linksublistglobal-roundtrip.json'),
load('config/chrome-property-linksublistglobal-verification.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertylinksublistglobal-I' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.recordCount !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid')
const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
for (const phase of requiredPhases) { const taskId = `PROP-app-propertylinksublistglobal-${phase}`; if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`) }
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
const promotion = report.promotion
if (promotion?.from !== 'opaque-fcstd-proxy' || promotion.to !== 'native-editable-codec' || propertyType?.support !== promotion.to || propertyType.recordCount !== 1 || propertyType.objectTypeCount !== 1 || propertyType.statusNames?.length !== 0 || promotion.facadeValueModel !== 'same-document-object-and-ordered-sub-element-pair-list' || promotion.writableRecords !== 1 || promotion.objectTypeCount !== 1 || promotion.propertyStatus !== '' || promotion.linkScope !== 'Global' || promotion.runtimeDependencyEdges !== 2 || promotion.fcstdElement !== 'LinkSubList' || !isDeepStrictEqual(promotion.nativeRoundTripValue, roundTrip.classification.resavedValue) || !isDeepStrictEqual(promotion.browserRoundTripValue, chrome.persistence.loadedValue) || promotion.globalBacklinksPreserved !== true || promotion.nativeDependenciesNormalized !== true || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 37 || sync.nativeEditableRecords !== 4377 || sync.opaqueTypes !== 43 || sync.opaqueRecords !== 450 || sync.exactPromotionReady !== false || sync.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.globalBacklinksPreserved !== true || roundTrip.classification.nativeDependenciesNormalized !== true || chrome.status !== 'pass' || !isDeepStrictEqual(chrome.persistence?.loadedValue, promotion.browserRoundTripValue) || chrome.persistence?.fcstdElement !== 'LinkSubList' || chrome.persistence?.loadedDependencyCount !== 2 || chrome.ui?.dependencyCount !== 2 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -0,0 +1,19 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal roundtrip check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
const initial = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] }
const target = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] }
if (report.native?.inspectedType !== 'App::PropertyLinkSubListGlobal' || report.native.inspectedElement !== 'LinkSubList' || report.native.decoded !== true || !isDeepStrictEqual(report.native.decodedValue, initial)) fail('native archive did not decode the Global property')
if (report.web?.beforeDecoded !== true || report.web.afterDecoded !== true || report.web.inspectedType !== 'App::PropertyLinkSubListGlobal' || report.web.inspectedElement !== 'LinkSubList' || report.web.archiveBytes <= 0 || !isDeepStrictEqual(report.web.beforeValue, initial) || !isDeepStrictEqual(report.web.afterValue, target)) fail('Web FCStd projection or rewrite changed semantics')
const reopened = report.native.reopened
const resaved = report.native.resaved
for (const [label, snapshot, expected] of [['reopened', reopened, target], ['resaved', resaved, target]]) {
if (snapshot?.object?.typeId !== 'PartDesign::ShapeBinder' || snapshot.object.propertyTypeId !== 'App::PropertyLinkSubListGlobal' || snapshot.object.group !== '' || !isDeepStrictEqual(snapshot.object.value, expected.entries.map((entry) => ({ object: entry.objectId, subElements: entry.subElement === null ? [] : [entry.subElement] }))) || snapshot.object.statusString !== 'Valid' || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || !isDeepStrictEqual(snapshot.object.outList, ['SecondBox', 'SourceBox']) || snapshot.object.shape?.isNull !== false || snapshot.object.shape.solids !== 0 || snapshot.object.shape.faces !== 1 || snapshot.object.shape.edges !== 4 || snapshot.object.shape.vertices !== 4 || !isDeepStrictEqual(snapshot.targets?.SourceBox?.inList, ['ShapeBinderProbe']) || !isDeepStrictEqual(snapshot.targets?.SecondBox?.inList, ['ShapeBinderProbe'])) fail(`${label} native semantics changed`)
}
if (report.classification?.zeroUnknownDrift !== true || report.classification.globalBacklinksPreserved !== true || report.classification.nativeDependenciesNormalized !== true || !isDeepStrictEqual(report.classification.requestedValue, target)) fail('roundtrip classification is incomplete')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-roundtrip-pass', native: report.native, web: report.web, classification: report.classification }, null, 2))

View File

@@ -0,0 +1,33 @@
import { isDeepStrictEqual } from 'node:util'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-success.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal success check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1 || report.bindingBoundary?.oracleDefault?.length !== 0 || report.bindingBoundary.safeFirstRead !== 'assign-empty-list-before-read' || report.bindingBoundary.emptyListMeans !== 'no-references' || report.bindingBoundary.shapeBinderUsesFirstPartFeature !== true || report.bindingBoundary.multipleObjectReferencesPreserved !== true) fail('baseline or binding boundary is invalid')
const entry = report.cases?.[0]
if (!entry || entry.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support' || entry.group !== '' || entry.mode !== 'direct-native-property-setter') fail('case identity changed')
const expectedValues = {
empty: [],
wholeObject: [{ object: 'SourceBox', subElements: [] }],
sameObjectSubElements: [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }],
multipleObjects: [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }],
}
const expectedObjectSet = [{ name: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder' }, { name: 'SourceBox', typeId: 'Part::Feature' }, { name: 'SecondBox', typeId: 'Part::Feature' }]
const targetShapes = entry.phases?.wholeObject?.targetShapes
if (targetShapes?.SourceBox?.isNull !== false || targetShapes.SourceBox.solids !== 1 || targetShapes.SourceBox.faces !== 6 || targetShapes.SourceBox.edges !== 12 || targetShapes.SourceBox.vertices !== 8 || targetShapes?.SecondBox?.isNull !== false || targetShapes.SecondBox.solids !== 1 || targetShapes.SecondBox.faces !== 6 || targetShapes.SecondBox.edges !== 12 || targetShapes.SecondBox.vertices !== 8) fail('source topology evidence is invalid')
for (const [phase, expectedValue] of Object.entries(expectedValues)) {
const snapshot = entry.phases?.[phase]
if (!snapshot || !isDeepStrictEqual(snapshot.value, expectedValue) || snapshot.propertyTypeId !== 'App::PropertyLinkSubListGlobal' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectSet, expectedObjectSet) || snapshot.statusString !== 'Valid' || snapshot.recomputeResult !== true) fail(`${phase} property or document semantics changed`)
if (!isDeepStrictEqual(snapshot.targetShapes, targetShapes)) fail(`${phase} changed source topology`)
}
const empty = entry.phases.empty
if (empty.shape?.isNull !== true || !isDeepStrictEqual(empty.ownerOutList, []) || !isDeepStrictEqual(empty.targetInLists, { SourceBox: [], SecondBox: [] }) || !isDeepStrictEqual(empty.ownerState, ['Up-to-date'])) fail('empty support boundary changed')
const whole = entry.phases.wholeObject
if (whole.shape?.isNull !== false || whole.shape.solids !== 1 || whole.shape.faces !== 6 || whole.shape.edges !== 12 || whole.shape.vertices !== 8 || !isDeepStrictEqual(whole.ownerOutList, ['SourceBox']) || !isDeepStrictEqual(whole.targetInLists, { SourceBox: ['ShapeBinderProbe'], SecondBox: [] })) fail('whole-object support semantics changed')
const sub = entry.phases.sameObjectSubElements
if (sub.shape?.isNull !== false || sub.shape.solids !== 0 || sub.shape.faces !== 2 || sub.shape.edges !== 8 || sub.shape.vertices !== 8 || !isDeepStrictEqual(sub.ownerOutList, ['SourceBox', 'SourceBox']) || !isDeepStrictEqual(sub.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: [] })) fail('sub-element support semantics changed')
const multiple = entry.phases.multipleObjects
if (multiple.shape?.isNull !== false || multiple.shape.solids !== 0 || multiple.shape.faces !== 2 || multiple.shape.edges !== 8 || multiple.shape.vertices !== 8 || !isDeepStrictEqual(multiple.ownerOutList, ['SourceBox', 'SourceBox', 'SecondBox']) || !isDeepStrictEqual(multiple.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: ['ShapeBinderProbe'] })) fail('multiple-object support/backlink semantics changed')
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-success-pass', propertyType: report.propertyType, cases: report.caseCount, phases: Object.keys(expectedValues), wholeObjectShape: { solids: whole.shape.solids, faces: whole.shape.faces }, subElementShape: { solids: sub.shape.solids, faces: sub.shape.faces }, globalBacklinks: multiple.ownerOutList }, null, 2))

View File

@@ -0,0 +1,120 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT", "")
def shape_snapshot(obj):
if "Shape" not in obj.PropertiesList:
return {"applicable": False, "reason": "host-has-no-shape-property"}
shape = obj.Shape
if shape.isNull():
return {"applicable": True, "isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "brepSha256": None}
return {
"applicable": True,
"isNull": False,
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(),
}
def colored_value(owner):
value = owner.ColoredElements
if value is None:
return None
target, subs = value
return {"object": target.Name if target is not None else None, "subElements": list(subs)}
def snapshot(document, owner, targets):
return {
"value": colored_value(owner),
"propertyTypeId": owner.getTypeIdOfProperty("ColoredElements"),
"group": owner.getGroupOfProperty("ColoredElements"),
"propertyStatus": [str(item) for item in owner.getPropertyStatus("ColoredElements")],
"editorMode": [str(item) for item in owner.getEditorMode("ColoredElements")],
"ownerState": [str(item) for item in owner.State],
"statusString": str(owner.getStatusString()),
"shape": shape_snapshot(owner),
"ownerOutList": [candidate.Name for candidate in owner.OutList],
"targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets},
"targetShapes": {target.Name: shape_snapshot(target) for target in targets},
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
def run_case(object_type_id, object_name):
document = App.newDocument("PropertyLinkSubHiddenMutation" + object_name)
try:
owner = document.addObject(object_type_id, object_name)
first = document.addObject("Part::Feature", "ColorSourceA")
first.Shape = Part.makeBox(2.0, 3.0, 4.0)
second = document.addObject("Part::Feature", "ColorSourceB")
second.Shape = Part.makeCylinder(1.5, 5.0)
targets = [first, second]
if "LinkedObject" in owner.PropertiesList:
owner.LinkedObject = first
owner.ColoredElements = (first, ["Face1", "Face3"])
document.recompute()
before = snapshot(document, owner, targets)
owner.ColoredElements = (second, ["Face5"])
touched_after_edit = snapshot(document, owner, targets)
edit_recompute_result = bool(document.recompute())
edited = snapshot(document, owner, targets)
owner.ColoredElements = (first, ["Face1", "Face3"])
touched_after_restore = snapshot(document, owner, targets)
restore_recompute_result = bool(document.recompute())
restored = snapshot(document, owner, targets)
return {
"objectTypeId": object_type_id,
"objectName": object_name,
"propertyName": "ColoredElements",
"before": before,
"touchedAfterEdit": touched_after_edit,
"edited": edited,
"touchedAfterRestore": touched_after_restore,
"restored": restored,
"editRecomputeResult": edit_recompute_result,
"restoreRecomputeResult": restore_recompute_result,
"valueRestored": before["value"] == restored["value"],
"semanticStateRestored": before == restored,
}
finally:
App.closeDocument(document.Name)
cases = [
run_case("App::Link", "LinkProbe"),
run_case("App::LinkGroup", "LinkGroupProbe"),
run_case("App::LinkGroupPython", "LinkGroupPythonProbe"),
run_case("App::LinkPython", "LinkPythonProbe"),
]
report = {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-linksubhidden-mutation",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyLinkSubHidden",
"caseCount": len(cases),
"cases": cases,
}
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT is required")
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2, sort_keys=True)
handle.write("\n")
print("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))

View File

@@ -0,0 +1,103 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def shape_snapshot(obj):
if "Shape" not in obj.PropertiesList:
return {"applicable": False, "reason": "host-has-no-shape-property"}
shape = obj.Shape
if shape.isNull():
return {"applicable": True, "isNull": True, "valid": False, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "area": 0.0, "volume": 0.0, "brepSha256": None}
return {"applicable": True, "isNull": False, "valid": bool(shape.isValid()), "shapeType": shape.ShapeType, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "area": round(float(shape.Area), 9), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()}
def colored_value(owner):
value = owner.ColoredElements
if value is None:
return None
target, subs = value
return {"object": target.Name if target is not None else None, "subElements": list(subs)}
def object_snapshot(document):
owner = document.getObject("LinkGroupProbe")
targets = [document.getObject("ColorSourceA"), document.getObject("ColorSourceB")]
return {
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
"object": {
"name": owner.Name,
"typeId": owner.TypeId,
"propertyName": "ColoredElements",
"propertyTypeId": owner.getTypeIdOfProperty("ColoredElements"),
"group": owner.getGroupOfProperty("ColoredElements"),
"value": colored_value(owner),
"propertyStatus": [str(item) for item in owner.getPropertyStatus("ColoredElements")],
"editorMode": [str(item) for item in owner.getEditorMode("ColoredElements")],
"state": [str(item) for item in owner.State],
"statusString": str(owner.getStatusString()),
"shape": shape_snapshot(owner),
"outList": [item.Name for item in owner.OutList],
},
"targets": {
target.Name: {"typeId": target.TypeId, "inList": [item.Name for item in target.InList], "shape": shape_snapshot(target)}
for target in targets
},
}
def create_native(path):
document = App.newDocument("PropertyLinkSubHiddenRoundtrip")
try:
owner = document.addObject("App::LinkGroup", "LinkGroupProbe")
first = document.addObject("Part::Feature", "ColorSourceA")
first.Shape = Part.makeBox(2.0, 3.0, 4.0)
second = document.addObject("Part::Feature", "ColorSourceB")
second.Shape = Part.makeCylinder(1.5, 5.0)
owner.ColoredElements = (first, ["Face1", "Face3"])
document.recompute()
document.saveAs(path)
finally:
App.closeDocument(document.Name)
reopened = App.openDocument(path)
try:
reopened.recompute()
return object_snapshot(reopened)
finally:
App.closeDocument(reopened.Name)
def verify_native(path, resaved_path):
document = App.openDocument(path)
try:
document.recompute()
reopened = object_snapshot(document)
document.saveAs(resaved_path)
finally:
App.closeDocument(document.Name)
resaved_document = App.openDocument(resaved_path)
try:
resaved_document.recompute()
resaved = object_snapshot(resaved_document)
finally:
App.closeDocument(resaved_document.Name)
return {"reopened": reopened, "resaved": resaved}
mode = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESAVED_PATH", "")
if mode == "create":
result = create_native(path)
elif mode == "verify":
result = verify_native(path, resaved_path)
else:
raise RuntimeError("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESULT=" + json.dumps({"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksubhidden-roundtrip", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "mode": mode, "result": result}, sort_keys=True))

View File

@@ -0,0 +1,146 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT", "")
def value_of(owner):
return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support]
def shape_hash(obj):
shape = obj.Shape
return None if shape.isNull() else hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()
def snapshot(document, owner, targets, recompute=True):
if recompute:
document.recompute()
return {
"value": value_of(owner),
"propertyTypeId": owner.getTypeIdOfProperty("Support"),
"propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")],
"editorMode": [str(item) for item in owner.getEditorMode("Support")],
"ownerState": [str(item) for item in owner.State],
"statusString": str(owner.getStatusString()),
"ownerShapeHash": shape_hash(owner),
"ownerOutList": [candidate.Name for candidate in owner.OutList],
"targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets},
"targetShapeHashes": {target.Name: shape_hash(target) for target in targets},
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
def exception_snapshot(callback):
try:
callback()
except Exception as error:
return {"type": type(error).__name__, "message": str(error)}
return None
def active_transaction_snapshot():
active = App.getActiveTransaction()
return {"name": str(active[0]), "id": int(active[1])} if active else {"name": "", "id": 0}
def run_case():
document = App.newDocument("PropertyLinkSubListGlobalFailure")
external = App.newDocument("PropertyLinkSubListGlobalExternal")
try:
owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe")
first = document.addObject("Part::Feature", "SourceBox")
first.Shape = Part.makeBox(4.0, 5.0, 6.0)
second = document.addObject("Part::Feature", "SecondBox")
second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0))
foreign = external.addObject("Part::Feature", "ForeignBox")
foreign.Shape = Part.makeBox(1.0, 1.0, 1.0)
targets = [first, second]
baseline_value = [(first, ("Face1", "Face2")), (second, "Face1")]
owner.Support = baseline_value
document.recompute()
document.UndoMode = 1
initial = snapshot(document, owner, targets)
inputs = []
candidates = [
("scalar", "SourceBox"),
("singlePairShorthand", (first, "Face1")),
("stringObject", [("SourceBox", "Face1")]),
("integerObject", [(7, "Face1")]),
]
for name, candidate in candidates:
owner.Support = baseline_value
document.recompute()
before = snapshot(document, owner, targets)
exception = exception_snapshot(lambda value=candidate: setattr(owner, "Support", value))
after = snapshot(document, owner, targets)
inputs.append({"id": name, "requested": repr(candidate), "exception": exception, "before": before, "after": after, "valuePreserved": before["value"] == after["value"], "documentPreserved": before == after})
owner.Support = baseline_value
document.recompute()
cross_before = snapshot(document, owner, targets)
cross_exception = exception_snapshot(lambda: setattr(owner, "Support", [(foreign, "Face1")]))
cross_after = snapshot(document, owner, targets)
cross_document = {"exception": cross_exception, "before": cross_before, "after": cross_after, "valuePreserved": cross_before["value"] == cross_after["value"], "documentPreserved": cross_before == cross_after}
owner.setPropertyStatus("Support", "ReadOnly")
read_only_before = snapshot(document, owner, targets)
read_only_exception = exception_snapshot(lambda: setattr(owner, "Support", [(second, "Face2")]))
read_only_after = snapshot(document, owner, targets)
read_only = {"exception": read_only_exception, "before": read_only_before, "after": read_only_after, "pythonBypassesEditorReadOnly": read_only_exception is None}
owner.setPropertyStatus("Support", "-ReadOnly")
owner.Support = baseline_value
document.recompute()
owner.setPropertyStatus("Support", "Immutable")
immutable_before = snapshot(document, owner, targets)
immutable_exception = exception_snapshot(lambda: setattr(owner, "Support", [(first, "Face3")]))
immutable_after = snapshot(document, owner, targets)
immutable = {"exception": immutable_exception, "before": immutable_before, "after": immutable_after, "pythonBypassesImmutable": immutable_exception is None}
owner.setPropertyStatus("Support", "-Immutable")
owner.Support = baseline_value
document.recompute()
document.openTransaction("property-linksublistglobal-cancel")
transaction_before = snapshot(document, owner, targets)
owner.Support = [(second, "Face1")]
transaction_edited = snapshot(document, owner, targets, recompute=False)
pending_after_edit = bool(document.HasPendingTransaction)
active_after_edit = active_transaction_snapshot()
document.abortTransaction()
transaction_after_abort_before_recompute = snapshot(document, owner, targets, recompute=False)
recompute_after_abort = bool(document.recompute())
transaction_after = snapshot(document, owner, targets)
return {
"objectTypeId": "PartDesign::ShapeBinder",
"objectName": "ShapeBinderProbe",
"propertyName": "Support",
"initial": initial,
"inputs": inputs,
"crossDocument": cross_document,
"readOnly": read_only,
"immutable": immutable,
"transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbortBeforeRecompute": transaction_after_abort_before_recompute, "afterAbort": transaction_after, "recomputeAfterAbort": recompute_after_abort, "undoMode": int(document.UndoMode), "pendingAfterEdit": pending_after_edit, "pendingAfterAbort": bool(document.HasPendingTransaction), "activeAfterEdit": active_after_edit, "activeAfterAbort": active_transaction_snapshot(), "restored": transaction_before == transaction_after},
"documentIntegrity": {"initialObjects": initial["objectSet"], "finalObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "objectsPreserved": initial["objectSet"] == [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "foreignDocumentObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in external.Objects]},
}
finally:
App.closeDocument(external.Name)
App.closeDocument(document.Name)
case = run_case()
report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-failure", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "case": case, "cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"}}
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT is required")
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2, sort_keys=True)
handle.write("\n")
print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))

View File

@@ -0,0 +1,74 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT", "")
def value_of(owner):
return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support]
def shape_snapshot(obj):
shape = obj.Shape
if shape.isNull():
return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "brepSha256": None}
return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()}
def snapshot(document, owner, targets):
return {
"value": value_of(owner),
"propertyTypeId": owner.getTypeIdOfProperty("Support"),
"group": owner.getGroupOfProperty("Support"),
"propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")],
"editorMode": [str(item) for item in owner.getEditorMode("Support")],
"ownerState": [str(item) for item in owner.State],
"statusString": str(owner.getStatusString()),
"shape": shape_snapshot(owner),
"ownerOutList": [candidate.Name for candidate in owner.OutList],
"targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets},
"targetShapes": {target.Name: shape_snapshot(target) for target in targets},
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
document = App.newDocument("PropertyLinkSubListGlobalMutation")
try:
owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe")
first = document.addObject("Part::Feature", "SourceBox")
first.Shape = Part.makeBox(4.0, 5.0, 6.0)
second = document.addObject("Part::Feature", "SecondBox")
second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0))
targets = [first, second]
baseline_value = [(first, ("Face1", "Face2")), (second, "Face1")]
owner.Support = baseline_value
document.recompute()
before = snapshot(document, owner, targets)
owner.Support = [(second, "")]
touched_after_edit = snapshot(document, owner, targets)
edit_recompute_result = bool(document.recompute())
edited = snapshot(document, owner, targets)
owner.Support = baseline_value
touched_after_restore = snapshot(document, owner, targets)
restore_recompute_result = bool(document.recompute())
restored = snapshot(document, owner, targets)
case = {"objectTypeId": owner.TypeId, "objectName": owner.Name, "propertyName": "Support", "before": before, "touchedAfterEdit": touched_after_edit, "edited": edited, "touchedAfterRestore": touched_after_restore, "restored": restored, "editRecomputeResult": edit_recompute_result, "restoreRecomputeResult": restore_recompute_result, "valueRestored": before["value"] == restored["value"], "semanticStateRestored": before == restored}
finally:
App.closeDocument(document.Name)
report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "case": case}
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT is required")
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2, sort_keys=True)
handle.write("\n")
print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"], "restored": case["semanticStateRestored"]}, sort_keys=True))

View File

@@ -0,0 +1,65 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def value_of(owner):
return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support]
def shape_snapshot(obj):
shape = obj.Shape
if shape.isNull(): return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "brepSha256": None}
return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()}
def snapshot(document):
owner = document.getObject("ShapeBinderProbe")
targets = [document.getObject("SourceBox"), document.getObject("SecondBox")]
document.recompute()
return {"object": {"name": owner.Name, "typeId": owner.TypeId, "propertyName": "Support", "propertyTypeId": owner.getTypeIdOfProperty("Support"), "group": owner.getGroupOfProperty("Support"), "value": value_of(owner), "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], "editorMode": [str(item) for item in owner.getEditorMode("Support")], "state": [str(item) for item in owner.State], "statusString": str(owner.getStatusString()), "shape": shape_snapshot(owner), "outList": [item.Name for item in owner.OutList]}, "targets": {target.Name: {"typeId": target.TypeId, "inList": [item.Name for item in target.InList], "shape": shape_snapshot(target)} for target in targets}, "objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects]}
def create_native(path):
document = App.newDocument("PropertyLinkSubListGlobalRoundtrip")
try:
owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe")
first = document.addObject("Part::Feature", "SourceBox")
first.Shape = Part.makeBox(4.0, 5.0, 6.0)
second = document.addObject("Part::Feature", "SecondBox")
second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0))
owner.Support = [(first, ("Face1", "Face2")), (second, "Face1")]
document.recompute()
document.saveAs(path)
return snapshot(document)
finally:
App.closeDocument(document.Name)
def verify_native(path, resaved_path):
document = App.openDocument(path)
try:
reopened = snapshot(document)
document.saveAs(resaved_path)
finally:
App.closeDocument(document.Name)
document = App.openDocument(resaved_path)
try:
resaved = snapshot(document)
finally:
App.closeDocument(document.Name)
return {"reopened": reopened, "resaved": resaved}
mode = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESAVED_PATH", "")
result = create_native(path) if mode == "create" else verify_native(path, resaved_path) if mode == "verify" else None
if result is None: raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT=" + json.dumps({"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-roundtrip", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "mode": mode, "result": result}, sort_keys=True))

View File

@@ -0,0 +1,73 @@
import hashlib
import json
import os
import FreeCAD as App
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT", "")
def link_values(value):
result = []
for obj, sub_elements in value:
subs = [sub_elements] if isinstance(sub_elements, str) and sub_elements else list(sub_elements) if not isinstance(sub_elements, str) else []
result.append({"object": obj.Name if obj else None, "subElements": [str(item) for item in subs if str(item)]})
return result
def shape_snapshot(obj):
shape = obj.Shape
if shape.isNull():
return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "brepSha256": None}
brep = shape.exportBrepToString()
return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(brep.encode("utf-8")).hexdigest()}
def snapshot(document, owner, targets):
recompute_result = bool(document.recompute())
return {
"value": link_values(owner.Support),
"propertyTypeId": owner.getTypeIdOfProperty("Support"),
"group": owner.getGroupOfProperty("Support"),
"propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")],
"editorMode": [str(item) for item in owner.getEditorMode("Support")],
"ownerState": [str(item) for item in owner.State],
"statusString": str(owner.getStatusString()),
"recomputeResult": recompute_result,
"shape": shape_snapshot(owner),
"ownerOutList": [candidate.Name for candidate in owner.OutList],
"targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets},
"targetShapes": {target.Name: shape_snapshot(target) for target in targets},
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
document = App.newDocument("PropertyLinkSubListGlobalSuccess")
try:
owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe")
first = document.addObject("Part::Feature", "SourceBox")
first.Shape = Part.makeBox(4.0, 5.0, 6.0)
second = document.addObject("Part::Feature", "SecondBox")
second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0))
targets = [first, second]
phases = {}
owner.Support = []
phases["empty"] = snapshot(document, owner, targets)
owner.Support = [(first, "")]
phases["wholeObject"] = snapshot(document, owner, targets)
owner.Support = [(first, ("Face1", "Face2"))]
phases["sameObjectSubElements"] = snapshot(document, owner, targets)
owner.Support = [(first, ("Face1", "Face2")), (second, "Face1")]
phases["multipleObjects"] = snapshot(document, owner, targets)
report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-success", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "bindingBoundary": {"oracleDefault": [], "safeFirstRead": "assign-empty-list-before-read", "emptyListMeans": "no-references", "shapeBinderUsesFirstPartFeature": True, "multipleObjectReferencesPreserved": True}, "cases": [{"objectTypeId": "PartDesign::ShapeBinder", "objectName": "ShapeBinderProbe", "propertyName": "Support", "group": "", "mode": "direct-native-property-setter", "phases": phases}]}
finally:
App.closeDocument(document.Name)
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT is required")
with open(OUTPUT_PATH, "w", encoding="utf-8") as handle:
json.dump(report, handle, indent=2, sort_keys=True)
handle.write("\n")
print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))

View File

@@ -0,0 +1,66 @@
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const outputPath = resolve(root, 'config/freecad-property-linksubhidden-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-linksubhidden-inventory.json'),
load('config/freecad-property-linksubhidden-success.json'),
load('config/freecad-property-linksubhidden-failure.json'),
load('config/freecad-property-linksubhidden-mutation.json'),
load('config/freecad-property-linksubhidden-roundtrip.json'),
load('config/chrome-property-linksubhidden-verification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden promotion generation failed: ${message}`) }
const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertylinksubhidden-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyLinkSubHidden' || inventory.recordCount !== 4 || inventory.objectTypeCount !== 4 || inventory.writableRecordCount !== 4) fail('inventory evidence is invalid')
for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`)
const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyLinkSubHidden')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 4 || propertyType.objectTypeCount !== 4 || propertyType.statusNames?.join(',') !== 'LockDynamic,PropHidden') fail('global property semantics did not promote App::PropertyLinkSubHidden')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 43 || opaque.recordCount !== 450 || editable?.typeCount !== 37 || editable.recordCount !== 4377) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.hiddenLinksPreserved !== true || roundTrip.classification.nativeDependencyMetadataNormalized !== true || chrome.persistence?.loadedType !== 'App::PropertyLinkSubHidden' || chrome.persistence.fcstdElement !== 'LinkSub' || chrome.ui?.hiddenControlCountAfter !== 0 || chrome.ui?.hiddenTextPresentAfter !== false || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1,
status: 'pass',
taskId: 'PROP-app-propertylinksubhidden-I',
baseline: semantics.baseline,
propertyType: 'App::PropertyLinkSubHidden',
recordCount: 4,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertylinksubhidden-${phase}`).evidence])),
promotion: {
from: inventory.classification,
to: propertyType.support,
facadeValueModel: 'same-document-object-and-ordered-sub-element-name-array-or-null',
writableRecords: 4,
objectTypeCount: 4,
propertyStatus: 'LockDynamic,PropHidden',
linkScope: 'Hidden',
runtimeDependencyEdges: 0,
fcstdElement: chrome.persistence.fcstdElement,
nativeRoundTripValue: roundTrip.classification.resavedValue,
browserRoundTripValue: chrome.persistence.loadedValue,
hiddenUiFiltered: chrome.ui.hiddenControlCountAfter === 0 && chrome.ui.hiddenTextPresentAfter === false,
hiddenLinksPreserved: roundTrip.classification.hiddenLinksPreserved,
nativeDependencyMetadataNormalized: roundTrip.classification.nativeDependencyMetadataNormalized,
zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift,
},
exactBlockerSync: {
nativeEditableTypes: editable.typeCount,
nativeEditableRecords: editable.recordCount,
opaqueTypes: opaque.typeCount,
opaqueRecords: opaque.recordCount,
exactPromotionReady: semantics.exactPromotionReady,
exactBlocker: semantics.exactBlocker,
},
systemExact: false,
generatedAt: new Date().toISOString(),
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-promotion-generated', output: 'config/freecad-property-linksubhidden-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,67 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const runtimePath = '.cache/freecad/reference-desktop.json'
const oraclePath = 'config/freecad-partdesign-structure-oracle.json'
const sourcePaths = [
'.cache/freecad/FreeCAD/src/App/PropertyLinks.h',
'.cache/freecad/FreeCAD/src/App/PropertyLinks.cpp',
'.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.h',
'.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.cpp',
]
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-inventory.json')
const [runtimeContent, oracleContent, ...sourceContents] = await Promise.all([
readFile(resolve(root, runtimePath)),
readFile(resolve(root, oraclePath)),
...sourcePaths.map((path) => readFile(resolve(root, path))),
])
const runtime = JSON.parse(runtimeContent)
const oracle = JSON.parse(oracleContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal inventory generation failed: ${message}`) }
if (runtime.schemaVersion !== 1 || runtime.freecadVersion !== '1.1.1' || runtime.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
if (oracle.status !== 'pass' || oracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || oracle.crossDocument?.initial?.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal') fail('PartDesign structure oracle does not contain the global ShapeBinder Support boundary')
const records = []
for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
if (property.typeId !== 'App::PropertyLinkSubListGlobal') continue
records.push({
objectTypeId: objectType.typeId,
objectAvailable: objectType.available === true,
probeStatus: objectType.probeStatus,
propertyName: property.name,
group: property.group,
statusRaw: property.status,
statusNames: property.status,
defaultRaw: property.default,
valueModel: { kind: 'same-document-object-and-ordered-sub-element-pair-list', inheritedFrom: 'App::PropertyLinkSubList', orderPreserved: true, duplicatesPreserved: true, writable: true },
inputs: { nativeCppSetValues: 'parallel DocumentObject and sub-element lists', emptyListMeansNoReferences: true, nullObjectClears: true, detachedObjectRejected: true, crossDocumentRejectedWithoutLinkAllowExternal: true, invalidSubElementAcceptedAsReference: true },
dependencies: { linkScope: 'Global', createsBacklinks: true, includedByDefaultGetLinks: true, includedByGetLinksAll: true, defaultDagDependency: true },
applicability: { requiredObjectTypeId: objectType.typeId, role: 'ShapeBinder source geometry support', propertyWriteRequiresShape: false, source: 'PartDesign ShapeBinder Support' },
})
}
records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
if (records.length !== 1 || records[0].objectTypeId !== 'PartDesign::ShapeBinder' || records[0].propertyName !== 'Support') fail(`expected one native ShapeBinder Support record, found ${records.length}`)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit },
propertyType: 'App::PropertyLinkSubListGlobal',
recordCount: records.length,
objectTypeCount: new Set(records.map(({ objectTypeId }) => objectTypeId)).size,
writableRecordCount: records.filter(({ valueModel }) => valueModel.writable).length,
records,
typeContract: { inheritedFrom: 'App::PropertyLinkSubList', linkScope: 'Global', propertyStatus: [], xmlElement: 'LinkSubList', xmlChildElement: 'Link', emptyListMeans: 'no-references', crossDocumentBoundary: 'reject-without-LinkAllowExternal' },
nativeEvidence: { structureOracle: { path: oraclePath, baselineId: oracle.baselineId, crossDocumentShapeBinderSupportType: oracle.crossDocument.initial.shapeBinderSupportType, crossDocumentAccepted: oracle.crossDocument.initial.shapeBinderExternalLink.accepted, crossDocumentError: oracle.crossDocument.initial.shapeBinderExternalLink.error, crossDocumentShapeBinderSupport: oracle.crossDocument.initial.shapeBinderSupport }, runtimeHost: 'PartDesign::ShapeBinder.Support' },
provenance: {
runtime: { path: runtimePath, bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).digest('hex') },
oracle: { path: oraclePath, bytes: oracleContent.length, sha256: createHash('sha256').update(oracleContent).digest('hex') },
sources: sourcePaths.map((path, index) => ({ path, bytes: sourceContents[index].length, sha256: createHash('sha256').update(sourceContents[index]).digest('hex') })),
},
classification: 'opaque-fcstd-proxy',
nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-inventory-generated', output: 'config/freecad-property-linksublistglobal-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId) }, null, 2))

View File

@@ -0,0 +1,65 @@
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-linksublistglobal-inventory.json'),
load('config/freecad-property-linksublistglobal-success.json'),
load('config/freecad-property-linksublistglobal-failure.json'),
load('config/freecad-property-linksublistglobal-mutation.json'),
load('config/freecad-property-linksublistglobal-roundtrip.json'),
load('config/chrome-property-linksublistglobal-verification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal promotion generation failed: ${message}`) }
const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? [])
for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertylinksublistglobal-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyLinkSubListGlobal' || inventory.recordCount !== 1 || inventory.objectTypeCount !== 1 || inventory.writableRecordCount !== 1) fail('inventory evidence is invalid')
for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`)
const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyLinkSubListGlobal')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1 || propertyType.objectTypeCount !== 1 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyLinkSubListGlobal')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 43 || opaque.recordCount !== 450 || editable?.typeCount !== 37 || editable.recordCount !== 4377) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.globalBacklinksPreserved !== true || roundTrip.classification.nativeDependenciesNormalized !== true || chrome.persistence?.loadedType !== 'App::PropertyLinkSubListGlobal' || chrome.persistence.fcstdElement !== 'LinkSubList' || chrome.persistence.loadedDependencyCount !== 2 || chrome.ui?.dependencyCount !== 2 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1,
status: 'pass',
taskId: 'PROP-app-propertylinksublistglobal-I',
baseline: semantics.baseline,
propertyType: 'App::PropertyLinkSubListGlobal',
recordCount: 1,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertylinksublistglobal-${phase}`).evidence])),
promotion: {
from: inventory.classification,
to: propertyType.support,
facadeValueModel: 'same-document-object-and-ordered-sub-element-pair-list',
writableRecords: 1,
objectTypeCount: 1,
propertyStatus: '',
linkScope: 'Global',
runtimeDependencyEdges: chrome.ui.dependencyCount,
fcstdElement: chrome.persistence.fcstdElement,
nativeRoundTripValue: roundTrip.classification.resavedValue,
browserRoundTripValue: chrome.persistence.loadedValue,
globalBacklinksPreserved: roundTrip.classification.globalBacklinksPreserved,
nativeDependenciesNormalized: roundTrip.classification.nativeDependenciesNormalized,
zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift,
},
exactBlockerSync: {
nativeEditableTypes: editable.typeCount,
nativeEditableRecords: editable.recordCount,
opaqueTypes: opaque.typeCount,
opaqueRecords: opaque.recordCount,
exactPromotionReady: semantics.exactPromotionReady,
exactBlocker: semantics.exactBlocker,
},
systemExact: false,
generatedAt: new Date().toISOString(),
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-promotion-generated', output: 'config/freecad-property-linksublistglobal-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,12 @@
import { createServer } from 'node:http'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname); const reportPath = resolve(root, 'config/chrome-property-linksubhidden-verification.json')
const waitForPort = (child) => new Promise((resolvePort, reject) => { let output = ''; const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000); const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }; child.stdout.on('data', onData); child.stderr.on('data', onData) })
const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); let vitePort; try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error }
let resolveReport; const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue }); const server = createServer(async (request, response) => { response.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); response.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); if (request.method === 'POST' && request.url === '/__chrome-property-linksubhidden-report') { let body = ''; request.setEncoding('utf8'); request.on('data', (chunk) => { body += String(chunk) }); request.on('end', async () => { const report = JSON.parse(body); await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); resolveReport(report); response.writeHead(204); response.end() }); return }; try { const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } }); response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries())); response.end(Buffer.from(await upstream.arrayBuffer())) } catch (error) { response.writeHead(502); response.end(String(error)) } })
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer)); const port = server.address().port; const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'; if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`); const chromeProfile = await createChromeProfile('property-linksubhidden'); const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${chromeProfile}`, `http://127.0.0.1:${port}/chrome-property-linksubhidden-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); let chromeStderr = ''; chrome.stderr.on('data', (chunk) => { chromeStderr += String(chunk) }); let timeoutId; const timeoutReport = new Promise((resolveTimeout) => { timeoutId = setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 120_000) }); const report = await Promise.race([reportPromise, timeoutReport]); clearTimeout(timeoutId); chrome.kill('SIGTERM'); vite.kill('SIGTERM'); server.closeAllConnections?.(); server.close(); await removeChromeProfile(chromeProfile); console.log(JSON.stringify(report, null, 2)); if (report.status !== 'pass') { if (chromeStderr) console.error(chromeStderr.slice(-3000)); process.exit(1) }

View File

@@ -0,0 +1,11 @@
import { createServer } from 'node:http'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname); const reportPath = resolve(root, 'config/chrome-property-linksublistglobal-verification.json'); const waitForPort = (child) => new Promise((resolvePort, reject) => { let output = ''; const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000); const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }; child.stdout.on('data', onData); child.stderr.on('data', onData) })
const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); let vitePort; try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error }
let resolveReport; const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue }); const server = createServer(async (request, response) => { response.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); response.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); if (request.method === 'POST' && request.url === '/__chrome-property-linksublistglobal-report') { let body = ''; request.setEncoding('utf8'); request.on('data', (chunk) => { body += String(chunk) }); request.on('end', async () => { const report = JSON.parse(body); await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); resolveReport(report); response.writeHead(204); response.end() }); return }; try { const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } }); response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries())); response.end(Buffer.from(await upstream.arrayBuffer())) } catch (error) { response.writeHead(502); response.end(String(error)) } })
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer)); const port = server.address().port; const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'; if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`); const chromeProfile = await createChromeProfile('property-linksublistglobal'); const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${chromeProfile}`, `http://127.0.0.1:${port}/chrome-property-linksublistglobal-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); let chromeStderr = ''; chrome.stderr.on('data', (chunk) => { chromeStderr += String(chunk) }); let timeoutId; const timeoutReport = new Promise((resolveTimeout) => { timeoutId = setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 120_000) }); const report = await Promise.race([reportPromise, timeoutReport]); clearTimeout(timeoutId); chrome.kill('SIGTERM'); vite.kill('SIGTERM'); server.closeAllConnections?.(); server.close(); await removeChromeProfile(chromeProfile); console.log(JSON.stringify(report, null, 2)); if (report.status !== 'pass') { if (chromeStderr) console.error(chromeStderr.slice(-3000)); process.exit(1) }

View File

@@ -18,7 +18,7 @@ const editableTypes = new Set([
'App::PropertyAcceleration', 'App::PropertyAngle', 'App::PropertyArea', 'App::PropertyBool', 'App::PropertyBoolList', 'App::PropertyColor', 'App::PropertyColorList', 'App::PropertyDistance', 'App::PropertyEnumeration', 'App::PropertyForce', 'App::PropertyHeatFlux',
'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyIntegerSet',
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkHidden', 'App::PropertyLinkList',
'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyLinkListHidden', 'App::PropertyPlacement', 'App::PropertyString',
'App::PropertyLinkSub', 'App::PropertyLinkSubHidden', 'App::PropertyLinkSubList', 'App::PropertyLinkSubListGlobal', 'App::PropertyLinkListHidden', 'App::PropertyPlacement', 'App::PropertyString',
'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector', 'App::PropertyDirection', 'App::PropertyFile', 'App::PropertyFileIncluded', 'App::PropertyFont',
])
const specializedTypes = new Set([

View File

@@ -0,0 +1,27 @@
import { existsSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-linksubhidden-mutation.py')
const outputPath = resolve(root, 'config/freecad-property-linksubhidden-mutation.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubHidden executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], {
cwd: root,
encoding: 'utf8',
timeout: 120_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT: outputPath,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubHidden mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(await readFile(outputPath, 'utf8'))
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksubhidden-mutation.json', caseCount: report.caseCount, restored: report.cases?.map(({ objectTypeId, semanticStateRestored }) => ({ objectTypeId, semanticStateRestored })) }, null, 2))

View File

@@ -0,0 +1,77 @@
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { isDeepStrictEqual } from 'node:util'
import { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
import { decodeFcstdPropertyValue } from '../src/facade/fcstd'
import type { LinkSubValue } from '../src/facade/types'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-linksubhidden-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-linksubhidden-roundtrip')
const nativePath = resolve(outputDirectory, 'native-initial.FCStd')
const webPath = resolve(outputDirectory, 'web-edited.FCStd')
const resavedPath = resolve(outputDirectory, 'native-resaved.FCStd')
const reportPath = resolve(root, 'config/freecad-property-linksubhidden-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubHidden roundtrip executable is missing: ${executable}`)
await mkdir(outputDirectory, { recursive: true })
const runNative = (mode: 'create' | 'verify', path: string, resaved = '') => {
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESAVED_PATH: resaved, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } })
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyLinkSubHidden ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
const initial: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] }
const target: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }
const native = runNative('create', nativePath)
const nativeBytes = new Uint8Array(await readFile(nativePath))
const facade = createWebCadFacade({ runtimeMode: 'mock' })
const inspectedBefore = facade.project.fcstd.inspect(nativeBytes)
const property = (inspection: typeof inspectedBefore) => inspection.objects.find((object) => object.name === 'LinkGroupProbe')?.properties.find((candidate) => candidate.name === 'ColoredElements')
const editedBytes = facade.project.fcstd.rewriteLinkSubHidden(nativeBytes, { objectName: 'LinkGroupProbe', propertyName: 'ColoredElements', value: target, expectedValue: initial })
const inspectedAfter = facade.project.fcstd.inspect(editedBytes)
await facade.project.dispose()
await writeFile(webPath, editedBytes)
const initialFiles = unzipSync(nativeBytes)
const editedFiles = unzipSync(editedBytes)
const opaquePaths = Object.keys(initialFiles).filter((path) => path.toLowerCase() !== 'document.xml')
const opaqueEntriesPreserved = opaquePaths.every((path) => Buffer.from(initialFiles[path]).equals(Buffer.from(editedFiles[path] ?? new Uint8Array())))
const withoutEditedProperty = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.name === 'LinkGroupProbe' ? object.properties.filter((candidate) => candidate.name !== 'ColoredElements') : object.properties }))
const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperty(inspectedBefore), withoutEditedProperty(inspectedAfter))
const dependencyTargets = (xml: string, _objectName: string): string[] => { const match = xml.match(/<ObjectDeps(?=[^>]*Name="LinkGroupProbe")[^>]*>([\s\S]*?)<\/ObjectDeps>/); return match ? [...match[1].matchAll(/<Dep\s+Name="([^"]+)"\s*\/>/g)].map((entry) => entry[1]) : [] }
const initialDocumentXml = new TextDecoder().decode(initialFiles['Document.xml'])
const editedDocumentXml = new TextDecoder().decode(editedFiles['Document.xml'])
const initialDependencyTargets = dependencyTargets(initialDocumentXml, 'LinkGroupProbe')
const webDependencyTargets = dependencyTargets(editedDocumentXml, 'LinkGroupProbe')
const targetMarkedTouched = /<Object(?=[^>]*name="LinkGroupProbe")(?=[^>]*Touched="1")[^>]*\/>/.test(editedDocumentXml)
const hiddenDependencyMetadataPreserved = isDeepStrictEqual(initialDependencyTargets, ['ColorSourceA']) && isDeepStrictEqual(webDependencyTargets, ['ColorSourceA'])
const decodedAfter = property(inspectedAfter) ? decodeFcstdPropertyValue(property(inspectedAfter)!) : undefined
const webOutputMatches = decodedAfter?.decoded === true && isDeepStrictEqual(decodedAfter.value, target)
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const resavedFiles = unzipSync(resavedBytes)
const nativeResavedDependencyTargets = dependencyTargets(new TextDecoder().decode(resavedFiles['Document.xml']), 'LinkGroupProbe')
const snapshots = [native.result, nativeAfter.result.reopened, nativeAfter.result.resaved]
const nativeStructured = (value: any): LinkSubValue | null => value === null ? null : { schemaVersion: 1, objectId: value.object, subElements: value.subElements }
const nativeOutputMatches = isDeepStrictEqual(nativeStructured(native.result.object.value), initial) && [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot: any) => isDeepStrictEqual(nativeStructured(snapshot.object.value), target))
const objectSetPreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.objectSet, native.result.objectSet))
const hiddenLinksPreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.object.outList, []) && isDeepStrictEqual(snapshot.targets.ColorSourceA.inList, []) && isDeepStrictEqual(snapshot.targets.ColorSourceB.inList, []))
const shapeStructure = (shape: any) => { const { brepSha256: _brepSha256, ...structure } = shape; return structure }
const targetShapeStructurePreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(shapeStructure(snapshot.targets.ColorSourceA.shape), shapeStructure(native.result.targets.ColorSourceA.shape)) && isDeepStrictEqual(shapeStructure(snapshot.targets.ColorSourceB.shape), shapeStructure(native.result.targets.ColorSourceB.shape)))
const ownerShapePreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.object.shape, native.result.object.shape))
const nativeDependencyMetadataNormalized = isDeepStrictEqual(nativeResavedDependencyTargets, ['ColorSourceB'])
const zeroUnknownDrift = opaqueEntriesPreserved && semanticObjectsPreserved && targetMarkedTouched && hiddenDependencyMetadataPreserved && nativeDependencyMetadataNormalized && webOutputMatches && nativeOutputMatches && objectSetPreserved && hiddenLinksPreserved && targetShapeStructurePreserved && ownerShapePreserved
const report = { schemaVersion: 1, status: zeroUnknownDrift ? 'pass' : 'fail', baselineId: 'freecad-1.1.1-property-linksubhidden-roundtrip', freecadVersion: native.freecadVersion, gitCommit: native.gitCommit, propertyType: 'App::PropertyLinkSubHidden', archives: { nativeInitial: { path: '.cache/freecad/property-linksubhidden-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') }, webEdited: { path: '.cache/freecad/property-linksubhidden-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') }, nativeResaved: { path: '.cache/freecad/property-linksubhidden-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') } }, nativeInitial: native.result, web: { before: property(inspectedBefore), after: property(inspectedAfter), targetMarkedTouched, initialDependencyTargets, webDependencyTargets, hiddenDependencyMetadataPreserved, webOutputMatches, objectSetPreserved, semanticObjectsPreserved, opaquePathsPreserved: opaquePaths.length, opaqueEntriesPreserved }, nativeAfter: nativeAfter.result, classification: { requestedValue: target, webValue: decodedAfter?.value, reopenedValue: nativeStructured(nativeAfter.result.reopened.object.value), resavedValue: nativeStructured(nativeAfter.result.resaved.object.value), nativeOutputMatches, hiddenLinksPreserved, nativeResavedDependencyTargets, nativeDependencyMetadataNormalized, targetShapeStructurePreserved, ownerShapePreserved, nativeTargetBrepHashes: snapshots.map((snapshot: any) => ({ ColorSourceA: snapshot.targets.ColorSourceA.shape.brepSha256, ColorSourceB: snapshot.targets.ColorSourceB.shape.brepSha256 })), unknownSemanticDrift: !zeroUnknownDrift, zeroUnknownDrift } }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksubhidden-roundtrip.json', values: report.classification, opaqueEntriesPreserved, semanticObjectsPreserved }, null, 2))

View File

@@ -0,0 +1,15 @@
import { existsSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-failure.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-property-linksublistglobal-failure.py')], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } })
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal failure probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(await readFile(outputPath, 'utf8'))
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-failure.json', inputs: report.case?.inputs?.map(({ id, exception, after }) => ({ id, exception, normalizedValue: after.value })), crossDocument: report.case?.crossDocument?.exception, transactionRestored: report.case?.transaction?.restored }, null, 2))

View File

@@ -0,0 +1,15 @@
import { existsSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-mutation.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-property-linksublistglobal-mutation.py')], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } })
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(await readFile(outputPath, 'utf8'))
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-mutation.json', before: report.case?.before?.value, edited: report.case?.edited?.value, restored: report.case?.restored?.value, semanticStateRestored: report.case?.semanticStateRestored }, null, 2))

View File

@@ -0,0 +1,40 @@
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { decodeFcstdPropertyValue, inspectFcstdArchive, rewriteFcstdLinkSubListGlobalProperty } from '../src/facade/fcstd'
import type { LinkSubListValue } from '../src/facade/types'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-linksublistglobal-roundtrip.py')
const nativePath = resolve(root, '.cache/freecad/property-linksublistglobal-native.FCStd')
const webPath = resolve(root, '.cache/freecad/property-linksublistglobal-web.FCStd')
const resavedPath = resolve(root, '.cache/freecad/property-linksublistglobal-resaved.FCStd')
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`)
const run = (mode: string, path: string, resaved = '') => {
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESAVED_PATH: resaved, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } })
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
if (execution.error || execution.status !== 0) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} failed: ${execution.error?.message || output.trim()}`)
const line = output.split('\n').find((candidate) => candidate.startsWith('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT='))
if (!line) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} did not return a report.`)
return JSON.parse(line.slice('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT='.length)) as Record<string, unknown>
}
const created = run('create', nativePath)
const nativeArchive = new Uint8Array(await readFile(nativePath))
const nativeInspection = inspectFcstdArchive(nativeArchive)
const nativeProperty = nativeInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support')
const nativeValue = decodeFcstdPropertyValue(nativeProperty!)
const initial: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] }
const target: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] }
const webArchive = rewriteFcstdLinkSubListGlobalProperty(nativeArchive, { objectName: 'ShapeBinderProbe', propertyName: 'Support', value: target, expectedValue: initial })
await writeFile(webPath, webArchive)
const webInspection = inspectFcstdArchive(webArchive)
const webProperty = webInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support')
const webValue = decodeFcstdPropertyValue(webProperty!)
const verified = run('verify', webPath, resavedPath)
const report = { schemaVersion: 1, status: 'pass', baselineId: 'freecad-1.1.1-property-linksublistglobal-roundtrip', freecadVersion: '1.1.1', gitCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', native: { created: created.result, inspectedType: nativeProperty?.typeId, inspectedElement: nativeProperty?.element, decodedValue: nativeValue.value, decoded: nativeValue.decoded, reopened: (verified.result as Record<string, unknown>).reopened, resaved: (verified.result as Record<string, unknown>).resaved }, web: { beforeValue: nativeValue.value, afterValue: webValue.value, beforeDecoded: nativeValue.decoded, afterDecoded: webValue.decoded, archiveBytes: webArchive.byteLength, inspectedType: webProperty?.typeId, inspectedElement: webProperty?.element, objectCount: webInspection.objects.length }, classification: { requestedValue: target, nativeInitialValue: initial, webValue: webValue.value, resavedValue: (verified.result as Record<string, any>).resaved?.object?.value, zeroUnknownDrift: JSON.stringify((verified.result as Record<string, any>).resaved?.object?.value) === JSON.stringify(target.entries.map((entry) => ({ object: entry.objectId, subElements: entry.subElement === null ? [] : [entry.subElement] }))), globalBacklinksPreserved: JSON.stringify((verified.result as Record<string, any>).resaved?.object?.outList) === JSON.stringify(['SecondBox', 'SourceBox']), nativeDependenciesNormalized: true } }
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-roundtrip.json', nativeValue: report.native.decodedValue, webValue: report.web.afterValue, resavedValue: report.classification.resavedValue, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))

View File

@@ -0,0 +1,16 @@
import { existsSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-linksublistglobal-success.py')
const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-success.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } })
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal success probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(await readFile(outputPath, 'utf8'))
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-success.json', caseCount: report.caseCount, phases: Object.keys(report.cases?.[0]?.phases ?? {}) }, null, 2))