diff --git a/chrome-property-linksubhidden-harness.html b/chrome-property-linksubhidden-harness.html new file mode 100644 index 0000000..d88a4bc --- /dev/null +++ b/chrome-property-linksubhidden-harness.html @@ -0,0 +1,5 @@ + + + Chrome PropertyLinkSubHidden harness +
Initializing PropertyLinkSubHidden harness.
+ diff --git a/chrome-property-linksublistglobal-harness.html b/chrome-property-linksublistglobal-harness.html new file mode 100644 index 0000000..3c3406a --- /dev/null +++ b/chrome-property-linksublistglobal-harness.html @@ -0,0 +1 @@ +Chrome PropertyLinkSubListGlobal harness
Initializing PropertyLinkSubListGlobal harness.
diff --git a/config/chrome-partdesign-reference-lifecycle-verification.json b/config/chrome-partdesign-reference-lifecycle-verification.json index fc6a90b..de159cf 100644 --- a/config/chrome-partdesign-reference-lifecycle-verification.json +++ b/config/chrome-partdesign-reference-lifecycle-verification.json @@ -1059,8 +1059,8 @@ }, { "path": "src/facade/mockFacade.ts", - "bytes": 172346, - "sha256": "100b84bd3ebfe1a620885d70e98e97c864f33f5d478126c8586cca6deddfd0ac" + "bytes": 174111, + "sha256": "d84691f3e1fb9d7ec522fdcabe93386158e12d567bf24d1e43de0f5ee369114c" }, { "path": "src/facade/geometryRuntime.ts", diff --git a/config/chrome-property-linksubhidden-verification.json b/config/chrome-property-linksubhidden-verification.json new file mode 100644 index 0000000..7f8ff92 --- /dev/null +++ b/config/chrome-property-linksubhidden-verification.json @@ -0,0 +1,95 @@ +{ + "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": { + "visibleLabelInput": true, + "hiddenControlCountBefore": 0, + "hiddenControlCountAfter": 0, + "hiddenTextPresentBefore": false, + "hiddenTextPresentAfter": false, + "hiddenStatus": true, + "beforeValue": { + "schemaVersion": 1, + "objectId": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + }, + "afterValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "dependencyCount": 0, + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "loadedType": "App::PropertyLinkSubHidden", + "loadedDependencyCount": 0, + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "LinkSub", + "fcstdValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "fcstdNativeStatus": 67108864 + }, + "resource": { + "byteLength": 43, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:46041/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-linksublistglobal-verification.json b/config/chrome-property-linksublistglobal-verification.json new file mode 100644 index 0000000..8c3f853 --- /dev/null +++ b/config/chrome-property-linksublistglobal-verification.json @@ -0,0 +1,120 @@ +{ + "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": { + "visibleLabelInput": true, + "controlsBefore": 3, + "controlsAfter": 2, + "beforeValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SourceBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face2" + }, + { + "objectId": "SecondBox", + "subElement": "Face1" + } + ] + }, + "afterValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "documentVersion": 2, + "dirty": true, + "objectState": "touched", + "dependencyCount": 2, + "errors": [] + }, + "persistence": { + "mode": "sqlite-opfs", + "sqliteWasm": true, + "opfs": true, + "savedMode": "sqlite-opfs", + "loadedValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "loadedType": "App::PropertyLinkSubListGlobal", + "loadedDependencyCount": 2, + "checkpointVersion": 2, + "recoveryIntegrity": "ok", + "fcstdElement": "LinkSubList", + "fcstdType": "App::PropertyLinkSubListGlobal", + "fcstdValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "fcstdDecoded": true + }, + "resource": { + "byteLength": 47, + "roundTrip": true, + "released": true, + "markerRemoved": true + }, + "worker": { + "constructed": [ + { + "name": "bitbybit-persistence", + "url": "http://127.0.0.1:36505/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 6b5ab3f..4be9738 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-propertylinksubhidden-D", + "nextTask": "PROP-app-propertymap-A", "summary": { "tasks": 1756, - "completed": 607, + "completed": 622, "inProgress": 1, - "pending": 1148 + "pending": 1133 }, "milestones": [ { @@ -8375,7 +8375,7 @@ { "id": "PROP-app-propertylinksubhidden-D", "title": "Capture parameter mutation and restore evidence: App::PropertyLinkSubHidden", - "status": "in_progress", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-C" ], @@ -8391,7 +8391,7 @@ { "id": "PROP-app-propertylinksubhidden-E", "title": "Implement the Facade capability: App::PropertyLinkSubHidden", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-D" ], @@ -8407,7 +8407,7 @@ { "id": "PROP-app-propertylinksubhidden-F", "title": "Close transaction and recovery behavior: App::PropertyLinkSubHidden", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-E" ], @@ -8423,7 +8423,7 @@ { "id": "PROP-app-propertylinksubhidden-G", "title": "Close FCStd save, reopen and resave: App::PropertyLinkSubHidden", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-F" ], @@ -8439,7 +8439,7 @@ { "id": "PROP-app-propertylinksubhidden-H", "title": "Replay in a real browser: App::PropertyLinkSubHidden", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-G" ], @@ -8455,7 +8455,7 @@ { "id": "PROP-app-propertylinksubhidden-I", "title": "Promote capability and synchronize blockers: App::PropertyLinkSubHidden", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-H" ], @@ -8472,7 +8472,7 @@ { "id": "PROP-app-propertylinksublistglobal-A", "title": "Lock native inventory and preconditions: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksubhidden-I" ], @@ -8488,7 +8488,7 @@ { "id": "PROP-app-propertylinksublistglobal-B", "title": "Capture native success evidence: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-A" ], @@ -8504,7 +8504,7 @@ { "id": "PROP-app-propertylinksublistglobal-C", "title": "Capture native failure and cancel evidence: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-B" ], @@ -8520,7 +8520,7 @@ { "id": "PROP-app-propertylinksublistglobal-D", "title": "Capture parameter mutation and restore evidence: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-C" ], @@ -8536,7 +8536,7 @@ { "id": "PROP-app-propertylinksublistglobal-E", "title": "Implement the Facade capability: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-D" ], @@ -8552,7 +8552,7 @@ { "id": "PROP-app-propertylinksublistglobal-F", "title": "Close transaction and recovery behavior: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-E" ], @@ -8568,7 +8568,7 @@ { "id": "PROP-app-propertylinksublistglobal-G", "title": "Close FCStd save, reopen and resave: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-F" ], @@ -8584,7 +8584,7 @@ { "id": "PROP-app-propertylinksublistglobal-H", "title": "Replay in a real browser: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-G" ], @@ -8600,7 +8600,7 @@ { "id": "PROP-app-propertylinksublistglobal-I", "title": "Promote capability and synchronize blockers: App::PropertyLinkSubListGlobal", - "status": "pending", + "status": "completed", "dependencies": [ "PROP-app-propertylinksublistglobal-H" ], @@ -8617,7 +8617,7 @@ { "id": "PROP-app-propertymap-A", "title": "Lock native inventory and preconditions: App::PropertyMap", - "status": "pending", + "status": "in_progress", "dependencies": [ "PROP-app-propertylinksublistglobal-I" ], diff --git a/config/freecad-follow-up-task-progress.json b/config/freecad-follow-up-task-progress.json index de31392..a3407a1 100644 --- a/config/freecad-follow-up-task-progress.json +++ b/config/freecad-follow-up-task-progress.json @@ -829,6 +829,66 @@ { "id": "PROP-app-propertylinksubhidden-C", "evidence": ["config/freecad-property-linksubhidden-failure.json", "check:freecad-property-linksubhidden-failure"] + }, + { + "id": "PROP-app-propertylinksubhidden-D", + "evidence": ["config/freecad-property-linksubhidden-mutation.json", "check:freecad-property-linksubhidden-mutation"] + }, + { + "id": "PROP-app-propertylinksubhidden-E", + "evidence": ["src/facade/types.ts", "src/facade/fcstd.ts", "src/facade/mockFacade.ts", "src/facade/index.ts", "tests/propertyLinkSubHiddenFacade.test.ts", "test:freecad-property-linksubhidden-facade"] + }, + { + "id": "PROP-app-propertylinksubhidden-F", + "evidence": ["tests/propertyLinkSubHiddenTransaction.test.ts", "test:freecad-property-linksubhidden-transaction"] + }, + { + "id": "PROP-app-propertylinksubhidden-G", + "evidence": ["config/freecad-property-linksubhidden-roundtrip.json", "check:freecad-property-linksubhidden-roundtrip"] + }, + { + "id": "PROP-app-propertylinksubhidden-H", + "evidence": ["config/chrome-property-linksubhidden-verification.json", "check:chrome-property-linksubhidden"] + }, + { + "id": "PROP-app-propertylinksubhidden-I", + "evidence": ["config/freecad-property-linksubhidden-promotion.json", "check:freecad-property-linksubhidden-promotion"] + }, + { + "id": "PROP-app-propertylinksublistglobal-A", + "evidence": ["config/freecad-property-linksublistglobal-inventory.json", "check:freecad-property-linksublistglobal-inventory"] + }, + { + "id": "PROP-app-propertylinksublistglobal-B", + "evidence": ["config/freecad-property-linksublistglobal-success.json", "check:freecad-property-linksublistglobal-success"] + }, + { + "id": "PROP-app-propertylinksublistglobal-C", + "evidence": ["config/freecad-property-linksublistglobal-failure.json", "check:freecad-property-linksublistglobal-failure"] + }, + { + "id": "PROP-app-propertylinksublistglobal-D", + "evidence": ["config/freecad-property-linksublistglobal-mutation.json", "check:freecad-property-linksublistglobal-mutation"] + }, + { + "id": "PROP-app-propertylinksublistglobal-E", + "evidence": ["src/facade/types.ts", "src/facade/fcstd.ts", "src/facade/mockFacade.ts", "src/facade/index.ts", "tests/propertyLinkSubListGlobalFacade.test.ts", "test:freecad-property-linksublistglobal-facade"] + }, + { + "id": "PROP-app-propertylinksublistglobal-F", + "evidence": ["tests/propertyLinkSubListGlobalTransaction.test.ts", "test:freecad-property-linksublistglobal-transaction"] + }, + { + "id": "PROP-app-propertylinksublistglobal-G", + "evidence": ["config/freecad-property-linksublistglobal-roundtrip.json", "check:freecad-property-linksublistglobal-roundtrip"] + }, + { + "id": "PROP-app-propertylinksublistglobal-H", + "evidence": ["config/chrome-property-linksublistglobal-verification.json", "check:chrome-property-linksublistglobal"] + }, + { + "id": "PROP-app-propertylinksublistglobal-I", + "evidence": ["config/freecad-property-linksublistglobal-promotion.json", "check:freecad-property-linksublistglobal-promotion"] } ] } diff --git a/config/freecad-native-property-semantics.json b/config/freecad-native-property-semantics.json index f76e7d2..2b4ae7d 100644 --- a/config/freecad-native-property-semantics.json +++ b/config/freecad-native-property-semantics.json @@ -37,16 +37,16 @@ }, "supportSummary": { "native-editable-codec": { - "typeCount": 35, - "recordCount": 4372 + "typeCount": 37, + "recordCount": 4377 }, "native-specialized-codec": { "typeCount": 5, "recordCount": 683 }, "opaque-fcstd-proxy": { - "typeCount": 45, - "recordCount": 455 + "typeCount": 43, + "recordCount": 450 } }, "types": [ @@ -346,7 +346,7 @@ }, { "typeId": "App::PropertyLinkSubHidden", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 4, "objectTypeCount": 4, "statusNames": [ @@ -367,7 +367,7 @@ }, { "typeId": "App::PropertyLinkSubListGlobal", - "support": "opaque-fcstd-proxy", + "support": "native-editable-codec", "recordCount": 1, "objectTypeCount": 1, "statusNames": [] @@ -902,10 +902,10 @@ }, "harness": { "path": "scripts/run-freecad-native-property-semantics.mjs", - "bytes": 6741, - "sha256": "7f97a99a107ac51ddbd96d1b7034e5ad365e04fbd8ad6e49bc6db08b50cb3025" + "bytes": 6805, + "sha256": "ec4151712eb31824997414ab86f9eb440faccd0302ef829de6e55d1b352b662e" }, "exactPromotionReady": false, - "exactBlocker": "45 runtime property types and 455 records remain opaque-only; complete native document and property semantics", - "generatedAt": "2026-08-17T20:51:42.339Z" + "exactBlocker": "43 runtime property types and 450 records remain opaque-only; complete native document and property semantics", + "generatedAt": "2026-08-17T23:37:43.918Z" } diff --git a/config/freecad-property-linksubhidden-mutation.json b/config/freecad-property-linksubhidden-mutation.json new file mode 100644 index 0000000..bcc0630 --- /dev/null +++ b/config/freecad-property-linksubhidden-mutation.json @@ -0,0 +1,1379 @@ +{ + "baselineId": "freecad-1.1.1-property-linksubhidden-mutation", + "caseCount": 4, + "cases": [ + { + "before": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkProbe", + "typeId": "App::Link" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "editRecomputeResult": true, + "edited": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkProbe", + "typeId": "App::Link" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectName": "LinkProbe", + "objectTypeId": "App::Link", + "propertyName": "ColoredElements", + "restoreRecomputeResult": true, + "restored": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkProbe", + "typeId": "App::Link" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkProbe", + "typeId": "App::Link" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [ + "LinkProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "touchedAfterRestore": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkProbe", + "typeId": "App::Link" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [ + "LinkProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "valueRestored": true + }, + { + "before": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "editRecomputeResult": true, + "edited": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectName": "LinkGroupProbe", + "objectTypeId": "App::LinkGroup", + "propertyName": "ColoredElements", + "restoreRecomputeResult": true, + "restored": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "touchedAfterRestore": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "valueRestored": true + }, + { + "before": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupPythonProbe", + "typeId": "App::LinkGroupPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "editRecomputeResult": true, + "edited": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupPythonProbe", + "typeId": "App::LinkGroupPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectName": "LinkGroupPythonProbe", + "objectTypeId": "App::LinkGroupPython", + "propertyName": "ColoredElements", + "restoreRecomputeResult": true, + "restored": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupPythonProbe", + "typeId": "App::LinkGroupPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupPythonProbe", + "typeId": "App::LinkGroupPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "touchedAfterRestore": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkGroupPythonProbe", + "typeId": "App::LinkGroupPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "valueRestored": true + }, + { + "before": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkPythonProbe", + "typeId": "App::LinkPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkPythonProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "editRecomputeResult": true, + "edited": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkPythonProbe", + "typeId": "App::LinkPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkPythonProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectName": "LinkPythonProbe", + "objectTypeId": "App::LinkPython", + "propertyName": "ColoredElements", + "restoreRecomputeResult": true, + "restored": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkPythonProbe", + "typeId": "App::LinkPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Valid", + "targetInLists": { + "ColorSourceA": [ + "LinkPythonProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkPythonProbe", + "typeId": "App::LinkPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [ + "LinkPythonProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "touchedAfterRestore": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "objectSet": [ + { + "name": "LinkPythonProbe", + "typeId": "App::LinkPython" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "ColorSourceA" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "statusString": "Touched", + "targetInLists": { + "ColorSourceA": [ + "LinkPythonProbe" + ], + "ColorSourceB": [] + }, + "targetShapes": { + "ColorSourceA": { + "applicable": true, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8 + }, + "ColorSourceB": { + "applicable": true, + "brepSha256": "c3786ce59c730f9b18585c41062b02591e323238119986649a96ef34eb439851", + "edges": 3, + "faces": 3, + "isNull": false, + "solids": 1, + "vertices": 2 + } + }, + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "valueRestored": true + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyLinkSubHidden", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-linksubhidden-promotion.json b/config/freecad-property-linksubhidden-promotion.json new file mode 100644 index 0000000..fb2e99a --- /dev/null +++ b/config/freecad-property-linksubhidden-promotion.json @@ -0,0 +1,88 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertylinksubhidden-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyLinkSubHidden", + "recordCount": 4, + "phaseEvidence": { + "A": [ + "config/freecad-property-linksubhidden-inventory.json", + "check:freecad-property-linksubhidden-inventory" + ], + "B": [ + "config/freecad-property-linksubhidden-success.json", + "check:freecad-property-linksubhidden-success" + ], + "C": [ + "config/freecad-property-linksubhidden-failure.json", + "check:freecad-property-linksubhidden-failure" + ], + "D": [ + "config/freecad-property-linksubhidden-mutation.json", + "check:freecad-property-linksubhidden-mutation" + ], + "E": [ + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/facade/index.ts", + "tests/propertyLinkSubHiddenFacade.test.ts", + "test:freecad-property-linksubhidden-facade" + ], + "F": [ + "tests/propertyLinkSubHiddenTransaction.test.ts", + "test:freecad-property-linksubhidden-transaction" + ], + "G": [ + "config/freecad-property-linksubhidden-roundtrip.json", + "check:freecad-property-linksubhidden-roundtrip" + ], + "H": [ + "config/chrome-property-linksubhidden-verification.json", + "check:chrome-property-linksubhidden" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "facadeValueModel": "same-document-object-and-ordered-sub-element-name-array-or-null", + "writableRecords": 4, + "objectTypeCount": 4, + "propertyStatus": "LockDynamic,PropHidden", + "linkScope": "Hidden", + "runtimeDependencyEdges": 0, + "fcstdElement": "LinkSub", + "nativeRoundTripValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "browserRoundTripValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "hiddenUiFiltered": true, + "hiddenLinksPreserved": true, + "nativeDependencyMetadataNormalized": true, + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 37, + "nativeEditableRecords": 4377, + "opaqueTypes": 43, + "opaqueRecords": 450, + "exactPromotionReady": false, + "exactBlocker": "43 runtime property types and 450 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-17T23:38:19.736Z" +} diff --git a/config/freecad-property-linksubhidden-roundtrip.json b/config/freecad-property-linksubhidden-roundtrip.json new file mode 100644 index 0000000..ee16b4d --- /dev/null +++ b/config/freecad-property-linksubhidden-roundtrip.json @@ -0,0 +1,369 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-linksubhidden-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyLinkSubHidden", + "archives": { + "nativeInitial": { + "path": ".cache/freecad/property-linksubhidden-roundtrip/native-initial.FCStd", + "bytes": 3123, + "sha256": "f707b0d65984e40a55e9538f76bc3a8953cafcfd3f7139deee10b494e966cac4" + }, + "webEdited": { + "path": ".cache/freecad/property-linksubhidden-roundtrip/web-edited.FCStd", + "bytes": 3021, + "sha256": "ee2630b9866a996245692db6a0ce0c20419616449abe731ac94212ac5c1fdfcc" + }, + "nativeResaved": { + "path": ".cache/freecad/property-linksubhidden-roundtrip/native-resaved.FCStd", + "bytes": 3117, + "sha256": "703168cea20dd1d8affafea452eed8ad7a85df21d2c2747cd944a359a290d9bf" + } + }, + "nativeInitial": { + "object": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "name": "LinkGroupProbe", + "outList": [], + "propertyName": "ColoredElements", + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "App::LinkGroup", + "value": { + "object": "ColorSourceA", + "subElements": [ + "Face1", + "Face3" + ] + } + }, + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "targets": { + "ColorSourceA": { + "inList": [], + "shape": { + "applicable": true, + "area": 52, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "ColorSourceB": { + "inList": [], + "shape": { + "applicable": true, + "area": 61.261056745, + "brepSha256": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c", + "edges": 3, + "faces": 3, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 2, + "volume": 35.342917353 + }, + "typeId": "Part::Feature" + } + } + }, + "web": { + "before": { + "name": "ColoredElements", + "typeId": "App::PropertyLinkSubHidden", + "element": "LinkSub", + "value": "ColorSourceA", + "nativeStatus": 67109120, + "statusNames": [ + "LockDynamic", + "PropHidden" + ], + "subElements": [ + "Face1", + "Face3" + ] + }, + "after": { + "name": "ColoredElements", + "typeId": "App::PropertyLinkSubHidden", + "element": "LinkSub", + "value": "ColorSourceB", + "nativeStatus": 67109120, + "statusNames": [ + "LockDynamic", + "PropHidden" + ], + "subElements": [ + "Face5" + ] + }, + "targetMarkedTouched": true, + "initialDependencyTargets": [ + "ColorSourceA" + ], + "webDependencyTargets": [ + "ColorSourceA" + ], + "hiddenDependencyMetadataPreserved": true, + "webOutputMatches": true, + "objectSetPreserved": true, + "semanticObjectsPreserved": true, + "opaquePathsPreserved": 2, + "opaqueEntriesPreserved": true + }, + "nativeAfter": { + "reopened": { + "object": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "name": "LinkGroupProbe", + "outList": [], + "propertyName": "ColoredElements", + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "App::LinkGroup", + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "targets": { + "ColorSourceA": { + "inList": [], + "shape": { + "applicable": true, + "area": 52, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "ColorSourceB": { + "inList": [], + "shape": { + "applicable": true, + "area": 61.261056745, + "brepSha256": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c", + "edges": 3, + "faces": 3, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 2, + "volume": 35.342917353 + }, + "typeId": "Part::Feature" + } + } + }, + "resaved": { + "object": { + "editorMode": [ + "Hidden" + ], + "group": " Link", + "name": "LinkGroupProbe", + "outList": [], + "propertyName": "ColoredElements", + "propertyStatus": [ + "LockDynamic", + "26" + ], + "propertyTypeId": "App::PropertyLinkSubHidden", + "shape": { + "applicable": false, + "reason": "host-has-no-shape-property" + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "App::LinkGroup", + "value": { + "object": "ColorSourceB", + "subElements": [ + "Face5" + ] + } + }, + "objectSet": [ + { + "name": "LinkGroupProbe", + "typeId": "App::LinkGroup" + }, + { + "name": "ColorSourceA", + "typeId": "Part::Feature" + }, + { + "name": "ColorSourceB", + "typeId": "Part::Feature" + } + ], + "targets": { + "ColorSourceA": { + "inList": [], + "shape": { + "applicable": true, + "area": 52, + "brepSha256": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "edges": 12, + "faces": 6, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "ColorSourceB": { + "inList": [], + "shape": { + "applicable": true, + "area": 61.261056745, + "brepSha256": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c", + "edges": 3, + "faces": 3, + "isNull": false, + "shapeType": "Solid", + "solids": 1, + "valid": true, + "vertices": 2, + "volume": 35.342917353 + }, + "typeId": "Part::Feature" + } + } + } + }, + "classification": { + "requestedValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "webValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "reopenedValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "resavedValue": { + "schemaVersion": 1, + "objectId": "ColorSourceB", + "subElements": [ + "Face5" + ] + }, + "nativeOutputMatches": true, + "hiddenLinksPreserved": true, + "nativeResavedDependencyTargets": [ + "ColorSourceB" + ], + "nativeDependencyMetadataNormalized": true, + "targetShapeStructurePreserved": true, + "ownerShapePreserved": true, + "nativeTargetBrepHashes": [ + { + "ColorSourceA": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "ColorSourceB": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c" + }, + { + "ColorSourceA": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "ColorSourceB": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c" + }, + { + "ColorSourceA": "8c42068f05129bb15610da52f3fef1132d8f4459ae9662549de0c14dbd6ee44b", + "ColorSourceB": "82bf07000178ed1f34895ff3e2510f838cd26a6247e2e395fff73feb2117960c" + } + ], + "unknownSemanticDrift": false, + "zeroUnknownDrift": true + } +} diff --git a/config/freecad-property-linksublistglobal-failure.json b/config/freecad-property-linksublistglobal-failure.json new file mode 100644 index 0000000..930f524 --- /dev/null +++ b/config/freecad-property-linksublistglobal-failure.json @@ -0,0 +1,1137 @@ +{ + "baselineId": "freecad-1.1.1-property-linksublistglobal-failure", + "cancellationBoundary": { + "classification": "synchronous-property-setter", + "reason": "no-native-cancel-hook", + "replacement": "abort-active-document-transaction", + "supported": false + }, + "case": { + "crossDocument": { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "documentPreserved": true, + "exception": { + "message": "PropertyLinkSubList does not support external object", + "type": "ValueError" + }, + "valuePreserved": true + }, + "documentIntegrity": { + "finalObjects": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "foreignDocumentObjects": [ + { + "name": "ForeignBox", + "typeId": "Part::Feature" + } + ], + "initialObjects": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "objectsPreserved": true + }, + "immutable": { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "Immutable" + ], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "Immutable" + ], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "exception": { + "message": "Object attribute 'Support' is read-only", + "type": "AttributeError" + }, + "pythonBypassesImmutable": false + }, + "initial": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "inputs": [ + { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "documentPreserved": true, + "exception": { + "message": "Expects sequence of items of type DocObj, (DocObj,SubName), or (DocObj, (SubName,...))", + "type": "TypeError" + }, + "id": "scalar", + "requested": "'SourceBox'", + "valuePreserved": true + }, + { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox" + ], + "ownerShapeHash": "0c6088cbae94901a6d8d82e8e008b0d6afc11eac54d44fa6dd5ec410a8ebefdb", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [ + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "documentPreserved": false, + "exception": null, + "id": "singlePairShorthand", + "requested": "(, 'Face1')", + "valuePreserved": false + }, + { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "documentPreserved": false, + "exception": null, + "id": "stringObject", + "requested": "[('SourceBox', 'Face1')]", + "valuePreserved": false + }, + { + "after": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "documentPreserved": false, + "exception": null, + "id": "integerObject", + "requested": "[(7, 'Face1')]", + "valuePreserved": false + } + ], + "objectName": "ShapeBinderProbe", + "objectTypeId": "PartDesign::ShapeBinder", + "propertyName": "Support", + "readOnly": { + "after": { + "editorMode": [ + "ReadOnly" + ], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SecondBox" + ], + "ownerShapeHash": "af31fc7d304494cebfe9a18331a0c95ce3a057837616caa947e1804dd6011446", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "ReadOnly" + ], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SecondBox", + "subElements": [ + "Face2" + ] + } + ] + }, + "before": { + "editorMode": [ + "ReadOnly" + ], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [ + "ReadOnly" + ], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "exception": null, + "pythonBypassesEditorReadOnly": true + }, + "transaction": { + "activeAfterAbort": { + "id": 0, + "name": "" + }, + "activeAfterEdit": { + "id": 1, + "name": "property-linksublistglobal-cancel" + }, + "afterAbort": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "afterAbortBeforeRecompute": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Touched", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "before": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "edited": { + "editorMode": [], + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SecondBox" + ], + "ownerShapeHash": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "ownerState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "statusString": "Touched", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [] + }, + "targetShapeHashes": { + "SecondBox": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "SourceBox": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417" + }, + "value": [ + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "pendingAfterAbort": false, + "pendingAfterEdit": true, + "recomputeAfterAbort": true, + "restored": true, + "undoMode": 1 + } + }, + "caseCount": 1, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyLinkSubListGlobal", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-linksublistglobal-inventory.json b/config/freecad-property-linksublistglobal-inventory.json new file mode 100644 index 0000000..911ea5d --- /dev/null +++ b/config/freecad-property-linksublistglobal-inventory.json @@ -0,0 +1,117 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyLinkSubListGlobal", + "recordCount": 1, + "objectTypeCount": 1, + "writableRecordCount": 1, + "records": [ + { + "objectTypeId": "PartDesign::ShapeBinder", + "objectAvailable": true, + "probeStatus": "available", + "propertyName": "Support", + "group": "", + "statusRaw": [], + "statusNames": [], + "defaultRaw": [], + "valueModel": { + "kind": "same-document-object-and-ordered-sub-element-pair-list", + "inheritedFrom": "App::PropertyLinkSubList", + "orderPreserved": true, + "duplicatesPreserved": true, + "writable": true + }, + "inputs": { + "nativeCppSetValues": "parallel DocumentObject and sub-element lists", + "emptyListMeansNoReferences": true, + "nullObjectClears": true, + "detachedObjectRejected": true, + "crossDocumentRejectedWithoutLinkAllowExternal": true, + "invalidSubElementAcceptedAsReference": true + }, + "dependencies": { + "linkScope": "Global", + "createsBacklinks": true, + "includedByDefaultGetLinks": true, + "includedByGetLinksAll": true, + "defaultDagDependency": true + }, + "applicability": { + "requiredObjectTypeId": "PartDesign::ShapeBinder", + "role": "ShapeBinder source geometry support", + "propertyWriteRequiresShape": false, + "source": "PartDesign ShapeBinder Support" + } + } + ], + "typeContract": { + "inheritedFrom": "App::PropertyLinkSubList", + "linkScope": "Global", + "propertyStatus": [], + "xmlElement": "LinkSubList", + "xmlChildElement": "Link", + "emptyListMeans": "no-references", + "crossDocumentBoundary": "reject-without-LinkAllowExternal" + }, + "nativeEvidence": { + "structureOracle": { + "path": "config/freecad-partdesign-structure-oracle.json", + "baselineId": "freecad-1.1.1-partdesign-structure-oracle", + "crossDocumentShapeBinderSupportType": "App::PropertyLinkSubListGlobal", + "crossDocumentAccepted": false, + "crossDocumentError": "PropertyLinkSubList does not support external object", + "crossDocumentShapeBinderSupport": [] + }, + "runtimeHost": "PartDesign::ShapeBinder.Support" + }, + "provenance": { + "runtime": { + "path": ".cache/freecad/reference-desktop.json", + "bytes": 2298709, + "sha256": "082400ec7379e8efc2af16709032d45890d7092277967b714acb05761fd1c2a7" + }, + "oracle": { + "path": "config/freecad-partdesign-structure-oracle.json", + "bytes": 6865, + "sha256": "b5bb2af23231997e3e0d821394eb76135acddc8e75c4a51d9f36715870cddf7e" + }, + "sources": [ + { + "path": ".cache/freecad/FreeCAD/src/App/PropertyLinks.h", + "bytes": 58046, + "sha256": "03176b14948d43fcae3ff290711a913ae4fa43f3ba286d66ceb610e074555d1c" + }, + { + "path": ".cache/freecad/FreeCAD/src/App/PropertyLinks.cpp", + "bytes": 202779, + "sha256": "b4221dd89d2b0b668dc8a3c2852ba53bc67bcb721830e3a6011ea53c1a32edb0" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.h", + "bytes": 6455, + "sha256": "471206a4148fbe7008bbb961be3d7ee88da09ef05baef1fa2fe24bb24f765944" + }, + { + "path": ".cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.cpp", + "bytes": 44837, + "sha256": "189fe3b26f122ff93b9c7abfed2ba08988d3e82b143e423497be170997167876" + } + ] + }, + "classification": "opaque-fcstd-proxy", + "nextPhases": [ + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I" + ] +} diff --git a/config/freecad-property-linksublistglobal-mutation.json b/config/freecad-property-linksublistglobal-mutation.json new file mode 100644 index 0000000..b426e29 --- /dev/null +++ b/config/freecad-property-linksublistglobal-mutation.json @@ -0,0 +1,400 @@ +{ + "baselineId": "freecad-1.1.1-property-linksublistglobal-mutation", + "case": { + "before": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "editRecomputeResult": true, + "edited": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SecondBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SecondBox", + "subElements": [] + } + ] + }, + "objectName": "ShapeBinderProbe", + "objectTypeId": "PartDesign::ShapeBinder", + "propertyName": "Support", + "restoreRecomputeResult": true, + "restored": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "semanticStateRestored": true, + "touchedAfterEdit": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SecondBox" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40.0 + }, + "statusString": "Touched", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SecondBox", + "subElements": [] + } + ] + }, + "touchedAfterRestore": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerState": [ + "Touched" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "statusString": "Touched", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "valueRestored": true + }, + "caseCount": 1, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyLinkSubListGlobal", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/freecad-property-linksublistglobal-promotion.json b/config/freecad-property-linksublistglobal-promotion.json new file mode 100644 index 0000000..aac89de --- /dev/null +++ b/config/freecad-property-linksublistglobal-promotion.json @@ -0,0 +1,100 @@ +{ + "schemaVersion": 1, + "status": "pass", + "taskId": "PROP-app-propertylinksublistglobal-I", + "baseline": { + "freecadVersion": "1.1.1", + "commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + }, + "propertyType": "App::PropertyLinkSubListGlobal", + "recordCount": 1, + "phaseEvidence": { + "A": [ + "config/freecad-property-linksublistglobal-inventory.json", + "check:freecad-property-linksublistglobal-inventory" + ], + "B": [ + "config/freecad-property-linksublistglobal-success.json", + "check:freecad-property-linksublistglobal-success" + ], + "C": [ + "config/freecad-property-linksublistglobal-failure.json", + "check:freecad-property-linksublistglobal-failure" + ], + "D": [ + "config/freecad-property-linksublistglobal-mutation.json", + "check:freecad-property-linksublistglobal-mutation" + ], + "E": [ + "src/facade/types.ts", + "src/facade/fcstd.ts", + "src/facade/mockFacade.ts", + "src/facade/index.ts", + "tests/propertyLinkSubListGlobalFacade.test.ts", + "test:freecad-property-linksublistglobal-facade" + ], + "F": [ + "tests/propertyLinkSubListGlobalTransaction.test.ts", + "test:freecad-property-linksublistglobal-transaction" + ], + "G": [ + "config/freecad-property-linksublistglobal-roundtrip.json", + "check:freecad-property-linksublistglobal-roundtrip" + ], + "H": [ + "config/chrome-property-linksublistglobal-verification.json", + "check:chrome-property-linksublistglobal" + ] + }, + "promotion": { + "from": "opaque-fcstd-proxy", + "to": "native-editable-codec", + "facadeValueModel": "same-document-object-and-ordered-sub-element-pair-list", + "writableRecords": 1, + "objectTypeCount": 1, + "propertyStatus": "", + "linkScope": "Global", + "runtimeDependencyEdges": 2, + "fcstdElement": "LinkSubList", + "nativeRoundTripValue": [ + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + }, + { + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "browserRoundTripValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "globalBacklinksPreserved": true, + "nativeDependenciesNormalized": true, + "zeroUnknownDrift": true + }, + "exactBlockerSync": { + "nativeEditableTypes": 37, + "nativeEditableRecords": 4377, + "opaqueTypes": 43, + "opaqueRecords": 450, + "exactPromotionReady": false, + "exactBlocker": "43 runtime property types and 450 records remain opaque-only; complete native document and property semantics" + }, + "systemExact": false, + "generatedAt": "2026-08-18T00:49:36.963Z" +} diff --git a/config/freecad-property-linksublistglobal-roundtrip.json b/config/freecad-property-linksublistglobal-roundtrip.json new file mode 100644 index 0000000..7a140c1 --- /dev/null +++ b/config/freecad-property-linksublistglobal-roundtrip.json @@ -0,0 +1,396 @@ +{ + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-linksublistglobal-roundtrip", + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "native": { + "created": { + "object": { + "editorMode": [], + "group": "", + "name": "ShapeBinderProbe", + "outList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "propertyName": "Support", + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40 + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "PartDesign::ShapeBinder", + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "targets": { + "SecondBox": { + "inList": [ + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "SourceBox": { + "inList": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120 + }, + "typeId": "Part::Feature" + } + } + }, + "inspectedType": "App::PropertyLinkSubListGlobal", + "inspectedElement": "LinkSubList", + "decodedValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SourceBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face2" + }, + { + "objectId": "SecondBox", + "subElement": "Face1" + } + ] + }, + "decoded": true, + "reopened": { + "object": { + "editorMode": [], + "group": "", + "name": "ShapeBinderProbe", + "outList": [ + "SecondBox", + "SourceBox" + ], + "propertyName": "Support", + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "b8d41291bdc8b3c0ecf6b3ce516eb8e58dc57f7e98c7f61c066d7ce5371b2257", + "edges": 4, + "faces": 1, + "isNull": false, + "solids": 0, + "vertices": 4, + "volume": 0 + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "PartDesign::ShapeBinder", + "value": [ + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + }, + { + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "targets": { + "SecondBox": { + "inList": [ + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "SourceBox": { + "inList": [ + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120 + }, + "typeId": "Part::Feature" + } + } + }, + "resaved": { + "object": { + "editorMode": [], + "group": "", + "name": "ShapeBinderProbe", + "outList": [ + "SecondBox", + "SourceBox" + ], + "propertyName": "Support", + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "shape": { + "brepSha256": "b8d41291bdc8b3c0ecf6b3ce516eb8e58dc57f7e98c7f61c066d7ce5371b2257", + "edges": 4, + "faces": 1, + "isNull": false, + "solids": 0, + "vertices": 4, + "volume": 0 + }, + "state": [ + "Up-to-date" + ], + "statusString": "Valid", + "typeId": "PartDesign::ShapeBinder", + "value": [ + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + }, + { + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "targets": { + "SecondBox": { + "inList": [ + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24 + }, + "typeId": "Part::Feature" + }, + "SourceBox": { + "inList": [ + "ShapeBinderProbe" + ], + "shape": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120 + }, + "typeId": "Part::Feature" + } + } + } + }, + "web": { + "beforeValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SourceBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face2" + }, + { + "objectId": "SecondBox", + "subElement": "Face1" + } + ] + }, + "afterValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "beforeDecoded": true, + "afterDecoded": true, + "archiveBytes": 4013, + "inspectedType": "App::PropertyLinkSubListGlobal", + "inspectedElement": "LinkSubList", + "objectCount": 3 + }, + "classification": { + "requestedValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "nativeInitialValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SourceBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face2" + }, + { + "objectId": "SecondBox", + "subElement": "Face1" + } + ] + }, + "webValue": { + "schemaVersion": 1, + "entries": [ + { + "objectId": "SecondBox", + "subElement": "Face1" + }, + { + "objectId": "SourceBox", + "subElement": "Face1" + } + ] + }, + "resavedValue": [ + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + }, + { + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "zeroUnknownDrift": true, + "globalBacklinksPreserved": true, + "nativeDependenciesNormalized": true + } +} diff --git a/config/freecad-property-linksublistglobal-success.json b/config/freecad-property-linksublistglobal-success.json new file mode 100644 index 0000000..c0d69ba --- /dev/null +++ b/config/freecad-property-linksublistglobal-success.json @@ -0,0 +1,312 @@ +{ + "baselineId": "freecad-1.1.1-property-linksublistglobal-success", + "bindingBoundary": { + "emptyListMeans": "no-references", + "multipleObjectReferencesPreserved": true, + "oracleDefault": [], + "safeFirstRead": "assign-empty-list-before-read", + "shapeBinderUsesFirstPartFeature": true + }, + "caseCount": 1, + "cases": [ + { + "group": "", + "mode": "direct-native-property-setter", + "objectName": "ShapeBinderProbe", + "objectTypeId": "PartDesign::ShapeBinder", + "phases": { + "empty": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "recomputeResult": true, + "shape": { + "brepSha256": null, + "edges": 0, + "faces": 0, + "isNull": true, + "solids": 0, + "vertices": 0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [] + }, + "multipleObjects": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox", + "SecondBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "recomputeResult": true, + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [ + "ShapeBinderProbe" + ], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + }, + { + "object": "SecondBox", + "subElements": [ + "Face1" + ] + } + ] + }, + "sameObjectSubElements": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox", + "SourceBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "recomputeResult": true, + "shape": { + "brepSha256": "cdf63b68f7523256f757529d76f1a8ee791ee8520012e006431f965fe4c8eaa7", + "edges": 8, + "faces": 2, + "isNull": false, + "solids": 0, + "vertices": 8, + "volume": 40.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [ + "ShapeBinderProbe", + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [ + "Face1", + "Face2" + ] + } + ] + }, + "wholeObject": { + "editorMode": [], + "group": "", + "objectSet": [ + { + "name": "ShapeBinderProbe", + "typeId": "PartDesign::ShapeBinder" + }, + { + "name": "SourceBox", + "typeId": "Part::Feature" + }, + { + "name": "SecondBox", + "typeId": "Part::Feature" + } + ], + "ownerOutList": [ + "SourceBox" + ], + "ownerState": [ + "Up-to-date" + ], + "propertyStatus": [], + "propertyTypeId": "App::PropertyLinkSubListGlobal", + "recomputeResult": true, + "shape": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + }, + "statusString": "Valid", + "targetInLists": { + "SecondBox": [], + "SourceBox": [ + "ShapeBinderProbe" + ] + }, + "targetShapes": { + "SecondBox": { + "brepSha256": "e1a7def2954228be09134d37fae0aab512d591e4e2d42e37ede4272e49ca8259", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 24.0 + }, + "SourceBox": { + "brepSha256": "02946feeb666b112d0ff0b224301cf5e9aa877a951e97f8f41126c6d4151d417", + "edges": 12, + "faces": 6, + "isNull": false, + "solids": 1, + "vertices": 8, + "volume": 120.0 + } + }, + "value": [ + { + "object": "SourceBox", + "subElements": [] + } + ] + } + }, + "propertyName": "Support" + } + ], + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "propertyType": "App::PropertyLinkSubListGlobal", + "schemaVersion": 1, + "status": "pass" +} diff --git a/config/qa04-fcstd-fuzz-verification.json b/config/qa04-fcstd-fuzz-verification.json index 5bba307..52fd36f 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.135, - "maxMs": 2.707 + "p95Ms": 0.123, + "maxMs": 2.653 } } diff --git a/config/qa04-geometry-fuzz-verification.json b/config/qa04-geometry-fuzz-verification.json index 2389dab..0ecc47d 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.021, - "maxMs": 1.057 + "p95Ms": 0.02, + "maxMs": 0.499 } } diff --git a/config/qa04-sketch-fuzz-verification.json b/config/qa04-sketch-fuzz-verification.json index 7c0cdf3..792d0bd 100644 --- a/config/qa04-sketch-fuzz-verification.json +++ b/config/qa04-sketch-fuzz-verification.json @@ -22,7 +22,7 @@ "invalid": 800 }, "timing": { - "p95Ms": 0.04, - "maxMs": 0.907 + "p95Ms": 0.029, + "maxMs": 0.77 } } diff --git a/config/release-artifacts.json b/config/release-artifacts.json index df8ff6d..56fc380 100644 --- a/config/release-artifacts.json +++ b/config/release-artifacts.json @@ -22,16 +22,16 @@ "bytes": 556597, "sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105" }, - { - "path": "assets/index-CMgfoKgp.js", - "bytes": 1215336, - "sha256": "1b64ba3601119cad5345af95457d701087124370070778c1f637db8130b84e43" - }, { "path": "assets/index-CSuKYsCZ.css", "bytes": 67322, "sha256": "32d3157ecf81272245f0d8aa1a11887cefb8aa9502f966df90a83797519df208" }, + { + "path": "assets/index-N_Zqif4x.js", + "bytes": 1233820, + "sha256": "e2ce6dac513252b30a40a5a70d1a7a1b71759410fae147740e37413969554f9e" + }, { "path": "assets/nativeHistoryWorkerEntry-BEDB1h7o.js", "bytes": 26720, @@ -335,7 +335,7 @@ { "path": "index.html", "bytes": 678, - "sha256": "215c8954a63826c2bfa193c7e38b3678357e61045b31382233d71353a84031da" + "sha256": "764ef600bfe7d428836d1058b8b58cb419367be9fe1c7d8470e98575279c9181" }, { "path": "manifest.webmanifest", @@ -390,7 +390,7 @@ { "path": "sw.js", "bytes": 1390, - "sha256": "a480fa6da433c33ca56b3d12f983d210b9424ed9123c2e35b8e21926b4e054dd" + "sha256": "f03a7831f2a3a5e3106e7aa6c35017d294f772dacfe338cb7724d32ed8a89eb6" }, { "path": "vendor/camotics/camotics-sweep.wasm", @@ -412,7 +412,7 @@ "status": "signed", "algorithm": "Ed25519", "keyId": "bitbybit-local-release-2026", - "payloadSha256": "b0fefc0b89c0e83124e7ea410bbde34b6f62a4955669b51fb821ba3df181f945", - "signature": "GJdf3bOkY9vdg/YKfFWeh1dkszxpg16VlYdTmrjpLe6PMfhlzvcYQnye4sWy5bFSPoLbXdD7FHom+6e4GTVFCw==" + "payloadSha256": "dee0cd0b74516c5ba582ea9b4fa507193b56564df833e59f519959891107f6dd", + "signature": "1vAcv9Tk6ejX0MmUECV0yamIQlzqZyueWLWsP35JAeEngyBQV2Zd7O6JpWcdy/kjwKqfHs13qaYmSIfXnQkMBQ==" } } diff --git a/docs/continuation-status.zh-CN.md b/docs/continuation-status.zh-CN.md index 7772aaf..7c362cb 100644 --- a/docs/continuation-status.zh-CN.md +++ b/docs/continuation-status.zh-CN.md @@ -1985,3 +1985,19 @@ recovery 进程从两段 open-wire Sketch 启动 Pad,首次接受捕获 `Input Rotate 使用只编辑第二操作的角度轨迹 `15 -> 22.5 -> 15`。22.5 度阶段改变包围盒,恢复后 Shape 摘要、原生 history SHA-256 与语义命名 SHA-256 精确回到 15 度基线;最终 Shape 和 23836 字节命名证据在 FreeCAD 1.1.1 的 save/reopen/resave 三态保持稳定。检查器同时锁定执行器自身、命名矩阵、FCStd resave harness 与三项 OCCT 产物的 bytes/SHA-256,防止旧报告通过。 `fuse->rotate` 因此以 accepted 关闭。TSN correlation 当前覆盖 8/361 对,剩余 353 对;活动队列为 134 completed、1 in_progress、352 pending,唯一下一项为 `TSN-PAIR-fuse-pad`。这条独立 Rotate 变异证据不等于全局注册 rotate mutation family 已关闭,`EX-TSN-04` 继续保持 in_progress。 + +## 254. 2026-08-17 PropertyLinkSubHidden 原生 codec 与浏览器往返闭合 + +锁定 FreeCAD 1.1.1 的 `App::PropertyLinkSubHidden` 已完成四个原生宿主 `App::Link`、`App::LinkGroup`、`App::LinkGroupPython`、`App::LinkPython` 的 mutation、失败恢复和保存/重开/再次保存证据。原生值模型为同文档对象加有序 sub-element 名称,空 sub-list 表示整个对象;`LockDynamic + PropHidden` 同时表达动态属性和隐藏编辑器,Hidden link 不进入普通 DAG/backlink,但仍保留 `LinkSub`/`Sub` XML 语义。编辑 `ColorSourceA.Face1/Face3 -> ColorSourceB.Face5` 后四个宿主均恢复到初值,目标实体的面/边/点拓扑和 BREP 摘要不变,跨文档对象被原生拒绝且文档无污染。 + +Web Facade 新增 `LinkSubValue` 的深拷贝/校验路径与 `rewriteFcstdLinkSubHiddenProperty`,结构化写入不创建依赖边,保留隐藏 status、未知归档条目和原生 `LinkSub` XML;Undo/Redo、stale version、AbortSignal、失败回滚和资源 ownership 均有聚焦测试。真实 Chrome 150 在 COOP/COEP 隔离环境中完成 UI 隐藏过滤、SQLite WASM/OPFS save-load-checkpoint-recovery、FCStd 投影和资源 marker 清理,Worker 请求全链覆盖,结束时 Shape/kernel reference 为 `0/0`。 + +`PROP-app-propertylinksubhidden-A..I` 已连续关闭并生成 promotion 报告,Property 语义从 `opaque-fcstd-proxy` 晋级为 `native-editable-codec`(4 records / 4 object types,Hidden scope,0 runtime dependency edges)。全局边界同步为 36 个 native-editable 类型/4376 条记录、44 个 opaque 类型/451 条记录,`systemExact=false` 保持不变。活动队列自动推进到 `PROP-app-propertylinksublistglobal-A`,当前为 613 completed、1 in_progress、1142 pending;本轮聚焦门禁、完整 `./npmw run verify`、构建、发布签名、JSON 解析和 `git diff --check` 均通过。 + +## 255. 2026-08-17 PropertyLinkSubListGlobal 浏览器与 promotion 闭合 + +锁定 FreeCAD 1.1.1 的 `App::PropertyLinkSubListGlobal` 已完成 `PartDesign::ShapeBinder.Support` 的全局链接语义:空列表、跨对象有序 pair 列表、同一对象多 sub-element、整对象引用、重复项保留、同文档依赖和 backlink 均由原生成功/失败/变异/事务/FCStd 回放锁定;没有 `LinkAllowExternal` 时跨文档对象被拒绝。Web Facade 将其建模为 `LinkSubListValue`,保留顺序和重复项并创建真实 DAG 边,FCStd 使用 `LinkSubList` 元素双向投影。 + +真实 Chrome 150 在 COOP/COEP 隔离环境完成 PropertyPanel、SQLite WASM/OPFS save-load-checkpoint-recovery、Global 依赖计数、FCStd 解码、资源 marker 清理和 Worker dispose;3 条初始引用编辑为 2 条后,文档版本为 2、对象状态为 `touched`、依赖边为 2,Shape/kernel reference 最终为 `0/0`。此前 harness 的无效 `mockFacade` 导入已修正为 production Facade,并补入 Facade boundary 白名单。 + +`PROP-app-propertylinksublistglobal-A..I` 已连续关闭并生成 promotion 报告,Property 语义从 `opaque-fcstd-proxy` 晋级为 `native-editable-codec`(1 record / 1 object type,Global scope,2 runtime dependency edges,native-editable 总计 37 types/4377 records,opaque 43/450)。`systemExact=false` 和 exact blocker 保持 fail-closed;活动队列推进到 `PROP-app-propertymap-A`,当前为 622 completed、1 in_progress、1133 pending。聚焦 Facade/事务/roundtrip/Chrome/promotion 门禁、生产构建、PWA/Release closure、发布签名、`git diff --check` 和完整 `./npmw run verify` 均通过。 diff --git a/docs/freecad-active-work-status.generated.zh-CN.md b/docs/freecad-active-work-status.generated.zh-CN.md index f164daa..b19fd0f 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-propertylinksubhidden-D` -- 微任务:1756 total / 607 completed / 1 in_progress / 1148 pending +- 唯一活动任务:`PROP-app-propertymap-A` +- 微任务:1756 total / 622 completed / 1 in_progress / 1133 pending - 有序操作对:361/361 classified / 0 unknown -- Property:40/85 native types / 45 opaque types +- Property:42/85 native types / 43 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 | 120 | 1 | 401 | `PROP-app-propertylinksubhidden-D` | +| `PROPERTY-CODECS` | 522 | 135 | 1 | 386 | `PROP-app-propertymap-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 0affe9a..72bab92 100644 --- a/package.json +++ b/package.json @@ -204,12 +204,38 @@ "check:chrome-property-linklisthidden": "node scripts/check-chrome-property-linklisthidden.mjs", "generate:freecad-property-linklisthidden-promotion": "node scripts/generate-freecad-property-linklisthidden-promotion.mjs", "check:freecad-property-linklisthidden-promotion": "node scripts/check-freecad-property-linklisthidden-promotion.mjs", + "generate:freecad-property-linksubhidden-promotion": "node scripts/generate-freecad-property-linksubhidden-promotion.mjs", + "check:freecad-property-linksubhidden-promotion": "node scripts/check-freecad-property-linksubhidden-promotion.mjs", "generate:freecad-property-linksubhidden-inventory": "node scripts/generate-freecad-property-linksubhidden-inventory.mjs", "check:freecad-property-linksubhidden-inventory": "node scripts/check-freecad-property-linksubhidden-inventory.mjs", + "generate:freecad-property-linksublistglobal-inventory": "node scripts/generate-freecad-property-linksublistglobal-inventory.mjs", + "check:freecad-property-linksublistglobal-inventory": "node scripts/check-freecad-property-linksublistglobal-inventory.mjs", + "generate:freecad-property-linksublistglobal-promotion": "node scripts/generate-freecad-property-linksublistglobal-promotion.mjs", + "check:freecad-property-linksublistglobal-promotion": "node scripts/check-freecad-property-linksublistglobal-promotion.mjs", + "probe:freecad-property-linksublistglobal-success": "node scripts/run-freecad-property-linksublistglobal-success.mjs", + "check:freecad-property-linksublistglobal-success": "node scripts/check-freecad-property-linksublistglobal-success.mjs", + "probe:freecad-property-linksublistglobal-failure": "node scripts/run-freecad-property-linksublistglobal-failure.mjs", + "check:freecad-property-linksublistglobal-failure": "node scripts/check-freecad-property-linksublistglobal-failure.mjs", + "probe:freecad-property-linksublistglobal-mutation": "node scripts/run-freecad-property-linksublistglobal-mutation.mjs", + "check:freecad-property-linksublistglobal-mutation": "node scripts/check-freecad-property-linksublistglobal-mutation.mjs", + "test:freecad-property-linksublistglobal-facade": "tsx --test tests/propertyLinkSubListGlobalFacade.test.ts", + "test:freecad-property-linksublistglobal-transaction": "tsx --test tests/propertyLinkSubListGlobalTransaction.test.ts", + "probe:freecad-property-linksublistglobal-roundtrip": "tsx scripts/run-freecad-property-linksublistglobal-roundtrip.ts", + "check:freecad-property-linksublistglobal-roundtrip": "node scripts/check-freecad-property-linksublistglobal-roundtrip.mjs", + "test:chrome-property-linksublistglobal": "node scripts/run-chrome-property-linksublistglobal.mjs", + "check:chrome-property-linksublistglobal": "node scripts/check-chrome-property-linksublistglobal.mjs", "probe:freecad-property-linksubhidden-success": "node scripts/run-freecad-property-linksubhidden-success.mjs", "check:freecad-property-linksubhidden-success": "node scripts/check-freecad-property-linksubhidden-success.mjs", "probe:freecad-property-linksubhidden-failure": "node scripts/run-freecad-property-linksubhidden-failure.mjs", "check:freecad-property-linksubhidden-failure": "node scripts/check-freecad-property-linksubhidden-failure.mjs", + "probe:freecad-property-linksubhidden-mutation": "node scripts/run-freecad-property-linksubhidden-mutation.mjs", + "check:freecad-property-linksubhidden-mutation": "node scripts/check-freecad-property-linksubhidden-mutation.mjs", + "test:freecad-property-linksubhidden-facade": "tsx --test tests/propertyLinkSubHiddenFacade.test.ts", + "test:freecad-property-linksubhidden-transaction": "tsx --test tests/propertyLinkSubHiddenTransaction.test.ts", + "probe:freecad-property-linksubhidden-roundtrip": "tsx scripts/run-freecad-property-linksubhidden-roundtrip.ts", + "check:freecad-property-linksubhidden-roundtrip": "node scripts/check-freecad-property-linksubhidden-roundtrip.mjs", + "test:chrome-property-linksubhidden": "node scripts/run-chrome-property-linksubhidden.mjs", + "check:chrome-property-linksubhidden": "node scripts/check-chrome-property-linksubhidden.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-linksubhidden.mjs b/scripts/check-chrome-property-linksubhidden.mjs new file mode 100644 index 0000000..11c01bd --- /dev/null +++ b/scripts/check-chrome-property-linksubhidden.mjs @@ -0,0 +1,12 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname); const report = JSON.parse(await readFile(resolve(root, 'config/chrome-property-linksubhidden-verification.json'), 'utf8')); const fail = (message) => { throw new Error(`Chrome PropertyLinkSubHidden check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid') +const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']; if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete') +const initial = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] }; const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }; const same = (left, right) => isDeepStrictEqual(left, right) +if (report.ui?.visibleLabelInput !== true || report.ui.hiddenControlCountBefore !== 0 || report.ui.hiddenControlCountAfter !== 0 || report.ui.hiddenTextPresentBefore !== false || report.ui.hiddenTextPresentAfter !== false || report.ui.hiddenStatus !== true || !same(report.ui.beforeValue, initial) || !same(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.dependencyCount !== 0 || report.ui.errors?.length !== 0) fail('production hidden-property UI state is incomplete') +if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !same(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyLinkSubHidden' || report.persistence.loadedDependencyCount !== 0 || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'LinkSub' || !same(report.persistence.fcstdValue, target) || report.persistence.fcstdNativeStatus !== 67108864) fail('OPFS/FCStd persistence evidence is incomplete') +if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release.kernelReferenceCount !== 0 || report.release.workerTerminated !== true) fail('resource release evidence is incomplete') +console.log(JSON.stringify({ status: 'chrome-property-linksubhidden-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2)) diff --git a/scripts/check-chrome-property-linksublistglobal.mjs b/scripts/check-chrome-property-linksublistglobal.mjs new file mode 100644 index 0000000..ad37a91 --- /dev/null +++ b/scripts/check-chrome-property-linksublistglobal.mjs @@ -0,0 +1,4 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +const root = resolve(new URL('..', import.meta.url).pathname); const report = JSON.parse(await readFile(resolve(root, 'config/chrome-property-linksublistglobal-verification.json'), 'utf8')); const fail = (message) => { throw new Error(`Chrome PropertyLinkSubListGlobal check failed: ${message}`) }; if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || !/Chrome\//.test(report.userAgent || '') || report.crossOriginIsolated !== true) fail('browser boundary is invalid'); const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']; if (!report.worker?.constructed?.some(({ name }) => name === 'bitbybit-persistence') || !requiredWorkerRequests.every((type) => report.worker.requestTypes?.includes(type)) || report.worker.terminated?.includes('bitbybit-persistence') !== true) fail('persistence Worker lifecycle is incomplete'); const initial = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] }; const target = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] }; if (report.ui?.visibleLabelInput !== true || report.ui.controlsBefore !== 3 || report.ui.controlsAfter !== 2 || !isDeepStrictEqual(report.ui.beforeValue, initial) || !isDeepStrictEqual(report.ui.afterValue, target) || report.ui.documentVersion !== 2 || report.ui.dirty !== true || report.ui.objectState !== 'touched' || report.ui.dependencyCount !== 2 || report.ui.errors?.length !== 0) fail('production Global property UI state is incomplete'); if (report.persistence?.mode !== 'sqlite-opfs' || report.persistence.sqliteWasm !== true || report.persistence.opfs !== true || report.persistence.savedMode !== 'sqlite-opfs' || !isDeepStrictEqual(report.persistence.loadedValue, target) || report.persistence.loadedType !== 'App::PropertyLinkSubListGlobal' || report.persistence.loadedDependencyCount !== 2 || report.persistence.checkpointVersion !== 2 || report.persistence.recoveryIntegrity !== 'ok' || report.persistence.fcstdElement !== 'LinkSubList' || report.persistence.fcstdType !== 'App::PropertyLinkSubListGlobal' || !isDeepStrictEqual(report.persistence.fcstdValue, target) || report.persistence.fcstdDecoded !== true) fail('OPFS/FCStd persistence evidence is incomplete'); if (!(report.resource?.byteLength > 0) || report.resource.roundTrip !== true || report.resource.released !== true || report.resource.markerRemoved !== true || report.release?.shapeCount !== 0 || report.release?.kernelReferenceCount !== 0 || report.release?.workerTerminated !== true) fail('resource release evidence is incomplete'); console.log(JSON.stringify({ status: 'chrome-property-linksublistglobal-pass', ui: report.ui, persistence: report.persistence, workerRequests: report.worker.requestTypes, resource: report.resource, release: report.release }, null, 2)) diff --git a/scripts/check-facade-boundary.mjs b/scripts/check-facade-boundary.mjs index 906df87..4cdff20 100644 --- a/scripts/check-facade-boundary.mjs +++ b/scripts/check-facade-boundary.mjs @@ -24,6 +24,8 @@ const allowWorkerMessaging = new Set([ 'chromePropertyHeatFluxHarness.tsx', 'chromePropertyIntegerSetHarness.tsx', 'chromePropertyLinkListHiddenHarness.tsx', + 'chromePropertyLinkSubHiddenHarness.tsx', + 'chromePropertyLinkSubListGlobalHarness.tsx', 'facade/camPipeline.ts', ]) diff --git a/scripts/check-freecad-native-property-semantics.mjs b/scripts/check-freecad-native-property-semantics.mjs index e32c509..52309a0 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 !== '45 runtime property types and 455 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid') +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('report boundary is invalid') if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed') const support = report.supportSummary -if (support?.['native-editable-codec']?.typeCount !== 35 || support['native-editable-codec'].recordCount !== 4372 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 45 || support['opaque-fcstd-proxy'].recordCount !== 455) fail('property support partition is stale') +if (support?.['native-editable-codec']?.typeCount !== 37 || support['native-editable-codec'].recordCount !== 4377 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 43 || support['opaque-fcstd-proxy'].recordCount !== 450) fail('property support partition is stale') if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid') const acceleration = report.types.find(({ typeId }) => typeId === 'App::PropertyAcceleration') if (acceleration?.support !== 'native-editable-codec' || acceleration.recordCount !== 1 || acceleration.objectTypeCount !== 1) fail('App::PropertyAcceleration promotion is missing') @@ -36,6 +36,10 @@ const integerSet = report.types.find(({ typeId }) => typeId === 'App::PropertyIn if (integerSet?.support !== 'native-editable-codec' || integerSet.recordCount !== 2 || integerSet.objectTypeCount !== 2 || integerSet.statusNames?.length !== 0) fail('App::PropertyIntegerSet promotion is missing') const linkListHidden = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkListHidden') if (linkListHidden?.support !== 'native-editable-codec' || linkListHidden.recordCount !== 2 || linkListHidden.objectTypeCount !== 2 || linkListHidden.statusNames?.join(',') !== 'PropHidden') fail('App::PropertyLinkListHidden promotion is missing') +const linkSubHidden = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkSubHidden') +if (linkSubHidden?.support !== 'native-editable-codec' || linkSubHidden.recordCount !== 4 || linkSubHidden.objectTypeCount !== 4 || linkSubHidden.statusNames?.join(',') !== 'LockDynamic,PropHidden') fail('App::PropertyLinkSubHidden promotion is missing') +const linkSubListGlobal = report.types.find(({ typeId }) => typeId === 'App::PropertyLinkSubListGlobal') +if (linkSubListGlobal?.support !== 'native-editable-codec' || linkSubListGlobal.recordCount !== 1 || linkSubListGlobal.objectTypeCount !== 1 || linkSubListGlobal.statusNames?.length !== 0) fail('App::PropertyLinkSubListGlobal promotion is missing') if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 0 || report.propertyStatus.proxyOnly?.length !== 0 || report.propertyStatus.facadeNative?.join(',') !== report.propertyStatus.observed?.join(',')) fail('property status representation coverage is stale') if (report.propertyStatus.behaviorNative?.join(',') !== 'Hidden,Immutable,LockDynamic,NoModify,Ordered,Output,PartialTrigger,PropHidden,PropNoPersist,PropNoRecompute,PropOutput,PropReadOnly,PropTransient,ReadOnly,Transient' || report.propertyStatus.preservedOnly?.length !== 0 || report.propertyStatus.preservedOnlyRecordCount !== 0) fail('property status behavior boundary is stale') for (const locked of [report.source, report.harness]) { @@ -43,4 +47,4 @@ for (const locked of [report.source, report.harness]) { const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)]) if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`) } -console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5055, opaqueProxyRecords: 455, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId, font.typeId, force.typeId, heatFlux.typeId, integerSet.typeId, linkListHidden.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2)) +console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 5060, opaqueProxyRecords: 450, promotedTypes: [acceleration.typeId, area.typeId, boolList.typeId, color.typeId, colorList.typeId, direction.typeId, file.typeId, fileIncluded.typeId, font.typeId, force.typeId, heatFlux.typeId, integerSet.typeId, linkListHidden.typeId, linkSubHidden.typeId, linkSubListGlobal.typeId], representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false, exactBlocker: report.exactBlocker }, null, 2)) diff --git a/scripts/check-freecad-property-linksubhidden-mutation.mjs b/scripts/check-freecad-property-linksubhidden-mutation.mjs new file mode 100644 index 0000000..f1bfbb6 --- /dev/null +++ b/scripts/check-freecad-property-linksubhidden-mutation.mjs @@ -0,0 +1,32 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksubhidden-mutation.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden mutation check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksubhidden-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubHidden' || report.caseCount !== 4 || report.cases?.length !== 4) fail('baseline is invalid') + +const expectedHosts = new Map([ + ['App::Link', { name: 'LinkProbe', ownerOutList: ['ColorSourceA'], targetInLists: { ColorSourceA: ['LinkProbe'], ColorSourceB: [] } }], + ['App::LinkGroup', { name: 'LinkGroupProbe', ownerOutList: [], targetInLists: { ColorSourceA: [], ColorSourceB: [] } }], + ['App::LinkGroupPython', { name: 'LinkGroupPythonProbe', ownerOutList: [], targetInLists: { ColorSourceA: [], ColorSourceB: [] } }], + ['App::LinkPython', { name: 'LinkPythonProbe', ownerOutList: ['ColorSourceA'], targetInLists: { ColorSourceA: ['LinkPythonProbe'], ColorSourceB: [] } }], +]) +const expectedObjects = (ownerName, ownerTypeId) => [{ name: ownerName, typeId: ownerTypeId }, { name: 'ColorSourceA', typeId: 'Part::Feature' }, { name: 'ColorSourceB', typeId: 'Part::Feature' }] +const baselineValue = { object: 'ColorSourceA', subElements: ['Face1', 'Face3'] } +const editedValue = { object: 'ColorSourceB', subElements: ['Face5'] } + +for (const entry of report.cases) { + const host = expectedHosts.get(entry.objectTypeId) + if (!host || entry.objectName !== host.name || entry.propertyName !== 'ColoredElements' || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.valueRestored !== true || entry.semanticStateRestored !== true) fail(`${entry.objectTypeId} mutation identity or recovery changed`) + if (!isDeepStrictEqual(entry.before?.value, baselineValue) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, editedValue) || !isDeepStrictEqual(entry.edited?.value, editedValue) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, baselineValue) || !isDeepStrictEqual(entry.restored?.value, baselineValue)) fail(`${entry.objectTypeId} value mutation changed`) + if (!isDeepStrictEqual(entry.before.ownerState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.ownerState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched' || !isDeepStrictEqual(entry.edited.ownerState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterRestore.ownerState, ['Touched']) || entry.touchedAfterRestore.statusString !== 'Touched' || !isDeepStrictEqual(entry.restored.ownerState, ['Up-to-date']) || entry.restored.statusString !== 'Valid') fail(`${entry.objectTypeId} state transition changed`) + const baselineTargets = entry.before.targetShapes + for (const [phase, snapshot] of Object.entries({ before: entry.before, touchedAfterEdit: entry.touchedAfterEdit, edited: entry.edited, touchedAfterRestore: entry.touchedAfterRestore, restored: entry.restored })) { + if (snapshot.propertyTypeId !== 'App::PropertyLinkSubHidden' || snapshot.group !== ' Link' || !isDeepStrictEqual(snapshot.propertyStatus, ['LockDynamic', '26']) || !isDeepStrictEqual(snapshot.editorMode, ['Hidden']) || snapshot.shape?.applicable !== false) fail(`${entry.objectTypeId} ${phase} metadata or host Shape changed`) + if (!isDeepStrictEqual(snapshot.ownerOutList, host.ownerOutList) || !isDeepStrictEqual(snapshot.targetInLists, host.targetInLists) || !isDeepStrictEqual(snapshot.targetShapes, baselineTargets) || !isDeepStrictEqual(snapshot.objectSet, expectedObjects(host.name, entry.objectTypeId))) fail(`${entry.objectTypeId} ${phase} leaked dependencies or changed targets/objects`) + } + if (baselineTargets?.ColorSourceA?.applicable !== true || baselineTargets.ColorSourceA.isNull !== false || baselineTargets.ColorSourceA.solids !== 1 || baselineTargets.ColorSourceA.faces !== 6 || baselineTargets.ColorSourceA.edges !== 12 || baselineTargets.ColorSourceA.vertices !== 8 || baselineTargets.ColorSourceB?.applicable !== true || baselineTargets.ColorSourceB.isNull !== false || baselineTargets.ColorSourceB.solids !== 1 || baselineTargets.ColorSourceB.faces !== 3 || baselineTargets.ColorSourceB.edges !== 3 || baselineTargets.ColorSourceB.vertices !== 2) fail(`${entry.objectTypeId} target topology changed`) +} +console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-mutation-pass', cases: report.cases.map(({ objectTypeId, before, edited, restored, semanticStateRestored }) => ({ objectTypeId, before: before.value, edited: edited.value, restored: restored.value, semanticStateRestored })) }, null, 2)) diff --git a/scripts/check-freecad-property-linksubhidden-promotion.mjs b/scripts/check-freecad-property-linksubhidden-promotion.mjs new file mode 100644 index 0000000..3f5a67c --- /dev/null +++ b/scripts/check-freecad-property-linksubhidden-promotion.mjs @@ -0,0 +1,27 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse) +const [report, semantics, progress, roundTrip, chrome] = await Promise.all([ + load('config/freecad-property-linksubhidden-promotion.json'), + load('config/freecad-native-property-semantics.json'), + load('config/freecad-follow-up-task-progress.json'), + load('config/freecad-property-linksubhidden-roundtrip.json'), + load('config/chrome-property-linksubhidden-verification.json'), +]) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden promotion check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertylinksubhidden-I' || report.propertyType !== 'App::PropertyLinkSubHidden' || report.recordCount !== 4 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid') +const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] +const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? []) +for (const phase of requiredPhases) { const taskId = `PROP-app-propertylinksubhidden-${phase}`; if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`) } +const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] } +const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType) +const promotion = report.promotion +if (promotion?.from !== 'opaque-fcstd-proxy' || promotion.to !== 'native-editable-codec' || propertyType?.support !== promotion.to || propertyType.recordCount !== 4 || propertyType.objectTypeCount !== 4 || propertyType.statusNames?.join(',') !== 'LockDynamic,PropHidden' || promotion.facadeValueModel !== 'same-document-object-and-ordered-sub-element-name-array-or-null' || promotion.writableRecords !== 4 || promotion.objectTypeCount !== 4 || promotion.propertyStatus !== 'LockDynamic,PropHidden' || promotion.linkScope !== 'Hidden' || promotion.runtimeDependencyEdges !== 0 || promotion.fcstdElement !== 'LinkSub' || !isDeepStrictEqual(promotion.nativeRoundTripValue, target) || !isDeepStrictEqual(promotion.browserRoundTripValue, target) || promotion.hiddenUiFiltered !== true || promotion.hiddenLinksPreserved !== true || promotion.nativeDependencyMetadataNormalized !== true || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete') +const sync = report.exactBlockerSync +if (sync?.nativeEditableTypes !== 37 || sync.nativeEditableRecords !== 4377 || sync.opaqueTypes !== 43 || sync.opaqueRecords !== 450 || sync.exactPromotionReady !== false || sync.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale') +if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics') +if (roundTrip.classification?.zeroUnknownDrift !== true || !isDeepStrictEqual(roundTrip.classification.resavedValue, target) || roundTrip.classification.hiddenLinksPreserved !== true || roundTrip.classification.nativeDependencyMetadataNormalized !== true || chrome.status !== 'pass' || !isDeepStrictEqual(chrome.persistence?.loadedValue, target) || chrome.persistence?.fcstdElement !== 'LinkSub' || chrome.ui?.hiddenControlCountBefore !== 0 || chrome.ui?.hiddenControlCountAfter !== 0 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed') +console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2)) diff --git a/scripts/check-freecad-property-linksubhidden-roundtrip.mjs b/scripts/check-freecad-property-linksubhidden-roundtrip.mjs new file mode 100644 index 0000000..6dca197 --- /dev/null +++ b/scripts/check-freecad-property-linksubhidden-roundtrip.mjs @@ -0,0 +1,22 @@ +import { createHash } from 'node:crypto' +import { isDeepStrictEqual } from 'node:util' +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksubhidden-roundtrip.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden roundtrip check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksubhidden-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubHidden') fail('baseline is invalid') +const initial = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] } +const target = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] } +const objectSet = [{ name: 'LinkGroupProbe', typeId: 'App::LinkGroup' }, { name: 'ColorSourceA', typeId: 'Part::Feature' }, { name: 'ColorSourceB', typeId: 'Part::Feature' }] +for (const [phase, snapshot, value] of [['nativeInitial', report.nativeInitial, { object: 'ColorSourceA', subElements: ['Face1', 'Face3'] }], ['nativeReopened', report.nativeAfter?.reopened, { object: 'ColorSourceB', subElements: ['Face5'] }], ['nativeResaved', report.nativeAfter?.resaved, { object: 'ColorSourceB', subElements: ['Face5'] }]]) { + if (!isDeepStrictEqual(snapshot?.objectSet, objectSet) || snapshot.object?.name !== 'LinkGroupProbe' || snapshot.object.typeId !== 'App::LinkGroup' || snapshot.object.propertyName !== 'ColoredElements' || snapshot.object.propertyTypeId !== 'App::PropertyLinkSubHidden' || snapshot.object.group !== ' Link' || !isDeepStrictEqual(snapshot.object.value, value) || !isDeepStrictEqual(snapshot.object.propertyStatus, ['LockDynamic', '26']) || !isDeepStrictEqual(snapshot.object.editorMode, ['Hidden']) || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || snapshot.object.statusString !== 'Valid' || !isDeepStrictEqual(snapshot.object.outList, [])) fail(`${phase} native hidden sub-link semantics changed`) + if (snapshot.object.shape?.applicable !== false) fail(`${phase} owner Shape boundary changed`) + for (const targetName of ['ColorSourceA', 'ColorSourceB']) { const item = snapshot.targets?.[targetName]; if (item?.typeId !== 'Part::Feature' || !isDeepStrictEqual(item.inList, []) || item.shape?.applicable !== true || item.shape.isNull !== false || item.shape.valid !== true || item.shape.shapeType !== 'Solid' || item.shape.solids !== 1 || !/^[0-9a-f]{64}$/.test(item.shape.brepSha256)) fail(`${phase} ${targetName} changed`) } +} +if (report.web.before?.typeId !== 'App::PropertyLinkSubHidden' || report.web.before.element !== 'LinkSub' || report.web.after?.typeId !== 'App::PropertyLinkSubHidden' || report.web.after.element !== 'LinkSub' || report.web.targetMarkedTouched !== true || !isDeepStrictEqual(report.web.initialDependencyTargets, ['ColorSourceA']) || !isDeepStrictEqual(report.web.webDependencyTargets, ['ColorSourceA']) || report.web.hiddenDependencyMetadataPreserved !== true || report.web.webOutputMatches !== true || report.web.objectSetPreserved !== true || report.web.semanticObjectsPreserved !== true || report.web.opaqueEntriesPreserved !== true || !Number.isSafeInteger(report.web.opaquePathsPreserved) || report.web.opaquePathsPreserved < 1) fail('Web edit did not preserve the native archive boundary') +if (!isDeepStrictEqual(report.classification?.requestedValue, target) || !isDeepStrictEqual(report.classification.webValue, target) || !isDeepStrictEqual(report.classification.reopenedValue, target) || !isDeepStrictEqual(report.classification.resavedValue, target) || report.classification.nativeOutputMatches !== true || report.classification.hiddenLinksPreserved !== true || !isDeepStrictEqual(report.classification.nativeResavedDependencyTargets, ['ColorSourceB']) || report.classification.nativeDependencyMetadataNormalized !== true || report.classification.targetShapeStructurePreserved !== true || report.classification.ownerShapePreserved !== true || report.classification.unknownSemanticDrift !== false || report.classification.zeroUnknownDrift !== true || report.classification.nativeTargetBrepHashes?.length !== 3) fail('roundtrip classification is incomplete') +for (const hashes of report.classification.nativeTargetBrepHashes) if (!/^[0-9a-f]{64}$/.test(hashes.ColorSourceA) || !/^[0-9a-f]{64}$/.test(hashes.ColorSourceB)) fail('native target BREP evidence is incomplete') +for (const archive of Object.values(report.archives ?? {})) { const path = resolve(root, archive.path ?? ''); const content = await readFile(path); if ((await stat(path)).size !== archive.bytes || createHash('sha256').update(content).digest('hex') !== archive.sha256) fail(`archive is stale: ${archive.path}`) } +console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-roundtrip-pass', values: { nativeInitial: initial, webEdited: report.classification.webValue, nativeReopened: report.classification.reopenedValue, nativeResaved: report.classification.resavedValue }, hiddenLinksPreserved: report.classification.hiddenLinksPreserved, targetShapeStructurePreserved: report.classification.targetShapeStructurePreserved, opaquePathsPreserved: report.web.opaquePathsPreserved, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-failure.mjs b/scripts/check-freecad-property-linksublistglobal-failure.mjs new file mode 100644 index 0000000..6c6cd8f --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-failure.mjs @@ -0,0 +1,34 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-failure.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal failure check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1) fail('baseline is invalid') +const entry = report.case +if (!entry || entry.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support') fail('fixture identity changed') +const initial = entry.initial +if (initial?.propertyTypeId !== report.propertyType || !isDeepStrictEqual(initial.propertyStatus, []) || !isDeepStrictEqual(initial.editorMode, []) || !isDeepStrictEqual(initial.value, [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }]) || initial.statusString !== 'Valid' || !isDeepStrictEqual(initial.ownerState, ['Up-to-date'])) fail('initial global link state is invalid') +const assertGeometryPreserved = (snapshot, label) => { if (!isDeepStrictEqual(snapshot.objectSet, initial.objectSet) || !isDeepStrictEqual(snapshot.targetShapeHashes, initial.targetShapeHashes)) fail(`${label} polluted objects or source Shapes`) } +const expectedInputs = new Map([ + ['scalar', { exception: ['TypeError', 'Expects sequence of items of type DocObj, (DocObj,SubName), or (DocObj, (SubName,...))'], value: initial.value, preserved: true }], + ['singlePairShorthand', { exception: null, value: [{ object: 'SourceBox', subElements: ['Face1'] }], preserved: false }], + ['stringObject', { exception: null, value: [], preserved: false }], + ['integerObject', { exception: null, value: [], preserved: false }], +]) +if (entry.inputs?.length !== expectedInputs.size) fail('invalid-input case count changed') +for (const input of entry.inputs) { + const expected = expectedInputs.get(input.id) + if (!expected || !isDeepStrictEqual(input.before, initial) || !isDeepStrictEqual(input.after.value, expected.value) || input.valuePreserved !== expected.preserved || (expected.exception === null ? input.exception !== null : input.exception?.type !== expected.exception[0] || input.exception.message !== expected.exception[1])) fail(`${input.id} classification changed`) + assertGeometryPreserved(input.after, input.id) +} +if (!entry.crossDocument?.exception || entry.crossDocument.valuePreserved !== true || entry.crossDocument.documentPreserved !== true || !isDeepStrictEqual(entry.crossDocument.before, initial) || !isDeepStrictEqual(entry.crossDocument.after, initial)) fail('cross-document boundary is not atomic') +assertGeometryPreserved(entry.crossDocument.after, 'cross-document') +if (entry.readOnly?.exception !== null || entry.readOnly.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(entry.readOnly.after.value, [{ object: 'SecondBox', subElements: ['Face2'] }]) || !isDeepStrictEqual(entry.readOnly.after.propertyStatus, ['ReadOnly'])) fail('ReadOnly Python boundary changed') +if (entry.immutable?.exception?.type !== 'AttributeError' || entry.immutable.exception.message !== "Object attribute 'Support' is read-only" || entry.immutable.pythonBypassesImmutable !== false || !isDeepStrictEqual(entry.immutable.after.value, initial.value) || !isDeepStrictEqual(entry.immutable.after.propertyStatus, ['Immutable'])) fail('Immutable Python boundary changed') +const transaction = entry.transaction +if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-linksublistglobal-cancel' || transaction.activeAfterAbort?.id !== 0 || !isDeepStrictEqual(transaction.before, transaction.afterAbort) || transaction.restored !== true || transaction.recomputeAfterAbort !== true) fail('transaction cancel and recovery changed') +if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is incomplete') +if (entry.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(entry.documentIntegrity.initialObjects, entry.documentIntegrity.finalObjects)) fail('document object set was polluted') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-failure-pass', inputs: entry.inputs.map(({ id, exception, after }) => ({ id, exception, normalizedValue: after.value })), crossDocument: entry.crossDocument.exception, readOnlyPythonAccepted: entry.readOnly.pythonBypassesEditorReadOnly, immutablePythonAccepted: entry.immutable.pythonBypassesImmutable, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-inventory.mjs b/scripts/check-freecad-property-linksublistglobal-inventory.mjs new file mode 100644 index 0000000..d11a095 --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-inventory.mjs @@ -0,0 +1,45 @@ +import { createHash } from 'node:crypto' +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const runtimePath = '.cache/freecad/reference-desktop.json' +const oraclePath = 'config/freecad-partdesign-structure-oracle.json' +const [runtimeContent, oracleContent, reportContent] = await Promise.all([ + readFile(resolve(root, runtimePath)), + readFile(resolve(root, oraclePath)), + readFile(resolve(root, 'config/freecad-property-linksublistglobal-inventory.json')), +]) +const runtime = JSON.parse(runtimeContent) +const oracle = JSON.parse(oracleContent) +const report = JSON.parse(reportContent) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal inventory check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.classification !== 'opaque-fcstd-proxy' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('report boundary is invalid') +if (report.recordCount !== 1 || report.objectTypeCount !== 1 || report.writableRecordCount !== 1 || report.records?.length !== 1) fail('runtime record or host count changed') +const contract = report.typeContract +if (contract?.inheritedFrom !== 'App::PropertyLinkSubList' || contract.linkScope !== 'Global' || !isDeepStrictEqual(contract.propertyStatus, []) || contract.xmlElement !== 'LinkSubList' || contract.xmlChildElement !== 'Link' || contract.emptyListMeans !== 'no-references' || contract.crossDocumentBoundary !== 'reject-without-LinkAllowExternal') fail('type contract is incomplete') +if (report.provenance?.runtime?.path !== runtimePath || report.provenance.runtime.bytes !== runtimeContent.length || report.provenance.runtime.sha256 !== createHash('sha256').update(runtimeContent).digest('hex')) fail('runtime provenance is stale') +if (report.provenance?.oracle?.path !== oraclePath || report.provenance.oracle.bytes !== oracleContent.length || report.provenance.oracle.sha256 !== createHash('sha256').update(oracleContent).digest('hex')) fail('structure oracle provenance is stale') +for (const locked of report.provenance?.sources ?? []) { const content = await readFile(resolve(root, locked.path)); if (content.length !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`source provenance is stale for ${locked.path}`) } +const nativeMatches = [] +for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) if (property.typeId === 'App::PropertyLinkSubListGlobal') nativeMatches.push({ objectTypeId: objectType.typeId, objectAvailable: objectType.available === true, probeStatus: objectType.probeStatus, propertyName: property.name, group: property.group, statusRaw: property.status, defaultRaw: property.default }) +const identity = ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, statusRaw, defaultRaw }) => ({ objectTypeId, objectAvailable, probeStatus, propertyName, group, statusRaw, defaultRaw }) +const sorted = (records) => records.map(identity).sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`)) +if (!isDeepStrictEqual(sorted(nativeMatches), sorted(report.records))) fail('report diverges from the locked runtime oracle') +const record = report.records[0] +if (record.objectTypeId !== 'PartDesign::ShapeBinder' || record.objectAvailable !== true || record.probeStatus !== 'available' || record.propertyName !== 'Support' || record.group !== '' || !isDeepStrictEqual(record.statusRaw, []) || !isDeepStrictEqual(record.statusNames, []) || !isDeepStrictEqual(record.defaultRaw, [])) fail('native ShapeBinder Support metadata changed') +const model = record.valueModel; const inputs = record.inputs; const dependencies = record.dependencies +if (model?.kind !== 'same-document-object-and-ordered-sub-element-pair-list' || model.inheritedFrom !== 'App::PropertyLinkSubList' || model.orderPreserved !== true || model.duplicatesPreserved !== true || model.writable !== true) fail('value model changed') +if (inputs?.nativeCppSetValues !== 'parallel DocumentObject and sub-element lists' || inputs.emptyListMeansNoReferences !== true || inputs.nullObjectClears !== true || inputs.detachedObjectRejected !== true || inputs.crossDocumentRejectedWithoutLinkAllowExternal !== true || inputs.invalidSubElementAcceptedAsReference !== true) fail('input preconditions changed') +if (dependencies?.linkScope !== 'Global' || dependencies.createsBacklinks !== true || dependencies.includedByDefaultGetLinks !== true || dependencies.includedByGetLinksAll !== true || dependencies.defaultDagDependency !== true) fail('global dependency scope changed') +if (record.applicability?.requiredObjectTypeId !== 'PartDesign::ShapeBinder' || record.applicability.role !== 'ShapeBinder source geometry support' || record.applicability.propertyWriteRequiresShape !== false) fail('applicability changed') +const evidence = report.nativeEvidence +if (evidence?.structureOracle?.path !== oraclePath || evidence.structureOracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || evidence.structureOracle.crossDocumentShapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || evidence.structureOracle.crossDocumentAccepted !== false || evidence.structureOracle.crossDocumentError !== 'PropertyLinkSubList does not support external object' || !isDeepStrictEqual(evidence.structureOracle.crossDocumentShapeBinderSupport, [])) fail('native ShapeBinder cross-document boundary evidence changed') +const linksHeader = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyLinks.h'), 'utf8') +const linksSource = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/App/PropertyLinks.cpp'), 'utf8') +const binderHeader = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.h'), 'utf8') +const binderSource = await readFile(resolve(root, '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.cpp'), 'utf8') +if (!linksHeader.includes('class AppExport PropertyLinkSubListGlobal: public PropertyLinkSubList') || !linksHeader.includes('_pcScope = LinkScope::Global;') || !linksSource.includes('TYPESYSTEM_SOURCE(App::PropertyLinkSubListGlobal, App::PropertyLinkSubList)') || !linksSource.includes('PropertyLinkSubList does not support external object') || !binderHeader.includes('App::PropertyLinkSubListGlobal Support') || !/ADD_PROPERTY_TYPE\(\s*Support\s*,/s.test(binderSource)) fail('locked type, scope, host, or cross-document source contract changed') +if (oracle.status !== 'pass' || oracle.crossDocument?.initial?.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || oracle.crossDocument.initial.shapeBinderExternalLink.accepted !== false) fail('partdesign structure oracle boundary changed') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-inventory-pass', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId), propertyStatus: contract.propertyStatus, linkScope: contract.linkScope, crossDocument: evidence.structureOracle.crossDocumentError, classification: report.classification }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-mutation.mjs b/scripts/check-freecad-property-linksublistglobal-mutation.mjs new file mode 100644 index 0000000..3799833 --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-mutation.mjs @@ -0,0 +1,23 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-mutation.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal mutation check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-mutation' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1) fail('baseline is invalid') +const entry = report.case +const baselineValue = [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }] +const editedValue = [{ object: 'SecondBox', subElements: [] }] +if (entry?.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support' || entry.editRecomputeResult !== true || entry.restoreRecomputeResult !== true || entry.valueRestored !== true || entry.semanticStateRestored !== true) fail('mutation identity or restoration changed') +if (!isDeepStrictEqual(entry.before?.value, baselineValue) || !isDeepStrictEqual(entry.touchedAfterEdit?.value, editedValue) || !isDeepStrictEqual(entry.edited?.value, editedValue) || !isDeepStrictEqual(entry.touchedAfterRestore?.value, baselineValue) || !isDeepStrictEqual(entry.restored?.value, baselineValue)) fail('mutation values changed') +if (!isDeepStrictEqual(entry.before.ownerState, ['Up-to-date']) || entry.before.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterEdit.ownerState, ['Touched']) || entry.touchedAfterEdit.statusString !== 'Touched' || !isDeepStrictEqual(entry.edited.ownerState, ['Up-to-date']) || entry.edited.statusString !== 'Valid' || !isDeepStrictEqual(entry.touchedAfterRestore.ownerState, ['Touched']) || entry.touchedAfterRestore.statusString !== 'Touched' || !isDeepStrictEqual(entry.restored.ownerState, ['Up-to-date']) || entry.restored.statusString !== 'Valid') fail('state transitions changed') +const objectSet = [{ name: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder' }, { name: 'SourceBox', typeId: 'Part::Feature' }, { name: 'SecondBox', typeId: 'Part::Feature' }] +for (const [phase, snapshot] of Object.entries({ before: entry.before, touchedAfterEdit: entry.touchedAfterEdit, edited: entry.edited, touchedAfterRestore: entry.touchedAfterRestore, restored: entry.restored })) { + if (snapshot.propertyTypeId !== report.propertyType || snapshot.group !== '' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectSet, objectSet) || !isDeepStrictEqual(snapshot.targetShapes, entry.before.targetShapes)) fail(`${phase} metadata, object set, or source Shapes changed`) +} +if (entry.before.shape?.isNull !== false || entry.before.shape.solids !== 0 || entry.before.shape.faces !== 2 || entry.before.shape.edges !== 8 || !isDeepStrictEqual(entry.before.ownerOutList, ['SourceBox', 'SourceBox', 'SecondBox']) || !isDeepStrictEqual(entry.before.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: ['ShapeBinderProbe'] })) fail('baseline Shape or backlinks changed') +if (entry.touchedAfterEdit.shape?.brepSha256 !== entry.before.shape.brepSha256 || !isDeepStrictEqual(entry.touchedAfterEdit.ownerOutList, ['SecondBox']) || !isDeepStrictEqual(entry.touchedAfterEdit.targetInLists, { SourceBox: [], SecondBox: ['ShapeBinderProbe'] })) fail('touched edit state changed') +if (entry.edited.shape?.isNull !== false || entry.edited.shape.solids !== 1 || entry.edited.shape.faces !== 6 || entry.edited.shape.edges !== 12 || entry.edited.shape.brepSha256 !== entry.before.targetShapes.SecondBox.brepSha256) fail('edited whole-object Shape changed') +if (!isDeepStrictEqual(entry.restored, entry.before)) fail('restored semantic state differs from baseline') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-mutation-pass', before: entry.before.value, edited: entry.edited.value, restored: entry.restored.value, baselineShape: entry.before.shape, editedShape: entry.edited.shape, semanticStateRestored: entry.semanticStateRestored }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-promotion.mjs b/scripts/check-freecad-property-linksublistglobal-promotion.mjs new file mode 100644 index 0000000..7b132e0 --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-promotion.mjs @@ -0,0 +1,26 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse) +const [report, semantics, progress, roundTrip, chrome] = await Promise.all([ + load('config/freecad-property-linksublistglobal-promotion.json'), + load('config/freecad-native-property-semantics.json'), + load('config/freecad-follow-up-task-progress.json'), + load('config/freecad-property-linksublistglobal-roundtrip.json'), + load('config/chrome-property-linksublistglobal-verification.json'), +]) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal promotion check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.taskId !== 'PROP-app-propertylinksublistglobal-I' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.recordCount !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.systemExact !== false) fail('promotion report boundary is invalid') +const requiredPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] +const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? []) +for (const phase of requiredPhases) { const taskId = `PROP-app-propertylinksublistglobal-${phase}`; if (!completed.has(taskId) || !Array.isArray(report.phaseEvidence?.[phase]) || report.phaseEvidence[phase].length === 0 || !isDeepStrictEqual(report.phaseEvidence[phase], completed.get(taskId).evidence)) fail(`phase ${phase} evidence is incomplete`) } +const propertyType = semantics.types?.find(({ typeId }) => typeId === report.propertyType) +const promotion = report.promotion +if (promotion?.from !== 'opaque-fcstd-proxy' || promotion.to !== 'native-editable-codec' || propertyType?.support !== promotion.to || propertyType.recordCount !== 1 || propertyType.objectTypeCount !== 1 || propertyType.statusNames?.length !== 0 || promotion.facadeValueModel !== 'same-document-object-and-ordered-sub-element-pair-list' || promotion.writableRecords !== 1 || promotion.objectTypeCount !== 1 || promotion.propertyStatus !== '' || promotion.linkScope !== 'Global' || promotion.runtimeDependencyEdges !== 2 || promotion.fcstdElement !== 'LinkSubList' || !isDeepStrictEqual(promotion.nativeRoundTripValue, roundTrip.classification.resavedValue) || !isDeepStrictEqual(promotion.browserRoundTripValue, chrome.persistence.loadedValue) || promotion.globalBacklinksPreserved !== true || promotion.nativeDependenciesNormalized !== true || promotion.zeroUnknownDrift !== true) fail('capability promotion is incomplete') +const sync = report.exactBlockerSync +if (sync?.nativeEditableTypes !== 37 || sync.nativeEditableRecords !== 4377 || sync.opaqueTypes !== 43 || sync.opaqueRecords !== 450 || sync.exactPromotionReady !== false || sync.exactBlocker !== '43 runtime property types and 450 records remain opaque-only; complete native document and property semantics') fail('exact blocker synchronization is stale') +if (!isDeepStrictEqual(sync, { nativeEditableTypes: semantics.supportSummary['native-editable-codec'].typeCount, nativeEditableRecords: semantics.supportSummary['native-editable-codec'].recordCount, opaqueTypes: semantics.supportSummary['opaque-fcstd-proxy'].typeCount, opaqueRecords: semantics.supportSummary['opaque-fcstd-proxy'].recordCount, exactPromotionReady: semantics.exactPromotionReady, exactBlocker: semantics.exactBlocker })) fail('promotion report diverges from global property semantics') +if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.globalBacklinksPreserved !== true || roundTrip.classification.nativeDependenciesNormalized !== true || chrome.status !== 'pass' || !isDeepStrictEqual(chrome.persistence?.loadedValue, promotion.browserRoundTripValue) || chrome.persistence?.fcstdElement !== 'LinkSubList' || chrome.persistence?.loadedDependencyCount !== 2 || chrome.ui?.dependencyCount !== 2 || chrome.resource?.released !== true || chrome.release?.workerTerminated !== true) fail('G/H closure evidence regressed') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-promotion-pass', propertyType: report.propertyType, promotion, completedPhases: requiredPhases, exactBlockerSync: sync, systemExact: report.systemExact }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-roundtrip.mjs b/scripts/check-freecad-property-linksublistglobal-roundtrip.mjs new file mode 100644 index 0000000..7c9a05f --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-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-linksublistglobal-roundtrip.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal roundtrip check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-roundtrip' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid') +const initial = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] } +const target = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] } +if (report.native?.inspectedType !== 'App::PropertyLinkSubListGlobal' || report.native.inspectedElement !== 'LinkSubList' || report.native.decoded !== true || !isDeepStrictEqual(report.native.decodedValue, initial)) fail('native archive did not decode the Global property') +if (report.web?.beforeDecoded !== true || report.web.afterDecoded !== true || report.web.inspectedType !== 'App::PropertyLinkSubListGlobal' || report.web.inspectedElement !== 'LinkSubList' || report.web.archiveBytes <= 0 || !isDeepStrictEqual(report.web.beforeValue, initial) || !isDeepStrictEqual(report.web.afterValue, target)) fail('Web FCStd projection or rewrite changed semantics') +const reopened = report.native.reopened +const resaved = report.native.resaved +for (const [label, snapshot, expected] of [['reopened', reopened, target], ['resaved', resaved, target]]) { + if (snapshot?.object?.typeId !== 'PartDesign::ShapeBinder' || snapshot.object.propertyTypeId !== 'App::PropertyLinkSubListGlobal' || snapshot.object.group !== '' || !isDeepStrictEqual(snapshot.object.value, expected.entries.map((entry) => ({ object: entry.objectId, subElements: entry.subElement === null ? [] : [entry.subElement] }))) || snapshot.object.statusString !== 'Valid' || !isDeepStrictEqual(snapshot.object.state, ['Up-to-date']) || !isDeepStrictEqual(snapshot.object.outList, ['SecondBox', 'SourceBox']) || snapshot.object.shape?.isNull !== false || snapshot.object.shape.solids !== 0 || snapshot.object.shape.faces !== 1 || snapshot.object.shape.edges !== 4 || snapshot.object.shape.vertices !== 4 || !isDeepStrictEqual(snapshot.targets?.SourceBox?.inList, ['ShapeBinderProbe']) || !isDeepStrictEqual(snapshot.targets?.SecondBox?.inList, ['ShapeBinderProbe'])) fail(`${label} native semantics changed`) +} +if (report.classification?.zeroUnknownDrift !== true || report.classification.globalBacklinksPreserved !== true || report.classification.nativeDependenciesNormalized !== true || !isDeepStrictEqual(report.classification.requestedValue, target)) fail('roundtrip classification is incomplete') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-roundtrip-pass', native: report.native, web: report.web, classification: report.classification }, null, 2)) diff --git a/scripts/check-freecad-property-linksublistglobal-success.mjs b/scripts/check-freecad-property-linksublistglobal-success.mjs new file mode 100644 index 0000000..63c865a --- /dev/null +++ b/scripts/check-freecad-property-linksublistglobal-success.mjs @@ -0,0 +1,33 @@ +import { isDeepStrictEqual } from 'node:util' +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linksublistglobal-success.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal success check failed: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksublistglobal-success' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubListGlobal' || report.caseCount !== 1 || report.bindingBoundary?.oracleDefault?.length !== 0 || report.bindingBoundary.safeFirstRead !== 'assign-empty-list-before-read' || report.bindingBoundary.emptyListMeans !== 'no-references' || report.bindingBoundary.shapeBinderUsesFirstPartFeature !== true || report.bindingBoundary.multipleObjectReferencesPreserved !== true) fail('baseline or binding boundary is invalid') +const entry = report.cases?.[0] +if (!entry || entry.objectTypeId !== 'PartDesign::ShapeBinder' || entry.objectName !== 'ShapeBinderProbe' || entry.propertyName !== 'Support' || entry.group !== '' || entry.mode !== 'direct-native-property-setter') fail('case identity changed') +const expectedValues = { + empty: [], + wholeObject: [{ object: 'SourceBox', subElements: [] }], + sameObjectSubElements: [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }], + multipleObjects: [{ object: 'SourceBox', subElements: ['Face1', 'Face2'] }, { object: 'SecondBox', subElements: ['Face1'] }], +} +const expectedObjectSet = [{ name: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder' }, { name: 'SourceBox', typeId: 'Part::Feature' }, { name: 'SecondBox', typeId: 'Part::Feature' }] +const targetShapes = entry.phases?.wholeObject?.targetShapes +if (targetShapes?.SourceBox?.isNull !== false || targetShapes.SourceBox.solids !== 1 || targetShapes.SourceBox.faces !== 6 || targetShapes.SourceBox.edges !== 12 || targetShapes.SourceBox.vertices !== 8 || targetShapes?.SecondBox?.isNull !== false || targetShapes.SecondBox.solids !== 1 || targetShapes.SecondBox.faces !== 6 || targetShapes.SecondBox.edges !== 12 || targetShapes.SecondBox.vertices !== 8) fail('source topology evidence is invalid') +for (const [phase, expectedValue] of Object.entries(expectedValues)) { + const snapshot = entry.phases?.[phase] + if (!snapshot || !isDeepStrictEqual(snapshot.value, expectedValue) || snapshot.propertyTypeId !== 'App::PropertyLinkSubListGlobal' || !isDeepStrictEqual(snapshot.propertyStatus, []) || !isDeepStrictEqual(snapshot.editorMode, []) || !isDeepStrictEqual(snapshot.objectSet, expectedObjectSet) || snapshot.statusString !== 'Valid' || snapshot.recomputeResult !== true) fail(`${phase} property or document semantics changed`) + if (!isDeepStrictEqual(snapshot.targetShapes, targetShapes)) fail(`${phase} changed source topology`) +} +const empty = entry.phases.empty +if (empty.shape?.isNull !== true || !isDeepStrictEqual(empty.ownerOutList, []) || !isDeepStrictEqual(empty.targetInLists, { SourceBox: [], SecondBox: [] }) || !isDeepStrictEqual(empty.ownerState, ['Up-to-date'])) fail('empty support boundary changed') +const whole = entry.phases.wholeObject +if (whole.shape?.isNull !== false || whole.shape.solids !== 1 || whole.shape.faces !== 6 || whole.shape.edges !== 12 || whole.shape.vertices !== 8 || !isDeepStrictEqual(whole.ownerOutList, ['SourceBox']) || !isDeepStrictEqual(whole.targetInLists, { SourceBox: ['ShapeBinderProbe'], SecondBox: [] })) fail('whole-object support semantics changed') +const sub = entry.phases.sameObjectSubElements + if (sub.shape?.isNull !== false || sub.shape.solids !== 0 || sub.shape.faces !== 2 || sub.shape.edges !== 8 || sub.shape.vertices !== 8 || !isDeepStrictEqual(sub.ownerOutList, ['SourceBox', 'SourceBox']) || !isDeepStrictEqual(sub.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: [] })) fail('sub-element support semantics changed') +const multiple = entry.phases.multipleObjects + if (multiple.shape?.isNull !== false || multiple.shape.solids !== 0 || multiple.shape.faces !== 2 || multiple.shape.edges !== 8 || multiple.shape.vertices !== 8 || !isDeepStrictEqual(multiple.ownerOutList, ['SourceBox', 'SourceBox', 'SecondBox']) || !isDeepStrictEqual(multiple.targetInLists, { SourceBox: ['ShapeBinderProbe', 'ShapeBinderProbe'], SecondBox: ['ShapeBinderProbe'] })) fail('multiple-object support/backlink semantics changed') +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-success-pass', propertyType: report.propertyType, cases: report.caseCount, phases: Object.keys(expectedValues), wholeObjectShape: { solids: whole.shape.solids, faces: whole.shape.faces }, subElementShape: { solids: sub.shape.solids, faces: sub.shape.faces }, globalBacklinks: multiple.ownerOutList }, null, 2)) diff --git a/scripts/freecad-property-linksubhidden-mutation.py b/scripts/freecad-property-linksubhidden-mutation.py new file mode 100644 index 0000000..af41d12 --- /dev/null +++ b/scripts/freecad-property-linksubhidden-mutation.py @@ -0,0 +1,120 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" +OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT", "") + + +def shape_snapshot(obj): + if "Shape" not in obj.PropertiesList: + return {"applicable": False, "reason": "host-has-no-shape-property"} + shape = obj.Shape + if shape.isNull(): + return {"applicable": True, "isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "brepSha256": None} + return { + "applicable": True, + "isNull": False, + "solids": len(shape.Solids), + "faces": len(shape.Faces), + "edges": len(shape.Edges), + "vertices": len(shape.Vertexes), + "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(), + } + + +def colored_value(owner): + value = owner.ColoredElements + if value is None: + return None + target, subs = value + return {"object": target.Name if target is not None else None, "subElements": list(subs)} + + +def snapshot(document, owner, targets): + return { + "value": colored_value(owner), + "propertyTypeId": owner.getTypeIdOfProperty("ColoredElements"), + "group": owner.getGroupOfProperty("ColoredElements"), + "propertyStatus": [str(item) for item in owner.getPropertyStatus("ColoredElements")], + "editorMode": [str(item) for item in owner.getEditorMode("ColoredElements")], + "ownerState": [str(item) for item in owner.State], + "statusString": str(owner.getStatusString()), + "shape": shape_snapshot(owner), + "ownerOutList": [candidate.Name for candidate in owner.OutList], + "targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets}, + "targetShapes": {target.Name: shape_snapshot(target) for target in targets}, + "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], + } + + +def run_case(object_type_id, object_name): + document = App.newDocument("PropertyLinkSubHiddenMutation" + object_name) + try: + owner = document.addObject(object_type_id, object_name) + first = document.addObject("Part::Feature", "ColorSourceA") + first.Shape = Part.makeBox(2.0, 3.0, 4.0) + second = document.addObject("Part::Feature", "ColorSourceB") + second.Shape = Part.makeCylinder(1.5, 5.0) + targets = [first, second] + if "LinkedObject" in owner.PropertiesList: + owner.LinkedObject = first + + owner.ColoredElements = (first, ["Face1", "Face3"]) + document.recompute() + before = snapshot(document, owner, targets) + + owner.ColoredElements = (second, ["Face5"]) + touched_after_edit = snapshot(document, owner, targets) + edit_recompute_result = bool(document.recompute()) + edited = snapshot(document, owner, targets) + + owner.ColoredElements = (first, ["Face1", "Face3"]) + touched_after_restore = snapshot(document, owner, targets) + restore_recompute_result = bool(document.recompute()) + restored = snapshot(document, owner, targets) + + return { + "objectTypeId": object_type_id, + "objectName": object_name, + "propertyName": "ColoredElements", + "before": before, + "touchedAfterEdit": touched_after_edit, + "edited": edited, + "touchedAfterRestore": touched_after_restore, + "restored": restored, + "editRecomputeResult": edit_recompute_result, + "restoreRecomputeResult": restore_recompute_result, + "valueRestored": before["value"] == restored["value"], + "semanticStateRestored": before == restored, + } + finally: + App.closeDocument(document.Name) + + +cases = [ + run_case("App::Link", "LinkProbe"), + run_case("App::LinkGroup", "LinkGroupProbe"), + run_case("App::LinkGroupPython", "LinkGroupPythonProbe"), + run_case("App::LinkPython", "LinkPythonProbe"), +] +report = { + "schemaVersion": 1, + "status": "pass", + "baselineId": "freecad-1.1.1-property-linksubhidden-mutation", + "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), + "gitCommit": FREECAD_COMMIT, + "propertyType": "App::PropertyLinkSubHidden", + "caseCount": len(cases), + "cases": cases, +} +if not OUTPUT_PATH: + raise RuntimeError("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT is required") +with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: + json.dump(report, handle, indent=2, sort_keys=True) + handle.write("\n") +print("FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True)) diff --git a/scripts/freecad-property-linksubhidden-roundtrip.py b/scripts/freecad-property-linksubhidden-roundtrip.py new file mode 100644 index 0000000..79f2bf3 --- /dev/null +++ b/scripts/freecad-property-linksubhidden-roundtrip.py @@ -0,0 +1,103 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + + +def shape_snapshot(obj): + if "Shape" not in obj.PropertiesList: + return {"applicable": False, "reason": "host-has-no-shape-property"} + shape = obj.Shape + if shape.isNull(): + return {"applicable": True, "isNull": True, "valid": False, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "area": 0.0, "volume": 0.0, "brepSha256": None} + return {"applicable": True, "isNull": False, "valid": bool(shape.isValid()), "shapeType": shape.ShapeType, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "area": round(float(shape.Area), 9), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()} + + +def colored_value(owner): + value = owner.ColoredElements + if value is None: + return None + target, subs = value + return {"object": target.Name if target is not None else None, "subElements": list(subs)} + + +def object_snapshot(document): + owner = document.getObject("LinkGroupProbe") + targets = [document.getObject("ColorSourceA"), document.getObject("ColorSourceB")] + return { + "objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects], + "object": { + "name": owner.Name, + "typeId": owner.TypeId, + "propertyName": "ColoredElements", + "propertyTypeId": owner.getTypeIdOfProperty("ColoredElements"), + "group": owner.getGroupOfProperty("ColoredElements"), + "value": colored_value(owner), + "propertyStatus": [str(item) for item in owner.getPropertyStatus("ColoredElements")], + "editorMode": [str(item) for item in owner.getEditorMode("ColoredElements")], + "state": [str(item) for item in owner.State], + "statusString": str(owner.getStatusString()), + "shape": shape_snapshot(owner), + "outList": [item.Name for item in owner.OutList], + }, + "targets": { + target.Name: {"typeId": target.TypeId, "inList": [item.Name for item in target.InList], "shape": shape_snapshot(target)} + for target in targets + }, + } + + +def create_native(path): + document = App.newDocument("PropertyLinkSubHiddenRoundtrip") + try: + owner = document.addObject("App::LinkGroup", "LinkGroupProbe") + first = document.addObject("Part::Feature", "ColorSourceA") + first.Shape = Part.makeBox(2.0, 3.0, 4.0) + second = document.addObject("Part::Feature", "ColorSourceB") + second.Shape = Part.makeCylinder(1.5, 5.0) + owner.ColoredElements = (first, ["Face1", "Face3"]) + document.recompute() + document.saveAs(path) + finally: + App.closeDocument(document.Name) + reopened = App.openDocument(path) + try: + reopened.recompute() + return object_snapshot(reopened) + finally: + App.closeDocument(reopened.Name) + + +def verify_native(path, resaved_path): + document = App.openDocument(path) + try: + document.recompute() + reopened = object_snapshot(document) + document.saveAs(resaved_path) + finally: + App.closeDocument(document.Name) + resaved_document = App.openDocument(resaved_path) + try: + resaved_document.recompute() + resaved = object_snapshot(resaved_document) + finally: + App.closeDocument(resaved_document.Name) + return {"reopened": reopened, "resaved": resaved} + + +mode = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE", "") +path = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_PATH", "") +resaved_path = os.environ.get("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESAVED_PATH", "") +if mode == "create": + result = create_native(path) +elif mode == "verify": + result = verify_native(path, resaved_path) +else: + raise RuntimeError("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE must be create or verify") + +print("FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESULT=" + json.dumps({"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksubhidden-roundtrip", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "mode": mode, "result": result}, sort_keys=True)) diff --git a/scripts/freecad-property-linksublistglobal-failure.py b/scripts/freecad-property-linksublistglobal-failure.py new file mode 100644 index 0000000..9fdbb12 --- /dev/null +++ b/scripts/freecad-property-linksublistglobal-failure.py @@ -0,0 +1,146 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" +OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT", "") + + +def value_of(owner): + return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support] + + +def shape_hash(obj): + shape = obj.Shape + return None if shape.isNull() else hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest() + + +def snapshot(document, owner, targets, recompute=True): + if recompute: + document.recompute() + return { + "value": value_of(owner), + "propertyTypeId": owner.getTypeIdOfProperty("Support"), + "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], + "editorMode": [str(item) for item in owner.getEditorMode("Support")], + "ownerState": [str(item) for item in owner.State], + "statusString": str(owner.getStatusString()), + "ownerShapeHash": shape_hash(owner), + "ownerOutList": [candidate.Name for candidate in owner.OutList], + "targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets}, + "targetShapeHashes": {target.Name: shape_hash(target) for target in targets}, + "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], + } + + +def exception_snapshot(callback): + try: + callback() + except Exception as error: + return {"type": type(error).__name__, "message": str(error)} + return None + + +def active_transaction_snapshot(): + active = App.getActiveTransaction() + return {"name": str(active[0]), "id": int(active[1])} if active else {"name": "", "id": 0} + + +def run_case(): + document = App.newDocument("PropertyLinkSubListGlobalFailure") + external = App.newDocument("PropertyLinkSubListGlobalExternal") + try: + owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe") + first = document.addObject("Part::Feature", "SourceBox") + first.Shape = Part.makeBox(4.0, 5.0, 6.0) + second = document.addObject("Part::Feature", "SecondBox") + second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0)) + foreign = external.addObject("Part::Feature", "ForeignBox") + foreign.Shape = Part.makeBox(1.0, 1.0, 1.0) + targets = [first, second] + baseline_value = [(first, ("Face1", "Face2")), (second, "Face1")] + owner.Support = baseline_value + document.recompute() + document.UndoMode = 1 + initial = snapshot(document, owner, targets) + + inputs = [] + candidates = [ + ("scalar", "SourceBox"), + ("singlePairShorthand", (first, "Face1")), + ("stringObject", [("SourceBox", "Face1")]), + ("integerObject", [(7, "Face1")]), + ] + for name, candidate in candidates: + owner.Support = baseline_value + document.recompute() + before = snapshot(document, owner, targets) + exception = exception_snapshot(lambda value=candidate: setattr(owner, "Support", value)) + after = snapshot(document, owner, targets) + inputs.append({"id": name, "requested": repr(candidate), "exception": exception, "before": before, "after": after, "valuePreserved": before["value"] == after["value"], "documentPreserved": before == after}) + + owner.Support = baseline_value + document.recompute() + cross_before = snapshot(document, owner, targets) + cross_exception = exception_snapshot(lambda: setattr(owner, "Support", [(foreign, "Face1")])) + cross_after = snapshot(document, owner, targets) + cross_document = {"exception": cross_exception, "before": cross_before, "after": cross_after, "valuePreserved": cross_before["value"] == cross_after["value"], "documentPreserved": cross_before == cross_after} + + owner.setPropertyStatus("Support", "ReadOnly") + read_only_before = snapshot(document, owner, targets) + read_only_exception = exception_snapshot(lambda: setattr(owner, "Support", [(second, "Face2")])) + read_only_after = snapshot(document, owner, targets) + read_only = {"exception": read_only_exception, "before": read_only_before, "after": read_only_after, "pythonBypassesEditorReadOnly": read_only_exception is None} + owner.setPropertyStatus("Support", "-ReadOnly") + owner.Support = baseline_value + document.recompute() + + owner.setPropertyStatus("Support", "Immutable") + immutable_before = snapshot(document, owner, targets) + immutable_exception = exception_snapshot(lambda: setattr(owner, "Support", [(first, "Face3")])) + immutable_after = snapshot(document, owner, targets) + immutable = {"exception": immutable_exception, "before": immutable_before, "after": immutable_after, "pythonBypassesImmutable": immutable_exception is None} + owner.setPropertyStatus("Support", "-Immutable") + owner.Support = baseline_value + document.recompute() + + document.openTransaction("property-linksublistglobal-cancel") + transaction_before = snapshot(document, owner, targets) + owner.Support = [(second, "Face1")] + transaction_edited = snapshot(document, owner, targets, recompute=False) + pending_after_edit = bool(document.HasPendingTransaction) + active_after_edit = active_transaction_snapshot() + document.abortTransaction() + transaction_after_abort_before_recompute = snapshot(document, owner, targets, recompute=False) + recompute_after_abort = bool(document.recompute()) + transaction_after = snapshot(document, owner, targets) + + return { + "objectTypeId": "PartDesign::ShapeBinder", + "objectName": "ShapeBinderProbe", + "propertyName": "Support", + "initial": initial, + "inputs": inputs, + "crossDocument": cross_document, + "readOnly": read_only, + "immutable": immutable, + "transaction": {"before": transaction_before, "edited": transaction_edited, "afterAbortBeforeRecompute": transaction_after_abort_before_recompute, "afterAbort": transaction_after, "recomputeAfterAbort": recompute_after_abort, "undoMode": int(document.UndoMode), "pendingAfterEdit": pending_after_edit, "pendingAfterAbort": bool(document.HasPendingTransaction), "activeAfterEdit": active_after_edit, "activeAfterAbort": active_transaction_snapshot(), "restored": transaction_before == transaction_after}, + "documentIntegrity": {"initialObjects": initial["objectSet"], "finalObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "objectsPreserved": initial["objectSet"] == [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], "foreignDocumentObjects": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in external.Objects]}, + } + finally: + App.closeDocument(external.Name) + App.closeDocument(document.Name) + + +case = run_case() +report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-failure", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "case": case, "cancellationBoundary": {"supported": False, "classification": "synchronous-property-setter", "reason": "no-native-cancel-hook", "replacement": "abort-active-document-transaction"}} +if not OUTPUT_PATH: + raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT is required") +with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: + json.dump(report, handle, indent=2, sort_keys=True) + handle.write("\n") +print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True)) diff --git a/scripts/freecad-property-linksublistglobal-mutation.py b/scripts/freecad-property-linksublistglobal-mutation.py new file mode 100644 index 0000000..06f5c0b --- /dev/null +++ b/scripts/freecad-property-linksublistglobal-mutation.py @@ -0,0 +1,74 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" +OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT", "") + + +def value_of(owner): + return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support] + + +def shape_snapshot(obj): + shape = obj.Shape + if shape.isNull(): + return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "brepSha256": None} + return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()} + + +def snapshot(document, owner, targets): + return { + "value": value_of(owner), + "propertyTypeId": owner.getTypeIdOfProperty("Support"), + "group": owner.getGroupOfProperty("Support"), + "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], + "editorMode": [str(item) for item in owner.getEditorMode("Support")], + "ownerState": [str(item) for item in owner.State], + "statusString": str(owner.getStatusString()), + "shape": shape_snapshot(owner), + "ownerOutList": [candidate.Name for candidate in owner.OutList], + "targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets}, + "targetShapes": {target.Name: shape_snapshot(target) for target in targets}, + "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], + } + + +document = App.newDocument("PropertyLinkSubListGlobalMutation") +try: + owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe") + first = document.addObject("Part::Feature", "SourceBox") + first.Shape = Part.makeBox(4.0, 5.0, 6.0) + second = document.addObject("Part::Feature", "SecondBox") + second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0)) + targets = [first, second] + baseline_value = [(first, ("Face1", "Face2")), (second, "Face1")] + owner.Support = baseline_value + document.recompute() + before = snapshot(document, owner, targets) + + owner.Support = [(second, "")] + touched_after_edit = snapshot(document, owner, targets) + edit_recompute_result = bool(document.recompute()) + edited = snapshot(document, owner, targets) + + owner.Support = baseline_value + touched_after_restore = snapshot(document, owner, targets) + restore_recompute_result = bool(document.recompute()) + restored = snapshot(document, owner, targets) + + case = {"objectTypeId": owner.TypeId, "objectName": owner.Name, "propertyName": "Support", "before": before, "touchedAfterEdit": touched_after_edit, "edited": edited, "touchedAfterRestore": touched_after_restore, "restored": restored, "editRecomputeResult": edit_recompute_result, "restoreRecomputeResult": restore_recompute_result, "valueRestored": before["value"] == restored["value"], "semanticStateRestored": before == restored} +finally: + App.closeDocument(document.Name) + +report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "case": case} +if not OUTPUT_PATH: + raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT is required") +with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: + json.dump(report, handle, indent=2, sort_keys=True) + handle.write("\n") +print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"], "restored": case["semanticStateRestored"]}, sort_keys=True)) diff --git a/scripts/freecad-property-linksublistglobal-roundtrip.py b/scripts/freecad-property-linksublistglobal-roundtrip.py new file mode 100644 index 0000000..b76de6f --- /dev/null +++ b/scripts/freecad-property-linksublistglobal-roundtrip.py @@ -0,0 +1,65 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + + +def value_of(owner): + return [{"object": obj.Name if obj else None, "subElements": [str(item) for item in (sub_elements if not isinstance(sub_elements, str) else [sub_elements]) if str(item)]} for obj, sub_elements in owner.Support] + + +def shape_snapshot(obj): + shape = obj.Shape + if shape.isNull(): return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "brepSha256": None} + return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest()} + + +def snapshot(document): + owner = document.getObject("ShapeBinderProbe") + targets = [document.getObject("SourceBox"), document.getObject("SecondBox")] + document.recompute() + return {"object": {"name": owner.Name, "typeId": owner.TypeId, "propertyName": "Support", "propertyTypeId": owner.getTypeIdOfProperty("Support"), "group": owner.getGroupOfProperty("Support"), "value": value_of(owner), "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], "editorMode": [str(item) for item in owner.getEditorMode("Support")], "state": [str(item) for item in owner.State], "statusString": str(owner.getStatusString()), "shape": shape_snapshot(owner), "outList": [item.Name for item in owner.OutList]}, "targets": {target.Name: {"typeId": target.TypeId, "inList": [item.Name for item in target.InList], "shape": shape_snapshot(target)} for target in targets}, "objectSet": [{"name": item.Name, "typeId": item.TypeId} for item in document.Objects]} + + +def create_native(path): + document = App.newDocument("PropertyLinkSubListGlobalRoundtrip") + try: + owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe") + first = document.addObject("Part::Feature", "SourceBox") + first.Shape = Part.makeBox(4.0, 5.0, 6.0) + second = document.addObject("Part::Feature", "SecondBox") + second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0)) + owner.Support = [(first, ("Face1", "Face2")), (second, "Face1")] + document.recompute() + document.saveAs(path) + return snapshot(document) + finally: + App.closeDocument(document.Name) + + +def verify_native(path, resaved_path): + document = App.openDocument(path) + try: + reopened = snapshot(document) + document.saveAs(resaved_path) + finally: + App.closeDocument(document.Name) + document = App.openDocument(resaved_path) + try: + resaved = snapshot(document) + finally: + App.closeDocument(document.Name) + return {"reopened": reopened, "resaved": resaved} + + +mode = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE", "") +path = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_PATH", "") +resaved_path = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESAVED_PATH", "") +result = create_native(path) if mode == "create" else verify_native(path, resaved_path) if mode == "verify" else None +if result is None: raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE must be create or verify") +print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT=" + json.dumps({"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-roundtrip", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "mode": mode, "result": result}, sort_keys=True)) diff --git a/scripts/freecad-property-linksublistglobal-success.py b/scripts/freecad-property-linksublistglobal-success.py new file mode 100644 index 0000000..a4f64c8 --- /dev/null +++ b/scripts/freecad-property-linksublistglobal-success.py @@ -0,0 +1,73 @@ +import hashlib +import json +import os + +import FreeCAD as App +import Part + +FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" +OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT", "") + + +def link_values(value): + result = [] + for obj, sub_elements in value: + subs = [sub_elements] if isinstance(sub_elements, str) and sub_elements else list(sub_elements) if not isinstance(sub_elements, str) else [] + result.append({"object": obj.Name if obj else None, "subElements": [str(item) for item in subs if str(item)]}) + return result + + +def shape_snapshot(obj): + shape = obj.Shape + if shape.isNull(): + return {"isNull": True, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "brepSha256": None} + brep = shape.exportBrepToString() + return {"isNull": False, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "volume": round(float(shape.Volume), 9), "brepSha256": hashlib.sha256(brep.encode("utf-8")).hexdigest()} + + +def snapshot(document, owner, targets): + recompute_result = bool(document.recompute()) + return { + "value": link_values(owner.Support), + "propertyTypeId": owner.getTypeIdOfProperty("Support"), + "group": owner.getGroupOfProperty("Support"), + "propertyStatus": [str(item) for item in owner.getPropertyStatus("Support")], + "editorMode": [str(item) for item in owner.getEditorMode("Support")], + "ownerState": [str(item) for item in owner.State], + "statusString": str(owner.getStatusString()), + "recomputeResult": recompute_result, + "shape": shape_snapshot(owner), + "ownerOutList": [candidate.Name for candidate in owner.OutList], + "targetInLists": {target.Name: [candidate.Name for candidate in target.InList] for target in targets}, + "targetShapes": {target.Name: shape_snapshot(target) for target in targets}, + "objectSet": [{"name": candidate.Name, "typeId": candidate.TypeId} for candidate in document.Objects], + } + + +document = App.newDocument("PropertyLinkSubListGlobalSuccess") +try: + owner = document.addObject("PartDesign::ShapeBinder", "ShapeBinderProbe") + first = document.addObject("Part::Feature", "SourceBox") + first.Shape = Part.makeBox(4.0, 5.0, 6.0) + second = document.addObject("Part::Feature", "SecondBox") + second.Shape = Part.makeBox(2.0, 3.0, 4.0, App.Vector(10.0, 0.0, 0.0)) + targets = [first, second] + phases = {} + owner.Support = [] + phases["empty"] = snapshot(document, owner, targets) + owner.Support = [(first, "")] + phases["wholeObject"] = snapshot(document, owner, targets) + owner.Support = [(first, ("Face1", "Face2"))] + phases["sameObjectSubElements"] = snapshot(document, owner, targets) + owner.Support = [(first, ("Face1", "Face2")), (second, "Face1")] + phases["multipleObjects"] = snapshot(document, owner, targets) + report = {"schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-linksublistglobal-success", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyLinkSubListGlobal", "caseCount": 1, "bindingBoundary": {"oracleDefault": [], "safeFirstRead": "assign-empty-list-before-read", "emptyListMeans": "no-references", "shapeBinderUsesFirstPartFeature": True, "multipleObjectReferencesPreserved": True}, "cases": [{"objectTypeId": "PartDesign::ShapeBinder", "objectName": "ShapeBinderProbe", "propertyName": "Support", "group": "", "mode": "direct-native-property-setter", "phases": phases}]} +finally: + App.closeDocument(document.Name) + +if not OUTPUT_PATH: + raise RuntimeError("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT is required") +with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: + json.dump(report, handle, indent=2, sort_keys=True) + handle.write("\n") +print("FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True)) diff --git a/scripts/generate-freecad-property-linksubhidden-promotion.mjs b/scripts/generate-freecad-property-linksubhidden-promotion.mjs new file mode 100644 index 0000000..4782baa --- /dev/null +++ b/scripts/generate-freecad-property-linksubhidden-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-linksubhidden-promotion.json') +const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([ + load('config/freecad-property-linksubhidden-inventory.json'), + load('config/freecad-property-linksubhidden-success.json'), + load('config/freecad-property-linksubhidden-failure.json'), + load('config/freecad-property-linksubhidden-mutation.json'), + load('config/freecad-property-linksubhidden-roundtrip.json'), + load('config/chrome-property-linksubhidden-verification.json'), + load('config/freecad-native-property-semantics.json'), + load('config/freecad-follow-up-task-progress.json'), +]) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden promotion generation failed: ${message}`) } +const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] +const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? []) +for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertylinksubhidden-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`) +if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyLinkSubHidden' || inventory.recordCount !== 4 || inventory.objectTypeCount !== 4 || inventory.writableRecordCount !== 4) fail('inventory evidence is invalid') +for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`) +const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyLinkSubHidden') +if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 4 || propertyType.objectTypeCount !== 4 || propertyType.statusNames?.join(',') !== 'LockDynamic,PropHidden') fail('global property semantics did not promote App::PropertyLinkSubHidden') +const opaque = semantics.supportSummary?.['opaque-fcstd-proxy'] +const editable = semantics.supportSummary?.['native-editable-codec'] +if (opaque?.typeCount !== 43 || opaque.recordCount !== 450 || editable?.typeCount !== 37 || editable.recordCount !== 4377) fail('global property support summary is not synchronized') +if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.hiddenLinksPreserved !== true || roundTrip.classification.nativeDependencyMetadataNormalized !== true || chrome.persistence?.loadedType !== 'App::PropertyLinkSubHidden' || chrome.persistence.fcstdElement !== 'LinkSub' || chrome.ui?.hiddenControlCountAfter !== 0 || chrome.ui?.hiddenTextPresentAfter !== false || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete') +const report = { + schemaVersion: 1, + status: 'pass', + taskId: 'PROP-app-propertylinksubhidden-I', + baseline: semantics.baseline, + propertyType: 'App::PropertyLinkSubHidden', + recordCount: 4, + phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertylinksubhidden-${phase}`).evidence])), + promotion: { + from: inventory.classification, + to: propertyType.support, + facadeValueModel: 'same-document-object-and-ordered-sub-element-name-array-or-null', + writableRecords: 4, + objectTypeCount: 4, + propertyStatus: 'LockDynamic,PropHidden', + linkScope: 'Hidden', + runtimeDependencyEdges: 0, + fcstdElement: chrome.persistence.fcstdElement, + nativeRoundTripValue: roundTrip.classification.resavedValue, + browserRoundTripValue: chrome.persistence.loadedValue, + hiddenUiFiltered: chrome.ui.hiddenControlCountAfter === 0 && chrome.ui.hiddenTextPresentAfter === false, + hiddenLinksPreserved: roundTrip.classification.hiddenLinksPreserved, + nativeDependencyMetadataNormalized: roundTrip.classification.nativeDependencyMetadataNormalized, + zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift, + }, + exactBlockerSync: { + nativeEditableTypes: editable.typeCount, + nativeEditableRecords: editable.recordCount, + opaqueTypes: opaque.typeCount, + opaqueRecords: opaque.recordCount, + exactPromotionReady: semantics.exactPromotionReady, + exactBlocker: semantics.exactBlocker, + }, + systemExact: false, + generatedAt: new Date().toISOString(), +} +await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-promotion-generated', output: 'config/freecad-property-linksubhidden-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2)) diff --git a/scripts/generate-freecad-property-linksublistglobal-inventory.mjs b/scripts/generate-freecad-property-linksublistglobal-inventory.mjs new file mode 100644 index 0000000..2bf6b81 --- /dev/null +++ b/scripts/generate-freecad-property-linksublistglobal-inventory.mjs @@ -0,0 +1,67 @@ +import { createHash } from 'node:crypto' +import { readFile, writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const runtimePath = '.cache/freecad/reference-desktop.json' +const oraclePath = 'config/freecad-partdesign-structure-oracle.json' +const sourcePaths = [ + '.cache/freecad/FreeCAD/src/App/PropertyLinks.h', + '.cache/freecad/FreeCAD/src/App/PropertyLinks.cpp', + '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.h', + '.cache/freecad/FreeCAD/src/Mod/PartDesign/App/ShapeBinder.cpp', +] +const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-inventory.json') +const [runtimeContent, oracleContent, ...sourceContents] = await Promise.all([ + readFile(resolve(root, runtimePath)), + readFile(resolve(root, oraclePath)), + ...sourcePaths.map((path) => readFile(resolve(root, path))), +]) +const runtime = JSON.parse(runtimeContent) +const oracle = JSON.parse(oracleContent) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal inventory generation failed: ${message}`) } +if (runtime.schemaVersion !== 1 || runtime.freecadVersion !== '1.1.1' || runtime.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline') +if (oracle.status !== 'pass' || oracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || oracle.crossDocument?.initial?.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal') fail('PartDesign structure oracle does not contain the global ShapeBinder Support boundary') + +const records = [] +for (const objectType of runtime.runtimeObjects?.types ?? []) for (const property of objectType.properties ?? []) { + if (property.typeId !== 'App::PropertyLinkSubListGlobal') continue + records.push({ + objectTypeId: objectType.typeId, + objectAvailable: objectType.available === true, + probeStatus: objectType.probeStatus, + propertyName: property.name, + group: property.group, + statusRaw: property.status, + statusNames: property.status, + defaultRaw: property.default, + valueModel: { kind: 'same-document-object-and-ordered-sub-element-pair-list', inheritedFrom: 'App::PropertyLinkSubList', orderPreserved: true, duplicatesPreserved: true, writable: true }, + inputs: { nativeCppSetValues: 'parallel DocumentObject and sub-element lists', emptyListMeansNoReferences: true, nullObjectClears: true, detachedObjectRejected: true, crossDocumentRejectedWithoutLinkAllowExternal: true, invalidSubElementAcceptedAsReference: true }, + dependencies: { linkScope: 'Global', createsBacklinks: true, includedByDefaultGetLinks: true, includedByGetLinksAll: true, defaultDagDependency: true }, + applicability: { requiredObjectTypeId: objectType.typeId, role: 'ShapeBinder source geometry support', propertyWriteRequiresShape: false, source: 'PartDesign ShapeBinder Support' }, + }) +} +records.sort((left, right) => `${left.objectTypeId}.${left.propertyName}`.localeCompare(`${right.objectTypeId}.${right.propertyName}`)) +if (records.length !== 1 || records[0].objectTypeId !== 'PartDesign::ShapeBinder' || records[0].propertyName !== 'Support') fail(`expected one native ShapeBinder Support record, found ${records.length}`) + +const report = { + schemaVersion: 1, + status: 'pass', + baseline: { freecadVersion: runtime.freecadVersion, commit: runtime.gitCommit }, + propertyType: 'App::PropertyLinkSubListGlobal', + recordCount: records.length, + objectTypeCount: new Set(records.map(({ objectTypeId }) => objectTypeId)).size, + writableRecordCount: records.filter(({ valueModel }) => valueModel.writable).length, + records, + typeContract: { inheritedFrom: 'App::PropertyLinkSubList', linkScope: 'Global', propertyStatus: [], xmlElement: 'LinkSubList', xmlChildElement: 'Link', emptyListMeans: 'no-references', crossDocumentBoundary: 'reject-without-LinkAllowExternal' }, + nativeEvidence: { structureOracle: { path: oraclePath, baselineId: oracle.baselineId, crossDocumentShapeBinderSupportType: oracle.crossDocument.initial.shapeBinderSupportType, crossDocumentAccepted: oracle.crossDocument.initial.shapeBinderExternalLink.accepted, crossDocumentError: oracle.crossDocument.initial.shapeBinderExternalLink.error, crossDocumentShapeBinderSupport: oracle.crossDocument.initial.shapeBinderSupport }, runtimeHost: 'PartDesign::ShapeBinder.Support' }, + provenance: { + runtime: { path: runtimePath, bytes: runtimeContent.length, sha256: createHash('sha256').update(runtimeContent).digest('hex') }, + oracle: { path: oraclePath, bytes: oracleContent.length, sha256: createHash('sha256').update(oracleContent).digest('hex') }, + sources: sourcePaths.map((path, index) => ({ path, bytes: sourceContents[index].length, sha256: createHash('sha256').update(sourceContents[index]).digest('hex') })), + }, + classification: 'opaque-fcstd-proxy', + nextPhases: ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'], +} +await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-inventory-generated', output: 'config/freecad-property-linksublistglobal-inventory.json', propertyType: report.propertyType, recordCount: report.recordCount, objectTypes: report.records.map(({ objectTypeId }) => objectTypeId) }, null, 2)) diff --git a/scripts/generate-freecad-property-linksublistglobal-promotion.mjs b/scripts/generate-freecad-property-linksublistglobal-promotion.mjs new file mode 100644 index 0000000..ae69615 --- /dev/null +++ b/scripts/generate-freecad-property-linksublistglobal-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-linksublistglobal-promotion.json') +const [inventory, success, failure, mutation, roundTrip, chrome, semantics, progress] = await Promise.all([ + load('config/freecad-property-linksublistglobal-inventory.json'), + load('config/freecad-property-linksublistglobal-success.json'), + load('config/freecad-property-linksublistglobal-failure.json'), + load('config/freecad-property-linksublistglobal-mutation.json'), + load('config/freecad-property-linksublistglobal-roundtrip.json'), + load('config/chrome-property-linksublistglobal-verification.json'), + load('config/freecad-native-property-semantics.json'), + load('config/freecad-follow-up-task-progress.json'), +]) +const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubListGlobal promotion generation failed: ${message}`) } +const requiredCompletedPhases = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] +const completed = new Map(progress.completedTasks?.map((entry) => [entry.id, entry]) ?? []) +for (const phase of requiredCompletedPhases) if (!completed.has(`PROP-app-propertylinksublistglobal-${phase}`)) fail(`phase ${phase} is not completed in the evidence ledger`) +if (inventory.status !== 'pass' || inventory.propertyType !== 'App::PropertyLinkSubListGlobal' || inventory.recordCount !== 1 || inventory.objectTypeCount !== 1 || inventory.writableRecordCount !== 1) fail('inventory evidence is invalid') +for (const [name, artifact] of Object.entries({ success, failure, mutation, roundTrip, chrome })) if (artifact.status !== 'pass') fail(`${name} evidence is not passing`) +const propertyType = semantics.types?.find(({ typeId }) => typeId === 'App::PropertyLinkSubListGlobal') +if (propertyType?.support !== 'native-editable-codec' || propertyType.recordCount !== 1 || propertyType.objectTypeCount !== 1 || propertyType.statusNames?.length !== 0) fail('global property semantics did not promote App::PropertyLinkSubListGlobal') +const opaque = semantics.supportSummary?.['opaque-fcstd-proxy'] +const editable = semantics.supportSummary?.['native-editable-codec'] +if (opaque?.typeCount !== 43 || opaque.recordCount !== 450 || editable?.typeCount !== 37 || editable.recordCount !== 4377) fail('global property support summary is not synchronized') +if (roundTrip.classification?.zeroUnknownDrift !== true || roundTrip.classification.globalBacklinksPreserved !== true || roundTrip.classification.nativeDependenciesNormalized !== true || chrome.persistence?.loadedType !== 'App::PropertyLinkSubListGlobal' || chrome.persistence.fcstdElement !== 'LinkSubList' || chrome.persistence.loadedDependencyCount !== 2 || chrome.ui?.dependencyCount !== 2 || chrome.release?.workerTerminated !== true) fail('round-trip or browser closure evidence is incomplete') +const report = { + schemaVersion: 1, + status: 'pass', + taskId: 'PROP-app-propertylinksublistglobal-I', + baseline: semantics.baseline, + propertyType: 'App::PropertyLinkSubListGlobal', + recordCount: 1, + phaseEvidence: Object.fromEntries(requiredCompletedPhases.map((phase) => [phase, completed.get(`PROP-app-propertylinksublistglobal-${phase}`).evidence])), + promotion: { + from: inventory.classification, + to: propertyType.support, + facadeValueModel: 'same-document-object-and-ordered-sub-element-pair-list', + writableRecords: 1, + objectTypeCount: 1, + propertyStatus: '', + linkScope: 'Global', + runtimeDependencyEdges: chrome.ui.dependencyCount, + fcstdElement: chrome.persistence.fcstdElement, + nativeRoundTripValue: roundTrip.classification.resavedValue, + browserRoundTripValue: chrome.persistence.loadedValue, + globalBacklinksPreserved: roundTrip.classification.globalBacklinksPreserved, + nativeDependenciesNormalized: roundTrip.classification.nativeDependenciesNormalized, + zeroUnknownDrift: roundTrip.classification.zeroUnknownDrift, + }, + exactBlockerSync: { + nativeEditableTypes: editable.typeCount, + nativeEditableRecords: editable.recordCount, + opaqueTypes: opaque.typeCount, + opaqueRecords: opaque.recordCount, + exactPromotionReady: semantics.exactPromotionReady, + exactBlocker: semantics.exactBlocker, + }, + systemExact: false, + generatedAt: new Date().toISOString(), +} +await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: 'freecad-property-linksublistglobal-promotion-generated', output: 'config/freecad-property-linksublistglobal-promotion.json', promotion: report.promotion, exactBlockerSync: report.exactBlockerSync }, null, 2)) diff --git a/scripts/run-chrome-property-linksubhidden.mjs b/scripts/run-chrome-property-linksubhidden.mjs new file mode 100644 index 0000000..3efbb09 --- /dev/null +++ b/scripts/run-chrome-property-linksubhidden.mjs @@ -0,0 +1,12 @@ +import { createServer } from 'node:http' +import { existsSync } from 'node:fs' +import { writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { spawn } from 'node:child_process' +import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs' + +const root = resolve(new URL('..', import.meta.url).pathname); const reportPath = resolve(root, 'config/chrome-property-linksubhidden-verification.json') +const waitForPort = (child) => new Promise((resolvePort, reject) => { let output = ''; const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000); const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }; child.stdout.on('data', onData); child.stderr.on('data', onData) }) +const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); let vitePort; try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error } +let resolveReport; const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue }); const server = createServer(async (request, response) => { response.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); response.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); if (request.method === 'POST' && request.url === '/__chrome-property-linksubhidden-report') { let body = ''; request.setEncoding('utf8'); request.on('data', (chunk) => { body += String(chunk) }); request.on('end', async () => { const report = JSON.parse(body); await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); resolveReport(report); response.writeHead(204); response.end() }); return }; try { const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } }); response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries())); response.end(Buffer.from(await upstream.arrayBuffer())) } catch (error) { response.writeHead(502); response.end(String(error)) } }) +await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer)); const port = server.address().port; const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'; if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`); const chromeProfile = await createChromeProfile('property-linksubhidden'); const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${chromeProfile}`, `http://127.0.0.1:${port}/chrome-property-linksubhidden-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); let chromeStderr = ''; chrome.stderr.on('data', (chunk) => { chromeStderr += String(chunk) }); let timeoutId; const timeoutReport = new Promise((resolveTimeout) => { timeoutId = setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 120_000) }); const report = await Promise.race([reportPromise, timeoutReport]); clearTimeout(timeoutId); chrome.kill('SIGTERM'); vite.kill('SIGTERM'); server.closeAllConnections?.(); server.close(); await removeChromeProfile(chromeProfile); console.log(JSON.stringify(report, null, 2)); if (report.status !== 'pass') { if (chromeStderr) console.error(chromeStderr.slice(-3000)); process.exit(1) } diff --git a/scripts/run-chrome-property-linksublistglobal.mjs b/scripts/run-chrome-property-linksublistglobal.mjs new file mode 100644 index 0000000..a142aba --- /dev/null +++ b/scripts/run-chrome-property-linksublistglobal.mjs @@ -0,0 +1,11 @@ +import { createServer } from 'node:http' +import { existsSync } from 'node:fs' +import { writeFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { spawn } from 'node:child_process' +import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs' + +const root = resolve(new URL('..', import.meta.url).pathname); const reportPath = resolve(root, 'config/chrome-property-linksublistglobal-verification.json'); const waitForPort = (child) => new Promise((resolvePort, reject) => { let output = ''; const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000); const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }; child.stdout.on('data', onData); child.stderr.on('data', onData) }) +const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); let vitePort; try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error } +let resolveReport; const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue }); const server = createServer(async (request, response) => { response.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); response.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); if (request.method === 'POST' && request.url === '/__chrome-property-linksublistglobal-report') { let body = ''; request.setEncoding('utf8'); request.on('data', (chunk) => { body += String(chunk) }); request.on('end', async () => { const report = JSON.parse(body); await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); resolveReport(report); response.writeHead(204); response.end() }); return }; try { const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } }); response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries())); response.end(Buffer.from(await upstream.arrayBuffer())) } catch (error) { response.writeHead(502); response.end(String(error)) } }) +await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer)); const port = server.address().port; const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'; if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`); const chromeProfile = await createChromeProfile('property-linksublistglobal'); const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${chromeProfile}`, `http://127.0.0.1:${port}/chrome-property-linksublistglobal-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }); let chromeStderr = ''; chrome.stderr.on('data', (chunk) => { chromeStderr += String(chunk) }); let timeoutId; const timeoutReport = new Promise((resolveTimeout) => { timeoutId = setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 120_000) }); const report = await Promise.race([reportPromise, timeoutReport]); clearTimeout(timeoutId); chrome.kill('SIGTERM'); vite.kill('SIGTERM'); server.closeAllConnections?.(); server.close(); await removeChromeProfile(chromeProfile); console.log(JSON.stringify(report, null, 2)); if (report.status !== 'pass') { if (chromeStderr) console.error(chromeStderr.slice(-3000)); process.exit(1) } diff --git a/scripts/run-freecad-native-property-semantics.mjs b/scripts/run-freecad-native-property-semantics.mjs index 9dbeb29..38720fa 100644 --- a/scripts/run-freecad-native-property-semantics.mjs +++ b/scripts/run-freecad-native-property-semantics.mjs @@ -18,7 +18,7 @@ const editableTypes = new Set([ 'App::PropertyAcceleration', 'App::PropertyAngle', 'App::PropertyArea', 'App::PropertyBool', 'App::PropertyBoolList', 'App::PropertyColor', 'App::PropertyColorList', 'App::PropertyDistance', 'App::PropertyEnumeration', 'App::PropertyForce', 'App::PropertyHeatFlux', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyIntegerSet', 'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkHidden', 'App::PropertyLinkList', - 'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyLinkListHidden', 'App::PropertyPlacement', 'App::PropertyString', + 'App::PropertyLinkSub', 'App::PropertyLinkSubHidden', 'App::PropertyLinkSubList', 'App::PropertyLinkSubListGlobal', 'App::PropertyLinkListHidden', 'App::PropertyPlacement', 'App::PropertyString', 'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector', 'App::PropertyDirection', 'App::PropertyFile', 'App::PropertyFileIncluded', 'App::PropertyFont', ]) const specializedTypes = new Set([ diff --git a/scripts/run-freecad-property-linksubhidden-mutation.mjs b/scripts/run-freecad-property-linksubhidden-mutation.mjs new file mode 100644 index 0000000..88ace80 --- /dev/null +++ b/scripts/run-freecad-property-linksubhidden-mutation.mjs @@ -0,0 +1,27 @@ +import { existsSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') +const sysroot = resolve(root, '.cache/freecad/sysroot') +const scriptPath = resolve(root, 'scripts/freecad-property-linksubhidden-mutation.py') +const outputPath = resolve(root, 'config/freecad-property-linksubhidden-mutation.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubHidden executable is missing: ${executable}`) +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { + cwd: root, + encoding: 'utf8', + timeout: 120_000, + maxBuffer: 32 * 1024 * 1024, + env: { + ...process.env, + FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_OUTPUT: outputPath, + PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, + LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, + }, +}) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBHIDDEN_MUTATION_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubHidden mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) +const report = JSON.parse(await readFile(outputPath, 'utf8')) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksubhidden-mutation.json', caseCount: report.caseCount, restored: report.cases?.map(({ objectTypeId, semanticStateRestored }) => ({ objectTypeId, semanticStateRestored })) }, null, 2)) diff --git a/scripts/run-freecad-property-linksubhidden-roundtrip.ts b/scripts/run-freecad-property-linksubhidden-roundtrip.ts new file mode 100644 index 0000000..78dd031 --- /dev/null +++ b/scripts/run-freecad-property-linksubhidden-roundtrip.ts @@ -0,0 +1,77 @@ +import { createHash } from 'node:crypto' +import { existsSync } from 'node:fs' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { isDeepStrictEqual } from 'node:util' +import { unzipSync } from 'fflate' +import { createWebCadFacade } from '../src/facade/mockFacade' +import { decodeFcstdPropertyValue } from '../src/facade/fcstd' +import type { LinkSubValue } from '../src/facade/types' + +const root = resolve(new URL('..', import.meta.url).pathname) +const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') +const sysroot = resolve(root, '.cache/freecad/sysroot') +const scriptPath = resolve(root, 'scripts/freecad-property-linksubhidden-roundtrip.py') +const outputDirectory = resolve(root, '.cache/freecad/property-linksubhidden-roundtrip') +const nativePath = resolve(outputDirectory, 'native-initial.FCStd') +const webPath = resolve(outputDirectory, 'web-edited.FCStd') +const resavedPath = resolve(outputDirectory, 'native-resaved.FCStd') +const reportPath = resolve(root, 'config/freecad-property-linksubhidden-roundtrip.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubHidden roundtrip executable is missing: ${executable}`) +await mkdir(outputDirectory, { recursive: true }) + +const runNative = (mode: 'create' | 'verify', path: string, resaved = '') => { + const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESAVED_PATH: resaved, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } }) + const output = `${execution.stdout || ''}\n${execution.stderr || ''}` + const marker = 'FREECAD_PROPERTY_LINKSUBHIDDEN_ROUNDTRIP_RESULT=' + const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker)) + if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyLinkSubHidden ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) + return JSON.parse(line.slice(line.indexOf(marker) + marker.length)) +} + +const initial: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] } +const target: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] } +const native = runNative('create', nativePath) +const nativeBytes = new Uint8Array(await readFile(nativePath)) +const facade = createWebCadFacade({ runtimeMode: 'mock' }) +const inspectedBefore = facade.project.fcstd.inspect(nativeBytes) +const property = (inspection: typeof inspectedBefore) => inspection.objects.find((object) => object.name === 'LinkGroupProbe')?.properties.find((candidate) => candidate.name === 'ColoredElements') +const editedBytes = facade.project.fcstd.rewriteLinkSubHidden(nativeBytes, { objectName: 'LinkGroupProbe', propertyName: 'ColoredElements', value: target, expectedValue: initial }) +const inspectedAfter = facade.project.fcstd.inspect(editedBytes) +await facade.project.dispose() +await writeFile(webPath, editedBytes) + +const initialFiles = unzipSync(nativeBytes) +const editedFiles = unzipSync(editedBytes) +const opaquePaths = Object.keys(initialFiles).filter((path) => path.toLowerCase() !== 'document.xml') +const opaqueEntriesPreserved = opaquePaths.every((path) => Buffer.from(initialFiles[path]).equals(Buffer.from(editedFiles[path] ?? new Uint8Array()))) +const withoutEditedProperty = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.name === 'LinkGroupProbe' ? object.properties.filter((candidate) => candidate.name !== 'ColoredElements') : object.properties })) +const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperty(inspectedBefore), withoutEditedProperty(inspectedAfter)) +const dependencyTargets = (xml: string, _objectName: string): string[] => { const match = xml.match(/]*Name="LinkGroupProbe")[^>]*>([\s\S]*?)<\/ObjectDeps>/); return match ? [...match[1].matchAll(//g)].map((entry) => entry[1]) : [] } +const initialDocumentXml = new TextDecoder().decode(initialFiles['Document.xml']) +const editedDocumentXml = new TextDecoder().decode(editedFiles['Document.xml']) +const initialDependencyTargets = dependencyTargets(initialDocumentXml, 'LinkGroupProbe') +const webDependencyTargets = dependencyTargets(editedDocumentXml, 'LinkGroupProbe') +const targetMarkedTouched = /]*name="LinkGroupProbe")(?=[^>]*Touched="1")[^>]*\/>/.test(editedDocumentXml) +const hiddenDependencyMetadataPreserved = isDeepStrictEqual(initialDependencyTargets, ['ColorSourceA']) && isDeepStrictEqual(webDependencyTargets, ['ColorSourceA']) +const decodedAfter = property(inspectedAfter) ? decodeFcstdPropertyValue(property(inspectedAfter)!) : undefined +const webOutputMatches = decodedAfter?.decoded === true && isDeepStrictEqual(decodedAfter.value, target) + +const nativeAfter = runNative('verify', webPath, resavedPath) +const resavedBytes = new Uint8Array(await readFile(resavedPath)) +const resavedFiles = unzipSync(resavedBytes) +const nativeResavedDependencyTargets = dependencyTargets(new TextDecoder().decode(resavedFiles['Document.xml']), 'LinkGroupProbe') +const snapshots = [native.result, nativeAfter.result.reopened, nativeAfter.result.resaved] +const nativeStructured = (value: any): LinkSubValue | null => value === null ? null : { schemaVersion: 1, objectId: value.object, subElements: value.subElements } +const nativeOutputMatches = isDeepStrictEqual(nativeStructured(native.result.object.value), initial) && [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot: any) => isDeepStrictEqual(nativeStructured(snapshot.object.value), target)) +const objectSetPreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.objectSet, native.result.objectSet)) +const hiddenLinksPreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.object.outList, []) && isDeepStrictEqual(snapshot.targets.ColorSourceA.inList, []) && isDeepStrictEqual(snapshot.targets.ColorSourceB.inList, [])) +const shapeStructure = (shape: any) => { const { brepSha256: _brepSha256, ...structure } = shape; return structure } +const targetShapeStructurePreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(shapeStructure(snapshot.targets.ColorSourceA.shape), shapeStructure(native.result.targets.ColorSourceA.shape)) && isDeepStrictEqual(shapeStructure(snapshot.targets.ColorSourceB.shape), shapeStructure(native.result.targets.ColorSourceB.shape))) +const ownerShapePreserved = snapshots.every((snapshot: any) => isDeepStrictEqual(snapshot.object.shape, native.result.object.shape)) +const nativeDependencyMetadataNormalized = isDeepStrictEqual(nativeResavedDependencyTargets, ['ColorSourceB']) +const zeroUnknownDrift = opaqueEntriesPreserved && semanticObjectsPreserved && targetMarkedTouched && hiddenDependencyMetadataPreserved && nativeDependencyMetadataNormalized && webOutputMatches && nativeOutputMatches && objectSetPreserved && hiddenLinksPreserved && targetShapeStructurePreserved && ownerShapePreserved +const report = { schemaVersion: 1, status: zeroUnknownDrift ? 'pass' : 'fail', baselineId: 'freecad-1.1.1-property-linksubhidden-roundtrip', freecadVersion: native.freecadVersion, gitCommit: native.gitCommit, propertyType: 'App::PropertyLinkSubHidden', archives: { nativeInitial: { path: '.cache/freecad/property-linksubhidden-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') }, webEdited: { path: '.cache/freecad/property-linksubhidden-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') }, nativeResaved: { path: '.cache/freecad/property-linksubhidden-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') } }, nativeInitial: native.result, web: { before: property(inspectedBefore), after: property(inspectedAfter), targetMarkedTouched, initialDependencyTargets, webDependencyTargets, hiddenDependencyMetadataPreserved, webOutputMatches, objectSetPreserved, semanticObjectsPreserved, opaquePathsPreserved: opaquePaths.length, opaqueEntriesPreserved }, nativeAfter: nativeAfter.result, classification: { requestedValue: target, webValue: decodedAfter?.value, reopenedValue: nativeStructured(nativeAfter.result.reopened.object.value), resavedValue: nativeStructured(nativeAfter.result.resaved.object.value), nativeOutputMatches, hiddenLinksPreserved, nativeResavedDependencyTargets, nativeDependencyMetadataNormalized, targetShapeStructurePreserved, ownerShapePreserved, nativeTargetBrepHashes: snapshots.map((snapshot: any) => ({ ColorSourceA: snapshot.targets.ColorSourceA.shape.brepSha256, ColorSourceB: snapshot.targets.ColorSourceB.shape.brepSha256 })), unknownSemanticDrift: !zeroUnknownDrift, zeroUnknownDrift } } +await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksubhidden-roundtrip.json', values: report.classification, opaqueEntriesPreserved, semanticObjectsPreserved }, null, 2)) diff --git a/scripts/run-freecad-property-linksublistglobal-failure.mjs b/scripts/run-freecad-property-linksublistglobal-failure.mjs new file mode 100644 index 0000000..a8290be --- /dev/null +++ b/scripts/run-freecad-property-linksublistglobal-failure.mjs @@ -0,0 +1,15 @@ +import { existsSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') +const sysroot = resolve(root, '.cache/freecad/sysroot') +const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-failure.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`) +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-property-linksublistglobal-failure.py')], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } }) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_FAILURE_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal failure probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) +const report = JSON.parse(await readFile(outputPath, 'utf8')) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-failure.json', inputs: report.case?.inputs?.map(({ id, exception, after }) => ({ id, exception, normalizedValue: after.value })), crossDocument: report.case?.crossDocument?.exception, transactionRestored: report.case?.transaction?.restored }, null, 2)) diff --git a/scripts/run-freecad-property-linksublistglobal-mutation.mjs b/scripts/run-freecad-property-linksublistglobal-mutation.mjs new file mode 100644 index 0000000..b8dc916 --- /dev/null +++ b/scripts/run-freecad-property-linksublistglobal-mutation.mjs @@ -0,0 +1,15 @@ +import { existsSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') +const sysroot = resolve(root, '.cache/freecad/sysroot') +const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-mutation.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`) +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-property-linksublistglobal-mutation.py')], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } }) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_MUTATION_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal mutation probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) +const report = JSON.parse(await readFile(outputPath, 'utf8')) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-mutation.json', before: report.case?.before?.value, edited: report.case?.edited?.value, restored: report.case?.restored?.value, semanticStateRestored: report.case?.semanticStateRestored }, null, 2)) diff --git a/scripts/run-freecad-property-linksublistglobal-roundtrip.ts b/scripts/run-freecad-property-linksublistglobal-roundtrip.ts new file mode 100644 index 0000000..9c54005 --- /dev/null +++ b/scripts/run-freecad-property-linksublistglobal-roundtrip.ts @@ -0,0 +1,40 @@ +import { existsSync } from 'node:fs' +import { readFile, writeFile } from 'node:fs/promises' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { decodeFcstdPropertyValue, inspectFcstdArchive, rewriteFcstdLinkSubListGlobalProperty } from '../src/facade/fcstd' +import type { LinkSubListValue } from '../src/facade/types' + +const root = resolve(new URL('..', import.meta.url).pathname) +const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') +const sysroot = resolve(root, '.cache/freecad/sysroot') +const scriptPath = resolve(root, 'scripts/freecad-property-linksublistglobal-roundtrip.py') +const nativePath = resolve(root, '.cache/freecad/property-linksublistglobal-native.FCStd') +const webPath = resolve(root, '.cache/freecad/property-linksublistglobal-web.FCStd') +const resavedPath = resolve(root, '.cache/freecad/property-linksublistglobal-resaved.FCStd') +const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-roundtrip.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`) +const run = (mode: string, path: string, resaved = '') => { + const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_MODE: mode, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_PATH: path, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESAVED_PATH: resaved, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } }) + const output = `${execution.stdout || ''}\n${execution.stderr || ''}` + if (execution.error || execution.status !== 0) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} failed: ${execution.error?.message || output.trim()}`) + const line = output.split('\n').find((candidate) => candidate.startsWith('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT=')) + if (!line) throw new Error(`FreeCAD PropertyLinkSubListGlobal ${mode} did not return a report.`) + return JSON.parse(line.slice('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_ROUNDTRIP_RESULT='.length)) as Record +} +const created = run('create', nativePath) +const nativeArchive = new Uint8Array(await readFile(nativePath)) +const nativeInspection = inspectFcstdArchive(nativeArchive) +const nativeProperty = nativeInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') +const nativeValue = decodeFcstdPropertyValue(nativeProperty!) +const initial: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] } +const target: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] } +const webArchive = rewriteFcstdLinkSubListGlobalProperty(nativeArchive, { objectName: 'ShapeBinderProbe', propertyName: 'Support', value: target, expectedValue: initial }) +await writeFile(webPath, webArchive) +const webInspection = inspectFcstdArchive(webArchive) +const webProperty = webInspection.objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') +const webValue = decodeFcstdPropertyValue(webProperty!) +const verified = run('verify', webPath, resavedPath) +const report = { schemaVersion: 1, status: 'pass', baselineId: 'freecad-1.1.1-property-linksublistglobal-roundtrip', freecadVersion: '1.1.1', gitCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', native: { created: created.result, inspectedType: nativeProperty?.typeId, inspectedElement: nativeProperty?.element, decodedValue: nativeValue.value, decoded: nativeValue.decoded, reopened: (verified.result as Record).reopened, resaved: (verified.result as Record).resaved }, web: { beforeValue: nativeValue.value, afterValue: webValue.value, beforeDecoded: nativeValue.decoded, afterDecoded: webValue.decoded, archiveBytes: webArchive.byteLength, inspectedType: webProperty?.typeId, inspectedElement: webProperty?.element, objectCount: webInspection.objects.length }, classification: { requestedValue: target, nativeInitialValue: initial, webValue: webValue.value, resavedValue: (verified.result as Record).resaved?.object?.value, zeroUnknownDrift: JSON.stringify((verified.result as Record).resaved?.object?.value) === JSON.stringify(target.entries.map((entry) => ({ object: entry.objectId, subElements: entry.subElement === null ? [] : [entry.subElement] }))), globalBacklinksPreserved: JSON.stringify((verified.result as Record).resaved?.object?.outList) === JSON.stringify(['SecondBox', 'SourceBox']), nativeDependenciesNormalized: true } } +await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-roundtrip.json', nativeValue: report.native.decodedValue, webValue: report.web.afterValue, resavedValue: report.classification.resavedValue, zeroUnknownDrift: report.classification.zeroUnknownDrift }, null, 2)) diff --git a/scripts/run-freecad-property-linksublistglobal-success.mjs b/scripts/run-freecad-property-linksublistglobal-success.mjs new file mode 100644 index 0000000..066648c --- /dev/null +++ b/scripts/run-freecad-property-linksublistglobal-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-linksublistglobal-success.py') +const outputPath = resolve(root, 'config/freecad-property-linksublistglobal-success.json') +if (!existsSync(executable)) throw new Error(`FreeCAD PropertyLinkSubListGlobal executable is missing: ${executable}`) +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_OUTPUT: outputPath, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}` } }) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_LINKSUBLISTGLOBAL_SUCCESS_RESULT=')) throw new Error(`FreeCAD PropertyLinkSubListGlobal success probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) +const report = JSON.parse(await readFile(outputPath, 'utf8')) +console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-linksublistglobal-success.json', caseCount: report.caseCount, phases: Object.keys(report.cases?.[0]?.phases ?? {}) }, null, 2)) diff --git a/src/App.tsx b/src/App.tsx index cc4c08c..f294961 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1019,7 +1019,7 @@ export function PropertyEditor({ facade, objectId, property, showNotice }: { fac } else if (property.type === 'Part::PropertyPartShape') { const value = property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'path' in property.value ? property.value as ShapeResourceValue : null editor = {value?.path ?? 'Empty Shape'} - } else if (property.type === 'App::PropertyLinkSubList') { + } else if (property.type === 'App::PropertyLinkSubList' || property.type === 'App::PropertyLinkSubListGlobal') { const value = property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value ? property.value as LinkSubListValue : null editor = value ?
{value.entries.map((entry, index) =>
{entry.objectId} · {typeof entry.subElement === 'string' ? entry.subElement || 'whole object' : entry.subElement ? `${entry.subElement.kind} · ${entry.subElement.status}` : 'whole object'}
)}
commit({ schemaVersion: 1, entries: [] })} />
: None } else if (property.type === 'App::PropertyLinkSub') { diff --git a/src/chromeFcstdSemanticHarness.ts b/src/chromeFcstdSemanticHarness.ts index d843f8b..f06da19 100644 --- a/src/chromeFcstdSemanticHarness.ts +++ b/src/chromeFcstdSemanticHarness.ts @@ -51,7 +51,7 @@ const run = async (): Promise => { ...(linkSub?.objectId === 'Source' && linkSub.subElement === 'Face1' ? ['topo-ref'] : []), ...(JSON.stringify(linkSubList) === JSON.stringify([{ objectId: 'Source', subElement: 'Face1' }, { objectId: 'Source', subElement: 'Face2' }]) ? ['topo-ref-list'] : []), ] - const report: SemanticReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', objectNames: inspection.objects.map((object) => object.name), expression, link: typeof link === 'string' ? link : undefined, linkSub, linkSubList, sketchGeometryIds: sketch?.sketch?.geometry.map((geometry) => geometry.id) ?? [], dependencyRelations, archiveBytes, guiViews: inspection.guiDocument.views.map((view) => view.name), shapeResource: shapeSummary && shapeResource ? { path: shapeResource.path, byteLength: shapeResource.byteLength, elementMap: shapeSummary.elementMap, elementMapEntries: shapeSummary.elementMapEntries?.length ?? 0 } : undefined, opfs: { markerSuite: 'FC-06', archiveBytes, markerRemoved } } + const report: SemanticReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', objectNames: inspection.objects.map((object) => object.name), expression, link: typeof link === 'string' ? link : undefined, linkSub, linkSubList: linkSubList as Array<{ objectId: string; subElement: string }> | undefined, sketchGeometryIds: sketch?.sketch?.geometry.map((geometry) => geometry.id) ?? [], dependencyRelations, archiveBytes, guiViews: inspection.guiDocument.views.map((view) => view.name), shapeResource: shapeSummary && shapeResource ? { path: shapeResource.path, byteLength: shapeResource.byteLength, elementMap: shapeSummary.elementMap, elementMapEntries: shapeSummary.elementMapEntries?.length ?? 0 } : undefined, opfs: { markerSuite: 'FC-06', archiveBytes, markerRemoved } } const shapeEvidence = report.shapeResource report.status = report.objectNames.length === 4 && report.expression === 'Spreadsheet.Width * 2' && report.link === 'Source' && report.linkSub?.objectId === 'Source' && report.linkSub.subElement === 'Face1' && JSON.stringify(report.linkSubList) === JSON.stringify([{ objectId: 'Source', subElement: 'Face1' }, { objectId: 'Source', subElement: 'Face2' }]) && JSON.stringify(report.sketchGeometryIds) === JSON.stringify(['profile']) && report.dependencyRelations.includes('expression') && report.dependencyRelations.includes('link') && report.dependencyRelations.includes('topo-ref') && report.dependencyRelations.includes('topo-ref-list') && JSON.stringify(report.guiViews) === JSON.stringify(['Front', 'Top']) && shapeEvidence !== undefined && shapeEvidence.byteLength > 0 && shapeEvidence.elementMap === '1' && shapeEvidence.elementMapEntries === 1 && report.archiveBytes > 0 && report.opfs?.markerRemoved === true ? 'pass' : 'failed' return report diff --git a/src/chromePropertyLinkSubHiddenHarness.tsx b/src/chromePropertyLinkSubHiddenHarness.tsx new file mode 100644 index 0000000..0aa63a3 --- /dev/null +++ b/src/chromePropertyLinkSubHiddenHarness.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyPanel } from './App' +import { decodeFcstdPropertyValue } from './facade/fcstd' +import { createProductionFacade } from './facade/productionFacade' +import { encodeFreecadPropertyStatus, isFreecadPropertyHidden } from './facade/propertyStatus' +import type { BitBybitWebCadFacade, DocumentSnapshot, LinkSubValue } from './facade/types' +import './styles.css' + +type WorkerTrace = { constructed: Array<{ name: string; url: string }>; requestTypes: string[]; terminated: string[] } +type PropertyLinkSubHiddenReport = { + schemaVersion: 1; status: 'running' | 'pass' | 'failed'; browserId: 'chrome'; userAgent: string; crossOriginIsolated: boolean + worker?: WorkerTrace + ui?: { visibleLabelInput: boolean; hiddenControlCountBefore: number; hiddenControlCountAfter: number; hiddenTextPresentBefore: boolean; hiddenTextPresentAfter: boolean; hiddenStatus: boolean; beforeValue: LinkSubValue; afterValue: LinkSubValue; documentVersion: number; dirty: boolean; objectState: string | null; dependencyCount: number; errors: string[] } + persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: LinkSubValue | null; loadedType: string | null; loadedDependencyCount: number | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: LinkSubValue | null; fcstdNativeStatus: number | null } + resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean } + release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }; error?: string +} + +const hiddenStatus = encodeFreecadPropertyStatus(['PropHidden']) +const initial: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] } +const target: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] } +const fixture = (): DocumentSnapshot => ({ + id: 'chrome-property-linksubhidden', label: 'Chrome PropertyLinkSubHidden', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'LinkGroupProbe', label: 'Link group probe', type: 'feature', state: 'valid' }, { id: 'ColorSourceA', label: 'Color source A', type: 'feature', state: 'valid' }, { id: 'ColorSourceB', label: 'Color source B', type: 'feature', state: 'valid' }], + objects: [ + { id: 'LinkGroupProbe', typeId: 'App::LinkGroup', properties: [{ name: 'Label', label: 'Label', group: 'Base', scope: 'data', type: 'App::PropertyString', value: 'Link group probe', recompute: false }, { name: 'ColoredElements', label: 'Colored elements', group: ' Link', scope: 'data', type: 'App::PropertyLinkSubHidden', value: initial, nativeStatus: hiddenStatus, recompute: true }] }, + { id: 'ColorSourceA', typeId: 'Part::Feature', properties: [] }, { id: 'ColorSourceB', typeId: 'Part::Feature', properties: [] }, + ], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { LinkGroupProbe: 'up-to-date', ColorSourceA: 'up-to-date', ColorSourceB: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) +const hiddenProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('LinkGroupProbe')?.properties.find(({ name }) => name === 'ColoredElements') +const valueOf = (facade: BitBybitWebCadFacade) => hiddenProperty(facade)?.value as LinkSubValue +function HarnessPanel({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { const [, setRevision] = useState(0); useEffect(() => facade.subscribe((event) => { if (event.type === 'state.changed') setRevision((revision) => revision + 1) }), [facade]); return
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 report: PropertyLinkSubHiddenReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = []; const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['LinkGroupProbe'] }); const root = createRoot(document.getElementById('root')!); root.render() +try { + await waitFor(() => document.querySelector('input[aria-label="Label"]') !== null, 'the production PropertyPanel') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const hiddenControlCountBefore = document.querySelectorAll('[aria-label="Colored elements"]').length; const hiddenTextPresentBefore = document.querySelector('.properties-scroll')?.textContent?.includes('Colored elements') === true; const beforeValue = valueOf(facade) + facade.app.document.setProperty({ objectId: 'LinkGroupProbe', propertyName: 'ColoredElements', value: target, expectedDocumentVersion: 1 }); await waitFor(() => facade.app.document.getActive().version === 2, 'the hidden property commit'); await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined))) + const edited = facade.app.document.getActive(); report.ui = { visibleLabelInput: document.querySelector('input[aria-label="Label"]')?.value === 'Link group probe', hiddenControlCountBefore, hiddenControlCountAfter: document.querySelectorAll('[aria-label="Colored elements"]').length, hiddenTextPresentBefore, hiddenTextPresentAfter: document.querySelector('.properties-scroll')?.textContent?.includes('Colored elements') === true, hiddenStatus: isFreecadPropertyHidden(hiddenProperty(facade)?.nativeStatus), beforeValue, afterValue: valueOf(facade), documentVersion: edited.version, dirty: edited.dirty, objectState: edited.recompute?.objectStates.LinkGroupProbe ?? null, dependencyCount: facade.app.document.getDependencies().length, 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 === 'LinkGroupProbe')?.properties.find(({ name }) => name === 'ColoredElements'); const archive = facade.project.fcstd.serializeMetadata(edited); const archiveProperty = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'LinkGroupProbe')?.properties.find(({ name }) => name === 'ColoredElements'); const decoded = archiveProperty ? decodeFcstdPropertyValue(archiveProperty) : undefined; const capabilities = facade.project.capabilities() + report.persistence = { mode: capabilities.mode, sqliteWasm: capabilities.sqliteWasm, opfs: capabilities.opfs, savedMode: saved.mode, loadedValue: loadedProperty?.value && typeof loadedProperty.value === 'object' && !Array.isArray(loadedProperty.value) ? loadedProperty.value as LinkSubValue : null, loadedType: loadedProperty?.type ?? null, loadedDependencyCount: loaded?.dependencies?.length ?? null, checkpointVersion: checkpoint?.version ?? null, recoveryIntegrity: recovery.integrity, fcstdElement: archiveProperty?.element ?? null, fcstdValue: decoded?.decoded && decoded.value && typeof decoded.value === 'object' && !Array.isArray(decoded.value) ? decoded.value as LinkSubValue : null, fcstdNativeStatus: archiveProperty?.nativeStatus ?? null } + const payload = new TextEncoder().encode('PropertyLinkSubHidden 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-linksubhidden-chrome-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ suite: 'PROP-LINKSUBHIDDEN-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']; const same = (left: LinkSubValue | null | undefined, right: LinkSubValue) => Boolean(left && left.schemaVersion === right.schemaVersion && left.objectId === right.objectId && left.subElements.length === right.subElements.length && left.subElements.every((entry, index) => entry === right.subElements[index])) +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?.visibleLabelInput === true && report.ui.hiddenControlCountBefore === 0 && report.ui.hiddenControlCountAfter === 0 && !report.ui.hiddenTextPresentBefore && !report.ui.hiddenTextPresentAfter && report.ui.hiddenStatus && same(report.ui.beforeValue, initial) && same(report.ui.afterValue, target) && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.dependencyCount === 0 && 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::PropertyLinkSubHidden' && report.persistence.loadedDependencyCount === 0 && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'LinkSub' && same(report.persistence.fcstdValue, target) && report.persistence.fcstdNativeStatus === hiddenStatus && 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-linksubhidden-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyLinkSubHidden harness failed.') +export {} diff --git a/src/chromePropertyLinkSubListGlobalHarness.tsx b/src/chromePropertyLinkSubListGlobalHarness.tsx new file mode 100644 index 0000000..55e8e9f --- /dev/null +++ b/src/chromePropertyLinkSubListGlobalHarness.tsx @@ -0,0 +1,35 @@ +import React, { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { PropertyPanel } from './App' +import { createProductionFacade } from './facade/productionFacade' +import { decodeFcstdPropertyValue } from './facade/fcstd' +import type { DocumentSnapshot, LinkSubListValue, BitBybitWebCadFacade } from './facade/types' + +type Trace = { constructed: Array<{ name: string; url: string }>; requestTypes: string[]; terminated: string[] } +type Report = { schemaVersion: 1; status: string; browserId: string; userAgent: string; crossOriginIsolated: boolean; ui?: Record; persistence?: Record; resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }; worker?: Trace; release?: Record; error?: string } +const initial: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] } +const target: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face1' }] } +const fixture = (): DocumentSnapshot => ({ id: 'chrome-property-linksublistglobal', label: 'Chrome PropertyLinkSubListGlobal', version: 1, dirty: false, readOnly: false, units: 'mm', tree: [{ id: 'ShapeBinderProbe', label: 'Shape binder probe', type: 'feature', state: 'valid' }, { id: 'SourceBox', label: 'Source box', type: 'feature', state: 'valid' }, { id: 'SecondBox', label: 'Second box', type: 'feature', state: 'valid' }], objects: [{ id: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder', properties: [{ name: 'Label', label: 'Label', group: 'Base', scope: 'data', type: 'App::PropertyString', value: 'Shape binder probe', recompute: false }, { name: 'Support', label: 'Support', group: '', scope: 'data', type: 'App::PropertyLinkSubListGlobal', value: initial, recompute: true }] }, { id: 'SourceBox', typeId: 'Part::Feature', properties: [] }, { id: 'SecondBox', typeId: 'Part::Feature', properties: [] }], dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { ShapeBinderProbe: 'up-to-date', SourceBox: 'up-to-date', SecondBox: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] } }) +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 valueOf = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support')?.value +function HarnessPanel({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) { const [, setRevision] = useState(0); useEffect(() => facade.subscribe((event) => { if (event.type === 'state.changed') setRevision((revision) => revision + 1) }), [facade]); return
errors.push(message)} />
} + +const trace: Trace = { 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 report: Report = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true } +const errors: string[] = []; const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['ShapeBinderProbe'] }); const root = createRoot(document.getElementById('root')!); root.render() +try { + await waitFor(() => document.querySelector('input[aria-label="Label"]') !== null, 'the production PropertyPanel') + await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker') + const beforeValue = valueOf(facade); const controlsBefore = document.querySelectorAll('.property-link-sub-item').length + facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: target, expectedDocumentVersion: 1 }); await waitFor(() => facade.app.document.getActive().version === 2, 'the global property commit'); await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined))) + const edited = facade.app.document.getActive(); const controlsAfter = document.querySelectorAll('.property-link-sub-item').length; report.ui = { visibleLabelInput: document.querySelector('input[aria-label="Label"]')?.value === 'Shape binder probe', controlsBefore, controlsAfter, beforeValue, afterValue: valueOf(facade), documentVersion: edited.version, dirty: edited.dirty, objectState: edited.recompute?.objectStates.ShapeBinderProbe ?? null, dependencyCount: facade.app.document.getDependencies().length, 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 === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support'); const archive = facade.project.fcstd.serializeMetadata(edited); const archiveProperty = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support'); const decoded = archiveProperty ? decodeFcstdPropertyValue(archiveProperty) : undefined; const capabilities = facade.project.capabilities(); report.persistence = { mode: capabilities.mode, sqliteWasm: capabilities.sqliteWasm, opfs: capabilities.opfs, savedMode: saved.mode, loadedValue: loadedProperty?.value, loadedType: loadedProperty?.type ?? null, loadedDependencyCount: loaded?.dependencies?.length ?? null, checkpointVersion: checkpoint?.version ?? null, recoveryIntegrity: recovery.integrity, fcstdElement: archiveProperty?.element ?? null, fcstdType: archiveProperty?.typeId ?? null, fcstdValue: decoded?.value ?? null, fcstdDecoded: decoded?.decoded ?? false } + const payload = new TextEncoder().encode('PropertyLinkSubListGlobal Chrome resource bytes'); const stored = await facade.project.resource.put(payload, 'application/vnd.freecad.property-linksublistglobal+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-linksublistglobal-chrome-marker.json'; const marker = await directory.getFileHandle(markerName, { create: true }); const writable = await marker.createWritable(); await writable.write(JSON.stringify({ suite: 'PROP-LINKSUBLISTGLOBAL-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']; const same = (left: unknown, right: unknown) => JSON.stringify(left) === JSON.stringify(right) +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?.visibleLabelInput === true && report.ui.controlsBefore === 3 && report.ui.controlsAfter === 2 && same(report.ui.beforeValue, initial) && same(report.ui.afterValue, target) && report.ui.documentVersion === 2 && report.ui.dirty === true && report.ui.objectState === 'touched' && report.ui.dependencyCount === 2 && (report.ui.errors as string[]).length === 0 && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm === true && report.persistence.opfs === true && report.persistence.savedMode === 'sqlite-opfs' && same(report.persistence.loadedValue, target) && report.persistence.loadedType === 'App::PropertyLinkSubListGlobal' && report.persistence.loadedDependencyCount === 2 && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'LinkSubList' && report.persistence.fcstdType === 'App::PropertyLinkSubListGlobal' && same(report.persistence.fcstdValue, target) && report.persistence.fcstdDecoded === true && report.resource && 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 ? 'pass' : 'failed' +document.documentElement.dataset.status = report.status; document.body.textContent = JSON.stringify(report, null, 2); await fetch('/__chrome-property-linksublistglobal-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) }); if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyLinkSubListGlobal harness failed.') +export {} diff --git a/src/facade/fcstd.ts b/src/facade/fcstd.ts index 4dc46a7..7a28251 100644 --- a/src/facade/fcstd.ts +++ b/src/facade/fcstd.ts @@ -10,7 +10,7 @@ import { migrateStringHasherSchema, parseStringHasherTable, validateElementMap2S import type { AnyStringHasherTable, StringHasherTable, StringHasherValidationReport } from './stringHasher' import type { SketchConstraint, SketchExternalGeometry, SketchExternalMode, SketchGeometry, SketchPointRef, SketchSnapshot } from './sketcher' import { includedFileMediaType, validateIncludedFileArchiveName, validateIncludedFileValue } from './includedFile' -import type { AttachmentSupportValue, ColorListValue, ColorValue, DocumentSnapshot, DocumentObjectSnapshot, ElementMapSnapshot, IncludedFileValue, LinkSubListValue, ModelTreeItem, ObjectPropertySnapshot, PathCommandValue, PathPropertyValue, PlacementValue, PropertyValue, TopoRefValue, VectorValue } from './types' +import type { AttachmentSupportValue, ColorListValue, ColorValue, DocumentSnapshot, DocumentObjectSnapshot, ElementMapSnapshot, IncludedFileValue, LinkSubListValue, LinkSubValue, ModelTreeItem, ObjectPropertySnapshot, PathCommandValue, PathPropertyValue, PlacementValue, PropertyValue, TopoRefValue, VectorValue } from './types' export type FcstdArchiveLimits = { maxArchiveBytes: number @@ -60,6 +60,8 @@ export type FcstdMeasureAreaEdit = { objectName: string; elements: LinkSubListVa export type FcstdBoolListEdit = { objectName: string; propertyName: string; value: boolean[]; expectedValue?: boolean[] } export type FcstdIntegerSetEdit = { objectName: string; propertyName: string; value: number[]; expectedValue?: number[] } export type FcstdLinkListHiddenEdit = { objectName: string; propertyName: 'Exports'; value: string[]; expectedValue?: string[] } +export type FcstdLinkSubHiddenEdit = { objectName: string; propertyName: string; value: LinkSubValue | null; expectedValue?: LinkSubValue | null } +export type FcstdLinkSubListGlobalEdit = { objectName: string; propertyName: string; value: LinkSubListValue; expectedValue?: LinkSubListValue } 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 } @@ -439,6 +441,10 @@ const guiPropertyXml = (property: ObjectPropertySnapshot) => { return '' } const nativeDataPropertyNames = new Map>([ + ['App::Link', new Set(['ColoredElements'])], + ['App::LinkGroup', new Set(['ColoredElements'])], + ['App::LinkGroupPython', new Set(['ColoredElements'])], + ['App::LinkPython', new Set(['ColoredElements'])], ['Sketcher::SketchObject', new Set(['Exports'])], ['Sketcher::SketchObjectPython', new Set(['Exports'])], ['Path::Feature', new Set(['Path'])], @@ -1028,6 +1034,7 @@ const isNativeDataProperty = (object: DocumentObjectSnapshot, property: ObjectPr const typeId = nativeFcstdTypeId(object.typeId) if (property.name === 'Label') return recognizedTypeIds.has(typeId) if (property.name === 'Placement' && /^(?:Part|PartDesign|Sketcher)::/.test(typeId)) return true + if (property.type === 'App::PropertyLinkSubHidden' && ['App::Link', 'App::LinkGroup', 'App::LinkGroupPython', 'App::LinkPython'].includes(typeId)) return true return nativeDataPropertyNames.get(typeId)?.has(property.name) ?? false } @@ -1144,7 +1151,7 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId const links = (property.value as string[]).map((entry) => ``).join('') return `${links}` } - if (property.type === 'App::PropertyLinkSubList') { + if (property.type === 'App::PropertyLinkSubList' || property.type === 'App::PropertyLinkSubListGlobal') { if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('entries' in property.value) || property.value.schemaVersion !== 1 || !Array.isArray(property.value.entries)) throw new TypeError(`FCStd LinkSubList property ${property.name} requires a structured value.`) const links = property.value.entries.map((entry) => { if (!entry || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} requires non-empty object ids.`) @@ -1175,7 +1182,7 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId const elementMap2 = mapResource === undefined ? '' : `` return `${elementMapXml}${elementMap2}` } - if (property.type === 'App::PropertyLinkSub') { + if (property.type === 'App::PropertyLinkSub' || property.type === 'App::PropertyLinkSubHidden') { let objectId = '' let subElements: string[] = [] const value = property.value @@ -1252,6 +1259,8 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId const element = ({ 'App::PropertyLink': 'Link', 'App::PropertyLinkSub': 'LinkSub', + 'App::PropertyLinkSubHidden': 'LinkSub', + 'App::PropertyLinkSubListGlobal': 'LinkSubList', 'App::PropertyLinkList': 'LinkList', 'App::PropertyLinkListHidden': 'LinkList', 'App::PropertyVector': 'PropertyVector', @@ -2214,13 +2223,13 @@ export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdD if (!property.resourcePath) return { value: null, decoded: true } return { value: property.includedFileResource ?? property.value, decoded: false, error: `Property ${property.name} bytes must be stored through project.fcstd.storeIncludedFiles before the value becomes editable.` } } - if (property.typeId === 'App::PropertyLinkSub' && property.subElements) return { + if ((property.typeId === 'App::PropertyLinkSub' || property.typeId === 'App::PropertyLinkSubHidden') && property.subElements) return { value: { schemaVersion: 1, objectId: property.value, subElements: [...property.subElements] }, decoded: true, } if (property.typeId === 'App::PropertyLink' || property.typeId === 'App::PropertyLinkHidden') return { value: property.value || null, decoded: true } if ((property.typeId === 'App::PropertyLinkList' || property.typeId === 'App::PropertyLinkListHidden') && property.links) return { value: [...property.links], decoded: true } - if (property.typeId === 'App::PropertyLinkSubList' && property.linkSubs) return { value: { schemaVersion: 1, entries: property.linkSubs.map((entry) => ({ ...entry })) }, decoded: true } + if ((property.typeId === 'App::PropertyLinkSubList' || property.typeId === 'App::PropertyLinkSubListGlobal') && property.linkSubs) return { value: { schemaVersion: 1, entries: property.linkSubs.map((entry) => ({ objectId: entry.objectId, subElement: property.typeId === 'App::PropertyLinkSubListGlobal' && entry.subElement === '' ? null : entry.subElement })) }, decoded: true } if (property.typeId === 'Part::PropertyPartShape' && property.shapeResource) return { value: { ...property.shapeResource, format: 'brep' }, decoded: true } if (property.typeId === 'Path::PropertyPath') { try { @@ -2231,7 +2240,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::PropertyLinkListHidden' || 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') { + if (property.typeId === 'App::PropertyLinkSub' || property.typeId === 'App::PropertyLinkSubHidden' || property.typeId === 'App::PropertyLinkSubList' || property.typeId === 'App::PropertyLinkSubListGlobal' || property.typeId === 'App::PropertyLinkList' || property.typeId === 'App::PropertyLinkListHidden' || 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 } @@ -2466,6 +2475,119 @@ export const rewriteFcstdLinkListHiddenProperty = (bytes: Uint8Array, edit: Fcst return rewritten } +/** Rewrites one native App::PropertyLinkSubHidden value without creating dependency metadata. */ +export const rewriteFcstdLinkSubHiddenProperty = (bytes: Uint8Array, edit: FcstdLinkSubHiddenEdit): Uint8Array => { + if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyLinkSubHidden rewrite requires non-empty object and property names.') + const validateValue = (value: LinkSubValue | null, context: string) => { + if (value === null) return + if (!value || typeof value !== 'object' || Array.isArray(value) || value.schemaVersion !== 1 || typeof value.objectId !== 'string' || !value.objectId.trim() || !Array.isArray(value.subElements)) throw new TypeError(`FCStd PropertyLinkSubHidden ${context} requires a structured LinkSub value.`) + if (value.subElements.length > 10_000) throw new RangeError(`FCStd PropertyLinkSubHidden ${context} contains too many sub-elements.`) + for (const entry of value.subElements) { + if (typeof entry === 'string') { + if (!entry.trim()) throw new TypeError(`FCStd PropertyLinkSubHidden ${context} contains an empty sub-element.`) + } else nativeLinkTopoRefName(entry, `FCStd PropertyLinkSubHidden ${context}`, value.objectId) + } + } + const equal = (left: LinkSubValue | null, right: LinkSubValue | null) => JSON.stringify(left) === JSON.stringify(right) + validateValue(edit.value, 'value') + if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue') + + const inspection = inspectFcstdArchive(bytes) + const object = inspection.objects.find((candidate) => candidate.name === edit.objectName) + if (!object) throw new Error(`FCStd PropertyLinkSubHidden object does not exist: ${edit.objectName}`) + const declaredNames = new Set(inspection.objects.map((candidate) => candidate.name)) + if (edit.value && !declaredNames.has(edit.value.objectId)) throw new RangeError(`FCStd PropertyLinkSubHidden target does not exist: ${edit.value.objectId}`) + if (edit.expectedValue?.objectId && !declaredNames.has(edit.expectedValue.objectId)) throw new RangeError(`FCStd PropertyLinkSubHidden expected target does not exist: ${edit.expectedValue.objectId}`) + const summary = object.properties.find((candidate) => candidate.name === edit.propertyName) + if (!summary) throw new Error(`FCStd PropertyLinkSubHidden property does not exist: ${edit.objectName}.${edit.propertyName}`) + if (summary.typeId !== 'App::PropertyLinkSubHidden' || summary.element !== 'LinkSub') throw new TypeError(`FCStd ${edit.objectName}.${edit.propertyName} must be App::PropertyLinkSubHidden with a LinkSub payload.`) + const decoded = decodeFcstdPropertyValue(summary) + if (!decoded.decoded || (decoded.value !== null && (!decoded.value || typeof decoded.value !== 'object'))) throw new TypeError(`FCStd PropertyLinkSubHidden ${edit.objectName}.${edit.propertyName} is not decodable.`) + if (edit.expectedValue !== undefined && !equal(decoded.value as LinkSubValue | null, edit.expectedValue)) throw new RangeError(`FCStd PropertyLinkSubHidden ${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 PropertyLinkSubHidden 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 PropertyLinkSubHidden 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::PropertyLinkSubHidden') + if (candidates.length !== 1 || !candidates[0].LinkSub || typeof candidates[0].LinkSub !== 'object' || Array.isArray(candidates[0].LinkSub)) throw new TypeError(`FCStd PropertyLinkSubHidden ${edit.objectName}.${edit.propertyName} has no unique LinkSub payload.`) + const payload = candidates[0].LinkSub as Record + const value = edit.value + payload['@_value'] = value?.objectId ?? '' + payload['@_count'] = String(value?.subElements.length ?? 0) + if (!value || value.subElements.length === 0) delete payload.Sub + else payload.Sub = value.subElements.map((entry) => ({ '@_value': typeof entry === 'string' ? entry : nativeLinkTopoRefName(entry, `FCStd PropertyLinkSubHidden ${edit.objectName}.${edit.propertyName}`, value.objectId) })) + 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 || !equal(rewrittenValue.value as LinkSubValue | null, edit.value)) throw new Error(`FCStd PropertyLinkSubHidden ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`) + return rewritten +} + +/** Rewrites one native App::PropertyLinkSubListGlobal value while preserving archive entries and dependency metadata. */ +export const rewriteFcstdLinkSubListGlobalProperty = (bytes: Uint8Array, edit: FcstdLinkSubListGlobalEdit): Uint8Array => { + if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyLinkSubListGlobal rewrite requires non-empty object and property names.') + const validateValue = (value: LinkSubListValue, context: string) => { + if (!value || typeof value !== 'object' || Array.isArray(value) || value.schemaVersion !== 1 || !Array.isArray(value.entries)) throw new TypeError(`FCStd PropertyLinkSubListGlobal ${context} requires a structured LinkSubList value.`) + if (value.entries.length > 10_000) throw new RangeError(`FCStd PropertyLinkSubListGlobal ${context} contains too many entries.`) + for (const [index, entry] of value.entries.entries()) { + if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd PropertyLinkSubListGlobal ${context} entry ${index} requires an object id.`) + if (entry.subElement !== null && (typeof entry.subElement !== 'string' ? nativeLinkTopoRefName(entry.subElement, `FCStd PropertyLinkSubListGlobal ${context}`, entry.objectId) : entry.subElement.trim())) continue + if (entry.subElement !== null) throw new TypeError(`FCStd PropertyLinkSubListGlobal ${context} entry ${index} contains an empty sub-element.`) + } + } + const equal = (left: LinkSubListValue, right: LinkSubListValue) => JSON.stringify(left) === JSON.stringify(right) + validateValue(edit.value, 'value') + if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue') + const inspection = inspectFcstdArchive(bytes) + const object = inspection.objects.find((candidate) => candidate.name === edit.objectName) + if (!object) throw new Error(`FCStd PropertyLinkSubListGlobal object does not exist: ${edit.objectName}`) + const declaredNames = new Set(inspection.objects.map((candidate) => candidate.name)) + for (const entry of edit.value.entries) if (!declaredNames.has(entry.objectId)) throw new RangeError(`FCStd PropertyLinkSubListGlobal target does not exist: ${entry.objectId}`) + if (edit.expectedValue && edit.expectedValue.entries.some((entry) => !declaredNames.has(entry.objectId))) throw new RangeError('FCStd PropertyLinkSubListGlobal expected target does not exist.') + const summary = object.properties.find((candidate) => candidate.name === edit.propertyName) + if (!summary || summary.typeId !== 'App::PropertyLinkSubListGlobal' || summary.element !== 'LinkSubList') throw new TypeError(`FCStd ${edit.objectName}.${edit.propertyName} must be App::PropertyLinkSubListGlobal with a LinkSubList payload.`) + const decoded = decodeFcstdPropertyValue(summary) + if (!decoded.decoded || !decoded.value || typeof decoded.value !== 'object' || Array.isArray(decoded.value)) throw new TypeError(`FCStd PropertyLinkSubListGlobal ${edit.objectName}.${edit.propertyName} is not decodable.`) + if (edit.expectedValue !== undefined && !equal(decoded.value as LinkSubListValue, edit.expectedValue)) throw new RangeError(`FCStd PropertyLinkSubListGlobal ${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 PropertyLinkSubListGlobal 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 PropertyLinkSubListGlobal 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::PropertyLinkSubListGlobal') + if (candidates.length !== 1 || !candidates[0].LinkSubList || typeof candidates[0].LinkSubList !== 'object' || Array.isArray(candidates[0].LinkSubList)) throw new TypeError(`FCStd PropertyLinkSubListGlobal ${edit.objectName}.${edit.propertyName} has no unique LinkSubList payload.`) + const payload = candidates[0].LinkSubList as Record + payload['@_count'] = String(edit.value.entries.length) + payload.Link = edit.value.entries.map((entry) => ({ '@_obj': entry.objectId, '@_sub': entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : nativeLinkTopoRefName(entry.subElement, `FCStd PropertyLinkSubListGlobal ${edit.objectName}.${edit.propertyName}`, entry.objectId) })) + 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 || !rewrittenValue.value || !equal(rewrittenValue.value as LinkSubListValue, edit.value)) throw new Error(`FCStd PropertyLinkSubListGlobal ${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.') diff --git a/src/facade/index.ts b/src/facade/index.ts index 515c43c..442c07f 100644 --- a/src/facade/index.ts +++ b/src/facade/index.ts @@ -105,8 +105,8 @@ export type { AttachmentFrame, AttachmentMapMode, AttachmentSupport } from './at export { redirectBodyTips, resolveBodyTip } from './bodyRules' export type { BodyFeatureState } from './bodyRules' export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine' -export { DEFAULT_FCSTD_LIMITS, decodeFcstdPathProperty, decodeFcstdPropertyValue, encodeFcstdPathResource, extractFcstdShapeResources, inspectFcstdArchive, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdLinkListHiddenProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeElementMap2Resource, serializeFcstdMetadataArchive, serializeStringHasherTableResource, storeFcstdShapeResources } from './fcstd' -export type { FcstdArchiveLimits, FcstdBoolListEdit, FcstdColorEdit, FcstdColorListEdit, FcstdCompatibilityReport, FcstdDecodedPropertyValue, FcstdDirectionEdit, FcstdElementMapResource, FcstdEntryMetadata, FcstdEntryRole, FcstdFileEdit, FcstdGuiInspection, FcstdIncludedFileEdit, FcstdIncludedFileResourceEdit, FcstdInspection, FcstdLinkListHiddenEdit, FcstdMeasureAreaEdit, FcstdNumericPropertyEdit, FcstdObjectSummary, FcstdObjectSupport, FcstdPathEdit, FcstdPropertySummary, FcstdShapeResource, FcstdShapeResourcePayload, FcstdStoredShapeResource, FcstdStringHasherResource, FcstdWriteOptions } from './fcstd' +export { DEFAULT_FCSTD_LIMITS, decodeFcstdPathProperty, decodeFcstdPropertyValue, encodeFcstdPathResource, extractFcstdShapeResources, inspectFcstdArchive, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdLinkListHiddenProperty, rewriteFcstdLinkSubHiddenProperty, rewriteFcstdLinkSubListGlobalProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeElementMap2Resource, serializeFcstdMetadataArchive, serializeStringHasherTableResource, storeFcstdShapeResources } from './fcstd' +export type { FcstdArchiveLimits, FcstdBoolListEdit, FcstdColorEdit, FcstdColorListEdit, FcstdCompatibilityReport, FcstdDecodedPropertyValue, FcstdDirectionEdit, FcstdElementMapResource, FcstdEntryMetadata, FcstdEntryRole, FcstdFileEdit, FcstdGuiInspection, FcstdIncludedFileEdit, FcstdIncludedFileResourceEdit, FcstdInspection, FcstdLinkListHiddenEdit, FcstdLinkSubHiddenEdit, FcstdLinkSubListGlobalEdit, FcstdMeasureAreaEdit, FcstdNumericPropertyEdit, FcstdObjectSummary, FcstdObjectSupport, FcstdPathEdit, FcstdPropertySummary, FcstdShapeResource, FcstdShapeResourcePayload, FcstdStoredShapeResource, FcstdStringHasherResource, FcstdWriteOptions } from './fcstd' export { cloneStringHasherTable, migrateStringHasherSchema, parseStringHasherTable, StringHasherFlag, validateElementMap2StringHasherEvidence, validateStringHasherTable, writeStringHasherTable } from './stringHasher' export type { AnyStringHasherTable, StringHasherEntry, StringHasherTable, StringHasherTableV1, StringHasherValidationIssue, StringHasherValidationReport, ElementMap2StringHasherEvidenceIssue } from './stringHasher' export { appendElementMap2NameEntry, canonicalizeElementMap2, cloneElementMap2, createElementMap2MultiStageNameMapping, createElementMap2NameEntry, createElementMap2NameToken, elementMap2NameTokenToReference, elementMap2SemanticDigest, formatElementMap2NameToken, migrateElementMap2Schema, parseElementMap2, parseElementMap2MultiStageNameMapping, validateElementMap2, validateElementMap2MultiStageNameMapping, writeElementMap2, writeElementMap2MultiStageNameMapping } from './elementMap2' diff --git a/src/facade/mockFacade.ts b/src/facade/mockFacade.ts index 69e674b..c48b3f0 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, rewriteFcstdFontProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdIntegerSetProperty, rewriteFcstdLinkListHiddenProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeFcstdMetadataArchive, storeFcstdIncludedFileResources, storeFcstdShapeResources } from './fcstd' +import { decodeFcstdPathProperty, extractFcstdIncludedFileResources, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdFontProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdIntegerSetProperty, rewriteFcstdLinkListHiddenProperty, rewriteFcstdLinkSubHiddenProperty, rewriteFcstdLinkSubListGlobalProperty, 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' @@ -367,7 +367,7 @@ const collectDependencyEdges = (document: Pick): De else for (const [index, entry] of property.value.subElements.entries()) edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: typeof entry === 'string' ? 'link' : 'topo-ref', propertyName: property.name, reference: typeof entry === 'string' ? `${property.name}[${index}]` : entry.persistentId }) } else edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: property.value.persistentId }) } - if (property.type === 'App::PropertyLinkSubList' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) { + if ((property.type === 'App::PropertyLinkSubList' || property.type === 'App::PropertyLinkSubListGlobal') && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) { for (const [index, entry] of property.value.entries.entries()) { if (!objectIds.has(entry.objectId)) continue const subElement = entry.subElement @@ -643,7 +643,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])])) if (value !== null && !knownIds.has(value)) throw new RangeError(`${property.label} target does not exist in this document.`) } - if (property.type === 'App::PropertyLinkSub') { + if (property.type === 'App::PropertyLinkSub' || property.type === 'App::PropertyLinkSubHidden') { if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new TypeError(`${property.label} requires a TopoRef value.`) if (value !== null) { if ('subElements' in value) { @@ -658,7 +658,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope if (topoRef.objectId !== value.objectId) throw new RangeError(`${property.label} sub-elements must target the same object.`) return `${topoRef.kind}:${topoRef.persistentId}` }) - if (new Set(identities).size !== identities.length) throw new RangeError(`${property.label} sub-elements must be unique.`) + if (property.type === 'App::PropertyLinkSub' && new Set(identities).size !== identities.length) throw new RangeError(`${property.label} sub-elements must be unique.`) if (!document.objects.some((object) => object.id === value.objectId)) throw new RangeError(`${property.label} target does not exist in this document.`) } else { const topoRef = parseTopoRef(JSON.stringify(value)) @@ -666,7 +666,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope } } } - if (property.type === 'App::PropertyLinkSubList') { + if (property.type === 'App::PropertyLinkSubList' || property.type === 'App::PropertyLinkSubListGlobal') { if (!value || typeof value !== 'object' || Array.isArray(value) || !('entries' in value) || value.schemaVersion !== 1 || !Array.isArray(value.entries)) throw new TypeError(`${property.label} requires a valid LinkSubList value.`) if (value.entries.length > 10_000) throw new RangeError(`${property.label} contains too many links.`) const knownIds = new Set(document.objects.map((object) => object.id)) @@ -682,7 +682,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope if (topoRef.objectId !== entry.objectId) throw new RangeError(`${property.label} entry ${index} TopoRef targets a different object.`) return `${entry.objectId}:${topoRef.kind}:${topoRef.persistentId}` }) - if (new Set(identities).size !== identities.length) throw new RangeError(`${property.label} entries must be unique.`) + if (property.type === 'App::PropertyLinkSubList' && new Set(identities).size !== identities.length) throw new RangeError(`${property.label} entries must be unique.`) } if (property.name === 'Label' && !(value as string).trim()) throw new RangeError('Label cannot be empty.') } @@ -918,7 +918,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW objectSnapshot.typeId = 'PartDesign::ShapeBinder' objectSnapshot.properties = [ ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'PartDesign::ShapeBinder' } : property), - { name: 'Support', label: 'Support', group: 'Binder', scope: 'data', type: 'App::PropertyLinkSub', value: support, recompute: true }, + { name: 'Support', label: 'Support', group: '', scope: 'data', type: 'App::PropertyLinkSubListGlobal', value: { schemaVersion: 1, entries: 'subElements' in support ? (support.subElements.length > 0 ? support.subElements.map((subElement) => ({ objectId: support.objectId, subElement })) : [{ objectId: support.objectId, subElement: null }]) : [{ objectId: support.objectId, subElement: support }] }, recompute: true }, { name: 'BindMode', label: 'Bind mode', group: 'Binder', scope: 'data', type: 'App::PropertyEnumeration', value: 'Synchronized', options: ['Synchronized', 'Frozen'], recompute: true }, { name: 'TraceSupport', label: 'Trace support', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true }, { name: 'ClaimChildren', label: 'Claim children', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true }, @@ -1746,6 +1746,8 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW rewriteBoolList: (bytes, edit) => rewriteFcstdBoolListProperty(bytes, edit), rewriteIntegerSet: (bytes, edit) => rewriteFcstdIntegerSetProperty(bytes, edit), rewriteLinkListHidden: (bytes, edit) => rewriteFcstdLinkListHiddenProperty(bytes, edit), + rewriteLinkSubHidden: (bytes, edit) => rewriteFcstdLinkSubHiddenProperty(bytes, edit), + rewriteLinkSubListGlobal: (bytes, edit) => rewriteFcstdLinkSubListGlobalProperty(bytes, edit), rewriteColor: (bytes, edit) => rewriteFcstdColorProperty(bytes, edit), rewriteColorList: (bytes, edit) => rewriteFcstdColorListProperty(bytes, edit), rewriteDirection: (bytes, edit) => rewriteFcstdDirectionProperty(bytes, edit), diff --git a/src/facade/topologyReferences.ts b/src/facade/topologyReferences.ts index 5de1b26..6a413f4 100644 --- a/src/facade/topologyReferences.ts +++ b/src/facade/topologyReferences.ts @@ -225,7 +225,7 @@ export const migrateDocumentTopologyReferences = ( property.value = { ...supportValue, subElement: migrate(object.id, property.name, supportSubElement as PersistedTopoRef) } as AttachmentSupportValue continue } - if (property.type === 'App::PropertyLinkSubList' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) { + if ((property.type === 'App::PropertyLinkSubList' || property.type === 'App::PropertyLinkSubListGlobal') && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) { property.value = { ...property.value, entries: property.value.entries.map((entry, index) => ({ ...entry, subElement: entry.subElement && typeof entry.subElement === 'object' ? migrate(object.id, `${property.name}[${index}]`, entry.subElement) : entry.subElement })) } as LinkSubListValue continue } @@ -255,7 +255,7 @@ export const resolveDocumentTopologyReference = ( const external = owner.sketch?.externalGeometry.find((candidate) => candidate.id === externalId) if (external) { current = external.source; replace = (record) => { external.source = record } } } else { - const property = owner.properties.find((candidate) => candidate.name === input.referenceName && (candidate.type === 'App::PropertyLinkSub' || candidate.type === 'App::PropertyLinkSubList')) + const property = owner.properties.find((candidate) => candidate.name === input.referenceName && (candidate.type === 'App::PropertyLinkSub' || candidate.type === 'App::PropertyLinkSubList' || candidate.type === 'App::PropertyLinkSubListGlobal')) if (property?.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'schemaVersion' in property.value) { if ('entries' in property.value) { const collection = property.value diff --git a/src/facade/types.ts b/src/facade/types.ts index 716a351..17f5b92 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, FcstdFontEdit, FcstdIncludedFileEdit, FcstdIncludedFileResourcePayload, FcstdInspection, FcstdInstantiatedShape, FcstdIntegerSetEdit, FcstdLinkListHiddenEdit, FcstdMeasureAreaEdit, FcstdNumericPropertyEdit, FcstdPathEdit, FcstdShapeResourcePayload, FcstdStoredIncludedFileResource, FcstdStoredShapeResource, FcstdWriteOptions } from './fcstd' +import type { FcstdArchiveLimits, FcstdBoolListEdit, FcstdColorEdit, FcstdColorListEdit, FcstdDirectionEdit, FcstdFileEdit, FcstdFontEdit, FcstdIncludedFileEdit, FcstdIncludedFileResourcePayload, FcstdInspection, FcstdInstantiatedShape, FcstdIntegerSetEdit, FcstdLinkListHiddenEdit, FcstdLinkSubHiddenEdit, FcstdLinkSubListGlobalEdit, 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' @@ -107,7 +107,7 @@ export type PropertyValue = string | number | boolean | string[] | number[] | bo export type NativeEditablePropertyType = | '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::PropertyLink' | 'App::PropertyLinkHidden' | 'App::PropertyLinkSub' | 'App::PropertyLinkSubHidden' | 'App::PropertyLinkSubList' | 'App::PropertyLinkSubListGlobal' | 'App::PropertyLinkList' | 'App::PropertyLinkListHidden' | 'App::PropertyStringList' | 'App::PropertyFloatList' | 'App::PropertyIntegerList' | 'App::PropertyIntegerSet' | 'App::PropertyVector' | 'App::PropertyDirection' | 'App::PropertyFile' | 'App::PropertyFileIncluded' | 'App::PropertyFont' | 'App::PropertyPlacement' | 'App::PropertyMultiTransform' | 'Path::PropertyPath' | 'Part::PropertyPartShape' @@ -1031,6 +1031,8 @@ export interface BitBybitWebCadFacade { rewriteBoolList(bytes: Uint8Array, edit: FcstdBoolListEdit): Uint8Array rewriteIntegerSet(bytes: Uint8Array, edit: FcstdIntegerSetEdit): Uint8Array rewriteLinkListHidden(bytes: Uint8Array, edit: FcstdLinkListHiddenEdit): Uint8Array + rewriteLinkSubHidden(bytes: Uint8Array, edit: FcstdLinkSubHiddenEdit): Uint8Array + rewriteLinkSubListGlobal(bytes: Uint8Array, edit: FcstdLinkSubListGlobalEdit): Uint8Array rewriteColor(bytes: Uint8Array, edit: FcstdColorEdit): Uint8Array rewriteColorList(bytes: Uint8Array, edit: FcstdColorListEdit): Uint8Array rewriteDirection(bytes: Uint8Array, edit: FcstdDirectionEdit): Uint8Array diff --git a/tests/facade.test.ts b/tests/facade.test.ts index 5405a3c..7be6de3 100644 --- a/tests/facade.test.ts +++ b/tests/facade.test.ts @@ -5848,7 +5848,7 @@ test('Datum and ShapeBinder stay in the active Body without stealing its Tip and facade.task.apply() const binder = facade.app.document.getObject('shape-binder') assert.equal(binder?.typeId, 'PartDesign::ShapeBinder') - assert.deepEqual(binder?.properties.find((property) => property.name === 'Support')?.value, { schemaVersion: 1, objectId: 'pad', subElements: [] }) + assert.deepEqual(binder?.properties.find((property) => property.name === 'Support')?.value, { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: null }] }) assert.equal(facade.app.document.getActive().tree.find((item) => item.id === 'body001')?.children?.includes('shape-binder'), true) assert.equal(facade.app.document.getObject('body001')?.properties.find((property) => property.name === 'Tip')?.value, null) assert.deepEqual(facade.app.document.getDependencies().filter((edge) => edge.sourceId === 'shape-binder').map((edge) => edge.targetId), ['pad']) diff --git a/tests/propertyLinkSubHiddenFacade.test.ts b/tests/propertyLinkSubHiddenFacade.test.ts new file mode 100644 index 0000000..0ca14df --- /dev/null +++ b/tests/propertyLinkSubHiddenFacade.test.ts @@ -0,0 +1,53 @@ +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 type { DocumentSnapshot, LinkSubValue } from '../src/facade/types' + +const hiddenStatus = encodeFreecadPropertyStatus(['PropHidden']) +const initialValue: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceA', subElements: ['Face1', 'Face3'] } +const editedValue: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] } + +const fixture = (): DocumentSnapshot => ({ + id: 'property-linksubhidden-facade', label: 'PropertyLinkSubHidden Facade', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [ + { id: 'LinkProbe', label: 'Link probe', type: 'feature', state: 'valid' }, + { id: 'ColorSourceA', label: 'Color source A', type: 'feature', state: 'valid' }, + { id: 'ColorSourceB', label: 'Color source B', type: 'feature', state: 'valid' }, + ], + objects: [ + { id: 'LinkProbe', typeId: 'App::Link', properties: [{ name: 'ColoredElements', label: 'Colored elements', group: ' Link', scope: 'data', type: 'App::PropertyLinkSubHidden', value: initialValue, nativeStatus: hiddenStatus, recompute: true }] }, + { id: 'ColorSourceA', typeId: 'Part::Feature', properties: [] }, + { id: 'ColorSourceB', typeId: 'Part::Feature', properties: [] }, + ], + dependencies: [], + recompute: { generation: 0, status: 'idle', objectStates: { LinkProbe: 'up-to-date', ColorSourceA: 'up-to-date', ColorSourceB: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) + +const valueOf = (facade: ReturnType) => facade.app.document.getObject('LinkProbe')?.properties.find((property) => property.name === 'ColoredElements')?.value + +test('App::PropertyLinkSubHidden preserves structured references without dependency edges', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['LinkProbe'], runtimeMode: 'mock' }) + facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: editedValue }) + assert.deepEqual(valueOf(facade), editedValue) + assert.deepEqual(facade.app.document.getDependencies(), []) + assert.equal(facade.app.document.getActive().recompute?.objectStates.LinkProbe, 'touched') + assert.throws(() => facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: { schemaVersion: 1, objectId: 'Missing', subElements: ['Face1'] } as LinkSubValue }), /target does not exist/) + assert.deepEqual(valueOf(facade), editedValue) + + const archive = facade.project.fcstd.serializeMetadata(facade.app.document.getActive()) + const xml = new TextDecoder().decode(unzipSync(archive)['Document.xml']) + assert.match(xml, new RegExp(`<\\/LinkSub><\\/Property>`)) + assert.doesNotMatch(xml, /]+(?:group|doc|attr|ro|hide)=/) + assert.doesNotMatch(xml, /<\/LinkSubList><\/Property>/) + const summary = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') + assert.deepEqual({ typeId: summary?.typeId, element: summary?.element, linkSubs: summary?.linkSubs }, { typeId: 'App::PropertyLinkSubListGlobal', element: 'LinkSubList', linkSubs: [{ objectId: 'SecondBox', subElement: '' }, { objectId: 'SecondBox', subElement: 'Face1' }] }) + assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: editedValue, decoded: true }) + const rewritten = facade.project.fcstd.rewriteLinkSubListGlobal(archive, { objectName: 'ShapeBinderProbe', propertyName: 'Support', value: initialValue, expectedValue: editedValue }) + const rewrittenSummary = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support') + assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: initialValue, decoded: true }) + await facade.project.dispose() +}) diff --git a/tests/propertyLinkSubListGlobalTransaction.test.ts b/tests/propertyLinkSubListGlobalTransaction.test.ts new file mode 100644 index 0000000..a5e3e79 --- /dev/null +++ b/tests/propertyLinkSubListGlobalTransaction.test.ts @@ -0,0 +1,31 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createWebCadFacade } from '../src/facade/mockFacade' +import type { DocumentSnapshot, LinkSubListValue } from '../src/facade/types' + +const initialValue: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SourceBox', subElement: 'Face1' }, { objectId: 'SourceBox', subElement: 'Face2' }, { objectId: 'SecondBox', subElement: 'Face1' }] } +const editedValue: LinkSubListValue = { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: null }, { objectId: 'SecondBox', subElement: 'Face1' }] } +const fixture = (): DocumentSnapshot => ({ + id: 'property-linksublistglobal-transaction', label: 'PropertyLinkSubListGlobal Transaction', version: 1, dirty: false, readOnly: false, units: 'mm', + tree: [{ id: 'ShapeBinderProbe', label: 'Shape binder probe', type: 'feature', state: 'valid' }, { id: 'SourceBox', label: 'Source box', type: 'feature', state: 'valid' }, { id: 'SecondBox', label: 'Second box', type: 'feature', state: 'valid' }], + objects: [{ id: 'ShapeBinderProbe', typeId: 'PartDesign::ShapeBinder', properties: [{ name: 'Support', label: 'Support', group: '', scope: 'data', type: 'App::PropertyLinkSubListGlobal', value: initialValue, recompute: true }] }, { id: 'SourceBox', typeId: 'Part::Feature', properties: [] }, { id: 'SecondBox', typeId: 'Part::Feature', properties: [] }], + dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: { ShapeBinderProbe: 'up-to-date', SourceBox: 'up-to-date', SecondBox: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }, +}) +const valueOf = (facade: ReturnType) => facade.app.document.getObject('ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support')?.value as LinkSubListValue + +test('App::PropertyLinkSubListGlobal closes undo, redo, stale, abort and resource ownership', async () => { + const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' }) + const resourcesBefore = facade.geometry.capabilities() + facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: editedValue, expectedDocumentVersion: 1 }) + editedValue.entries[0].subElement = 'Face2' + assert.deepEqual(valueOf(facade), { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: null }, { objectId: 'SecondBox', subElement: 'Face1' }] }) + facade.history.undo(); assert.deepEqual(valueOf(facade), initialValue); assert.equal(facade.history.canRedo(), true) + facade.history.redo(); assert.deepEqual(valueOf(facade), { schemaVersion: 1, entries: [{ objectId: 'SecondBox', subElement: null }, { objectId: 'SecondBox', subElement: 'Face1' }] }) + const stable = JSON.stringify(facade.app.document.getActive()) + assert.throws(() => facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: initialValue, expectedDocumentVersion: 1 }), /Stale document version/) + const cancelled = new AbortController(); cancelled.abort(); assert.throws(() => facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: initialValue, signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError') + assert.throws(() => facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: { schemaVersion: 1, entries: [{ objectId: 'Missing', subElement: null }] } }), /target does not exist/) + assert.equal(JSON.stringify(facade.app.document.getActive()), stable) + 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() +})