From 8f6053089a78647b5a2190678817f60cdd43c728 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Mon, 17 Aug 2026 09:10:47 -0400 Subject: [PATCH] feat: promote font force heat flux and integer set properties --- chrome-property-font-harness.html | 5 + chrome-property-force-harness.html | 5 + chrome-property-heatflux-harness.html | 6 + chrome-property-integerset-harness.html | 6 + ...sign-reference-lifecycle-verification.json | 28 +- config/chrome-property-font-verification.json | 63 +++ .../chrome-property-force-verification.json | 64 +++ ...chrome-property-heatflux-verification.json | 64 +++ ...rome-property-integerset-verification.json | 74 ++++ config/freecad-active-work-queue.json | 62 +-- config/freecad-follow-up-task-progress.json | 235 +++++++++++ config/freecad-native-property-semantics.json | 24 +- ...eecad-property-acceleration-promotion.json | 12 +- config/freecad-property-area-promotion.json | 12 +- .../freecad-property-boollist-promotion.json | 12 +- config/freecad-property-color-promotion.json | 12 +- .../freecad-property-colorlist-promotion.json | 12 +- .../freecad-property-direction-promotion.json | 12 +- config/freecad-property-file-promotion.json | 12 +- ...eecad-property-fileincluded-promotion.json | 12 +- config/freecad-property-font-failure.json | 252 ++++++++++++ config/freecad-property-font-inventory.json | 140 +++++++ config/freecad-property-font-mutation.json | 84 ++++ config/freecad-property-font-promotion.json | 72 ++++ config/freecad-property-font-roundtrip.json | 121 ++++++ config/freecad-property-font-success.json | 118 ++++++ config/freecad-property-force-failure.json | 241 ++++++++++++ config/freecad-property-force-inventory.json | 136 +++++++ config/freecad-property-force-mutation.json | 298 ++++++++++++++ config/freecad-property-force-promotion.json | 79 ++++ config/freecad-property-force-roundtrip.json | 311 +++++++++++++++ config/freecad-property-force-success.json | 231 +++++++++++ config/freecad-property-heatflux-failure.json | 181 +++++++++ .../freecad-property-heatflux-inventory.json | 55 +++ .../freecad-property-heatflux-mutation.json | 358 +++++++++++++++++ .../freecad-property-heatflux-promotion.json | 78 ++++ .../freecad-property-heatflux-roundtrip.json | 145 +++++++ config/freecad-property-heatflux-success.json | 183 +++++++++ .../freecad-property-integerset-failure.json | 236 +++++++++++ ...freecad-property-integerset-inventory.json | 71 ++++ .../freecad-property-integerset-mutation.json | 369 ++++++++++++++++++ ...freecad-property-integerset-promotion.json | 80 ++++ ...freecad-property-integerset-roundtrip.json | 136 +++++++ .../freecad-property-integerset-success.json | 309 +++++++++++++++ config/freecad-web-exact-parity-plan.json | 2 +- config/qa04-fcstd-fuzz-verification.json | 4 +- config/qa04-geometry-fuzz-verification.json | 4 +- config/qa04-sketch-fuzz-verification.json | 4 +- config/release-artifacts.json | 14 +- ...ecad-active-work-status.generated.zh-CN.md | 8 +- package.json | 64 +++ scripts/check-chrome-property-font.mjs | 15 + scripts/check-chrome-property-force.mjs | 13 + scripts/check-chrome-property-heatflux.mjs | 13 + scripts/check-chrome-property-integerset.mjs | 15 + scripts/check-facade-boundary.mjs | 4 + ...heck-freecad-native-property-semantics.mjs | 14 +- ...reecad-property-acceleration-promotion.mjs | 4 +- .../check-freecad-property-area-promotion.mjs | 4 +- ...ck-freecad-property-boollist-promotion.mjs | 4 +- ...check-freecad-property-color-promotion.mjs | 4 +- ...k-freecad-property-colorlist-promotion.mjs | 4 +- ...k-freecad-property-direction-promotion.mjs | 4 +- .../check-freecad-property-file-promotion.mjs | 4 +- ...reecad-property-fileincluded-promotion.mjs | 4 +- .../check-freecad-property-font-failure.mjs | 18 + .../check-freecad-property-font-inventory.mjs | 36 ++ .../check-freecad-property-font-mutation.mjs | 16 + .../check-freecad-property-font-promotion.mjs | 31 ++ .../check-freecad-property-font-roundtrip.mjs | 19 + .../check-freecad-property-font-success.mjs | 17 + .../check-freecad-property-force-failure.mjs | 25 ++ ...check-freecad-property-force-inventory.mjs | 34 ++ .../check-freecad-property-force-mutation.mjs | 20 + ...check-freecad-property-force-promotion.mjs | 31 ++ ...check-freecad-property-force-roundtrip.mjs | 23 ++ .../check-freecad-property-force-success.mjs | 28 ++ ...heck-freecad-property-heatflux-failure.mjs | 23 ++ ...ck-freecad-property-heatflux-inventory.mjs | 26 ++ ...eck-freecad-property-heatflux-mutation.mjs | 20 + ...ck-freecad-property-heatflux-promotion.mjs | 31 ++ ...ck-freecad-property-heatflux-roundtrip.mjs | 22 ++ ...heck-freecad-property-heatflux-success.mjs | 26 ++ ...ck-freecad-property-integerset-failure.mjs | 28 ++ ...-freecad-property-integerset-inventory.mjs | 31 ++ ...k-freecad-property-integerset-mutation.mjs | 22 ++ ...-freecad-property-integerset-promotion.mjs | 21 + ...-freecad-property-integerset-roundtrip.mjs | 17 + ...ck-freecad-property-integerset-success.mjs | 24 ++ scripts/freecad-property-font-failure.py | 95 +++++ scripts/freecad-property-font-mutation.py | 51 +++ scripts/freecad-property-font-roundtrip.py | 81 ++++ scripts/freecad-property-font-success.py | 49 +++ scripts/freecad-property-force-failure.py | 154 ++++++++ scripts/freecad-property-force-mutation.py | 94 +++++ scripts/freecad-property-force-roundtrip.py | 101 +++++ scripts/freecad-property-force-success.py | 73 ++++ scripts/freecad-property-heatflux-failure.py | 130 ++++++ scripts/freecad-property-heatflux-mutation.py | 90 +++++ .../freecad-property-heatflux-roundtrip.py | 93 +++++ scripts/freecad-property-heatflux-success.py | 68 ++++ .../freecad-property-integerset-failure.py | 99 +++++ .../freecad-property-integerset-mutation.py | 74 ++++ .../freecad-property-integerset-roundtrip.py | 75 ++++ .../freecad-property-integerset-success.py | 80 ++++ ...reecad-property-acceleration-promotion.mjs | 4 +- ...nerate-freecad-property-area-promotion.mjs | 4 +- ...te-freecad-property-boollist-promotion.mjs | 4 +- ...erate-freecad-property-color-promotion.mjs | 4 +- ...e-freecad-property-colorlist-promotion.mjs | 4 +- ...e-freecad-property-direction-promotion.mjs | 4 +- ...nerate-freecad-property-file-promotion.mjs | 4 +- ...reecad-property-fileincluded-promotion.mjs | 4 +- ...nerate-freecad-property-font-inventory.mjs | 87 +++++ ...nerate-freecad-property-font-promotion.mjs | 65 +++ ...erate-freecad-property-force-inventory.mjs | 64 +++ ...erate-freecad-property-force-promotion.mjs | 67 ++++ ...te-freecad-property-heatflux-inventory.mjs | 64 +++ ...te-freecad-property-heatflux-promotion.mjs | 66 ++++ ...-freecad-property-integerset-inventory.mjs | 44 +++ ...-freecad-property-integerset-promotion.mjs | 31 ++ scripts/run-chrome-property-font.mjs | 61 +++ scripts/run-chrome-property-force.mjs | 40 ++ scripts/run-chrome-property-heatflux.mjs | 41 ++ scripts/run-chrome-property-integerset.mjs | 62 +++ .../run-freecad-native-property-semantics.mjs | 6 +- scripts/run-freecad-property-font-failure.mjs | 19 + .../run-freecad-property-font-mutation.mjs | 19 + .../run-freecad-property-font-roundtrip.ts | 105 +++++ scripts/run-freecad-property-font-success.mjs | 19 + .../run-freecad-property-force-failure.mjs | 29 ++ .../run-freecad-property-force-mutation.mjs | 29 ++ .../run-freecad-property-force-roundtrip.ts | 92 +++++ .../run-freecad-property-force-success.mjs | 29 ++ .../run-freecad-property-heatflux-failure.mjs | 29 ++ ...run-freecad-property-heatflux-mutation.mjs | 30 ++ ...run-freecad-property-heatflux-roundtrip.ts | 92 +++++ .../run-freecad-property-heatflux-success.mjs | 29 ++ ...un-freecad-property-integerset-failure.mjs | 16 + ...n-freecad-property-integerset-mutation.mjs | 16 + ...n-freecad-property-integerset-roundtrip.ts | 70 ++++ ...un-freecad-property-integerset-success.mjs | 16 + src/App.tsx | 4 +- src/chromePropertyFontHarness.tsx | 184 +++++++++ src/chromePropertyForceHarness.tsx | 128 ++++++ src/chromePropertyHeatFluxHarness.tsx | 128 ++++++ src/chromePropertyIntegerSetHarness.tsx | 190 +++++++++ src/facade/fcstd.ts | 139 ++++++- src/facade/mockFacade.ts | 22 +- src/facade/types.ts | 8 +- src/facade/units.ts | 10 +- tests/propertyFontFacade.test.ts | 80 ++++ tests/propertyFontTransaction.test.ts | 74 ++++ tests/propertyForceFacade.test.ts | 61 +++ tests/propertyForceTransaction.test.ts | 52 +++ tests/propertyHeatFluxFacade.test.ts | 47 +++ tests/propertyHeatFluxTransaction.test.ts | 44 +++ tests/propertyIntegerSetFacade.test.ts | 47 +++ tests/propertyIntegerSetTransaction.test.ts | 45 +++ 159 files changed, 9687 insertions(+), 163 deletions(-) create mode 100644 chrome-property-font-harness.html create mode 100644 chrome-property-force-harness.html create mode 100644 chrome-property-heatflux-harness.html create mode 100644 chrome-property-integerset-harness.html create mode 100644 config/chrome-property-font-verification.json create mode 100644 config/chrome-property-force-verification.json create mode 100644 config/chrome-property-heatflux-verification.json create mode 100644 config/chrome-property-integerset-verification.json create mode 100644 config/freecad-property-font-failure.json create mode 100644 config/freecad-property-font-inventory.json create mode 100644 config/freecad-property-font-mutation.json create mode 100644 config/freecad-property-font-promotion.json create mode 100644 config/freecad-property-font-roundtrip.json create mode 100644 config/freecad-property-font-success.json create mode 100644 config/freecad-property-force-failure.json create mode 100644 config/freecad-property-force-inventory.json create mode 100644 config/freecad-property-force-mutation.json create mode 100644 config/freecad-property-force-promotion.json create mode 100644 config/freecad-property-force-roundtrip.json create mode 100644 config/freecad-property-force-success.json create mode 100644 config/freecad-property-heatflux-failure.json create mode 100644 config/freecad-property-heatflux-inventory.json create mode 100644 config/freecad-property-heatflux-mutation.json create mode 100644 config/freecad-property-heatflux-promotion.json create mode 100644 config/freecad-property-heatflux-roundtrip.json create mode 100644 config/freecad-property-heatflux-success.json create mode 100644 config/freecad-property-integerset-failure.json create mode 100644 config/freecad-property-integerset-inventory.json create mode 100644 config/freecad-property-integerset-mutation.json create mode 100644 config/freecad-property-integerset-promotion.json create mode 100644 config/freecad-property-integerset-roundtrip.json create mode 100644 config/freecad-property-integerset-success.json create mode 100644 scripts/check-chrome-property-font.mjs create mode 100644 scripts/check-chrome-property-force.mjs create mode 100644 scripts/check-chrome-property-heatflux.mjs create mode 100644 scripts/check-chrome-property-integerset.mjs create mode 100644 scripts/check-freecad-property-font-failure.mjs create mode 100644 scripts/check-freecad-property-font-inventory.mjs create mode 100644 scripts/check-freecad-property-font-mutation.mjs create mode 100644 scripts/check-freecad-property-font-promotion.mjs create mode 100644 scripts/check-freecad-property-font-roundtrip.mjs create mode 100644 scripts/check-freecad-property-font-success.mjs create mode 100644 scripts/check-freecad-property-force-failure.mjs create mode 100644 scripts/check-freecad-property-force-inventory.mjs create mode 100644 scripts/check-freecad-property-force-mutation.mjs create mode 100644 scripts/check-freecad-property-force-promotion.mjs create mode 100644 scripts/check-freecad-property-force-roundtrip.mjs create mode 100644 scripts/check-freecad-property-force-success.mjs create mode 100644 scripts/check-freecad-property-heatflux-failure.mjs create mode 100644 scripts/check-freecad-property-heatflux-inventory.mjs create mode 100644 scripts/check-freecad-property-heatflux-mutation.mjs create mode 100644 scripts/check-freecad-property-heatflux-promotion.mjs create mode 100644 scripts/check-freecad-property-heatflux-roundtrip.mjs create mode 100644 scripts/check-freecad-property-heatflux-success.mjs create mode 100644 scripts/check-freecad-property-integerset-failure.mjs create mode 100644 scripts/check-freecad-property-integerset-inventory.mjs create mode 100644 scripts/check-freecad-property-integerset-mutation.mjs create mode 100644 scripts/check-freecad-property-integerset-promotion.mjs create mode 100644 scripts/check-freecad-property-integerset-roundtrip.mjs create mode 100644 scripts/check-freecad-property-integerset-success.mjs create mode 100644 scripts/freecad-property-font-failure.py create mode 100644 scripts/freecad-property-font-mutation.py create mode 100644 scripts/freecad-property-font-roundtrip.py create mode 100644 scripts/freecad-property-font-success.py create mode 100644 scripts/freecad-property-force-failure.py create mode 100644 scripts/freecad-property-force-mutation.py create mode 100644 scripts/freecad-property-force-roundtrip.py create mode 100644 scripts/freecad-property-force-success.py create mode 100644 scripts/freecad-property-heatflux-failure.py create mode 100644 scripts/freecad-property-heatflux-mutation.py create mode 100644 scripts/freecad-property-heatflux-roundtrip.py create mode 100644 scripts/freecad-property-heatflux-success.py create mode 100644 scripts/freecad-property-integerset-failure.py create mode 100644 scripts/freecad-property-integerset-mutation.py create mode 100644 scripts/freecad-property-integerset-roundtrip.py create mode 100644 scripts/freecad-property-integerset-success.py create mode 100644 scripts/generate-freecad-property-font-inventory.mjs create mode 100644 scripts/generate-freecad-property-font-promotion.mjs create mode 100644 scripts/generate-freecad-property-force-inventory.mjs create mode 100644 scripts/generate-freecad-property-force-promotion.mjs create mode 100644 scripts/generate-freecad-property-heatflux-inventory.mjs create mode 100644 scripts/generate-freecad-property-heatflux-promotion.mjs create mode 100644 scripts/generate-freecad-property-integerset-inventory.mjs create mode 100644 scripts/generate-freecad-property-integerset-promotion.mjs create mode 100644 scripts/run-chrome-property-font.mjs create mode 100644 scripts/run-chrome-property-force.mjs create mode 100644 scripts/run-chrome-property-heatflux.mjs create mode 100644 scripts/run-chrome-property-integerset.mjs create mode 100644 scripts/run-freecad-property-font-failure.mjs create mode 100644 scripts/run-freecad-property-font-mutation.mjs create mode 100644 scripts/run-freecad-property-font-roundtrip.ts create mode 100644 scripts/run-freecad-property-font-success.mjs create mode 100644 scripts/run-freecad-property-force-failure.mjs create mode 100644 scripts/run-freecad-property-force-mutation.mjs create mode 100644 scripts/run-freecad-property-force-roundtrip.ts create mode 100644 scripts/run-freecad-property-force-success.mjs create mode 100644 scripts/run-freecad-property-heatflux-failure.mjs create mode 100644 scripts/run-freecad-property-heatflux-mutation.mjs create mode 100644 scripts/run-freecad-property-heatflux-roundtrip.ts create mode 100644 scripts/run-freecad-property-heatflux-success.mjs create mode 100644 scripts/run-freecad-property-integerset-failure.mjs create mode 100644 scripts/run-freecad-property-integerset-mutation.mjs create mode 100644 scripts/run-freecad-property-integerset-roundtrip.ts create mode 100644 scripts/run-freecad-property-integerset-success.mjs create mode 100644 src/chromePropertyFontHarness.tsx create mode 100644 src/chromePropertyForceHarness.tsx create mode 100644 src/chromePropertyHeatFluxHarness.tsx create mode 100644 src/chromePropertyIntegerSetHarness.tsx create mode 100644 tests/propertyFontFacade.test.ts create mode 100644 tests/propertyFontTransaction.test.ts create mode 100644 tests/propertyForceFacade.test.ts create mode 100644 tests/propertyForceTransaction.test.ts create mode 100644 tests/propertyHeatFluxFacade.test.ts create mode 100644 tests/propertyHeatFluxTransaction.test.ts create mode 100644 tests/propertyIntegerSetFacade.test.ts create mode 100644 tests/propertyIntegerSetTransaction.test.ts diff --git a/chrome-property-font-harness.html b/chrome-property-font-harness.html new file mode 100644 index 0000000..85599c3 --- /dev/null +++ b/chrome-property-font-harness.html @@ -0,0 +1,5 @@ + + + Chrome PropertyFont harness +
Initializing PropertyFont harness.
+ diff --git a/chrome-property-force-harness.html b/chrome-property-force-harness.html new file mode 100644 index 0000000..fa2c5e9 --- /dev/null +++ b/chrome-property-force-harness.html @@ -0,0 +1,5 @@ + + + Chrome PropertyForce harness +
Initializing PropertyForce harness.
+ diff --git a/chrome-property-heatflux-harness.html b/chrome-property-heatflux-harness.html new file mode 100644 index 0000000..e0ec540 --- /dev/null +++ b/chrome-property-heatflux-harness.html @@ -0,0 +1,6 @@ + + + Chrome PropertyHeatFlux harness +
Initializing PropertyHeatFlux harness.
+ + diff --git a/chrome-property-integerset-harness.html b/chrome-property-integerset-harness.html new file mode 100644 index 0000000..2265840 --- /dev/null +++ b/chrome-property-integerset-harness.html @@ -0,0 +1,6 @@ + + + Chrome PropertyIntegerSet harness +
Initializing PropertyIntegerSet harness.
+ + diff --git a/config/chrome-partdesign-reference-lifecycle-verification.json b/config/chrome-partdesign-reference-lifecycle-verification.json index fc91400..fc6a90b 100644 --- a/config/chrome-partdesign-reference-lifecycle-verification.json +++ b/config/chrome-partdesign-reference-lifecycle-verification.json @@ -90,7 +90,7 @@ } ], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -139,7 +139,7 @@ "binder": { "support": [], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -237,7 +237,7 @@ } ], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -286,7 +286,7 @@ "binder": { "support": [], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -335,7 +335,7 @@ "binder": { "support": [], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -433,7 +433,7 @@ } ], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -531,7 +531,7 @@ } ], "area": 54, - "shapeId": "shape-mswzv1y3-3", + "shapeId": "shape-msx0x35t-3", "state": [ "Up-to-date" ], @@ -629,7 +629,7 @@ } ], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -727,7 +727,7 @@ } ], "area": 54, - "shapeId": "shape-mswzv1y3-3", + "shapeId": "shape-msx0x35t-3", "state": [ "Up-to-date" ], @@ -825,7 +825,7 @@ } ], "area": 54, - "shapeId": "shape-mswzv1y3-3", + "shapeId": "shape-msx0x35t-3", "state": [ "Up-to-date" ], @@ -923,7 +923,7 @@ } ], "area": 30, - "shapeId": "shape-mswzv1xd-1", + "shapeId": "shape-msx0x350-1", "state": [ "Up-to-date" ], @@ -1021,7 +1021,7 @@ } ], "area": 54, - "shapeId": "shape-mswzv1y3-3", + "shapeId": "shape-msx0x35t-3", "state": [ "Up-to-date" ], @@ -1059,8 +1059,8 @@ }, { "path": "src/facade/mockFacade.ts", - "bytes": 171839, - "sha256": "5124f81066ec2a292d4c770d9f48b4908e844ac874542f2af34c70c5514514a6" + "bytes": 172346, + "sha256": "100b84bd3ebfe1a620885d70e98e97c864f33f5d478126c8586cca6deddfd0ac" }, { "path": "src/facade/geometryRuntime.ts", diff --git a/config/chrome-property-font-verification.json b/config/chrome-property-font-verification.json new file mode 100644 index 0000000..ee9bcb1 --- /dev/null +++ b/config/chrome-property-font-verification.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": 1, + "status": "pass", + "browserId": "chrome", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "crossOriginIsolated": true, + "ui": { + "inputType": "text", + "label": "Font", + "beforeValue": "osifont", + "afterValue": "Noto Sans CJK SC", + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": "Noto Sans CJK SC", + "loadedType": "App::PropertyFont", + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "String", + "fcstdValue": "Noto Sans CJK SC" + }, + "resource": { + "byteLength": 34, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:36431/src/facade/persistenceWorker.ts?worker_file&type=module" + } + ], + "requestTypes": [ + "initialize", + "save-document", + "load-document", + "load-checkpoint", + "recovery-report", + "put-resource", + "get-resource", + "release-resource", + "get-resource", + "dispose" + ], + "terminated": [ + "bitbybit-persistence" + ] + }, + "release": { + "shapeCount": 0, + "kernelReferenceCount": 0, + "workerTerminated": true + } +} diff --git a/config/chrome-property-force-verification.json b/config/chrome-property-force-verification.json new file mode 100644 index 0000000..98af671 --- /dev/null +++ b/config/chrome-property-force-verification.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "status": "pass", + "browserId": "chrome", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "crossOriginIsolated": true, + "ui": { + "inputType": "number", + "label": "Force", + "unit": "mm*kg/s^2", + "beforeValue": 0, + "afterValue": 500000, + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": 500000, + "loadedType": "App::PropertyForce", + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "Float", + "fcstdValue": "500000" + }, + "resource": { + "byteLength": 35, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:42533/src/facade/persistenceWorker.ts?worker_file&type=module" + } + ], + "requestTypes": [ + "initialize", + "save-document", + "load-document", + "load-checkpoint", + "recovery-report", + "put-resource", + "get-resource", + "release-resource", + "get-resource", + "dispose" + ], + "terminated": [ + "bitbybit-persistence" + ] + }, + "release": { + "shapeCount": 0, + "kernelReferenceCount": 0, + "workerTerminated": true + } +} diff --git a/config/chrome-property-heatflux-verification.json b/config/chrome-property-heatflux-verification.json new file mode 100644 index 0000000..8bf4415 --- /dev/null +++ b/config/chrome-property-heatflux-verification.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "status": "pass", + "browserId": "chrome", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "crossOriginIsolated": true, + "ui": { + "inputType": "number", + "label": "DFlux", + "unit": "kg/s^3", + "beforeValue": 0, + "afterValue": 500, + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": 500, + "loadedType": "App::PropertyHeatFlux", + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "Float", + "fcstdValue": "500" + }, + "resource": { + "byteLength": 38, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:45625/src/facade/persistenceWorker.ts?worker_file&type=module" + } + ], + "requestTypes": [ + "initialize", + "save-document", + "load-document", + "load-checkpoint", + "recovery-report", + "put-resource", + "get-resource", + "release-resource", + "get-resource", + "dispose" + ], + "terminated": [ + "bitbybit-persistence" + ] + }, + "release": { + "shapeCount": 0, + "kernelReferenceCount": 0, + "workerTerminated": true + } +} diff --git a/config/chrome-property-integerset-verification.json b/config/chrome-property-integerset-verification.json new file mode 100644 index 0000000..75a7a1c --- /dev/null +++ b/config/chrome-property-integerset-verification.json @@ -0,0 +1,74 @@ +{ + "schemaVersion": 1, + "status": "pass", + "browserId": "chrome", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36", + "crossOriginIsolated": true, + "ui": { + "inputType": "text", + "label": "Nodes", + "beforeText": "1, 3", + "afterText": "-1, 8", + "beforeValue": [ + 1, + 3 + ], + "afterValue": [ + -1, + 8 + ], + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": [ + -1, + 8 + ], + "loadedType": "App::PropertyIntegerSet", + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "IntegerSet", + "fcstdValue": "[-1,8]" + }, + "resource": { + "byteLength": 34, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:41119/src/facade/persistenceWorker.ts?worker_file&type=module" + } + ], + "requestTypes": [ + "initialize", + "save-document", + "load-document", + "load-checkpoint", + "recovery-report", + "put-resource", + "get-resource", + "release-resource", + "get-resource", + "dispose" + ], + "terminated": [ + "bitbybit-persistence" + ] + }, + "release": { + "shapeCount": 0, + "kernelReferenceCount": 0, + "workerTerminated": true + } +} diff --git a/config/freecad-active-work-queue.json b/config/freecad-active-work-queue.json index f0df22f..ff64292 100644 --- a/config/freecad-active-work-queue.json +++ b/config/freecad-active-work-queue.json @@ -24,12 +24,12 @@ ], "followUpProgress": "config/freecad-follow-up-task-progress.json" }, - "nextTask": "PROP-app-propertyfont-A", + "nextTask": "PROP-app-propertyintegerset-A", "summary": { "tasks": 1756, - "completed": 559, + "completed": 586, "inProgress": 1, - "pending": 1196 + "pending": 1169 }, "milestones": [ { @@ -7602,7 +7602,7 @@ { "id": "PROP-app-propertyfont-A", "title": "Lock native inventory and preconditions: App::PropertyFont", - "status": "in_progress", + "status": "completed", "dependencies": [ "PROP-app-propertyfileincluded-I" ], @@ -7618,7 +7618,7 @@ { "id": "PROP-app-propertyfont-B", "title": "Capture native success evidence: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-A" ], @@ -7634,7 +7634,7 @@ { "id": "PROP-app-propertyfont-C", "title": "Capture native failure and cancel evidence: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-B" ], @@ -7650,7 +7650,7 @@ { "id": "PROP-app-propertyfont-D", "title": "Capture parameter mutation and restore evidence: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-C" ], @@ -7666,7 +7666,7 @@ { "id": "PROP-app-propertyfont-E", "title": "Implement the Facade capability: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-D" ], @@ -7682,7 +7682,7 @@ { "id": "PROP-app-propertyfont-F", "title": "Close transaction and recovery behavior: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-E" ], @@ -7698,7 +7698,7 @@ { "id": "PROP-app-propertyfont-G", "title": "Close FCStd save, reopen and resave: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-F" ], @@ -7714,7 +7714,7 @@ { "id": "PROP-app-propertyfont-H", "title": "Replay in a real browser: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-G" ], @@ -7730,7 +7730,7 @@ { "id": "PROP-app-propertyfont-I", "title": "Promote capability and synchronize blockers: App::PropertyFont", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-H" ], @@ -7747,7 +7747,7 @@ { "id": "PROP-app-propertyforce-A", "title": "Lock native inventory and preconditions: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyfont-I" ], @@ -7763,7 +7763,7 @@ { "id": "PROP-app-propertyforce-B", "title": "Capture native success evidence: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-A" ], @@ -7779,7 +7779,7 @@ { "id": "PROP-app-propertyforce-C", "title": "Capture native failure and cancel evidence: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-B" ], @@ -7795,7 +7795,7 @@ { "id": "PROP-app-propertyforce-D", "title": "Capture parameter mutation and restore evidence: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-C" ], @@ -7811,7 +7811,7 @@ { "id": "PROP-app-propertyforce-E", "title": "Implement the Facade capability: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-D" ], @@ -7827,7 +7827,7 @@ { "id": "PROP-app-propertyforce-F", "title": "Close transaction and recovery behavior: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-E" ], @@ -7843,7 +7843,7 @@ { "id": "PROP-app-propertyforce-G", "title": "Close FCStd save, reopen and resave: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-F" ], @@ -7859,7 +7859,7 @@ { "id": "PROP-app-propertyforce-H", "title": "Replay in a real browser: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-G" ], @@ -7875,7 +7875,7 @@ { "id": "PROP-app-propertyforce-I", "title": "Promote capability and synchronize blockers: App::PropertyForce", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-H" ], @@ -7892,7 +7892,7 @@ { "id": "PROP-app-propertyheatflux-A", "title": "Lock native inventory and preconditions: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyforce-I" ], @@ -7908,7 +7908,7 @@ { "id": "PROP-app-propertyheatflux-B", "title": "Capture native success evidence: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-A" ], @@ -7924,7 +7924,7 @@ { "id": "PROP-app-propertyheatflux-C", "title": "Capture native failure and cancel evidence: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-B" ], @@ -7940,7 +7940,7 @@ { "id": "PROP-app-propertyheatflux-D", "title": "Capture parameter mutation and restore evidence: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-C" ], @@ -7956,7 +7956,7 @@ { "id": "PROP-app-propertyheatflux-E", "title": "Implement the Facade capability: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-D" ], @@ -7972,7 +7972,7 @@ { "id": "PROP-app-propertyheatflux-F", "title": "Close transaction and recovery behavior: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-E" ], @@ -7988,7 +7988,7 @@ { "id": "PROP-app-propertyheatflux-G", "title": "Close FCStd save, reopen and resave: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-F" ], @@ -8004,7 +8004,7 @@ { "id": "PROP-app-propertyheatflux-H", "title": "Replay in a real browser: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-G" ], @@ -8020,7 +8020,7 @@ { "id": "PROP-app-propertyheatflux-I", "title": "Promote capability and synchronize blockers: App::PropertyHeatFlux", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertyheatflux-H" ], @@ -8037,7 +8037,7 @@ { "id": "PROP-app-propertyintegerset-A", "title": "Lock native inventory and preconditions: App::PropertyIntegerSet", - "status": "pending", + "status": "in_progress", "dependencies": [ "PROP-app-propertyheatflux-I" ], diff --git a/config/freecad-follow-up-task-progress.json b/config/freecad-follow-up-task-progress.json index 30fd93b..35cacfc 100644 --- a/config/freecad-follow-up-task-progress.json +++ b/config/freecad-follow-up-task-progress.json @@ -542,6 +542,241 @@ "config/freecad-property-fileincluded-promotion.json", "check:freecad-property-fileincluded-promotion" ] + }, + { + "id": "PROP-app-propertyfont-A", + "evidence": [ + "config/freecad-property-font-inventory.json", + "check:freecad-property-font-inventory" + ] + }, + { + "id": "PROP-app-propertyfont-B", + "evidence": [ + "config/freecad-property-font-success.json", + "check:freecad-property-font-success" + ] + }, + { + "id": "PROP-app-propertyfont-C", + "evidence": [ + "config/freecad-property-font-failure.json", + "check:freecad-property-font-failure" + ] + }, + { + "id": "PROP-app-propertyfont-D", + "evidence": [ + "config/freecad-property-font-mutation.json", + "check:freecad-property-font-mutation" + ] + }, + { + "id": "PROP-app-propertyfont-E", + "evidence": [ + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyFontFacade.test.ts", + "test:freecad-property-font-facade" + ] + }, + { + "id": "PROP-app-propertyfont-F", + "evidence": [ + "tests/propertyFontTransaction.test.ts", + "test:freecad-property-font-transaction" + ] + }, + { + "id": "PROP-app-propertyfont-G", + "evidence": [ + "config/freecad-property-font-roundtrip.json", + "check:freecad-property-font-roundtrip" + ] + }, + { + "id": "PROP-app-propertyfont-H", + "evidence": [ + "config/chrome-property-font-verification.json", + "check:chrome-property-font" + ] + }, + { + "id": "PROP-app-propertyfont-I", + "evidence": [ + "config/freecad-property-font-promotion.json", + "check:freecad-property-font-promotion" + ] + }, + { + "id": "PROP-app-propertyforce-A", + "evidence": [ + "config/freecad-property-force-inventory.json", + "check:freecad-property-force-inventory" + ] + }, + { + "id": "PROP-app-propertyforce-B", + "evidence": [ + "config/freecad-property-force-success.json", + "check:freecad-property-force-success" + ] + }, + { + "id": "PROP-app-propertyforce-C", + "evidence": [ + "config/freecad-property-force-failure.json", + "check:freecad-property-force-failure" + ] + }, + { + "id": "PROP-app-propertyforce-D", + "evidence": [ + "config/freecad-property-force-mutation.json", + "check:freecad-property-force-mutation" + ] + }, + { + "id": "PROP-app-propertyforce-E", + "evidence": [ + "src/facade/units.ts", + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyForceFacade.test.ts", + "test:freecad-property-force-facade" + ] + }, + { + "id": "PROP-app-propertyforce-F", + "evidence": [ + "tests/propertyForceTransaction.test.ts", + "test:freecad-property-force-transaction" + ] + }, + { + "id": "PROP-app-propertyforce-G", + "evidence": [ + "config/freecad-property-force-roundtrip.json", + "check:freecad-property-force-roundtrip" + ] + }, + { + "id": "PROP-app-propertyforce-H", + "evidence": [ + "config/chrome-property-force-verification.json", + "check:chrome-property-force" + ] + }, + { + "id": "PROP-app-propertyforce-I", + "evidence": [ + "config/freecad-property-force-promotion.json", + "check:freecad-property-force-promotion" + ] + }, + { + "id": "PROP-app-propertyheatflux-A", + "evidence": [ + "config/freecad-property-heatflux-inventory.json", + "check:freecad-property-heatflux-inventory" + ] + }, + { + "id": "PROP-app-propertyheatflux-B", + "evidence": [ + "config/freecad-property-heatflux-success.json", + "check:freecad-property-heatflux-success" + ] + }, + { + "id": "PROP-app-propertyheatflux-C", + "evidence": [ + "config/freecad-property-heatflux-failure.json", + "check:freecad-property-heatflux-failure" + ] + }, + { + "id": "PROP-app-propertyheatflux-D", + "evidence": [ + "config/freecad-property-heatflux-mutation.json", + "check:freecad-property-heatflux-mutation" + ] + }, + { + "id": "PROP-app-propertyheatflux-E", + "evidence": [ + "src/facade/units.ts", + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyHeatFluxFacade.test.ts", + "test:freecad-property-heatflux-facade" + ] + }, + { + "id": "PROP-app-propertyheatflux-F", + "evidence": [ + "tests/propertyHeatFluxTransaction.test.ts", + "test:freecad-property-heatflux-transaction" + ] + }, + { + "id": "PROP-app-propertyheatflux-G", + "evidence": [ + "config/freecad-property-heatflux-roundtrip.json", + "check:freecad-property-heatflux-roundtrip" + ] + }, + { + "id": "PROP-app-propertyheatflux-H", + "evidence": [ + "config/chrome-property-heatflux-verification.json", + "check:chrome-property-heatflux" + ] + }, + { + "id": "PROP-app-propertyheatflux-I", + "evidence": [ + "config/freecad-property-heatflux-promotion.json", + "check:freecad-property-heatflux-promotion" + ] + }, + { + "id": "PROP-app-propertyintegerset-A", + "evidence": ["config/freecad-property-integerset-inventory.json", "check:freecad-property-integerset-inventory"] + }, + { + "id": "PROP-app-propertyintegerset-B", + "evidence": ["config/freecad-property-integerset-success.json", "check:freecad-property-integerset-success"] + }, + { + "id": "PROP-app-propertyintegerset-C", + "evidence": ["config/freecad-property-integerset-failure.json", "check:freecad-property-integerset-failure"] + }, + { + "id": "PROP-app-propertyintegerset-D", + "evidence": ["config/freecad-property-integerset-mutation.json", "check:freecad-property-integerset-mutation"] + }, + { + "id": "PROP-app-propertyintegerset-E", + "evidence": ["src/facade/types.ts", "src/facade/fcstd.ts", "src/facade/mockFacade.ts", "src/App.tsx", "tests/propertyIntegerSetFacade.test.ts", "test:freecad-property-integerset-facade"] + }, + { + "id": "PROP-app-propertyintegerset-F", + "evidence": ["tests/propertyIntegerSetTransaction.test.ts", "test:freecad-property-integerset-transaction"] + }, + { + "id": "PROP-app-propertyintegerset-G", + "evidence": ["config/freecad-property-integerset-roundtrip.json", "check:freecad-property-integerset-roundtrip"] + }, + { + "id": "PROP-app-propertyintegerset-H", + "evidence": ["config/chrome-property-integerset-verification.json", "check:chrome-property-integerset"] } ] } diff --git a/config/freecad-native-property-semantics.json b/config/freecad-native-property-semantics.json index 7f264da..e15dd73 100644 --- a/config/freecad-native-property-semantics.json +++ b/config/freecad-native-property-semantics.json @@ -37,16 +37,16 @@ }, "supportSummary": { "native-editable-codec": { - "typeCount": 30, - "recordCount": 4361 + "typeCount": 34, + "recordCount": 4370 }, "native-specialized-codec": { "typeCount": 5, "recordCount": 683 }, "opaque-fcstd-proxy": { - "typeCount": 50, - "recordCount": 466 + "typeCount": 46, + "recordCount": 457 } }, "types": [ @@ -215,14 +215,14 @@ }, { "typeId": "App::PropertyFont", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 2, "objectTypeCount": 2, "statusNames": [] }, { "typeId": "App::PropertyForce", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 4, "objectTypeCount": 2, "statusNames": [ @@ -231,7 +231,7 @@ }, { "typeId": "App::PropertyHeatFlux", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 1, "objectTypeCount": 1, "statusNames": [] @@ -271,7 +271,7 @@ }, { "typeId": "App::PropertyIntegerSet", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 2, "objectTypeCount": 2, "statusNames": [] @@ -902,10 +902,10 @@ }, "harness": { "path": "scripts/run-freecad-native-property-semantics.mjs", - "bytes": 6615, - "sha256": "fad232fd5a013d40639069df2f8752f04fc5d373c6a7a7ece8ed3276612af9f3" + "bytes": 6710, + "sha256": "7362b5e31dfcfba144c78f525cf5e1c94ab687e2a57dd34118d4428e79867804" }, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics", - "generatedAt": "2026-08-17T08:49:24.621Z" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics", + "generatedAt": "2026-08-17T13:03:27.855Z" } diff --git a/config/freecad-property-acceleration-promotion.json b/config/freecad-property-acceleration-promotion.json index ad2b8f7..bcc82d6 100644 --- a/config/freecad-property-acceleration-promotion.json +++ b/config/freecad-property-acceleration-promotion.json @@ -54,13 +54,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.800Z" + "generatedAt": "2026-08-17T13:03:42.988Z" } diff --git a/config/freecad-property-area-promotion.json b/config/freecad-property-area-promotion.json index 39bbf14..7b8f095 100644 --- a/config/freecad-property-area-promotion.json +++ b/config/freecad-property-area-promotion.json @@ -59,13 +59,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.776Z" + "generatedAt": "2026-08-17T13:03:43.029Z" } diff --git a/config/freecad-property-boollist-promotion.json b/config/freecad-property-boollist-promotion.json index 3b28bd2..7ac6764 100644 --- a/config/freecad-property-boollist-promotion.json +++ b/config/freecad-property-boollist-promotion.json @@ -68,13 +68,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.825Z" + "generatedAt": "2026-08-17T13:03:43.058Z" } diff --git a/config/freecad-property-color-promotion.json b/config/freecad-property-color-promotion.json index 75b5161..14c2b35 100644 --- a/config/freecad-property-color-promotion.json +++ b/config/freecad-property-color-promotion.json @@ -68,13 +68,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.829Z" + "generatedAt": "2026-08-17T13:03:43.033Z" } diff --git a/config/freecad-property-colorlist-promotion.json b/config/freecad-property-colorlist-promotion.json index ba555d6..5b9b80d 100644 --- a/config/freecad-property-colorlist-promotion.json +++ b/config/freecad-property-colorlist-promotion.json @@ -85,13 +85,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.826Z" + "generatedAt": "2026-08-17T13:03:42.988Z" } diff --git a/config/freecad-property-direction-promotion.json b/config/freecad-property-direction-promotion.json index 26a19f3..589a65f 100644 --- a/config/freecad-property-direction-promotion.json +++ b/config/freecad-property-direction-promotion.json @@ -65,13 +65,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.839Z" + "generatedAt": "2026-08-17T13:03:43.033Z" } diff --git a/config/freecad-property-file-promotion.json b/config/freecad-property-file-promotion.json index 59675c0..b081e5b 100644 --- a/config/freecad-property-file-promotion.json +++ b/config/freecad-property-file-promotion.json @@ -59,13 +59,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.800Z" + "generatedAt": "2026-08-17T13:03:43.095Z" } diff --git a/config/freecad-property-fileincluded-promotion.json b/config/freecad-property-fileincluded-promotion.json index 653e495..485e6d2 100644 --- a/config/freecad-property-fileincluded-promotion.json +++ b/config/freecad-property-fileincluded-promotion.json @@ -64,13 +64,13 @@ "zeroUnknownDrift": true }, "exactBlockerSync": { - "nativeEditableTypes": 30, - "nativeEditableRecords": 4361, - "opaqueTypes": 50, - "opaqueRecords": 466, + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, "exactPromotionReady": false, - "exactBlocker": "50 runtime property types and 466 records remain opaque-only; complete native document and property semantics" + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" }, "systemExact": false, - "generatedAt": "2026-08-17T08:49:33.815Z" + "generatedAt": "2026-08-17T13:03:43.103Z" } diff --git a/config/freecad-property-font-failure.json b/config/freecad-property-font-failure.json new file mode 100644 index 0000000..a742d28 --- /dev/null +++ b/config/freecad-property-font-failure.json @@ -0,0 +1,252 @@ +{ + "baselineId": "freecad-1.1.1-property-font-failure", + "cancellationBoundary": { + "classification": "synchronous-property-setter", + "reason": "no-native-cancel-hook", + "replacement": "abort-active-document-transaction", + "supported": false + }, + "disabled": { + "after": { + "editorMode": [ + "ReadOnly" + ], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [ + "Immutable", + "ReadOnly" + ], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "afterObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "before": { + "editorMode": [ + "ReadOnly" + ], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [ + "Immutable", + "ReadOnly" + ], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "beforeObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "classification": "editor-read-only-and-python-immutable", + "editorMode": [ + "ReadOnly" + ], + "exception": { + "message": "Object attribute 'Font' is read-only", + "type": "AttributeError" + }, + "restoredEditorMode": [], + "restoredStatus": [], + "status": [ + "Immutable", + "ReadOnly" + ] + }, + "documentIntegrity": { + "finalObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "initialObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectCount": 1, + "objectsPreserved": true + }, + "failure": { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "afterObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "beforeObjects": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "exception": { + "message": "type must be str or unicode, not int", + "type": "TypeError" + }, + "id": "wrong-type", + "objectsPreserved": true, + "requested": 12345 + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "initial": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "object": { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + }, + "property": { + "name": "Font", + "typeId": "App::PropertyFont" + }, + "propertyType": "App::PropertyFont", + "schemaVersion": 1, + "status": "pass", + "transaction": { + "activeAfterAbort": { + "id": 0, + "name": "" + }, + "activeAfterEdit": { + "id": 1, + "name": "property-font-cancel" + }, + "afterAbort": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "osifont" + }, + "edited": { + "editorMode": [], + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "statusString": "Valid", + "value": "Transaction Font" + }, + "objectsAfter": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectsBefore": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectsRestored": true, + "pendingAfterAbort": false, + "pendingAfterEdit": true, + "restored": true, + "undoMode": 1 + } +} diff --git a/config/freecad-property-font-inventory.json b/config/freecad-property-font-inventory.json new file mode 100644 index 0000000..ee0a55b --- /dev/null +++ b/config/freecad-property-font-inventory.json @@ -0,0 +1,140 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyFont", + "recordCount": 2, + "objectTypeCount": 2, + "records": [ + { + "objectTypeId": "TechDraw::DrawViewAnnotation", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Font", + "group": "Annotation", + "status": [], + "defaultRaw": "osifont", + "valueModel": { + "kind": "font-family-string", + "representation": "App::PropertyString-derived UTF-8 font family name", + "writable": true, + "installedFontRequired": false + }, + "inputs": { + "accepted": [ + "utf8-string" + ], + "emptyStringAccepted": true, + "arbitraryFamilyNameAccepted": true + }, + "editor": "Gui::PropertyEditor::PropertyFontItem", + "editorModel": "QFontDatabase family list exposed through QComboBox", + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "TechDraw::DrawViewAnnotation", + "propertyWriteRequiresShape": false, + "source": "Document.supportedTypes runtime inventory" + } + }, + { + "objectTypeId": "TechDraw::DrawViewSpreadsheet", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Font", + "group": "Spreadsheet", + "status": [], + "defaultRaw": "osifont", + "valueModel": { + "kind": "font-family-string", + "representation": "App::PropertyString-derived UTF-8 font family name", + "writable": true, + "installedFontRequired": false + }, + "inputs": { + "accepted": [ + "utf8-string" + ], + "emptyStringAccepted": true, + "arbitraryFamilyNameAccepted": true + }, + "editor": "Gui::PropertyEditor::PropertyFontItem", + "editorModel": "QFontDatabase family list exposed through QComboBox", + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "TechDraw::DrawViewSpreadsheet", + "propertyWriteRequiresShape": false, + "source": "Document.supportedTypes runtime inventory" + } + } + ], + "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": ".cache/freecad/reference-desktop.json", + "bytes": 2298709, + "sha256": "082400ec7379e8efc2af16709032d45890d7092277967b714acb05761fd1c2a7" + }, + "sources": [ + { + "path": ".cache/freecad/FreeCAD/src/App/PropertyStandard.h", + "bytes": 37529, + "sha256": "3555ab386b4815329b09604bbd70626c2b24b66f90a7ff2c31052b55fd3c9244" + }, + { + "path": ".cache/freecad/FreeCAD/src/App/PropertyStandard.cpp", + "bytes": 99757, + "sha256": "eeefbbd7020c85c21e2e6b5f777088261668492845cf4d172e31f600fe9b96fd" + }, + { + "path": ".cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.h", + "bytes": 41826, + "sha256": "aaade89f5386b5d061b1b2d09a4fd01e7a27e3c601117fc00e44e115f347d9ed" + }, + { + "path": ".cache/freecad/FreeCAD/src/Gui/propertyeditor/PropertyItem.cpp", + "bytes": 142093, + "sha256": "bf2f7cb4c01034425f4ebd46f901ada03c5fa02dcffbc1d3936e49ef7bc43b52" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewAnnotation.h", + "bytes": 3471, + "sha256": "41361c931510ab4adc8348b1427bf80da150315c72b1bb6f99eb3d94ee396c18" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewAnnotation.cpp", + "bytes": 6635, + "sha256": "b86a2a1837584d036734924492a1768a88aed4a902e9c7e934b4d2f2d9a79e9b" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSpreadsheet.h", + "bytes": 3203, + "sha256": "2037de09272de8684cbf37d94e442561581595de9b6a42798581dfd2b7d98ac0" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/TechDraw/App/DrawViewSpreadsheet.cpp", + "bytes": 17747, + "sha256": "3256a14e826d91ea8eafe573b9282b42f164ff735408e9ba83daa8e50dfb28ec" + } + ] + }, + "classification": "opaque-fcstd-proxy", + "nextPhases": [ + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I" + ] +} diff --git a/config/freecad-property-font-mutation.json b/config/freecad-property-font-mutation.json new file mode 100644 index 0000000..89cdac0 --- /dev/null +++ b/config/freecad-property-font-mutation.json @@ -0,0 +1,84 @@ +{ + "baselineId": "freecad-1.1.1-property-font-mutation", + "classification": { + "geometryApplicability": "not-applicable-no-shape-property", + "objectSetStable": true, + "restoreExact": true, + "unknownSemanticDrift": false, + "valueChanged": true + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "object": { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + }, + "phases": { + "before": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": false, + "statusString": "Valid", + "value": "osifont" + }, + "edited": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "statusString": "Valid", + "value": "Liberation Sans" + }, + "restored": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "statusString": "Valid", + "value": "osifont" + } + }, + "property": { + "name": "Font", + "typeId": "App::PropertyFont" + }, + "propertyType": "App::PropertyFont", + "requested": { + "before": "osifont", + "edited": "Liberation Sans", + "restored": "osifont" + }, + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-font-promotion.json b/config/freecad-property-font-promotion.json new file mode 100644 index 0000000..42e8b18 --- /dev/null +++ b/config/freecad-property-font-promotion.json @@ -0,0 +1,72 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertyfont-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyFont", + "recordCount": 2, + "phaseEvidence": { + "A": [ + "config/freecad-property-font-inventory.json", + "check:freecad-property-font-inventory" + ], + "B": [ + "config/freecad-property-font-success.json", + "check:freecad-property-font-success" + ], + "C": [ + "config/freecad-property-font-failure.json", + "check:freecad-property-font-failure" + ], + "D": [ + "config/freecad-property-font-mutation.json", + "check:freecad-property-font-mutation" + ], + "E": [ + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyFontFacade.test.ts", + "test:freecad-property-font-facade" + ], + "F": [ + "tests/propertyFontTransaction.test.ts", + "test:freecad-property-font-transaction" + ], + "G": [ + "config/freecad-property-font-roundtrip.json", + "check:freecad-property-font-roundtrip" + ], + "H": [ + "config/chrome-property-font-verification.json", + "check:chrome-property-font" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "facadeValueModel": "bounded-utf8-font-family-string-with-native-string-fcstd-payload", + "writableRecords": 2, + "objectTypeCount": 2, + "fcstdElement": "String", + "defaultValue": "osifont", + "nativeRoundTripValue": "Noto Sans CJK SC", + "browserRoundTripValue": "Noto Sans CJK SC", + "fontResourceBoundary": "font family name persists in FCStd; font files and availability remain presentation-only", + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, + "exactPromotionReady": false, + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-17T13:03:43.084Z" +} diff --git a/config/freecad-property-font-roundtrip.json b/config/freecad-property-font-roundtrip.json new file mode 100644 index 0000000..7e1ac0c --- /dev/null +++ b/config/freecad-property-font-roundtrip.json @@ -0,0 +1,121 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-font-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyFont", + "archives": { + "nativeInitial": { + "path": ".cache/freecad/property-font-roundtrip/native-initial.FCStd", + "bytes": 1486, + "sha256": "de40c35893bffdffdf414568591f709d26870f55243a154c7e38d870e589a8c7" + }, + "webEdited": { + "path": ".cache/freecad/property-font-roundtrip/web-edited.FCStd", + "bytes": 1335, + "sha256": "6d653056914cf3c7440741753e0502d516d86fd6dc9504d5f774bf079f03f017" + }, + "nativeResaved": { + "path": ".cache/freecad/property-font-roundtrip/native-resaved.FCStd", + "bytes": 1494, + "sha256": "37b9ef6010010cb6ecce19cf84e269fdc08f8398c6d575f027b4a6cac0fe51c4" + } + }, + "nativeInitial": { + "object": { + "editorMode": [], + "hasShapeProperty": false, + "name": "FontProbe", + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "TechDraw::DrawViewAnnotation", + "value": "osifont" + }, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ] + }, + "web": { + "before": { + "name": "Font", + "typeId": "App::PropertyFont", + "element": "String", + "value": "osifont" + }, + "after": { + "name": "Font", + "typeId": "App::PropertyFont", + "element": "String", + "value": "Noto Sans CJK SC" + }, + "targetMarkedTouched": true, + "webOutputMatches": true, + "semanticObjectsPreserved": true, + "opaquePathsPreserved": 0, + "opaqueEntriesPreserved": true + }, + "nativeAfter": { + "reopened": { + "object": { + "editorMode": [], + "hasShapeProperty": false, + "name": "FontProbe", + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "TechDraw::DrawViewAnnotation", + "value": "Noto Sans CJK SC" + }, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ] + }, + "resaved": { + "object": { + "editorMode": [], + "hasShapeProperty": false, + "name": "FontProbe", + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "TechDraw::DrawViewAnnotation", + "value": "Noto Sans CJK SC" + }, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ] + } + }, + "classification": { + "requestedValue": "Noto Sans CJK SC", + "webValue": "Noto Sans CJK SC", + "reopenedValue": "Noto Sans CJK SC", + "resavedValue": "Noto Sans CJK SC", + "nativeOutputMatches": true, + "objectSetPreserved": true, + "nativeStateStable": true, + "fontResourceBoundary": "font family name persists in FCStd; font files and availability remain presentation-only", + "unknownSemanticDrift": false, + "zeroUnknownDrift": true + } +} diff --git a/config/freecad-property-font-success.json b/config/freecad-property-font-success.json new file mode 100644 index 0000000..7d7b786 --- /dev/null +++ b/config/freecad-property-font-success.json @@ -0,0 +1,118 @@ +{ + "baselineId": "freecad-1.1.1-property-font-success", + "diagnostics": { + "documentObjectCount": 1, + "exception": null + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "object": { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + }, + "phases": { + "default": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "requested": "osifont", + "statusString": "Valid", + "value": "osifont" + }, + "empty-family": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "requested": "", + "statusString": "Valid", + "value": "" + }, + "named-family": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "requested": "DejaVu Sans", + "statusString": "Valid", + "value": "DejaVu Sans" + }, + "restored": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "requested": "osifont", + "statusString": "Valid", + "value": "osifont" + }, + "unicode-family": { + "editorMode": [], + "hasShapeProperty": false, + "objectSet": [ + { + "name": "FontProbe", + "typeId": "TechDraw::DrawViewAnnotation" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyFont", + "recomputeResult": true, + "requested": "Noto Sans CJK SC", + "statusString": "Valid", + "value": "Noto Sans CJK SC" + } + }, + "property": { + "group": "Annotation", + "name": "Font", + "typeId": "App::PropertyFont" + }, + "propertyType": "App::PropertyFont", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-force-failure.json b/config/freecad-property-force-failure.json new file mode 100644 index 0000000..8349dab --- /dev/null +++ b/config/freecad-property-force-failure.json @@ -0,0 +1,241 @@ +{ + "baselineId": "freecad-1.1.1-property-force-failure", + "cancellationBoundary": { + "classification": "synchronous-property-setter", + "reason": "no-native-cancel-hook", + "replacement": "abort-active-document-transaction", + "supported": false + }, + "disabled": { + "after": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "before": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "classification": "editor-read-only-and-python-immutable", + "editorMode": [ + "ReadOnly" + ], + "exception": { + "message": "Object attribute 'Force' is read-only", + "type": "AttributeError" + }, + "objectsPreserved": true, + "requested": "750 N", + "restoredEditorMode": [], + "restoredStatus": [], + "status": [ + "Immutable", + "ReadOnly" + ], + "valuePreserved": true + }, + "documentIntegrity": { + "finalObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "initialObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "objectCount": 2, + "objectsPreserved": true + }, + "failures": [ + { + "after": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "afterObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "before": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "beforeObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "exception": { + "message": "Not matching Unit!", + "type": "ArithmeticError" + }, + "id": "wrong-dimension", + "objectsPreserved": true, + "polluted": false, + "requested": "5 mm", + "valuePreserved": true + }, + { + "after": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "afterObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "before": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "beforeObjects": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "exception": { + "message": "syntax error", + "type": "ParserError" + }, + "id": "malformed-text", + "objectsPreserved": true, + "polluted": false, + "requested": "not-a-quantity", + "valuePreserved": true + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "initial": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "object": { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + "outputBoundary": { + "after": { + "numeric": 125000, + "raw": "125000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "assignmentException": null, + "before": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "classification": "prop-output-ui-read-only-python-mutable", + "editorMode": [], + "property": { + "name": "ForceX", + "typeId": "App::PropertyForce" + }, + "restored": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "restoredExactly": true, + "status": [ + "27" + ] + }, + "property": { + "name": "Force", + "typeId": "App::PropertyForce" + }, + "schemaVersion": 1, + "status": "pass", + "transaction": { + "activeAfterAbort": { + "id": 0, + "name": "" + }, + "activeAfterEdit": { + "id": 1, + "name": "property-force-cancel" + }, + "afterAbort": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "before": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "edited": { + "numeric": 250000, + "raw": "250000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + }, + "objectsAfter": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "objectsBefore": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "objectsRestored": true, + "pendingAfterAbort": false, + "pendingAfterEdit": true, + "restored": true, + "undoMode": 1 + } +} diff --git a/config/freecad-property-force-inventory.json b/config/freecad-property-force-inventory.json new file mode 100644 index 0000000..668a648 --- /dev/null +++ b/config/freecad-property-force-inventory.json @@ -0,0 +1,136 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyForce", + "recordCount": 4, + "writableRecordCount": 1, + "outputRecordCount": 3, + "records": [ + { + "objectTypeId": "Fem::ConstraintForce", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Force", + "group": "", + "status": [], + "defaultRaw": "0.0 mm*kg/s^2", + "valueModel": { + "kind": "quantity", + "dimension": "mass*length/time^2", + "internalUnit": "mm*kg/s^2", + "displayUnit": "N", + "newtonScale": 1000, + "defaultValue": 0, + "writable": true, + "derivedOutput": false, + "acceptedBoundaryEvidence": "inventory-only; value and boundary acceptance remain pending in phase B/C" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::ConstraintForce", + "source": "Document.supportedTypes runtime inventory" + } + }, + { + "objectTypeId": "Fem::ConstraintRigidBody", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "ForceX", + "group": "ConstraintRigidBody", + "status": [ + 27 + ], + "defaultRaw": "0.0 mm*kg/s^2", + "valueModel": { + "kind": "quantity", + "dimension": "mass*length/time^2", + "internalUnit": "mm*kg/s^2", + "displayUnit": "N", + "newtonScale": 1000, + "defaultValue": 0, + "writable": false, + "derivedOutput": true, + "acceptedBoundaryEvidence": "inventory-only; value and boundary acceptance remain pending in phase B/C" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::ConstraintRigidBody", + "source": "Document.supportedTypes runtime inventory" + } + }, + { + "objectTypeId": "Fem::ConstraintRigidBody", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "ForceY", + "group": "ConstraintRigidBody", + "status": [ + 27 + ], + "defaultRaw": "0.0 mm*kg/s^2", + "valueModel": { + "kind": "quantity", + "dimension": "mass*length/time^2", + "internalUnit": "mm*kg/s^2", + "displayUnit": "N", + "newtonScale": 1000, + "defaultValue": 0, + "writable": false, + "derivedOutput": true, + "acceptedBoundaryEvidence": "inventory-only; value and boundary acceptance remain pending in phase B/C" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::ConstraintRigidBody", + "source": "Document.supportedTypes runtime inventory" + } + }, + { + "objectTypeId": "Fem::ConstraintRigidBody", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "ForceZ", + "group": "ConstraintRigidBody", + "status": [ + 27 + ], + "defaultRaw": "0.0 mm*kg/s^2", + "valueModel": { + "kind": "quantity", + "dimension": "mass*length/time^2", + "internalUnit": "mm*kg/s^2", + "displayUnit": "N", + "newtonScale": 1000, + "defaultValue": 0, + "writable": false, + "derivedOutput": true, + "acceptedBoundaryEvidence": "inventory-only; value and boundary acceptance remain pending in phase B/C" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::ConstraintRigidBody", + "source": "Document.supportedTypes runtime inventory" + } + } + ], + "source": { + "path": ".cache/freecad/reference-desktop.json", + "bytes": 2298709, + "sha256": "082400ec7379e8efc2af16709032d45890d7092277967b714acb05761fd1c2a7" + }, + "classification": "opaque-fcstd-proxy", + "nextPhases": [ + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I" + ] +} diff --git a/config/freecad-property-force-mutation.json b/config/freecad-property-force-mutation.json new file mode 100644 index 0000000..b605d96 --- /dev/null +++ b/config/freecad-property-force-mutation.json @@ -0,0 +1,298 @@ +{ + "baselineId": "freecad-1.1.1-property-force-mutation", + "before": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "editorMode": [], + "enableAmplitude": false, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "reversed": false, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "classification": { + "geometry": "not-applicable-no-shape-property", + "neighborPropertiesPreserved": true, + "propertyChanged": true, + "propertyRestored": true, + "semanticStateRestored": true + }, + "document": { + "objectsBefore": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + } + ], + "objectsPreserved": true, + "objectsRestored": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + } + ] + }, + "edited": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "editorMode": [], + "enableAmplitude": false, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "reversed": false, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 500000, + "raw": "500000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "neighbors": { + "before": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "enableAmplitude": false, + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "reversed": false + }, + "edited": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "enableAmplitude": false, + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "reversed": false + }, + "preserved": true, + "restored": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "enableAmplitude": false, + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "reversed": false + } + }, + "object": { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + "property": { + "name": "Force", + "typeId": "App::PropertyForce" + }, + "recompute": { + "edit": 1, + "initial": 1, + "restore": 1 + }, + "restored": { + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "directionVector": [ + 0, + 0, + 1 + ], + "editorMode": [], + "enableAmplitude": false, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmplitudeValues", + "Direction", + "DirectionVector", + "EnableAmplitude", + "ExpressionEngine", + "Force", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Reversed", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "reversed": false, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "schemaVersion": 1, + "status": "pass", + "touchedAfterEdit": { + "mustExecute": false, + "objectState": [ + "Touched" + ], + "value": { + "numeric": 500000, + "raw": "500000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "touchedAfterRestore": { + "mustExecute": false, + "objectState": [ + "Touched" + ], + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } +} diff --git a/config/freecad-property-force-promotion.json b/config/freecad-property-force-promotion.json new file mode 100644 index 0000000..14d2bf9 --- /dev/null +++ b/config/freecad-property-force-promotion.json @@ -0,0 +1,79 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertyforce-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyForce", + "recordCount": 4, + "phaseEvidence": { + "A": [ + "config/freecad-property-force-inventory.json", + "check:freecad-property-force-inventory" + ], + "B": [ + "config/freecad-property-force-success.json", + "check:freecad-property-force-success" + ], + "C": [ + "config/freecad-property-force-failure.json", + "check:freecad-property-force-failure" + ], + "D": [ + "config/freecad-property-force-mutation.json", + "check:freecad-property-force-mutation" + ], + "E": [ + "src/facade/units.ts", + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyForceFacade.test.ts", + "test:freecad-property-force-facade" + ], + "F": [ + "tests/propertyForceTransaction.test.ts", + "test:freecad-property-force-transaction" + ], + "G": [ + "config/freecad-property-force-roundtrip.json", + "check:freecad-property-force-roundtrip" + ], + "H": [ + "config/chrome-property-force-verification.json", + "check:chrome-property-force" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "facadeValueModel": "finite-number-internal-mm-kg-per-s2-with-registered-force-units", + "internalUnit": "mm*kg/s^2", + "displayUnits": [ + "N", + "kN", + "lbf" + ], + "newtonScale": 1000, + "writableRecords": 1, + "outputRecords": 3, + "fcstdElement": "Float", + "nativeRoundTripValue": 500000, + "browserRoundTripValue": 500000, + "outputPropertiesPreserved": true, + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, + "exactPromotionReady": false, + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-17T13:03:43.093Z" +} diff --git a/config/freecad-property-force-roundtrip.json b/config/freecad-property-force-roundtrip.json new file mode 100644 index 0000000..d27338a --- /dev/null +++ b/config/freecad-property-force-roundtrip.json @@ -0,0 +1,311 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-force-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "archives": { + "nativeInitial": { + "path": ".cache/freecad/property-force-roundtrip/native-initial.FCStd", + "bytes": 2179, + "sha256": "9045ec785084839c25bab5807d3d801faf3091ea83bf402621938b3bac13efea" + }, + "webEdited": { + "path": ".cache/freecad/property-force-roundtrip/web-edited.FCStd", + "bytes": 2000, + "sha256": "31df91b2712ce8fadb07d5487a3b036a603ad6a894092eb111e7c27ab64959a2" + }, + "nativeResaved": { + "path": ".cache/freecad/property-force-roundtrip/native-resaved.FCStd", + "bytes": 2147, + "sha256": "3b05532197026bf0516f8d594d02516d21092ab8fd3e5f823f76b03e226b2c43" + } + }, + "nativeInitial": { + "force": { + "name": "ForceProbe", + "property": { + "editorMode": [], + "name": "Force", + "status": [], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintForce" + }, + "objectSet": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "rigid": { + "name": "RigidForceProbe", + "outputs": [ + { + "editorMode": [], + "name": "ForceX", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceY", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceZ", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } + ], + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintRigidBody" + } + }, + "web": { + "before": { + "name": "Force", + "typeId": "App::PropertyForce", + "element": "Float", + "value": "0.0000000000000000" + }, + "after": { + "name": "Force", + "typeId": "App::PropertyForce", + "element": "Float", + "value": "500000" + }, + "objectSetPreserved": true, + "semanticObjectsPreserved": true, + "opaquePathsPreserved": 4, + "opaqueEntriesPreserved": true + }, + "nativeAfter": { + "reopened": { + "force": { + "name": "ForceProbe", + "property": { + "editorMode": [], + "name": "Force", + "status": [], + "typeId": "App::PropertyForce", + "value": { + "numeric": 500000, + "raw": "500000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintForce" + }, + "objectSet": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "rigid": { + "name": "RigidForceProbe", + "outputs": [ + { + "editorMode": [], + "name": "ForceX", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceY", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceZ", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } + ], + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintRigidBody" + } + }, + "resaved": { + "force": { + "name": "ForceProbe", + "property": { + "editorMode": [], + "name": "Force", + "status": [], + "typeId": "App::PropertyForce", + "value": { + "numeric": 500000, + "raw": "500000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintForce" + }, + "objectSet": [ + { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + { + "name": "RigidForceProbe", + "typeId": "Fem::ConstraintRigidBody" + } + ], + "rigid": { + "name": "RigidForceProbe", + "outputs": [ + { + "editorMode": [], + "name": "ForceX", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceY", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "name": "ForceZ", + "status": [ + "27" + ], + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } + ], + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintRigidBody" + } + } + }, + "classification": { + "webValue": 500000, + "reopenedValue": 500000, + "resavedValue": 500000, + "outputPropertiesPreserved": true, + "nativeOutputMatches": true, + "unknownSemanticDrift": false, + "zeroUnknownDrift": true + } +} diff --git a/config/freecad-property-force-success.json b/config/freecad-property-force-success.json new file mode 100644 index 0000000..74805fc --- /dev/null +++ b/config/freecad-property-force-success.json @@ -0,0 +1,231 @@ +{ + "baselineId": "freecad-1.1.1-property-force-success", + "cases": [ + { + "editorMode": [], + "group": "", + "id": "default", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": null, + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "", + "id": "nominal-newton", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": "500 N", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 500000, + "raw": "500000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "", + "id": "zero-boundary", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": "0 N", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "", + "id": "negative-boundary", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": "-5 N", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": -5000, + "raw": "-5000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "", + "id": "kilonewton", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": "2 kN", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 2000000, + "raw": "2000000.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "", + "id": "restored", + "objectName": "ForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintForce", + "propertyName": "Force", + "requested": "0 N", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } + ], + "diagnostics": { + "documentObjectCount": 2, + "exception": null + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "object": { + "name": "ForceProbe", + "typeId": "Fem::ConstraintForce" + }, + "outputProperties": [ + { + "editorMode": [], + "group": "ConstraintRigidBody", + "objectName": "RigidForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintRigidBody", + "propertyName": "ForceX", + "shape": { + "applicable": false + }, + "status": [ + "27" + ], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "ConstraintRigidBody", + "objectName": "RigidForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintRigidBody", + "propertyName": "ForceY", + "shape": { + "applicable": false + }, + "status": [ + "27" + ], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + }, + { + "editorMode": [], + "group": "ConstraintRigidBody", + "objectName": "RigidForceProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintRigidBody", + "propertyName": "ForceZ", + "shape": { + "applicable": false + }, + "status": [ + "27" + ], + "statusString": "Valid", + "typeId": "App::PropertyForce", + "value": { + "numeric": 0, + "raw": "0.0 mm*kg/s^2", + "unit": "Unit: mm*kg/s^2 (1,1,-2,0,0,0,0,0) [Force]" + } + } + ], + "property": { + "group": "", + "name": "Force", + "typeId": "App::PropertyForce" + }, + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-heatflux-failure.json b/config/freecad-property-heatflux-failure.json new file mode 100644 index 0000000..2187f72 --- /dev/null +++ b/config/freecad-property-heatflux-failure.json @@ -0,0 +1,181 @@ +{ + "baselineId": "freecad-1.1.1-property-heatflux-failure", + "cancellationBoundary": { + "classification": "synchronous-property-setter", + "reason": "no-native-cancel-hook", + "replacement": "abort-active-document-transaction", + "supported": false + }, + "disabled": { + "after": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "before": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "classification": "editor-read-only-and-python-immutable", + "editorMode": [ + "ReadOnly" + ], + "exception": { + "message": "Object attribute 'DFlux' is read-only", + "type": "AttributeError" + }, + "objectsPreserved": true, + "requested": "750 W/m^2", + "restoredEditorMode": [], + "restoredStatus": [], + "status": [ + "Immutable", + "ReadOnly" + ], + "valuePreserved": true + }, + "documentIntegrity": { + "finalObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "initialObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "objectCount": 1, + "objectsPreserved": true + }, + "failures": [ + { + "after": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "afterObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "before": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "beforeObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "exception": { + "message": "Not matching Unit!", + "type": "ArithmeticError" + }, + "id": "wrong-dimension", + "objectsPreserved": true, + "polluted": false, + "requested": "5 mm", + "valuePreserved": true + }, + { + "after": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "afterObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "before": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "beforeObjects": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "exception": { + "message": "syntax error", + "type": "ParserError" + }, + "id": "malformed-text", + "objectsPreserved": true, + "polluted": false, + "requested": "not-a-quantity", + "valuePreserved": true + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "initial": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "object": { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + }, + "property": { + "name": "DFlux", + "typeId": "App::PropertyHeatFlux" + }, + "schemaVersion": 1, + "status": "pass", + "transaction": { + "activeAfterAbort": { + "id": 0, + "name": "" + }, + "activeAfterEdit": { + "id": 1, + "name": "property-heatflux-cancel" + }, + "afterAbort": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "before": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "edited": { + "numeric": 250, + "raw": "250.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + }, + "objectsAfter": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "objectsBefore": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "objectsRestored": true, + "pendingAfterAbort": false, + "pendingAfterEdit": true, + "restored": true, + "undoMode": 1 + } +} diff --git a/config/freecad-property-heatflux-inventory.json b/config/freecad-property-heatflux-inventory.json new file mode 100644 index 0000000..12d5006 --- /dev/null +++ b/config/freecad-property-heatflux-inventory.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyHeatFlux", + "recordCount": 1, + "writableRecordCount": 1, + "outputRecordCount": 0, + "records": [ + { + "objectTypeId": "Fem::ConstraintHeatflux", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "DFlux", + "group": "ConstraintHeatflux", + "status": [], + "defaultRaw": "0.0 kg/s^3", + "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": "Fem::ConstraintHeatflux", + "source": "Document.supportedTypes runtime inventory" + } + } + ], + "source": { + "path": ".cache/freecad/reference-desktop.json", + "bytes": 2298709, + "sha256": "082400ec7379e8efc2af16709032d45890d7092277967b714acb05761fd1c2a7" + }, + "classification": "opaque-fcstd-proxy", + "nextPhases": [ + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I" + ] +} diff --git a/config/freecad-property-heatflux-mutation.json b/config/freecad-property-heatflux-mutation.json new file mode 100644 index 0000000..0322ee9 --- /dev/null +++ b/config/freecad-property-heatflux-mutation.json @@ -0,0 +1,358 @@ +{ + "baselineId": "freecad-1.1.1-property-heatflux-mutation", + "before": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "editorMode": [], + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "scale": 1, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "classification": { + "geometry": "not-applicable-no-shape-property", + "neighborPropertiesPreserved": true, + "propertyChanged": true, + "propertyRestored": true, + "semanticStateRestored": true + }, + "document": { + "objectsBefore": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ], + "objectsPreserved": true, + "objectsRestored": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ] + }, + "edited": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "editorMode": [], + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "scale": 1, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 500, + "raw": "500.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "neighbors": { + "before": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "scale": 1 + }, + "edited": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "scale": 1 + }, + "preserved": true, + "restored": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "scale": 1 + } + }, + "object": { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + }, + "property": { + "name": "DFlux", + "typeId": "App::PropertyHeatFlux" + }, + "recompute": { + "edit": 1, + "initial": 1, + "restore": 1 + }, + "restored": { + "ambientTemp": { + "numeric": 0, + "raw": "0.0 K", + "unit": "Unit: K (0,0,0,0,1,0,0,0) [Temperature]" + }, + "amplitudeValues": [ + "0, 0", + "1, 1" + ], + "constraintType": "Convection", + "editorMode": [], + "emissivity": 0, + "enableAmplitude": false, + "filmCoef": { + "numeric": 0, + "raw": "0.0 kg/(s^3*K)", + "unit": "Unit: kg/(s^3*K) (0,1,-3,0,-1,0,0,0) [ThermalTransferCoefficient]" + }, + "mustExecute": false, + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "AmbientTemp", + "AmplitudeValues", + "CavityName", + "CavityRadiation", + "ConstraintType", + "DFlux", + "Emissivity", + "EnableAmplitude", + "ExpressionEngine", + "FilmCoef", + "Label", + "Label2", + "NormalDirection", + "Normals", + "Points", + "References", + "Scale", + "Suppressed", + "Visibility" + ], + "propertyStatus": [], + "scale": 1, + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "schemaVersion": 1, + "status": "pass", + "touchedAfterEdit": { + "mustExecute": false, + "objectState": [ + "Touched" + ], + "value": { + "numeric": 500, + "raw": "500.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "touchedAfterRestore": { + "mustExecute": false, + "objectState": [ + "Touched" + ], + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + } +} diff --git a/config/freecad-property-heatflux-promotion.json b/config/freecad-property-heatflux-promotion.json new file mode 100644 index 0000000..ace9636 --- /dev/null +++ b/config/freecad-property-heatflux-promotion.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertyheatflux-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyHeatFlux", + "recordCount": 1, + "phaseEvidence": { + "A": [ + "config/freecad-property-heatflux-inventory.json", + "check:freecad-property-heatflux-inventory" + ], + "B": [ + "config/freecad-property-heatflux-success.json", + "check:freecad-property-heatflux-success" + ], + "C": [ + "config/freecad-property-heatflux-failure.json", + "check:freecad-property-heatflux-failure" + ], + "D": [ + "config/freecad-property-heatflux-mutation.json", + "check:freecad-property-heatflux-mutation" + ], + "E": [ + "src/facade/units.ts", + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyHeatFluxFacade.test.ts", + "test:freecad-property-heatflux-facade" + ], + "F": [ + "tests/propertyHeatFluxTransaction.test.ts", + "test:freecad-property-heatflux-transaction" + ], + "G": [ + "config/freecad-property-heatflux-roundtrip.json", + "check:freecad-property-heatflux-roundtrip" + ], + "H": [ + "config/chrome-property-heatflux-verification.json", + "check:chrome-property-heatflux" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "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": 1, + "outputRecords": 0, + "fcstdElement": "Float", + "nativeRoundTripValue": 500, + "browserRoundTripValue": 500, + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, + "exactPromotionReady": false, + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-17T13:03:43.078Z" +} diff --git a/config/freecad-property-heatflux-roundtrip.json b/config/freecad-property-heatflux-roundtrip.json new file mode 100644 index 0000000..e910c8e --- /dev/null +++ b/config/freecad-property-heatflux-roundtrip.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-heatflux-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "archives": { + "nativeInitial": { + "path": ".cache/freecad/property-heatflux-roundtrip/native-initial.FCStd", + "bytes": 1788, + "sha256": "cedd198d5ec128292628bd27cf58f33e4114049f9ff36b788eecf70762336e5a" + }, + "webEdited": { + "path": ".cache/freecad/property-heatflux-roundtrip/web-edited.FCStd", + "bytes": 1607, + "sha256": "d26eba8335628c223d41a383ee3eb4b563369ba8acb9f5c5ea4bd52f6d15cc95" + }, + "nativeResaved": { + "path": ".cache/freecad/property-heatflux-roundtrip/native-resaved.FCStd", + "bytes": 1794, + "sha256": "a92d764cb58b9cbe5ad496ea967eccbc900d42ca26b425f74c3708315cf40604" + } + }, + "nativeInitial": { + "heatFlux": { + "name": "HeatFluxProbe", + "property": { + "editorMode": [], + "group": "ConstraintHeatflux", + "name": "DFlux", + "status": [], + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintHeatflux" + }, + "objectSet": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ] + }, + "web": { + "before": { + "name": "DFlux", + "typeId": "App::PropertyHeatFlux", + "element": "Float", + "value": "0.0000000000000000" + }, + "after": { + "name": "DFlux", + "typeId": "App::PropertyHeatFlux", + "element": "Float", + "value": "500" + }, + "objectSetPreserved": true, + "semanticObjectsPreserved": true, + "opaquePathsPreserved": 2, + "opaqueEntriesPreserved": true + }, + "nativeAfter": { + "reopened": { + "heatFlux": { + "name": "HeatFluxProbe", + "property": { + "editorMode": [], + "group": "ConstraintHeatflux", + "name": "DFlux", + "status": [], + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 500, + "raw": "500.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintHeatflux" + }, + "objectSet": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ] + }, + "resaved": { + "heatFlux": { + "name": "HeatFluxProbe", + "property": { + "editorMode": [], + "group": "ConstraintHeatflux", + "name": "DFlux", + "status": [], + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 500, + "raw": "500.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + "shape": { + "applicable": false + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::ConstraintHeatflux" + }, + "objectSet": [ + { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + } + ] + } + }, + "classification": { + "webValue": 500, + "reopenedValue": 500, + "resavedValue": 500, + "nativeValueMatches": true, + "unknownSemanticDrift": false, + "zeroUnknownDrift": true + } +} diff --git a/config/freecad-property-heatflux-success.json b/config/freecad-property-heatflux-success.json new file mode 100644 index 0000000..ec4d608 --- /dev/null +++ b/config/freecad-property-heatflux-success.json @@ -0,0 +1,183 @@ +{ + "baselineId": "freecad-1.1.1-property-heatflux-success", + "cases": [ + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "default", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": null, + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "nominal-watt-per-square-meter", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "500 W/m^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 500, + "raw": "500.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "zero-boundary", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "0 W/m^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "negative-boundary", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "-5 W/m^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": -5, + "raw": "-5.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "kilowatt-per-square-meter", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "2 kW/m^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 2000, + "raw": "2000.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "watt-per-square-millimeter", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "1 W/mm^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 1000000, + "raw": "1000000.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + }, + { + "editorMode": [], + "group": "ConstraintHeatflux", + "id": "restored", + "objectName": "HeatFluxProbe", + "objectState": [ + "Up-to-date" + ], + "objectTypeId": "Fem::ConstraintHeatflux", + "propertyName": "DFlux", + "requested": "0 W/m^2", + "shape": { + "applicable": false + }, + "status": [], + "statusString": "Valid", + "typeId": "App::PropertyHeatFlux", + "value": { + "numeric": 0, + "raw": "0.0 kg/s^3", + "unit": "Unit: kg/s^3 (0,1,-3,0,0,0,0,0) [HeatFlux]" + } + } + ], + "diagnostics": { + "documentObjectCount": 1, + "exception": null + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "object": { + "name": "HeatFluxProbe", + "typeId": "Fem::ConstraintHeatflux" + }, + "property": { + "group": "ConstraintHeatflux", + "name": "DFlux", + "typeId": "App::PropertyHeatFlux" + }, + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-integerset-failure.json b/config/freecad-property-integerset-failure.json new file mode 100644 index 0000000..ee7c1f7 --- /dev/null +++ b/config/freecad-property-integerset-failure.json @@ -0,0 +1,236 @@ +{ + "baselineId": "freecad-1.1.1-property-integerset-failure", + "cancellationBoundary": { + "classification": "synchronous-property-setter", + "reason": "no-native-cancel-hook", + "replacement": "abort-active-document-transaction", + "supported": false + }, + "disabled": { + "after": [ + 1, + 3 + ], + "before": [ + 1, + 3 + ], + "classification": "editor-read-only-and-python-immutable", + "editorMode": [ + "ReadOnly" + ], + "exception": { + "message": "Object attribute 'Nodes' is read-only", + "type": "AttributeError" + }, + "objectsPreserved": true, + "requested": [ + 2, + 4 + ], + "restoredEditorMode": [], + "restoredStatus": [], + "status": [ + "Immutable", + "ReadOnly" + ], + "valuePreserved": true + }, + "documentIntegrity": { + "finalObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "initialObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectsPreserved": true + }, + "failures": [ + { + "after": [ + 1, + 3 + ], + "afterObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "before": [ + 1, + 3 + ], + "beforeObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "exception": { + "message": "type in list must be int, not float", + "type": "TypeError" + }, + "id": "float-item", + "objectsPreserved": true, + "polluted": false, + "requested": "[1, 1.5]", + "valuePreserved": true + }, + { + "after": [ + 1, + 3 + ], + "afterObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "before": [ + 1, + 3 + ], + "beforeObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "exception": { + "message": "type in list must be int, not str", + "type": "TypeError" + }, + "id": "string-item", + "objectsPreserved": true, + "polluted": false, + "requested": "[1, '3']", + "valuePreserved": true + }, + { + "after": [ + 1, + 3 + ], + "afterObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "before": [ + 1, + 3 + ], + "beforeObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "exception": { + "message": "type must be int or list of int, not set", + "type": "TypeError" + }, + "id": "set-object", + "objectsPreserved": true, + "polluted": false, + "requested": "{1, 3}", + "valuePreserved": true + }, + { + "after": [ + 1, + 3 + ], + "afterObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "before": [ + 1, + 3 + ], + "beforeObjects": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "exception": { + "message": "type must be int or list of int, not NoneType", + "type": "TypeError" + }, + "id": "none", + "objectsPreserved": true, + "polluted": false, + "requested": "None", + "valuePreserved": true + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "initial": [ + 1, + 3 + ], + "object": { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + }, + "property": { + "name": "Nodes", + "typeId": "App::PropertyIntegerSet" + }, + "propertyType": "App::PropertyIntegerSet", + "schemaVersion": 1, + "status": "pass", + "transaction": { + "activeAfterAbort": { + "id": 0, + "name": "" + }, + "activeAfterEdit": { + "id": 1, + "name": "property-integerset-cancel" + }, + "afterAbort": [ + 1, + 3 + ], + "before": [ + 1, + 3 + ], + "edited": [ + 2, + 4 + ], + "objectsAfter": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectsBefore": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectsRestored": true, + "pendingAfterAbort": false, + "pendingAfterEdit": true, + "restored": true, + "undoMode": 1 + } +} diff --git a/config/freecad-property-integerset-inventory.json b/config/freecad-property-integerset-inventory.json new file mode 100644 index 0000000..594e923 --- /dev/null +++ b/config/freecad-property-integerset-inventory.json @@ -0,0 +1,71 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyIntegerSet", + "recordCount": 2, + "writableRecordCount": 2, + "records": [ + { + "objectTypeId": "Fem::FemSetElementNodesObject", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Elements", + "group": "Element indexes", + "status": [], + "defaultRaw": "set()", + "valueModel": { + "kind": "integer-set", + "writable": true, + "elementType": "signed-integer", + "cardinality": "variable-unique-unordered", + "webRepresentation": "sorted-safe-integer-array" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::FemSetElementNodesObject", + "source": "Document.supportedTypes runtime inventory" + } + }, + { + "objectTypeId": "Fem::FemSetNodesObject", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Nodes", + "group": "Node indexes", + "status": [], + "defaultRaw": "set()", + "valueModel": { + "kind": "integer-set", + "writable": true, + "elementType": "signed-integer", + "cardinality": "variable-unique-unordered", + "webRepresentation": "sorted-safe-integer-array" + }, + "dependencies": [], + "applicability": { + "requiredObjectTypeId": "Fem::FemSetNodesObject", + "source": "Document.supportedTypes runtime inventory" + } + } + ], + "source": { + "path": ".cache/freecad/reference-desktop.json", + "bytes": 2298709, + "sha256": "082400ec7379e8efc2af16709032d45890d7092277967b714acb05761fd1c2a7" + }, + "classification": "opaque-fcstd-proxy", + "nextPhases": [ + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I" + ] +} diff --git a/config/freecad-property-integerset-mutation.json b/config/freecad-property-integerset-mutation.json new file mode 100644 index 0000000..2181419 --- /dev/null +++ b/config/freecad-property-integerset-mutation.json @@ -0,0 +1,369 @@ +{ + "baselineId": "freecad-1.1.1-property-integerset-mutation", + "caseCount": 2, + "cases": [ + { + "before": { + "editorMode": [], + "femMesh": null, + "group": "Node indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Nodes", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + 1, + 3 + ] + }, + "edited": { + "editorMode": [], + "femMesh": null, + "group": "Node indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Nodes", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + 2, + 5 + ] + }, + "neighborsPreserved": true, + "objectName": "NodesProbe", + "objectTypeId": "Fem::FemSetNodesObject", + "objectsRestored": true, + "propertyName": "Nodes", + "recompute": { + "edit": true, + "initial": true, + "restore": true + }, + "restored": { + "editorMode": [], + "femMesh": null, + "group": "Node indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Nodes", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + 1, + 3 + ] + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [], + "femMesh": null, + "group": "Node indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyNames": [ + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Nodes", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 2, + 5 + ] + }, + "touchedAfterRestore": { + "editorMode": [], + "femMesh": null, + "group": "Node indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyNames": [ + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Nodes", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 1, + 3 + ] + }, + "valueRestored": true + }, + { + "before": { + "editorMode": [], + "femMesh": null, + "group": "Element indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "Elements", + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + 4, + 10 + ] + }, + "edited": { + "editorMode": [], + "femMesh": null, + "group": "Element indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "Elements", + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + -1, + 8 + ] + }, + "neighborsPreserved": true, + "objectName": "ElementsProbe", + "objectTypeId": "Fem::FemSetElementNodesObject", + "objectsRestored": true, + "propertyName": "Elements", + "recompute": { + "edit": true, + "initial": true, + "restore": true + }, + "restored": { + "editorMode": [], + "femMesh": null, + "group": "Element indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Up-to-date" + ], + "propertyNames": [ + "Elements", + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Valid", + "value": [ + 4, + 10 + ] + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [], + "femMesh": null, + "group": "Element indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyNames": [ + "Elements", + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + -1, + 8 + ] + }, + "touchedAfterRestore": { + "editorMode": [], + "femMesh": null, + "group": "Element indexes", + "mustExecute": false, + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyNames": [ + "Elements", + "ExpressionEngine", + "FemMesh", + "Label", + "Label2", + "Visibility" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 4, + 10 + ] + }, + "valueRestored": true + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyIntegerSet", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-integerset-promotion.json b/config/freecad-property-integerset-promotion.json new file mode 100644 index 0000000..393dc5c --- /dev/null +++ b/config/freecad-property-integerset-promotion.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertyintegerset-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyIntegerSet", + "recordCount": 2, + "phaseEvidence": { + "A": [ + "config/freecad-property-integerset-inventory.json", + "check:freecad-property-integerset-inventory" + ], + "B": [ + "config/freecad-property-integerset-success.json", + "check:freecad-property-integerset-success" + ], + "C": [ + "config/freecad-property-integerset-failure.json", + "check:freecad-property-integerset-failure" + ], + "D": [ + "config/freecad-property-integerset-mutation.json", + "check:freecad-property-integerset-mutation" + ], + "E": [ + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/App.tsx", + "tests/propertyIntegerSetFacade.test.ts", + "test:freecad-property-integerset-facade" + ], + "F": [ + "tests/propertyIntegerSetTransaction.test.ts", + "test:freecad-property-integerset-transaction" + ], + "G": [ + "config/freecad-property-integerset-roundtrip.json", + "check:freecad-property-integerset-roundtrip" + ], + "H": [ + "config/chrome-property-integerset-verification.json", + "check:chrome-property-integerset" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "facadeValueModel": "sorted-unique-safe-integer-array", + "writableRecords": 2, + "nativeEmptyBindingBoundary": { + "neverAssignedEmptyRead": "native-process-termination", + "oracleDefault": "set()", + "safeEquivalent": "assign-empty-list-before-read" + }, + "fcstdElement": "IntegerSet", + "nativeRoundTripValue": [ + -1, + 8 + ], + "browserRoundTripValue": [ + -1, + 8 + ], + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 34, + "nativeEditableRecords": 4370, + "opaqueTypes": 46, + "opaqueRecords": 457, + "exactPromotionReady": false, + "exactBlocker": "46 runtime property types and 457 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-17T13:03:43.129Z" +} diff --git a/config/freecad-property-integerset-roundtrip.json b/config/freecad-property-integerset-roundtrip.json new file mode 100644 index 0000000..d4582be --- /dev/null +++ b/config/freecad-property-integerset-roundtrip.json @@ -0,0 +1,136 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-integerset-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "archives": { + "nativeInitial": { + "path": ".cache/freecad/property-integerset-roundtrip/native-initial.FCStd", + "bytes": 1268, + "sha256": "7554c51a0f98f011c64a1c14a4d4aa8de81082cf841f84270a612663f7af89c9" + }, + "webEdited": { + "path": ".cache/freecad/property-integerset-roundtrip/web-edited.FCStd", + "bytes": 1105, + "sha256": "a9e2e46bd71e206712091c0ed66e27538d5ea38ab6e3b0e8768038962ff34b65" + }, + "nativeResaved": { + "path": ".cache/freecad/property-integerset-roundtrip/native-resaved.FCStd", + "bytes": 1269, + "sha256": "f852a1202b3ee7ad3e2314d6b46d4df22c57bdaecc61a021c585d12c3acd7f59" + } + }, + "nativeInitial": { + "object": { + "editorMode": [], + "group": "Node indexes", + "hasShapeProperty": false, + "name": "NodesProbe", + "propertyName": "Nodes", + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::FemSetNodesObject", + "value": [ + 1, + 3 + ] + }, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ] + }, + "web": { + "before": { + "name": "Nodes", + "typeId": "App::PropertyIntegerSet", + "element": "IntegerSet", + "value": "[1,3]" + }, + "after": { + "name": "Nodes", + "typeId": "App::PropertyIntegerSet", + "element": "IntegerSet", + "value": "[-1,8]" + }, + "objectSetPreserved": true, + "semanticObjectsPreserved": true, + "opaquePathsPreserved": 0, + "opaqueEntriesPreserved": true + }, + "nativeAfter": { + "reopened": { + "object": { + "editorMode": [], + "group": "Node indexes", + "hasShapeProperty": false, + "name": "NodesProbe", + "propertyName": "Nodes", + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::FemSetNodesObject", + "value": [ + -1, + 8 + ] + }, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ] + }, + "resaved": { + "object": { + "editorMode": [], + "group": "Node indexes", + "hasShapeProperty": false, + "name": "NodesProbe", + "propertyName": "Nodes", + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "Fem::FemSetNodesObject", + "value": [ + -1, + 8 + ] + }, + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ] + } + }, + "classification": { + "webValue": "[-1,8]", + "reopenedValue": [ + -1, + 8 + ], + "resavedValue": [ + -1, + 8 + ], + "nativeOutputMatches": true, + "unknownSemanticDrift": false, + "zeroUnknownDrift": true + } +} diff --git a/config/freecad-property-integerset-success.json b/config/freecad-property-integerset-success.json new file mode 100644 index 0000000..da3a602 --- /dev/null +++ b/config/freecad-property-integerset-success.json @@ -0,0 +1,309 @@ +{ + "baselineId": "freecad-1.1.1-property-integerset-success", + "bindingBoundary": { + "neverAssignedEmptyRead": "native-process-termination", + "oracleDefault": "set()", + "safeEquivalent": "assign-empty-list-before-read" + }, + "caseCount": 2, + "cases": [ + { + "group": "Node indexes", + "mode": "direct-native-property-setter", + "objectName": "NodesProbe", + "objectTypeId": "Fem::FemSetNodesObject", + "phases": { + "deduplicated": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 1, + 2, + 3 + ] + }, + "explicitEmpty": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [] + }, + "largeSafeInteger": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 9007199254740991 + ] + }, + "negativeBoundary": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + -2, + 0, + 5 + ] + }, + "restored": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [] + }, + "scalar": { + "editorMode": [], + "group": "Node indexes", + "objectSet": [ + { + "name": "NodesProbe", + "typeId": "Fem::FemSetNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 7 + ] + } + }, + "propertyName": "Nodes" + }, + { + "group": "Element indexes", + "mode": "direct-native-property-setter", + "objectName": "ElementsProbe", + "objectTypeId": "Fem::FemSetElementNodesObject", + "phases": { + "deduplicated": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 4, + 10 + ] + }, + "explicitEmpty": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [] + }, + "largeSafeInteger": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 9007199254740991 + ] + }, + "negativeBoundary": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + -2, + 0, + 5 + ] + }, + "restored": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [] + }, + "scalar": { + "editorMode": [], + "group": "Element indexes", + "objectSet": [ + { + "name": "ElementsProbe", + "typeId": "Fem::FemSetElementNodesObject" + } + ], + "objectState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyIntegerSet", + "recomputeResult": true, + "shape": { + "applicable": false + }, + "statusString": "Touched", + "value": [ + 7 + ] + } + }, + "propertyName": "Elements" + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyIntegerSet", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-web-exact-parity-plan.json b/config/freecad-web-exact-parity-plan.json index 573077c..b363b2b 100644 --- a/config/freecad-web-exact-parity-plan.json +++ b/config/freecad-web-exact-parity-plan.json @@ -41,7 +41,7 @@ "id": "EX03", "title": "Application document, property and recompute semantics", "tasks": [ - { "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-native-property-semantics", "check:freecad-property-status", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Of 85 runtime property types and 5510 records, 50 types/466 records remain opaque-only; all 15 observed status classes now have typed Facade behavior, including LockDynamic dynamic mutation and PartialTrigger partial-document observer transactions, but the remaining codecs, editors and complete document observer semantics are not closed"] }, + { "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-native-property-semantics", "check:freecad-property-status", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Of 85 runtime property types and 5510 records, 46 types/457 records remain opaque-only; all 15 observed status classes now have typed Facade behavior, including LockDynamic dynamic mutation and PartialTrigger partial-document observer transactions, but the remaining codecs, editors and complete document observer semantics are not closed"] }, { "id": "EX-DOC-02", "title": "Match transactions, DAG recompute, partial failure and document observers", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01", "EX-TSN-04"], "deliverables": ["Transaction and observer event oracle", "Partial recompute scheduler", "Failure recovery contract"], "acceptance": ["Undo, redo, abort, stale and cyclic dependency behavior matches FreeCAD"], "evidence": ["check:document-closure", "test:facade"], "exactBlockedBy": ["Observer ordering and every partial-recompute branch are not yet oracle-backed"] }, { "id": "EX-DOC-03", "title": "Complete expressions, links, extensions, groups and App object lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01"], "deliverables": ["Expression dependency grammar", "Link and extension lifecycle", "Group and clone semantics"], "acceptance": ["Copy, clone, delete, relink and expression edits survive native round-trip"], "evidence": ["check:fcstd-closure", "check:chrome-production-document"], "exactBlockedBy": ["FeaturePython-backed extensions remain non-executable"] }, { "id": "EX-DOC-04", "title": "Complete multi-document, external link, merge and recovery workflows", "priority": "P1", "status": "pending", "dependencies": ["EX-DOC-02", "EX-DOC-03"], "deliverables": ["Cross-document Link corpus", "Merge and conflict semantics", "Crash and autosave recovery"], "acceptance": ["Open, close, relink, merge and recovery match the native document graph"], "evidence": ["check:chrome-persistence", "check:project-migration"], "exactBlockedBy": ["Current application is project-first and does not implement every desktop multi-document workflow"] }, diff --git a/config/qa04-fcstd-fuzz-verification.json b/config/qa04-fcstd-fuzz-verification.json index 59f89d1..5bba307 100644 --- a/config/qa04-fcstd-fuzz-verification.json +++ b/config/qa04-fcstd-fuzz-verification.json @@ -24,7 +24,7 @@ "wrong-document-root": 77 }, "timing": { - "p95Ms": 0.13, - "maxMs": 2.66 + "p95Ms": 0.135, + "maxMs": 2.707 } } diff --git a/config/qa04-geometry-fuzz-verification.json b/config/qa04-geometry-fuzz-verification.json index bc29e2d..2389dab 100644 --- a/config/qa04-geometry-fuzz-verification.json +++ b/config/qa04-geometry-fuzz-verification.json @@ -41,7 +41,7 @@ "zero-revolution-axis": 67 }, "timing": { - "p95Ms": 0.02, - "maxMs": 0.997 + "p95Ms": 0.021, + "maxMs": 1.057 } } diff --git a/config/qa04-sketch-fuzz-verification.json b/config/qa04-sketch-fuzz-verification.json index 8784560..7c0cdf3 100644 --- a/config/qa04-sketch-fuzz-verification.json +++ b/config/qa04-sketch-fuzz-verification.json @@ -22,7 +22,7 @@ "invalid": 800 }, "timing": { - "p95Ms": 0.03, - "maxMs": 0.659 + "p95Ms": 0.04, + "maxMs": 0.907 } } diff --git a/config/release-artifacts.json b/config/release-artifacts.json index 4abca6e..df8ff6d 100644 --- a/config/release-artifacts.json +++ b/config/release-artifacts.json @@ -23,9 +23,9 @@ "sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105" }, { - "path": "assets/index-CNTlr1np.js", - "bytes": 1210736, - "sha256": "4d9e8b6e6e70a13d6a5c34a39eaa62a537de8cefde60d09125392b5a14512363" + "path": "assets/index-CMgfoKgp.js", + "bytes": 1215336, + "sha256": "1b64ba3601119cad5345af95457d701087124370070778c1f637db8130b84e43" }, { "path": "assets/index-CSuKYsCZ.css", @@ -335,7 +335,7 @@ { "path": "index.html", "bytes": 678, - "sha256": "233c411801542bf802d4d701437c593620350c13fd1eec069c00d083b80a764d" + "sha256": "215c8954a63826c2bfa193c7e38b3678357e61045b31382233d71353a84031da" }, { "path": "manifest.webmanifest", @@ -390,7 +390,7 @@ { "path": "sw.js", "bytes": 1390, - "sha256": "041bc2f315a15f659a5471f382af087e61a8c4405fd8a725c068ddf898e7386e" + "sha256": "a480fa6da433c33ca56b3d12f983d210b9424ed9123c2e35b8e21926b4e054dd" }, { "path": "vendor/camotics/camotics-sweep.wasm", @@ -412,7 +412,7 @@ "status": "signed", "algorithm": "Ed25519", "keyId": "bitbybit-local-release-2026", - "payloadSha256": "7cfc2e7f4b34be4c4b5deb75ace67753f9e219ef51b5be74f5a5c1d4f895f3cb", - "signature": "9fhwYyVCjTXXsQUvePx8I31qBp2FsW6b1YOW6UUtR2FDhrqwPtttqLrxSPcH6S88++u805jHATBE5cTijm4VBw==" + "payloadSha256": "b0fefc0b89c0e83124e7ea410bbde34b6f62a4955669b51fb821ba3df181f945", + "signature": "GJdf3bOkY9vdg/YKfFWeh1dkszxpg16VlYdTmrjpLe6PMfhlzvcYQnye4sWy5bFSPoLbXdD7FHom+6e4GTVFCw==" } } diff --git a/docs/freecad-active-work-status.generated.zh-CN.md b/docs/freecad-active-work-status.generated.zh-CN.md index 9f1c8ef..d788b90 100644 --- a/docs/freecad-active-work-status.generated.zh-CN.md +++ b/docs/freecad-active-work-status.generated.zh-CN.md @@ -3,10 +3,10 @@ 生成日期:2026-08-15。基线:FreeCAD 1.1.1 / `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`。 -- 唯一活动任务:`PROP-app-propertyfont-A` -- 微任务:1756 total / 559 completed / 1 in_progress / 1196 pending +- 唯一活动任务:`PROP-app-propertyintegerset-A` +- 微任务:1756 total / 586 completed / 1 in_progress / 1169 pending - 有序操作对:361/361 classified / 0 unknown -- Property:35/85 native types / 50 opaque types +- Property:38/85 native types / 47 opaque types - exact 任务:5/52 completed - 模块能力:0 exact / 26 compatible / 5 proxy / 3 development @@ -16,7 +16,7 @@ | `ORA-GUI-WORKFLOWS` | 7 | 7 | 0 | 0 | `-` | | `TSN-RECOVERY-DRIFT` | 13 | 13 | 0 | 0 | `-` | | `TSN-ORDERED-PAIRS` | 361 | 361 | 0 | 0 | `-` | -| `PROPERTY-CODECS` | 522 | 72 | 1 | 449 | `PROP-app-propertyfont-A` | +| `PROPERTY-CODECS` | 522 | 99 | 1 | 422 | `PROP-app-propertyintegerset-A` | | `DOCUMENT-SEMANTICS` | 90 | 0 | 0 | 90 | `-` | | `CORE-MODELING` | 171 | 0 | 0 | 171 | `-` | | `FCSTD-AND-FORMATS` | 72 | 0 | 0 | 72 | `-` | diff --git a/package.json b/package.json index 72189dc..ca0272b 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,70 @@ "check:chrome-property-fileincluded": "node scripts/check-chrome-property-fileincluded.mjs", "generate:freecad-property-fileincluded-promotion": "node scripts/generate-freecad-property-fileincluded-promotion.mjs", "check:freecad-property-fileincluded-promotion": "node scripts/check-freecad-property-fileincluded-promotion.mjs", + "generate:freecad-property-font-inventory": "node scripts/generate-freecad-property-font-inventory.mjs", + "check:freecad-property-font-inventory": "node scripts/check-freecad-property-font-inventory.mjs", + "probe:freecad-property-font-success": "node scripts/run-freecad-property-font-success.mjs", + "check:freecad-property-font-success": "node scripts/check-freecad-property-font-success.mjs", + "probe:freecad-property-font-failure": "node scripts/run-freecad-property-font-failure.mjs", + "check:freecad-property-font-failure": "node scripts/check-freecad-property-font-failure.mjs", + "probe:freecad-property-font-mutation": "node scripts/run-freecad-property-font-mutation.mjs", + "check:freecad-property-font-mutation": "node scripts/check-freecad-property-font-mutation.mjs", + "test:freecad-property-font-facade": "tsx --test tests/propertyFontFacade.test.ts", + "test:freecad-property-font-transaction": "tsx --test tests/propertyFontTransaction.test.ts", + "probe:freecad-property-font-roundtrip": "tsx scripts/run-freecad-property-font-roundtrip.ts", + "check:freecad-property-font-roundtrip": "node scripts/check-freecad-property-font-roundtrip.mjs", + "test:chrome-property-font": "node scripts/run-chrome-property-font.mjs", + "check:chrome-property-font": "node scripts/check-chrome-property-font.mjs", + "generate:freecad-property-font-promotion": "node scripts/generate-freecad-property-font-promotion.mjs", + "check:freecad-property-font-promotion": "node scripts/check-freecad-property-font-promotion.mjs", + "generate:freecad-property-force-inventory": "node scripts/generate-freecad-property-force-inventory.mjs", + "check:freecad-property-force-inventory": "node scripts/check-freecad-property-force-inventory.mjs", + "probe:freecad-property-force-success": "node scripts/run-freecad-property-force-success.mjs", + "check:freecad-property-force-success": "node scripts/check-freecad-property-force-success.mjs", + "probe:freecad-property-force-failure": "node scripts/run-freecad-property-force-failure.mjs", + "check:freecad-property-force-failure": "node scripts/check-freecad-property-force-failure.mjs", + "probe:freecad-property-force-mutation": "node scripts/run-freecad-property-force-mutation.mjs", + "check:freecad-property-force-mutation": "node scripts/check-freecad-property-force-mutation.mjs", + "test:freecad-property-force-facade": "tsx --test tests/propertyForceFacade.test.ts", + "test:freecad-property-force-transaction": "tsx --test tests/propertyForceTransaction.test.ts", + "probe:freecad-property-force-roundtrip": "tsx scripts/run-freecad-property-force-roundtrip.ts", + "check:freecad-property-force-roundtrip": "node scripts/check-freecad-property-force-roundtrip.mjs", + "test:chrome-property-force": "node scripts/run-chrome-property-force.mjs", + "check:chrome-property-force": "node scripts/check-chrome-property-force.mjs", + "generate:freecad-property-force-promotion": "node scripts/generate-freecad-property-force-promotion.mjs", + "check:freecad-property-force-promotion": "node scripts/check-freecad-property-force-promotion.mjs", + "generate:freecad-property-heatflux-inventory": "node scripts/generate-freecad-property-heatflux-inventory.mjs", + "check:freecad-property-heatflux-inventory": "node scripts/check-freecad-property-heatflux-inventory.mjs", + "probe:freecad-property-heatflux-success": "node scripts/run-freecad-property-heatflux-success.mjs", + "check:freecad-property-heatflux-success": "node scripts/check-freecad-property-heatflux-success.mjs", + "probe:freecad-property-heatflux-failure": "node scripts/run-freecad-property-heatflux-failure.mjs", + "check:freecad-property-heatflux-failure": "node scripts/check-freecad-property-heatflux-failure.mjs", + "probe:freecad-property-heatflux-mutation": "node scripts/run-freecad-property-heatflux-mutation.mjs", + "check:freecad-property-heatflux-mutation": "node scripts/check-freecad-property-heatflux-mutation.mjs", + "test:freecad-property-heatflux-facade": "tsx --test tests/propertyHeatFluxFacade.test.ts", + "test:freecad-property-heatflux-transaction": "tsx --test tests/propertyHeatFluxTransaction.test.ts", + "probe:freecad-property-heatflux-roundtrip": "tsx scripts/run-freecad-property-heatflux-roundtrip.ts", + "check:freecad-property-heatflux-roundtrip": "node scripts/check-freecad-property-heatflux-roundtrip.mjs", + "test:chrome-property-heatflux": "node scripts/run-chrome-property-heatflux.mjs", + "check:chrome-property-heatflux": "node scripts/check-chrome-property-heatflux.mjs", + "generate:freecad-property-heatflux-promotion": "node scripts/generate-freecad-property-heatflux-promotion.mjs", + "check:freecad-property-heatflux-promotion": "node scripts/check-freecad-property-heatflux-promotion.mjs", + "generate:freecad-property-integerset-inventory": "node scripts/generate-freecad-property-integerset-inventory.mjs", + "check:freecad-property-integerset-inventory": "node scripts/check-freecad-property-integerset-inventory.mjs", + "probe:freecad-property-integerset-success": "node scripts/run-freecad-property-integerset-success.mjs", + "check:freecad-property-integerset-success": "node scripts/check-freecad-property-integerset-success.mjs", + "probe:freecad-property-integerset-failure": "node scripts/run-freecad-property-integerset-failure.mjs", + "check:freecad-property-integerset-failure": "node scripts/check-freecad-property-integerset-failure.mjs", + "probe:freecad-property-integerset-mutation": "node scripts/run-freecad-property-integerset-mutation.mjs", + "check:freecad-property-integerset-mutation": "node scripts/check-freecad-property-integerset-mutation.mjs", + "test:freecad-property-integerset-facade": "tsx --test tests/propertyIntegerSetFacade.test.ts", + "test:freecad-property-integerset-transaction": "tsx --test tests/propertyIntegerSetTransaction.test.ts", + "probe:freecad-property-integerset-roundtrip": "tsx scripts/run-freecad-property-integerset-roundtrip.ts", + "check:freecad-property-integerset-roundtrip": "node scripts/check-freecad-property-integerset-roundtrip.mjs", + "test:chrome-property-integerset": "node scripts/run-chrome-property-integerset.mjs", + "check:chrome-property-integerset": "node scripts/check-chrome-property-integerset.mjs", + "generate:freecad-property-integerset-promotion": "node scripts/generate-freecad-property-integerset-promotion.mjs", + "check:freecad-property-integerset-promotion": "node scripts/check-freecad-property-integerset-promotion.mjs", "probe:freecad-property-file-success": "node scripts/run-freecad-property-file-success.mjs", "check:freecad-property-file-success": "node scripts/check-freecad-property-file-success.mjs", "probe:freecad-property-file-failure": "node scripts/run-freecad-property-file-failure.mjs", diff --git a/scripts/check-chrome-property-font.mjs b/scripts/check-chrome-property-font.mjs new file mode 100644 index 0000000..9578c85 --- /dev/null +++ b/scripts/check-chrome-property-font.mjs @@ -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)) diff --git a/scripts/check-chrome-property-force.mjs b/scripts/check-chrome-property-force.mjs new file mode 100644 index 0000000..c2d0f40 --- /dev/null +++ b/scripts/check-chrome-property-force.mjs @@ -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)) diff --git a/scripts/check-chrome-property-heatflux.mjs b/scripts/check-chrome-property-heatflux.mjs new file mode 100644 index 0000000..cc9007f --- /dev/null +++ b/scripts/check-chrome-property-heatflux.mjs @@ -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)) diff --git a/scripts/check-chrome-property-integerset.mjs b/scripts/check-chrome-property-integerset.mjs new file mode 100644 index 0000000..3533962 --- /dev/null +++ b/scripts/check-chrome-property-integerset.mjs @@ -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)) diff --git a/scripts/check-facade-boundary.mjs b/scripts/check-facade-boundary.mjs index 5717f5e..bf075dc 100644 --- a/scripts/check-facade-boundary.mjs +++ b/scripts/check-facade-boundary.mjs @@ -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', ]) diff --git a/scripts/check-freecad-native-property-semantics.mjs b/scripts/check-freecad-native-property-semantics.mjs index ba5dfe8..044f70d 100644 --- a/scripts/check-freecad-native-property-semantics.mjs +++ b/scripts/check-freecad-native-property-semantics.mjs @@ -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)) diff --git a/scripts/check-freecad-property-acceleration-promotion.mjs b/scripts/check-freecad-property-acceleration-promotion.mjs index 3e77f0e..eb86b0c 100644 --- a/scripts/check-freecad-property-acceleration-promotion.mjs +++ b/scripts/check-freecad-property-acceleration-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-area-promotion.mjs b/scripts/check-freecad-property-area-promotion.mjs index 627b59b..abe3e6a 100644 --- a/scripts/check-freecad-property-area-promotion.mjs +++ b/scripts/check-freecad-property-area-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-boollist-promotion.mjs b/scripts/check-freecad-property-boollist-promotion.mjs index dbf2068..b21ca46 100644 --- a/scripts/check-freecad-property-boollist-promotion.mjs +++ b/scripts/check-freecad-property-boollist-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-color-promotion.mjs b/scripts/check-freecad-property-color-promotion.mjs index ab614cd..a72fbcf 100644 --- a/scripts/check-freecad-property-color-promotion.mjs +++ b/scripts/check-freecad-property-color-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-colorlist-promotion.mjs b/scripts/check-freecad-property-colorlist-promotion.mjs index 5690d9d..d6e40a0 100644 --- a/scripts/check-freecad-property-colorlist-promotion.mjs +++ b/scripts/check-freecad-property-colorlist-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-direction-promotion.mjs b/scripts/check-freecad-property-direction-promotion.mjs index 756e2cd..6265c00 100644 --- a/scripts/check-freecad-property-direction-promotion.mjs +++ b/scripts/check-freecad-property-direction-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-file-promotion.mjs b/scripts/check-freecad-property-file-promotion.mjs index 670aa09..1690e44 100644 --- a/scripts/check-freecad-property-file-promotion.mjs +++ b/scripts/check-freecad-property-file-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-fileincluded-promotion.mjs b/scripts/check-freecad-property-fileincluded-promotion.mjs index c5dc00a..bbdbbb6 100644 --- a/scripts/check-freecad-property-fileincluded-promotion.mjs +++ b/scripts/check-freecad-property-fileincluded-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-font-failure.mjs b/scripts/check-freecad-property-font-failure.mjs new file mode 100644 index 0000000..35e939d --- /dev/null +++ b/scripts/check-freecad-property-font-failure.mjs @@ -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)) diff --git a/scripts/check-freecad-property-font-inventory.mjs b/scripts/check-freecad-property-font-inventory.mjs new file mode 100644 index 0000000..ee9ac55 --- /dev/null +++ b/scripts/check-freecad-property-font-inventory.mjs @@ -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)) diff --git a/scripts/check-freecad-property-font-mutation.mjs b/scripts/check-freecad-property-font-mutation.mjs new file mode 100644 index 0000000..6b85a9e --- /dev/null +++ b/scripts/check-freecad-property-font-mutation.mjs @@ -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)) diff --git a/scripts/check-freecad-property-font-promotion.mjs b/scripts/check-freecad-property-font-promotion.mjs new file mode 100644 index 0000000..070090c --- /dev/null +++ b/scripts/check-freecad-property-font-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-font-roundtrip.mjs b/scripts/check-freecad-property-font-roundtrip.mjs new file mode 100644 index 0000000..0372da6 --- /dev/null +++ b/scripts/check-freecad-property-font-roundtrip.mjs @@ -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)) diff --git a/scripts/check-freecad-property-font-success.mjs b/scripts/check-freecad-property-font-success.mjs new file mode 100644 index 0000000..d5b1847 --- /dev/null +++ b/scripts/check-freecad-property-font-success.mjs @@ -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)) diff --git a/scripts/check-freecad-property-force-failure.mjs b/scripts/check-freecad-property-force-failure.mjs new file mode 100644 index 0000000..1540b21 --- /dev/null +++ b/scripts/check-freecad-property-force-failure.mjs @@ -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)) diff --git a/scripts/check-freecad-property-force-inventory.mjs b/scripts/check-freecad-property-force-inventory.mjs new file mode 100644 index 0000000..0a3cee0 --- /dev/null +++ b/scripts/check-freecad-property-force-inventory.mjs @@ -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)) diff --git a/scripts/check-freecad-property-force-mutation.mjs b/scripts/check-freecad-property-force-mutation.mjs new file mode 100644 index 0000000..4e7343c --- /dev/null +++ b/scripts/check-freecad-property-force-mutation.mjs @@ -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)) diff --git a/scripts/check-freecad-property-force-promotion.mjs b/scripts/check-freecad-property-force-promotion.mjs new file mode 100644 index 0000000..5a9e62b --- /dev/null +++ b/scripts/check-freecad-property-force-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-force-roundtrip.mjs b/scripts/check-freecad-property-force-roundtrip.mjs new file mode 100644 index 0000000..ee3488e --- /dev/null +++ b/scripts/check-freecad-property-force-roundtrip.mjs @@ -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)) diff --git a/scripts/check-freecad-property-force-success.mjs b/scripts/check-freecad-property-force-success.mjs new file mode 100644 index 0000000..adcac57 --- /dev/null +++ b/scripts/check-freecad-property-force-success.mjs @@ -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)) diff --git a/scripts/check-freecad-property-heatflux-failure.mjs b/scripts/check-freecad-property-heatflux-failure.mjs new file mode 100644 index 0000000..ed42ef4 --- /dev/null +++ b/scripts/check-freecad-property-heatflux-failure.mjs @@ -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)) diff --git a/scripts/check-freecad-property-heatflux-inventory.mjs b/scripts/check-freecad-property-heatflux-inventory.mjs new file mode 100644 index 0000000..e3837d6 --- /dev/null +++ b/scripts/check-freecad-property-heatflux-inventory.mjs @@ -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)) diff --git a/scripts/check-freecad-property-heatflux-mutation.mjs b/scripts/check-freecad-property-heatflux-mutation.mjs new file mode 100644 index 0000000..b2c1a82 --- /dev/null +++ b/scripts/check-freecad-property-heatflux-mutation.mjs @@ -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)) diff --git a/scripts/check-freecad-property-heatflux-promotion.mjs b/scripts/check-freecad-property-heatflux-promotion.mjs new file mode 100644 index 0000000..6c4b5f2 --- /dev/null +++ b/scripts/check-freecad-property-heatflux-promotion.mjs @@ -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)) + + diff --git a/scripts/check-freecad-property-heatflux-roundtrip.mjs b/scripts/check-freecad-property-heatflux-roundtrip.mjs new file mode 100644 index 0000000..a90f7ba --- /dev/null +++ b/scripts/check-freecad-property-heatflux-roundtrip.mjs @@ -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)) diff --git a/scripts/check-freecad-property-heatflux-success.mjs b/scripts/check-freecad-property-heatflux-success.mjs new file mode 100644 index 0000000..2c4c565 --- /dev/null +++ b/scripts/check-freecad-property-heatflux-success.mjs @@ -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)) + diff --git a/scripts/check-freecad-property-integerset-failure.mjs b/scripts/check-freecad-property-integerset-failure.mjs new file mode 100644 index 0000000..5c5cc2e --- /dev/null +++ b/scripts/check-freecad-property-integerset-failure.mjs @@ -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)) + diff --git a/scripts/check-freecad-property-integerset-inventory.mjs b/scripts/check-freecad-property-integerset-inventory.mjs new file mode 100644 index 0000000..b7f2f4d --- /dev/null +++ b/scripts/check-freecad-property-integerset-inventory.mjs @@ -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)) diff --git a/scripts/check-freecad-property-integerset-mutation.mjs b/scripts/check-freecad-property-integerset-mutation.mjs new file mode 100644 index 0000000..8a417eb --- /dev/null +++ b/scripts/check-freecad-property-integerset-mutation.mjs @@ -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)) + diff --git a/scripts/check-freecad-property-integerset-promotion.mjs b/scripts/check-freecad-property-integerset-promotion.mjs new file mode 100644 index 0000000..036eb74 --- /dev/null +++ b/scripts/check-freecad-property-integerset-promotion.mjs @@ -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)) diff --git a/scripts/check-freecad-property-integerset-roundtrip.mjs b/scripts/check-freecad-property-integerset-roundtrip.mjs new file mode 100644 index 0000000..28d67e4 --- /dev/null +++ b/scripts/check-freecad-property-integerset-roundtrip.mjs @@ -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)) diff --git a/scripts/check-freecad-property-integerset-success.mjs b/scripts/check-freecad-property-integerset-success.mjs new file mode 100644 index 0000000..481dd7c --- /dev/null +++ b/scripts/check-freecad-property-integerset-success.mjs @@ -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)) + diff --git a/scripts/freecad-property-font-failure.py b/scripts/freecad-property-font-failure.py new file mode 100644 index 0000000..5bbf785 --- /dev/null +++ b/scripts/freecad-property-font-failure.py @@ -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)) diff --git a/scripts/freecad-property-font-mutation.py b/scripts/freecad-property-font-mutation.py new file mode 100644 index 0000000..ae821fa --- /dev/null +++ b/scripts/freecad-property-font-mutation.py @@ -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)) diff --git a/scripts/freecad-property-font-roundtrip.py b/scripts/freecad-property-font-roundtrip.py new file mode 100644 index 0000000..658fd79 --- /dev/null +++ b/scripts/freecad-property-font-roundtrip.py @@ -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)) diff --git a/scripts/freecad-property-font-success.py b/scripts/freecad-property-font-success.py new file mode 100644 index 0000000..3dee26a --- /dev/null +++ b/scripts/freecad-property-font-success.py @@ -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)) diff --git a/scripts/freecad-property-force-failure.py b/scripts/freecad-property-force-failure.py new file mode 100644 index 0000000..cd7f539 --- /dev/null +++ b/scripts/freecad-property-force-failure.py @@ -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)) diff --git a/scripts/freecad-property-force-mutation.py b/scripts/freecad-property-force-mutation.py new file mode 100644 index 0000000..5d92ec6 --- /dev/null +++ b/scripts/freecad-property-force-mutation.py @@ -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)) diff --git a/scripts/freecad-property-force-roundtrip.py b/scripts/freecad-property-force-roundtrip.py new file mode 100644 index 0000000..788cb15 --- /dev/null +++ b/scripts/freecad-property-force-roundtrip.py @@ -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)) diff --git a/scripts/freecad-property-force-success.py b/scripts/freecad-property-force-success.py new file mode 100644 index 0000000..c8fc5bd --- /dev/null +++ b/scripts/freecad-property-force-success.py @@ -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)) diff --git a/scripts/freecad-property-heatflux-failure.py b/scripts/freecad-property-heatflux-failure.py new file mode 100644 index 0000000..17f704f --- /dev/null +++ b/scripts/freecad-property-heatflux-failure.py @@ -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)) + diff --git a/scripts/freecad-property-heatflux-mutation.py b/scripts/freecad-property-heatflux-mutation.py new file mode 100644 index 0000000..726808d --- /dev/null +++ b/scripts/freecad-property-heatflux-mutation.py @@ -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)) + diff --git a/scripts/freecad-property-heatflux-roundtrip.py b/scripts/freecad-property-heatflux-roundtrip.py new file mode 100644 index 0000000..900bbce --- /dev/null +++ b/scripts/freecad-property-heatflux-roundtrip.py @@ -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)) + diff --git a/scripts/freecad-property-heatflux-success.py b/scripts/freecad-property-heatflux-success.py new file mode 100644 index 0000000..c2f72d7 --- /dev/null +++ b/scripts/freecad-property-heatflux-success.py @@ -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)) + diff --git a/scripts/freecad-property-integerset-failure.py b/scripts/freecad-property-integerset-failure.py new file mode 100644 index 0000000..dfce044 --- /dev/null +++ b/scripts/freecad-property-integerset-failure.py @@ -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)) + diff --git a/scripts/freecad-property-integerset-mutation.py b/scripts/freecad-property-integerset-mutation.py new file mode 100644 index 0000000..54adc1e --- /dev/null +++ b/scripts/freecad-property-integerset-mutation.py @@ -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)) + diff --git a/scripts/freecad-property-integerset-roundtrip.py b/scripts/freecad-property-integerset-roundtrip.py new file mode 100644 index 0000000..b1b46fb --- /dev/null +++ b/scripts/freecad-property-integerset-roundtrip.py @@ -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)) + diff --git a/scripts/freecad-property-integerset-success.py b/scripts/freecad-property-integerset-success.py new file mode 100644 index 0000000..c340005 --- /dev/null +++ b/scripts/freecad-property-integerset-success.py @@ -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)) + diff --git a/scripts/generate-freecad-property-acceleration-promotion.mjs b/scripts/generate-freecad-property-acceleration-promotion.mjs index d181d0c..d435b86 100644 --- a/scripts/generate-freecad-property-acceleration-promotion.mjs +++ b/scripts/generate-freecad-property-acceleration-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-area-promotion.mjs b/scripts/generate-freecad-property-area-promotion.mjs index 8a51cf3..e38d656 100644 --- a/scripts/generate-freecad-property-area-promotion.mjs +++ b/scripts/generate-freecad-property-area-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-boollist-promotion.mjs b/scripts/generate-freecad-property-boollist-promotion.mjs index bf3e475..a9732c4 100644 --- a/scripts/generate-freecad-property-boollist-promotion.mjs +++ b/scripts/generate-freecad-property-boollist-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-color-promotion.mjs b/scripts/generate-freecad-property-color-promotion.mjs index f821ed8..c80fe0e 100644 --- a/scripts/generate-freecad-property-color-promotion.mjs +++ b/scripts/generate-freecad-property-color-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-colorlist-promotion.mjs b/scripts/generate-freecad-property-colorlist-promotion.mjs index e9f410f..fddac0d 100644 --- a/scripts/generate-freecad-property-colorlist-promotion.mjs +++ b/scripts/generate-freecad-property-colorlist-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-direction-promotion.mjs b/scripts/generate-freecad-property-direction-promotion.mjs index d1a82e7..badd62a 100644 --- a/scripts/generate-freecad-property-direction-promotion.mjs +++ b/scripts/generate-freecad-property-direction-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-file-promotion.mjs b/scripts/generate-freecad-property-file-promotion.mjs index 995ff94..20d5b99 100644 --- a/scripts/generate-freecad-property-file-promotion.mjs +++ b/scripts/generate-freecad-property-file-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-fileincluded-promotion.mjs b/scripts/generate-freecad-property-fileincluded-promotion.mjs index 14bde25..eacf6df 100644 --- a/scripts/generate-freecad-property-fileincluded-promotion.mjs +++ b/scripts/generate-freecad-property-fileincluded-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-font-inventory.mjs b/scripts/generate-freecad-property-font-inventory.mjs new file mode 100644 index 0000000..6cdad6c --- /dev/null +++ b/scripts/generate-freecad-property-font-inventory.mjs @@ -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)) diff --git a/scripts/generate-freecad-property-font-promotion.mjs b/scripts/generate-freecad-property-font-promotion.mjs new file mode 100644 index 0000000..b3e07c0 --- /dev/null +++ b/scripts/generate-freecad-property-font-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-force-inventory.mjs b/scripts/generate-freecad-property-force-inventory.mjs new file mode 100644 index 0000000..dc46c9d --- /dev/null +++ b/scripts/generate-freecad-property-force-inventory.mjs @@ -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)) diff --git a/scripts/generate-freecad-property-force-promotion.mjs b/scripts/generate-freecad-property-force-promotion.mjs new file mode 100644 index 0000000..01ed3d0 --- /dev/null +++ b/scripts/generate-freecad-property-force-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-heatflux-inventory.mjs b/scripts/generate-freecad-property-heatflux-inventory.mjs new file mode 100644 index 0000000..ff0bed6 --- /dev/null +++ b/scripts/generate-freecad-property-heatflux-inventory.mjs @@ -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)) + diff --git a/scripts/generate-freecad-property-heatflux-promotion.mjs b/scripts/generate-freecad-property-heatflux-promotion.mjs new file mode 100644 index 0000000..335822f --- /dev/null +++ b/scripts/generate-freecad-property-heatflux-promotion.mjs @@ -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)) + + diff --git a/scripts/generate-freecad-property-integerset-inventory.mjs b/scripts/generate-freecad-property-integerset-inventory.mjs new file mode 100644 index 0000000..20e08dc --- /dev/null +++ b/scripts/generate-freecad-property-integerset-inventory.mjs @@ -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)) + diff --git a/scripts/generate-freecad-property-integerset-promotion.mjs b/scripts/generate-freecad-property-integerset-promotion.mjs new file mode 100644 index 0000000..b9cff9e --- /dev/null +++ b/scripts/generate-freecad-property-integerset-promotion.mjs @@ -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)) + diff --git a/scripts/run-chrome-property-font.mjs b/scripts/run-chrome-property-font.mjs new file mode 100644 index 0000000..74609f9 --- /dev/null +++ b/scripts/run-chrome-property-font.mjs @@ -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) +} diff --git a/scripts/run-chrome-property-force.mjs b/scripts/run-chrome-property-force.mjs new file mode 100644 index 0000000..b0bac79 --- /dev/null +++ b/scripts/run-chrome-property-force.mjs @@ -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) } diff --git a/scripts/run-chrome-property-heatflux.mjs b/scripts/run-chrome-property-heatflux.mjs new file mode 100644 index 0000000..90db19c --- /dev/null +++ b/scripts/run-chrome-property-heatflux.mjs @@ -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) } + diff --git a/scripts/run-chrome-property-integerset.mjs b/scripts/run-chrome-property-integerset.mjs new file mode 100644 index 0000000..69f12f8 --- /dev/null +++ b/scripts/run-chrome-property-integerset.mjs @@ -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) +} + diff --git a/scripts/run-freecad-native-property-semantics.mjs b/scripts/run-freecad-native-property-semantics.mjs index afc2d6d..bab5415 100644 --- a/scripts/run-freecad-native-property-semantics.mjs +++ b/scripts/run-freecad-native-property-semantics.mjs @@ -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', diff --git a/scripts/run-freecad-property-font-failure.mjs b/scripts/run-freecad-property-font-failure.mjs new file mode 100644 index 0000000..6baa036 --- /dev/null +++ b/scripts/run-freecad-property-font-failure.mjs @@ -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)) diff --git a/scripts/run-freecad-property-font-mutation.mjs b/scripts/run-freecad-property-font-mutation.mjs new file mode 100644 index 0000000..4034464 --- /dev/null +++ b/scripts/run-freecad-property-font-mutation.mjs @@ -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)) diff --git a/scripts/run-freecad-property-font-roundtrip.ts b/scripts/run-freecad-property-font-roundtrip.ts new file mode 100644 index 0000000..37e5f8b --- /dev/null +++ b/scripts/run-freecad-property-font-roundtrip.ts @@ -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 = /]*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)) diff --git a/scripts/run-freecad-property-font-success.mjs b/scripts/run-freecad-property-font-success.mjs new file mode 100644 index 0000000..a6df8f9 --- /dev/null +++ b/scripts/run-freecad-property-font-success.mjs @@ -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)) diff --git a/scripts/run-freecad-property-force-failure.mjs b/scripts/run-freecad-property-force-failure.mjs new file mode 100644 index 0000000..52e7014 --- /dev/null +++ b/scripts/run-freecad-property-force-failure.mjs @@ -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)) diff --git a/scripts/run-freecad-property-force-mutation.mjs b/scripts/run-freecad-property-force-mutation.mjs new file mode 100644 index 0000000..eb4ded2 --- /dev/null +++ b/scripts/run-freecad-property-force-mutation.mjs @@ -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)) diff --git a/scripts/run-freecad-property-force-roundtrip.ts b/scripts/run-freecad-property-force-roundtrip.ts new file mode 100644 index 0000000..96948ea --- /dev/null +++ b/scripts/run-freecad-property-force-roundtrip.ts @@ -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: '' } : 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)) diff --git a/scripts/run-freecad-property-force-success.mjs b/scripts/run-freecad-property-force-success.mjs new file mode 100644 index 0000000..eec68a8 --- /dev/null +++ b/scripts/run-freecad-property-force-success.mjs @@ -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)) diff --git a/scripts/run-freecad-property-heatflux-failure.mjs b/scripts/run-freecad-property-heatflux-failure.mjs new file mode 100644 index 0000000..013ad4d --- /dev/null +++ b/scripts/run-freecad-property-heatflux-failure.mjs @@ -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)) diff --git a/scripts/run-freecad-property-heatflux-mutation.mjs b/scripts/run-freecad-property-heatflux-mutation.mjs new file mode 100644 index 0000000..9430afb --- /dev/null +++ b/scripts/run-freecad-property-heatflux-mutation.mjs @@ -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)) + diff --git a/scripts/run-freecad-property-heatflux-roundtrip.ts b/scripts/run-freecad-property-heatflux-roundtrip.ts new file mode 100644 index 0000000..6b3f5e8 --- /dev/null +++ b/scripts/run-freecad-property-heatflux-roundtrip.ts @@ -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: '' } : 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)) + diff --git a/scripts/run-freecad-property-heatflux-success.mjs b/scripts/run-freecad-property-heatflux-success.mjs new file mode 100644 index 0000000..deb6449 --- /dev/null +++ b/scripts/run-freecad-property-heatflux-success.mjs @@ -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)) diff --git a/scripts/run-freecad-property-integerset-failure.mjs b/scripts/run-freecad-property-integerset-failure.mjs new file mode 100644 index 0000000..06a989b --- /dev/null +++ b/scripts/run-freecad-property-integerset-failure.mjs @@ -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)) diff --git a/scripts/run-freecad-property-integerset-mutation.mjs b/scripts/run-freecad-property-integerset-mutation.mjs new file mode 100644 index 0000000..26fc48d --- /dev/null +++ b/scripts/run-freecad-property-integerset-mutation.mjs @@ -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)) diff --git a/scripts/run-freecad-property-integerset-roundtrip.ts b/scripts/run-freecad-property-integerset-roundtrip.ts new file mode 100644 index 0000000..c5d85de --- /dev/null +++ b/scripts/run-freecad-property-integerset-roundtrip.ts @@ -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)) + diff --git a/scripts/run-freecad-property-integerset-success.mjs b/scripts/run-freecad-property-integerset-success.mjs new file mode 100644 index 0000000..ccc9097 --- /dev/null +++ b/scripts/run-freecad-property-integerset-success.mjs @@ -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)) diff --git a/src/App.tsx b/src/App.tsx index 0ce545e..b4c1e08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1010,6 +1010,7 @@ export function PropertyEditor({ facade, objectId, property, showNotice }: { fac if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) editor = {formatted} else if (property.type === 'App::PropertyFileIncluded') editor = else if (property.type === 'App::PropertyBool') editor = commit(event.target.checked)} /> + else if (property.type === 'App::PropertyFont') editor = { if (!commit(event.target.value)) event.target.value = String(property.value) }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} /> else if (property.type === 'App::PropertyEnumeration') editor = else if (property.type === 'App::PropertyLink') { const document = facade.app.document.getActive() @@ -1055,6 +1056,7 @@ export function PropertyEditor({ facade, objectId, property, showNotice }: { fac editor =
{(['x', 'y', 'z'] as const).map((key) => commit({ ...vector, [key]: Number(event.target.value) })} />)}
} else if (property.type === 'App::PropertyLinkList' || property.type === 'App::PropertyStringList') editor = commit(event.target.value.split(',').map((entry) => entry.trim()).filter(Boolean))} /> else if (property.type === 'App::PropertyBoolList') editor = entry ? 'true' : 'false').join(', ')} aria-label={property.label} onBlur={(event) => { const raw = event.target.value.split(',').map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0); const values = raw.map((entry) => entry === 'true' || entry === '1' ? true : entry === 'false' || entry === '0' ? false : null); if (values.some((value) => value === null) || !commit(values as boolean[])) event.target.value = (property.value as boolean[]).map((entry) => entry ? 'true' : 'false').join(', ') }} /> + else if (property.type === 'App::PropertyIntegerSet') editor = { const raw = event.target.value.split(',').map((entry) => entry.trim()).filter(Boolean); const values = raw.map(Number); if (!values.every(Number.isSafeInteger) || !commit(values)) event.target.value = (property.value as number[]).join(', ') }} /> else if (property.type === 'App::PropertyFloatList') editor = { const values = event.target.value.split(',').map((entry) => Number(entry.trim())); if (!values.every((value) => Number.isFinite(value))) { event.target.value = (property.value as number[]).join(', '); return } if (!commit(values)) event.target.value = (property.value as number[]).join(', ') }} /> else if (property.type === 'App::PropertyColorList') editor = else if (property.type === 'App::PropertyColor' && property.scope === 'view') editor = @@ -1063,7 +1065,7 @@ export function PropertyEditor({ facade, objectId, property, showNotice }: { fac const hex = colorValueHex(color) editor = } - else if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger') editor = + else if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger') editor = else editor = { if (!commit(event.target.value)) event.target.value = String(property.value ?? '') }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} /> return <>
{property.label}
{editor}
{property.expression &&
{property.expression}
} } diff --git a/src/chromePropertyFontHarness.tsx b/src/chromePropertyFontHarness.tsx new file mode 100644 index 0000000..ae10d3d --- /dev/null +++ b/src/chromePropertyFontHarness.tsx @@ -0,0 +1,184 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyEditor } from './App' +import { createProductionFacade } from './facade/productionFacade' +import type { BitBybitWebCadFacade, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types' +import './styles.css' + +type WorkerTrace = { + constructed: Array<{ name: string; url: string }> + requestTypes: string[] + terminated: string[] +} + +type PropertyFontReport = { + schemaVersion: 1 + status: 'running' | 'pass' | 'failed' + browserId: 'chrome' + userAgent: string + crossOriginIsolated: boolean + worker?: WorkerTrace + ui?: { inputType: string; label: string; beforeValue: string; afterValue: string; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] } + persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: string | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null } + resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean } + release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean } + error?: string +} + +const fixture = (): DocumentSnapshot => ({ + id: 'chrome-property-font', + label: 'Chrome PropertyFont', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: [{ id: 'FontProbe', label: 'Font probe', type: 'feature', state: 'valid' }], + objects: [{ + id: 'FontProbe', + typeId: 'TechDraw::DrawViewAnnotation', + properties: [{ name: 'Font', label: 'Font', group: '', scope: 'data', type: 'App::PropertyFont', value: 'osifont', recompute: true }], + }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { FontProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const fontProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('FontProbe')?.properties.find((property) => property.name === 'Font') + +function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { + const [property, setProperty] = useState(() => fontProperty(facade) ?? null) + useEffect(() => facade.subscribe((event) => { + if (event.type === 'state.changed') setProperty(fontProperty(facade) ?? null) + }), [facade]) + if (!property) return
Font property is missing.
+ return
Property view

Font probe

Font
errors.push(message)} />
+} + +const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { + if (predicate()) return + await new Promise((resolve) => setTimeout(resolve, 25)) + } + throw new Error(`Timed out waiting for ${message}.`) +} + +const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] } +const BrowserWorker = globalThis.Worker +class TrackedWorker extends BrowserWorker { + private readonly traceName: string + + constructor(scriptURL: string | URL, options?: WorkerOptions) { + super(scriptURL, options) + this.traceName = options?.name ?? 'unnamed' + trace.constructed.push({ name: this.traceName, url: String(scriptURL) }) + } + + override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) { + if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type) + if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions) + else if (transferOrOptions) super.postMessage(message, transferOrOptions) + else super.postMessage(message) + } + + override terminate() { + trace.terminated.push(this.traceName) + super.terminate() + } +} +Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker }) + +const initial = 'osifont' +const target = 'Noto Sans CJK SC' +const report: PropertyFontReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = [] +const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['FontProbe'] }) +const root = createRoot(document.getElementById('root')!) +root.render() + +try { + await waitFor(() => document.querySelector('input[aria-label="Font"]') !== null, 'the production PropertyFont editor') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const input = document.querySelector('input[aria-label="Font"]')! + const beforeValue = String(fontProperty(facade)?.value ?? '') + input.focus() + input.value = target + input.dispatchEvent(new Event('input', { bubbles: true })) + input.blur() + await waitFor(() => facade.app.document.getActive().version === 2, 'the Font property commit') + const edited = facade.app.document.getActive() + report.ui = { + inputType: input.type, + label: input.getAttribute('aria-label') ?? '', + beforeValue, + afterValue: String(fontProperty(facade)?.value ?? ''), + documentVersion: edited.version, + dirty: edited.dirty, + objectState: edited.recompute?.objectStates.FontProbe ?? null, + errors, + } + + const saved = await facade.project.save() + const loaded = await facade.project.load(edited.id) + const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version) + const recovery = await facade.project.recovery(edited.id) + const loadedProperty = loaded?.objects.find((object) => object.id === 'FontProbe')?.properties.find((property) => property.name === 'Font') + const archive = facade.project.fcstd.serializeMetadata(edited) + const archiveProperty = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'FontProbe')?.properties.find((property) => property.name === 'Font') + const capabilities = facade.project.capabilities() + report.persistence = { + mode: capabilities.mode, + sqliteWasm: capabilities.sqliteWasm, + opfs: capabilities.opfs, + savedMode: saved.mode, + loadedValue: typeof loadedProperty?.value === 'string' ? loadedProperty.value : null, + loadedType: loadedProperty?.type ?? null, + checkpointVersion: checkpoint?.version ?? null, + recoveryIntegrity: recovery.integrity, + fcstdElement: archiveProperty?.element ?? null, + fcstdValue: archiveProperty?.value ?? null, + } + + const payload = new TextEncoder().encode('PropertyFont Chrome resource bytes') + const stored = await facade.project.resource.put(payload, 'application/octet-stream') + const restored = await facade.project.resource.get(stored.hash) + await facade.project.resource.release(stored.hash) + const released = await facade.project.resource.get(stored.hash) === null + const directory = await navigator.storage.getDirectory() + const markerName = 'property-font-chrome-marker.json' + const marker = await directory.getFileHandle(markerName, { create: true }) + const writable = await marker.createWritable() + await writable.write(JSON.stringify({ suite: 'PROP-FONT-H', value: target })) + await writable.close() + await directory.removeEntry(markerName) + let markerRemoved = false + try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } + report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved } +} catch (error) { + report.error = error instanceof Error ? error.stack || error.message : String(error) +} finally { + root.unmount() + facade.geometry.dispose() + await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` }) + Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker }) + const geometry = facade.geometry.capabilities() + report.worker = trace + report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') } +} + +const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose'] +report.status = !report.error + && report.crossOriginIsolated + && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true + && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type)) + && report.ui?.inputType === 'text' && report.ui.label === 'Font' && report.ui.beforeValue === initial && report.ui.afterValue === target && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0 + && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && 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 + && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved + && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated + ? 'pass' : 'failed' + +document.documentElement.dataset.status = report.status +document.body.textContent = JSON.stringify(report, null, 2) +await fetch('/__chrome-property-font-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }) +if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyFont harness failed.') + +export {} diff --git a/src/chromePropertyForceHarness.tsx b/src/chromePropertyForceHarness.tsx new file mode 100644 index 0000000..362834c --- /dev/null +++ b/src/chromePropertyForceHarness.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyEditor } from './App' +import { createProductionFacade } from './facade/productionFacade' +import type { BitBybitWebCadFacade, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types' +import './styles.css' + +type WorkerTrace = { constructed: Array<{ name: string; url: string }>; requestTypes: string[]; terminated: string[] } +type PropertyForceReport = { + schemaVersion: 1 + status: 'running' | 'pass' | 'failed' + browserId: 'chrome' + userAgent: string + crossOriginIsolated: boolean + worker?: WorkerTrace + ui?: { inputType: string; label: string; unit: string; beforeValue: number; afterValue: number; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] } + persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: number | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null } + resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean } + release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean } + error?: string +} + +const fixture = (): DocumentSnapshot => ({ + id: 'chrome-property-force', label: 'Chrome PropertyForce', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'ForceProbe', label: 'Force probe', type: 'feature', state: 'valid' }], + objects: [{ id: 'ForceProbe', typeId: 'Fem::ConstraintForce', properties: [{ name: 'Force', label: 'Force', group: '', scope: 'data', type: 'App::PropertyForce', value: 0, unit: 'mm*kg/s^2', recompute: true }] }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { ForceProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const forceProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('ForceProbe')?.properties.find(({ name }) => name === 'Force') + +function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { + const [property, setProperty] = useState(() => forceProperty(facade) ?? null) + useEffect(() => facade.subscribe((event) => { if (event.type === 'state.changed') setProperty(forceProperty(facade) ?? null) }), [facade]) + if (!property) return
Force property is missing.
+ return
Property view

Force probe

Force
errors.push(message)} />
+} + +const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 25)) } + throw new Error(`Timed out waiting for ${message}.`) +} + +const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] } +const BrowserWorker = globalThis.Worker +class TrackedWorker extends BrowserWorker { + private readonly traceName: string + constructor(scriptURL: string | URL, options?: WorkerOptions) { super(scriptURL, options); this.traceName = options?.name ?? 'unnamed'; trace.constructed.push({ name: this.traceName, url: String(scriptURL) }) } + override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) { + if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type) + if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions) + else if (transferOrOptions) super.postMessage(message, transferOrOptions) + else super.postMessage(message) + } + override terminate() { trace.terminated.push(this.traceName); super.terminate() } +} +Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker }) + +const initial = 0 +const target = 500000 +const report: PropertyForceReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = [] +const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['ForceProbe'] }) +const root = createRoot(document.getElementById('root')!) +root.render() + +try { + await waitFor(() => document.querySelector('input[aria-label="Force"]') !== null, 'the production PropertyForce editor') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const input = document.querySelector('input[aria-label="Force"]')! + const beforeValue = Number(input.value) + input.focus(); input.value = String(target); input.dispatchEvent(new Event('input', { bubbles: true })); input.blur() + await waitFor(() => facade.app.document.getActive().version === 2, 'the Force property commit') + const edited = facade.app.document.getActive() + report.ui = { inputType: input.type, label: input.getAttribute('aria-label') ?? '', unit: input.closest('label')?.querySelector('span')?.textContent ?? '', beforeValue, afterValue: Number(forceProperty(facade)?.value), documentVersion: edited.version, dirty: edited.dirty, objectState: edited.recompute?.objectStates.ForceProbe ?? null, errors } + + const saved = await facade.project.save() + const loaded = await facade.project.load(edited.id) + const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version) + const recovery = await facade.project.recovery(edited.id) + const loadedProperty = loaded?.objects.find(({ id }) => id === 'ForceProbe')?.properties.find(({ name }) => name === 'Force') + const archive = facade.project.fcstd.serializeMetadata(edited) + const archiveProperty = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force') + const capabilities = facade.project.capabilities() + report.persistence = { mode: capabilities.mode, sqliteWasm: capabilities.sqliteWasm, opfs: capabilities.opfs, savedMode: saved.mode, loadedValue: typeof loadedProperty?.value === 'number' ? loadedProperty.value : null, loadedType: loadedProperty?.type ?? null, checkpointVersion: checkpoint?.version ?? null, recoveryIntegrity: recovery.integrity, fcstdElement: archiveProperty?.element ?? null, fcstdValue: archiveProperty?.value ?? null } + + const payload = new TextEncoder().encode('PropertyForce Chrome resource bytes') + const stored = await facade.project.resource.put(payload, 'application/octet-stream') + const restored = await facade.project.resource.get(stored.hash) + await facade.project.resource.release(stored.hash) + const released = await facade.project.resource.get(stored.hash) === null + const directory = await navigator.storage.getDirectory() + const markerName = 'property-force-chrome-marker.json' + const marker = await directory.getFileHandle(markerName, { create: true }) + const writable = await marker.createWritable() + await writable.write(JSON.stringify({ suite: 'PROP-FORCE-H', value: target })) + await writable.close(); await directory.removeEntry(markerName) + let markerRemoved = false + try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } + report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved } +} catch (error) { + report.error = error instanceof Error ? error.stack || error.message : String(error) +} finally { + root.unmount(); facade.geometry.dispose() + await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` }) + Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker }) + const geometry = facade.geometry.capabilities() + report.worker = trace + report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') } +} + +const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose'] +report.status = !report.error && report.crossOriginIsolated + && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true + && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type)) + && report.ui?.inputType === 'number' && report.ui.label === 'Force' && report.ui.unit === 'mm*kg/s^2' && report.ui.beforeValue === initial && report.ui.afterValue === target && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0 + && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && report.persistence.loadedValue === target && report.persistence.loadedType === 'App::PropertyForce' && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'Float' && report.persistence.fcstdValue === String(target) + && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved + && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated ? 'pass' : 'failed' + +document.documentElement.dataset.status = report.status +document.body.textContent = JSON.stringify(report, null, 2) +await fetch('/__chrome-property-force-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }) +if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyForce harness failed.') + +export {} diff --git a/src/chromePropertyHeatFluxHarness.tsx b/src/chromePropertyHeatFluxHarness.tsx new file mode 100644 index 0000000..041f1f6 --- /dev/null +++ b/src/chromePropertyHeatFluxHarness.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyEditor } from './App' +import { createProductionFacade } from './facade/productionFacade' +import type { BitBybitWebCadFacade, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types' +import './styles.css' + +type WorkerTrace = { constructed: Array<{ name: string; url: string }>; requestTypes: string[]; terminated: string[] } +type PropertyHeatFluxReport = { + schemaVersion: 1 + status: 'running' | 'pass' | 'failed' + browserId: 'chrome' + userAgent: string + crossOriginIsolated: boolean + worker?: WorkerTrace + ui?: { inputType: string; label: string; unit: string; beforeValue: number; afterValue: number; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] } + persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: number | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null } + resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean } + release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean } + error?: string +} + +const fixture = (): DocumentSnapshot => ({ + id: 'chrome-property-heatflux', label: 'Chrome PropertyHeatFlux', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'HeatFluxProbe', label: 'Heat flux probe', type: 'feature', state: 'valid' }], + objects: [{ id: 'HeatFluxProbe', typeId: 'Fem::ConstraintHeatflux', properties: [{ name: 'DFlux', label: 'DFlux', group: 'ConstraintHeatflux', scope: 'data', type: 'App::PropertyHeatFlux', value: 0, unit: 'kg/s^3', recompute: true }] }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { HeatFluxProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const heatFluxProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux') + +function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { + const [property, setProperty] = useState(() => heatFluxProperty(facade) ?? null) + useEffect(() => facade.subscribe((event) => { if (event.type === 'state.changed') setProperty(heatFluxProperty(facade) ?? null) }), [facade]) + if (!property) return
DFlux property is missing.
+ return
Property view

Heat flux probe

ConstraintHeatflux
errors.push(message)} />
+} + +const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 25)) } + throw new Error(`Timed out waiting for ${message}.`) +} + +const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] } +const BrowserWorker = globalThis.Worker +class TrackedWorker extends BrowserWorker { + private readonly traceName: string + constructor(scriptURL: string | URL, options?: WorkerOptions) { super(scriptURL, options); this.traceName = options?.name ?? 'unnamed'; trace.constructed.push({ name: this.traceName, url: String(scriptURL) }) } + override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) { + if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type) + if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions) + else if (transferOrOptions) super.postMessage(message, transferOrOptions) + else super.postMessage(message) + } + override terminate() { trace.terminated.push(this.traceName); super.terminate() } +} +Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker }) + +const initial = 0 +const target = 500 +const report: PropertyHeatFluxReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = [] +const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['HeatFluxProbe'] }) +const root = createRoot(document.getElementById('root')!) +root.render() + +try { + await waitFor(() => document.querySelector('input[aria-label="DFlux"]') !== null, 'the production PropertyHeatFlux editor') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const input = document.querySelector('input[aria-label="DFlux"]')! + const beforeValue = Number(input.value) + input.focus(); input.value = String(target); input.dispatchEvent(new Event('input', { bubbles: true })); input.blur() + await waitFor(() => facade.app.document.getActive().version === 2, 'the DFlux property commit') + const edited = facade.app.document.getActive() + report.ui = { inputType: input.type, label: input.getAttribute('aria-label') ?? '', unit: input.closest('label')?.querySelector('span')?.textContent ?? '', beforeValue, afterValue: Number(heatFluxProperty(facade)?.value), documentVersion: edited.version, dirty: edited.dirty, objectState: edited.recompute?.objectStates.HeatFluxProbe ?? null, errors } + + const saved = await facade.project.save() + const loaded = await facade.project.load(edited.id) + const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version) + const recovery = await facade.project.recovery(edited.id) + const loadedProperty = loaded?.objects.find(({ id }) => id === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux') + const archive = facade.project.fcstd.serializeMetadata(edited) + const archiveProperty = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux') + const capabilities = facade.project.capabilities() + report.persistence = { mode: capabilities.mode, sqliteWasm: capabilities.sqliteWasm, opfs: capabilities.opfs, savedMode: saved.mode, loadedValue: typeof loadedProperty?.value === 'number' ? loadedProperty.value : null, loadedType: loadedProperty?.type ?? null, checkpointVersion: checkpoint?.version ?? null, recoveryIntegrity: recovery.integrity, fcstdElement: archiveProperty?.element ?? null, fcstdValue: archiveProperty?.value ?? null } + + const payload = new TextEncoder().encode('PropertyHeatFlux Chrome resource bytes') + const stored = await facade.project.resource.put(payload, 'application/octet-stream') + const restored = await facade.project.resource.get(stored.hash) + await facade.project.resource.release(stored.hash) + const released = await facade.project.resource.get(stored.hash) === null + const directory = await navigator.storage.getDirectory() + const markerName = 'property-heatflux-chrome-marker.json' + const marker = await directory.getFileHandle(markerName, { create: true }) + const writable = await marker.createWritable() + await writable.write(JSON.stringify({ suite: 'PROP-HEATFLUX-H', value: target })) + await writable.close(); await directory.removeEntry(markerName) + let markerRemoved = false + try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } + report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved } +} catch (error) { + report.error = error instanceof Error ? error.stack || error.message : String(error) +} finally { + root.unmount(); facade.geometry.dispose() + await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` }) + Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker }) + const geometry = facade.geometry.capabilities() + report.worker = trace + report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') } +} + +const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose'] +report.status = !report.error && report.crossOriginIsolated + && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true + && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type)) + && report.ui?.inputType === 'number' && report.ui.label === 'DFlux' && report.ui.unit === 'kg/s^3' && report.ui.beforeValue === initial && report.ui.afterValue === target && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0 + && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && report.persistence.loadedValue === target && report.persistence.loadedType === 'App::PropertyHeatFlux' && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'Float' && report.persistence.fcstdValue === String(target) + && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved + && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated ? 'pass' : 'failed' + +document.documentElement.dataset.status = report.status +document.body.textContent = JSON.stringify(report, null, 2) +await fetch('/__chrome-property-heatflux-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }) +if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyHeatFlux harness failed.') + +export {} diff --git a/src/chromePropertyIntegerSetHarness.tsx b/src/chromePropertyIntegerSetHarness.tsx new file mode 100644 index 0000000..be8b9fe --- /dev/null +++ b/src/chromePropertyIntegerSetHarness.tsx @@ -0,0 +1,190 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyEditor } from './App' +import { createProductionFacade } from './facade/productionFacade' +import type { BitBybitWebCadFacade, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types' +import './styles.css' + +type WorkerTrace = { + constructed: Array<{ name: string; url: string }> + requestTypes: string[] + terminated: string[] +} + +type PropertyIntegerSetReport = { + schemaVersion: 1 + status: 'running' | 'pass' | 'failed' + browserId: 'chrome' + userAgent: string + crossOriginIsolated: boolean + worker?: WorkerTrace + ui?: { inputType: string; label: string; beforeText: string; afterText: string; beforeValue: number[]; afterValue: number[]; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] } + persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: number[] | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null } + resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean } + release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean } + error?: string +} + +const fixture = (): DocumentSnapshot => ({ + id: 'chrome-property-integerset', + label: 'Chrome PropertyIntegerSet', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: [{ id: 'NodesProbe', label: 'Nodes probe', type: 'feature', state: 'valid' }], + objects: [{ + id: 'NodesProbe', + typeId: 'Fem::FemSetNodesObject', + properties: [{ name: 'Nodes', label: 'Nodes', group: 'Node indexes', scope: 'data', type: 'App::PropertyIntegerSet', value: [1, 3], recompute: true }], + }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { NodesProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const integerSetProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('NodesProbe')?.properties.find((property) => property.name === 'Nodes') + +function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { + const [property, setProperty] = useState(() => integerSetProperty(facade) ?? null) + useEffect(() => facade.subscribe((event) => { + if (event.type === 'state.changed') setProperty(integerSetProperty(facade) ?? null) + }), [facade]) + if (!property) return
Nodes property is missing.
+ return
Property view

Nodes probe

Node indexes
errors.push(message)} />
+} + +const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { + if (predicate()) return + await new Promise((resolve) => setTimeout(resolve, 25)) + } + throw new Error(`Timed out waiting for ${message}.`) +} + +const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] } +const BrowserWorker = globalThis.Worker +class TrackedWorker extends BrowserWorker { + private readonly traceName: string + + constructor(scriptURL: string | URL, options?: WorkerOptions) { + super(scriptURL, options) + this.traceName = options?.name ?? 'unnamed' + trace.constructed.push({ name: this.traceName, url: String(scriptURL) }) + } + + override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) { + if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type) + if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions) + else if (transferOrOptions) super.postMessage(message, transferOrOptions) + else super.postMessage(message) + } + + override terminate() { + trace.terminated.push(this.traceName) + super.terminate() + } +} +Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker }) + +const target = [-1, 8] +const report: PropertyIntegerSetReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = [] +const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['NodesProbe'] }) +const root = createRoot(document.getElementById('root')!) +root.render() + +try { + await waitFor(() => document.querySelector('input[aria-label="Nodes"]') !== null, 'the production PropertyIntegerSet editor') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const input = document.querySelector('input[aria-label="Nodes"]')! + const beforeText = input.value + const beforeValue = [...(integerSetProperty(facade)?.value as number[])] + input.focus() + input.value = '8, -1, 8' + input.dispatchEvent(new Event('input', { bubbles: true })) + input.blur() + await waitFor(() => facade.app.document.getActive().version === 2, 'the UI property commit') + const edited = facade.app.document.getActive() + const editedProperty = integerSetProperty(facade) + report.ui = { + inputType: input.type, + label: input.getAttribute('aria-label') ?? '', + beforeText, + afterText: (editedProperty?.value as number[]).join(', '), + beforeValue, + afterValue: [...(editedProperty?.value as number[])], + documentVersion: edited.version, + dirty: edited.dirty, + objectState: edited.recompute?.objectStates.NodesProbe ?? null, + errors, + } + + const saved = await facade.project.save() + const loaded = await facade.project.load(edited.id) + const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version) + const recovery = await facade.project.recovery(edited.id) + const loadedProperty = loaded?.objects.find((object) => object.id === 'NodesProbe')?.properties.find((property) => property.name === 'Nodes') + const loadedValue = loadedProperty?.value + const loadedIntegerSet = Array.isArray(loadedValue) && loadedValue.every((entry): entry is number => typeof entry === 'number' && Number.isSafeInteger(entry)) ? loadedValue : null + const archive = facade.project.fcstd.serializeMetadata(edited) + const archiveProperty = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'NodesProbe')?.properties.find((property) => property.name === 'Nodes') + const capabilities = facade.project.capabilities() + report.persistence = { + mode: capabilities.mode, + sqliteWasm: capabilities.sqliteWasm, + opfs: capabilities.opfs, + savedMode: saved.mode, + loadedValue: loadedIntegerSet ? [...loadedIntegerSet] : null, + loadedType: loadedProperty?.type ?? null, + checkpointVersion: checkpoint?.version ?? null, + recoveryIntegrity: recovery.integrity, + fcstdElement: archiveProperty?.element ?? null, + fcstdValue: archiveProperty?.value ?? null, + } + + const payload = new TextEncoder().encode('PropertyIntegerSet Chrome resource') + const stored = await facade.project.resource.put(payload, 'application/vnd.freecad.property-integerset+json') + const restored = await facade.project.resource.get(stored.hash) + await facade.project.resource.release(stored.hash) + const released = await facade.project.resource.get(stored.hash) === null + const directory = await navigator.storage.getDirectory() + const markerName = 'property-integerset-chrome-marker.json' + const marker = await directory.getFileHandle(markerName, { create: true }) + const writable = await marker.createWritable() + await writable.write(JSON.stringify({ suite: 'PROP-INTEGERSET-H', value: target })) + await writable.close() + await directory.removeEntry(markerName) + let markerRemoved = false + try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' } + report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved } +} catch (error) { + report.error = error instanceof Error ? error.stack || error.message : String(error) +} finally { + root.unmount() + facade.geometry.dispose() + await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` }) + Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker }) + const geometry = facade.geometry.capabilities() + report.worker = trace + report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') } +} + +const same = (left: number[] | null | undefined, right: number[]) => Array.isArray(left) && left.length === right.length && left.every((entry, index) => entry === right[index]) +const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose'] +report.status = !report.error + && report.crossOriginIsolated + && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true + && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type)) + && report.ui?.inputType === 'text' && report.ui.label === 'Nodes' && report.ui.beforeText === '1, 3' && report.ui.afterText === '-1, 8' && same(report.ui.beforeValue, [1, 3]) && same(report.ui.afterValue, target) && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0 + && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && same(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]' + && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved + && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated + ? 'pass' : 'failed' + +document.documentElement.dataset.status = report.status +document.body.textContent = JSON.stringify(report, null, 2) +await fetch('/__chrome-property-integerset-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }) +if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyIntegerSet harness failed.') + +export {} diff --git a/src/facade/fcstd.ts b/src/facade/fcstd.ts index b58c547..a9e2667 100644 --- a/src/facade/fcstd.ts +++ b/src/facade/fcstd.ts @@ -55,13 +55,15 @@ export type FcstdPropertySummary = { export type FcstdDecodedPropertyValue = { value: unknown; decoded: boolean; error?: string } export type FcstdPathAccessOptions = { allowFeaturePython?: boolean } export type FcstdPathEdit = { objectName: string; propertyName?: string; value: PathPropertyValue; allowFeaturePython?: boolean } -export type FcstdNumericPropertyEdit = { objectName: string; propertyName: string; typeId: 'App::PropertyAcceleration'; value: number; expectedValue?: number } +export type FcstdNumericPropertyEdit = { objectName: string; propertyName: string; typeId: 'App::PropertyAcceleration' | 'App::PropertyForce' | 'App::PropertyHeatFlux'; value: number; expectedValue?: number } export type FcstdMeasureAreaEdit = { objectName: string; elements: LinkSubListValue; area: number; expectedElements?: LinkSubListValue; expectedArea?: number } export type FcstdBoolListEdit = { objectName: string; propertyName: string; value: boolean[]; expectedValue?: boolean[] } +export type FcstdIntegerSetEdit = { objectName: string; propertyName: string; value: number[]; expectedValue?: number[] } export type FcstdColorEdit = { objectName: string; propertyName: string; value: ColorValue; expectedValue?: ColorValue } export type FcstdColorListEdit = { objectName: string; propertyName: string; value: ColorListValue; expectedValue?: ColorListValue } export type FcstdDirectionEdit = { objectName: string; propertyName: string; value: VectorValue; expectedValue?: VectorValue } export type FcstdFileEdit = { objectName: string; propertyName: string; value: string; expectedValue?: string } +export type FcstdFontEdit = { objectName: string; propertyName: string; value: string; expectedValue?: string } export type FcstdIncludedFileEdit = { objectName: string; propertyName: string; value: IncludedFileValue; expectedValue?: IncludedFileValue } export type FcstdIncludedFileResourceEdit = FcstdIncludedFileEdit & { resourceBytes: Uint8Array; expectedResourceBytes?: Uint8Array } @@ -405,6 +407,12 @@ const propertyFileValue = (value: unknown, name: string) => { if (value && (value.includes('\0') || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value) || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.split('/').some((segment) => segment === '.' || segment === '..' || segment.length === 0))) throw new RangeError(`FCStd PropertyFile ${name} requires a safe project-relative file reference.`) return value } +const propertyFontValue = (value: unknown, name: string) => { + if (typeof value !== 'string') throw new TypeError(`FCStd PropertyFont ${name} requires a string.`) + if (value.length > 1024) throw new RangeError(`FCStd PropertyFont ${name} family name is too long.`) + if (value.includes('\0') || /[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) throw new RangeError(`FCStd PropertyFont ${name} contains unsupported control characters.`) + return value +} const guiPropertyXml = (property: ObjectPropertySnapshot) => { const nativeType = property.type === 'App::PropertyFloat' && ['Deviation', 'LineWidth', 'PointSize'].includes(property.name) ? 'App::PropertyFloatConstraint' : property.type @@ -453,9 +461,11 @@ const nativeDataPropertyNames = new Map>([ ['App::Part', new Set(['Color'])], ['Assembly::AssemblyLink', new Set(['Color'])], ['Assembly::AssemblyObject', new Set(['Color'])], - ['TechDraw::DrawViewAnnotation', new Set(['TextColor'])], + ['Fem::ConstraintForce', new Set(['Force'])], + ['Fem::ConstraintRigidBody', new Set(['ForceX', 'ForceY', 'ForceZ'])], + ['TechDraw::DrawViewAnnotation', new Set(['Font', 'TextColor'])], ['TechDraw::DrawViewDraft', new Set(['Color'])], - ['TechDraw::DrawViewSpreadsheet', new Set(['TextColor'])], + ['TechDraw::DrawViewSpreadsheet', new Set(['Font', 'TextColor'])], ['Surface::GeomFillSurface', new Set(['ReversedList'])], ['App::Link', new Set(['VisibilityList'])], ['App::LinkGroup', new Set(['VisibilityList'])], @@ -1185,6 +1195,12 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId const values = (property.value as number[]).map((entry) => ``).join('') return `${values}` } + if (property.type === 'App::PropertyIntegerSet') { + if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet property ${property.name} requires a safe-integer array.`) + const normalized = [...new Set(property.value as number[])].sort((left, right) => left - right) + const values = normalized.map((entry) => ``).join('') + return `${values}` + } if (property.type === 'App::PropertyStringList') { if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'string')) throw new TypeError(`FCStd StringList property ${property.name} requires a string list.`) const values = (property.value as string[]).map((entry) => ``).join('') @@ -1213,12 +1229,13 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId if (typeof property.value !== 'string') throw new TypeError(`FCStd String property ${property.name} requires a string.`) return `` } + if (property.type === 'App::PropertyFont') return `` if (property.type === 'App::PropertyFile') return `` if (property.type === 'App::PropertyBool') { if (typeof property.value !== 'boolean') throw new TypeError(`FCStd Bool property ${property.name} requires a boolean.`) return `` } - if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision') { + if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision') { if (typeof property.value !== 'number' || !Number.isFinite(property.value)) throw new TypeError(`FCStd numeric property ${property.name} requires a finite number.`) return `` } @@ -1666,6 +1683,18 @@ const structuredPropertyValue = (element: string, elementValue: unknown, propert if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error(`FCStd IntegerList count does not match its child elements.`) return JSON.stringify(values) } + if (element === 'IntegerSet' && elementValue && typeof elementValue === 'object') { + const values = asArray((elementValue as Record).I as Record | Record[] | undefined).map((entry, index) => { + const text = attribute(entry, 'v') || attribute(entry, 'value') + const value = Number(text) + if (!/^-?\d+$/.test(text) || !Number.isSafeInteger(value)) throw new Error(`FCStd IntegerSet item ${index} must be a safe integer.`) + return value + }) + const countText = attribute(elementValue, 'count') + if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error('FCStd IntegerSet count does not match its child elements.') + if (new Set(values).size !== values.length) throw new Error('FCStd IntegerSet contains duplicate values.') + return JSON.stringify([...values].sort((left, right) => left - right)) + } if (element === 'StringList' && elementValue && typeof elementValue === 'object') { const values = asArray((elementValue as Record).String as Record | Record[] | undefined).map((entry) => attribute(entry, 'value')) const countText = attribute(elementValue, 'count') @@ -2125,7 +2154,7 @@ const sketchFromPropertySummaries = (objectId: string, summaries: FcstdPropertyS } export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdDecodedPropertyValue => { - const numericTypes = new Set(['App::PropertyLength', 'App::PropertyDistance', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyArea', 'App::PropertyQuantityConstraint', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyPrecision', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyPercent']) + const numericTypes = new Set(['App::PropertyLength', 'App::PropertyDistance', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyArea', 'App::PropertyForce', 'App::PropertyHeatFlux', 'App::PropertyQuantityConstraint', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyPrecision', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyPercent']) if (numericTypes.has(property.typeId)) { const value = Number(property.value) return Number.isFinite(value) ? { value, decoded: true } : { value: property.value, decoded: false, error: `Property ${property.name} is not a finite number.` } @@ -2167,6 +2196,14 @@ export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdD return { value: property.value, decoded: false, error: error instanceof Error ? error.message : String(error) } } } + if (property.typeId === 'App::PropertyFont') { + if (property.element !== 'String') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native String payload.` } + try { + return { value: propertyFontValue(property.value, property.name), decoded: true } + } catch (error) { + return { value: property.value, decoded: false, error: error instanceof Error ? error.message : String(error) } + } + } if (property.typeId === 'App::PropertyFileIncluded') { if (property.element !== 'FileIncluded') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native FileIncluded payload.` } if (!property.resourcePath) return { value: null, decoded: true } @@ -2189,7 +2226,7 @@ export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdD } } if (property.typeId === 'App::PropertyDirection' && property.element !== 'PropertyVector') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native PropertyVector payload.` } - if (property.typeId === 'App::PropertyLinkSub' || property.typeId === 'App::PropertyLinkSubList' || property.typeId === 'App::PropertyLinkList' || property.typeId === 'App::PropertyBoolList' || property.typeId === 'App::PropertyFloatList' || property.typeId === 'App::PropertyIntegerList' || property.typeId === 'App::PropertyStringList' || property.typeId === 'App::PropertyVector' || property.typeId === 'App::PropertyDirection' || property.typeId === 'App::PropertyPlacement' || property.typeId === 'App::PropertyMultiTransform' || property.typeId === 'Part::PropertyGeometryList' || property.typeId === 'Sketcher::PropertyConstraintList') { + if (property.typeId === 'App::PropertyLinkSub' || property.typeId === 'App::PropertyLinkSubList' || property.typeId === 'App::PropertyLinkList' || property.typeId === 'App::PropertyBoolList' || property.typeId === 'App::PropertyFloatList' || property.typeId === 'App::PropertyIntegerList' || property.typeId === 'App::PropertyIntegerSet' || property.typeId === 'App::PropertyStringList' || property.typeId === 'App::PropertyVector' || property.typeId === 'App::PropertyDirection' || property.typeId === 'App::PropertyPlacement' || property.typeId === 'App::PropertyMultiTransform' || property.typeId === 'Part::PropertyGeometryList' || property.typeId === 'Sketcher::PropertyConstraintList') { try { return { value: JSON.parse(property.value), decoded: true } } catch (error) { return { value: property.value, decoded: false, error: `Property ${property.name} contains invalid JSON: ${error instanceof Error ? error.message : String(error)}` } } } return { value: property.value, decoded: true } @@ -2325,6 +2362,54 @@ export const rewriteFcstdBoolListProperty = (bytes: Uint8Array, edit: FcstdBoolL return rewritten } +/** Rewrites one native App::PropertyIntegerSet while preserving every other archive entry. */ +export const rewriteFcstdIntegerSetProperty = (bytes: Uint8Array, edit: FcstdIntegerSetEdit): Uint8Array => { + if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd IntegerSet rewrite requires non-empty object and property names.') + const normalize = (value: number[], context: string) => { + if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet ${context} requires a safe-integer array.`) + return [...new Set(value)].sort((left, right) => left - right) + } + const equal = (left: number[], right: number[]) => left.length === right.length && left.every((entry, index) => entry === right[index]) + const nextValue = normalize(edit.value, 'value') + const expectedValue = edit.expectedValue === undefined ? undefined : normalize(edit.expectedValue, 'expectedValue') + + const inspection = inspectFcstdArchive(bytes) + const object = inspection.objects.find((candidate) => candidate.name === edit.objectName) + if (!object) throw new Error(`FCStd IntegerSet object does not exist: ${edit.objectName}`) + const summary = object.properties.find((candidate) => candidate.name === edit.propertyName) + if (!summary) throw new Error(`FCStd IntegerSet property does not exist: ${edit.objectName}.${edit.propertyName}`) + if (summary.typeId !== 'App::PropertyIntegerSet' || summary.element !== 'IntegerSet') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyIntegerSet with an IntegerSet payload.`) + const decoded = decodeFcstdPropertyValue(summary) + if (!decoded.decoded || !Array.isArray(decoded.value) || !decoded.value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} is not decodable.`) + if (expectedValue !== undefined && !equal(decoded.value as number[], expectedValue)) throw new RangeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} does not match expectedValue.`) + + const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS) + const files = unzipSync(bytes) + const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path + if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.') + const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS) + const declarations = asArray((((parsed.root.Objects as Record | undefined)?.Object) as Record | Record[] | undefined)).filter((candidate) => attribute(candidate, 'name') === edit.objectName) + if (declarations.length !== 1) throw new Error(`FCStd IntegerSet object declaration is not unique: ${edit.objectName}`) + declarations[0]['@_Touched'] = '1' + const objectData = asArray((((parsed.root.ObjectData as Record | undefined)?.Object) as Record | Record[] | undefined)).filter((candidate) => attribute(candidate, 'name') === edit.objectName) + if (objectData.length !== 1) throw new Error(`FCStd IntegerSet object data is not unique: ${edit.objectName}`) + const properties = asArray(((((objectData[0].Properties as Record | undefined)?.Property)) as Record | Record[] | undefined)) + const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyIntegerSet') + if (candidates.length !== 1 || !candidates[0].IntegerSet || typeof candidates[0].IntegerSet !== 'object' || Array.isArray(candidates[0].IntegerSet)) throw new TypeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} has no unique IntegerSet payload.`) + const payload = candidates[0].IntegerSet as Record + payload['@_count'] = String(nextValue.length) + payload.I = nextValue.map((value) => ({ '@_v': String(value) })) + const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root }) + files[documentPath] = new TextEncoder().encode(documentXml) + const output: Record = {} + for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path]) + const rewritten = new Uint8Array(zipSync(output, { level: 6 })) + const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName) + const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined + if (!rewrittenValue?.decoded || !Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as number[], nextValue)) throw new Error(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`) + return rewritten +} + /** Rewrites one native App::PropertyColor packed RGBA value while preserving every other archive entry. */ export const rewriteFcstdColorProperty = (bytes: Uint8Array, edit: FcstdColorEdit): Uint8Array => { if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyColor rewrite requires non-empty object and property names.') @@ -2519,6 +2604,48 @@ export const rewriteFcstdFileProperty = (bytes: Uint8Array, edit: FcstdFileEdit) return rewritten } +/** Rewrites one native App::PropertyFont String payload while retaining its distinct TypeId. */ +export const rewriteFcstdFontProperty = (bytes: Uint8Array, edit: FcstdFontEdit): Uint8Array => { + if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyFont rewrite requires non-empty object and property names.') + const value = propertyFontValue(edit.value, `${edit.objectName}.${edit.propertyName}`) + const expectedValue = edit.expectedValue === undefined ? undefined : propertyFontValue(edit.expectedValue, `${edit.objectName}.${edit.propertyName}.expectedValue`) + + const inspection = inspectFcstdArchive(bytes) + const object = inspection.objects.find((candidate) => candidate.name === edit.objectName) + if (!object) throw new Error(`FCStd PropertyFont object does not exist: ${edit.objectName}`) + const summary = object.properties.find((candidate) => candidate.name === edit.propertyName) + if (!summary) throw new Error(`FCStd PropertyFont property does not exist: ${edit.objectName}.${edit.propertyName}`) + if (summary.typeId !== 'App::PropertyFont' || summary.element !== 'String') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyFont with a String payload.`) + const decoded = decodeFcstdPropertyValue(summary) + if (!decoded.decoded || typeof decoded.value !== 'string') throw new TypeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} is not decodable.`) + if (expectedValue !== undefined && decoded.value !== expectedValue) throw new RangeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} does not match expectedValue.`) + + const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS) + const files = unzipSync(bytes) + const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path + if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.') + const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS) + const objectDeclarations = asArray((((parsed.root.Objects as Record | undefined)?.Object) as Record | Record[] | undefined)) + const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName) + if (declarations.length !== 1) throw new Error(`FCStd PropertyFont object declaration is not unique: ${edit.objectName}`) + declarations[0]['@_Touched'] = '1' + const objectData = asArray((((parsed.root.ObjectData as Record | undefined)?.Object) as Record | Record[] | undefined)) + const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName) + if (data.length !== 1) throw new Error(`FCStd PropertyFont object data is not unique: ${edit.objectName}`) + const properties = asArray(((((data[0].Properties as Record | undefined)?.Property)) as Record | Record[] | undefined)) + const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyFont') + if (candidates.length !== 1 || !candidates[0].String || typeof candidates[0].String !== 'object' || Array.isArray(candidates[0].String)) throw new TypeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} has no unique String payload.`) + ;(candidates[0].String as Record)['@_value'] = value + files[documentPath] = new TextEncoder().encode(new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })) + const output: Record = {} + for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path]) + const rewritten = new Uint8Array(zipSync(output, { level: 6 })) + const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName) + const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined + if (!rewrittenValue?.decoded || rewrittenValue.value !== value) throw new Error(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`) + return rewritten +} + /** Rewrites one native App::PropertyFileIncluded entry while preserving its archive identity. */ export const rewriteFcstdIncludedFileProperty = (bytes: Uint8Array, edit: FcstdIncludedFileResourceEdit): Uint8Array => { if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyFileIncluded rewrite requires non-empty object and property names.') diff --git a/src/facade/mockFacade.ts b/src/facade/mockFacade.ts index 52d2384..e990e10 100644 --- a/src/facade/mockFacade.ts +++ b/src/facade/mockFacade.ts @@ -45,7 +45,7 @@ import { DependencyGraph, createRecomputeSnapshot, type DependencyEdge } from '. import { convertQuantity, evaluateQuantityExpression, getUnit, quantityDimensionForUnit, quantityFromNumber, quantityFromUnit, type Quantity } from './units' import { carbonCopySketchGeometry, cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, projectSketchGeometry, solveSketch, type SketchConstraint, type SketchExternalGeometry, type SketchGeometry, type SketchSnapshot } from './sketcher' import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, sketchProfile, type RecomputeExecutionOptions } from './recomputeEngine' -import { decodeFcstdPathProperty, extractFcstdIncludedFileResources, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeFcstdMetadataArchive, storeFcstdIncludedFileResources, storeFcstdShapeResources } from './fcstd' +import { decodeFcstdPathProperty, extractFcstdIncludedFileResources, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdFontProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdIntegerSetProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeFcstdMetadataArchive, storeFcstdIncludedFileResources, storeFcstdShapeResources } from './fcstd' import type { FcstdWriteOptions } from './fcstd' import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics' import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef, resolveDocumentTopologyReference } from './topologyReferences' @@ -570,8 +570,13 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) throw new Error(`${property.label} is read-only.`) if (property.type === 'App::PropertyBool' && typeof value !== 'boolean') throw new TypeError(`${property.label} requires a boolean value.`) if (property.type === 'App::PropertyBoolList' && (!Array.isArray(value) || !value.every((entry) => typeof entry === 'boolean'))) throw new TypeError(`${property.label} requires a boolean list.`) + if (property.type === 'App::PropertyIntegerSet' && (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry)))) throw new TypeError(`${property.label} requires a safe-integer array.`) if (property.type === 'App::PropertyColorList' && (!Array.isArray(value) || !value.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel))))) throw new TypeError(`${property.label} requires a list of four finite RGBA channels.`) - if ((property.type === 'App::PropertyString' || property.type === 'App::PropertyEnumeration') && typeof value !== 'string') throw new TypeError(`${property.label} requires a string value.`) + if ((property.type === 'App::PropertyString' || property.type === 'App::PropertyEnumeration' || property.type === 'App::PropertyFont') && typeof value !== 'string') throw new TypeError(`${property.label} requires a string value.`) + if (property.type === 'App::PropertyFont') { + if ((value as string).length > 1024) throw new RangeError(`${property.label} font family name is too long.`) + if ((value as string).includes('\0') || /[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value as string)) throw new RangeError(`${property.label} contains unsupported control characters.`) + } if (property.type === 'App::PropertyFile') { if (typeof value !== 'string') throw new TypeError(`${property.label} requires a project-relative file reference.`) if (value.length > 4096) throw new RangeError(`${property.label} file reference is too long.`) @@ -587,7 +592,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope if (typeof value !== 'string') throw new TypeError(`${property.label} requires a #RRGGBB color.`) } else if (!Array.isArray(value) || value.length !== 4 || !value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))) throw new TypeError(`${property.label} requires four finite RGBA channels.`) } - if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`) + if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`) if ((property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && !Number.isSafeInteger(value)) throw new TypeError(`${property.label} requires an integer value.`) if (property.name === 'Occurrences' && ((value as number) < 2 || (value as number) > 100)) throw new RangeError('Occurrences must be between 2 and 100.') if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyQuantityConstraint') && (value as number) < 0) throw new RangeError(`${property.label} cannot be negative.`) @@ -1064,7 +1069,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW if (property.type === 'App::PropertyLinkList' && Array.isArray(candidate) && candidate.every((entry) => typeof entry === 'string')) return { ...property, value: loftCommands.has(commandId) && commandId !== 'loft-part' ? candidate.slice(1) : [...candidate] } if (property.type === 'App::PropertyBool' && typeof candidate === 'boolean') return { ...property, value: candidate } if (property.type === 'App::PropertyMultiTransform' && candidate && typeof candidate === 'object') return { ...property, value: clonePropertyValue(candidate as MultiTransformValue) } - if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && typeof candidate === 'number' && Number.isFinite(candidate)) return { ...property, value: candidate } + if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && typeof candidate === 'number' && Number.isFinite(candidate)) return { ...property, value: candidate } if (property.type === 'App::PropertyEnumeration' && typeof candidate === 'string' && property.options?.includes(candidate)) return { ...property, value: candidate } return property }) @@ -1118,7 +1123,8 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName) if (propertyIndex < 0) throw new Error(`Property does not exist: ${objectId}.${propertyName}`) const sourceProperty = sourceObject.properties[propertyIndex] - validatePropertyValue(state.document, sourceProperty, value) + const propertyValue = sourceProperty.type === 'App::PropertyIntegerSet' ? [...new Set(value as number[])].sort((left, right) => left - right) : value + validatePropertyValue(state.document, sourceProperty, propertyValue) const derivedArea = sourceObject.typeId === 'Measure::MeasureArea' && propertyName === 'Elements' ? measureAreaFromElements(state.document, value as LinkSubListValue) : undefined @@ -1132,7 +1138,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW const document = cloneDocumentSnapshot(state.document) const object = document.objects[objectIndex] - object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(value), expression: undefined, expressionError: undefined } + object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(propertyValue), expression: undefined, expressionError: undefined } if (derivedArea !== undefined) { const areaProperty = object.properties.find((property) => property.name === 'Area' && property.type === 'App::PropertyArea') if (!areaProperty || !(areaProperty.readOnly || isFreecadPropertyReadOnly(areaProperty.nativeStatus))) throw new Error('Measure::MeasureArea requires a read-only App::PropertyArea output.') @@ -1365,7 +1371,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW if (propertyIndex < 0) throw new Error(`Property does not exist: ${objectId}.${propertyName}`) const sourceProperty = sourceObject.properties[propertyIndex] if (sourceProperty.readOnly || isFreecadPropertyReadOnly(sourceProperty.nativeStatus)) throw new Error(`${sourceProperty.label} is read-only.`) - if (!['App::PropertyLength', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyFloat', 'App::PropertyPercent'].includes(sourceProperty.type)) throw new TypeError(`${sourceProperty.label} does not accept expressions.`) + if (!['App::PropertyLength', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyForce', 'App::PropertyFloat', 'App::PropertyPercent'].includes(sourceProperty.type)) throw new TypeError(`${sourceProperty.label} does not accept expressions.`) const evaluated = expectedExpressionValue(sourceProperty, expression, state.document) const references = expressionReferences(expression) const variables = expressionVariables(state.document) @@ -1738,10 +1744,12 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW rewriteNumeric: (bytes, edit) => rewriteFcstdNumericProperty(bytes, edit), rewriteMeasureArea: (bytes, edit) => rewriteFcstdMeasureArea(bytes, edit), rewriteBoolList: (bytes, edit) => rewriteFcstdBoolListProperty(bytes, edit), + rewriteIntegerSet: (bytes, edit) => rewriteFcstdIntegerSetProperty(bytes, edit), rewriteColor: (bytes, edit) => rewriteFcstdColorProperty(bytes, edit), rewriteColorList: (bytes, edit) => rewriteFcstdColorListProperty(bytes, edit), rewriteDirection: (bytes, edit) => rewriteFcstdDirectionProperty(bytes, edit), rewriteFile: (bytes, edit) => rewriteFcstdFileProperty(bytes, edit), + rewriteFont: (bytes, edit) => rewriteFcstdFontProperty(bytes, edit), rewriteIncludedFile: async (bytes, edit) => { const resourceBytes = await projectPersistence.resource.get(edit.value.resourceHash) if (!resourceBytes) throw new Error(`Project resource is missing for FCStd PropertyFileIncluded rewrite: ${edit.value.resourceHash}`) diff --git a/src/facade/types.ts b/src/facade/types.ts index ed9e662..7f4bbac 100644 --- a/src/facade/types.ts +++ b/src/facade/types.ts @@ -3,7 +3,7 @@ import type { DependencyEdge, RecomputeSnapshot, RecomputePlan } from './depende import type { Quantity, QuantityDimension } from './units' import type { SketchConstraint, SketchExternalGeometry, SketchGeometry, SketchSnapshot, SketchSolveResult } from './sketcher' import type { RecomputeExecutionOptions, RecomputeExecutionResult } from './recomputeEngine' -import type { FcstdArchiveLimits, FcstdBoolListEdit, FcstdColorEdit, FcstdColorListEdit, FcstdDirectionEdit, FcstdFileEdit, FcstdIncludedFileEdit, FcstdIncludedFileResourcePayload, FcstdInspection, FcstdInstantiatedShape, FcstdMeasureAreaEdit, FcstdNumericPropertyEdit, FcstdPathEdit, FcstdShapeResourcePayload, FcstdStoredIncludedFileResource, FcstdStoredShapeResource, FcstdWriteOptions } from './fcstd' +import type { FcstdArchiveLimits, FcstdBoolListEdit, FcstdColorEdit, FcstdColorListEdit, FcstdDirectionEdit, FcstdFileEdit, FcstdFontEdit, FcstdIncludedFileEdit, FcstdIncludedFileResourcePayload, FcstdInspection, FcstdInstantiatedShape, FcstdIntegerSetEdit, FcstdMeasureAreaEdit, FcstdNumericPropertyEdit, FcstdPathEdit, FcstdShapeResourcePayload, FcstdStoredIncludedFileResource, FcstdStoredShapeResource, FcstdWriteOptions } from './fcstd' import type { NativeOcctHistoryCapabilities, NativeOcctHistoryProvider } from './nativeHistoryProtocol' import type { FacadeRuntimeProfile } from './runtimeProfile' import type { CamApi } from './cam' @@ -105,11 +105,11 @@ export type ShapeResourceValue = { export type PropertyValue = string | number | boolean | string[] | number[] | boolean[] | ColorValue | ColorListValue | IncludedFileValue | VectorValue | PlacementValue | AttachmentSupportValue | MultiTransformValue | PathPropertyValue | ShapeResourceValue | TopoRefValue | LinkSubValue | LinkSubListValue | null export type NativeEditablePropertyType = - | 'App::PropertyString' | 'App::PropertyLength' | 'App::PropertyDistance' | 'App::PropertyAngle' | 'App::PropertyAcceleration' | 'App::PropertyArea' + | 'App::PropertyString' | 'App::PropertyLength' | 'App::PropertyDistance' | 'App::PropertyAngle' | 'App::PropertyAcceleration' | 'App::PropertyArea' | 'App::PropertyForce' | 'App::PropertyHeatFlux' | 'App::PropertyQuantityConstraint' | 'App::PropertyBool' | 'App::PropertyBoolList' | 'App::PropertyEnumeration' | 'App::PropertyLink' | 'App::PropertyLinkHidden' | 'App::PropertyLinkSub' | 'App::PropertyLinkSubList' | 'App::PropertyLinkList' | 'App::PropertyStringList' | 'App::PropertyFloatList' - | 'App::PropertyIntegerList' | 'App::PropertyVector' | 'App::PropertyDirection' | 'App::PropertyFile' | 'App::PropertyFileIncluded' | 'App::PropertyPlacement' + | 'App::PropertyIntegerList' | 'App::PropertyIntegerSet' | 'App::PropertyVector' | 'App::PropertyDirection' | 'App::PropertyFile' | 'App::PropertyFileIncluded' | 'App::PropertyFont' | 'App::PropertyPlacement' | 'App::PropertyMultiTransform' | 'Path::PropertyPath' | 'Part::PropertyPartShape' | 'App::PropertyColor' | 'App::PropertyColorList' | 'App::PropertyPercent' | 'App::PropertyFloat' | 'App::PropertyFloatConstraint' | 'App::PropertyPrecision' | 'App::PropertyInteger' @@ -1029,10 +1029,12 @@ export interface BitBybitWebCadFacade { rewriteNumeric(bytes: Uint8Array, edit: FcstdNumericPropertyEdit): Uint8Array rewriteMeasureArea(bytes: Uint8Array, edit: FcstdMeasureAreaEdit): Uint8Array rewriteBoolList(bytes: Uint8Array, edit: FcstdBoolListEdit): Uint8Array + rewriteIntegerSet(bytes: Uint8Array, edit: FcstdIntegerSetEdit): Uint8Array rewriteColor(bytes: Uint8Array, edit: FcstdColorEdit): Uint8Array rewriteColorList(bytes: Uint8Array, edit: FcstdColorListEdit): Uint8Array rewriteDirection(bytes: Uint8Array, edit: FcstdDirectionEdit): Uint8Array rewriteFile(bytes: Uint8Array, edit: FcstdFileEdit): Uint8Array + rewriteFont(bytes: Uint8Array, edit: FcstdFontEdit): Uint8Array rewriteIncludedFile(bytes: Uint8Array, edit: FcstdIncludedFileEdit): Promise } resource: { diff --git a/src/facade/units.ts b/src/facade/units.ts index 57e2796..b198bae 100644 --- a/src/facade/units.ts +++ b/src/facade/units.ts @@ -1,4 +1,4 @@ -export type QuantityDimension = 'dimensionless' | 'length' | 'angle' | 'area' | 'volume' | 'acceleration' | 'percent' +export type QuantityDimension = 'dimensionless' | 'length' | 'angle' | 'area' | 'volume' | 'acceleration' | 'force' | 'heat-flux' | 'percent' export type Quantity = { value: number @@ -29,6 +29,14 @@ const definitions: UnitDefinition[] = [ { id: 'm/s^2', symbol: 'm/s^2', dimension: 'acceleration', factor: 1000 }, { id: 'in/s^2', symbol: 'in/s^2', dimension: 'acceleration', factor: 25.4 }, { id: 'ft/s^2', symbol: 'ft/s^2', dimension: 'acceleration', factor: 304.8 }, + { id: 'mm*kg/s^2', symbol: 'mm*kg/s^2', dimension: 'force', factor: 1 }, + { id: 'N', symbol: 'N', dimension: 'force', factor: 1_000 }, + { id: 'kN', symbol: 'kN', dimension: 'force', factor: 1_000_000 }, + { id: 'lbf', symbol: 'lbf', dimension: 'force', factor: 4_448.2216152605 }, + { id: 'kg/s^3', symbol: 'kg/s^3', dimension: 'heat-flux', factor: 1 }, + { id: 'W/m^2', symbol: 'W/m^2', dimension: 'heat-flux', factor: 1 }, + { id: 'kW/m^2', symbol: 'kW/m^2', dimension: 'heat-flux', factor: 1_000 }, + { id: 'W/mm^2', symbol: 'W/mm^2', dimension: 'heat-flux', factor: 1_000_000 }, { id: 'deg', symbol: 'deg', dimension: 'angle', factor: 1 }, { id: 'rad', symbol: 'rad', dimension: 'angle', factor: 180 / Math.PI }, { id: '%', symbol: '%', dimension: 'percent', factor: 1 }, diff --git a/tests/propertyFontFacade.test.ts b/tests/propertyFontFacade.test.ts new file mode 100644 index 0000000..6bf585c --- /dev/null +++ b/tests/propertyFontFacade.test.ts @@ -0,0 +1,80 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { unzipSync } from 'fflate' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { decodeFcstdPropertyValue } from '../src/facade/fcstd' +import type { DocumentSnapshot, FacadeEvent, ObjectPropertySnapshot } from '../src/facade/types' + +const hosts = [ + { id: 'FontAnnotation', typeId: 'TechDraw::DrawViewAnnotation', group: 'Annotation' }, + { id: 'FontSpreadsheet', typeId: 'TechDraw::DrawViewSpreadsheet', group: 'Spreadsheet' }, +] as const + +const property = (group: string): ObjectPropertySnapshot => ({ name: 'Font', label: 'Font', group, scope: 'data', type: 'App::PropertyFont', value: 'osifont', recompute: true }) +const fixture = (): DocumentSnapshot => ({ + id: 'property-font-facade', + label: 'PropertyFont Facade', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: hosts.map(({ id }) => ({ id, label: id, type: 'feature', state: 'valid' })), + objects: hosts.map(({ id, typeId, group }) => ({ id, typeId, properties: [property(group)] })), + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: Object.fromEntries(hosts.map(({ id }) => [id, 'up-to-date'])), dirtyObjects: [], order: [], errors: [] }, +}) + +test('App::PropertyFont preserves arbitrary UTF-8 family names through the Facade', () => { + const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['FontAnnotation'], runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { + if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) + }) + facade.app.document.setProperty({ objectId: 'FontAnnotation', propertyName: 'Font', value: 'Noto Sans CJK SC' }) + assert.equal(facade.app.document.getObject('FontAnnotation')?.properties[0].value, 'Noto Sans CJK SC') + assert.equal(facade.app.document.getActive().version, 2) + assert.equal(facade.app.document.getActive().dirty, true) + assert.equal(facade.app.document.getActive().recompute?.objectStates.FontAnnotation, 'touched') + assert.deepEqual(events.map(({ type }) => type), ['property.before-change', 'property.changed', 'transaction.committed']) + + facade.app.document.setProperty({ objectId: 'FontAnnotation', propertyName: 'Font', value: '' }) + assert.equal(facade.app.document.getObject('FontAnnotation')?.properties[0].value, '') + const stable = JSON.stringify(facade.app.document.getActive()) + const eventCount = events.length + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontAnnotation', propertyName: 'Font', value: 42 as unknown as string }), /requires a string value/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontAnnotation', propertyName: 'Font', value: `Bad\0Font` }), /unsupported control characters/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontAnnotation', propertyName: 'Font', value: 'a'.repeat(1025) }), /font family name is too long/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stable) + assert.equal(events.length, eventCount) +}) + +test('App::PropertyFont keeps its TypeId around native String FCStd payloads', async () => { + const document = fixture() + document.objects[0].properties[0].value = 'DejaVu Sans' + document.objects[1].properties[0].value = 'Liberation Serif' + const facade = createWebCadFacade({ initialDocument: document, runtimeMode: 'mock' }) + const archive = facade.project.fcstd.serializeMetadata(document) + const documentXml = new TextDecoder().decode(unzipSync(archive)['Document.xml']) + assert.match(documentXml, /<\/Property>/) + assert.match(documentXml, /<\/Property>/) + assert.doesNotMatch(documentXml, /]+(?:group|doc|attr|ro|hide)=/) + + const inspection = facade.project.fcstd.inspect(archive) + for (const [index, host] of hosts.entries()) { + const object = inspection.objects.find(({ name }) => name === host.id) + assert.equal(object?.support, 'recognized') + const summary = object?.properties.find(({ name }) => name === 'Font') + assert.deepEqual({ typeId: summary?.typeId, element: summary?.element }, { typeId: 'App::PropertyFont', element: 'String' }) + assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: document.objects[index].properties[0].value, decoded: true }) + } + assert.equal(decodeFcstdPropertyValue({ name: 'Font', typeId: 'App::PropertyFont', element: 'PropertyFont', value: 'osifont' }).decoded, false) + + const rewritten = facade.project.fcstd.rewriteFont(archive, { objectName: 'FontAnnotation', propertyName: 'Font', value: 'Noto Sans', expectedValue: 'DejaVu Sans' }) + const rewrittenProperty = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'FontAnnotation')?.properties.find(({ name }) => name === 'Font') + assert.deepEqual(decodeFcstdPropertyValue(rewrittenProperty!), { value: 'Noto Sans', decoded: true }) + assert.match(new TextDecoder().decode(unzipSync(rewritten)['Document.xml']), /]*name="FontAnnotation")(?=[^>]*Touched="1")[^>]*\/>/) + assert.throws(() => facade.project.fcstd.rewriteFont(archive, { objectName: 'FontAnnotation', propertyName: 'Font', value: 'Noto Sans', expectedValue: 'Wrong' }), /does not match expectedValue/) + assert.throws(() => facade.project.fcstd.rewriteFont(archive, { objectName: 'FontAnnotation', propertyName: 'Font', value: `Bad\0Font` }), /unsupported control characters/) + assert.throws(() => facade.project.fcstd.rewriteFont(archive, { objectName: 'FontAnnotation', propertyName: 'Missing', value: 'Noto Sans' }), /property does not exist/) + await facade.project.dispose() +}) diff --git a/tests/propertyFontTransaction.test.ts b/tests/propertyFontTransaction.test.ts new file mode 100644 index 0000000..fce318c --- /dev/null +++ b/tests/propertyFontTransaction.test.ts @@ -0,0 +1,74 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { encodeFreecadPropertyStatus } from '../src/facade/propertyStatus' +import type { DocumentSnapshot, FacadeEvent } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-font-transaction', + label: 'PropertyFont Transaction', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: [ + { id: 'FontProbe', label: 'Font probe', type: 'feature', state: 'valid' }, + { id: 'ImmutableFontProbe', label: 'Immutable font probe', type: 'feature', state: 'valid' }, + ], + objects: [ + { id: 'FontProbe', typeId: 'TechDraw::DrawViewAnnotation', properties: [{ name: 'Font', label: 'Font', group: 'Annotation', scope: 'data', type: 'App::PropertyFont', value: 'osifont', recompute: true }] }, + { id: 'ImmutableFontProbe', typeId: 'TechDraw::DrawViewAnnotation', properties: [{ name: 'Font', label: 'Immutable font', group: 'Annotation', scope: 'data', type: 'App::PropertyFont', value: 'osifont', nativeStatus: encodeFreecadPropertyStatus(['Immutable']), readOnly: true, recompute: true }] }, + ], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { FontProbe: 'up-to-date', ImmutableFontProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const valueOf = (facade: ReturnType, objectId = 'FontProbe') => facade.app.document.getObject(objectId)?.properties.find(({ name }) => name === 'Font')?.value + +test('App::PropertyFont transaction closes undo, redo, stale, cancel, failure and resource ownership', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { + if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) + }) + const resourcesBefore = facade.geometry.capabilities() + facade.app.document.setProperty({ objectId: 'FontProbe', propertyName: 'Font', value: 'DejaVu Sans', expectedDocumentVersion: 1 }) + assert.equal(valueOf(facade), 'DejaVu Sans') + assert.equal(facade.app.document.getActive().version, 2) + assert.equal(facade.app.document.getActive().dirty, true) + assert.equal(facade.app.document.getActive().recompute?.objectStates.FontProbe, 'touched') + assert.deepEqual(events.map(({ type }) => type), ['property.before-change', 'property.changed', 'transaction.committed']) + assert.equal(new Set(events.map((event) => 'transactionId' in event ? event.transactionId : '')).size, 1) + + facade.history.undo() + assert.equal(valueOf(facade), 'osifont') + assert.equal(facade.app.document.getActive().version, 1) + assert.equal(facade.history.canUndo(), false) + assert.equal(facade.history.canRedo(), true) + facade.history.redo() + assert.equal(valueOf(facade), 'DejaVu Sans') + assert.equal(facade.app.document.getActive().version, 2) + assert.equal(facade.history.canUndo(), true) + assert.equal(facade.history.canRedo(), false) + + const stableState = JSON.stringify(facade.app.document.getActive()) + const stableEvents = events.length + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontProbe', propertyName: 'Font', value: 'Stale Font', expectedDocumentVersion: 1 }), /Stale document version: expected 1, current 2/) + const cancelled = new AbortController() + cancelled.abort() + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontProbe', propertyName: 'Font', value: 'Cancelled Font', expectedDocumentVersion: 2, signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError') + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontProbe', propertyName: 'Font', value: 42 as unknown as string }), /requires a string value/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'FontProbe', propertyName: 'Font', value: `Bad\0Font` }), /unsupported control characters/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'ImmutableFontProbe', propertyName: 'Font', value: 'Unlocked Font' }), /Immutable font is read-only/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stableState) + assert.equal(events.length, stableEvents) + assert.equal(facade.history.canUndo(), true) + assert.equal(facade.history.canRedo(), false) + + const resourcesAfter = facade.geometry.capabilities() + assert.deepEqual( + { shapeCount: resourcesAfter.shapeCount, kernelReferenceCount: resourcesAfter.kernelReferenceCount, releasedShapeCount: resourcesAfter.releasedShapeCount }, + { shapeCount: resourcesBefore.shapeCount, kernelReferenceCount: resourcesBefore.kernelReferenceCount, releasedShapeCount: resourcesBefore.releasedShapeCount }, + ) + await facade.project.dispose() +}) diff --git a/tests/propertyForceFacade.test.ts b/tests/propertyForceFacade.test.ts new file mode 100644 index 0000000..1e431f1 --- /dev/null +++ b/tests/propertyForceFacade.test.ts @@ -0,0 +1,61 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { unzipSync } from 'fflate' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { decodeFcstdPropertyValue } from '../src/facade/fcstd' +import { encodeFreecadPropertyStatus } from '../src/facade/propertyStatus' +import { convertQuantity, quantityFromUnit } from '../src/facade/units' +import type { DocumentSnapshot } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-force-facade', + label: 'PropertyForce Facade', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: [ + { id: 'ForceProbe', label: 'Force probe', type: 'feature', state: 'valid' }, + { id: 'RigidForceProbe', label: 'Rigid force probe', type: 'feature', state: 'valid' }, + ], + objects: [ + { id: 'ForceProbe', typeId: 'Fem::ConstraintForce', properties: [{ name: 'Force', label: 'Force', group: '', scope: 'data', type: 'App::PropertyForce', value: 0, unit: 'mm*kg/s^2', recompute: true }] }, + { id: 'RigidForceProbe', typeId: 'Fem::ConstraintRigidBody', properties: [{ name: 'ForceX', label: 'Force X', group: 'ConstraintRigidBody', scope: 'data', type: 'App::PropertyForce', value: 0, unit: 'mm*kg/s^2', nativeStatus: encodeFreecadPropertyStatus(['PropOutput']), recompute: true }] }, + ], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { ForceProbe: 'up-to-date', RigidForceProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +test('App::PropertyForce uses the native internal unit and Float FCStd codec', () => { + const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['ForceProbe'], runtimeMode: 'mock' }) + facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: 500000 }) + const edited = facade.app.document.getActive() + const property = edited.objects[0].properties[0] + assert.deepEqual({ type: property.type, value: property.value, unit: property.unit }, { type: 'App::PropertyForce', value: 500000, unit: 'mm*kg/s^2' }) + assert.equal(edited.version, 2) + assert.equal(edited.dirty, true) + assert.equal(edited.recompute?.objectStates.ForceProbe, 'touched') + assert.throws(() => facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: '500 N' as unknown as number }), /finite numeric value/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: Number.POSITIVE_INFINITY }), /finite numeric value/) + facade.app.document.setProperty({ objectId: 'RigidForceProbe', propertyName: 'ForceX', value: 1000 }) + assert.equal(facade.app.document.getObject('RigidForceProbe')?.properties[0].value, 1000) + assert.equal(facade.app.document.getActive().recompute?.objectStates.RigidForceProbe, 'up-to-date') + + const archive = facade.project.fcstd.serializeMetadata(edited) + const documentXml = new TextDecoder().decode(unzipSync(archive)['Document.xml']) + assert.match(documentXml, /<\/Property>/) + assert.doesNotMatch(documentXml, /]+group=/) + const summary = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force') + assert.deepEqual({ typeId: summary?.typeId, element: summary?.element }, { typeId: 'App::PropertyForce', element: 'Float' }) + assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: 500000, decoded: true }) + + const rewritten = facade.project.fcstd.rewriteNumeric(archive, { objectName: 'ForceProbe', propertyName: 'Force', typeId: 'App::PropertyForce', value: -5000, expectedValue: 500000 }) + const rewrittenSummary = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'ForceProbe')?.properties.find(({ name }) => name === 'Force') + assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: -5000, decoded: true }) + assert.throws(() => facade.project.fcstd.rewriteNumeric(archive, { objectName: 'ForceProbe', propertyName: 'Force', typeId: 'App::PropertyForce', value: 1000, expectedValue: 1 }), /expected 1, found 500000/) + assert.equal(convertQuantity(quantityFromUnit(2, 'kN'), 'N'), 2000) + assert.equal(convertQuantity(quantityFromUnit(1, 'N'), 'mm*kg/s^2'), 1000) + facade.app.document.setExpression({ objectId: 'ForceProbe', propertyName: 'Force', expression: '2 N' }) + assert.equal(facade.app.document.getObject('ForceProbe')?.properties[0].value, 2000) + assert.equal(facade.app.document.getObject('ForceProbe')?.properties[0].expression, '2 N') +}) diff --git a/tests/propertyForceTransaction.test.ts b/tests/propertyForceTransaction.test.ts new file mode 100644 index 0000000..48a47f9 --- /dev/null +++ b/tests/propertyForceTransaction.test.ts @@ -0,0 +1,52 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { encodeFreecadPropertyStatus } from '../src/facade/propertyStatus' +import type { DocumentSnapshot, FacadeEvent } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-force-transaction', label: 'PropertyForce Transaction', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'ForceProbe', label: 'Force probe', type: 'feature', state: 'valid' }, { id: 'OutputForceProbe', label: 'Output force probe', type: 'feature', state: 'valid' }], + objects: [ + { id: 'ForceProbe', typeId: 'Fem::ConstraintForce', properties: [{ name: 'Force', label: 'Force', group: '', scope: 'data', type: 'App::PropertyForce', value: 0, unit: 'mm*kg/s^2', recompute: true }] }, + { id: 'OutputForceProbe', typeId: 'Fem::ConstraintRigidBody', properties: [{ name: 'ForceX', label: 'Force X', group: 'ConstraintRigidBody', scope: 'data', type: 'App::PropertyForce', value: 0, unit: 'mm*kg/s^2', nativeStatus: encodeFreecadPropertyStatus(['PropOutput']), recompute: true }] }, + ], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { ForceProbe: 'up-to-date', OutputForceProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const force = (facade: ReturnType) => facade.app.document.getObject('ForceProbe')?.properties[0].value + +test('App::PropertyForce transaction closes undo, redo, abort, stale, failure and resource ownership', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) }) + const resourcesBefore = facade.geometry.capabilities() + facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: 500000, expectedDocumentVersion: 1 }) + assert.equal(force(facade), 500000) + assert.equal(facade.app.document.getActive().version, 2) + assert.deepEqual(events.map(({ type }) => type), ['property.before-change', 'property.changed', 'transaction.committed']) + assert.equal(new Set(events.map((event) => 'transactionId' in event ? event.transactionId : '')).size, 1) + facade.history.undo() + assert.equal(force(facade), 0) + assert.equal(facade.history.canRedo(), true) + facade.history.redo() + assert.equal(force(facade), 500000) + assert.equal(facade.history.canUndo(), true) + + facade.app.document.setProperty({ objectId: 'OutputForceProbe', propertyName: 'ForceX', value: 1000, expectedDocumentVersion: 2 }) + assert.equal(facade.app.document.getObject('OutputForceProbe')?.properties[0].value, 1000) + assert.equal(facade.app.document.getActive().recompute?.objectStates.OutputForceProbe, 'up-to-date') + + const stable = JSON.stringify(facade.app.document.getActive()) + const eventCount = events.length + assert.throws(() => facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: 250000, expectedDocumentVersion: 1 }), /Stale document version/) + const cancelled = new AbortController(); cancelled.abort() + assert.throws(() => facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: 250000, signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError') + assert.throws(() => facade.app.document.setProperty({ objectId: 'ForceProbe', propertyName: 'Force', value: 'invalid' as unknown as number }), /finite numeric value/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stable) + assert.equal(events.length, eventCount) + const resourcesAfter = facade.geometry.capabilities() + assert.deepEqual({ shapeCount: resourcesAfter.shapeCount, kernelReferenceCount: resourcesAfter.kernelReferenceCount, releasedShapeCount: resourcesAfter.releasedShapeCount }, { shapeCount: resourcesBefore.shapeCount, kernelReferenceCount: resourcesBefore.kernelReferenceCount, releasedShapeCount: resourcesBefore.releasedShapeCount }) + await facade.project.dispose() +}) diff --git a/tests/propertyHeatFluxFacade.test.ts b/tests/propertyHeatFluxFacade.test.ts new file mode 100644 index 0000000..f5c2707 --- /dev/null +++ b/tests/propertyHeatFluxFacade.test.ts @@ -0,0 +1,47 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { unzipSync } from 'fflate' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { decodeFcstdPropertyValue } from '../src/facade/fcstd' +import { convertQuantity, quantityFromUnit } from '../src/facade/units' +import type { DocumentSnapshot } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-heatflux-facade', + label: 'PropertyHeatFlux Facade', + version: 1, + dirty: false, + readOnly: false, + units: 'mm', + tree: [{ id: 'HeatFluxProbe', label: 'Heat flux probe', type: 'feature', state: 'valid' }], + objects: [{ id: 'HeatFluxProbe', typeId: 'Fem::ConstraintHeatflux', properties: [{ name: 'DFlux', label: 'DFlux', group: 'ConstraintHeatflux', scope: 'data', type: 'App::PropertyHeatFlux', value: 0, unit: 'kg/s^3', recompute: true }] }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { HeatFluxProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +test('App::PropertyHeatFlux uses the native internal unit and Float FCStd codec', () => { + const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['HeatFluxProbe'], runtimeMode: 'mock' }) + facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: 500 }) + const edited = facade.app.document.getActive() + const property = edited.objects[0].properties[0] + assert.deepEqual({ type: property.type, value: property.value, unit: property.unit }, { type: 'App::PropertyHeatFlux', value: 500, unit: 'kg/s^3' }) + assert.equal(edited.version, 2) + assert.equal(edited.dirty, true) + assert.equal(edited.recompute?.objectStates.HeatFluxProbe, 'touched') + assert.throws(() => facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: '500 W/m^2' as unknown as number }), /finite numeric value/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: Number.POSITIVE_INFINITY }), /finite numeric value/) + + const archive = facade.project.fcstd.serializeMetadata(edited) + const documentXml = new TextDecoder().decode(unzipSync(archive)['Document.xml']) + assert.match(documentXml, /]*><\/Property>/) + const summary = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux') + assert.deepEqual({ typeId: summary?.typeId, element: summary?.element }, { typeId: 'App::PropertyHeatFlux', element: 'Float' }) + assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: 500, decoded: true }) + + const rewritten = facade.project.fcstd.rewriteNumeric(archive, { objectName: 'HeatFluxProbe', propertyName: 'DFlux', typeId: 'App::PropertyHeatFlux', value: -5, expectedValue: 500 }) + const rewrittenSummary = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux') + assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: -5, decoded: true }) + assert.throws(() => facade.project.fcstd.rewriteNumeric(archive, { objectName: 'HeatFluxProbe', propertyName: 'DFlux', typeId: 'App::PropertyHeatFlux', value: 1000, expectedValue: 1 }), /expected 1, found 500/) + assert.equal(convertQuantity(quantityFromUnit(2, 'kW/m^2'), 'W/m^2'), 2000) + assert.equal(convertQuantity(quantityFromUnit(1, 'W/mm^2'), 'kg/s^3'), 1000000) +}) diff --git a/tests/propertyHeatFluxTransaction.test.ts b/tests/propertyHeatFluxTransaction.test.ts new file mode 100644 index 0000000..9133269 --- /dev/null +++ b/tests/propertyHeatFluxTransaction.test.ts @@ -0,0 +1,44 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createWebCadFacade } from '../src/facade/mockFacade' +import type { DocumentSnapshot, FacadeEvent } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-heatflux-transaction', label: 'PropertyHeatFlux Transaction', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'HeatFluxProbe', label: 'Heat flux probe', type: 'feature', state: 'valid' }], + objects: [{ id: 'HeatFluxProbe', typeId: 'Fem::ConstraintHeatflux', properties: [{ name: 'DFlux', label: 'DFlux', group: 'ConstraintHeatflux', scope: 'data', type: 'App::PropertyHeatFlux', value: 0, unit: 'kg/s^3', recompute: true }] }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { HeatFluxProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const heatFlux = (facade: ReturnType) => facade.app.document.getObject('HeatFluxProbe')?.properties[0].value + +test('App::PropertyHeatFlux transaction closes undo, redo, abort, stale, failure and resource ownership', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) }) + const resourcesBefore = facade.geometry.capabilities() + facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: 500, expectedDocumentVersion: 1 }) + assert.equal(heatFlux(facade), 500) + assert.equal(facade.app.document.getActive().version, 2) + assert.deepEqual(events.map(({ type }) => type), ['property.before-change', 'property.changed', 'transaction.committed']) + assert.equal(new Set(events.map((event) => 'transactionId' in event ? event.transactionId : '')).size, 1) + facade.history.undo() + assert.equal(heatFlux(facade), 0) + assert.equal(facade.history.canRedo(), true) + facade.history.redo() + assert.equal(heatFlux(facade), 500) + assert.equal(facade.history.canUndo(), true) + + const stable = JSON.stringify(facade.app.document.getActive()) + const eventCount = events.length + assert.throws(() => facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: 250, expectedDocumentVersion: 1 }), /Stale document version/) + const cancelled = new AbortController(); cancelled.abort() + assert.throws(() => facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: 250, signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError') + assert.throws(() => facade.app.document.setProperty({ objectId: 'HeatFluxProbe', propertyName: 'DFlux', value: 'invalid' as unknown as number }), /finite numeric value/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stable) + assert.equal(events.length, eventCount) + const resourcesAfter = facade.geometry.capabilities() + assert.deepEqual({ shapeCount: resourcesAfter.shapeCount, kernelReferenceCount: resourcesAfter.kernelReferenceCount, releasedShapeCount: resourcesAfter.releasedShapeCount }, { shapeCount: resourcesBefore.shapeCount, kernelReferenceCount: resourcesBefore.kernelReferenceCount, releasedShapeCount: resourcesBefore.releasedShapeCount }) + await facade.project.dispose() +}) diff --git a/tests/propertyIntegerSetFacade.test.ts b/tests/propertyIntegerSetFacade.test.ts new file mode 100644 index 0000000..ea98ec7 --- /dev/null +++ b/tests/propertyIntegerSetFacade.test.ts @@ -0,0 +1,47 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { unzipSync } from 'fflate' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { decodeFcstdPropertyValue } from '../src/facade/fcstd' +import type { DocumentSnapshot, FacadeEvent } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-integerset-facade', label: 'PropertyIntegerSet Facade', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'NodesProbe', label: 'Nodes probe', type: 'feature', state: 'valid' }, { id: 'ElementsProbe', label: 'Elements probe', type: 'feature', state: 'valid' }], + objects: [ + { id: 'NodesProbe', typeId: 'Fem::FemSetNodesObject', properties: [{ name: 'Nodes', label: 'Nodes', group: 'Node indexes', scope: 'data', type: 'App::PropertyIntegerSet', value: [], recompute: true }] }, + { id: 'ElementsProbe', typeId: 'Fem::FemSetElementNodesObject', properties: [{ name: 'Elements', label: 'Elements', group: 'Element indexes', scope: 'data', type: 'App::PropertyIntegerSet', value: [4, 10], recompute: true }] }, + ], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { NodesProbe: 'up-to-date', ElementsProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const valueOf = (facade: ReturnType, objectId: string, propertyName: string) => facade.app.document.getObject(objectId)?.properties.find((property) => property.name === propertyName)?.value + +test('App::PropertyIntegerSet normalizes unique integers and uses the native IntegerSet codec', () => { + const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['NodesProbe'], runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) }) + facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [3, 1, 3, 2] }) + assert.deepEqual(valueOf(facade, 'NodesProbe', 'Nodes'), [1, 2, 3]) + assert.equal(facade.app.document.getActive().version, 2) + assert.equal(facade.app.document.getActive().recompute?.objectStates.NodesProbe, 'touched') + assert.deepEqual(events.map((event) => event.type), ['property.before-change', 'property.changed', 'transaction.committed']) + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [1, 1.5] }), /safe-integer array/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: ['1'] as unknown as number[] }), /safe-integer array/) + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [Number.MAX_SAFE_INTEGER + 1] }), /safe-integer array/) + assert.deepEqual(valueOf(facade, 'NodesProbe', 'Nodes'), [1, 2, 3]) + + const archive = facade.project.fcstd.serializeMetadata(facade.app.document.getActive()) + const xml = new TextDecoder().decode(unzipSync(archive)['Document.xml']) + assert.match(xml, /]*><\/IntegerSet><\/Property>/) + const summary = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'NodesProbe')?.properties.find(({ name }) => name === 'Nodes') + assert.deepEqual({ typeId: summary?.typeId, element: summary?.element }, { typeId: 'App::PropertyIntegerSet', element: 'IntegerSet' }) + assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: [1, 2, 3], decoded: true }) + + const rewritten = facade.project.fcstd.rewriteIntegerSet(archive, { objectName: 'NodesProbe', propertyName: 'Nodes', value: [8, -1, 8], expectedValue: [1, 2, 3] }) + const rewrittenSummary = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'NodesProbe')?.properties.find(({ name }) => name === 'Nodes') + assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: [-1, 8], decoded: true }) + assert.throws(() => facade.project.fcstd.rewriteIntegerSet(archive, { objectName: 'NodesProbe', propertyName: 'Nodes', value: [4], expectedValue: [9] }), /does not match expectedValue/) +}) + diff --git a/tests/propertyIntegerSetTransaction.test.ts b/tests/propertyIntegerSetTransaction.test.ts new file mode 100644 index 0000000..ea73d7a --- /dev/null +++ b/tests/propertyIntegerSetTransaction.test.ts @@ -0,0 +1,45 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createWebCadFacade } from '../src/facade/mockFacade' +import type { DocumentSnapshot, FacadeEvent } from '../src/facade/types' + +const fixture = (): DocumentSnapshot => ({ + id: 'property-integerset-transaction', label: 'PropertyIntegerSet Transaction', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'NodesProbe', label: 'Nodes probe', type: 'feature', state: 'valid' }], + objects: [{ id: 'NodesProbe', typeId: 'Fem::FemSetNodesObject', properties: [{ name: 'Nodes', label: 'Nodes', group: 'Node indexes', scope: 'data', type: 'App::PropertyIntegerSet', value: [1, 3], recompute: true }] }], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { NodesProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const valueOf = (facade: ReturnType) => facade.app.document.getObject('NodesProbe')?.properties[0].value + +test('App::PropertyIntegerSet transaction closes normalization, undo, redo, stale, cancel, failure and resources', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' }) + const events: FacadeEvent[] = [] + facade.subscribe((event) => { if (event.type === 'property.before-change' || event.type === 'property.changed' || event.type === 'transaction.committed') events.push(event) }) + const resourcesBefore = facade.geometry.capabilities() + const requested = [5, 2, 5] + facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: requested, expectedDocumentVersion: 1 }) + requested[0] = 99 + assert.deepEqual(valueOf(facade), [2, 5]) + const exposed = valueOf(facade) as number[] + exposed[0] = 99 + assert.deepEqual(valueOf(facade), [2, 5]) + assert.equal(facade.app.document.getActive().version, 2) + assert.deepEqual(events.map(({ type }) => type), ['property.before-change', 'property.changed', 'transaction.committed']) + assert.equal(new Set(events.map((event) => 'transactionId' in event ? event.transactionId : '')).size, 1) + facade.history.undo(); assert.deepEqual(valueOf(facade), [1, 3]); assert.equal(facade.history.canRedo(), true) + facade.history.redo(); assert.deepEqual(valueOf(facade), [2, 5]); assert.equal(facade.history.canUndo(), true) + + const stable = JSON.stringify(facade.app.document.getActive()) + const eventCount = events.length + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [7], expectedDocumentVersion: 1 }), /Stale document version/) + const cancelled = new AbortController(); cancelled.abort() + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [7], signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError') + assert.throws(() => facade.app.document.setProperty({ objectId: 'NodesProbe', propertyName: 'Nodes', value: [1.5] }), /safe-integer array/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stable) + assert.equal(events.length, eventCount) + const resourcesAfter = facade.geometry.capabilities() + assert.deepEqual({ shapeCount: resourcesAfter.shapeCount, kernelReferenceCount: resourcesAfter.kernelReferenceCount, releasedShapeCount: resourcesAfter.releasedShapeCount }, { shapeCount: resourcesBefore.shapeCount, kernelReferenceCount: resourcesBefore.kernelReferenceCount, releasedShapeCount: resourcesBefore.releasedShapeCount }) + await facade.project.dispose() +})