feat: promote font force heat flux and integer set properties
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-17 09:10:47 -04:00
parent d11566403d
commit 8f6053089a
159 changed files with 9687 additions and 163 deletions

View File

@@ -0,0 +1,15 @@
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-font-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`Chrome PropertyFont 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 = 'osifont'
const target = 'Noto Sans CJK SC'
if (report.ui?.inputType !== 'text' || report.ui.label !== 'Font' || report.ui.beforeValue !== initial || report.ui.afterValue !== target || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI font edit evidence is incomplete')
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedValue !== target || report.persistence.loadedType !== 'App::PropertyFont' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'String' || report.persistence.fcstdValue !== target) 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-font-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -0,0 +1,13 @@
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-force-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`Chrome PropertyForce 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')
if (report.ui?.inputType !== 'number' || report.ui.label !== 'Force' || report.ui.unit !== 'mm*kg/s^2' || report.ui.beforeValue !== 0 || report.ui.afterValue !== 500000 || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI Force edit evidence is incomplete')
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedValue !== 500000 || report.persistence.loadedType !== 'App::PropertyForce' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'Float' || report.persistence.fcstdValue !== '500000') 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-force-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -0,0 +1,13 @@
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-heatflux-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`Chrome PropertyHeatFlux 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')
if (report.ui?.inputType !== 'number' || report.ui.label !== 'DFlux' || report.ui.unit !== 'kg/s^3' || report.ui.beforeValue !== 0 || report.ui.afterValue !== 500 || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI HeatFlux edit evidence is incomplete')
if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || report.persistence.loadedValue !== 500 || report.persistence.loadedType !== 'App::PropertyHeatFlux' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'Float' || report.persistence.fcstdValue !== '500') 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-heatflux-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -0,0 +1,15 @@
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-integerset-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`Chrome PropertyIntegerSet 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 target = [-1, 8]
if (report.ui?.inputType !== 'text' || report.ui.label !== 'Nodes' || report.ui.beforeText !== '1, 3' || report.ui.afterText !== '-1, 8' || !isDeepStrictEqual(report.ui.beforeValue, [1, 3]) || !isDeepStrictEqual(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.errors?.length !== 0) fail('production UI edit evidence 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::PropertyIntegerSet' || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'IntegerSet' || report.persistence.fcstdValue !== '[-1,8]') 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-integerset-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2))

View File

@@ -19,6 +19,10 @@ const allowWorkerMessaging = new Set([
'chromePropertyDirectionHarness.tsx',
'chromePropertyFileHarness.tsx',
'chromePropertyFileIncludedHarness.tsx',
'chromePropertyFontHarness.tsx',
'chromePropertyForceHarness.tsx',
'chromePropertyHeatFluxHarness.tsx',
'chromePropertyIntegerSetHarness.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 !== '50 runtime property types and 466 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 !== '46 runtime property types and 457 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 !== 30 || support['native-editable-codec'].recordCount !== 4361 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 50 || support['opaque-fcstd-proxy'].recordCount !== 466) fail('property support partition is stale')
if (support?.['native-editable-codec']?.typeCount !== 34 || support['native-editable-codec'].recordCount !== 4370 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 46 || support['opaque-fcstd-proxy'].recordCount !== 457) 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')
@@ -26,6 +26,14 @@ const file = report.types.find(({ typeId }) => typeId === 'App::PropertyFile')
if (file?.support !== 'native-editable-codec' || file.recordCount !== 16 || file.objectTypeCount !== 12 || file.statusNames?.length !== 0) fail('App::PropertyFile promotion is missing')
const fileIncluded = report.types.find(({ typeId }) => typeId === 'App::PropertyFileIncluded')
if (fileIncluded?.support !== 'native-editable-codec' || fileIncluded.recordCount !== 20 || fileIncluded.objectTypeCount !== 15 || fileIncluded.statusNames?.join(',') !== 'PropOutput,ReadOnly') fail('App::PropertyFileIncluded promotion is missing')
const font = report.types.find(({ typeId }) => typeId === 'App::PropertyFont')
if (font?.support !== 'native-editable-codec' || font.recordCount !== 2 || font.objectTypeCount !== 2 || font.statusNames?.length !== 0) fail('App::PropertyFont promotion is missing')
const force = report.types.find(({ typeId }) => typeId === 'App::PropertyForce')
if (force?.support !== 'native-editable-codec' || force.recordCount !== 4 || force.objectTypeCount !== 2 || force.statusNames?.join(',') !== 'PropOutput') fail('App::PropertyForce promotion is missing')
const heatFlux = report.types.find(({ typeId }) => typeId === 'App::PropertyHeatFlux')
if (heatFlux?.support !== 'native-editable-codec' || heatFlux.recordCount !== 1 || heatFlux.objectTypeCount !== 1 || heatFlux.statusNames?.length !== 0) fail('App::PropertyHeatFlux promotion is missing')
const integerSet = report.types.find(({ typeId }) => typeId === 'App::PropertyIntegerSet')
if (integerSet?.support !== 'native-editable-codec' || integerSet.recordCount !== 2 || integerSet.objectTypeCount !== 2 || integerSet.statusNames?.length !== 0) fail('App::PropertyIntegerSet 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]) {
@@ -33,4 +41,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: 5044, opaqueProxyRecords: 466, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.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: 5053, opaqueProxyRecords: 457, 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], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2))

View File

@@ -21,7 +21,9 @@ for (const phase of requiredPhases) {
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || report.promotion.facadeValueModel !== 'finite-number-mm/s^2' || report.promotion.fcstdElement !== 'Float' || report.promotion.nativeRoundTripValue !== 500 || report.promotion.browserRoundTripValue !== 500 || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (JSON.stringify(sync) !== JSON.stringify({ 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 || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'Float' || chrome.persistence.loadedValue !== 500 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-acceleration-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -21,7 +21,9 @@ for (const phase of requiredPhases) {
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.join(',') !== 'PropOutput,PropReadOnly' || report.promotion.facadeValueModel !== 'read-only-derived-mm^2-with-link-sub-list-input' || report.promotion.inputType !== 'App::PropertyLinkSubList' || report.promotion.fcstdAreaElement !== 'Float' || report.promotion.fcstdElementsElement !== 'LinkSubList' || report.promotion.nativeRoundTripArea !== 20 || report.promotion.browserRoundTripArea !== 0 || report.promotion.browserRoundTripElementCount !== 0 || report.promotion.sourceGeometryPreserved !== true || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (JSON.stringify(sync) !== JSON.stringify({ 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.sourceGeometryPreserved !== true || chrome.status !== 'pass' || chrome.ui?.areaReadOnly !== true || chrome.persistence?.fcstdAreaElement !== 'Float' || chrome.persistence.fcstdElementsElement !== 'LinkSubList' || chrome.persistence.loadedArea !== 0 || chrome.persistence.loadedElementCount !== 0 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-area-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -23,7 +23,9 @@ const target = [true, false, true, false]
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic' || report.promotion.facadeValueModel !== 'variable-boolean-array' || report.promotion.writableRecords !== 3 || report.promotion.immutableHiddenRecords !== 4 || report.promotion.fcstdElement !== 'BoolList' || !isDeepStrictEqual(report.promotion.nativeRoundTripValue, target) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'BoolList' || !isDeepStrictEqual(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-boollist-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -24,7 +24,9 @@ const approximatelyTarget = (value) => Array.isArray(value) && value.length ===
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'four-finite-rgba-channels-with-8-bit-fcstd-packing' || report.promotion.writableRecords !== 8 || report.promotion.fcstdElement !== 'PropertyColor' || report.promotion.fcstdPackedValue !== '862362060' || !approximatelyTarget(report.promotion.nativeRoundTripValue) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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 || !approximatelyTarget(roundTrip.classification.resavedValue) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'PropertyColor' || chrome.persistence.fcstdValue !== '862362060' || !isDeepStrictEqual(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-color-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -25,7 +25,9 @@ const approximately = (value, target, tolerance = 1e-7) => Array.isArray(value)
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'ordered-variable-rgba-array-with-external-8-bit-fcstd-packing' || report.promotion.writableRecords !== 2 || report.promotion.fcstdElement !== 'ColorList' || report.promotion.fcstdResourcePath !== 'ColourList' || report.promotion.fcstdResourceBytes !== 12 || !approximately(report.promotion.nativeRoundTripValue, nativeTarget) || !isDeepStrictEqual(report.promotion.browserRoundTripValue, browserTarget) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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 || !approximately(roundTrip.classification.resavedValue, nativeTarget) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'ColorList' || chrome.persistence.fcstdResourcePath !== 'ColourList' || chrome.persistence.fcstdResourceBytes !== 12 || !isDeepStrictEqual(chrome.persistence.loadedValue, browserTarget) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-colorlist-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -24,7 +24,9 @@ const vectorEquals = (left, right) => !!left && (['x', 'y', 'z'].every((axis) =>
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || report.promotion.facadeValueModel !== 'three-finite-vector-components-with-native-propertyvector-fcstd-payload' || report.promotion.writableRecords !== 2 || report.promotion.fcstdElement !== 'PropertyVector' || !vectorEquals(report.promotion.nativeRoundTripValue, target) || !vectorEquals(report.promotion.browserRoundTripValue, target) || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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 || !vectorEquals(roundTrip.classification.resavedValue, target) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'PropertyVector' || chrome.persistence.fcstdValue !== JSON.stringify(target) || !vectorEquals(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-direction-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -24,7 +24,9 @@ const browserTarget = 'project-files/chrome-updated.obj'
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 16 || propertyType.objectTypeCount !== 12 || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'safe-project-relative-path-string-with-native-string-fcstd-payload' || report.promotion.writableRecords !== 16 || report.promotion.objectTypeCount !== 12 || report.promotion.fcstdElement !== 'String' || report.promotion.nativeRoundTripValue !== nativeTarget || report.promotion.browserRoundTripValue !== browserTarget || report.promotion.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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.resavedValue !== nativeTarget || roundTrip.classification.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'String' || chrome.persistence.fcstdValue !== browserTarget || chrome.persistence.loadedValue !== browserTarget || chrome.resource?.released !== true || chrome.resource.markerRemoved !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-file-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -23,7 +23,9 @@ const nativeTarget = '09f4e4853de4277e25871d26f2e953c6ea9c13a73c7a01bdfcb5209d17
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 20 || propertyType.objectTypeCount !== 15 || propertyType.statusNames?.join(',') !== 'PropOutput,ReadOnly' || report.promotion.facadeValueModel !== 'project-resource-identity-and-authoritative-bytes-with-native-fileincluded-fcstd-resource' || report.promotion.writableRecords !== 11 || report.promotion.readOnlyRecords !== 8 || report.promotion.outputRecords !== 1 || report.promotion.objectTypeCount !== 15 || report.promotion.fcstdElement !== 'FileIncluded' || report.promotion.nativeArchiveName !== 'payload.bin' || report.promotion.nativeRoundTripSha256 !== nativeTarget || report.promotion.browserArchiveName !== 'chrome-resource.bin' || report.promotion.browserRoundTripBytes !== 16 || report.promotion.externalResourceBoundary !== 'embedded FCStd ZIP entry restored to a read-only document transient file' || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 30 || sync.nativeEditableRecords !== 4361 || sync.opaqueTypes !== 50 || sync.opaqueRecords !== 466 || sync.exactPromotionReady !== false || sync.exactBlocker !== '50 runtime property types and 466 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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.resavedSha256 !== nativeTarget || roundTrip.classification.resavedResourceMatches !== true || roundTrip.classification.externalResourceBoundary !== 'embedded FCStd ZIP entry restored to a read-only document transient file' || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'FileIncluded' || chrome.persistence.fcstdResourcePath !== 'chrome-resource.bin' || chrome.persistence.fcstdResourceBytes !== 16 || chrome.persistence.fcstdResourceMatches !== true || chrome.resource?.documentRoundTrip !== true || chrome.resource.independentReleased !== true || chrome.resource.markerRemoved !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-promotion-pass', propertyType: report.propertyType, promotion: report.promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -0,0 +1,18 @@
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-font-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyFont failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-font-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFont') fail('baseline is invalid')
if (report.object?.name !== 'FontProbe' || report.object.typeId !== 'TechDraw::DrawViewAnnotation' || report.property?.name !== 'Font' || report.property.typeId !== 'App::PropertyFont' || report.initial?.value !== 'osifont') fail('failure fixture identity changed')
const failure = report.failure
if (failure?.id !== 'wrong-type' || failure.exception?.type !== 'TypeError' || failure.before?.value !== 'osifont' || failure.after?.value !== 'osifont' || failure.objectsPreserved !== true || !isDeepStrictEqual(failure.beforeObjects, failure.afterObjects)) fail('wrong type did not reject without mutation or pollution')
const disabled = report.disabled
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || !disabled.status?.includes('Immutable') || !disabled.editorMode?.includes('ReadOnly') || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'Font' is read-only" || disabled.before?.value !== 'osifont' || disabled.after?.value !== 'osifont' || !isDeepStrictEqual(disabled.beforeObjects, disabled.afterObjects) || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('disabled property state did not reject and restore cleanly')
const transaction = report.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-font-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.before?.value !== 'osifont' || transaction.edited?.value !== 'Transaction Font' || transaction.afterAbort?.value !== 'osifont' || transaction.restored !== true || transaction.objectsRestored !== true) fail('transaction abort did not restore PropertyFont')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit')
if (report.documentIntegrity?.objectsPreserved !== true || report.documentIntegrity.objectCount !== 1 || !isDeepStrictEqual(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
console.log(JSON.stringify({ status: 'freecad-property-font-failure-pass', failure: { type: failure.exception.type, valuePreserved: failure.after.value }, disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,36 @@
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 reportPath = 'config/freecad-property-font-inventory.json'
const [runtimeContent, reportContent] = await Promise.all([readFile(resolve(root, runtimePath)), readFile(resolve(root, reportPath))])
const runtime = JSON.parse(runtimeContent)
const report = JSON.parse(reportContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyFont inventory check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyFont' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid')
if (report.recordCount !== 2 || report.objectTypeCount !== 2 || report.records?.length !== 2) fail('runtime record or host count changed')
if (report.storage?.inheritedFrom !== 'App::PropertyString' || report.storage.xmlElement !== 'String' || report.storage.scalarEncoding !== 'UTF-8 font family name' || report.storage.fontResourceEmbedded !== false || report.storage.browserBoundary !== 'preserve the family name; browser font availability is presentation-only') fail('storage boundary is invalid')
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')
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::PropertyFont') nativeMatches.push({ objectTypeId: objectType.typeId, objectAvailable: objectType.available === true, probeStatus: objectType.probeStatus, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
}
const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, status, 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 expectedKeys = ['TechDraw::DrawViewAnnotation.Font', 'TechDraw::DrawViewSpreadsheet.Font']
if (!isDeepStrictEqual(report.records.map(({ objectTypeId, propertyName }) => `${objectTypeId}.${propertyName}`), expectedKeys)) fail('native host/property identities changed')
for (const record of report.records) {
if (record.objectAvailable !== true || record.probeStatus !== 'available' || record.defaultRaw !== 'osifont' || !isDeepStrictEqual(record.status, []) || record.valueModel?.kind !== 'font-family-string' || record.valueModel.representation !== 'App::PropertyString-derived UTF-8 font family name' || record.valueModel.writable !== true || record.valueModel.installedFontRequired !== false || record.inputs?.accepted?.join('|') !== 'utf8-string' || record.inputs.emptyStringAccepted !== true || record.editor !== 'Gui::PropertyEditor::PropertyFontItem' || record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== record.objectTypeId) fail(`native contract changed for ${record.objectTypeId}.${record.propertyName}`)
}
const sources = Object.fromEntries(await Promise.all(report.provenance.sources.map(async ({ path }) => [path, await readFile(resolve(root, path), 'utf8')])))
if (!sources['.cache/freecad/FreeCAD/src/App/PropertyStandard.h'].includes('class AppExport PropertyFont: public PropertyString') || !sources['.cache/freecad/FreeCAD/src/App/PropertyStandard.h'].includes('Gui::PropertyEditor::PropertyFontItem') || !sources['.cache/freecad/FreeCAD/src/App/PropertyStandard.cpp'].includes('TYPESYSTEM_SOURCE(App::PropertyFont, App::PropertyString)') || !sources['.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp'].includes('QFontDatabase().families(QFontDatabase::Any)') || !sources['.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp'].includes('auto cb = new QComboBox(parent)') || !sources['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewAnnotation.cpp'].includes('ADD_PROPERTY_TYPE(Font') || !sources['.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSpreadsheet.cpp'].includes('ADD_PROPERTY_TYPE(Font')) fail('locked PropertyFont class/editor/host contract changed')
console.log(JSON.stringify({ status: 'freecad-property-font-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId), defaultValue: 'osifont', classification: report.classification }, null, 2))

View File

@@ -0,0 +1,16 @@
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-font-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyFont mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-font-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFont' || report.object?.name !== 'FontProbe' || report.object.typeId !== 'TechDraw::DrawViewAnnotation' || report.property?.name !== 'Font' || report.property.typeId !== 'App::PropertyFont') fail('baseline or identity is invalid')
const expected = { before: 'osifont', edited: 'Liberation Sans', restored: 'osifont' }
if (!isDeepStrictEqual(report.requested, expected) || !report.phases?.before || !report.phases.edited || !report.phases.restored) fail('mutation phases are incomplete')
for (const [id, value] of Object.entries(expected)) {
const phase = report.phases[id]
if (phase.value !== value || phase.propertyTypeId !== 'App::PropertyFont' || !isDeepStrictEqual(phase.propertyStatus, []) || !isDeepStrictEqual(phase.editorMode, []) || !Array.isArray(phase.objectState) || typeof phase.statusString !== 'string' || typeof phase.recomputeResult !== 'boolean' || phase.hasShapeProperty !== false || !Array.isArray(phase.objectSet)) fail(`${id} phase metadata is invalid`)
}
if (report.classification?.valueChanged !== true || report.classification.restoreExact !== true || report.classification.objectSetStable !== true || report.classification.geometryApplicability !== 'not-applicable-no-shape-property' || report.classification.unknownSemanticDrift !== false) fail('mutation classification is not exact')
console.log(JSON.stringify({ status: 'freecad-property-font-mutation-pass', before: report.phases.before.value, edited: report.phases.edited.value, restored: report.phases.restored.value, restoreExact: report.classification.restoreExact, unknownSemanticDrift: report.classification.unknownSemanticDrift }, null, 2))

View File

@@ -0,0 +1,31 @@
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-font-promotion.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
load('config/freecad-property-font-roundtrip.json'),
load('config/chrome-property-font-verification.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyFont promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyfont-I' || report.propertyType !== 'App::PropertyFont' || report.recordCount !== 2 || 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-propertyfont-${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 = 'Noto Sans CJK SC'
const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType)
if (report.promotion?.from !== 'opaque-fcstd-proxy' || report.promotion.to !== 'native-editable-codec' || propertyType?.support !== report.promotion.to || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0 || report.promotion.facadeValueModel !== 'bounded-utf8-font-family-string-with-native-string-fcstd-payload' || report.promotion.writableRecords !== 2 || report.promotion.objectTypeCount !== 2 || report.promotion.fcstdElement !== 'String' || report.promotion.defaultValue !== 'osifont' || report.promotion.nativeRoundTripValue !== target || report.promotion.browserRoundTripValue !== target || report.promotion.fontResourceBoundary !== 'font family name persists in FCStd; font files and availability remain presentation-only' || report.promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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.resavedValue !== target || roundTrip.classification.fontResourceBoundary !== 'font family name persists in FCStd; font files and availability remain presentation-only' || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'String' || chrome.persistence.fcstdValue !== target || chrome.persistence.loadedValue !== target || chrome.resource?.released !== true || chrome.resource.markerRemoved !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-font-promotion-pass', propertyType: report.propertyType, promotion: report.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-font-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyFont roundtrip check failed: ${message}`) }
const initial = 'osifont'
const target = 'Noto Sans CJK SC'
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-font-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFont') fail('baseline is invalid')
const objectSet = [{ name: 'FontProbe', typeId: 'TechDraw::DrawViewAnnotation' }]
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
if (snapshot?.object?.name !== 'FontProbe' || snapshot.object.typeId !== 'TechDraw::DrawViewAnnotation' || snapshot.object.propertyTypeId !== 'App::PropertyFont' || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || !isDeepStrictEqual(snapshot.object.editorMode, []) || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || snapshot.object.statusString !== 'Valid' || snapshot.object.hasShapeProperty !== false || !isDeepStrictEqual(snapshot.objectSet, objectSet)) fail(`${phase} native object semantics are incomplete`)
}
if (report.nativeInitial.object.value !== initial || report.nativeAfter.reopened.object.value !== target || report.nativeAfter.resaved.object.value !== target) fail('native PropertyFont values drifted')
if (report.web.before?.typeId !== 'App::PropertyFont' || report.web.before.element !== 'String' || report.web.before.value !== initial || report.web.after?.typeId !== 'App::PropertyFont' || report.web.after.element !== 'String' || report.web.after.value !== target || report.web.targetMarkedTouched !== true || report.web.webOutputMatches !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 0) fail('Web edit did not preserve native archive semantics')
if (report.classification?.requestedValue !== target || report.classification.webValue !== target || report.classification.reopenedValue !== target || report.classification.resavedValue !== target || report.classification.nativeOutputMatches !== true || report.classification.objectSetPreserved !== true || report.classification.nativeStateStable !== true || report.classification.fontResourceBoundary !== 'font family name persists in FCStd; font files and availability remain presentation-only' || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('round-trip classification is not exact for PropertyFont')
for (const artifact of Object.values(report.archives ?? {})) if (!artifact?.path || !Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || !/^[0-9a-f]{64}$/.test(artifact.sha256)) fail('archive evidence is incomplete')
console.log(JSON.stringify({ status: 'freecad-property-font-roundtrip-pass', values: { nativeInitial: report.nativeInitial.object.value, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, opaquePathsPreserved: report.web.opaquePathsPreserved, fontResourceBoundary: report.classification.fontResourceBoundary, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))

View File

@@ -0,0 +1,17 @@
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-font-success.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyFont success check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-font-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyFont') fail('baseline is invalid')
if (report.object?.name !== 'FontProbe' || report.object.typeId !== 'TechDraw::DrawViewAnnotation' || report.property?.name !== 'Font' || report.property.typeId !== 'App::PropertyFont' || report.property.group !== 'Annotation') fail('native object/property identity changed')
const expected = { default: 'osifont', 'named-family': 'DejaVu Sans', 'unicode-family': 'Noto Sans CJK SC', 'empty-family': '', restored: 'osifont' }
if (report.diagnostics?.exception !== null || report.diagnostics.documentObjectCount !== 1 || !isDeepStrictEqual(Object.keys(report.phases ?? {}).sort(), Object.keys(expected).sort())) fail('native success phases are incomplete')
const objectSet = [{ name: 'FontProbe', typeId: 'TechDraw::DrawViewAnnotation' }]
for (const [id, value] of Object.entries(expected)) {
const phase = report.phases[id]
if (phase?.requested !== value || phase.value !== value || phase.propertyTypeId !== 'App::PropertyFont' || !isDeepStrictEqual(phase.propertyStatus, []) || !isDeepStrictEqual(phase.editorMode, []) || !isDeepStrictEqual(phase.objectSet, objectSet) || typeof phase.recomputeResult !== 'boolean' || phase.hasShapeProperty !== false || !Array.isArray(phase.objectState) || typeof phase.statusString !== 'string') fail(`${id} native success semantics changed`)
}
console.log(JSON.stringify({ status: 'freecad-property-font-success-pass', propertyType: report.propertyType, phases: Object.keys(report.phases), values: Object.fromEntries(Object.entries(report.phases).map(([id, phase]) => [id, phase.value])), objectSet }, null, 2))

View File

@@ -0,0 +1,25 @@
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-force-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyForce failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-force-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'ForceProbe' || report.object.typeId !== 'Fem::ConstraintForce' || report.property?.name !== 'Force' || report.property.typeId !== 'App::PropertyForce' || report.initial?.numeric !== 0 || report.failures?.length !== 2) fail('failure fixture identity or cases changed')
const expectedFailures = new Map([
['wrong-dimension', { requested: '5 mm', type: 'ArithmeticError', message: 'Not matching Unit!' }],
['malformed-text', { requested: 'not-a-quantity', type: 'ParserError', message: 'syntax error' }],
])
for (const entry of report.failures) {
const expected = expectedFailures.get(entry.id)
if (!expected || entry.requested !== expected.requested || entry.exception?.type !== expected.type || entry.exception?.message !== expected.message || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || JSON.stringify(entry.before) !== JSON.stringify(entry.after) || JSON.stringify(entry.beforeObjects) !== JSON.stringify(entry.afterObjects)) fail(`${entry.id} did not reject without document pollution`)
}
const disabled = report.disabled
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || disabled.requested !== '750 N' || disabled.status?.includes('Immutable') !== true || disabled.status.includes('ReadOnly') !== true || JSON.stringify(disabled.editorMode) !== '["ReadOnly"]' || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'Force' is read-only" || disabled.valuePreserved !== true || disabled.objectsPreserved !== true || JSON.stringify(disabled.before) !== JSON.stringify(disabled.after) || JSON.stringify(disabled.restoredStatus) !== '[]' || JSON.stringify(disabled.restoredEditorMode) !== '[]') fail('disabled property state did not reject and restore cleanly')
const output = report.outputBoundary
if (output?.classification !== 'prop-output-ui-read-only-python-mutable' || output.property?.name !== 'ForceX' || output.property.typeId !== 'App::PropertyForce' || JSON.stringify(output.status) !== '["27"]' || JSON.stringify(output.editorMode) !== '[]' || output.assignmentException !== null || output.before?.numeric !== 0 || output.after?.numeric !== 125000 || output.restored?.numeric !== 0 || output.restoredExactly !== true) fail('PropOutput boundary changed')
const transaction = report.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-force-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || transaction.before?.numeric !== 0 || transaction.edited?.numeric !== 250000 || transaction.afterAbort?.numeric !== 0 || JSON.stringify(transaction.objectsBefore) !== JSON.stringify(transaction.objectsAfter)) fail('transaction abort did not restore the property')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit')
if (report.documentIntegrity?.objectsPreserved !== true || report.documentIntegrity.objectCount !== 2 || JSON.stringify(report.documentIntegrity.initialObjects) !== JSON.stringify(report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
console.log(JSON.stringify({ status: 'freecad-property-force-failure-pass', failures: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, outputBoundary: output.classification, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,34 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const reportPath = resolve(root, 'config/freecad-property-force-inventory.json')
const fail = (message) => { throw new Error(`FreeCAD PropertyForce inventory check failed: ${message}`) }
const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)])
const source = JSON.parse(sourceContent)
const report = JSON.parse(reportContent)
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyForce' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== createHash('sha256').update(sourceContent).digest('hex')) fail('source provenance is stale')
if (report.recordCount !== 4 || report.writableRecordCount !== 1 || report.outputRecordCount !== 3 || report.records?.length !== 4) fail('record cardinality is invalid')
const expected = [
['Fem::ConstraintForce', 'Force', '', '[]', true],
['Fem::ConstraintRigidBody', 'ForceX', 'ConstraintRigidBody', '[27]', false],
['Fem::ConstraintRigidBody', 'ForceY', 'ConstraintRigidBody', '[27]', false],
['Fem::ConstraintRigidBody', 'ForceZ', 'ConstraintRigidBody', '[27]', false],
]
for (const [objectTypeId, propertyName, group, status, writable] of expected) {
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName)
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== group || JSON.stringify(record.status) !== status || record.defaultRaw !== '0.0 mm*kg/s^2') fail(`${objectTypeId}.${propertyName} inventory changed`)
const model = record.valueModel
if (model?.kind !== 'quantity' || model.dimension !== 'mass*length/time^2' || model.internalUnit !== 'mm*kg/s^2' || model.displayUnit !== 'N' || model.newtonScale !== 1000 || model.defaultValue !== 0 || model.writable !== writable || model.derivedOutput !== !writable) fail(`${objectTypeId}.${propertyName} value model is invalid`)
if (record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId) fail(`${objectTypeId}.${propertyName} applicability is incomplete`)
}
const nativeMatches = []
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyForce') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
const reportMatches = report.records.map(({ objectTypeId, propertyName, group, status, defaultRaw }) => ({ objectTypeId, propertyName, group, status, defaultRaw }))
if (JSON.stringify(nativeMatches) !== JSON.stringify(reportMatches)) fail('report does not match the locked runtime oracle')
console.log(JSON.stringify({ status: 'freecad-property-force-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, outputRecordCount: report.outputRecordCount, objectTypes: [...new Set(report.records.map(({ objectTypeId }) => objectTypeId))], classification: report.classification }, null, 2))

View File

@@ -0,0 +1,20 @@
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-force-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyForce mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-force-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'ForceProbe' || report.object.typeId !== 'Fem::ConstraintForce' || report.property?.name !== 'Force' || report.property.typeId !== 'App::PropertyForce') fail('native object/property identity changed')
if (!(report.recompute?.initial > 0) || !(report.recompute.edit > 0) || !(report.recompute.restore > 0)) fail('initial, edit and restore were not recomputed')
for (const [phase, expected] of [['before', 0], ['edited', 500000], ['restored', 0]]) {
const snapshot = report[phase]
if (snapshot?.value?.numeric !== expected || typeof snapshot.value.unit !== 'string' || !snapshot.value.unit.includes('mm*kg/s^2') || JSON.stringify(snapshot.propertyStatus) !== '[]' || JSON.stringify(snapshot.editorMode) !== '[]' || snapshot.shape?.applicable !== false || !Array.isArray(snapshot.objectState)) fail(`${phase} native semantic state is invalid`)
if (snapshot.reversed !== false || snapshot.enableAmplitude !== false || JSON.stringify(snapshot.amplitudeValues) !== '["0, 0","1, 1"]' || JSON.stringify(snapshot.directionVector) !== '[0,0,1]' || !snapshot.propertyNames?.includes('Force')) fail(`${phase} neighboring FEM properties changed`)
}
if (report.touchedAfterEdit?.value?.numeric !== 500000 || JSON.stringify(report.touchedAfterEdit.objectState) !== '["Touched"]' || report.touchedAfterEdit.mustExecute !== false) fail('edit did not preserve native touched state')
if (report.touchedAfterRestore?.value?.numeric !== 0 || JSON.stringify(report.touchedAfterRestore.objectState) !== '["Touched"]' || report.touchedAfterRestore.mustExecute !== false) fail('restore did not preserve native touched state')
if (report.neighbors?.preserved !== true || JSON.stringify(report.neighbors.before) !== JSON.stringify(report.neighbors.edited) || JSON.stringify(report.neighbors.before) !== JSON.stringify(report.neighbors.restored)) fail('neighbor properties drifted')
if (report.document?.objectsPreserved !== true || JSON.stringify(report.document.objectsBefore) !== JSON.stringify(report.document.objectsRestored) || report.document.objectsBefore?.length !== 1) fail('mutation polluted the document object set')
if (report.classification?.propertyChanged !== true || report.classification.propertyRestored !== true || report.classification.semanticStateRestored !== true || report.classification.neighborPropertiesPreserved !== true || report.classification.geometry !== 'not-applicable-no-shape-property') fail('mutation/restore classification is incomplete')
console.log(JSON.stringify({ status: 'freecad-property-force-mutation-pass', recompute: report.recompute, values: { before: report.before.value.numeric, edited: report.edited.value.numeric, restored: report.restored.value.numeric }, neighborsPreserved: report.neighbors.preserved, semanticStateRestored: report.classification.semanticStateRestored, geometry: report.classification.geometry }, null, 2))

View File

@@ -0,0 +1,31 @@
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-force-promotion.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
load('config/freecad-property-force-roundtrip.json'),
load('config/chrome-property-force-verification.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyForce promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyforce-I' || report.propertyType !== 'App::PropertyForce' || 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-propertyforce-${phase}`
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || JSON.stringify(report.phaseEvidence[phase]) !== JSON.stringify(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 !== 4 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.join(',') !== 'PropOutput') fail('capability identity is incomplete')
if (promotion.facadeValueModel !== 'finite-number-internal-mm-kg-per-s2-with-registered-force-units' || promotion.internalUnit !== 'mm*kg/s^2' || promotion.displayUnits?.join(',') !== 'N,kN,lbf' || promotion.newtonScale !== 1000 || promotion.writableRecords !== 1 || promotion.outputRecords !== 3 || promotion.fcstdElement !== 'Float' || promotion.nativeRoundTripValue !== 500000 || promotion.browserRoundTripValue !== 500000 || promotion.outputPropertiesPreserved !== true || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (JSON.stringify(sync) !== JSON.stringify({ 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.outputPropertiesPreserved !== true || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'Float' || chrome.persistence.loadedValue !== 500000 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-force-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -0,0 +1,23 @@
import { createHash } from 'node:crypto'
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-force-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyForce roundtrip check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-force-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
if (JSON.stringify(snapshot?.objectSet) !== JSON.stringify([{ name: 'ForceProbe', typeId: 'Fem::ConstraintForce' }, { name: 'RigidForceProbe', typeId: 'Fem::ConstraintRigidBody' }])) fail(`${phase} object set changed`)
if (snapshot.force?.typeId !== 'Fem::ConstraintForce' || snapshot.force.property?.typeId !== 'App::PropertyForce' || snapshot.force.property.status?.length !== 0 || snapshot.force.shape?.applicable !== false) fail(`${phase} writable Force semantics are incomplete`)
if (snapshot.rigid?.typeId !== 'Fem::ConstraintRigidBody' || snapshot.rigid.outputs?.map(({ name }) => name).join(',') !== 'ForceX,ForceY,ForceZ' || snapshot.rigid.outputs.some((output) => output.typeId !== 'App::PropertyForce' || JSON.stringify(output.status) !== '["27"]' || output.value?.numeric !== 0) || snapshot.rigid.shape?.applicable !== false) fail(`${phase} output Force semantics are incomplete`)
}
if (report.nativeInitial.force.property.value.numeric !== 0 || report.nativeAfter.reopened.force.property.value.numeric !== 500000 || report.nativeAfter.resaved.force.property.value.numeric !== 500000) fail('native Force values changed')
if (report.web.before?.typeId !== 'App::PropertyForce' || report.web.before.element !== 'Float' || Number(report.web.before.value) !== 0 || report.web.after?.typeId !== 'App::PropertyForce' || report.web.after.element !== 'Float' || Number(report.web.after.value) !== 500000) fail('Web did not decode and encode the Force Float payload')
if (report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved)) fail('Web edit did not preserve native archive semantics')
if (report.classification?.webValue !== 500000 || report.classification.reopenedValue !== 500000 || report.classification.resavedValue !== 500000 || report.classification.outputPropertiesPreserved !== true || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('roundtrip classification 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-force-roundtrip-pass', values: { nativeInitial: report.nativeInitial.force.property.value.numeric, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, outputPropertiesPreserved: report.classification.outputPropertiesPreserved, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))

View File

@@ -0,0 +1,28 @@
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-force-success.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyForce success check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-force-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'ForceProbe' || report.object.typeId !== 'Fem::ConstraintForce' || report.property?.name !== 'Force' || report.property.typeId !== 'App::PropertyForce' || report.property.group !== '') fail('native object/property identity changed')
if (report.diagnostics?.exception !== null || report.diagnostics.documentObjectCount !== 2 || report.cases?.length !== 6 || report.outputProperties?.length !== 3) fail('native success cases are incomplete')
const expectedValues = new Map([
['default', 0],
['nominal-newton', 500000],
['zero-boundary', 0],
['negative-boundary', -5000],
['kilonewton', 2000000],
['restored', 0],
])
const byId = new Map(report.cases.map((entry) => [entry.id, entry]))
for (const [id, expected] of expectedValues) {
const entry = byId.get(id)
if (!entry || entry.objectTypeId !== 'Fem::ConstraintForce' || entry.propertyName !== 'Force' || entry.typeId !== 'App::PropertyForce' || entry.group !== '' || JSON.stringify(entry.status) !== '[]' || JSON.stringify(entry.editorMode) !== '[]') fail(`${id} native property metadata changed`)
if (entry.value?.numeric !== expected || typeof entry.value.unit !== 'string' || !entry.value.unit.includes('mm*kg/s^2') || entry.shape?.applicable !== false || !Array.isArray(entry.objectState)) fail(`${id} force value or object state is invalid`)
}
const expectedOutputNames = ['ForceX', 'ForceY', 'ForceZ']
for (const [index, output] of report.outputProperties.entries()) {
if (output.objectName !== 'RigidForceProbe' || output.objectTypeId !== 'Fem::ConstraintRigidBody' || output.propertyName !== expectedOutputNames[index] || output.typeId !== 'App::PropertyForce' || output.group !== 'ConstraintRigidBody' || JSON.stringify(output.status) !== '["27"]' || output.value?.numeric !== 0 || !String(output.value.unit).includes('mm*kg/s^2') || output.shape?.applicable !== false) fail(`${expectedOutputNames[index]} output boundary changed`)
}
console.log(JSON.stringify({ status: 'freecad-property-force-success-pass', cases: Object.fromEntries([...expectedValues].map(([id]) => [id, byId.get(id).value.numeric])), internalUnit: 'mm*kg/s^2', newtonScale: 1000, outputProperties: expectedOutputNames }, null, 2))

View File

@@ -0,0 +1,23 @@
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-heatflux-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-heatflux-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'HeatFluxProbe' || report.object.typeId !== 'Fem::ConstraintHeatflux' || report.property?.name !== 'DFlux' || report.property.typeId !== 'App::PropertyHeatFlux' || report.initial?.numeric !== 0 || report.failures?.length !== 2) fail('failure fixture identity or cases changed')
const expectedFailures = new Map([
['wrong-dimension', { requested: '5 mm', type: 'ArithmeticError', message: 'Not matching Unit!' }],
['malformed-text', { requested: 'not-a-quantity', type: 'ParserError', message: 'syntax error' }],
])
for (const entry of report.failures) {
const expected = expectedFailures.get(entry.id)
if (!expected || entry.requested !== expected.requested || entry.exception?.type !== expected.type || entry.exception?.message !== expected.message || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || JSON.stringify(entry.before) !== JSON.stringify(entry.after) || JSON.stringify(entry.beforeObjects) !== JSON.stringify(entry.afterObjects)) fail(`${entry.id} did not reject without document pollution`)
}
const disabled = report.disabled
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || disabled.requested !== '750 W/m^2' || disabled.status?.includes('Immutable') !== true || disabled.status.includes('ReadOnly') !== true || JSON.stringify(disabled.editorMode) !== '["ReadOnly"]' || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'DFlux' is read-only" || disabled.valuePreserved !== true || disabled.objectsPreserved !== true || JSON.stringify(disabled.before) !== JSON.stringify(disabled.after) || JSON.stringify(disabled.restoredStatus) !== '[]' || JSON.stringify(disabled.restoredEditorMode) !== '[]') fail('disabled property state did not reject and restore cleanly')
const transaction = report.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-heatflux-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || transaction.before?.numeric !== 0 || transaction.edited?.numeric !== 250 || transaction.afterAbort?.numeric !== 0 || JSON.stringify(transaction.objectsBefore) !== JSON.stringify(transaction.objectsAfter)) fail('transaction abort did not restore the property')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit')
if (report.documentIntegrity?.objectsPreserved !== true || report.documentIntegrity.objectCount !== 1 || JSON.stringify(report.documentIntegrity.initialObjects) !== JSON.stringify(report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document')
console.log(JSON.stringify({ status: 'freecad-property-heatflux-failure-pass', failures: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: { status: disabled.status, editorMode: disabled.editorMode, exception: disabled.exception }, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,26 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const reportPath = resolve(root, 'config/freecad-property-heatflux-inventory.json')
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux inventory check failed: ${message}`) }
const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)])
const source = JSON.parse(sourceContent)
const report = JSON.parse(reportContent)
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyHeatFlux' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== createHash('sha256').update(sourceContent).digest('hex')) fail('source provenance is stale')
if (report.recordCount !== 1 || report.writableRecordCount !== 1 || report.outputRecordCount !== 0 || report.records?.length !== 1) fail('record cardinality is invalid')
const record = report.records[0]
if (record.objectTypeId !== 'Fem::ConstraintHeatflux' || record.objectAvailable !== true || record.probeStatus !== 'available' || record.propertyName !== 'DFlux' || record.group !== 'ConstraintHeatflux' || JSON.stringify(record.status) !== '[]' || record.defaultRaw !== '0.0 kg/s^3') fail('Fem::ConstraintHeatflux.DFlux inventory changed')
const model = record.valueModel
if (model?.kind !== 'quantity' || model.dimension !== 'mass/time^3' || model.internalUnit !== 'kg/s^3' || model.displayUnit !== 'W/m^2' || model.wattsPerSquareMeterScale !== 1 || model.defaultValue !== 0 || model.writable !== true || model.derivedOutput !== false) fail('DFlux value model is invalid')
if (record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== 'Fem::ConstraintHeatflux') fail('DFlux applicability is incomplete')
const nativeMatches = []
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyHeatFlux') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
const reportMatches = report.records.map(({ objectTypeId, propertyName, group, status, defaultRaw }) => ({ objectTypeId, propertyName, group, status, defaultRaw }))
if (JSON.stringify(nativeMatches) !== JSON.stringify(reportMatches)) fail('report does not match the locked runtime oracle')
console.log(JSON.stringify({ status: 'freecad-property-heatflux-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, outputRecordCount: report.outputRecordCount, objectTypes: ['Fem::ConstraintHeatflux'], classification: report.classification }, null, 2))

View File

@@ -0,0 +1,20 @@
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-heatflux-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-heatflux-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'HeatFluxProbe' || report.object.typeId !== 'Fem::ConstraintHeatflux' || report.property?.name !== 'DFlux' || report.property.typeId !== 'App::PropertyHeatFlux') fail('native object/property identity changed')
if (!(report.recompute?.initial > 0) || !(report.recompute.edit > 0) || !(report.recompute.restore > 0)) fail('initial, edit and restore were not recomputed')
for (const [phase, expected] of [['before', 0], ['edited', 500], ['restored', 0]]) {
const snapshot = report[phase]
if (snapshot?.value?.numeric !== expected || typeof snapshot.value.unit !== 'string' || !snapshot.value.unit.includes('kg/s^3') || JSON.stringify(snapshot.propertyStatus) !== '[]' || JSON.stringify(snapshot.editorMode) !== '[]' || snapshot.shape?.applicable !== false || !Array.isArray(snapshot.objectState)) fail(`${phase} native semantic state is invalid`)
if (snapshot.constraintType !== 'Convection' || snapshot.ambientTemp?.numeric !== 0 || snapshot.filmCoef?.numeric !== 0 || snapshot.emissivity !== 0 || snapshot.enableAmplitude !== false || JSON.stringify(snapshot.amplitudeValues) !== '["0, 0","1, 1"]' || snapshot.scale !== 1 || !snapshot.propertyNames?.includes('DFlux')) fail(`${phase} neighboring FEM properties changed`)
}
if (report.touchedAfterEdit?.value?.numeric !== 500 || JSON.stringify(report.touchedAfterEdit.objectState) !== '["Touched"]' || report.touchedAfterEdit.mustExecute !== false) fail('edit did not preserve native touched state')
if (report.touchedAfterRestore?.value?.numeric !== 0 || JSON.stringify(report.touchedAfterRestore.objectState) !== '["Touched"]' || report.touchedAfterRestore.mustExecute !== false) fail('restore did not preserve native touched state')
if (report.neighbors?.preserved !== true || JSON.stringify(report.neighbors.before) !== JSON.stringify(report.neighbors.edited) || JSON.stringify(report.neighbors.before) !== JSON.stringify(report.neighbors.restored)) fail('neighbor properties drifted')
if (report.document?.objectsPreserved !== true || JSON.stringify(report.document.objectsBefore) !== JSON.stringify(report.document.objectsRestored) || report.document.objectsBefore?.length !== 1) fail('mutation polluted the document object set')
if (report.classification?.propertyChanged !== true || report.classification.propertyRestored !== true || report.classification.semanticStateRestored !== true || report.classification.neighborPropertiesPreserved !== true || report.classification.geometry !== 'not-applicable-no-shape-property') fail('mutation/restore classification is incomplete')
console.log(JSON.stringify({ status: 'freecad-property-heatflux-mutation-pass', recompute: report.recompute, values: { before: report.before.value.numeric, edited: report.edited.value.numeric, restored: report.restored.value.numeric }, neighborsPreserved: report.neighbors.preserved, semanticStateRestored: report.classification.semanticStateRestored, geometry: report.classification.geometry }, null, 2))

View File

@@ -0,0 +1,31 @@
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-heatflux-promotion.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
load('config/freecad-property-heatflux-roundtrip.json'),
load('config/chrome-property-heatflux-verification.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyheatflux-I' || report.propertyType !== 'App::PropertyHeatFlux' || 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-propertyheatflux-${phase}`
if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || JSON.stringify(report.phaseEvidence[phase]) !== JSON.stringify(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) fail('capability identity is incomplete')
if (promotion.facadeValueModel !== 'finite-number-internal-kg-per-s3-with-registered-heat-flux-units' || promotion.internalUnit !== 'kg/s^3' || promotion.displayUnits?.join(',') !== 'W/m^2,kW/m^2,W/mm^2' || promotion.wattsPerSquareMeterScale !== 1 || promotion.writableRecords !== 1 || promotion.outputRecords !== 0 || promotion.fcstdElement !== 'Float' || promotion.nativeRoundTripValue !== 500 || promotion.browserRoundTripValue !== 500 || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale')
if (JSON.stringify(sync) !== JSON.stringify({ 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.nativeValueMatches !== true || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'Float' || chrome.persistence.loadedValue !== 500 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-heatflux-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 { 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-heatflux-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux roundtrip check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-heatflux-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
for (const [phase, snapshot] of [['nativeInitial', report.nativeInitial], ['nativeReopened', report.nativeAfter?.reopened], ['nativeResaved', report.nativeAfter?.resaved]]) {
if (JSON.stringify(snapshot?.objectSet) !== JSON.stringify([{ name: 'HeatFluxProbe', typeId: 'Fem::ConstraintHeatflux' }])) fail(`${phase} object set changed`)
if (snapshot.heatFlux?.typeId !== 'Fem::ConstraintHeatflux' || snapshot.heatFlux.property?.name !== 'DFlux' || snapshot.heatFlux.property.typeId !== 'App::PropertyHeatFlux' || snapshot.heatFlux.property.group !== 'ConstraintHeatflux' || snapshot.heatFlux.property.status?.length !== 0 || snapshot.heatFlux.shape?.applicable !== false) fail(`${phase} writable HeatFlux semantics are incomplete`)
}
if (report.nativeInitial.heatFlux.property.value.numeric !== 0 || report.nativeAfter.reopened.heatFlux.property.value.numeric !== 500 || report.nativeAfter.resaved.heatFlux.property.value.numeric !== 500) fail('native HeatFlux values changed')
if (report.web.before?.typeId !== 'App::PropertyHeatFlux' || report.web.before.element !== 'Float' || Number(report.web.before.value) !== 0 || report.web.after?.typeId !== 'App::PropertyHeatFlux' || report.web.after.element !== 'Float' || Number(report.web.after.value) !== 500) fail('Web did not decode and encode the HeatFlux Float payload')
if (report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved)) fail('Web edit did not preserve native archive semantics')
if (report.classification?.webValue !== 500 || report.classification.reopenedValue !== 500 || report.classification.resavedValue !== 500 || report.classification.nativeValueMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('roundtrip classification 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-heatflux-roundtrip-pass', values: { nativeInitial: report.nativeInitial.heatFlux.property.value.numeric, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2))

View File

@@ -0,0 +1,26 @@
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-heatflux-success.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux success check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-heatflux-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
if (report.object?.name !== 'HeatFluxProbe' || report.object.typeId !== 'Fem::ConstraintHeatflux' || report.property?.name !== 'DFlux' || report.property.typeId !== 'App::PropertyHeatFlux' || report.property.group !== 'ConstraintHeatflux') fail('native object/property identity changed')
if (report.diagnostics?.exception !== null || report.diagnostics.documentObjectCount !== 1 || report.cases?.length !== 7) fail('native success cases are incomplete')
const expectedValues = new Map([
['default', 0],
['nominal-watt-per-square-meter', 500],
['zero-boundary', 0],
['negative-boundary', -5],
['kilowatt-per-square-meter', 2000],
['watt-per-square-millimeter', 1000000],
['restored', 0],
])
const byId = new Map(report.cases.map((entry) => [entry.id, entry]))
for (const [id, expected] of expectedValues) {
const entry = byId.get(id)
if (!entry || entry.objectTypeId !== 'Fem::ConstraintHeatflux' || entry.propertyName !== 'DFlux' || entry.typeId !== 'App::PropertyHeatFlux' || entry.group !== 'ConstraintHeatflux' || JSON.stringify(entry.status) !== '[]' || JSON.stringify(entry.editorMode) !== '[]') fail(`${id} native property metadata changed`)
if (entry.value?.numeric !== expected || typeof entry.value.unit !== 'string' || !entry.value.unit.includes('kg/s^3') || entry.shape?.applicable !== false || !Array.isArray(entry.objectState)) fail(`${id} heat flux value or object state is invalid`)
}
console.log(JSON.stringify({ status: 'freecad-property-heatflux-success-pass', cases: Object.fromEntries([...expectedValues].map(([id]) => [id, byId.get(id).value.numeric])), internalUnit: 'kg/s^3', wattsPerSquareMeterScale: 1 }, null, 2))

View File

@@ -0,0 +1,28 @@
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-integerset-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-integerset-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyIntegerSet') fail('baseline is invalid')
if (report.object?.name !== 'NodesProbe' || report.object.typeId !== 'Fem::FemSetNodesObject' || report.property?.name !== 'Nodes' || report.property.typeId !== 'App::PropertyIntegerSet' || !isDeepStrictEqual(report.initial, [1, 3])) fail('fixture identity changed')
const expected = new Map([
['float-item', { requested: '[1, 1.5]', message: 'type in list must be int, not float' }],
['string-item', { requested: "[1, '3']", message: 'type in list must be int, not str' }],
['set-object', { requested: '{1, 3}', message: 'type must be int or list of int, not set' }],
['none', { requested: 'None', message: 'type must be int or list of int, not NoneType' }],
])
if (report.failures?.length !== expected.size) fail('failure case count changed')
for (const entry of report.failures) {
const contract = expected.get(entry.id)
if (!contract || entry.requested !== contract.requested || entry.exception?.type !== 'TypeError' || entry.exception.message !== contract.message || entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.polluted !== false || !isDeepStrictEqual(entry.before, [1, 3]) || !isDeepStrictEqual(entry.after, [1, 3]) || !isDeepStrictEqual(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} did not reject atomically`)
}
const disabled = report.disabled
if (disabled?.classification !== 'editor-read-only-and-python-immutable' || !isDeepStrictEqual(disabled.requested, [2, 4]) || !isDeepStrictEqual(disabled.status, ['Immutable', 'ReadOnly']) || !isDeepStrictEqual(disabled.editorMode, ['ReadOnly']) || disabled.exception?.type !== 'AttributeError' || disabled.exception.message !== "Object attribute 'Nodes' is read-only" || disabled.valuePreserved !== true || disabled.objectsPreserved !== true || !isDeepStrictEqual(disabled.before, [1, 3]) || !isDeepStrictEqual(disabled.after, [1, 3]) || !isDeepStrictEqual(disabled.restoredStatus, []) || !isDeepStrictEqual(disabled.restoredEditorMode, [])) fail('disabled state changed')
const transaction = report.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-integerset-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || transaction.restored !== true || transaction.objectsRestored !== true || !isDeepStrictEqual(transaction.before, [1, 3]) || !isDeepStrictEqual(transaction.edited, [2, 4]) || !isDeepStrictEqual(transaction.afterAbort, [1, 3]) || !isDeepStrictEqual(transaction.objectsBefore, transaction.objectsAfter)) fail('transaction abort changed')
if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is incomplete')
if (report.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('document was polluted')
console.log(JSON.stringify({ status: 'freecad-property-integerset-failure-pass', failures: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: { status: disabled.status, exception: disabled.exception }, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,31 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const reportPath = resolve(root, 'config/freecad-property-integerset-inventory.json')
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet inventory check failed: ${message}`) }
const [sourceContent, reportContent] = await Promise.all([readFile(sourcePath), readFile(reportPath)])
const source = JSON.parse(sourceContent)
const report = JSON.parse(reportContent)
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyIntegerSet' || report.classification !== 'opaque-fcstd-proxy') fail('report boundary is invalid')
if (report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is not locked to FreeCAD 1.1.1')
if (report.source?.path !== '.cache/freecad/reference-desktop.json' || report.source.bytes !== sourceContent.length || report.source.sha256 !== createHash('sha256').update(sourceContent).digest('hex')) fail('source provenance is stale')
if (report.recordCount !== 2 || report.writableRecordCount !== 2 || report.records?.length !== 2) fail('record cardinality is invalid')
const expected = [
['Fem::FemSetElementNodesObject', 'Elements', 'Element indexes'],
['Fem::FemSetNodesObject', 'Nodes', 'Node indexes'],
]
for (const [objectTypeId, propertyName, group] of expected) {
const record = report.records.find((candidate) => candidate.objectTypeId === objectTypeId && candidate.propertyName === propertyName)
if (!record || record.objectAvailable !== true || record.probeStatus !== 'available' || record.group !== group || JSON.stringify(record.status) !== '[]' || record.defaultRaw !== 'set()') fail(`${objectTypeId}.${propertyName} inventory changed`)
const model = record.valueModel
if (model?.kind !== 'integer-set' || model.elementType !== 'signed-integer' || model.cardinality !== 'variable-unique-unordered' || model.webRepresentation !== 'sorted-safe-integer-array' || model.writable !== true) fail(`${objectTypeId}.${propertyName} value model is invalid`)
if (record.dependencies?.length !== 0 || record.applicability?.requiredObjectTypeId !== objectTypeId) fail(`${objectTypeId}.${propertyName} applicability is incomplete`)
}
const nativeMatches = []
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyIntegerSet') nativeMatches.push({ objectTypeId: objectType.typeId, propertyName: property.name, group: property.group, status: property.status, defaultRaw: property.default })
const reportMatches = report.records.map(({ objectTypeId, propertyName, group, status, defaultRaw }) => ({ objectTypeId, propertyName, group, status, defaultRaw }))
if (JSON.stringify(nativeMatches.sort((a, b) => `${a.objectTypeId}.${a.propertyName}`.localeCompare(`${b.objectTypeId}.${b.propertyName}`))) !== JSON.stringify(reportMatches)) fail('report does not match the locked runtime oracle')
console.log(JSON.stringify({ status: 'freecad-property-integerset-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, objectTypes: expected.map(([typeId]) => typeId), classification: report.classification }, null, 2))

View File

@@ -0,0 +1,22 @@
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-integerset-mutation.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet mutation check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-integerset-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyIntegerSet') fail('baseline is invalid')
if (report.caseCount !== 2 || report.cases?.length !== 2) fail('case inventory changed')
const expected = new Map([
['Fem::FemSetNodesObject', { property: 'Nodes', group: 'Node indexes', before: [1, 3], edited: [2, 5] }],
['Fem::FemSetElementNodesObject', { property: 'Elements', group: 'Element indexes', before: [4, 10], edited: [-1, 8] }],
])
for (const entry of report.cases) {
const contract = expected.get(entry.objectTypeId)
if (!contract || entry.propertyName !== contract.property || !isDeepStrictEqual(entry.before?.value, contract.before) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, contract.edited) || !isDeepStrictEqual(entry.edited?.value, contract.edited) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, contract.before) || !isDeepStrictEqual(entry.restored?.value, contract.before)) fail(`${entry.objectTypeId} values changed`)
if (!entry.recompute?.initial || !entry.recompute.edit || !entry.recompute.restore || entry.neighborsPreserved !== true || entry.valueRestored !== true || entry.semanticStateRestored !== true || entry.objectsRestored !== true) fail(`${entry.objectTypeId} did not restore exactly`)
for (const snapshot of [entry.before, entry.touchedAfterEdit, entry.edited, entry.touchedAfterRestore, entry.restored]) if (snapshot.propertyTypeId !== 'App::PropertyIntegerSet' || snapshot.group !== contract.group || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.femMesh !== null || !snapshot.propertyNames?.includes(contract.property) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 1 || !Array.isArray(snapshot.objectState)) fail(`${entry.objectTypeId} metadata changed`)
if (!isDeepStrictEqual(entry.touchedAfterEdit.objectState, ['Touched']) || !isDeepStrictEqual(entry.touchedAfterRestore.objectState, ['Touched'])) fail(`${entry.objectTypeId} touched trajectory changed`)
}
console.log(JSON.stringify({ status: 'freecad-property-integerset-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,21 @@
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-integerset-promotion.json'), load('config/freecad-native-property-semantics.json'), load('config/freecad-follow-up-task-progress.json'), load('config/freecad-property-integerset-roundtrip.json'), load('config/chrome-property-integerset-verification.json')])
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet promotion check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertyintegerset-I' || report.propertyType !== 'App::PropertyIntegerSet' || report.recordCount !== 2 || 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-propertyintegerset-${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 = [-1, 8]
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 !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0 || promotion.facadeValueModel !== 'sorted-unique-safe-integer-array' || promotion.writableRecords !== 2 || promotion.nativeEmptyBindingBoundary?.neverAssignedEmptyRead !== 'native-process-termination' || promotion.fcstdElement !== 'IntegerSet' || !isDeepStrictEqual(promotion.nativeRoundTripValue, target) || !isDeepStrictEqual(promotion.browserRoundTripValue, target) || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete')
const sync = report.exactBlockerSync
if (sync?.nativeEditableTypes !== 34 || sync.nativeEditableRecords !== 4370 || sync.opaqueTypes !== 46 || sync.opaqueRecords !== 457 || sync.exactPromotionReady !== false || sync.exactBlocker !== '46 runtime property types and 457 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) || chrome.status !== 'pass' || chrome.persistence?.fcstdElement !== 'IntegerSet' || !isDeepStrictEqual(chrome.persistence.loadedValue, target) || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed')
console.log(JSON.stringify({ status: 'freecad-property-integerset-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2))

View File

@@ -0,0 +1,17 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { isDeepStrictEqual } from 'node:util'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-integerset-roundtrip.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet roundtrip check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-integerset-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid')
for (const [phase, snapshot, value] of [['nativeInitial', report.nativeInitial, [1, 3]], ['nativeReopened', report.nativeAfter?.reopened, [-1, 8]], ['nativeResaved', report.nativeAfter?.resaved, [-1, 8]]]) {
if (!isDeepStrictEqual(snapshot?.objectSet, [{ name: 'NodesProbe', typeId: 'Fem::FemSetNodesObject' }]) || snapshot.object?.name !== 'NodesProbe' || snapshot.object.typeId !== 'Fem::FemSetNodesObject' || snapshot.object.propertyName !== 'Nodes' || snapshot.object.propertyTypeId !== 'App::PropertyIntegerSet' || snapshot.object.group !== 'Node indexes' || !isDeepStrictEqual(snapshot.object.value, value) || !isDeepStrictEqual(snapshot.object.propertyStatus, []) || snapshot.object.hasShapeProperty !== false) fail(`${phase} native semantics changed`)
}
if (report.web.before?.typeId !== 'App::PropertyIntegerSet' || report.web.before.element !== 'IntegerSet' || report.web.before.value !== '[1,3]' || report.web.after?.typeId !== 'App::PropertyIntegerSet' || report.web.after.element !== 'IntegerSet' || report.web.after.value !== '[-1,8]') fail('Web IntegerSet codec changed')
if (report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved)) fail('archive preservation changed')
if (report.classification?.webValue !== '[-1,8]' || !isDeepStrictEqual(report.classification.reopenedValue, [-1, 8]) || !isDeepStrictEqual(report.classification.resavedValue, [-1, 8]) || report.classification.nativeOutputMatches !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true) fail('roundtrip classification 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-integerset-roundtrip-pass', values: report.classification, opaquePathsPreserved: report.web.opaquePathsPreserved }, null, 2))

View File

@@ -0,0 +1,24 @@
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-integerset-success.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet success check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-integerset-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyIntegerSet') fail('baseline is invalid')
if (report.caseCount !== 2 || report.cases?.length !== 2 || report.bindingBoundary?.neverAssignedEmptyRead !== 'native-process-termination' || report.bindingBoundary.safeEquivalent !== 'assign-empty-list-before-read' || report.bindingBoundary.oracleDefault !== 'set()') fail('case inventory or native binding boundary changed')
const expected = new Map([
['Fem::FemSetNodesObject', { name: 'Nodes', group: 'Node indexes', deduplicated: [1, 2, 3] }],
['Fem::FemSetElementNodesObject', { name: 'Elements', group: 'Element indexes', deduplicated: [4, 10] }],
])
for (const entry of report.cases) {
const contract = expected.get(entry.objectTypeId)
if (!contract || entry.propertyName !== contract.name || entry.group !== contract.group || entry.mode !== 'direct-native-property-setter') fail(`${entry.objectTypeId} identity changed`)
const values = { explicitEmpty: [], deduplicated: contract.deduplicated, negativeBoundary: [-2, 0, 5], scalar: [7], largeSafeInteger: [9007199254740991], restored: [] }
for (const [phase, expectedValue] of Object.entries(values)) {
const snapshot = entry.phases?.[phase]
if (!isDeepStrictEqual(snapshot?.value, expectedValue) || snapshot.propertyTypeId !== 'App::PropertyIntegerSet' || snapshot.group !== contract.group || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || snapshot.shape?.applicable !== false || snapshot.objectSet?.length !== 1 || snapshot.objectSet[0].typeId !== entry.objectTypeId || !Array.isArray(snapshot.objectState)) fail(`${entry.objectTypeId} ${phase} semantics changed`)
}
}
console.log(JSON.stringify({ status: 'freecad-property-integerset-success-pass', cases: report.caseCount, values: Object.fromEntries(report.cases.map((entry) => [entry.propertyName, Object.fromEntries(Object.entries(entry.phases).map(([phase, snapshot]) => [phase, snapshot.value]))])), bindingBoundary: report.bindingBoundary }, null, 2))

View File

@@ -0,0 +1,95 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def object_set(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
def snapshot(document, obj):
document.recompute()
return {
"value": str(obj.Font),
"propertyTypeId": obj.getTypeIdOfProperty("Font"),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Font")],
"editorMode": [str(item) for item in obj.getEditorMode("Font")],
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"objectSet": object_set(document),
}
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()
if not active:
return {"name": "", "id": 0}
return {"name": str(active[0]), "id": int(active[1])}
def run():
document = App.newDocument("PropertyFontFailure")
try:
obj = document.addObject("TechDraw::DrawViewAnnotation", "FontProbe")
document.recompute()
document.UndoMode = 1
initial = snapshot(document, obj)
before = snapshot(document, obj)
before_objects = object_set(document)
wrong_type_exception = exception_snapshot(lambda: setattr(obj, "Font", 12345))
after = snapshot(document, obj)
obj.setEditorMode("Font", ["ReadOnly"])
obj.setPropertyStatus("Font", "Immutable")
disabled_before = snapshot(document, obj)
disabled_objects_before = object_set(document)
disabled_exception = exception_snapshot(lambda: setattr(obj, "Font", "Blocked Font"))
disabled_after = snapshot(document, obj)
disabled_objects_after = object_set(document)
disabled_status = [str(item) for item in obj.getPropertyStatus("Font")]
disabled_editor_mode = [str(item) for item in obj.getEditorMode("Font")]
obj.setPropertyStatus("Font", "-Immutable")
obj.setEditorMode("Font", 0)
document.openTransaction("property-font-cancel")
transaction_before = snapshot(document, obj)
transaction_objects_before = object_set(document)
obj.Font = "Transaction Font"
transaction_edited = snapshot(document, obj)
transaction_pending_after_edit = bool(document.HasPendingTransaction)
transaction_active_after_edit = active_transaction_snapshot()
document.abortTransaction()
transaction_after_abort = snapshot(document, obj)
transaction_objects_after = object_set(document)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-font-failure",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyFont",
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "Font", "typeId": obj.getTypeIdOfProperty("Font")},
"initial": initial,
"failure": {"id": "wrong-type", "requested": 12345, "exception": wrong_type_exception, "before": before, "after": after, "beforeObjects": before_objects, "afterObjects": object_set(document), "objectsPreserved": before_objects == object_set(document)},
"disabled": {"classification": "editor-read-only-and-python-immutable", "status": disabled_status, "editorMode": disabled_editor_mode, "exception": disabled_exception, "before": disabled_before, "after": disabled_after, "beforeObjects": disabled_objects_before, "afterObjects": disabled_objects_after, "restoredStatus": [str(item) for item in obj.getPropertyStatus("Font")], "restoredEditorMode": [str(item) for item in obj.getEditorMode("Font")]},
"transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbort": transaction_after_abort, "objectsBefore": transaction_objects_before, "objectsAfter": transaction_objects_after, "undoMode": int(document.UndoMode), "pendingAfterEdit": transaction_pending_after_edit, "pendingAfterAbort": bool(document.HasPendingTransaction), "activeAfterEdit": transaction_active_after_edit, "activeAfterAbort": active_transaction_snapshot(), "restored": transaction_before == transaction_after_abort, "objectsRestored": transaction_objects_before == transaction_objects_after},
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
"documentIntegrity": {"initialObjects": initial["objectSet"], "finalObjects": object_set(document), "objectsPreserved": initial["objectSet"] == object_set(document), "objectCount": len(document.Objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FONT_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,51 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def snapshot(document, obj):
recompute_result = bool(document.recompute())
return {
"value": str(obj.Font),
"propertyTypeId": obj.getTypeIdOfProperty("Font"),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Font")],
"editorMode": [str(item) for item in obj.getEditorMode("Font")],
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"recomputeResult": recompute_result,
"hasShapeProperty": "Shape" in obj.PropertiesList,
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
def run():
document = App.newDocument("PropertyFontMutation")
try:
obj = document.addObject("TechDraw::DrawViewAnnotation", "FontProbe")
document.recompute()
before = snapshot(document, obj)
obj.Font = "Liberation Sans"
edited = snapshot(document, obj)
obj.Font = "osifont"
restored = snapshot(document, obj)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-font-mutation",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyFont",
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "Font", "typeId": obj.getTypeIdOfProperty("Font")},
"requested": {"before": "osifont", "edited": "Liberation Sans", "restored": "osifont"},
"phases": {"before": before, "edited": edited, "restored": restored},
"classification": {"valueChanged": before["value"] != edited["value"], "restoreExact": restored["value"] == before["value"] and restored["propertyTypeId"] == before["propertyTypeId"] and restored["propertyStatus"] == before["propertyStatus"] and restored["editorMode"] == before["editorMode"] and restored["objectState"] == before["objectState"] and restored["statusString"] == before["statusString"] and restored["hasShapeProperty"] == before["hasShapeProperty"] and restored["objectSet"] == before["objectSet"], "objectSetStable": before["objectSet"] == edited["objectSet"] == restored["objectSet"], "geometryApplicability": "not-applicable-no-shape-property", "unknownSemanticDrift": False},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FONT_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,81 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def object_snapshot(document):
obj = document.getObject("FontProbe")
return {
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
"object": {
"name": obj.Name,
"typeId": obj.TypeId,
"propertyTypeId": obj.getTypeIdOfProperty("Font"),
"value": str(obj.Font),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Font")],
"editorMode": [str(item) for item in obj.getEditorMode("Font")],
"state": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"hasShapeProperty": "Shape" in obj.PropertiesList,
},
}
def create_native(path, initial_value):
document = App.newDocument("PropertyFontRoundtrip")
try:
obj = document.addObject("TechDraw::DrawViewAnnotation", "FontProbe")
obj.Font = initial_value
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_FONT_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_FONT_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_FONT_ROUNDTRIP_RESAVED_PATH", "")
initial_value = os.environ.get("FREECAD_PROPERTY_FONT_ROUNDTRIP_INITIAL", "")
if mode == "create":
result = create_native(path, initial_value)
elif mode == "verify":
result = verify_native(path, resaved_path)
else:
raise RuntimeError("FREECAD_PROPERTY_FONT_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_FONT_ROUNDTRIP_RESULT=" + json.dumps({
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-font-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,49 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def snapshot(document, obj, requested):
recompute_result = bool(document.recompute())
return {
"requested": requested,
"value": str(obj.Font),
"propertyTypeId": obj.getTypeIdOfProperty("Font"),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Font")],
"editorMode": [str(item) for item in obj.getEditorMode("Font")],
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"recomputeResult": recompute_result,
"hasShapeProperty": "Shape" in obj.PropertiesList,
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
def run():
document = App.newDocument("PropertyFontSuccess")
try:
obj = document.addObject("TechDraw::DrawViewAnnotation", "FontProbe")
phases = {"default": snapshot(document, obj, "osifont")}
for phase, value in [("named-family", "DejaVu Sans"), ("unicode-family", "Noto Sans CJK SC"), ("empty-family", ""), ("restored", "osifont")]:
obj.Font = value
phases[phase] = snapshot(document, obj, value)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-font-success",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyFont",
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "Font", "typeId": obj.getTypeIdOfProperty("Font"), "group": obj.getGroupOfProperty("Font")},
"phases": phases,
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FONT_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,154 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
INVALID_INPUTS = [
("wrong-dimension", "5 mm"),
("malformed-text", "not-a-quantity"),
]
def value_snapshot(obj, property_name):
value = getattr(obj, property_name)
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def object_snapshot(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj 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()
if not active:
return {"name": "", "id": 0}
return {"name": str(active[0]), "id": int(active[1])}
def run():
document = App.newDocument("PropertyForceFailure")
try:
force = document.addObject("Fem::ConstraintForce", "ForceProbe")
rigid = document.addObject("Fem::ConstraintRigidBody", "RigidForceProbe")
document.recompute()
document.UndoMode = 1
initial = value_snapshot(force, "Force")
initial_objects = object_snapshot(document)
failures = []
for case_id, requested in INVALID_INPUTS:
before = value_snapshot(force, "Force")
before_objects = object_snapshot(document)
exception = exception_snapshot(lambda: setattr(force, "Force", requested))
after = value_snapshot(force, "Force")
after_objects = object_snapshot(document)
failures.append({
"id": case_id,
"requested": requested,
"exception": exception,
"before": before,
"after": after,
"beforeObjects": before_objects,
"afterObjects": after_objects,
"valuePreserved": before == after,
"objectsPreserved": before_objects == after_objects,
"polluted": before_objects != after_objects,
})
disabled_before = value_snapshot(force, "Force")
disabled_objects_before = object_snapshot(document)
force.setEditorMode("Force", ["ReadOnly"])
force.setPropertyStatus("Force", "Immutable")
disabled_status = [str(item) for item in force.getPropertyStatus("Force")]
disabled_editor_mode = [str(item) for item in force.getEditorMode("Force")]
disabled_exception = exception_snapshot(lambda: setattr(force, "Force", "750 N"))
disabled_after = value_snapshot(force, "Force")
disabled_objects_after = object_snapshot(document)
force.setPropertyStatus("Force", "-Immutable")
force.setEditorMode("Force", 0)
disabled = {
"classification": "editor-read-only-and-python-immutable",
"requested": "750 N",
"status": disabled_status,
"editorMode": disabled_editor_mode,
"exception": disabled_exception,
"before": disabled_before,
"after": disabled_after,
"valuePreserved": disabled_before == disabled_after,
"objectsPreserved": disabled_objects_before == disabled_objects_after,
"restoredStatus": [str(item) for item in force.getPropertyStatus("Force")],
"restoredEditorMode": [str(item) for item in force.getEditorMode("Force")],
}
output_before = value_snapshot(rigid, "ForceX")
output_status = [str(item) for item in rigid.getPropertyStatus("ForceX")]
output_editor_mode = [str(item) for item in rigid.getEditorMode("ForceX")]
output_assignment_exception = exception_snapshot(lambda: setattr(rigid, "ForceX", "125 N"))
output_after = value_snapshot(rigid, "ForceX")
rigid.ForceX = "0 N"
output_restored = value_snapshot(rigid, "ForceX")
output = {
"classification": "prop-output-ui-read-only-python-mutable",
"property": {"name": "ForceX", "typeId": rigid.getTypeIdOfProperty("ForceX")},
"status": output_status,
"editorMode": output_editor_mode,
"assignmentException": output_assignment_exception,
"before": output_before,
"after": output_after,
"restored": output_restored,
"restoredExactly": output_before == output_restored,
}
document.openTransaction("property-force-cancel")
transaction_before = value_snapshot(force, "Force")
transaction_objects_before = object_snapshot(document)
force.Force = "250 N"
transaction_edited = value_snapshot(force, "Force")
transaction_pending_after_edit = bool(document.HasPendingTransaction)
transaction_active_after_edit = active_transaction_snapshot()
document.abortTransaction()
transaction_after = value_snapshot(force, "Force")
transaction_objects_after = object_snapshot(document)
final_objects = object_snapshot(document)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-force-failure",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": force.Name, "typeId": force.TypeId},
"property": {"name": "Force", "typeId": force.getTypeIdOfProperty("Force")},
"initial": initial,
"failures": failures,
"disabled": disabled,
"outputBoundary": output,
"transaction": {
"before": transaction_before,
"edited": transaction_edited,
"afterAbort": transaction_after,
"objectsBefore": transaction_objects_before,
"objectsAfter": transaction_objects_after,
"undoMode": int(document.UndoMode),
"pendingAfterEdit": transaction_pending_after_edit,
"pendingAfterAbort": bool(document.HasPendingTransaction),
"activeAfterEdit": transaction_active_after_edit,
"activeAfterAbort": active_transaction_snapshot(),
"restored": transaction_before == transaction_after,
"objectsRestored": transaction_objects_before == transaction_objects_after,
},
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": final_objects, "objectsPreserved": initial_objects == final_objects, "objectCount": len(final_objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FORCE_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,94 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def quantity_snapshot(value):
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def semantic_snapshot(obj):
direction = obj.DirectionVector
return {
"value": quantity_snapshot(obj.Force),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Force")],
"editorMode": [str(item) for item in obj.getEditorMode("Force")],
"reversed": bool(obj.Reversed),
"enableAmplitude": bool(obj.EnableAmplitude),
"amplitudeValues": list(obj.AmplitudeValues),
"directionVector": [round(float(direction.x), 9), round(float(direction.y), 9), round(float(direction.z), 9)],
"propertyNames": list(obj.PropertiesList),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"mustExecute": bool(obj.MustExecute),
}
def object_snapshot(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
def run():
document = App.newDocument("PropertyForceMutation")
try:
obj = document.addObject("Fem::ConstraintForce", "ForceProbe")
obj.Force = "0 N"
initial_recompute = int(document.recompute())
before = semantic_snapshot(obj)
objects_before = object_snapshot(document)
obj.Force = "500 N"
touched_after_edit = {
"value": quantity_snapshot(obj.Force),
"objectState": [str(item) for item in obj.State],
"mustExecute": bool(obj.MustExecute),
}
edit_recompute = int(document.recompute())
edited = semantic_snapshot(obj)
obj.Force = "0 N"
touched_after_restore = {
"value": quantity_snapshot(obj.Force),
"objectState": [str(item) for item in obj.State],
"mustExecute": bool(obj.MustExecute),
}
restore_recompute = int(document.recompute())
restored = semantic_snapshot(obj)
objects_restored = object_snapshot(document)
def neighbors(snapshot):
return {key: snapshot[key] for key in ["reversed", "enableAmplitude", "amplitudeValues", "directionVector", "propertyNames"]}
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-force-mutation",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "Force", "typeId": obj.getTypeIdOfProperty("Force")},
"recompute": {"initial": initial_recompute, "edit": edit_recompute, "restore": restore_recompute},
"touchedAfterEdit": touched_after_edit,
"touchedAfterRestore": touched_after_restore,
"before": before,
"edited": edited,
"restored": restored,
"neighbors": {"before": neighbors(before), "edited": neighbors(edited), "restored": neighbors(restored), "preserved": neighbors(before) == neighbors(edited) == neighbors(restored)},
"document": {"objectsBefore": objects_before, "objectsRestored": objects_restored, "objectsPreserved": objects_before == objects_restored},
"classification": {
"propertyChanged": before["value"] != edited["value"],
"propertyRestored": before["value"] == restored["value"],
"semanticStateRestored": before == restored,
"neighborPropertiesPreserved": neighbors(before) == neighbors(edited) == neighbors(restored),
"geometry": "not-applicable-no-shape-property",
},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FORCE_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,101 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def quantity_snapshot(value):
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def property_snapshot(obj, property_name):
return {
"name": property_name,
"typeId": obj.getTypeIdOfProperty(property_name),
"value": quantity_snapshot(getattr(obj, property_name)),
"status": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
}
def object_snapshot(document):
force = document.getObject("ForceProbe")
rigid = document.getObject("RigidForceProbe")
return {
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
"force": {
"name": force.Name,
"typeId": force.TypeId,
"property": property_snapshot(force, "Force"),
"shape": {"applicable": hasattr(force, "Shape")},
"state": [str(item) for item in force.State],
"statusString": str(force.getStatusString()),
},
"rigid": {
"name": rigid.Name,
"typeId": rigid.TypeId,
"outputs": [property_snapshot(rigid, name) for name in ["ForceX", "ForceY", "ForceZ"]],
"shape": {"applicable": hasattr(rigid, "Shape")},
"state": [str(item) for item in rigid.State],
"statusString": str(rigid.getStatusString()),
},
}
def create_native(path):
document = App.newDocument("PropertyForceRoundtrip")
try:
force = document.addObject("Fem::ConstraintForce", "ForceProbe")
force.Force = "0 N"
document.addObject("Fem::ConstraintRigidBody", "RigidForceProbe")
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_FORCE_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_FORCE_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_FORCE_ROUNDTRIP_RESAVED_PATH", "")
if mode == "create":
result = create_native(path)
elif mode == "verify":
result = verify_native(path, resaved_path)
else:
raise RuntimeError("FREECAD_PROPERTY_FORCE_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_FORCE_ROUNDTRIP_RESULT=" + json.dumps({
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-force-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 json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
CASES = [
("default", None),
("nominal-newton", "500 N"),
("zero-boundary", "0 N"),
("negative-boundary", "-5 N"),
("kilonewton", "2 kN"),
("restored", "0 N"),
]
def value_snapshot(obj, property_name):
value = getattr(obj, property_name)
return {
"raw": str(value),
"numeric": round(float(value.Value), 9),
"unit": str(value.Unit),
}
def property_snapshot(obj, property_name):
return {
"objectName": obj.Name,
"objectTypeId": obj.TypeId,
"propertyName": property_name,
"typeId": obj.getTypeIdOfProperty(property_name),
"group": obj.getGroupOfProperty(property_name),
"status": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
"value": value_snapshot(obj, property_name),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
}
def run():
document = App.newDocument("PropertyForceSuccess")
try:
force = document.addObject("Fem::ConstraintForce", "ForceProbe")
rigid = document.addObject("Fem::ConstraintRigidBody", "RigidForceProbe")
document.recompute()
cases = []
for case_id, requested in CASES:
if requested is not None:
force.Force = requested
document.recompute()
snapshot = property_snapshot(force, "Force")
snapshot.update({"id": case_id, "requested": requested})
cases.append(snapshot)
outputs = [property_snapshot(rigid, name) for name in ["ForceX", "ForceY", "ForceZ"]]
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-force-success",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": force.Name, "typeId": force.TypeId},
"property": {"name": "Force", "typeId": force.getTypeIdOfProperty("Force"), "group": force.getGroupOfProperty("Force")},
"cases": cases,
"outputProperties": outputs,
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_FORCE_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,130 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
INVALID_INPUTS = [("wrong-dimension", "5 mm"), ("malformed-text", "not-a-quantity")]
def value_snapshot(obj, property_name):
value = getattr(obj, property_name)
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def object_snapshot(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj 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()
if not active:
return {"name": "", "id": 0}
return {"name": str(active[0]), "id": int(active[1])}
def run():
document = App.newDocument("PropertyHeatFluxFailure")
try:
obj = document.addObject("Fem::ConstraintHeatflux", "HeatFluxProbe")
document.recompute()
document.UndoMode = 1
initial = value_snapshot(obj, "DFlux")
initial_objects = object_snapshot(document)
failures = []
for case_id, requested in INVALID_INPUTS:
before = value_snapshot(obj, "DFlux")
before_objects = object_snapshot(document)
exception = exception_snapshot(lambda: setattr(obj, "DFlux", requested))
after = value_snapshot(obj, "DFlux")
after_objects = object_snapshot(document)
failures.append({
"id": case_id,
"requested": requested,
"exception": exception,
"before": before,
"after": after,
"beforeObjects": before_objects,
"afterObjects": after_objects,
"valuePreserved": before == after,
"objectsPreserved": before_objects == after_objects,
"polluted": before_objects != after_objects,
})
disabled_before = value_snapshot(obj, "DFlux")
disabled_objects_before = object_snapshot(document)
obj.setEditorMode("DFlux", ["ReadOnly"])
obj.setPropertyStatus("DFlux", "Immutable")
disabled_status = [str(item) for item in obj.getPropertyStatus("DFlux")]
disabled_editor_mode = [str(item) for item in obj.getEditorMode("DFlux")]
disabled_exception = exception_snapshot(lambda: setattr(obj, "DFlux", "750 W/m^2"))
disabled_after = value_snapshot(obj, "DFlux")
disabled_objects_after = object_snapshot(document)
obj.setPropertyStatus("DFlux", "-Immutable")
obj.setEditorMode("DFlux", 0)
document.openTransaction("property-heatflux-cancel")
transaction_before = value_snapshot(obj, "DFlux")
transaction_objects_before = object_snapshot(document)
obj.DFlux = "250 W/m^2"
transaction_edited = value_snapshot(obj, "DFlux")
transaction_pending_after_edit = bool(document.HasPendingTransaction)
transaction_active_after_edit = active_transaction_snapshot()
document.abortTransaction()
transaction_after = value_snapshot(obj, "DFlux")
transaction_objects_after = object_snapshot(document)
final_objects = object_snapshot(document)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-heatflux-failure",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "DFlux", "typeId": obj.getTypeIdOfProperty("DFlux")},
"initial": initial,
"failures": failures,
"disabled": {
"classification": "editor-read-only-and-python-immutable",
"requested": "750 W/m^2",
"status": disabled_status,
"editorMode": disabled_editor_mode,
"exception": disabled_exception,
"before": disabled_before,
"after": disabled_after,
"valuePreserved": disabled_before == disabled_after,
"objectsPreserved": disabled_objects_before == disabled_objects_after,
"restoredStatus": [str(item) for item in obj.getPropertyStatus("DFlux")],
"restoredEditorMode": [str(item) for item in obj.getEditorMode("DFlux")],
},
"transaction": {
"before": transaction_before,
"edited": transaction_edited,
"afterAbort": transaction_after,
"objectsBefore": transaction_objects_before,
"objectsAfter": transaction_objects_after,
"undoMode": int(document.UndoMode),
"pendingAfterEdit": transaction_pending_after_edit,
"pendingAfterAbort": bool(document.HasPendingTransaction),
"activeAfterEdit": transaction_active_after_edit,
"activeAfterAbort": active_transaction_snapshot(),
"restored": transaction_before == transaction_after,
"objectsRestored": transaction_objects_before == transaction_objects_after,
},
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": final_objects, "objectsPreserved": initial_objects == final_objects, "objectCount": len(final_objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_HEATFLUX_FAILURE_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,90 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def quantity_snapshot(value):
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def semantic_snapshot(obj):
return {
"value": quantity_snapshot(obj.DFlux),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("DFlux")],
"editorMode": [str(item) for item in obj.getEditorMode("DFlux")],
"constraintType": str(obj.ConstraintType),
"ambientTemp": quantity_snapshot(obj.AmbientTemp),
"filmCoef": quantity_snapshot(obj.FilmCoef),
"emissivity": round(float(obj.Emissivity), 9),
"enableAmplitude": bool(obj.EnableAmplitude),
"amplitudeValues": list(obj.AmplitudeValues),
"scale": round(float(obj.Scale), 9),
"propertyNames": list(obj.PropertiesList),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"mustExecute": bool(obj.MustExecute),
}
def object_snapshot(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
def run():
document = App.newDocument("PropertyHeatFluxMutation")
try:
obj = document.addObject("Fem::ConstraintHeatflux", "HeatFluxProbe")
obj.DFlux = "0 W/m^2"
initial_recompute = int(document.recompute())
before = semantic_snapshot(obj)
objects_before = object_snapshot(document)
obj.DFlux = "500 W/m^2"
touched_after_edit = {"value": quantity_snapshot(obj.DFlux), "objectState": [str(item) for item in obj.State], "mustExecute": bool(obj.MustExecute)}
edit_recompute = int(document.recompute())
edited = semantic_snapshot(obj)
obj.DFlux = "0 W/m^2"
touched_after_restore = {"value": quantity_snapshot(obj.DFlux), "objectState": [str(item) for item in obj.State], "mustExecute": bool(obj.MustExecute)}
restore_recompute = int(document.recompute())
restored = semantic_snapshot(obj)
objects_restored = object_snapshot(document)
def neighbors(snapshot):
keys = ["constraintType", "ambientTemp", "filmCoef", "emissivity", "enableAmplitude", "amplitudeValues", "scale", "propertyNames"]
return {key: snapshot[key] for key in keys}
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-heatflux-mutation",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "DFlux", "typeId": obj.getTypeIdOfProperty("DFlux")},
"recompute": {"initial": initial_recompute, "edit": edit_recompute, "restore": restore_recompute},
"touchedAfterEdit": touched_after_edit,
"touchedAfterRestore": touched_after_restore,
"before": before,
"edited": edited,
"restored": restored,
"neighbors": {"before": neighbors(before), "edited": neighbors(edited), "restored": neighbors(restored), "preserved": neighbors(before) == neighbors(edited) == neighbors(restored)},
"document": {"objectsBefore": objects_before, "objectsRestored": objects_restored, "objectsPreserved": objects_before == objects_restored},
"classification": {
"propertyChanged": before["value"] != edited["value"],
"propertyRestored": before["value"] == restored["value"],
"semanticStateRestored": before == restored,
"neighborPropertiesPreserved": neighbors(before) == neighbors(edited) == neighbors(restored),
"geometry": "not-applicable-no-shape-property",
},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_HEATFLUX_MUTATION_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,93 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def quantity_snapshot(value):
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def property_snapshot(obj, property_name):
return {
"name": property_name,
"typeId": obj.getTypeIdOfProperty(property_name),
"group": obj.getGroupOfProperty(property_name),
"value": quantity_snapshot(getattr(obj, property_name)),
"status": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
}
def object_snapshot(document):
obj = document.getObject("HeatFluxProbe")
return {
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
"heatFlux": {
"name": obj.Name,
"typeId": obj.TypeId,
"property": property_snapshot(obj, "DFlux"),
"shape": {"applicable": hasattr(obj, "Shape")},
"state": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
},
}
def create_native(path):
document = App.newDocument("PropertyHeatFluxRoundtrip")
try:
obj = document.addObject("Fem::ConstraintHeatflux", "HeatFluxProbe")
obj.DFlux = "0 W/m^2"
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_HEATFLUX_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_HEATFLUX_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_HEATFLUX_ROUNDTRIP_RESAVED_PATH", "")
if mode == "create":
result = create_native(path)
elif mode == "verify":
result = verify_native(path, resaved_path)
else:
raise RuntimeError("FREECAD_PROPERTY_HEATFLUX_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_HEATFLUX_ROUNDTRIP_RESULT=" + json.dumps({
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-heatflux-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,68 @@
import json
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
CASES = [
("default", None),
("nominal-watt-per-square-meter", "500 W/m^2"),
("zero-boundary", "0 W/m^2"),
("negative-boundary", "-5 W/m^2"),
("kilowatt-per-square-meter", "2 kW/m^2"),
("watt-per-square-millimeter", "1 W/mm^2"),
("restored", "0 W/m^2"),
]
def value_snapshot(obj, property_name):
value = getattr(obj, property_name)
return {"raw": str(value), "numeric": round(float(value.Value), 9), "unit": str(value.Unit)}
def property_snapshot(obj, property_name):
return {
"objectName": obj.Name,
"objectTypeId": obj.TypeId,
"propertyName": property_name,
"typeId": obj.getTypeIdOfProperty(property_name),
"group": obj.getGroupOfProperty(property_name),
"status": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
"value": value_snapshot(obj, property_name),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
}
def run():
document = App.newDocument("PropertyHeatFluxSuccess")
try:
obj = document.addObject("Fem::ConstraintHeatflux", "HeatFluxProbe")
document.recompute()
cases = []
for case_id, requested in CASES:
if requested is not None:
obj.DFlux = requested
document.recompute()
snapshot = property_snapshot(obj, "DFlux")
snapshot.update({"id": case_id, "requested": requested})
cases.append(snapshot)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-heatflux-success",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "DFlux", "typeId": obj.getTypeIdOfProperty("DFlux"), "group": obj.getGroupOfProperty("DFlux")},
"cases": cases,
"diagnostics": {"exception": None, "documentObjectCount": len(document.Objects)},
}
finally:
App.closeDocument(document.Name)
print("FREECAD_PROPERTY_HEATFLUX_SUCCESS_RESULT=" + json.dumps(run(), sort_keys=True))

View File

@@ -0,0 +1,99 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_INTEGERSET_FAILURE_OUTPUT", "")
def values(obj):
return sorted(int(value) for value in obj.Nodes)
def objects(document):
return [{"name": obj.Name, "typeId": obj.TypeId} for obj 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():
document = App.newDocument("PropertyIntegerSetFailure")
try:
obj = document.addObject("Fem::FemSetNodesObject", "NodesProbe")
obj.Nodes = [1, 3]
document.recompute()
document.UndoMode = 1
initial = values(obj)
initial_objects = objects(document)
failures = []
for case_id, requested in [("float-item", [1, 1.5]), ("string-item", [1, "3"]), ("set-object", {1, 3}), ("none", None)]:
before = values(obj)
before_objects = objects(document)
exception = exception_snapshot(lambda value=requested: setattr(obj, "Nodes", value))
after = values(obj)
after_objects = objects(document)
failures.append({"id": case_id, "requested": repr(requested), "exception": exception, "before": before, "after": after, "beforeObjects": before_objects, "afterObjects": after_objects, "valuePreserved": before == after, "objectsPreserved": before_objects == after_objects, "polluted": before_objects != after_objects})
disabled_before = values(obj)
disabled_objects_before = objects(document)
obj.setEditorMode("Nodes", ["ReadOnly"])
obj.setPropertyStatus("Nodes", "Immutable")
disabled_status = [str(item) for item in obj.getPropertyStatus("Nodes")]
disabled_editor_mode = [str(item) for item in obj.getEditorMode("Nodes")]
disabled_exception = exception_snapshot(lambda: setattr(obj, "Nodes", [2, 4]))
disabled_after = values(obj)
disabled_objects_after = objects(document)
obj.setPropertyStatus("Nodes", "-Immutable")
obj.setEditorMode("Nodes", 0)
document.openTransaction("property-integerset-cancel")
transaction_before = values(obj)
transaction_objects_before = objects(document)
obj.Nodes = [2, 4, 4]
transaction_edited = values(obj)
pending_after_edit = bool(document.HasPendingTransaction)
active_after_edit = active_transaction_snapshot()
document.abortTransaction()
transaction_after = values(obj)
transaction_objects_after = objects(document)
return {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-integerset-failure",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyIntegerSet",
"object": {"name": obj.Name, "typeId": obj.TypeId},
"property": {"name": "Nodes", "typeId": obj.getTypeIdOfProperty("Nodes")},
"initial": initial,
"failures": failures,
"disabled": {"classification": "editor-read-only-and-python-immutable", "requested": [2, 4], "status": disabled_status, "editorMode": disabled_editor_mode, "exception": disabled_exception, "before": disabled_before, "after": disabled_after, "valuePreserved": disabled_before == disabled_after, "objectsPreserved": disabled_objects_before == disabled_objects_after, "restoredStatus": [str(item) for item in obj.getPropertyStatus("Nodes")], "restoredEditorMode": [str(item) for item in obj.getEditorMode("Nodes")]},
"transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbort": transaction_after, "objectsBefore": transaction_objects_before, "objectsAfter": transaction_objects_after, "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, "objectsRestored": transaction_objects_before == transaction_objects_after},
"cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"},
"documentIntegrity": {"initialObjects": initial_objects, "finalObjects": objects(document), "objectsPreserved": initial_objects == objects(document)},
}
finally:
App.closeDocument(document.Name)
report = run()
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_INTEGERSET_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_INTEGERSET_FAILURE_RESULT=" + json.dumps({"status": report["status"], "failureCount": len(report["failures"])}, sort_keys=True))

View File

@@ -0,0 +1,74 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_INTEGERSET_MUTATION_OUTPUT", "")
def snapshot(document, obj, property_name):
return {
"value": sorted(int(value) for value in getattr(obj, property_name)),
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
"group": obj.getGroupOfProperty(property_name),
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
"femMesh": obj.FemMesh.Name if obj.FemMesh else None,
"propertyNames": list(obj.PropertiesList),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"mustExecute": bool(obj.MustExecute),
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
}
def run_case(object_type_id, object_name, property_name, initial, edited):
document = App.newDocument("PropertyIntegerSetMutation" + object_name)
try:
obj = document.addObject(object_type_id, object_name)
setattr(obj, property_name, initial)
initial_recompute = bool(document.recompute())
before = snapshot(document, obj, property_name)
setattr(obj, property_name, edited)
touched_after_edit = snapshot(document, obj, property_name)
edit_recompute = bool(document.recompute())
after_edit = snapshot(document, obj, property_name)
setattr(obj, property_name, initial)
touched_after_restore = snapshot(document, obj, property_name)
restore_recompute = bool(document.recompute())
restored = snapshot(document, obj, property_name)
neighbors = lambda value: {key: value[key] for key in ["femMesh", "propertyNames"]}
return {
"objectTypeId": object_type_id,
"objectName": object_name,
"propertyName": property_name,
"recompute": {"initial": initial_recompute, "edit": edit_recompute, "restore": restore_recompute},
"before": before,
"touchedAfterEdit": touched_after_edit,
"edited": after_edit,
"touchedAfterRestore": touched_after_restore,
"restored": restored,
"neighborsPreserved": neighbors(before) == neighbors(after_edit) == neighbors(restored),
"valueRestored": before["value"] == restored["value"],
"semanticStateRestored": before == restored,
"objectsRestored": before["objectSet"] == restored["objectSet"],
}
finally:
App.closeDocument(document.Name)
cases = [
run_case("Fem::FemSetNodesObject", "NodesProbe", "Nodes", [1, 3], [5, 2, 5]),
run_case("Fem::FemSetElementNodesObject", "ElementsProbe", "Elements", [4, 10], [-1, 8, 8]),
]
report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-integerset-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyIntegerSet", "caseCount": len(cases), "cases": cases}
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_INTEGERSET_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_INTEGERSET_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))

View File

@@ -0,0 +1,75 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def object_snapshot(document):
obj = document.getObject("NodesProbe")
return {
"objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects],
"object": {
"name": obj.Name,
"typeId": obj.TypeId,
"propertyName": "Nodes",
"propertyTypeId": obj.getTypeIdOfProperty("Nodes"),
"group": obj.getGroupOfProperty("Nodes"),
"value": sorted(int(value) for value in obj.Nodes),
"propertyStatus": [str(item) for item in obj.getPropertyStatus("Nodes")],
"editorMode": [str(item) for item in obj.getEditorMode("Nodes")],
"state": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"hasShapeProperty": "Shape" in obj.PropertiesList,
},
}
def create_native(path):
document = App.newDocument("PropertyIntegerSetRoundtrip")
try:
obj = document.addObject("Fem::FemSetNodesObject", "NodesProbe")
obj.Nodes = [1, 3]
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_INTEGERSET_ROUNDTRIP_MODE", "")
path = os.environ.get("FREECAD_PROPERTY_INTEGERSET_ROUNDTRIP_PATH", "")
resaved_path = os.environ.get("FREECAD_PROPERTY_INTEGERSET_ROUNDTRIP_RESAVED_PATH", "")
if mode == "create":
result = create_native(path)
elif mode == "verify":
result = verify_native(path, resaved_path)
else:
raise RuntimeError("FREECAD_PROPERTY_INTEGERSET_ROUNDTRIP_MODE must be create or verify")
print("FREECAD_PROPERTY_INTEGERSET_ROUNDTRIP_RESULT=" + json.dumps({"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-integerset-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,80 @@
import json
import os
import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_INTEGERSET_SUCCESS_OUTPUT", "")
def value_snapshot(obj, property_name):
return sorted(int(value) for value in getattr(obj, property_name))
def property_snapshot(document, obj, property_name):
return {
"value": value_snapshot(obj, property_name),
"propertyTypeId": obj.getTypeIdOfProperty(property_name),
"group": obj.getGroupOfProperty(property_name),
"propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)],
"editorMode": [str(item) for item in obj.getEditorMode(property_name)],
"objectState": [str(item) for item in obj.State],
"statusString": str(obj.getStatusString()),
"recomputeResult": bool(document.recompute()),
"shape": {"applicable": hasattr(obj, "Shape")},
"objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects],
}
def run_case(object_type_id, object_name, property_name, group, nominal):
document = App.newDocument("PropertyIntegerSet" + object_name)
try:
obj = document.addObject(object_type_id, object_name)
# FreeCAD 1.1.1 can terminate when its never-assigned empty std::set binding is read.
setattr(obj, property_name, [])
phases = {"explicitEmpty": property_snapshot(document, obj, property_name)}
values = {
"deduplicated": nominal,
"negativeBoundary": [-2, 0, 5],
"scalar": 7,
"largeSafeInteger": [9007199254740991],
"restored": [],
}
for phase, value in values.items():
setattr(obj, property_name, value)
phases[phase] = property_snapshot(document, obj, property_name)
return {
"objectTypeId": object_type_id,
"objectName": object_name,
"propertyName": property_name,
"group": group,
"mode": "direct-native-property-setter",
"phases": phases,
}
finally:
App.closeDocument(document.Name)
cases = [
run_case("Fem::FemSetNodesObject", "NodesProbe", "Nodes", "Node indexes", [3, 1, 3, 2]),
run_case("Fem::FemSetElementNodesObject", "ElementsProbe", "Elements", "Element indexes", [10, 4, 10]),
]
report = {
"schemaVersion": 1,
"status": "pass",
"baselineId": "freecad-1.1.1-property-integerset-success",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"propertyType": "App::PropertyIntegerSet",
"caseCount": len(cases),
"bindingBoundary": {"neverAssignedEmptyRead": "native-process-termination", "safeEquivalent": "assign-empty-list-before-read", "oracleDefault": "set()"},
"cases": cases,
}
if not OUTPUT_PATH:
raise RuntimeError("FREECAD_PROPERTY_INTEGERSET_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_INTEGERSET_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1) fail('global property semantics did not promote App::PropertyAcceleration')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyAcceleration' || chrome.persistence.loadedValue !== 500 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -57,3 +57,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-acceleration-promotion-generated', output: 'config/freecad-property-acceleration-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1 || propertyType.statusNames?.join(',') !== 'PropOutput,PropReadOnly') fail('global property semantics did not promote App::PropertyArea with its native status')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.ui?.areaReadOnly !== true || chrome.persistence?.loadedAreaType !== 'App::PropertyArea' || chrome.persistence.loadedElementsType !== 'App::PropertyLinkSubList' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -61,3 +61,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-area-promotion-generated', output: 'config/freecad-property-area-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 7 || propertyType.objectTypeCount !== 7 || propertyType.statusNames?.join(',') !== 'Hidden,Immutable,LockDynamic') fail('global property semantics did not promote App::PropertyBoolList with its native status')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyBoolList' || chrome.persistence.fcstdElement !== 'BoolList' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -59,3 +59,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-boollist-promotion-generated', output: 'config/freecad-property-boollist-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 8 || propertyType.objectTypeCount !== 8 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyColor')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyColor' || chrome.persistence.fcstdElement !== 'PropertyColor' || chrome.persistence.fcstdValue !== '862362060' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -59,3 +59,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-color-promotion-generated', output: 'config/freecad-property-color-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyColorList')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyColorList' || chrome.persistence.fcstdElement !== 'ColorList' || chrome.persistence.fcstdResourcePath !== 'ColourList' || chrome.persistence.fcstdResourceBytes !== 12 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -60,3 +60,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-colorlist-promotion-generated', output: 'config/freecad-property-colorlist-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyDirection')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyDirection' || chrome.persistence.fcstdElement !== 'PropertyVector' || chrome.persistence.fcstdValue !== JSON.stringify({ x: 0.25, y: -0.5, z: 1.5 }) || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -58,3 +58,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-direction-promotion-generated', output: 'config/freecad-property-direction-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 16 || propertyType.objectTypeCount !== 12 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyFile')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.externalResourceBoundary !== 'project-relative-path; asset bytes remain external to FCStd' || chrome.persistence?.loadedType !== 'App::PropertyFile' || chrome.persistence.fcstdElement !== 'String' || chrome.persistence.fcstdValue !== 'project-files/chrome-updated.obj' || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -60,3 +60,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-file-promotion-generated', output: 'config/freecad-property-file-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -24,7 +24,7 @@ const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::Prop
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 20 || propertyType.objectTypeCount !== 15 || propertyType.statusNames?.join(',') !== 'PropOutput,ReadOnly') fail('global property semantics did not promote App::PropertyFileIncluded')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 50 || opaque.recordCount !== 466 || editable?.typeCount !== 30 || editable.recordCount !== 4361) fail('global property support summary is not synchronized')
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.archiveName !== 'payload.bin' || roundTrip.classification.resavedResourceMatches !== true || chrome.persistence?.loadedType !== 'App::PropertyFileIncluded' || chrome.persistence.fcstdElement !== 'FileIncluded' || chrome.persistence.fcstdResourceMatches !== true || chrome.resource?.documentRoundTrip !== true || chrome.resource.independentReleased !== true || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
@@ -64,3 +64,5 @@ const report = {
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-property-fileincluded-promotion-generated', output: 'config/freecad-property-fileincluded-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,87 @@
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 sourcePaths = [
'.cache/freecad/FreeCAD/src/App/PropertyStandard.h',
'.cache/freecad/FreeCAD/src/App/PropertyStandard.cpp',
'.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h',
'.cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp',
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewAnnotation.h',
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewAnnotation.cpp',
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSpreadsheet.h',
'.cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSpreadsheet.cpp',
]
const outputPath = resolve(root, 'config/freecad-property-font-inventory.json')
const [runtimeContent, ...sourceContents] = await Promise.all([
readFile(resolve(root, runtimePath)),
...sourcePaths.map((path) => readFile(resolve(root, path))),
])
const runtime = JSON.parse(runtimeContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyFont 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')
const records = []
for (const objectType of runtime.runtimeObjects?.types ?? []) {
for (const property of objectType.properties ?? []) {
if (property.typeId !== 'App::PropertyFont') continue
records.push({
objectTypeId: objectType.typeId,
objectAvailable: objectType.available === true,
probeStatus: objectType.probeStatus,
propertyName: property.name,
group: property.group,
status: property.status,
defaultRaw: property.default,
valueModel: {
kind: 'font-family-string',
representation: 'App::PropertyString-derived UTF-8 font family name',
writable: property.status.length === 0,
installedFontRequired: false,
},
inputs: {
accepted: ['utf8-string'],
emptyStringAccepted: true,
arbitraryFamilyNameAccepted: true,
},
editor: 'Gui::PropertyEditor::PropertyFontItem',
editorModel: 'QFontDatabase family list exposed through QComboBox',
dependencies: [],
applicability: {
requiredObjectTypeId: objectType.typeId,
propertyWriteRequiresShape: false,
source: 'Document.supportedTypes runtime inventory',
},
})
}
}
records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
if (records.length !== 2 || new Set(records.map(({ objectTypeId }) => objectTypeId)).size !== 2) fail(`expected two native App::PropertyFont records, found ${records.length}`)
if (records.some(({ defaultRaw }) => defaultRaw !== 'osifont')) fail('locked default font changed')
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit },
propertyType: 'App::PropertyFont',
recordCount: records.length,
objectTypeCount: new Set(records.map(({ objectTypeId }) => objectTypeId)).size,
records,
storage: {
inheritedFrom: 'App::PropertyString',
xmlElement: 'String',
scalarEncoding: 'UTF-8 font family name',
fontResourceEmbedded: false,
browserBoundary: 'preserve the family name; browser font availability is presentation-only',
},
provenance: {
runtime: { path: runtimePath, bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).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-font-inventory-generated', output: 'config/freecad-property-font-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypeCount: report.objectTypeCount }, 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-font-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-font-inventory.json'),
load('config/freecad-property-font-success.json'),
load('config/freecad-property-font-failure.json'),
load('config/freecad-property-font-mutation.json'),
load('config/freecad-property-font-roundtrip.json'),
load('config/chrome-property-font-verification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyFont 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-propertyfont-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyFont' || inventory.recordCount !== 2 || inventory.objectTypeCount !== 2 || inventory.records?.filter(({ valueModel }) => valueModel?.writable === true).length !== 2) 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::PropertyFont')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyFont')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.fontResourceBoundary !== 'font family name persists in FCStd; font files and availability remain presentation-only' || chrome.persistence?.loadedType !== 'App::PropertyFont' || chrome.persistence.fcstdElement !== 'String' || chrome.persistence.fcstdValue !== 'Noto Sans CJK SC' || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1,
status: 'pass',
taskId: 'PROP-app-propertyfont-I',
baseline: semantics.baseline,
propertyType: 'App::PropertyFont',
recordCount: 2,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyfont-${phase}`).evidence])),
promotion: {
from: inventory.classification,
to: propertyType.support,
facadeValueModel: 'bounded-utf8-font-family-string-with-native-string-fcstd-payload',
writableRecords: 2,
objectTypeCount: propertyType.objectTypeCount,
fcstdElement: chrome.persistence.fcstdElement,
defaultValue: success.phases.default.value,
nativeRoundTripValue: roundTrip.classification.resavedValue,
browserRoundTripValue: chrome.persistence.loadedValue,
fontResourceBoundary: roundTrip.classification.fontResourceBoundary,
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-font-promotion-generated', output: 'config/freecad-property-font-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,64 @@
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 sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const outputPath = resolve(root, 'config/freecad-property-force-inventory.json')
const sourceContent = await readFile(sourcePath)
const source = JSON.parse(sourceContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyForce inventory generation failed: ${message}`) }
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
const records = []
for (const objectType of source.runtimeObjects?.types ?? []) {
for (const property of objectType.properties ?? []) {
if (property.typeId !== 'App::PropertyForce') continue
const output = JSON.stringify(property.status) === '[27]'
records.push({
objectTypeId: objectType.typeId,
objectAvailable: objectType.available === true,
probeStatus: objectType.probeStatus,
propertyName: property.name,
group: property.group,
status: property.status,
defaultRaw: property.default,
valueModel: {
kind: 'quantity',
dimension: 'mass*length/time^2',
internalUnit: 'mm*kg/s^2',
displayUnit: 'N',
newtonScale: 1000,
defaultValue: 0,
writable: !output,
derivedOutput: output,
acceptedBoundaryEvidence: 'inventory-only; value and boundary acceptance remain pending in phase B/C',
},
dependencies: [],
applicability: {
requiredObjectTypeId: objectType.typeId,
source: 'Document.supportedTypes runtime inventory',
},
})
}
}
if (records.length !== 4) fail(`expected four native App::PropertyForce records, found ${records.length}`)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
propertyType: 'App::PropertyForce',
recordCount: records.length,
writableRecordCount: records.filter(({ valueModel }) => valueModel.writable).length,
outputRecordCount: records.filter(({ valueModel }) => valueModel.derivedOutput).length,
records,
source: {
path: '.cache/freecad/reference-desktop.json',
bytes: sourceContent.length,
sha256: createHash('sha256').update(sourceContent).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: report.status, output: 'config/freecad-property-force-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, outputRecordCount: report.outputRecordCount }, null, 2))

View File

@@ -0,0 +1,67 @@
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-force-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-force-inventory.json'),
load('config/freecad-property-force-success.json'),
load('config/freecad-property-force-failure.json'),
load('config/freecad-property-force-mutation.json'),
load('config/freecad-property-force-roundtrip.json'),
load('config/chrome-property-force-verification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyForce 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-propertyforce-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyForce' || inventory.recordCount !== 4 || inventory.writableRecordCount !== 1 || inventory.outputRecordCount !== 3) 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::PropertyForce')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 4 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.join(',') !== 'PropOutput') fail('global property semantics did not promote App::PropertyForce')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.outputPropertiesPreserved !== true || chrome.persistence?.loadedType !== 'App::PropertyForce' || chrome.persistence.loadedValue !== 500000 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1,
status: 'pass',
taskId: 'PROP-app-propertyforce-I',
baseline: semantics.baseline,
propertyType: 'App::PropertyForce',
recordCount: 4,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyforce-${phase}`).evidence])),
promotion: {
from: inventory.classification,
to: propertyType.support,
facadeValueModel: 'finite-number-internal-mm-kg-per-s2-with-registered-force-units',
internalUnit: 'mm*kg/s^2',
displayUnits: ['N', 'kN', 'lbf'],
newtonScale: 1000,
writableRecords: inventory.writableRecordCount,
outputRecords: inventory.outputRecordCount,
fcstdElement: chrome.persistence.fcstdElement,
nativeRoundTripValue: roundTrip.classification.resavedValue,
browserRoundTripValue: chrome.persistence.loadedValue,
outputPropertiesPreserved: roundTrip.classification.outputPropertiesPreserved,
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-force-promotion-generated', output: 'config/freecad-property-force-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,64 @@
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 sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const outputPath = resolve(root, 'config/freecad-property-heatflux-inventory.json')
const sourceContent = await readFile(sourcePath)
const source = JSON.parse(sourceContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux inventory generation failed: ${message}`) }
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
const records = []
for (const objectType of source.runtimeObjects?.types ?? []) {
for (const property of objectType.properties ?? []) {
if (property.typeId !== 'App::PropertyHeatFlux') continue
records.push({
objectTypeId: objectType.typeId,
objectAvailable: objectType.available === true,
probeStatus: objectType.probeStatus,
propertyName: property.name,
group: property.group,
status: property.status,
defaultRaw: property.default,
valueModel: {
kind: 'quantity',
dimension: 'mass/time^3',
internalUnit: 'kg/s^3',
displayUnit: 'W/m^2',
wattsPerSquareMeterScale: 1,
defaultValue: 0,
writable: true,
derivedOutput: false,
acceptedBoundaryEvidence: 'inventory-only; value and boundary acceptance remain pending in phase B/C',
},
dependencies: [],
applicability: {
requiredObjectTypeId: objectType.typeId,
source: 'Document.supportedTypes runtime inventory',
},
})
}
}
if (records.length !== 1) fail(`expected one native App::PropertyHeatFlux record, found ${records.length}`)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
propertyType: 'App::PropertyHeatFlux',
recordCount: records.length,
writableRecordCount: records.filter(({ valueModel }) => valueModel.writable).length,
outputRecordCount: records.filter(({ valueModel }) => valueModel.derivedOutput).length,
records,
source: {
path: '.cache/freecad/reference-desktop.json',
bytes: sourceContent.length,
sha256: createHash('sha256').update(sourceContent).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: report.status, output: 'config/freecad-property-heatflux-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount, outputRecordCount: report.outputRecordCount }, null, 2))

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-heatflux-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-heatflux-inventory.json'),
load('config/freecad-property-heatflux-success.json'),
load('config/freecad-property-heatflux-failure.json'),
load('config/freecad-property-heatflux-mutation.json'),
load('config/freecad-property-heatflux-roundtrip.json'),
load('config/chrome-property-heatflux-verification.json'),
load('config/freecad-native-property-semantics.json'),
load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyHeatFlux 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-propertyheatflux-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyHeatFlux' || inventory.recordCount !== 1 || inventory.writableRecordCount !== 1 || inventory.outputRecordCount !== 0) 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::PropertyHeatFlux')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1 || propertyType.objectTypeCount !== 1 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyHeatFlux')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.nativeValueMatches !== true || chrome.persistence?.loadedType !== 'App::PropertyHeatFlux' || chrome.persistence.loadedValue !== 500 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1,
status: 'pass',
taskId: 'PROP-app-propertyheatflux-I',
baseline: semantics.baseline,
propertyType: 'App::PropertyHeatFlux',
recordCount: 1,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyheatflux-${phase}`).evidence])),
promotion: {
from: inventory.classification,
to: propertyType.support,
facadeValueModel: 'finite-number-internal-kg-per-s3-with-registered-heat-flux-units',
internalUnit: 'kg/s^3',
displayUnits: ['W/m^2', 'kW/m^2', 'W/mm^2'],
wattsPerSquareMeterScale: 1,
writableRecords: inventory.writableRecordCount,
outputRecords: inventory.outputRecordCount,
fcstdElement: chrome.persistence.fcstdElement,
nativeRoundTripValue: roundTrip.classification.resavedValue,
browserRoundTripValue: chrome.persistence.loadedValue,
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-heatflux-promotion-generated', output: 'config/freecad-property-heatflux-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,44 @@
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 sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const outputPath = resolve(root, 'config/freecad-property-integerset-inventory.json')
const sourceContent = await readFile(sourcePath)
const source = JSON.parse(sourceContent)
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet inventory generation failed: ${message}`) }
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
const records = []
for (const objectType of source.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) {
if (property.typeId !== 'App::PropertyIntegerSet') continue
records.push({
objectTypeId: objectType.typeId,
objectAvailable: objectType.available === true,
probeStatus: objectType.probeStatus,
propertyName: property.name,
group: property.group,
status: property.status,
defaultRaw: property.default,
valueModel: { kind: 'integer-set', writable: property.status.length === 0, elementType: 'signed-integer', cardinality: 'variable-unique-unordered', webRepresentation: 'sorted-safe-integer-array' },
dependencies: [],
applicability: { requiredObjectTypeId: objectType.typeId, source: 'Document.supportedTypes runtime inventory' },
})
}
records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`))
if (records.length !== 2) fail(`expected two native App::PropertyIntegerSet records, found ${records.length}`)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
propertyType: 'App::PropertyIntegerSet',
recordCount: records.length,
writableRecordCount: records.filter(({ valueModel }) => valueModel.writable).length,
records,
source: { path: '.cache/freecad/reference-desktop.json', bytes: sourceContent.length, sha256: createHash('sha256').update(sourceContent).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: report.status, output: 'config/freecad-property-integerset-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, writableRecordCount: report.writableRecordCount }, null, 2))

View File

@@ -0,0 +1,31 @@
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-integerset-promotion.json')
const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([
load('config/freecad-property-integerset-inventory.json'), load('config/freecad-property-integerset-success.json'), load('config/freecad-property-integerset-failure.json'), load('config/freecad-property-integerset-mutation.json'), load('config/freecad-property-integerset-roundtrip.json'), load('config/chrome-property-integerset-verification.json'), load('config/freecad-native-property-semantics.json'), load('config/freecad-follow-up-task-progress.json'),
])
const fail = (message) => { throw new Error(`FreeCAD PropertyIntegerSet 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-propertyintegerset-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`)
if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyIntegerSet' || inventory.recordCount !== 2 || inventory.writableRecordCount !== 2) 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::PropertyIntegerSet')
if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 2 || propertyType.objectTypeCount !== 2 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyIntegerSet')
const opaque = semantics.supportSummary?.['opaque-fcstd-proxy']
const editable = semantics.supportSummary?.['native-editable-codec']
if (opaque?.typeCount !== 46 || opaque.recordCount !== 457 || editable?.typeCount !== 34 || editable.recordCount !== 4370) fail('global property support summary is not synchronized')
if (roundTrip.classification?.zeroUnknownDrift !== true || chrome.persistence?.loadedType !== 'App::PropertyIntegerSet' || chrome.persistence.fcstdElement !== 'IntegerSet' || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete')
const report = {
schemaVersion: 1, status: 'pass', taskId: 'PROP-app-propertyintegerset-I', baseline: semantics.baseline, propertyType: 'App::PropertyIntegerSet', recordCount: 2,
phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertyintegerset-${phase}`).evidence])),
promotion: { from: inventory.classification, to: propertyType.support, facadeValueModel: 'sorted-unique-safe-integer-array', writableRecords: 2, nativeEmptyBindingBoundary: success.bindingBoundary, fcstdElement: chrome.persistence.fcstdElement, nativeRoundTripValue: roundTrip.classification.resavedValue, browserRoundTripValue: chrome.persistence.loadedValue, 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-integerset-promotion-generated', output: 'config/freecad-property-integerset-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2))

View File

@@ -0,0 +1,61 @@
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-font-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-font-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-font')
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-font-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,40 @@
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-force-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-force-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-force')
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-force-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,41 @@
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-heatflux-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-heatflux-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-heatflux')
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-heatflux-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,62 @@
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-integerset-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-integerset-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-integerset')
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-integerset-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

@@ -15,11 +15,11 @@ const runtime = source.runtimeObjects
if (!runtime?.available || runtime.candidateCount !== 352 || runtime.availableCount !== 348 || runtime.unavailableCount !== 4 || !Array.isArray(runtime.types)) fail('runtime Document.supportedTypes inventory is incomplete')
const editableTypes = new Set([
'App::PropertyAcceleration', 'App::PropertyAngle', 'App::PropertyArea', 'App::PropertyBool', 'App::PropertyBoolList', 'App::PropertyColor', 'App::PropertyColorList', 'App::PropertyDistance', 'App::PropertyEnumeration',
'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
'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::PropertyPlacement', 'App::PropertyString',
'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector', 'App::PropertyDirection', 'App::PropertyFile', 'App::PropertyFileIncluded',
'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector', 'App::PropertyDirection', 'App::PropertyFile', 'App::PropertyFileIncluded', 'App::PropertyFont',
])
const specializedTypes = new Set([
'App::PropertyExpressionEngine', 'Part::PropertyGeometryList', 'Part::PropertyPartShape',

View File

@@ -0,0 +1,19 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-font-failure.json')
const scriptPath = resolve(root, 'scripts/freecad-property-font-failure.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyFont failure probe 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: 16 * 1024 * 1024, env: { ...process.env, 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_FONT_FAILURE_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyFont failure probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-font-failure.json', failure: report.failure, transaction: report.transaction, cancellationBoundary: report.cancellationBoundary }, null, 2))

View File

@@ -0,0 +1,19 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-font-mutation.json')
const scriptPath = resolve(root, 'scripts/freecad-property-font-mutation.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyFont mutation probe 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: 16 * 1024 * 1024, env: { ...process.env, 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_FONT_MUTATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyFont mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-font-mutation.json', classification: report.classification }, null, 2))

View File

@@ -0,0 +1,105 @@
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'
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-font-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-font-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-font-roundtrip.json')
const initial = 'osifont'
const target = 'Noto Sans CJK SC'
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyFont 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_FONT_ROUNDTRIP_MODE: mode,
FREECAD_PROPERTY_FONT_ROUNDTRIP_PATH: path,
FREECAD_PROPERTY_FONT_ROUNDTRIP_RESAVED_PATH: resaved,
FREECAD_PROPERTY_FONT_ROUNDTRIP_INITIAL: initial,
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_FONT_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyFont ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
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 === 'FontProbe')?.properties.find((candidate) => candidate.name === 'Font')
const editedBytes = facade.project.fcstd.rewriteFont(nativeBytes, { objectName: 'FontProbe', propertyName: 'Font', 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 === 'FontProbe' ? object.properties.filter((candidate) => candidate.name !== 'Font') : object.properties }))
const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperty(inspectedBefore), withoutEditedProperty(inspectedAfter))
const editedDocumentXml = new TextDecoder().decode(editedFiles['Document.xml'])
const targetMarkedTouched = /<Object(?=[^>]*name="FontProbe")(?=[^>]*Touched="1")[^>]*\/>/.test(editedDocumentXml)
const decodedAfter = property(inspectedAfter) ? decodeFcstdPropertyValue(property(inspectedAfter)!) : undefined
const webOutputMatches = decodedAfter?.decoded === true && decodedAfter.value === target
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const nativeSnapshots = [nativeAfter.result.reopened, nativeAfter.result.resaved]
const nativeOutputMatches = nativeSnapshots.every((snapshot: any) => snapshot.object.value === target)
const objectSetPreserved = nativeSnapshots.every((snapshot: any) => isDeepStrictEqual(native.result.objectSet, snapshot.objectSet))
const nativeStateStable = isDeepStrictEqual(nativeAfter.result.reopened, nativeAfter.result.resaved)
const zeroUnknownDrift = opaqueEntriesPreserved && semanticObjectsPreserved && targetMarkedTouched && webOutputMatches && nativeOutputMatches && objectSetPreserved && nativeStateStable
const archive = (path: string, bytes: Uint8Array) => ({ path, bytes: bytes.byteLength, sha256: createHash('sha256').update(bytes).digest('hex') })
const report = {
schemaVersion: 1,
status: zeroUnknownDrift ? 'pass' : 'fail',
baselineId: 'freecad-1.1.1-property-font-roundtrip',
freecadVersion: native.freecadVersion,
gitCommit: native.gitCommit,
propertyType: 'App::PropertyFont',
archives: {
nativeInitial: archive('.cache/freecad/property-font-roundtrip/native-initial.FCStd', nativeBytes),
webEdited: archive('.cache/freecad/property-font-roundtrip/web-edited.FCStd', editedBytes),
nativeResaved: archive('.cache/freecad/property-font-roundtrip/native-resaved.FCStd', resavedBytes),
},
nativeInitial: native.result,
web: { before: property(inspectedBefore), after: property(inspectedAfter), targetMarkedTouched, webOutputMatches, semanticObjectsPreserved, opaquePathsPreserved: opaquePaths.length, opaqueEntriesPreserved },
nativeAfter: nativeAfter.result,
classification: {
requestedValue: target,
webValue: decodedAfter?.value,
reopenedValue: nativeAfter.result.reopened.object.value,
resavedValue: nativeAfter.result.resaved.object.value,
nativeOutputMatches,
objectSetPreserved,
nativeStateStable,
fontResourceBoundary: 'font family name persists in FCStd; font files and availability remain presentation-only',
unknownSemanticDrift: !zeroUnknownDrift,
zeroUnknownDrift,
},
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-font-roundtrip.json', values: report.classification, opaqueEntriesPreserved }, null, 2))

View File

@@ -0,0 +1,19 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-font-success.json')
const scriptPath = resolve(root, 'scripts/freecad-property-font-success.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyFont success probe 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: 16 * 1024 * 1024, env: { ...process.env, 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_FONT_SUCCESS_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyFont success probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-font-success.json', phases: Object.keys(report.phases || {}), property: report.property }, null, 2))

View File

@@ -0,0 +1,29 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-force-failure.json')
const scriptPath = resolve(root, 'scripts/freecad-property-force-failure.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyForce failure probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_FORCE_FAILURE_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyForce failure probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-force-failure.json', failures: report.failures.length, disabled: report.disabled.classification, outputBoundary: report.outputBoundary.classification, transactionRestored: report.transaction.restored }, null, 2))

View File

@@ -0,0 +1,29 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-force-mutation.json')
const scriptPath = resolve(root, 'scripts/freecad-property-force-mutation.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyForce mutation probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_FORCE_MUTATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyForce mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-force-mutation.json', recompute: report.recompute, classification: report.classification }, null, 2))

View File

@@ -0,0 +1,92 @@
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 { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
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-force-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-force-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-force-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyForce 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_FORCE_ROUNDTRIP_MODE: mode,
FREECAD_PROPERTY_FORCE_ROUNDTRIP_PATH: path,
FREECAD_PROPERTY_FORCE_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_FORCE_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyForce ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
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 editedBytes = facade.project.fcstd.rewriteNumeric(nativeBytes, { objectName: 'ForceProbe', propertyName: 'Force', typeId: 'App::PropertyForce', value: 500000, expectedValue: 0 })
await writeFile(webPath, editedBytes)
const inspectedAfter = facade.project.fcstd.inspect(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 normalizeInspection = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.properties.map((property) => object.name === 'ForceProbe' && property.name === 'Force' ? { ...property, value: '<edited>' } : property) }))
const semanticObjectsPreserved = JSON.stringify(normalizeInspection(inspectedBefore)) === JSON.stringify(normalizeInspection(inspectedAfter))
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const nativeOutputMatches = [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot) => snapshot.force.property.value.numeric === 500000 && snapshot.rigid.outputs.every((output: { value: { numeric: number }, status: string[] }) => output.value.numeric === 0 && JSON.stringify(output.status) === '["27"]'))
const report = {
schemaVersion: 1,
status: 'pass',
baselineId: 'freecad-1.1.1-property-force-roundtrip',
freecadVersion: native.freecadVersion,
gitCommit: native.gitCommit,
archives: {
nativeInitial: { path: '.cache/freecad/property-force-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') },
webEdited: { path: '.cache/freecad/property-force-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') },
nativeResaved: { path: '.cache/freecad/property-force-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') },
},
nativeInitial: native.result,
web: {
before: inspectedBefore.objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force'),
after: inspectedAfter.objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force'),
objectSetPreserved: inspectedBefore.objects.map(({ name, typeId }) => `${name}:${typeId}`).join('|') === inspectedAfter.objects.map(({ name, typeId }) => `${name}:${typeId}`).join('|'),
semanticObjectsPreserved,
opaquePathsPreserved: opaquePaths.length,
opaqueEntriesPreserved,
},
nativeAfter: nativeAfter.result,
classification: {
webValue: Number(inspectedAfter.objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force')?.value),
reopenedValue: nativeAfter.result.reopened.force.property.value.numeric,
resavedValue: nativeAfter.result.resaved.force.property.value.numeric,
outputPropertiesPreserved: nativeOutputMatches,
nativeOutputMatches,
unknownSemanticDrift: !(opaqueEntriesPreserved && semanticObjectsPreserved && nativeOutputMatches),
zeroUnknownDrift: opaqueEntriesPreserved && semanticObjectsPreserved && nativeOutputMatches,
},
}
await facade.project.dispose()
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-force-roundtrip.json', classification: report.classification, opaqueEntriesPreserved: report.web.opaqueEntriesPreserved }, null, 2))

View File

@@ -0,0 +1,29 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-force-success.json')
const scriptPath = resolve(root, 'scripts/freecad-property-force-success.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyForce success probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_FORCE_SUCCESS_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyForce success probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-force-success.json', cases: report.cases.length, outputProperties: report.outputProperties.length, property: report.property }, null, 2))

View File

@@ -0,0 +1,29 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-heatflux-failure.json')
const scriptPath = resolve(root, 'scripts/freecad-property-heatflux-failure.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyHeatFlux failure probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_HEATFLUX_FAILURE_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyHeatFlux failure probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-heatflux-failure.json', failures: report.failures.length, disabled: report.disabled.classification, transactionRestored: report.transaction.restored }, null, 2))

View File

@@ -0,0 +1,30 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-heatflux-mutation.json')
const scriptPath = resolve(root, 'scripts/freecad-property-heatflux-mutation.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyHeatFlux mutation probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_HEATFLUX_MUTATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyHeatFlux mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-heatflux-mutation.json', recompute: report.recompute, classification: report.classification }, null, 2))

View File

@@ -0,0 +1,92 @@
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 { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
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-heatflux-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-heatflux-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-heatflux-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyHeatFlux 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_HEATFLUX_ROUNDTRIP_MODE: mode,
FREECAD_PROPERTY_HEATFLUX_ROUNDTRIP_PATH: path,
FREECAD_PROPERTY_HEATFLUX_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_HEATFLUX_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyHeatFlux ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
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 editedBytes = facade.project.fcstd.rewriteNumeric(nativeBytes, { objectName: 'HeatFluxProbe', propertyName: 'DFlux', typeId: 'App::PropertyHeatFlux', value: 500, expectedValue: 0 })
await writeFile(webPath, editedBytes)
const inspectedAfter = facade.project.fcstd.inspect(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 normalizeInspection = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.properties.map((property) => object.name === 'HeatFluxProbe' && property.name === 'DFlux' ? { ...property, value: '<edited>' } : property) }))
const semanticObjectsPreserved = JSON.stringify(normalizeInspection(inspectedBefore)) === JSON.stringify(normalizeInspection(inspectedAfter))
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const nativeValueMatches = [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot) => snapshot.heatFlux.property.value.numeric === 500)
const report = {
schemaVersion: 1,
status: 'pass',
baselineId: 'freecad-1.1.1-property-heatflux-roundtrip',
freecadVersion: native.freecadVersion,
gitCommit: native.gitCommit,
archives: {
nativeInitial: { path: '.cache/freecad/property-heatflux-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') },
webEdited: { path: '.cache/freecad/property-heatflux-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') },
nativeResaved: { path: '.cache/freecad/property-heatflux-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') },
},
nativeInitial: native.result,
web: {
before: inspectedBefore.objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux'),
after: inspectedAfter.objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux'),
objectSetPreserved: inspectedBefore.objects.map(({ name, typeId }) => `${name}:${typeId}`).join('|') === inspectedAfter.objects.map(({ name, typeId }) => `${name}:${typeId}`).join('|'),
semanticObjectsPreserved,
opaquePathsPreserved: opaquePaths.length,
opaqueEntriesPreserved,
},
nativeAfter: nativeAfter.result,
classification: {
webValue: Number(inspectedAfter.objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux')?.value),
reopenedValue: nativeAfter.result.reopened.heatFlux.property.value.numeric,
resavedValue: nativeAfter.result.resaved.heatFlux.property.value.numeric,
nativeValueMatches,
unknownSemanticDrift: !(opaqueEntriesPreserved && semanticObjectsPreserved && nativeValueMatches),
zeroUnknownDrift: opaqueEntriesPreserved && semanticObjectsPreserved && nativeValueMatches,
},
}
await facade.project.dispose()
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-heatflux-roundtrip.json', classification: report.classification, opaqueEntriesPreserved: report.web.opaqueEntriesPreserved }, null, 2))

View File

@@ -0,0 +1,29 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { writeFile } from 'node:fs/promises'
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 outputPath = resolve(root, 'config/freecad-property-heatflux-success.json')
const scriptPath = resolve(root, 'scripts/freecad-property-heatflux-success.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyHeatFlux success probe 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: 16 * 1024 * 1024,
env: {
...process.env,
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_HEATFLUX_SUCCESS_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyHeatFlux success probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-heatflux-success.json', cases: report.cases.length, property: report.property }, 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-integerset-failure.py')
const outputPath = resolve(root, 'config/freecad-property-integerset-failure.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyIntegerSet failure 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_INTEGERSET_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_INTEGERSET_FAILURE_RESULT=')) throw new Error(`FreeCAD PropertyIntegerSet 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-integerset-failure.json', failures: report.failures.map(({ id, exception }) => ({ id, exception })), disabled: report.disabled.classification, transactionRestored: report.transaction.restored }, 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-integerset-mutation.py')
const outputPath = resolve(root, 'config/freecad-property-integerset-mutation.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyIntegerSet mutation 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_INTEGERSET_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_INTEGERSET_MUTATION_RESULT=')) throw new Error(`FreeCAD PropertyIntegerSet 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-integerset-mutation.json', caseCount: report.caseCount, cases: report.cases.map(({ objectTypeId, before, edited, restored }) => ({ objectTypeId, before: before.value, edited: edited.value, restored: restored.value })) }, null, 2))

View File

@@ -0,0 +1,70 @@
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'
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-integerset-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-integerset-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-integerset-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyIntegerSet 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_INTEGERSET_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_INTEGERSET_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_INTEGERSET_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_INTEGERSET_ROUNDTRIP_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyIntegerSet ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}
const target = [-1, 8]
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 editedBytes = facade.project.fcstd.rewriteIntegerSet(nativeBytes, { objectName: 'NodesProbe', propertyName: 'Nodes', value: [8, -1, 8], expectedValue: [1, 3] })
await facade.project.dispose()
await writeFile(webPath, editedBytes)
const inspectedAfter = facade.project.fcstd.inspect(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 === 'NodesProbe' ? object.properties.filter((property) => property.name !== 'Nodes') : object.properties }))
const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperty(inspectedBefore), withoutEditedProperty(inspectedAfter))
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const nativeOutputMatches = [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot: any) => isDeepStrictEqual(snapshot.object.value, target))
const objectSetPreserved = isDeepStrictEqual(native.result.objectSet, nativeAfter.result.reopened.objectSet) && isDeepStrictEqual(native.result.objectSet, nativeAfter.result.resaved.objectSet)
const zeroUnknownDrift = opaqueEntriesPreserved && semanticObjectsPreserved && nativeOutputMatches && objectSetPreserved
const property = (inspection: typeof inspectedBefore) => inspection.objects.find((object) => object.name === 'NodesProbe')?.properties.find((candidate) => candidate.name === 'Nodes')
const report = {
schemaVersion: 1,
status: zeroUnknownDrift ? 'pass' : 'fail',
baselineId: 'freecad-1.1.1-property-integerset-roundtrip',
freecadVersion: native.freecadVersion,
gitCommit: native.gitCommit,
archives: {
nativeInitial: { path: '.cache/freecad/property-integerset-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') },
webEdited: { path: '.cache/freecad/property-integerset-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') },
nativeResaved: { path: '.cache/freecad/property-integerset-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') },
},
nativeInitial: native.result,
web: { before: property(inspectedBefore), after: property(inspectedAfter), objectSetPreserved, semanticObjectsPreserved, opaquePathsPreserved: opaquePaths.length, opaqueEntriesPreserved },
nativeAfter: nativeAfter.result,
classification: { webValue: property(inspectedAfter)?.value, reopenedValue: nativeAfter.result.reopened.object.value, resavedValue: nativeAfter.result.resaved.object.value, nativeOutputMatches, unknownSemanticDrift: !zeroUnknownDrift, zeroUnknownDrift },
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-integerset-roundtrip.json', values: report.classification, opaqueEntriesPreserved }, 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-integerset-success.py')
const outputPath = resolve(root, 'config/freecad-property-integerset-success.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyIntegerSet 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_INTEGERSET_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_INTEGERSET_SUCCESS_RESULT=')) throw new Error(`FreeCAD PropertyIntegerSet 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-integerset-success.json', caseCount: report.caseCount, bindingBoundary: report.bindingBoundary }, null, 2))