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

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

View File

@@ -0,0 +1,5 @@
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><title>Chrome PropertyLinkSubHidden harness</title></head>
<body><main id="root">Initializing PropertyLinkSubHidden harness.</main><script type="module" src="/src/chromePropertyLinkSubHiddenHarness.tsx"></script></body>
</html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><title>Chrome PropertyLinkSubListGlobal harness</title></head><body><main id="root">Initializing PropertyLinkSubListGlobal harness.</main><script type="module" src="/src/chromePropertyLinkSubListGlobalHarness.tsx"></script></body></html>

View File

@@ -1059,8 +1059,8 @@
},
{
"path": "src/facade/mockFacade.ts",
"bytes": 172346,
"sha256": "100b84bd3ebfe1a620885d70e98e97c864f33f5d478126c8586cca6deddfd0ac"
"bytes": 174111,
"sha256": "d84691f3e1fb9d7ec522fdcabe93386158e12d567bf24d1e43de0f5ee369114c"
},
{
"path": "src/facade/geometryRuntime.ts",

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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"
],

View File

@@ -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"]
}
]
}

View File

@@ -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"
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}

View File

@@ -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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
]
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}
}

View File

@@ -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"
}

View File

@@ -24,7 +24,7 @@
"wrong-document-root": 77
},
"timing": {
"p95Ms": 0.135,
"maxMs": 2.707
"p95Ms": 0.123,
"maxMs": 2.653
}
}

View File

@@ -41,7 +41,7 @@
"zero-revolution-axis": 67
},
"timing": {
"p95Ms": 0.021,
"maxMs": 1.057
"p95Ms": 0.02,
"maxMs": 0.499
}
}

View File

@@ -22,7 +22,7 @@
"invalid": 800
},
"timing": {
"p95Ms": 0.04,
"maxMs": 0.907
"p95Ms": 0.029,
"maxMs": 0.77
}
}

View File

@@ -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=="
}
}

View File

@@ -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` XMLUndo/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 typesHidden scope0 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 dispose3 条初始引用编辑为 2 条后,文档版本为 2、对象状态为 `touched`、依赖边为 2Shape/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 typeGlobal scope2 runtime dependency edgesnative-editable 总计 37 types/4377 recordsopaque 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` 均通过。

View File

@@ -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
- Property40/85 native types / 45 opaque types
- Property42/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 | `-` |

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = <span className="property-readonly" title={value?.path}>{value?.path ?? 'Empty Shape'}</span>
} 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 ? <div className="property-link-sub property-link-sub-list"><div className="property-link-sub-items">{value.entries.map((entry, index) => <div className="property-link-sub-item" key={`${entry.objectId}:${typeof entry.subElement === 'string' ? entry.subElement : entry.subElement?.persistentId ?? index}`}><span title={typeof entry.subElement === 'string' ? entry.subElement : entry.subElement?.persistentId}>{entry.objectId} · {typeof entry.subElement === 'string' ? entry.subElement || 'whole object' : entry.subElement ? `${entry.subElement.kind} · ${entry.subElement.status}` : 'whole object'}</span></div>)}</div><IconButton icon={X} label="Clear subshape links" onClick={() => commit({ schemaVersion: 1, entries: [] })} /></div> : <span className="property-readonly">None</span>
} else if (property.type === 'App::PropertyLinkSub') {

View File

@@ -51,7 +51,7 @@ const run = async (): Promise<SemanticReport> => {
...(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

View File

@@ -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 <section className="combo-property" data-harness-status="ready"><PropertyPanel facade={facade} objectId="LinkGroupProbe" scope="data" showNotice={(message) => errors.push(message)} /></section> }
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(<HarnessPanel facade={facade} errors={errors} />)
try {
await waitFor(() => document.querySelector<HTMLInputElement>('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<HTMLInputElement>('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 {}

View File

@@ -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<string, unknown>; persistence?: Record<string, unknown>; resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }; worker?: Trace; release?: Record<string, unknown>; 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 <section className="combo-property" data-harness-status="ready"><PropertyPanel facade={facade} objectId="ShapeBinderProbe" scope="data" showNotice={(message) => errors.push(message)} /></section> }
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(<HarnessPanel facade={facade} errors={errors} />)
try {
await waitFor(() => document.querySelector<HTMLInputElement>('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<HTMLInputElement>('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 {}

View File

@@ -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<string, ReadonlySet<string>>([
['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) => `<Link value="${xmlEscape(entry)}"/>`).join('')
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><LinkList count="${property.value.length}">${links}</LinkList></Property>`
}
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 ? '' : `<ElementMap2 file="${xmlEscape(mapResource)}"/>`
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Part${hasherIndexAttribute}${elementMap} file="${xmlEscape(property.value.path)}"/>${elementMapXml}${elementMap2}</Property>`
}
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<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown>
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<string, Uint8Array> = {}
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<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | 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<string, unknown>
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<string, Uint8Array> = {}
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.')

View File

@@ -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'

View File

@@ -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<DocumentSnapshot, 'objects'>): 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),

View File

@@ -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

View File

@@ -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

View File

@@ -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'])

View File

@@ -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<typeof createWebCadFacade>) => 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(`<Property name="ColoredElements" type="App::PropertyLinkSubHidden" status="${hiddenStatus}"><LinkSub value="ColorSourceB" count="1"><Sub value="Face5"/><\\/LinkSub><\\/Property>`))
assert.doesNotMatch(xml, /<Property name="ColoredElements"[^>]+(?:group|doc|attr|ro|hide)=/)
assert.doesNotMatch(xml, /<ObjectDeps Name="LinkProbe" Count="[1-9]/)
const summary = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'LinkProbe')?.properties.find(({ name }) => name === 'ColoredElements')
assert.deepEqual({ typeId: summary?.typeId, element: summary?.element, nativeStatus: summary?.nativeStatus, statusNames: summary?.statusNames }, { typeId: 'App::PropertyLinkSubHidden', element: 'LinkSub', nativeStatus: hiddenStatus, statusNames: ['PropHidden'] })
assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: editedValue, decoded: true })
const rewritten = facade.project.fcstd.rewriteLinkSubHidden(archive, { objectName: 'LinkProbe', propertyName: 'ColoredElements', value: initialValue, expectedValue: editedValue })
const rewrittenSummary = facade.project.fcstd.inspect(rewritten).objects.find(({ name }) => name === 'LinkProbe')?.properties.find(({ name }) => name === 'ColoredElements')
assert.deepEqual(decodeFcstdPropertyValue(rewrittenSummary!), { value: initialValue, decoded: true })
await facade.project.dispose()
})

View File

@@ -0,0 +1,67 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createWebCadFacade } from '../src/facade/mockFacade'
import { encodeFreecadPropertyStatus } from '../src/facade/propertyStatus'
import type { DocumentSnapshot, LinkSubValue } from '../src/facade/types'
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-transaction', label: 'PropertyLinkSubHidden Transaction', 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: encodeFreecadPropertyStatus(['PropHidden']), 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<typeof createWebCadFacade>) => facade.app.document.getObject('LinkProbe')?.properties.find((property) => property.name === 'ColoredElements')?.value as LinkSubValue
test('App::PropertyLinkSubHidden transaction closes undo, redo, stale, cancel, failure, hidden dependencies and resources', async () => {
const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' })
const resourcesBefore = facade.geometry.capabilities()
const requested: LinkSubValue = { schemaVersion: 1, objectId: 'ColorSourceB', subElements: ['Face5'] }
facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: requested, expectedDocumentVersion: 1 })
requested.subElements[0] = 'Face1'
assert.deepEqual(valueOf(facade), editedValue)
const exposed = valueOf(facade)
exposed.subElements[0] = 'Face1'
assert.deepEqual(valueOf(facade), editedValue)
assert.equal(facade.app.document.getActive().version, 2)
assert.equal(facade.app.document.getActive().dirty, true)
assert.equal(facade.app.document.getActive().recompute?.objectStates.LinkProbe, 'touched')
assert.deepEqual(facade.app.document.getDependencies(), [])
facade.history.undo()
assert.deepEqual(valueOf(facade), initialValue)
assert.equal(facade.app.document.getActive().version, 1)
assert.equal(facade.history.canUndo(), false)
assert.equal(facade.history.canRedo(), true)
facade.history.redo()
assert.deepEqual(valueOf(facade), editedValue)
assert.equal(facade.app.document.getActive().version, 2)
assert.equal(facade.history.canUndo(), true)
assert.equal(facade.history.canRedo(), false)
const stable = JSON.stringify(facade.app.document.getActive())
assert.throws(() => facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: editedValue, expectedDocumentVersion: 1 }), /Stale document version: expected 1, current 2/)
const cancelled = new AbortController()
cancelled.abort()
assert.throws(() => facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: initialValue, expectedDocumentVersion: 2, signal: cancelled.signal }), (error: unknown) => error instanceof DOMException && error.name === 'AbortError')
assert.throws(() => facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: { schemaVersion: 1, objectId: 'Missing', subElements: ['Face1'] } as LinkSubValue }), /target does not exist/)
assert.throws(() => facade.app.document.setProperty({ objectId: 'LinkProbe', propertyName: 'ColoredElements', value: { schemaVersion: 1, objectId: 'ColorSourceA', subElements: [1] } as unknown as LinkSubValue }), /TopoRef must be a JSON object/)
assert.equal(JSON.stringify(facade.app.document.getActive()), stable)
assert.equal(facade.history.canUndo(), true)
assert.equal(facade.history.canRedo(), false)
const resourcesAfter = facade.geometry.capabilities()
assert.deepEqual({ shapeCount: resourcesAfter.shapeCount, kernelReferenceCount: resourcesAfter.kernelReferenceCount, releasedShapeCount: resourcesAfter.releasedShapeCount }, { shapeCount: resourcesBefore.shapeCount, kernelReferenceCount: resourcesBefore.kernelReferenceCount, releasedShapeCount: resourcesBefore.releasedShapeCount })
await facade.project.dispose()
})

View File

@@ -0,0 +1,45 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
import { decodeFcstdPropertyValue } from '../src/facade/fcstd'
import type { DocumentSnapshot, 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-facade', label: 'PropertyLinkSubListGlobal Facade', 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<typeof createWebCadFacade>) => facade.app.document.getObject('ShapeBinderProbe')?.properties.find(({ name }) => name === 'Support')?.value as LinkSubListValue
test('App::PropertyLinkSubListGlobal preserves ordered duplicate references and real dependencies', async () => {
const facade = createWebCadFacade({ initialDocument: fixture(), runtimeMode: 'mock' })
facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: editedValue })
assert.deepEqual(valueOf(facade), editedValue)
assert.deepEqual(facade.app.document.getDependencies().map(({ targetId, relation, propertyName, reference }) => ({ targetId, relation, propertyName, reference })), [
{ targetId: 'SecondBox', relation: 'link', propertyName: 'Support', reference: 'Support[0]' },
{ targetId: 'SecondBox', relation: 'link', propertyName: 'Support', reference: 'Support[1]' },
])
assert.equal(facade.app.document.getActive().recompute?.objectStates.ShapeBinderProbe, 'touched')
assert.throws(() => facade.app.document.setProperty({ objectId: 'ShapeBinderProbe', propertyName: 'Support', value: { schemaVersion: 1, entries: [{ objectId: 'Missing', subElement: null }] } }), /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, /<Property name="Support" type="App::PropertyLinkSubListGlobal"><LinkSubList count="2"><Link obj="SecondBox" sub=""\/><Link obj="SecondBox" sub="Face1"\/><\/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()
})

View File

@@ -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<typeof createWebCadFacade>) => 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()
})