feat: advance FreeCAD exact parity evidence
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-14 22:39:16 -04:00
parent e3373c9d6c
commit 5bbd7b9d4f
64 changed files with 113069 additions and 21074 deletions

View File

@@ -11,6 +11,7 @@ FreeCAD-aligned web CAD frontend baseline for the BitBybit runtime boundary.
- FreeCAD-style application menus, workbench navigation, Combo View, Task Dock, viewport and Report view
- Machine-readable workbench and command manifest in `src/freecadManifest.ts`
- Current status, source-of-truth order and focused execution queue in [docs/current-work.zh-CN.md](docs/current-work.zh-CN.md)
- Re-audited completion status, critical path and smallest executable milestones in [docs/freecad-web-execution-master-plan.zh-CN.md](docs/freecad-web-execution-master-plan.zh-CN.md)
- Chinese implementation plan in `docs/web-cad-implementation-plan.zh-CN.md`
- Full 34-module FreeCAD 1.1.1 scope, Bitbybit/OCCT source-build track and executable task ledger in [docs/freecad-complete-parity-backlog.zh-CN.md](docs/freecad-complete-parity-backlog.zh-CN.md)
- Executable reference-oracle and R0-R9 delivery order in [docs/freecad-reference-execution-roadmap.zh-CN.md](docs/freecad-reference-execution-roadmap.zh-CN.md)

View File

@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"status": "failed",
"status": "pass",
"stages": [
"CAD",
"OCL",
@@ -29,7 +29,105 @@
"hasC359": true,
"hasC361": true
},
"error": "Error: LinuxCNC WASM bridge timed out waiting for parser/controller acceptance.\n at http://127.0.0.1:5201/src/facade/camPipeline.ts:103:18",
"machine": {
"backend": "linuxcnc-wasm",
"status": "accepted",
"dryRunStatus": "dry-run",
"parserAuthority": "linuxcnc-wasm",
"machineCase": "axis-vismach-5axis-table-rotary-tilting-xyzbc-trt",
"dryRunMachineCase": "axis-vismach-5axis-table-rotary-tilting-xyzbc-trt",
"lines": 65,
"blocks": 138,
"trajectorySegments": 48
},
"runtime": {
"server": "managed-vite-preview",
"worktreeRevision": "58f50e6d66287bc31e6d906e5c33658a348658e4",
"machineCase": "axis-vismach-5axis-table-rotary-tilting-xyzbc-trt",
"artifacts": [
{
"role": "entry-html",
"path": "index.html",
"runtimeLoaded": true,
"bytes": 61728,
"sha256": "7087d2ccfb9eb2b4967fa75a7d4069cc97ab62a58d4af4f18a7a449ddc1aa536"
},
{
"role": "case-registry",
"path": "linuxcnc/cases.json",
"runtimeLoaded": true,
"bytes": 2108501,
"sha256": "b9c614d3f139de57277d02944a3cb8d0e54f68da054021ac375be9ffbb4f95db"
},
{
"role": "machine-config",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"runtimeLoaded": true,
"bytes": 7179,
"sha256": "43c26a58030b6ec4a4319cde23e847cff0134e9f2777eef30848aa477bb87d38"
},
{
"role": "application",
"path": "assets/simulator-D-5Qaz4x.js",
"runtimeLoaded": true,
"bytes": 852452,
"sha256": "a429e0b7297fb169255a30acc48274ed17bbeb168fc1d90a13cf4b4e2c6db42f"
},
{
"role": "controller-worker",
"path": "assets/controller.worker-C1K9_tWf.js",
"runtimeLoaded": true,
"bytes": 365084,
"sha256": "7ac06534c9f270411f02efc56b31b9b862855f90ed31470f2dd22c66d631368f"
},
{
"role": "wasm-loader",
"path": "assets/linuxcnc-core-DHKZgRAr.js",
"runtimeLoaded": false,
"bytes": 320852,
"sha256": "fa73fa21a449efc18bbbf5e33e5ce4035bd8d8699f1cb7c72b88e280d362813a"
},
{
"role": "wasm-core",
"path": "assets/linuxcnc-core-XJNGI6z7.wasm",
"runtimeLoaded": true,
"bytes": 53030753,
"sha256": "5c1bb5f7648b7ced4ede192485d37aeee1d9e0dec4a86f2c235aeb3d8c3b647e"
}
],
"loadedArtifacts": [
{
"role": "entry-html",
"path": "index.html",
"status": 200
},
{
"role": "case-registry",
"path": "linuxcnc/cases.json",
"status": 200
},
{
"role": "machine-config",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"status": 200
},
{
"role": "application",
"path": "assets/simulator-D-5Qaz4x.js",
"status": 200
},
{
"role": "controller-worker",
"path": "assets/controller.worker-C1K9_tWf.js",
"status": 200
},
{
"role": "wasm-core",
"path": "assets/linuxcnc-core-XJNGI6z7.wasm",
"status": 200
}
]
},
"browserId": "chrome",
"browser": {
"product": "150.0.7871.128",

View File

@@ -21,7 +21,7 @@
"multiAxis": true
},
"geometry": {
"sourceShape": "shape-mst1ison-1",
"sourceShape": "shape-msthz6ws-1",
"valid": true,
"volume": 30
},

View File

@@ -90,7 +90,7 @@
}
],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -139,7 +139,7 @@
"binder": {
"support": [],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -237,7 +237,7 @@
}
],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -286,7 +286,7 @@
"binder": {
"support": [],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -335,7 +335,7 @@
"binder": {
"support": [],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -433,7 +433,7 @@
}
],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -531,7 +531,7 @@
}
],
"area": 54,
"shapeId": "shape-mss2ywwz-3",
"shapeId": "shape-mstl5fv3-3",
"state": [
"Up-to-date"
],
@@ -629,7 +629,7 @@
}
],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -727,7 +727,7 @@
}
],
"area": 54,
"shapeId": "shape-mss2ywwz-3",
"shapeId": "shape-mstl5fv3-3",
"state": [
"Up-to-date"
],
@@ -825,7 +825,7 @@
}
],
"area": 54,
"shapeId": "shape-mss2ywwz-3",
"shapeId": "shape-mstl5fv3-3",
"state": [
"Up-to-date"
],
@@ -923,7 +923,7 @@
}
],
"area": 30,
"shapeId": "shape-mss2yww8-1",
"shapeId": "shape-mstl5fuc-1",
"state": [
"Up-to-date"
],
@@ -1021,7 +1021,7 @@
}
],
"area": 54,
"shapeId": "shape-mss2ywwz-3",
"shapeId": "shape-mstl5fv3-3",
"state": [
"Up-to-date"
],
@@ -1059,8 +1059,8 @@
},
{
"path": "src/facade/mockFacade.ts",
"bytes": 150952,
"sha256": "a17f5c80c4f523776211e2ae341a60c2a55b06afd86369a3d2d71b888e75f48b"
"bytes": 160856,
"sha256": "998605cd6b17b82a2567006e6e34fba89f0a425afd383abad541dae03aebf478"
},
{
"path": "src/facade/geometryRuntime.ts",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,14 @@
"wrongBindings": 0,
"unexplainedRelations": 0,
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"mutationCases": 30,
"mutationFailures": 0,
"mutationStageRecords": 219,
"mutationStageRestoreFailures": 0,
"mutationFinalPropagationFailures": 0,
"mutationNamingRestoreDriftCases": 11,
"mutationNamingRestoreDriftStages": 28,
"elementMap2ParseFailures": 0,
"elementMap2SemanticFailures": 0,
"elementMap2TokenWriterFailures": 0,

View File

@@ -0,0 +1,40 @@
{
"schemaVersion": 1,
"baseline": {
"freecadVersion": "1.1.1",
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
},
"isolatedWorkbenchOracle": {
"report": ".cache/freecad/reference-desktop-gui-commands.json",
"reportSha256": "56da6a1bc360e76b9e36c0acc967d54d5fd28b7628056aba7af0c0ff84fb4e82",
"commandCount": 1111,
"commandUniverseSha256": "82901c576ef4b4cea1182f6505f5282a70b84ee8420105e83e58f65c0fb9685f",
"shardCount": 100,
"bimFirstTimeWelcome": "suppressed-before-workbench-activation"
},
"legacyAmbientReport": {
"report": ".cache/freecad/reference-desktop.json",
"reportSha256": "30ffbbdb4139a682486e6fed482a8d8d8a88c287a38dfec8d814e38f4e70fc80",
"commandCount": 1113
},
"contextOnlyCommands": [
{
"id": "Import_ReadBREP",
"classification": "explicit-module-import-command",
"sourceEvidence": [
"src/Mod/Import/Gui/Command.cpp:173"
],
"runtimeExpectation": "not-registered-by-workbench-activation;requires-explicit-ImportGui-command-initialization",
"exactTask": "EX-FMT-01"
},
{
"id": "NaviCubeDraggableCmd",
"classification": "context-menu-lazy-command",
"sourceEvidence": [
"src/Gui/NaviCube.cpp:1370"
],
"runtimeExpectation": "registered-only-when-the-navigation-cube-context-menu-is-first-created",
"exactTask": "EX-UI-04"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,242 @@
{
"schemaVersion": 1,
"baseline": {
"freecadVersion": "1.1.1",
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"profile": "desktop-xvfb-isolated-config"
},
"generatedBy": "./npmw run probe:freecad-gui-workflow",
"checkedBy": "./npmw run check:freecad-gui-workflow",
"family": {
"id": "partdesign-pad-task",
"primaryCommand": "PartDesign_Pad",
"exactTask": "EX-UI-03",
"source": ".cache/freecad/FreeCAD/src/Mod/PartDesign/Gui/Command.cpp"
},
"workflows": {
"success": {
"after": {
"acceptClicked": true,
"bodyGroup": [
"Sketch",
"Pad"
],
"bodyTip": "Pad",
"objectNames": [
"Body",
"Origin",
"X_Axis",
"Y_Axis",
"Z_Axis",
"XY_Plane",
"XZ_Plane",
"YZ_Plane",
"Origin001",
"Sketch",
"Pad"
],
"pad": {
"edgeCount": 12,
"faceCount": 6,
"length": 10,
"name": "Pad",
"profile": "Sketch",
"shapeValid": true,
"solidCount": 1,
"state": [
"Up-to-date"
],
"typeId": "PartDesign::Pad",
"vertexCount": 8,
"volume": 120
},
"redoCount": 0,
"redoNames": [],
"selection": [],
"undoCount": 1,
"undoNames": []
},
"baselineId": "freecad-1.1.1",
"before": {
"actionCount": 1,
"actionEnabled": true,
"activeWorkbench": "PartDesignWorkbench",
"bodyTip": "",
"commandActive": true,
"commandRegistered": true,
"objectNames": [
"Body",
"Origin",
"X_Axis",
"Y_Axis",
"Z_Axis",
"XY_Plane",
"XZ_Plane",
"YZ_Plane",
"Origin001",
"Sketch"
],
"selection": [
"Sketch"
],
"undoNames": []
},
"commandId": "PartDesign_Pad",
"freecadVersion": "1.1.1",
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"schemaVersion": 1,
"state": "success",
"success": true,
"taskPanel": {
"closed": {
"activeDialog": false,
"buttons": [
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "newFileButton",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "",
"role": "direct-push-button",
"text": "Open First Start Setup"
},
{
"default": false,
"enabled": true,
"objectName": "",
"role": "direct-push-button",
"text": "5.24 mm x 4.14 mm"
},
{
"default": false,
"enabled": true,
"objectName": "notificationArea",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "NavigationIndicator",
"role": "direct-push-button",
"text": "CAD "
}
],
"fieldClasses": [
"Gui::WorkbenchComboBox",
"QCheckBox"
],
"fieldCount": 2,
"focus": {
"className": "Gui::TaskView::TaskView",
"objectName": "Tasks"
},
"inEdit": "",
"phase": "closed"
},
"opened": {
"activeDialog": true,
"buttons": [
{
"default": false,
"enabled": true,
"objectName": "",
"role": "direct-push-button",
"text": "Done"
},
{
"default": false,
"enabled": true,
"objectName": "",
"role": "direct-push-button",
"text": "5.24 mm x 4.14 mm"
},
{
"default": false,
"enabled": true,
"objectName": "notificationArea",
"role": "direct-push-button",
"text": ""
},
{
"default": false,
"enabled": true,
"objectName": "NavigationIndicator",
"role": "direct-push-button",
"text": "CAD "
}
],
"fieldClasses": [
"Gui::WorkbenchComboBox",
"QComboBox"
],
"fieldCount": 4,
"focus": {
"className": "Gui::PrefQuantitySpinBox",
"objectName": "lengthEdit"
},
"inEdit": "Pad",
"phase": "opened"
},
"preview": {
"bodyTip": "Pad",
"length": 10,
"name": "Pad",
"profile": "Sketch",
"shapeValid": true,
"solidCount": 1,
"typeId": "PartDesign::Pad",
"volume": 120
}
},
"workflowId": "partdesign-pad-task"
}
},
"remainingStates": [
"disabled",
"failure",
"cancel",
"recovery"
],
"exactPromotionReady": false
}

View File

@@ -0,0 +1,110 @@
{
"schemaVersion": 1,
"baseline": {
"freecadVersion": "1.1.1",
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
},
"selectedAt": "2026-08-14",
"family": {
"id": "partdesign-pad-task",
"title": "Part Design Pad task lifecycle",
"workbench": "PartDesignWorkbench",
"primaryCommand": "PartDesign_Pad",
"commandIds": [
"PartDesign_Pad"
],
"primaryExactTask": "EX-UI-03",
"relatedExactTasks": [
"EX-ORA-01",
"EX-PD-01"
],
"selectionReason": "Pad is a P0 profile feature with existing native geometry, failure, history, naming and FCStd evidence. Its GUI command is document-sensitive, opens a parameter Task panel, supports preview and cancel, and can exercise success, disabled, failure and recovery without introducing a new workbench scope."
},
"sensitivityEvidence": {
"source": "config/freecad-gui-command-inventory.json",
"commandId": "PartDesign_Pad",
"documentSensitive": true,
"selectionSensitive": false,
"cases": {
"noDocument": [
false
],
"documentNoSelection": [
true
],
"selectedPartBox": [
true
]
}
},
"stateRecipes": {
"success": {
"setup": "Create a document, Body and closed rectangular Sketch; select the Sketch and invoke PartDesign_Pad.",
"action": "Set Length to 10 mm and accept the Task.",
"expected": "A Pad is committed as Body Tip, the transaction is undoable and the document recomputes valid."
},
"disabled": {
"setup": "Start the Part Design workbench with no active document.",
"action": "Query PartDesign_Pad command state without invoking it.",
"expected": "The command is inactive and no document, Task or transaction is created."
},
"failure": {
"setup": "Create an active Body with an invalid open-wire Sketch selected.",
"action": "Invoke PartDesign_Pad and attempt to accept the Task.",
"expected": "The native diagnostic is captured, no invalid Pad is committed and the last valid document state is retained."
},
"cancel": {
"setup": "Create an active Body with a valid closed Sketch and open PartDesign_Pad.",
"action": "Change Length, observe preview, then cancel the Task.",
"expected": "Preview state is discarded, no Pad is committed and selection, active Body and focus return to their pre-command state."
},
"recovery": {
"setup": "Continue from the failed or cancelled Pad workflow in the same document.",
"action": "Repair the profile or reopen Pad, enter a valid Length and accept; then undo and redo once.",
"expected": "The valid Pad is committed, Body Tip and Shape recover, undo/redo are stable and save-reopen preserves the result."
}
},
"progress": [
{
"taskId": "ORA-GUI-WF-000",
"status": "completed",
"evidence": [
"config/freecad-gui-workflow-plan.json",
"config/freecad-gui-command-inventory.json"
]
},
{
"taskId": "ORA-GUI-WF-001",
"status": "completed",
"evidence": [
"config/freecad-gui-workflow-oracle.json",
"scripts/check-freecad-gui-workflow-oracle.mjs"
]
},
{
"taskId": "ORA-GUI-WF-002",
"status": "in_progress",
"evidence": []
},
{
"taskId": "ORA-GUI-WF-003",
"status": "pending",
"evidence": []
},
{
"taskId": "ORA-GUI-WF-004",
"status": "pending",
"evidence": []
},
{
"taskId": "ORA-GUI-WF-005",
"status": "pending",
"evidence": []
},
{
"taskId": "ORA-GUI-WF-006",
"status": "pending",
"evidence": []
}
]
}

View File

@@ -7,8 +7,8 @@
},
"source": {
"path": ".cache/freecad/reference-desktop.json",
"bytes": 4030859,
"sha256": "995b495cd287cb43bed3d6a7cb69ae1f36d021a331ddf7c418304ece023e911e"
"bytes": 4699380,
"sha256": "30ffbbdb4139a682486e6fed482a8d8d8a88c287a38dfec8d814e38f4e70fc80"
},
"runtime": {
"registeredObjectTypes": 352,
@@ -37,16 +37,16 @@
},
"supportSummary": {
"native-editable-codec": {
"typeCount": 18,
"recordCount": 4135
"typeCount": 22,
"recordCount": 4304
},
"native-specialized-codec": {
"typeCount": 5,
"recordCount": 683
},
"opaque-fcstd-proxy": {
"typeCount": 62,
"recordCount": 692
"typeCount": 58,
"recordCount": 523
}
},
"types": [
@@ -195,7 +195,7 @@
},
{
"typeId": "App::PropertyFloatConstraint",
"support": "opaque-fcstd-proxy",
"support": "native-editable-codec",
"recordCount": 80,
"objectTypeCount": 64,
"statusNames": [
@@ -299,7 +299,7 @@
},
{
"typeId": "App::PropertyLinkHidden",
"support": "opaque-fcstd-proxy",
"support": "native-editable-codec",
"recordCount": 63,
"objectTypeCount": 63,
"statusNames": [
@@ -462,7 +462,7 @@
},
{
"typeId": "App::PropertyPrecision",
"support": "opaque-fcstd-proxy",
"support": "native-editable-codec",
"recordCount": 14,
"objectTypeCount": 6,
"statusNames": []
@@ -495,7 +495,7 @@
},
{
"typeId": "App::PropertyQuantityConstraint",
"support": "opaque-fcstd-proxy",
"support": "native-editable-codec",
"recordCount": 12,
"objectTypeCount": 7,
"statusNames": [
@@ -880,9 +880,11 @@
"behaviorNative": [
"Hidden",
"Immutable",
"LockDynamic",
"NoModify",
"Ordered",
"Output",
"PartialTrigger",
"PropHidden",
"PropNoPersist",
"PropNoRecompute",
@@ -892,21 +894,18 @@
"ReadOnly",
"Transient"
],
"preservedOnly": [
"LockDynamic",
"PartialTrigger"
],
"preservedOnlyRecordCount": 70,
"preservedOnly": [],
"preservedOnlyRecordCount": 0,
"proxyOnly": [],
"proxyOnlyRecordCount": 0,
"unknownNumericBits": []
},
"harness": {
"path": "scripts/run-freecad-native-property-semantics.mjs",
"bytes": 5949,
"sha256": "fb7fb74e4979b284e7a0f637d820bd183cea8e5cfb1348fd1730a891ac1306a2"
"bytes": 6166,
"sha256": "f6fcab14a46c9e7597f10808b31eb10cd5196bd90144c5b6a7a50e6993a1c3f6"
},
"exactPromotionReady": false,
"exactBlocker": "complete native document and property semantics",
"generatedAt": "2026-08-14T15:36:12.139Z"
"exactBlocker": "58 runtime property types and 523 records remain opaque-only; complete native document and property semantics",
"generatedAt": "2026-08-14T22:37:52.376Z"
}

View File

@@ -0,0 +1,233 @@
{
"schemaVersion": 1,
"baseline": {
"freecadVersion": "1.1.1",
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
},
"notBuiltModules": [
{
"name": "Cloud",
"buildOption": "BUILD_CLOUD",
"decision": "proxy",
"exactTask": "EX-PROXY-01",
"sourceEvidence": ["src/Mod/Cloud/CMakeLists.txt"],
"boundary": "Desktop network behavior is excluded; cloud payloads remain explicit resources without ambient network authority."
},
{
"name": "JtReader",
"buildOption": "BUILD_JTREADER",
"decision": "proxy",
"exactTask": "EX-FMT-04",
"sourceEvidence": ["src/Mod/JtReader/CMakeLists.txt"],
"boundary": "JT stays a lossless proxy resource until a licensed browser decoder has native differential evidence."
},
{
"name": "Sandbox",
"buildOption": "BUILD_SANDBOX",
"decision": "browser-reimplementation",
"exactTask": "EX-SCRIPT-01",
"sourceEvidence": ["src/Mod/Sandbox/CMakeLists.txt"],
"boundary": "The desktop developer sandbox is replaced by the capability-secure Facade script boundary."
},
{
"name": "TemplatePyMod",
"buildOption": "source-template-only",
"decision": "source-template",
"exactTask": "EX-SCRIPT-01",
"sourceEvidence": ["src/Mod/TemplatePyMod/CMakeLists.txt"],
"boundary": "The upstream template is source-only and is covered by the versioned Python/addon compatibility contract."
}
],
"sourceOnlyCommands": [
{
"id": "Arch_Floor",
"sourceModules": ["BIM"],
"classification": "legacy-lazy-command",
"sourceEvidence": ["src/Mod/BIM/ArchFloor.py"],
"runtimeExpectation": "registered-only-when-the-legacy-ArchFloor-module-is-imported",
"exactTask": "EX-BIM-01"
},
{
"id": "Arch_RebarTools",
"sourceModules": ["BIM"],
"classification": "optional-addon-command-group",
"sourceEvidence": ["src/Mod/BIM/InitGui.py"],
"runtimeExpectation": "registered-only-when-the-external-RebarTools-addon-imports-successfully",
"exactTask": "EX-ADDON-01"
},
{
"id": "CAM_%s",
"sourceModules": ["CAM"],
"classification": "dynamic-command-template",
"sourceEvidence": ["src/Mod/CAM/Path/Op/Gui/Base.py"],
"runtimeExpectation": "the-literal-template-must-not-register;-expanded-CAM-operation-commands-are-inventoried-separately",
"exactTask": "EX-CAM-01"
},
{
"id": "CAM_3dTools",
"sourceModules": ["CAM"],
"classification": "optional-preference-command-group",
"sourceEvidence": ["src/Mod/CAM/InitGui.py"],
"runtimeExpectation": "registered-only-when-advanced-OCL-features-are-enabled-and-the-OCL-import-succeeds",
"exactTask": "EX-CAM-01"
},
{
"id": "CAM_Camotics",
"sourceModules": ["CAM"],
"classification": "optional-executable-command",
"sourceEvidence": ["src/Mod/CAM/InitGui.py", "src/Mod/CAM/Path/Main/Gui/Camotics.py"],
"runtimeExpectation": "loaded-only-for-the-advanced-OCL-CAMotics-path-with-a-compatible-host-executable",
"exactTask": "EX-CAM-01"
},
{
"id": "CAM_Copy",
"sourceModules": ["CAM"],
"classification": "legacy-lazy-command",
"sourceEvidence": ["src/Mod/CAM/InitGui.py", "src/Mod/CAM/Path/Op/Gui/Copy.py"],
"runtimeExpectation": "the-default-workbench-uses-CAM_OperationCopy;CAM_Copy-registers-only-after-its-legacy-module-import",
"exactTask": "EX-CAM-01"
},
{
"id": "CAM_DrillingTools",
"sourceModules": ["CAM"],
"classification": "optional-preference-command-group",
"sourceEvidence": ["src/Mod/CAM/InitGui.py"],
"runtimeExpectation": "registered-only-when-experimental-features-add-CAM_Tapping-to-the-drilling-group",
"exactTask": "EX-CAM-01"
},
{
"id": "Draft_Hyperlink",
"sourceModules": ["Draft"],
"classification": "context-lazy-command",
"sourceEvidence": ["src/Mod/Draft/InitGui.py", "src/Mod/Draft/draftguitools/gui_hyperlink.py"],
"runtimeExpectation": "registered-only-after-a-text-object-context-menu-lazily-imports-the-hyperlink-module",
"exactTask": "EX-DRAFT-01"
},
{
"id": "TemplatePyCheckable",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyGroup",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyGrp_1",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyGrp_2",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyGrp_3",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd1",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd2",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd3",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd4",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd5",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
},
{
"id": "TemplatePyMod_Cmd6",
"sourceModules": ["TemplatePyMod"],
"classification": "source-template-command",
"sourceEvidence": ["src/Mod/TemplatePyMod/Commands.py", "src/Mod/TemplatePyMod/InitGui.py"],
"runtimeExpectation": "not-registered-because-TemplatePyMod-is-a-source-only-development-template",
"exactTask": "EX-SCRIPT-01"
}
],
"sourceOnlyTypeIds": [
{
"typeId": "App::DocumentObjectExtension",
"classification": "non-instantiable-extension-type",
"sourceEvidence": ["src/Mod/Test/Document.py"],
"runtimeExpectation": "must-reject-document-object-creation"
},
{
"typeId": "Assembly::Product",
"classification": "legacy-importer-type",
"sourceEvidence": ["src/Mod/Import/App/PlmXmlParser.py"],
"runtimeExpectation": "not-registered-by-locked-assembly-module"
},
{
"typeId": "Assembly::ProductRef",
"classification": "legacy-importer-type",
"sourceEvidence": ["src/Mod/Import/App/PlmXmlParser.py"],
"runtimeExpectation": "not-registered-by-locked-assembly-module"
},
{
"typeId": "Part::SketchObject",
"classification": "commented-test-reference",
"sourceEvidence": ["src/Mod/Part/TestPartGui.py"],
"runtimeExpectation": "not-a-runtime-type"
},
{
"typeId": "PartDesign::Additive",
"classification": "generated-type-prefix",
"sourceEvidence": ["src/Mod/PartDesign/Gui/CommandPrimitive.cpp"],
"runtimeExpectation": "prefix-only-additive-primitive-types-are-registered"
},
{
"typeId": "PartDesign::SketchObject",
"classification": "commented-test-reference",
"sourceEvidence": ["src/Mod/PartDesign/TestPartDesignGui.py"],
"runtimeExpectation": "not-a-runtime-type"
}
]
}

View File

@@ -167,6 +167,12 @@
],
"fcstd": {
"reopened": {
"codecValues": {
"FloatConstraint": 0.25,
"HiddenLink": "Target",
"Precision": 0.001,
"QuantityConstraint": 12.5
},
"properties": [
"Persisted",
"RuntimeTransient",
@@ -193,8 +199,12 @@
}
},
"xml": {
"floatConstraintNative": true,
"hiddenLinkNative": true,
"noPersistProperty": false,
"persistedProperty": true,
"precisionNative": true,
"quantityConstraintNative": true,
"runtimeTransientHasValue": false,
"runtimeTransientProperty": true,
"typeTransientHasValue": false,
@@ -211,13 +221,83 @@
"type": "RuntimeError"
}
},
"mutableRenameAndRemove": true,
"observerEvents": [
{
"object": "Feature",
"property": "MutableValue",
"type": "property.added"
},
{
"name": "dynamic-property-mutation",
"type": "transaction.opened"
},
{
"object": "Feature",
"property": "MutableValue",
"type": "property.before-change"
},
{
"object": "Feature",
"property": "MutableValue",
"type": "property.changed"
},
{
"object": "Feature",
"property": "RenamedValue",
"type": "property.removed"
},
{
"type": "transaction.committed"
}
],
"propertyStillPresent": true,
"renamedPropertyPresent": false,
"reportedStatus": [
"LockDynamic",
"21"
]
],
"undoAvailable": true
},
"schemaVersion": 1,
"partialTrigger": {
"observerEvents": [
{
"object": "Binder",
"property": "PartialLoad",
"type": "property.before-change"
},
{
"name": "partial-trigger-mutation",
"type": "transaction.opened"
},
{
"object": "Binder",
"property": "PartialLoad",
"type": "property.changed"
},
{
"object": "Binder",
"property": "Shape",
"type": "property.changed"
},
{
"type": "transaction.committed"
}
],
"recomputeCount": 1,
"reportedStatus": [
"PartialTrigger"
],
"state": {
"documentTouched": false,
"mustExecute": false,
"state": [
"Up-to-date"
]
},
"undoAvailable": true,
"value": true
},
"schemaVersion": 2,
"status": "pass"
}

View File

@@ -0,0 +1,764 @@
{
"baselineId": "freecad-1.1.1-tsn-stage-correlation",
"exactCorrelationReady": false,
"freecadVersion": "1.1.1",
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"harness": {
"bytes": 15851,
"path": "scripts/freecad-tsn-stage-correlation-oracle.py",
"sha256": "051d4b048801315de7ffe15d327ed2a61a136a8e4eb9570aa142e4cc07ad0055"
},
"mutations": [
{
"beforeParameter": 2,
"beforeTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"27ce709aac9ef81f00cbabe811ce71109a4d28b2bc70781692a1786f51d2d086",
"5f02884f9407c2c2cf1f6ad556796dab596a99fc89683c46ed0c9c06f5eaa3b6",
"0c4c788ee72519d174464d55a7432d148296f2b10ef2be6d474bdd15fed39cb4"
],
"downstreamShapeChanged": true,
"editedParameter": 2.5,
"editedTopologyDigests": [
"532c5e728dba7ca5efdd5af2339775d37cf7ad09572025eb8d02dbdb29ba4769",
"e86384c9bed306a1888640149d37044815c96cd35c8adfb68fb3f922c3f143a1",
"a39fadeaddb58af242c3144ffe2ada12e37ddad9453872683a98ee83bc98cdeb",
"3fb6cbd5903dfa65a79539b97cee4292eb791fc83e5eeadc16b2f9d25d6505df",
"376fe3fbd5d581b6c666aad7b4e5c9e383bc85ab806c46d2eaaa80fe675c23e7"
],
"featureName": "CutStage",
"operation": "cut",
"parameterChanged": true,
"propertyPath": "Length",
"restoreTopologyDriftStages": 3,
"restoredExactly": true,
"restoredGeometrically": true,
"restoredParameter": 2,
"restoredTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"3bf9c75c96da8b060cddb6acc9d8328fc7c4873298a02f97e53ca295958687fa",
"eca37e791bcbd7f6f4d946a98035adb91d50c56ed257c4af409a6b3032c3434a"
],
"restoredTopologyExactly": false,
"status": "pass",
"typeId": "PartDesign::Pocket"
},
{
"beforeParameter": 15,
"beforeTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"3bf9c75c96da8b060cddb6acc9d8328fc7c4873298a02f97e53ca295958687fa",
"eca37e791bcbd7f6f4d946a98035adb91d50c56ed257c4af409a6b3032c3434a"
],
"downstreamShapeChanged": true,
"editedParameter": 22.5,
"editedTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"featureName": "RotateStage",
"operation": "rotate",
"parameterChanged": true,
"propertyPath": "Angle",
"restoreTopologyDriftStages": 2,
"restoredExactly": true,
"restoredGeometrically": true,
"restoredParameter": 15,
"restoredTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"restoredTopologyExactly": false,
"status": "pass",
"typeId": "PartDesign::Feature"
},
{
"beforeParameter": 0.2,
"beforeTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"downstreamShapeChanged": true,
"editedParameter": 0.35,
"editedTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"featureName": "FilletStage",
"operation": "fillet",
"parameterChanged": true,
"propertyPath": "Radius",
"restoreTopologyDriftStages": 0,
"restoredExactly": true,
"restoredGeometrically": true,
"restoredParameter": 0.2,
"restoredTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"restoredTopologyExactly": true,
"status": "pass",
"typeId": "PartDesign::Fillet"
},
{
"beforeParameter": [
1,
0,
0
],
"beforeTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"downstreamShapeChanged": true,
"editedParameter": [
0,
0,
1
],
"editedTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"featureName": "MirroredStage",
"operation": "mirrored",
"parameterChanged": true,
"propertyPath": "PlaneNormal",
"restoreTopologyDriftStages": 0,
"restoredExactly": true,
"restoredGeometrically": true,
"restoredParameter": [
1,
0,
0
],
"restoredTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"restoredTopologyExactly": true,
"status": "pass",
"typeId": "PartDesign::Feature"
},
{
"beforeParameter": 2,
"beforeTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"downstreamShapeChanged": true,
"editedParameter": 3,
"editedTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"9bc91dea322c2bf63a7c27a765cb3081bd2d2c7627c83b870a53d973e9266bc6"
],
"featureName": "LinearPatternStage",
"operation": "linear-pattern",
"parameterChanged": true,
"propertyPath": "Occurrences",
"restoreTopologyDriftStages": 0,
"restoredExactly": true,
"restoredGeometrically": true,
"restoredParameter": 2,
"restoredTopologyDigests": [
"e5f9cf776ddc1289887018d55af7031b7e06f2f957cf9f8f14d2df4eae3864c2",
"54909d11a0a2c03e01c5c3d1dbfe534481f0efe9cd276efafdf4803213f16578",
"d707245f8805cc7393d782837b824734cf3c45056bccb15226dbc72bd7ca169b",
"014dbe05990637566dfb1c65cc942fc1bea2ce5ad19faa667ae1fa6014082472",
"a6cbc1de1003fc26ace333e9b04db01d3bd23a1075c8314a6036401f16e43bfa"
],
"restoredTopologyExactly": true,
"status": "pass",
"typeId": "PartDesign::Feature"
}
],
"nativeDesktopResaveCovered": true,
"operations": [
"cut",
"rotate",
"fillet",
"mirrored",
"linear-pattern"
],
"reopenedStages": [
{
"historyRelations": 102,
"historySources": [
"Base",
"CutStage",
"PocketSketch"
],
"mappedNameCount": 51,
"mappedNameDigest": "d81d46fb00e09fa055ee42530b484f4483e24e89f459e91c0e160a33bb3072f3",
"name": "CutStage",
"nativeBuilder": true,
"operation": "cut",
"shape": {
"area": 672,
"bounds": [
0,
0,
0,
20,
10,
4
],
"brepSha256": "335892b4ac3ea50c7344e4831ca9e2968146130f365acd14c2955d4a5764a746",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999999
},
"status": "Valid",
"typeId": "PartDesign::Pocket"
},
{
"historyRelations": 153,
"historySources": [
"Base",
"CutStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 51,
"mappedNameDigest": "672be1f75cb3f4c636751c511b2622474fa6968101b70c6f1f00e9f69c2d7a0b",
"name": "RotateStage",
"nativeBuilder": false,
"operation": "rotate",
"shape": {
"area": 671.9999999999998,
"bounds": [
-0.953353488403289,
-2.417819582470549,
0,
20.953353488403284,
12.417819582470553,
4
],
"brepSha256": "5691e5ab95001b9f54f5cd6e5ffc2b0150e24d600b0a57cc0db0ac5deb7bc52d",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999998
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 228,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "1b534b25e27970b232f867e64d77f6864878dfd7fccfc93b233e21d779b978ed",
"name": "FilletStage",
"nativeBuilder": true,
"operation": "fillet",
"shape": {
"area": 671.6394689145077,
"bounds": [
-0.953353488403289,
-2.372870608192231,
0,
20.953353488403284,
12.417819582470553,
4
],
"brepSha256": "3c0ffd6d586a2d709f1d3ec8239a1ad9617654fe60d967059db73fb6e0ca555d",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061432
},
"status": "Valid",
"typeId": "PartDesign::Fillet"
},
{
"historyRelations": 285,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "71c33cac052826ca3f8533de7cf0eee8f27393b6b88ca82c9516c1320ec74fe2",
"name": "MirroredStage",
"nativeBuilder": false,
"operation": "mirrored",
"shape": {
"area": 671.6394689145077,
"bounds": [
-20.953353488403287,
-2.3728706081922324,
0,
0.9533534884032838,
12.417819582470546,
4
],
"brepSha256": "d500b394fe084474934ab604ecd0c35525dc4ee2f213cfe2c0b52140ca8bcf17",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061435
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 456,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"LinearPatternStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 114,
"mappedNameDigest": "dddc8d0ec7b438ea429789c7332a361d749353443b65ce4f8cda169829174d27",
"name": "LinearPatternStage",
"nativeBuilder": false,
"operation": "linear-pattern",
"shape": {
"area": 1343.2789378290156,
"bounds": [
-20.953353488403287,
-2.3728706081922324,
0,
24.953353488403284,
12.417819582470546,
4
],
"brepSha256": "19819712984960022d9329b54f338f97d3d40dd30f0429b7b5f58ab517bf46a7",
"edges": 54,
"faces": 24,
"solids": 2,
"valid": true,
"vertices": 36,
"volume": 1535.9313274122867
},
"status": "Valid",
"typeId": "PartDesign::Feature"
}
],
"resavedStages": [
{
"historyRelations": 102,
"historySources": [
"Base",
"CutStage",
"PocketSketch"
],
"mappedNameCount": 51,
"mappedNameDigest": "d81d46fb00e09fa055ee42530b484f4483e24e89f459e91c0e160a33bb3072f3",
"name": "CutStage",
"nativeBuilder": true,
"operation": "cut",
"shape": {
"area": 672,
"bounds": [
0,
0,
0,
20,
10,
4
],
"brepSha256": "335892b4ac3ea50c7344e4831ca9e2968146130f365acd14c2955d4a5764a746",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999999
},
"status": "Valid",
"typeId": "PartDesign::Pocket"
},
{
"historyRelations": 153,
"historySources": [
"Base",
"CutStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 51,
"mappedNameDigest": "672be1f75cb3f4c636751c511b2622474fa6968101b70c6f1f00e9f69c2d7a0b",
"name": "RotateStage",
"nativeBuilder": false,
"operation": "rotate",
"shape": {
"area": 671.9999999999998,
"bounds": [
-0.953353488403289,
-2.417819582470549,
0,
20.953353488403284,
12.417819582470553,
4
],
"brepSha256": "5691e5ab95001b9f54f5cd6e5ffc2b0150e24d600b0a57cc0db0ac5deb7bc52d",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999998
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 228,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "1b534b25e27970b232f867e64d77f6864878dfd7fccfc93b233e21d779b978ed",
"name": "FilletStage",
"nativeBuilder": true,
"operation": "fillet",
"shape": {
"area": 671.6394689145077,
"bounds": [
-0.953353488403289,
-2.372870608192231,
0,
20.953353488403284,
12.417819582470553,
4
],
"brepSha256": "3c0ffd6d586a2d709f1d3ec8239a1ad9617654fe60d967059db73fb6e0ca555d",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061432
},
"status": "Valid",
"typeId": "PartDesign::Fillet"
},
{
"historyRelations": 285,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "71c33cac052826ca3f8533de7cf0eee8f27393b6b88ca82c9516c1320ec74fe2",
"name": "MirroredStage",
"nativeBuilder": false,
"operation": "mirrored",
"shape": {
"area": 671.6394689145077,
"bounds": [
-20.953353488403287,
-2.3728706081922324,
0,
0.9533534884032838,
12.417819582470546,
4
],
"brepSha256": "9b374288076f597adf12a73dc7f8301a4e397b5029d467aec8f297a73d96f768",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061435
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 456,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"LinearPatternStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 114,
"mappedNameDigest": "dddc8d0ec7b438ea429789c7332a361d749353443b65ce4f8cda169829174d27",
"name": "LinearPatternStage",
"nativeBuilder": false,
"operation": "linear-pattern",
"shape": {
"area": 1343.2789378290156,
"bounds": [
-20.953353488403287,
-2.3728706081922324,
0,
24.953353488403284,
12.417819582470546,
4
],
"brepSha256": "5749cad550f0f992c68ddefc5b48f6a5e92b8d77e173ccbf69cde5ef1b50762f",
"edges": 54,
"faces": 24,
"solids": 2,
"valid": true,
"vertices": 36,
"volume": 1535.9313274122867
},
"status": "Valid",
"typeId": "PartDesign::Feature"
}
],
"schemaVersion": 1,
"stages": [
{
"historyRelations": 102,
"historySources": [
"Base",
"CutStage",
"PocketSketch"
],
"mappedNameCount": 51,
"mappedNameDigest": "d81d46fb00e09fa055ee42530b484f4483e24e89f459e91c0e160a33bb3072f3",
"name": "CutStage",
"nativeBuilder": true,
"operation": "cut",
"shape": {
"area": 672,
"bounds": [
0,
0,
0,
20,
10,
4
],
"brepSha256": "8f2d397cb59d321e5739ba0b03bd64260fdd84045dd15686de80c4444c535505",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999999
},
"status": "Valid",
"typeId": "PartDesign::Pocket"
},
{
"historyRelations": 153,
"historySources": [
"Base",
"CutStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 51,
"mappedNameDigest": "672be1f75cb3f4c636751c511b2622474fa6968101b70c6f1f00e9f69c2d7a0b",
"name": "RotateStage",
"nativeBuilder": false,
"operation": "rotate",
"shape": {
"area": 672,
"bounds": [
-0.9533534884032862,
-2.417819582470549,
0,
20.953353488403287,
12.41781958247055,
4
],
"brepSha256": "4755383c92aa47477f9ecc5148a14518206815805144857a090ddc38e5469b97",
"edges": 24,
"faces": 11,
"solids": 1,
"valid": true,
"vertices": 16,
"volume": 767.9999999999999
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 228,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "1b534b25e27970b232f867e64d77f6864878dfd7fccfc93b233e21d779b978ed",
"name": "FilletStage",
"nativeBuilder": true,
"operation": "fillet",
"shape": {
"area": 671.6394689145077,
"bounds": [
-0.9533534884032862,
-2.372870608192231,
0,
20.953353488403287,
12.41781958247055,
4
],
"brepSha256": "b9e7ad2dbe16102b831a1cd2371c598ce5abe1c21ffd51407beed33665ba9923",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061437
},
"status": "Valid",
"typeId": "PartDesign::Fillet"
},
{
"historyRelations": 285,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 57,
"mappedNameDigest": "71c33cac052826ca3f8533de7cf0eee8f27393b6b88ca82c9516c1320ec74fe2",
"name": "MirroredStage",
"nativeBuilder": false,
"operation": "mirrored",
"shape": {
"area": 671.639468914508,
"bounds": [
-20.953353488403287,
-2.3728706081922315,
0,
0.9533534884032864,
12.417819582470552,
4
],
"brepSha256": "68c7026fbd830f1249fb23ca3fba785d10b290478b12fd1255796887e576d301",
"edges": 27,
"faces": 12,
"solids": 1,
"valid": true,
"vertices": 18,
"volume": 767.9656637061439
},
"status": "Valid",
"typeId": "PartDesign::Feature"
},
{
"historyRelations": 456,
"historySources": [
"Base",
"CutStage",
"FilletStage",
"LinearPatternStage",
"MirroredStage",
"PocketSketch",
"RotateStage"
],
"mappedNameCount": 114,
"mappedNameDigest": "dddc8d0ec7b438ea429789c7332a361d749353443b65ce4f8cda169829174d27",
"name": "LinearPatternStage",
"nativeBuilder": false,
"operation": "linear-pattern",
"shape": {
"area": 1343.278937829016,
"bounds": [
-20.953353488403287,
-2.3728706081922315,
0,
24.953353488403287,
12.417819582470552,
4
],
"brepSha256": "9207500ce3fae12e57cb3a9c04537bc4bf1dbd7b4eb10bf5ad01aa7a71c62821",
"edges": 54,
"faces": 24,
"solids": 2,
"valid": true,
"vertices": 36,
"volume": 1535.9313274122874
},
"status": "Valid",
"typeId": "PartDesign::Feature"
}
],
"status": "pass",
"summary": {
"mutationCases": 5,
"mutationPassed": 5,
"mutationRestoreTopologyDriftCases": 2,
"mutationRestoreTopologyDriftStages": 5,
"nativeBuilderStages": 2,
"proxyStages": 3,
"resaveNameDrift": 0,
"roundtripNameDrift": 0,
"stages": 5,
"unexplainedRelations": 0,
"wrongBindings": 0
}
}

View File

@@ -20,7 +20,7 @@
"id": "EX01",
"title": "Exhaustive FreeCAD oracle and optional dependency baseline",
"tasks": [
{ "id": "EX-ORA-01", "title": "Capture exhaustive runtime objects, properties, commands and GUI states", "priority": "P0", "status": "in_progress", "dependencies": [], "deliverables": ["Runtime oracle for every registered TypeId and command", "Success, failure, disabled and cancel fixtures", "Source-to-runtime trace"], "acceptance": ["All inventory candidates are promoted or explicitly excluded", "No command or property has an unknown runtime state"], "evidence": ["check:freecad-inventory", "check:freecad-type-properties", "check:freecad-gui-commands", "check:freecad-oracle-coverage"], "exactBlockedBy": ["Static candidates and GUI-only states are not exhaustively runtime-probed"] },
{ "id": "EX-ORA-01", "title": "Capture exhaustive runtime objects, properties, commands and GUI states", "priority": "P0", "status": "in_progress", "dependencies": [], "deliverables": ["Runtime oracle for every registered TypeId and command", "Success, failure, disabled and cancel fixtures", "Source-to-runtime trace"], "acceptance": ["All inventory candidates are promoted or explicitly excluded", "No command or property has an unknown runtime state"], "evidence": ["check:freecad-inventory", "check:freecad-type-properties", "check:freecad-gui-commands", "check:freecad-oracle-coverage", "check:freecad-gui-workflow"], "exactBlockedBy": ["The isolated desktop oracle covers 1111 workbench-startup commands in 100 fail-closed shards, 19 source-only commands and 2 context-only commands; the PartDesign Pad success Task workflow is captured, while disabled, failure, cancel, recovery and the remaining command-family workflows are still open"] },
{ "id": "EX-ORA-02", "title": "Expand geometry, history and FCStd golden corpus to every feature family", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Parameterized success and failure corpus", "Native Shape, history, ElementMap2 and StringHasher evidence", "FreeCAD save-reopen-resave evidence"], "acceptance": ["Every editable feature family has boundary and mutation fixtures", "Unknown semantic differences are zero"], "evidence": ["check:freecad-golden-fixtures", "check:freecad-composite-history-elementmap", "check:freecad-golden-coverage", "check:freecad-parameter-mutations", "check:freecad-partdesign-failures", "check:freecad-partdesign-revolution-groove", "check:freecad-partdesign-transform", "check:freecad-part-builders"], "exactBlockedBy": ["Parameter mutation matrix is fail-closed until every family has explicit edit/recompute/recovery and round-trip evidence"] },
{ "id": "EX-ORA-03", "title": "Lock all optional libraries, platform capabilities and license boundaries", "priority": "P1", "status": "pending", "dependencies": ["EX-ORA-01"], "deliverables": ["Optional dependency manifest", "Browser, sidecar or proxy decision per dependency", "Reproducible hashes and licenses"], "acceptance": ["Every optional FreeCAD capability has one executable strategy", "No undeclared host-only dependency affects a claim"], "evidence": ["check:baseline", "check:sbom"], "exactBlockedBy": ["Several desktop-only optional libraries remain proxy or sidecar capabilities"] }
]
@@ -34,14 +34,14 @@
{ "id": "EX-TSN-01", "title": "Capture native stage Shape and Generated/Modified/Deleted for every builder", "priority": "P0", "status": "completed", "dependencies": ["EX-KER-01", "EX-TSN-00"], "deliverables": ["All builder-stage captures", "Multi-input and one-to-many lineage", "Local feature recompute history"], "acceptance": ["Every feature result relation has a producing native stage", "No final-result index is applied to an intermediate Shape"], "evidence": ["check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-evidence"], "exactBlockedBy": [] },
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "check:freecad-naming-sdk", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-private-naming-boundary", "check:occt-history-artifact"], "exactBlockedBy": [] },
{ "id": "EX-TSN-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "test:facade", "check:freecad-isomorphic-provenance", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-isomorphic-provenance", "check:freecad-composite-history-elementmap", "check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["The 30-case native save-reopen-resave corpus now has zero mapped-name drift, but the exhaustive cross-feature transition matrix and production-chain desktop resave correlation remain incomplete"] }
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-isomorphic-provenance", "check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-correlation", "check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["The locked 30-case corpus now passes 30/30 native property mutations, final-shape propagation and 219/219 geometry-stage restores, but 11 PartDesign cases evolve 28 recovered naming-stage digests; the five-stage chain still has 3/5 generic Shape proxies, no rotate mutation family, 5 semantic topology restore drifts, and only 4/361 ordered operation pairs classified"] }
]
},
{
"id": "EX03",
"title": "Application document, property and recompute semantics",
"tasks": [
{ "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-native-property-semantics", "check:freecad-property-status", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Of 85 runtime property types and 5510 records, 62 types/692 records remain opaque-only; all 15 observed status classes are represented, 13 have native behavior evidence, and LockDynamic/PartialTrigger still lack dynamic mutation and partial-document observer implementation"] },
{ "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-native-property-semantics", "check:freecad-property-status", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Of 85 runtime property types and 5510 records, 58 types/523 records remain opaque-only; all 15 observed status classes now have typed Facade behavior, including LockDynamic dynamic mutation and PartialTrigger partial-document observer transactions, but the remaining codecs, editors and complete document observer semantics are not closed"] },
{ "id": "EX-DOC-02", "title": "Match transactions, DAG recompute, partial failure and document observers", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01", "EX-TSN-04"], "deliverables": ["Transaction and observer event oracle", "Partial recompute scheduler", "Failure recovery contract"], "acceptance": ["Undo, redo, abort, stale and cyclic dependency behavior matches FreeCAD"], "evidence": ["check:document-closure", "test:facade"], "exactBlockedBy": ["Observer ordering and every partial-recompute branch are not yet oracle-backed"] },
{ "id": "EX-DOC-03", "title": "Complete expressions, links, extensions, groups and App object lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01"], "deliverables": ["Expression dependency grammar", "Link and extension lifecycle", "Group and clone semantics"], "acceptance": ["Copy, clone, delete, relink and expression edits survive native round-trip"], "evidence": ["check:fcstd-closure", "check:chrome-production-document"], "exactBlockedBy": ["FeaturePython-backed extensions remain non-executable"] },
{ "id": "EX-DOC-04", "title": "Complete multi-document, external link, merge and recovery workflows", "priority": "P1", "status": "pending", "dependencies": ["EX-DOC-02", "EX-DOC-03"], "deliverables": ["Cross-document Link corpus", "Merge and conflict semantics", "Crash and autosave recovery"], "acceptance": ["Open, close, relink, merge and recovery match the native document graph"], "evidence": ["check:chrome-persistence", "check:project-migration"], "exactBlockedBy": ["Current application is project-first and does not implement every desktop multi-document workflow"] },

View File

@@ -0,0 +1,59 @@
{
"schemaVersion": 1,
"worktreeRevision": "58f50e6d66287bc31e6d906e5c33658a348658e4",
"chromeVersion": "150.0.7871.128",
"machineCase": "axis-vismach-5axis-table-rotary-tilting-xyzbc-trt",
"machineConfigPath": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"distRoot": "cnc_wams_gpt6/linuxcnc-master/web/dist",
"artifacts": [
{
"role": "entry-html",
"path": "index.html",
"runtimeLoaded": true,
"bytes": 61728,
"sha256": "7087d2ccfb9eb2b4967fa75a7d4069cc97ab62a58d4af4f18a7a449ddc1aa536"
},
{
"role": "case-registry",
"path": "linuxcnc/cases.json",
"runtimeLoaded": true,
"bytes": 2108501,
"sha256": "b9c614d3f139de57277d02944a3cb8d0e54f68da054021ac375be9ffbb4f95db"
},
{
"role": "machine-config",
"path": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"runtimeLoaded": true,
"bytes": 7179,
"sha256": "43c26a58030b6ec4a4319cde23e847cff0134e9f2777eef30848aa477bb87d38"
},
{
"role": "application",
"path": "assets/simulator-D-5Qaz4x.js",
"runtimeLoaded": true,
"bytes": 852452,
"sha256": "a429e0b7297fb169255a30acc48274ed17bbeb168fc1d90a13cf4b4e2c6db42f"
},
{
"role": "controller-worker",
"path": "assets/controller.worker-C1K9_tWf.js",
"runtimeLoaded": true,
"bytes": 365084,
"sha256": "7ac06534c9f270411f02efc56b31b9b862855f90ed31470f2dd22c66d631368f"
},
{
"role": "wasm-loader",
"path": "assets/linuxcnc-core-DHKZgRAr.js",
"runtimeLoaded": false,
"bytes": 320852,
"sha256": "fa73fa21a449efc18bbbf5e33e5ce4035bd8d8699f1cb7c72b88e280d362813a"
},
{
"role": "wasm-core",
"path": "assets/linuxcnc-core-XJNGI6z7.wasm",
"runtimeLoaded": true,
"bytes": 53030753,
"sha256": "5c1bb5f7648b7ced4ede192485d37aeee1d9e0dec4a86f2c235aeb3d8c3b647e"
}
]
}

View File

@@ -24,7 +24,7 @@
"wrong-document-root": 77
},
"timing": {
"p95Ms": 0.209,
"maxMs": 3.268
"p95Ms": 0.126,
"maxMs": 2.592
}
}

View File

@@ -41,7 +41,7 @@
"zero-revolution-axis": 67
},
"timing": {
"p95Ms": 0.022,
"maxMs": 1.146
"p95Ms": 0.02,
"maxMs": 0.928
}
}

View File

@@ -22,7 +22,7 @@
"invalid": 800
},
"timing": {
"p95Ms": 0.061,
"maxMs": 1.509
"p95Ms": 0.029,
"maxMs": 0.662
}
}

View File

@@ -28,9 +28,9 @@
"sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec"
},
{
"path": "assets/index-BZhOOUrh.js",
"bytes": 1135258,
"sha256": "f8d20cf0ceeb131295d21f8d9809a542aa8871a50e74a712fdc065be307fd021"
"path": "assets/index-BrkUKPvE.js",
"bytes": 1143618,
"sha256": "4324ab6351a3ef214762115d5b5ffc706883ccaf1e9b0f90577f912da1e4a223"
},
{
"path": "assets/nativeHistoryWorkerEntry-BEDB1h7o.js",
@@ -335,7 +335,7 @@
{
"path": "index.html",
"bytes": 678,
"sha256": "b78bfb7e023a1f03371b5f2c322cb367a9a05c622e45a06e37670c87cc68cd3a"
"sha256": "e3cfab4e89c7bc19c51f2529bb2c6c84a61064168029a3bdfee2260a49a71097"
},
{
"path": "manifest.webmanifest",
@@ -390,7 +390,7 @@
{
"path": "sw.js",
"bytes": 1390,
"sha256": "020250b7a88f8106b0666ad9d7070aefab28892371e4c670ab3d164b9e0922b6"
"sha256": "98ec667a774f8370ed654e457b86d4943b2f1eb89e228bd4e7e652b31a51b982"
},
{
"path": "vendor/camotics/camotics-sweep.wasm",
@@ -412,7 +412,7 @@
"status": "signed",
"algorithm": "Ed25519",
"keyId": "bitbybit-local-release-2026",
"payloadSha256": "44d3d76c20d3a5d4e691b2d98cc5dc6fa1df910e492bf811fa64133fb2f6177a",
"signature": "m2GxJ+PLZ2RcDSI9w0hUMF+ArOLKydE6ilbgRXKgJlj8qiIRrgvaFghAZQL23NGws++j/9Xv78EsaXRvZC8oDA=="
"payloadSha256": "5172b35d4d194483ac1934a1653259b1c7f5bb78c5ab4a68f7e5033cf2a22b86",
"signature": "FoaYFhliT+/njf8hXECKj5IoCeHPj1MnpbTBIw/FxzYpfyrgQ01nuPkwslM4okUk3d97cncjaXnEkDA7lZBtDw=="
}
}

View File

@@ -1885,3 +1885,29 @@ Node 与真实 Chrome 的 19-operation 生产矩阵均重跑通过。17 类样
扩展 `freecad-composite-history-elementmap-oracle` 的每个 30-case 流程:初次保存后关闭并重开,采集 ElementMap/`getElementMappedName()`;随后再次 `saveAs()`,再次关闭并重开,再采集同一组阶段和子形名称。报告新增 `nativeDesktopResaveCovered``resaveNameDrift` 及汇总计数,检查器要求 30/30 案例完成二次原生保存且 `roundtripNameDrift=0``resaveNameDrift=0`
本轮实际结果为 30 个案例、219 个阶段、91 个 ElementMap2 资源和 11 个 StringHasher 资源全部通过;二次保存后的命名漂移为 `0``wrongBindings/unexplainedRelations/roundtripNameDrift` 等 exact gate 指标继续为零。EX-TSN-04 已把该门禁登记为证据,但仍保持 `in_progress`:生产 Worker 五阶段链与桌面 30-case 语料尚未完成逐阶段相关性对账,跨特征 mutation 矩阵也未穷尽。完整 `./npmw run verify`、构建、签名发布和 `git diff --check` 均通过。
## 241. 2026-08-14 EX-TSN-04 桌面五阶段关联与跨特征 mutation 门
新增锁定 FreeCAD 1.1.1 的 `freecad-tsn-stage-correlation-oracle`,按生产顺序执行 Cut→Rotate→Fillet→Mirrored→LinearPattern采集每阶段 Shape、MappedName 摘要、`getElementHistory()` 来源图,并完成关闭、重开、二次 `saveAs()`、再次重开。该专用链与原 30-case corpus 合计形成 31 个桌面 resave 用例;新增链的 `wrongBindings=0``unexplainedRelations=0``roundtripNameDrift=0``resaveNameDrift=0`。exact history gate 现正式计入 `resaveNameDrift`,不再只检查首次 round-trip。
跨特征 mutation 对五个阶段各执行一次参数编辑、全链重算和恢复5/5 参数与几何恢复通过;现有 35-family 报告能为 Cut、Fillet、Mirrored、LinearPattern 提供独立原生 mutation对 Rotate 仍无注册 feature family。FreeCAD 的顺序 PartDesign 链拒绝继续变换 Mirrored 输出,原生错误为 `Only additive and subtractive features can be transformed`,因此专用顺序链的 Rotate、Mirrored、LinearPattern 仍只能标记为通用 Shape 代理,不能算原生 builder。Cut 与 Rotate 的恢复还导致 2/5 用例、5 个下游语义拓扑摘要变化,虽未形成错误绑定,仍阻断 exact。
新门禁将 19 类生产 operation 展开为 361 个有序组合;当前真实回放 4 个相邻转换,剩余 357 个尚未完成类型兼容分类。`EX-TSN-04` 因此保持 `in_progress`,机器化剩余量为 `native sequential builder=2/5``native mutation correlation=4/5``classified transition=4/361`。不得用当前零漂移结果越过这些显式阻断。
## 242. 2026-08-14 30-case 逐阶段 mutation 与恢复对账
`freecad-composite-history-elementmap-oracle` 已为固定的 `9 Boolean + 11 PartDesign + 6 复合 Boolean + 4 dress-up` 构造器增加逐案例 mutation 合同。Boolean 编辑 Cylinder.RadiusPartDesign 编辑 Pad.Length复合 Boolean 编辑第一层基体 Box.LengthFillet/Chamfer 分别编辑 Radius/Size每个目标均由 FreeCAD 报告为原生可编辑 Property并必须改变最终对象的解析几何摘要。采集器为 219 个阶段各保存 before/edited/restored 三相记录,共 657 条,包含稳定解析几何 digest、B-Rep SHA-256、relation digest 与 semantic-name digest。
锁定 FreeCAD 1.1.1 oracle 实跑为 `30/30 mutation pass`、最终传播失败 `0`、几何恢复失败 `0/219`。恢复后的文档状态作为实际 FCStd nominal 基线,再执行 save/reopen/resave30/30 case、219/219 阶段保持 `roundtripNameDrift=0``resaveNameDrift=0`91 个 ElementMap2 和 11 个 StringHasher 资源继续通过解析、写回与语义检查。恢复重算本身仍使 11 个 PartDesign case 的 28 个阶段产生 relation/semantic-name digest 演进;报告与 exact gate 将其作为独立非零指标保留,未把几何恢复等同于命名 exact。
新增共享 `freecad-composite-mutation-evidence` 校验器,锁定 30 个 case identity、四类配额、原生 Property/editor 状态、target/final stage ordinal、三相阶段 identity、最终传播和逐阶段恢复。TSN-04 checker 与 exact history gate 都按 case 一对一连接 resave 证据任一案例缺失、重复、错序或局部指标不一致即失败。FreeCAD 来源子形解析同时覆盖 `FaceN/EdgeN/VertexN``#d:1``#42``g1v2;SKT` 原生语法;当前 `wrongBindings=0``unexplainedRelations=0``EX-TSN-04` 仍为 `in_progress`:除 28 个恢复命名摘要演进外,五阶段链仍有 3/5 proxy、rotate mutation family 缺失、5 个拓扑恢复 drift 和 357 个未分类 operation pair。
## 243. 2026-08-14 执行重审与首个 GUI 成功工作流
新增 `freecad-web-execution-master-plan.zh-CN.md`,重新区分 83/83 兼容任务与 5/52 exact 任务,按 ORA、TSN、Property、Document、Kernel、Sketcher、Part、PartDesign、FCStd、GUI、工程工作台、平台和发布拆出最小 A-I 验收步骤。机器队列继续保持 `maxInProgress=1`,不以文档计划替代原生和浏览器证据。
`ORA-GUI-WF-000` 选择 `partdesign-pad-task`,代表命令为 `PartDesign_Pad`,主 exact 映射为 `EX-UI-03`,关联 `EX-ORA-01/EX-PD-01`。选择依据、成功/禁用/失败/取消/恢复配方和串行进度保存在 `config/freecad-gui-workflow-plan.json`,新增 check 保证命令敏感度与 1111 命令运行时清册一致。
`ORA-GUI-WF-001` 随后在隔离 Xvfb 的锁定 FreeCAD 1.1.1 桌面进程真实执行。原生命令从已选矩形 Sketch 打开 Pad TaskTaskView 的实际接受按钮为 `Done`;接受并清理 edit 状态后得到 `PartDesign::Pad`Body Group 为 `Sketch,Pad`、Tip=Pad、Profile=Sketch、Length=10实体为 6 面/12 边/8 点、体积 120、状态 Up-to-dateUndoCount=1。报告只声明 successremainingStates 仍为 disabled/failure/cancel/recovery。队列前移为 112 completed、1 in_progress、374 pending唯一下一项是 `ORA-GUI-WF-002`
本轮完整 `./npmw run verify` 以退出码 0 完成生产构建、79 个签名发布文件、34 模块 release closure 和 Ed25519 验签通过;能力矩阵仍准确保持 0 exact、26 compatible、5 proxy、3 development。

View File

@@ -18,6 +18,7 @@
- exact 计划共 52 项:`5 completed``36 in_progress``11 pending`
- 已完成的 exact 项只有 `EX-TSN-00..03``EX-PD-02`:原生命名证据边界、逐阶段历史、生产私有命名、同构来源处理,以及 Attachment/XLink 生命周期。
- exact 发布任务 `EX-REL-01` 仍为 `pending`34 个模块都不得提升为 exact。
- 当前工作树包含 TSN 逐阶段相关性、Property 状态/codec、CAM/LinuxCNC 验证资产、oracle 清册分类和首个 Pad GUI 工作流增量;聚焦门禁、完整 `verify`、发布产物验签和最终差异校验均已通过。
状态冲突时按以下顺序取真值:
@@ -27,6 +28,8 @@
4. `config/*-verification.json` 和原生 oracle某次具体环境运行的证据。
5. `docs/continuation-status.zh-CN.md`:按时间追加的历史日志,不作为当前总状态入口。
状态字段需要结合依赖读取:`in_progress` 只表示已有证据或实现,不表示当前可直接关闭;少数 `completed` 项是按自身验收范围关闭的独立切片,即使它引用的更大上游任务仍开放,也不能反推整条依赖链已经完成。
## 3. 当前架构主链
```text
@@ -47,19 +50,38 @@ FCStd / STEP / IGES / mesh / engineering resources
任何新功能都必须接入这条主链,不能从 React 绕过 Facade 直接调用 Three.js、OCCT、文件系统或持久化实现。
## 4. 执行优先级
## 4. 关键依赖链
```text
EX-ORA-01 -> EX-ORA-02 -> EX-KER-01
| |
+-> EX-DOC-01 +-> EX-PART-01 -> EX-PD-01
| ^
EX-TSN-00..03 -> EX-TSN-04 -----------+
| |
| +-> EX-DOC-02
| |
+--------------+-> EX-DOC-04 -> EX-FC-01
EX-DOC-01 -> EX-DOC-03 ---------^
核心链闭合后 -> GUI/工作台 -> 跨浏览器 QA -> EX-REL-01
```
当前形式上没有未完成前置的 P0 根任务只有 `EX-ORA-01``EX-TSN-04``EX-DOC-01` 已有可继续收敛的局部实现,但整体关闭仍依赖 `EX-ORA-01`Sketcher、Part、PartDesign、FCStd 和 CAM 不作为当前新增范围,除非它们是关闭上述根任务所需的验证夹具。
## 5. 执行优先级
### P0-A锁定完整原生 oracle
对应 `EX-ORA-01/02`穷尽运行时 TypeId、Property、GUI command、成功/失败/取消状态和 feature-family mutation 夹具。没有原生基线的功能不进入 exact 实现判断。
对应 `EX-ORA-01/02`当前隔离配置下有 1111 个工作台启动命令、19 个 source-only 命令和 2 个上下文延迟命令1111 个启动命令已拆为 100 个独立原生分片,全部完成 no-document/document/selection 三状态采样。首个 `PartDesign_Pad` 命令族已完成选择和原生成功 Task 工作流,下一项只采集无活动文档时的 disabled 状态;failure、cancel、recovery 与其他命令族仍保持开放。没有原生基线的功能不进入 exact 实现判断。
### P0-B关闭跨特征拓扑命名
对应 `EX-KER-01``EX-TSN-04`完成生产 Worker 五阶段链与桌面 30-case 逐阶段对账,补齐跨特征 mutation 矩阵、builder flags 和失败分支。目标是错误绑定、未解释关系和保存重开名称漂移均为零。
对应 `EX-KER-01``EX-TSN-04`锁定 30-case corpus 已按 `9 Boolean + 11 PartDesign + 6 复合 Boolean + 4 dress-up` 逐案例执行原生属性编辑30/30 都传播到最终 Shape219/219 阶段几何恢复657 条 before/edited/restored 阶段记录完成 fail-closed 对账;恢复状态再保存、重开、二次保存后的错误绑定、未解释关系和名称漂移均为零。FreeCAD 重算仍使 11 个 PartDesign 案例的 28 个 mutation 恢复阶段产生原生命名摘要演进。生产 Worker 五阶段链另有 5/5 几何编辑恢复,但顺序链只有 2/5 原生 builder3/5 为通用 Shape 代理rotate 缺少原生 mutation family2/5 恢复用例的 5 个下游语义拓扑摘要发生变化19 类生产操作的 361 个有序组合仅分类回放 4 个,因此门禁继续 fail-closed。
### P0-C关闭 App 文档语义
对应 `EX-DOC-01/02/03`当前 85 种运行时 Property 中仍有 62 种只能 opaque 保存。15 类已观察到的 Property status 均已进入类型化位掩码和 FCStd 无损读写,其中 13 类已由锁定的 FreeCAD 1.1.1 探针和 Facade 行为覆盖:`Output/PropOutput` 不 touch`PropNoRecompute` 只传播依赖对象,`Transient/PropTransient` 保留定义但跳过值,`PropNoPersist` 完全省略。`LockDynamic``PartialTrigger` 仍是保存与原生 oracle 记录,尚未进入动态属性结构变更和 partial document observer 行为。后续依次补齐剩余 Property codec/editor、事务与 observer 顺序、局部重算失败、Extension 和 FeaturePython 边界。
对应 `EX-DOC-01/02/03`。85 种运行时 Property、5510 条记录中22 种/4304 条已有通用原生 codec5 种/683 条已有专用 codec仍有 58 种/523 条只能 opaque 保存。15 类已观察到的 Property status 均已进入类型化 Facade 行为;`LockDynamic` 的动态属性增删改事务`PartialTrigger` partial-document observer 行为已有原生 oracle 与 Facade 测试。后续依次补齐剩余 Property codec/editor、完整 observer 顺序、局部重算失败、Extension 和 FeaturePython 边界。
### P0-D关闭核心建模语义
@@ -71,7 +93,42 @@ FCStd / STEP / IGES / mesh / engineering resources
P1/P2 的 GUI 全量对标、其他工作台、代理模块和跨浏览器发布,必须在上述 P0 主链有明确前置证据后推进。当前不再以增加工作台数量作为主要进度指标。
## 5. 每项工作的完成定义
## 6. 里程碑队列
按下表自上而下推进。并行上限为“一个实现里程碑 + 一个只读 oracle 准备项”;未达到退出条件时,不切换到新的工作台或长尾模块。
| 顺序 | 任务 | 当前状态 | 交付范围 | 退出条件 |
| --- | --- | --- | --- | --- |
| M0 | 收口当前工作树 | `completed` | 将 TSN、Property、CAM/LinuxCNC 三组已有增量分别核对证据和边界,不再混入新功能 | 聚焦门禁、完整 `verify``git diff --check`、全部 JSON 解析通过;计划与报告无陈旧哈希 |
| M1 | `EX-ORA-01` 运行时清册闭合 | `in_progress` | 对 352 个注册对象、1111 个启动 GUI command、19 个 source-only command、2 个 context-only command 和 GUI 工作流状态生成差异账本 | 每个候选均为 runtime-backed 或有机器可读排除原因;成功/禁用/失败/取消/恢复工作流未知项为零 |
| M2 | `EX-TSN-04A` 漂移判定 | `ready-after-M0` | 逐项解释 11 个 PartDesign case 的 28 个恢复命名摘要演进,以及五阶段链的 5 个恢复拓扑变化 | 每个差异都有 FreeCAD 复跑证据并被判定为稳定语义、允许演进或实现缺陷;未知项为零,缺陷有回归测试 |
| M3 | `EX-TSN-04B` 转换矩阵闭合 | `after-M2` | 将 19×19 的 361 个有序操作对按原生输入/输出类型分类;原生允许的组合使用真实 builder 回放,原生拒绝的组合保存失败 oracle | 361/361 已分类;可执行组合的 history/naming/mutation/resave 通过;不可执行组合的原生错误可复现;不再用通用 Shape 代理冒充 feature builder |
| M4 | `EX-DOC-01` Property 批次 | `after-M1` | 先做高复用 App 类型与链接/列表类型,再处理 Material、File、PythonObject 等带安全或工作台边界的类型 | 每批均具备原生值/状态/editor oracle、Facade codec、FCStd reopen/resave、Undo/Redo 和 Chrome 证据opaque 数量只按机器报告下降 |
| M5 | `EX-DOC-02/03` 文档闭环 | `after-M3+M4` | observer 顺序、事务 abort、局部重算失败、Extension、FeaturePython、copy/clone/delete/relink | 原生与浏览器事件序列一致;失败不污染最后有效状态;资源归零;随后再推进 `EX-DOC-04` |
| M6 | `EX-ORA-02/KER/PART/PD/SK` 纵向切片 | `after-M2+M5` | 由 oracle 差异报告选择一个特征族一次完成参数、失败、恢复、history、命名、FCStd 和 UI Task 生命周期 | 单特征全部证据通过后才选择下一族;不按整个工作台批量提升 exact |
| M7 | `EX-FC-01/FMT-01` | `after-M3+DOC-04` | 所有已支持对象、Property、Shape、ElementMap2、StringHasher 的 FreeCAD-Web-FreeCAD 双向 mutation随后补 STEP/IGES 高级元数据 | 两次保存重开后无未知语义漂移,未知资源按合同保留,格式差异全部登记 |
| M8 | `EX06..EX10` | `deferred` | GUI 全量、生产/工程工作台、代理策略、跨浏览器、性能、安全和发布 | 52/52 exact 任务、34/34 exact 模块及最终 promotion report 全部满足后才执行 `EX-REL-01` |
M3 不要求把 FreeCAD 本身拒绝的组合强行实现为顺序特征链。正确证据是:先由原生 FreeCAD 判断组合是否合法,再分别保存成功 builder 证据或稳定失败证据。M4 也不按记录数盲目排序;`PropertyPythonObject`、文件路径和 Material 类型必须先确定执行、安全和资源边界。
### 6.1 微任务队列
机器真值为 `config/freecad-active-work-queue.json`,由 `generate:freecad-active-work-queue` 从原生报告生成,并由 `check:freecad-active-work-queue` fail-closed 校验。当前共 487 个微任务112 `completed`、1 `in_progress`、374 `pending``maxInProgress=1`,唯一下一任务是 `ORA-GUI-WF-002`
| 微里程碑 | 任务粒度 | 数量 | 状态 | 下一退出条件 |
| --- | --- | ---: | --- | --- |
| `ORA-GUI-BASELINE` | 1 个配置边界或 1 个 GUI 命令分片 | 106 | 106 completed | 已关闭100/100 分片、1111/1111 命令状态、2 个上下文延迟命令均有证据 |
| `ORA-GUI-WORKFLOWS` | 1 个命令族的 1 种工作流状态 | 7 | 2 completed / 1 in_progress / 4 pending | `PartDesign_Pad` 选择与成功 Task 已完成;当前只执行 `ORA-GUI-WF-002` disabled 状态 |
| `TSN-RECOVERY-DRIFT` | 1 个原生 case 或 1 个生产 mutation | 13 | 13 pending | GUI workflow 七步关闭后,从 `partdesign-plain` 单 case 开始 |
| `TSN-ORDERED-PAIRS` | 1 个有序操作对 | 361 | 4 completed / 357 pending | 每次只分类并回放一个 `from->to`,不按特征族批量关闭 |
`ORA-GUI-WORKFLOWS` 严格按七步执行:选定命令族、成功、禁用、失败、取消、恢复、更新 blocker/门禁。一步只新增一种原生状态证据,前一步未通过时不启动后一步。
`TSN-RECOVERY-DRIFT` 的 13 个独立任务按以下顺序执行:`partdesign-plain``partdesign-midplane``partdesign-reverse``partdesign-taper``partdesign-twoside``partdesign-pocket``partdesign-pocket-through``partdesign-pocket-midplane``partdesign-pocket-twoside``partdesign-pocket-taper``partdesign-pocket-up-to-face`、生产 `cut`、生产 `rotate`。每个任务只允许一种结论:稳定语义、允许演进或实现缺陷;不得以几何恢复代替命名解释。
100 个 GUI 分片分别对应 `ORA-GUI-SHARD-000..099`,每片覆盖排序命令全集中的 11 或 12 个命令。每片有独立报告、SHA-256、起止索引和退出条件合并器要求 100 个区间连续、1111 个 ID 唯一且全集哈希一致。
## 7. 每项工作的完成定义
一个 FreeCAD 能力只有同时具备以下证据,才可关闭对应 exact 任务:
@@ -84,15 +141,49 @@ P1/P2 的 GUI 全量对标、其他工作台、代理模块和跨浏览器发布
7. 聚焦测试、相关 Chrome gate、计划/能力矩阵和差异说明同步更新。
8. exact promotion gate 不依靠改写报告或放宽断言通过。
## 6. 近期工作队列
## 8. 验证矩阵
1. 保持现有验证资产可复现,先处理任何 `failed` 浏览器 gate再开始新里程碑
2. 完成 `EX-TSN-04` 的生产五阶段链与桌面 resave 逐阶段相关性对账。
3.`EX-ORA-02` 输出的 feature-family 缺口中选择一个 P0 特征族,补齐 mutation、失败、恢复、round-trip 全证据。
4. 以原生 Property 缺口报告为输入推进 `EX-DOC-01`,每批只提升有 codec 和状态语义证据的类型。
5. 每个里程碑运行聚焦检查;准备合并时再运行 `./npmw run verify``git diff --check` 和 JSON 校验。
每次修改先运行所属聚焦门禁;只有聚焦门禁通过后才运行全量门禁
## 7. 明确不做
TSN/命名批次:
```bash
./npmw run check:freecad-composite-history-elementmap
./npmw run check:freecad-tsn-stage-correlation
./npmw run check:freecad-exact-history-elementmap-gate
./npmw run check:freecad-naming-production
./npmw run check:chrome-freecad-naming-production
```
Property/文档批次:
```bash
./npmw run check:freecad-native-property-semantics
./npmw run check:freecad-property-status
./npmw run test:facade
./npmw run check:freecad-fcstd-roundtrip
./npmw run check:chrome-fcstd-semantic
```
合并前统一门禁:
```bash
./npmw run check:execution-plan
./npmw run verify
git diff --check
find config -maxdepth 1 -type f -name '*.json' -print0 | xargs -0 -n1 jq empty
```
若修改了 oracle/harness 或验证报告生成逻辑,必须先实际执行对应 `probe:*`/`test:chrome-*`,再运行只读 `check:*`;静态旧报告不能作为完成证据。
## 9. 维护规则
- 本文只维护当前状态、关键路径和下一批队列;长期范围仍由 exact JSON 账本管理,历史事实只追加到 continuation 日志。
- 每个里程碑只更新自己直接影响的实现、测试、verification、exact blocker 和能力矩阵,不顺手刷新无关报告。
- `completed` 必须对应机器账本中的验收范围;局部进展写入 blocker 数字,不提前改状态。
- 任何 `failed` 浏览器 gate、陈旧 artifact hash、资源泄漏或未知语义差异优先于新增能力。
## 10. 明确不做
- 不把项目改成远程桌面、服务端截图流或仅 UI 仿制品。
- 不新增绕过 Facade 的几何、渲染、脚本或存储入口。

View File

@@ -20,7 +20,7 @@
2. 生成 TypeId、Property、GUI command、format、entrypoint inventory每个候选必须有源码位置和 runtime 状态。
3. 建立成功、失败、禁用、取消、重算、保存重开六类 oracle fixture。
4. 运行 `npm run check:freecad-inventory``check:freecad-type-properties``check:freecad-gui-commands``check:freecad-entrypoints`
5. 运行 `npm run check:freecad-oracle-coverage`,核对 34 个模块1113 个 GUI 命令是否与真实桌面报告逐项一致;该 gate 同时报告未构建模块和仍待逐对象 probe 的静态候选。
5. 运行 `npm run check:freecad-oracle-coverage`,核对 34 个模块、1111 个工作台启动 GUI 命令、19 个 source-only 命令和 2 个 context-only 命令是否与隔离的真实桌面报告逐项一致;该 gate 同时报告未构建模块和仍待逐对象 probe 的静态候选。
6. 运行 `npm run check:freecad-golden-coverage`,按 feature family 核对成功、失败、history、复合阶段、ElementMap2、FCStd 往返和浏览器回放七个维度;用 `-- --details` 输出逐 family 缺口。新增原生 feature-family fixture 时必须同时通过 `test:golden:freecad:families``test:golden:freecad:family-failures`,任何 missing 项继续保留为 EX-ORA-02 blocker。
7. 运行 `npm run check:freecad-partdesign-failures``npm run check:freecad-partdesign-revolution-groove``npm run check:freecad-partdesign-transform``npm run check:freecad-part-builders`。原生缺输入后仍为 `Up-to-date + Valid + null Shape` 的 feature 必须分类为 `accepted-empty`,不能计入失败覆盖;只有真实 rejected 诊断才能关闭 `failure-fixture` 维度。局部重算失败保留旧 Shape 时,必须同时保存 `Invalid` 状态、原生诊断和 stale Shape不得把旧 Shape 当作成功结果。

View File

@@ -421,6 +421,14 @@ EX-ORA-02 的 mutation corpus 由 `config/freecad-parameter-mutation-plan.json`
30 个复合模型的 219 个文档阶段已通过 FreeCAD 运行时 `getElementMappedName/getElementIndexedName/getElementHistory` 获得直接命名证据,并由两个门禁逐阶段验证。此处的 `native-evidence=219/219` 不等于内部 builder exact219 个阶段都有 IndexedName但只有 74 个阶段的全部子形有 MappedName145 个阶段只有原生索引身份;当前识别出的 42 个操作阶段仍为 `internalBuilderEvidence=0`。没有内部 builder Shape/history 时,最终几何不能用于制造中间 Generated/Modified/Deleted没有实际 StringHasher/MappedNameRef 时也不能制造私有 token同构候选没有唯一原生来源时继续持久化为 ambiguous。
### 22.1 30-case 原生 mutation 与恢复门2026-08-14
`freecad-composite-history-elementmap-oracle` 现把固定语料锁定为 `9 Boolean + 11 PartDesign + 6 复合 Boolean + 4 dress-up`,每个 case 声明目标对象、原生 Property TypeId、editor mode、编辑值和最终对象。采集器对每个 case 执行 before/edit/recompute/restore/recompute并为全部 219 个阶段保存三相几何、关系和命名摘要,共 657 条阶段记录。解析几何摘要包含拓扑计数、包围盒、顶点坐标、边长、面面积、面积和体积,不依赖 B-Rep 文本中的瞬时内部布局;原始 B-Rep SHA-256 仍单独保留作证据。
锁定 FreeCAD 1.1.1 实跑结果为 `mutation=30/30`、最终传播失败 `0`、几何恢复失败 `0/219`。mutation 恢复后的状态才作为 FCStd nominal 基线30 个 case 再完成 save/reopen/resave219 个阶段的 `roundtripNameDrift=0``resaveNameDrift=0`。FreeCAD 重算过程中11 个 PartDesign case 的 28 个阶段在参数恢复后产生不同的原生 relation/semantic-name digest该差异被单独锁定为 mutation naming drift不能与几何恢复混算也不能被零保存漂移掩盖。
`check:freecad-composite-history-elementmap``check:freecad-tsn-stage-correlation``check:freecad-exact-history-elementmap-gate` 共同按 case id、类别配额、stage ordinal 和三相 identity fail-closed。来源解析同时接受 FreeCAD 原生 `FaceN/EdgeN/VertexN``#d:1``#42``g1v2;SKT` 语法,不再把原生局部 token 误判为未解释关系。当前三门结果均为 `wrongBindings=0``unexplainedRelations=0`,但 mutation naming drift、五阶段 proxy/builder 缺口及未分类操作对仍使 `EX-TSN-04=in_progress``exactPromotionReady=false`
### 23. EX-PD-02 Attachment 与跨文档引用闭合2026-08-13
`EX-PD-02` 已按独立机器证据关闭。FreeCAD 1.1.1 原生 Attachment 注册表仍为 55 项,四类引擎实现并集为 50 项;新增全组合 oracle 将 Plane/Sketch/Line/Point 的所有模式及所有引用组合展开为 236 个可执行槽位。每项都通过初始重算、统一源几何平移和 FCStd 关闭重开Placement 与支持引用无漂移。原有未实现模式反例仍要求 `Touched + Invalid`,不会因本批完整执行覆盖而被误判为实现。

View File

@@ -0,0 +1,373 @@
# FreeCAD Web 转换执行总计划
更新时间2026-08-14
## 1. 目标、口径与真值
项目唯一目标是把锁定的 FreeCAD `1.1.1` 能力迁移到浏览器,并保留 FreeCAD 的文档、属性、重算、拓扑命名、文件和交互语义。项目不是远程桌面、截图流,也不是只复刻界面的通用 Web CAD。
进度必须同时区分两种口径:
1. 兼容实现口径:`config/freecad-execution-plan.json` 的 P01-P10 共 83 项均已完成,证明当前 Web 应用有可运行实现、门禁和明确能力边界。
2. exact 对标口径:`config/freecad-web-exact-parity-plan.json` 共 52 项,当前为 5 completed、36 in_progress、11 pending34 个模块为 0 exact、26 compatible、5 proxy、3 development。
因此当前结论是“可运行的 FreeCAD 风格 Web CAD 兼容基线已形成”不是“FreeCAD 已完整 Web 化”。只有 `EX-REL-01` 关闭、52/52 exact 任务完成、34/34 模块为 exact 且未知差异为零,才能改变系统级 exact claim。
发生状态冲突时按以下顺序取真值:
1. `config/freecad-web-exact-parity-plan.json`exact 任务、依赖、阻断项。
2. `config/release-capability-matrix.json`34 模块可对外声明的等级。
3. `config/freecad-active-work-queue.json`:当前唯一在执行的微任务。
4. `config/freecad-execution-plan.json`:兼容实现 P01-P10 的完成情况。
5. `config/*-verification.json`、原生报告和 Chrome 报告:某个环境中的实际证据。
6. 本文与 `docs/current-work.zh-CN.md`:解释、顺序和交接入口。
## 2. 当前完成情况重审
### 2.1 已形成闭环的兼容基线
- React/Vite FreeCAD 风格应用壳、菜单、工作台、Combo View、Task、视口、Report/Jobs/Diagnostics 和状态栏可以运行。
- React 只通过 `BitBybitWebCadFacade` 进入 CAD 领域Three.js、OCCT、SQLite/OPFS 和 Worker 没有成为 UI 业务旁路。
- Bitbybit OCCT Worker、原生 history provider、FreeCAD 私有命名 ABI、planegcs WASM 和 SQLite OPFS 均已有构建或运行门禁。
- Part、PartDesign、Sketcher、FCStd 和 26 个 compatible 模块已有代表性实现与 Chrome 证据。
- CAM 已有 CAD -> OpenCAMLib -> CAMotics -> G-code -> LinuxCNC WASM 的真实浏览器验证链。
- P01-P10 的 83/83 项兼容任务已完成,发布门 G0-G9 在该口径下通过。
### 2.2 已完成的 exact 独立切片
- `EX-TSN-00`:缺失原生命名或 builder 证据时 fail-closed。
- `EX-TSN-01`:已支持 builder 的逐阶段 Shape 与 Generated/Modified/Deleted 证据。
- `EX-TSN-02`:生产 FreeCAD 私有 MappedNameRef/StringHasher 命名来源。
- `EX-TSN-03`:同构拓扑只从唯一原生 provenance 稳定化,多来源保持 ambiguous。
- `EX-PD-02`Attachment、Datum、ShapeBinder/SubShapeBinder、多 Body 与 XLink 重连生命周期的独立验收范围。
这些任务按自身验收范围关闭,不表示它们依赖的整个 PartDesign、文档或内核任务已经 exact。
### 2.3 已量化但尚未关闭的 P0 阻断
- Oracle352 个注册对象、348 个可实例化对象、4 个不可实例化对象、1111 个启动 GUI command、19 个 source-only command 和 2 个 context-only command 已分类;命令族的成功、禁用、失败、取消、恢复流程仍未穷尽。
- Property85 种运行时类型、5510 条记录中22 种/4304 条为通用原生 codec5 种/683 条为专用 codec58 种/523 条仍为 opaque proxy。
- Property status15 类已观察状态均已进入类型化 Facade 行为;`LockDynamic``PartialTrigger` 已有原生事务证据,但完整 observer 与局部重算序列未关闭。
- TSN30/30 原生 mutation、219/219 几何阶段恢复通过11 个 PartDesign case 的 28 个恢复命名摘要演进仍需逐 case 解释。
- 生产命名链:五阶段 5/5 几何恢复,但仍有 3/5 通用 Shape 代理、缺 rotate mutation family、5 个下游语义拓扑恢复漂移。
- 有序操作19×19 共 361 对,仅 4 对已有分类回放357 对待原生接受/拒绝判定。
- FCStd支持的原生 codec 与未知资源保留已有证据,但所有对象/属性/Extension/Shape/ElementMap2/StringHasher 的 FreeCAD-Web-FreeCAD 双向变异未穷尽。
- GUI应用壳和代表性工作流已有浏览器证据但动态命令、Task、对话框、焦点、上下文和逐工作台流程没有完整原生对照。
- 发布:系统级 exact 为 false`EX-REL-01` 为 pending跨浏览器、长期运行和全模块 exact promotion 尚未开始。
## 3. 目标架构和不可破坏边界
```text
React / FreeCAD Web UI
|
BitBybitWebCadFacade + Command/Task contracts
|
Document + Property + Transaction + DAG/Recompute
|
Geometry / Sketch / Naming / Project / CAM facades
|
Dedicated Workers
|
Bitbybit OCCT + FreeCAD naming + planegcs + SQLite OPFS
|
FCStd / STEP / IGES / mesh / CAM / engineering resources
```
任何后续任务都必须满足以下边界:
- UI 不直接调用 Three.js、OCCT、OPFS、SQLite 或脚本执行器。
- 最终 Shape 相似不能替代逐阶段 history、命名、文档状态和 FCStd 证据。
- mock 和 Node 单测不能替代真实 Chrome Worker/WASM 回放。
- 未知属性和资源必须无损保留或拒绝编辑,不能猜测性改写。
- 原生来源不唯一时保持 ambiguous不能按数组顺序或网格索引稳定化。
- FreeCAD 本身拒绝的组合保存原生失败 oracle不强行实现为成功链。
- 每次只允许一个机器队列任务为 `in_progress`,避免同时扩展多个工作台。
## 4. 最小任务和完成定义
一个“最小实现任务”只处理一个命令、属性类型、特征参数分支、原生 case 或有序操作对。每项按以下九个独立检查点推进:
1. `A-inventory`锁定源码位置、TypeId/command id、依赖和适用对象。
2. `B-native-success`:采集 FreeCAD 1.1.1 成功结果、状态、事件和资源。
3. `C-native-negative`:分别采集 disabled、failure、cancel不适用时保存可验证原因。
4. `D-native-mutation`:编辑输入、重算、恢复,并记录逐阶段 Shape/history/naming。
5. `E-facade`:在 Facade/Worker 边界内实现结构化合同和失败诊断。
6. `F-transaction`:覆盖 Undo/Redo、abort、stale generation 和最后有效状态保留。
7. `G-roundtrip`FreeCAD 保存 -> Web 读取/修改/保存 -> FreeCAD 重开/再保存。
8. `H-browser`:真实 Chrome Worker/WASM/OPFS 回放,结束后 Shape/Worker/资源归零。
9. `I-promotion`:聚焦 check、计划 blocker、能力矩阵和差异账本同步。
只有 A-I 全部完成,最小能力才能提升 exact。若任务仅为 oracle 准备,则只关闭它明确声明的证据范围,不提前关闭实现任务。
## 5. 当前串行微任务队列
当前机器队列由 `scripts/generate-freecad-active-work-queue.mjs` 生成。每个条目只允许一个产物、一个聚焦检查和一个退出条件。
### 5.1 ORA GUI 基线106 项,已完成
- `ORA-GUI-SETUP-000..002`:拆分 GUI 探针、诊断 BIM 首启弹窗、隔离用户配置。
- `ORA-GUI-SHARD-000..099`100 个连续分片,覆盖 1111 个命令的 no-document/document/selection 三态。
- `ORA-GUI-MERGE-000`:合并分片并校验 ID 全集、连续区间和 SHA-256。
- `ORA-GUI-CONTEXT-000..001`:分类 `Import_ReadBREP``NaviCubeDraggableCmd` 的延迟注册边界。
### 5.2 ORA GUI 工作流7 项,执行中
- `ORA-GUI-WF-000`:选择首个命令族。已选 `PartDesign_Pad`,映射主任务 `EX-UI-03`,关联 `EX-ORA-01/EX-PD-01`
- `ORA-GUI-WF-001`Pad 原生成功工作流和聚焦断言已完成Task 打开/接受、Body Tip、体积、拓扑和 Undo 证据通过。
- `ORA-GUI-WF-002`:只采集无活动文档时的 disabled 工作流,执行中。
- `ORA-GUI-WF-003`:只采集无效 open-wire profile 的 failure 工作流。
- `ORA-GUI-WF-004`:只采集预览后 Cancel 的回滚、选择和焦点工作流。
- `ORA-GUI-WF-005`:只采集失败/取消后修复、成功、Undo/Redo、保存重开的恢复工作流。
- `ORA-GUI-WF-006`:更新 exact blocker、聚焦门禁和下一命令族选择规则。
### 5.3 TSN 恢复漂移13 项,待 ORA 工作流
按以下顺序一次解释一个 case
1. `partdesign-plain`
2. `partdesign-midplane`
3. `partdesign-reverse`
4. `partdesign-taper`
5. `partdesign-twoside`
6. `partdesign-pocket`
7. `partdesign-pocket-through`
8. `partdesign-pocket-midplane`
9. `partdesign-pocket-twoside`
10. `partdesign-pocket-taper`
11. `partdesign-pocket-up-to-face`
12. 生产 `cut`
13. 生产 `rotate`
每项必须得到且只能得到一种结论FreeCAD 稳定语义、允许的命名演进、实现缺陷。实现缺陷必须增加回归测试;未知不能作为结论。
### 5.4 TSN 有序操作对361 项4 完成/357 待办
操作全集为 Fuse、Cut、Common、Rotate、Pad、Pocket、Loft、Pipe、Revolution、Groove、Fillet、Chamfer、Hole、Draft、Thickness、LinearPattern、PolarPattern、Mirrored、MultiTransform。
每个 `from -> to` 是一个独立任务:
1. 确定 `from` 输出类型与 `to` 输入前置。
2. 在原生 FreeCAD 执行并记录接受或拒绝。
3. 接受时使用真实 builder采集 history/naming。
4. 编辑 `from` 参数并重算 `to`
5. 恢复参数并分类命名/拓扑变化。
6. 保存、重开、再保存并比较。
7. 在生产 Worker 回放并检查释放。
8. 拒绝时固定原生错误类型和文档未污染证据。
已覆盖的四对为 `cut->rotate``rotate->fillet``fillet->mirrored``mirrored->linear-pattern`。其余严格按机器队列逐对领取。
## 6. 后续关键路径微里程碑
### M00 当前工作树收口
状态completed聚焦门禁、完整 `verify`、发布产物验签、JSON 和 diff 校验均通过。
- M00.1 TSN 原生报告、resave 和 stage correlation 对账。
- M00.2 Property codec/status、Facade 行为和 FCStd 证据对账。
- M00.3 CAM/LinuxCNC artifact hash、浏览器报告和 parser authority 对账。
- M00.4 GUI 分片、source/context-only 分类和 oracle coverage 对账。
- M00.5 `git diff --check`、全部 JSON 解析、完整 `verify`
退出条件:所有生成文件未陈旧,全部门禁通过,没有以报告改写替代真实执行。
### M01 `EX-ORA-01` GUI 工作流闭合
状态:执行 `ORA-GUI-WF-002`
先完成 Pad 七步,再按风险选择下一族。每个后续命令族仍拆成 success/disabled/failure/cancel/recovery/update 六个状态任务。选择顺序:核心文档命令 -> Sketcher 创建/编辑 -> PartDesign 参数 Task -> Part builder -> FCStd import/export -> 其他工作台。
退出条件:每个运行时命令归入一个工作流族;每族五态都有原生证据或机器可读不适用原因;未知 GUI 状态为零。
### M02 `EX-TSN-04A` 恢复漂移闭合
状态:等待 M01。
- M02.1-M02.1111 个 PartDesign case逐 case 解释 28 个命名摘要演进。
- M02.12:生产 Cut 恢复拓扑漂移。
- M02.13:生产 Rotate 恢复拓扑漂移。
- M02.14:若发现实现缺陷,只修改一个 operation/case 并补回归。
- M02.15:汇总 zero-unknown drift gate不直接提升系统 exact。
退出条件13/13 case 有分类,未知为零,缺陷修复均有原生与浏览器证据。
### M03 `EX-TSN-04B` builder 与 19×19 转换矩阵
状态:等待 M02。
- M03.1:用真实 builder 替换五阶段链中 Cut 之后的通用 Shape 代理,一次替换一个 stage。
- M03.2:增加 Rotate 原生 mutation family。
- M03.3:按 361 个独立任务完成有序组合分类。
- M03.4:每个允许组合完成 mutation、restore、naming、resave、浏览器释放。
- M03.5:每个拒绝组合固定错误和无文档污染证据。
退出条件361/361 分类;所有原生允许组合为真实 builder所有拒绝组合可稳定复现0 generic Shape proxy、0 未解释恢复漂移。
### M04 `EX-DOC-01` Property codec 批次
状态:等待 M01可在不改变单任务并行上限的前提下准备只读 oracle。
每个类型严格执行第 4 节 A-I。批次内一次只领取一个 Property 类型,按复用度和安全风险排序:
- M04.A 量纲标量Acceleration、Area、Force、HeatFlux、Moment、Power、Pressure、Speed、Stiffness、StiffnessDensity、Temperature、ThermalTransferCoefficient、VectorDistance、Quantity。
- M04.B 数学与集合BoolList、Color、ColorList、Direction、Font、IntegerSet、Map、Matrix、PlacementList、Position、Rotation、UUID、VectorList。
- M04.C 链接族LinkListHidden、LinkSubHidden、LinkSubListGlobal、XLink、XLinkList、XLinkSub、XLinkSubList。
- M04.D MaterialApp PropertyMaterial/PropertyMaterialList 与 Materials::PropertyMaterial先固定 schema、继承和资源边界。
- M04.E 文件资源PropertyFile、PropertyFileIncluded、PropertyPath禁止把宿主绝对路径写入浏览器项目。
- M04.F PythonObject先定义禁止执行、可信序列化、代理编辑和迁移边界再决定 codec不得反序列化执行任意 Python。
- M04.G 几何/求解内核Part ShapeHistory/TopoShapeList/FilletEdgesMesh MeshKernel/CurvatureListFem FemMesh/PostDataObjectPoints PointKernelRobot Trajectory。
- M04.H 工作台专用Spreadsheet Sheet/RowHeights/ColumnWidthsTechDraw CenterLine/CosmeticEdge/CosmeticVertex/GeomFormat listsInspection DistanceList。
每个类型的退出条件:原生默认/非默认/边界值、所有观察 status、Data/View editor、动态增删、Undo/Redo、FCStd reopen/resave、Chrome 回放和资源释放全部通过。只有运行时报告变为 native codec 后opaque 计数才允许下降。
### M05 `EX-DOC-02/03` 文档语义
状态:等待 M03+M04。
- M05.1 对象 add/remove/rename 的 observer 前后事件顺序。
- M05.2 Property add/remove/rename/change 的事务顺序。
- M05.3 nested transaction、commit、abort、undo、redo。
- M05.4 DAG 脏标记、局部重算和跨分支顺序。
- M05.5 recompute 失败时保留最后有效 Shape 与诊断树。
- M05.6 partial document load、PartialTrigger、完整加载后重算。
- M05.7 copy/clone/delete/relink/group/extension 生命周期。
- M05.8 Extension 动态属性、版本迁移和未知 Extension 保留。
- M05.9 FeaturePython 默认禁执行、代理、签名能力和显式授权边界。
- M05.10 外链关闭、缺失、重开、路径恢复与多文档事务。
退出条件:原生和 Web 事件序列一致;失败不污染有效状态;重开后 DAG/链接/事务语义稳定;资源归零。
### M06 `EX-KER-01/ORA-02` 内核纵向切片
状态:等待 M02+M05 的可消费证据。
按一个 builder/参数分支执行 A-IBox、Cylinder、Cone、Sphere、Torus、Prism、Wedge、Helix、EllipsoidExtrusion、Revolution、Loft、SweepFuse、Cut、CommonFillet、Chamfer随后是 tolerance、healing、validation 和失败诊断。
退出条件参数、单位、placement、公差、有效性、质量属性、逐阶段 history、错误和 Worker 所有权均与原生 fixture 一致。
### M07 `EX-SK-01/02` Sketcher 纵向切片
状态:等待 KER/DOC 前置。
- M07.1 直线/圆/圆弧/椭圆/椭圆弧/双曲线/抛物线/B-spline 的创建与编辑。
- M07.2 几何约束每个 overload 的成功、冗余、冲突和退化。
- M07.3 尺寸、参考、Driving/Driven、单位和表达式。
- M07.4 外部几何 Projection/Intersection/Both 与 TopoRef 漂移。
- M07.5 InternalAlignment、SnellsLaw、B-spline Weight 辅助几何。
- M07.6 autoconstraint、trim/extend/split、construction、virtual space。
- M07.7 任务焦点、选择、键盘、cancel 和 solver stale/crash 恢复。
退出条件:每个 geometry/constraint overload 经过 A-I求解分类、自由度和诊断与原生一致。
### M08 `EX-PART-01` Part 纵向切片
状态:等待 M03+M06。
每个 primitive 和 builder 单独闭环;随后逐项完成 Boolean 参数、Refine、placement、multi-solid、healing、inspection、失败与恢复。不能用一个成功 Box 或最终体积代表整个 Part 工作台。
退出条件:所有注册 Part command/TypeId/参数分支均有原生与浏览器证据,未知选项为零。
### M09 `EX-PD-01` PartDesign 纵向切片
状态:等待 M03+M08。
顺序为 Pad -> Pocket -> Revolution -> Groove -> Additive/Subtractive Loft -> Additive/Subtractive Pipe -> Fillet -> Chamfer -> Hole -> Draft -> Thickness -> Linear/Polar Pattern -> Mirrored -> MultiTransform。每个特征再按模式拆分例如 Length/ThroughAll/UpToFace、Midplane/Reverse/TwoLengths/Taper、成功/无效 profile/失效 support/取消/恢复。
退出条件:每个模式完成 A-IBody Group/Tip、Support/Attachment、跨特征命名和 FCStd 保持一致。
### M10 `EX-FC-01/FMT-01` FCStd 与核心格式
状态:等待 M03+DOC-04。
- M10.1 每种已支持对象/Property 的 FreeCAD -> Web 无修改往返。
- M10.2 Web 修改 -> FreeCAD 重开 -> FreeCAD 再保存 -> Web 重开。
- M10.3 Shape、ElementMap2、StringHasher、Expression、GuiDocument 资源逐项变异。
- M10.4 未知 XML、BRep、脚本和 Extension 的字节保留或只读拒绝。
- M10.5 损坏 ZIP、路径穿越、压缩炸弹、超限和取消。
- M10.6 STEP/IGES/BREP 的单位、颜色、层、名称、装配关系和高级元数据。
退出条件:两次保存重开后未知语义漂移为零;不支持资源有明确只读/代理策略。
### M11 `EX-UI-01..05` GUI 全量对标
状态:先执行 M01完整阶段等待文档与核心特征稳定。
按应用壳、菜单、工具栏、工作台切换、MDI、Combo View、模型树、Task、Data/View 属性、对话框、快捷键、上下文菜单、状态栏、Report/Jobs、3D 选择逐域拆分。每域分别采集空文档、活动文档、单选、多选、Task、错误、取消和恢复状态随后按工作台完成端到端流程。
退出条件:命令可见/启用、事务、焦点、选择、树、属性、视图和诊断均与 oracle 对齐;桌面和 390px 视口无重叠、溢出和无名控件。
### M12 文档与工程工作台
状态deferred。
顺序为 Draft -> Spreadsheet -> TechDraw -> Plot -> 跨工作台文档闭环Assembly -> BIM/IFC -> Surface -> Mesh/MeshPart -> Material。每个对象/命令仍按 A-I不按整个工作台一次性提升 exact。
退出条件:相关 exact 任务逐项完成,跨工作台链接、表达式、导出和保存重开无未知差异。
### M13 平台工作台、脚本与代理
状态deferred。
顺序为 FEM、CAM、Robot、Points/ReverseEngineering、Inspection/Measure、Python/Macro、Addon最后处理 Cloud/Help/IDF/JT/OpenSCAD 的 browser implementation、sidecar、proxy 或正式排除。安全边界先于功能数量。
退出条件:每项均有可执行策略、依赖/许可证/SBOM、能力限制和失败策略代理不得被标记 exact。
### M14 QA、跨浏览器与发布
状态deferred日常门禁持续运行。
- M14.1 Chrome 当前基线重跑。
- M14.2 Firefox/Safari 或明确不支持的可复核边界。
- M14.3 Windows/macOS/Linux 与 GPU/软件渲染矩阵。
- M14.4 键盘、屏幕阅读器、locale、长文本和窄屏。
- M14.5 fuzz、1000 次重算、长会话、崩溃、迁移和回滚。
- M14.6 性能、WASM/JS/GPU/OPFS 保留量与资源归零。
- M14.7 SBOM、许可证、签名、离线安装和发布回滚。
- M14.8 52/52 exact、34/34 exact、零未知差异的 promotion report。
退出条件:`EX-REL-01` 的所有前置完成promotion gate 从 fail-closed 报告自然变为通过,不能通过放宽断言或改写静态报告达成。
## 7. 执行顺序
当前只允许以下主顺序:
```text
M00
-> M01 ORA GUI workflows
-> M02 TSN drift
-> M03 TSN ordered pairs / real builders
-> M04 Property codecs
-> M05 Document semantics
-> M06 Kernel oracle slices
-> M07 Sketcher
-> M08 Part
-> M09 PartDesign
-> M10 FCStd/core formats
-> M11 GUI full replay
-> M12 document/engineering workbenches
-> M13 platform/script/proxy
-> M14 QA/release
```
只读 oracle 准备可以与当前实现任务交错,但不能让机器队列出现第二个 `in_progress`,也不能绕过依赖提前提升 exact 状态。
## 8. 每轮执行模板
1.`config/freecad-active-work-queue.json` 读取唯一 `nextTask`
2. 验证依赖与输入报告哈希,确认没有陈旧 artifact。
3. 只修改该任务直接影响的 oracle、实现、测试、报告和 blocker。
4. 先运行该域聚焦 probe/test再运行只读 check。
5. 运行 `test:facade`、相关 Chrome gate、`check:execution-plan`
6. 运行完整 `./npmw run verify``git diff --check` 和全部 JSON 解析。
7. 将当前任务改为 completed并只把直接后继改为 in_progress。
8. 更新 `docs/current-work.zh-CN.md` 的数字和下一任务;历史事实追加到 continuation 日志。
任何失败报告、资源泄漏、哈希陈旧、未知语义差异都优先于新增功能。
## 9. 本轮实际启动点
`ORA-GUI-WF-000` 已完成:首个命令族为 `partdesign-pad-task`,代表命令 `PartDesign_Pad`,主 exact 映射为 `EX-UI-03`,关联 `EX-ORA-01/EX-PD-01`。选择依据和五态配方保存在 `config/freecad-gui-workflow-plan.json`,由 `check:freecad-gui-workflow-plan` 校验。
`ORA-GUI-WF-001` 也已完成:锁定 FreeCAD 1.1.1 的 Pad 成功 GUI 工作流通过原生命令打开 Task点击 `Done` 后清理 edit 状态,得到 `PartDesign::Pad`、Body Tip=Pad、体积 120、6 面/12 边/8 点和 UndoCount=1。
当前唯一下一任务为 `ORA-GUI-WF-002`:只采集无活动文档时 `PartDesign_Pad` 的 disabled 状态并证明调用被阻止且不创建文档、Task 或事务。failure/cancel/recovery 必须留给各自后继任务。

View File

@@ -14,7 +14,9 @@
"check:baseline": "node scripts/check-baseline.mjs",
"check:freecad-source": "node scripts/verify-freecad-source.mjs",
"check:bitbybit-history": "node scripts/check-bitbybit-history-surface.mjs",
"check:execution-plan": "node scripts/check-freecad-execution-plan.mjs && node scripts/check-freecad-web-exact-parity-plan.mjs && node scripts/check-freecad-web-exact-promotion.mjs && node scripts/check-freecad-oracle-coverage.mjs && node scripts/check-freecad-golden-coverage.mjs && node scripts/check-freecad-core-parameter-mutation-oracle.mjs && node scripts/generate-freecad-parameter-mutation-report.mjs --check && node scripts/check-freecad-tsn-stage-evidence.mjs",
"check:execution-plan": "node scripts/check-freecad-execution-plan.mjs && node scripts/check-freecad-web-exact-parity-plan.mjs && node scripts/check-freecad-web-exact-promotion.mjs && node scripts/check-freecad-oracle-coverage.mjs && node scripts/check-freecad-gui-workflow-plan.mjs && node scripts/check-freecad-gui-workflow-oracle.mjs && node scripts/check-freecad-golden-coverage.mjs && node scripts/check-freecad-core-parameter-mutation-oracle.mjs && node scripts/generate-freecad-parameter-mutation-report.mjs --check && node scripts/check-freecad-tsn-stage-evidence.mjs && node scripts/check-freecad-tsn-stage-correlation.mjs && node scripts/generate-freecad-active-work-queue.mjs --check",
"generate:freecad-active-work-queue": "node scripts/generate-freecad-active-work-queue.mjs",
"check:freecad-active-work-queue": "node scripts/generate-freecad-active-work-queue.mjs --check",
"check:freecad-web-exact-parity-plan": "node scripts/check-freecad-web-exact-parity-plan.mjs",
"check:freecad-web-exact-promotion": "node scripts/check-freecad-web-exact-promotion.mjs",
"generate:freecad-inventory": "node scripts/generate-freecad-source-inventory.mjs",
@@ -27,6 +29,9 @@
"check:freecad-property-status": "node scripts/check-freecad-property-status-oracle.mjs",
"generate:freecad-gui-commands": "node scripts/generate-freecad-gui-command-inventory.mjs",
"check:freecad-gui-commands": "node scripts/check-freecad-gui-command-inventory.mjs",
"check:freecad-gui-workflow-plan": "node scripts/check-freecad-gui-workflow-plan.mjs",
"probe:freecad-gui-workflow": "node scripts/run-freecad-gui-workflow-oracle.mjs",
"check:freecad-gui-workflow": "node scripts/check-freecad-gui-workflow-oracle.mjs",
"generate:freecad-entrypoints": "node scripts/generate-freecad-entrypoint-inventory.mjs",
"check:freecad-entrypoints": "node scripts/check-freecad-entrypoint-inventory.mjs",
"check:freecad-oracle-coverage": "node scripts/check-freecad-oracle-coverage.mjs",
@@ -47,6 +52,8 @@
"check:freecad-fcstd-roundtrip": "node scripts/check-freecad-fcstd-roundtrip-verification.mjs",
"probe:freecad-composite-history-elementmap": "node scripts/run-freecad-composite-history-elementmap-oracle.mjs",
"check:freecad-composite-history-elementmap": "npx tsx scripts/check-freecad-composite-history-elementmap-oracle.mjs",
"probe:freecad-tsn-stage-correlation": "node scripts/run-freecad-tsn-stage-correlation-oracle.mjs",
"check:freecad-tsn-stage-correlation": "node scripts/check-freecad-tsn-stage-correlation.mjs",
"check:freecad-exact-history-elementmap-gate": "npx tsx scripts/check-freecad-exact-history-elementmap-gate.mjs",
"check:freecad-native-naming-evidence": "npx tsx scripts/check-freecad-native-naming-evidence.mjs",
"test:golden:freecad:failures": "node scripts/run-freecad-golden-failures.mjs",
@@ -303,6 +310,9 @@
"test:golden:freecad": "node scripts/run-freecad-golden.mjs",
"test:golden:freecad:families": "node scripts/run-freecad-golden.mjs --manifest=fixtures/freecad-golden/feature-families/manifest.json",
"probe:freecad-reference": "node scripts/run-freecad-reference-probe.mjs",
"probe:freecad-gui-commands": "FREECAD_ORACLE_PROFILE=desktop FREECAD_REFERENCE_SCOPE=gui-commands node scripts/run-freecad-reference-probe.mjs",
"probe:freecad-gui-command-shards": "node scripts/run-freecad-gui-command-shards.mjs",
"merge:freecad-gui-command-shards": "node scripts/run-freecad-gui-command-shards.mjs --merge-only",
"ci:real": "node scripts/run-real-verification.mjs",
"ci:real:chrome": "node scripts/run-real-verification.mjs --lane=chrome --phase=execute",
"ci:check:chrome": "node scripts/run-real-verification.mjs --lane=chrome --phase=check",

View File

@@ -1,11 +1,33 @@
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { promisify } from 'node:util'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/chrome-cam-linuxcnc-machine-verification.json'), 'utf8'))
const manifest = JSON.parse(await readFile(resolve(root, 'config/linuxcnc-wasm-machine-artifact.json'), 'utf8'))
const offlineResources = JSON.parse(await readFile(resolve(root, 'config/offline-resources.json'), 'utf8'))
const lockedWorktree = offlineResources.archives.find((entry) => entry.id === 'linuxcnc-worktree')
const artifactRoles = (manifest.artifacts || []).map((artifact) => artifact.role)
if (manifest.schemaVersion !== 1 || manifest.worktreeRevision !== lockedWorktree?.revision || manifest.chromeVersion !== offlineResources.hostTools?.chrome || JSON.stringify(artifactRoles) !== JSON.stringify(['entry-html', 'case-registry', 'machine-config', 'application', 'controller-worker', 'wasm-loader', 'wasm-core'])) throw new Error('LinuxCNC WASM runtime manifest does not match the offline resource locks.')
if (manifest.artifacts.some(({ runtimeLoaded }) => typeof runtimeLoaded !== 'boolean') || manifest.artifacts.find(({ role }) => role === 'wasm-loader')?.runtimeLoaded !== false || manifest.artifacts.filter(({ role }) => role !== 'wasm-loader').some(({ runtimeLoaded }) => !runtimeLoaded)) throw new Error('LinuxCNC runtime-loaded artifact boundary is invalid.')
if (manifest.machineConfigPath !== manifest.artifacts.find(({ role }) => role === 'machine-config')?.path) throw new Error('LinuxCNC machine configuration is not pinned as a runtime artifact.')
const actualRevision = (await promisify(execFile)('git', ['-C', resolve(root, 'cnc_wams_gpt6/linuxcnc-master'), 'rev-parse', 'HEAD'])).stdout.trim()
if (actualRevision !== manifest.worktreeRevision) throw new Error('LinuxCNC worktree does not match the pinned revision.')
for (const artifact of manifest.artifacts || []) {
const bytes = await readFile(resolve(root, manifest.distRoot, artifact.path))
const sha256 = createHash('sha256').update(bytes).digest('hex')
if (bytes.byteLength !== artifact.bytes || sha256 !== artifact.sha256) throw new Error(`LinuxCNC WASM artifact does not match its pinned identity: ${artifact.path}.`)
}
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome') throw new Error('Chrome CAM LinuxCNC machine verification is not passing.')
if (JSON.stringify(report.stages) !== JSON.stringify(['CAD', 'OCL', 'CAMotics', 'GCODE', 'LinuxCNC WASM'])) throw new Error('CAM machine stage order is invalid.')
if (report.openCamLib?.backend !== 'upstream-opencamlib-wasm' || report.openCamLib.sourceRevision?.length !== 40 || report.openCamLib.artifactSha256?.length !== 64 || report.openCamLib.triangleCount < 2 || report.openCamLib.inputPoints < 2 || report.openCamLib.outputPoints < report.openCamLib.inputPoints || !(report.openCamLib.sampling > 0)) throw new Error('OpenCAMLib WASM stage evidence is incomplete.')
if (!report.gcode?.hasM428 || !report.gcode.hasM429 || !report.gcode.hasG93 || !report.gcode.hasG94 || !report.gcode.hasB || !report.gcode.hasC359 || !report.gcode.hasC361) throw new Error('LinuxCNC XYZBC RTCP G-code evidence is incomplete.')
if (report.machine?.backend !== 'linuxcnc-wasm' || report.machine.status !== 'accepted' || report.machine.dryRunStatus !== 'dry-run' || report.machine.parserAuthority !== 'linuxcnc-wasm' || report.machine.lines < 10 || report.machine.lines >= 1000 || report.machine.blocks < 2 || report.machine.blocks >= 1000 || report.machine.trajectorySegments < 2 || report.machine.trajectorySegments >= 1000) throw new Error('LinuxCNC WASM machine acceptance evidence is incomplete or belongs to a different program.')
if (report.machine?.backend !== 'linuxcnc-wasm' || report.machine.status !== 'accepted' || report.machine.dryRunStatus !== 'dry-run' || report.machine.parserAuthority !== 'linuxcnc-wasm' || report.machine.machineCase !== manifest.machineCase || report.machine.dryRunMachineCase !== manifest.machineCase || report.machine.lines < 10 || report.machine.lines >= 1000 || report.machine.blocks < 2 || report.machine.blocks >= 1000 || report.machine.trajectorySegments < 2 || report.machine.trajectorySegments >= 1000) throw new Error('LinuxCNC WASM machine acceptance evidence is incomplete or belongs to a different program or machine case.')
if (report.runtime?.server !== 'managed-vite-preview' || report.runtime.worktreeRevision !== manifest.worktreeRevision || report.runtime.machineCase !== report.machine.machineCase) throw new Error('LinuxCNC WASM managed runtime identity is incomplete.')
const chromeMajor = manifest.chromeVersion.split('.')[0]
if (report.browser?.product !== manifest.chromeVersion || !String(report.browser?.userAgent || '').includes(`Chrome/${chromeMajor}.`)) throw new Error('Chrome runtime is not the pinned version.')
if (JSON.stringify(report.runtime.artifacts) !== JSON.stringify(manifest.artifacts)) throw new Error('LinuxCNC WASM artifact hashes do not match the pinned manifest.')
if (JSON.stringify(report.runtime.loadedArtifacts) !== JSON.stringify(manifest.artifacts.filter(({ runtimeLoaded }) => runtimeLoaded).map(({ role, path }) => ({ role, path, status: 200 })))) throw new Error('Chrome did not load every pinned LinuxCNC runtime artifact.')
console.log(JSON.stringify({ status: 'chrome-cam-linuxcnc-machine-pass', stages: report.stages, openCamLib: report.openCamLib, gcode: report.gcode, machine: report.machine, browser: report.browser }, null, 2))

View File

@@ -1,16 +1,25 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { elementMap2SemanticDigest, parseElementMap2, validateElementMap2, writeElementMap2 } from '../src/facade/elementMap2.ts'
import { validateNativeNamingEvidence } from '../src/facade/nativeNamingEvidence.ts'
import { migrateStringHasherSchema, parseStringHasherTable, validateStringHasherTable, writeStringHasherTable } from '../src/facade/stringHasher.ts'
import { validateCompositeMutationEvidence } from './freecad-composite-mutation-evidence.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-elementmap-oracle.json'), 'utf8'))
const resaveReport = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-resave-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1-composite-history-elementmap2' || report.freecadVersion !== '1.1.1' || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('FreeCAD composite history ElementMap2 oracle baseline is invalid.')
if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases.')
if (resaveReport.schemaVersion !== 1 || resaveReport.baselineId !== report.baselineId || resaveReport.freecadVersion !== report.freecadVersion || resaveReport.gitCommit !== report.gitCommit || resaveReport.status !== 'pass' || resaveReport.summary?.cases !== 30 || resaveReport.summary?.passed !== 30 || resaveReport.summary?.failed !== 0 || resaveReport.summary?.roundtripNameDrift !== 0 || resaveReport.summary?.resaveNameDrift !== 0 || resaveReport.summary?.nativeDesktopResaveCases !== 30 || !Array.isArray(resaveReport.cases) || resaveReport.cases.length !== 30) throw new Error('FreeCAD composite resave oracle must contain exactly 30 passing save/reopen/resave cases.')
if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || report.summary?.stageCorrelations !== 219 || !(report.summary?.relationRecords > 0) || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases and 219 correlated stages.')
if (resaveReport.schemaVersion !== 1 || resaveReport.baselineId !== report.baselineId || resaveReport.freecadVersion !== report.freecadVersion || resaveReport.gitCommit !== report.gitCommit || resaveReport.status !== 'pass' || resaveReport.summary?.cases !== 30 || resaveReport.summary?.passed !== 30 || resaveReport.summary?.failed !== 0 || resaveReport.summary?.roundtripNameDrift !== 0 || resaveReport.summary?.resaveNameDrift !== 0 || resaveReport.summary?.nativeDesktopResaveCases !== 30 || resaveReport.summary?.stageCorrelations !== 219 || resaveReport.summary?.relationRecords !== report.summary.relationRecords || !Array.isArray(resaveReport.cases) || resaveReport.cases.length !== 30) throw new Error('FreeCAD composite resave oracle must contain exactly 30 passing save/reopen/resave cases and 219 correlated stages.')
const reportIds = report.cases.map(({ id }) => id)
const resaveIds = resaveReport.cases.map(({ id }) => id)
if (new Set(reportIds).size !== 30 || JSON.stringify(resaveIds) !== JSON.stringify(reportIds)) throw new Error('FreeCAD composite and resave case identities are not joined one-to-one.')
const resaveById = new Map(resaveReport.cases.map((fixture) => [fixture.id, fixture]))
for (const fixture of resaveReport.cases) if (fixture.roundtripNameDrift !== 0 || fixture.resaveNameDrift !== 0 || fixture.nativeDesktopResaveCovered !== true) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after native FreeCAD resave.`)
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)])) : value
const digest = (value) => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')
const mutationEvidence = validateCompositeMutationEvidence(report)
let parsedResources = 0
let parsedStringHasherResources = 0
let namingEvidenceStages = 0
@@ -22,12 +31,27 @@ let nativeIndexedNameStages = 0
let privateTokenEvidenceCompleteStages = 0
let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
let stageCorrelations = 0
let relationRecords = 0
let wrongBindings = 0
let unexplainedRelations = 0
for (const fixture of report.cases) {
if (fixture.status !== 'pass' || !fixture.finalObject || !fixture.stages?.length) throw new Error(`FreeCAD composite fixture ${fixture.id} has no valid feature history stages.`)
if (fixture.roundtripNameDrift !== 0) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after FreeCAD reopen.`)
const names = new Set()
const stageNames = new Set(fixture.stages.map(({ name }) => name))
const stageByName = new Map(fixture.stages.map((stage) => [stage.name, stage]))
const dependencyClosure = (stageName, seen = new Set()) => {
if (seen.has(stageName)) return seen
seen.add(stageName)
for (const linked of stageByName.get(stageName)?.linkedObjects ?? []) if (stageNames.has(linked)) dependencyClosure(linked, seen)
return seen
}
const resaveFixture = resaveById.get(fixture.id)
const correlations = resaveFixture?.stageCorrelations
if (!correlations || !['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(correlations[phase]) && correlations[phase].length === fixture.stages.length)) throw new Error(`FreeCAD composite fixture ${fixture.id} lacks complete initial/reopen/resave stage correlations.`)
let historyStages = 0
for (const stage of fixture.stages) {
for (const [ordinal, stage] of fixture.stages.entries()) {
if (!stage.shape?.valid || !stage.names?.length) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} has invalid topology.`)
namingEvidenceStages += 1
if (!['final-shape-only', 'opaque-preserved', 'native-evidence', 'ambiguous', 'missing'].includes(stage.namingEvidenceStatus)) namingEvidenceMissing += 1
@@ -49,6 +73,25 @@ for (const fixture of report.cases) {
else if (stage.nativeEvidence.status === 'native-evidence' && stage.nativeEvidence.internalBuilderEvidence !== true && /^(Part::(Fuse|Cut|Common|Extrusion|Revolution|Loft|Sweep|Fillet|Chamfer)|PartDesign::(Pad|Pocket|Revolution|Groove|AdditiveLoft|SubtractiveLoft|AdditivePipe|SubtractivePipe|Fillet|Chamfer|Draft|Thickness|Mirrored|MultiTransform|LinearPattern|PolarPattern|Hole))$/.test(stage.typeId || '')) internalBuilderEvidenceMissingStages += 1
const stageHasHistory = stage.names.some((entry) => Array.isArray(entry.history) && entry.history.length > 0)
if (stageHasHistory) historyStages += 1
if (!Array.isArray(stage.linkedObjects) || !Array.isArray(stage.relations) || stage.relations.length !== stage.names.length || stage.relationDigest !== digest(stage.relations) || !/^[0-9a-f]{64}$/.test(stage.semanticNameDigest || '')) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} lacks relation-level naming evidence.`)
const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${stage.name}`
const phaseRecords = ['initial', 'reopened', 'resaved'].map((phase) => correlations[phase][ordinal])
for (const record of phaseRecords) if (record.key !== expectedKey || record.ordinal !== ordinal || record.name !== stage.name || record.typeId !== stage.typeId || record.relationCount !== stage.relations.length) throw new Error(`FreeCAD composite fixture ${expectedKey} changed stage identity across native resave.`)
if (phaseRecords.some((record) => record.relationDigest !== stage.relationDigest || record.semanticNameDigest !== stage.semanticNameDigest)) throw new Error(`FreeCAD composite fixture ${expectedKey} changed relation or semantic-name evidence across native resave.`)
stageCorrelations += 1
relationRecords += stage.relations.length
const allowedSources = dependencyClosure(stage.name)
allowedSources.delete(stage.name)
let stageSourceRelations = 0
for (const [relationIndex, relation] of stage.relations.entries()) {
if (relation.resultName !== stage.names[relationIndex]?.name || !['generated', 'modified', 'preserved'].includes(relation.relation) || !Array.isArray(relation.sources)) unexplainedRelations += 1
for (const source of relation.sources) {
stageSourceRelations += 1
if (!stageNames.has(source.sourceObject) || !/^(?:(?:Face|Edge|Vertex)\d+|#[a-zA-Z0-9]+:[0-9a-fA-F]+|#[0-9a-fA-F]+|g\d+(?:v\d+)?;SKT)$/.test(source.sourceElement || '')) unexplainedRelations += 1
else if (!allowedSources.has(source.sourceObject)) wrongBindings += 1
}
}
if (stage.nativeEvidence.internalBuilderEvidence === true && stageSourceRelations === 0) unexplainedRelations += 1
for (const entry of stage.names) {
if (names.has(`${stage.name}:${entry.name}`)) throw new Error(`Duplicate ElementMap name ${fixture.id}/${stage.name}/${entry.name}.`)
names.add(`${stage.name}:${entry.name}`)
@@ -74,7 +117,9 @@ for (const fixture of report.cases) {
parsedResources += 1
}
}
if (stageCorrelations !== 219 || stageCorrelations !== report.summary.stageCorrelations || relationRecords !== report.summary.relationRecords) throw new Error(`FreeCAD stage-correlation totals drifted: stages=${stageCorrelations}, relations=${relationRecords}.`)
if (wrongBindings !== 0 || unexplainedRelations !== 0) throw new Error(`FreeCAD relation-level topology correlation failed: wrongBindings=${wrongBindings}, unexplainedRelations=${unexplainedRelations}.`)
if (parsedResources === 0) throw new Error('FreeCAD composite oracle did not capture any ElementMap2 resources.')
if (nativeIndexedNameStages !== namingEvidenceStages) throw new Error(`FreeCAD composite oracle must retain direct indexed-name evidence for every stage, found ${nativeIndexedNameStages}/${namingEvidenceStages}.`)
if (namingEvidenceMissing !== 0 || namingEvidenceBoundaryViolations !== 0) throw new Error(`FreeCAD naming evidence boundary is incomplete: missing=${namingEvidenceMissing}, violations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, nativeDesktopResaveCases: resaveReport.summary.nativeDesktopResaveCases, resaveNameDrift: resaveReport.summary.resaveNameDrift }, null, 2))
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, categoryCases: mutationEvidence.categoryCases, mutationCases: mutationEvidence.mutationCases, mutationPassed: mutationEvidence.mutationPassed, mutationStageRecords: mutationEvidence.mutationStageRecords, mutationPhaseStageRecords: mutationEvidence.mutationPhaseStageRecords, mutationFinalPropagationFailures: mutationEvidence.mutationFinalPropagationFailures, mutationStageRestoreFailures: mutationEvidence.mutationStageRestoreFailures, mutationNamingRestoreDriftCases: mutationEvidence.mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages: mutationEvidence.mutationNamingRestoreDriftStages, stageCorrelations, relationRecords, wrongBindings, unexplainedRelations, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, nativeDesktopResaveCases: resaveReport.summary.nativeDesktopResaveCases, resaveNameDrift: resaveReport.summary.resaveNameDrift }, null, 2))

View File

@@ -62,6 +62,8 @@ if (!report) fail('34-module desktop probe report is missing; run probe:freecad-
if (report.baselineId !== config.baselineId || report.freecadVersion !== '1.1.1' || report.guiUp !== true || report.moduleCount !== 34 || report.modules?.length !== 34) {
fail('desktop probe report is not a GUI-up 34-module FreeCAD 1.1.1 report.')
}
if (report.determinism?.schemaVersion !== 1 || report.determinism.normalizedProcessFields?.join(',') !== 'pointer-address,uuid,freecad-document-cache-run') fail('desktop probe does not declare deterministic process-field normalization.')
if (report.guiCommands?.stateProbe !== 'native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard' || report.guiCommands.stateCases?.join(',') !== 'noDocument,documentNoSelection,selectedPartBox' || report.guiCommands.directIsActiveBoundary !== 'Std_Expressions-only-qaction-guard-for-locked-1.1.1-null-qaction-crash' || report.guiCommands.stateCommandCount !== report.guiCommands.commands?.length) fail('desktop probe GUI state boundary is incomplete.')
const invalid = report.modules.filter((module) => !['compiled-importable', 'compiled-import-failure', 'gui-only-unprobeable', 'not-built'].includes(module.runtimeStatus))
if (invalid.length) fail(`probe contains invalid module statuses: ${invalid.map((module) => module.name).join(', ')}`)
const expectedObjects = ['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Feature', 'PartDesign::Feature', 'Sketcher::SketchObject']
@@ -74,6 +76,11 @@ const propertyFlags = report.objects.flatMap((object) => object.properties || []
if (!propertyFlags.some((status) => status.includes('Hidden')) || !propertyFlags.some((status) => status.includes('Output'))) {
fail('runtime property fixtures must include Hidden and Output status flags.')
}
for (const command of report.guiCommands?.commands ?? []) {
if (!Array.isArray(command.registeredWorkbenches) || command.registeredWorkbenches.length === 0 || !Array.isArray(command.observations) || command.observations.length !== 1 || command.observations.some((observation) => !Object.hasOwn(observation, 'noDocument') || !Object.hasOwn(observation, 'documentNoSelection') || !Object.hasOwn(observation, 'selectedPartBox'))) {
fail(`GUI command ${command.id || '<unknown>'} lacks no-document/document/selection observations.`)
}
}
const runtimeObjects = report.runtimeObjects
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types) || runtimeObjects.candidateCount !== runtimeObjects.types.length) {
fail('complete Document.supportedTypes runtime object evidence is missing.')

View File

@@ -2,16 +2,22 @@ import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { createElementMap2NameToken, elementMap2NameTokenToReference, elementMap2SemanticDigest, parseElementMap2, validateElementMap2, writeElementMap2 } from '../src/facade/elementMap2.ts'
import { migrateStringHasherSchema, parseStringHasherTable, validateElementMap2StringHasherEvidence, validateStringHasherTable, writeStringHasherTable } from '../src/facade/stringHasher.ts'
import { validateCompositeMutationEvidence } from './freecad-composite-mutation-evidence.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
const gate = await load('config/freecad-exact-history-elementmap-gate.json')
const oracle = await load('config/freecad-composite-history-elementmap-oracle.json')
const roundtrip = await load('config/freecad-fcstd-roundtrip-verification.json')
const resave = await load('config/freecad-composite-history-resave-verification.json')
const stageCorrelation = await load('config/freecad-tsn-stage-correlation-oracle.json')
if (oracle.baselineId !== gate.baselineId || oracle.freecadVersion !== gate.requiredFreecadVersion || oracle.gitCommit !== gate.requiredFreecadCommit || oracle.status !== 'pass' || oracle.summary?.cases !== gate.requiredCases || oracle.summary?.passed !== gate.requiredCases) throw new Error('Exact history/ElementMap2 gate baseline is not the locked FreeCAD 1.1.1 30-case report.')
const mutationEvidence = validateCompositeMutationEvidence(oracle)
if (mutationEvidence.mutationCases !== gate.requirements.mutationCases || mutationEvidence.mutationPassed !== gate.requirements.mutationCases - gate.requirements.mutationFailures || mutationEvidence.mutationStageRecords !== gate.requirements.mutationStageRecords || mutationEvidence.mutationStageRestoreFailures !== gate.requirements.mutationStageRestoreFailures || mutationEvidence.mutationFinalPropagationFailures !== gate.requirements.mutationFinalPropagationFailures || mutationEvidence.mutationNamingRestoreDriftCases !== gate.requirements.mutationNamingRestoreDriftCases || mutationEvidence.mutationNamingRestoreDriftStages !== gate.requirements.mutationNamingRestoreDriftStages) throw new Error('Exact history/ElementMap2 mutation requirements do not match per-case evidence.')
let wrongBindings = 0
let unexplainedRelations = 0
let roundtripNameDrift = 0
let resaveNameDrift = 0
let elementMap2ParseFailures = 0
let elementMap2SemanticFailures = 0
let elementMap2TokenWriterFailures = 0
@@ -31,7 +37,22 @@ let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
const builderStageType = /^(Part::(Fuse|Cut|Common|Extrusion|Revolution|Loft|Sweep|Fillet|Chamfer)|PartDesign::(Pad|Pocket|Revolution|Groove|AdditiveLoft|SubtractiveLoft|AdditivePipe|SubtractivePipe|Fillet|Chamfer|Draft|Thickness|Mirrored|MultiTransform|LinearPattern|PolarPattern|Hole))$/
let resources = 0
let caseReconciliations = 0
const resaveById = new Map((resave.cases || []).map((fixture) => [fixture.id, fixture]))
if (resaveById.size !== gate.requiredCases || resave.cases?.length !== gate.requiredCases) throw new Error('Exact gate resave evidence is not joined one-to-one with the locked corpus.')
const zeroMetrics = () => ({ wrongBindings, unexplainedRelations, roundtripNameDrift, resaveNameDrift, elementMap2ParseFailures, elementMap2SemanticFailures, elementMap2TokenWriterFailures, stringHasherParseFailures, stringHasherSemanticFailures, stringHasherEvidenceFailures, namingEvidenceMissing, namingEvidenceBoundaryViolations })
for (const fixture of oracle.cases) {
const metricsBefore = zeroMetrics()
const resaveFixture = resaveById.get(fixture.id)
if (!resaveFixture || resaveFixture.roundtripNameDrift !== 0 || resaveFixture.resaveNameDrift !== 0 || resaveFixture.nativeDesktopResaveCovered !== true) throw new Error(`Exact gate case ${fixture.id} lacks passing native save/reopen/resave evidence.`)
if (!['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(resaveFixture.stageCorrelations?.[phase]) && resaveFixture.stageCorrelations[phase].length === fixture.stages.length)) throw new Error(`Exact gate case ${fixture.id} lacks complete stage correlations.`)
for (const [ordinal, stage] of fixture.stages.entries()) {
const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${stage.name}`
for (const phase of ['initial', 'reopened', 'resaved']) {
const record = resaveFixture.stageCorrelations[phase][ordinal]
if (record.key !== expectedKey || record.ordinal !== ordinal || record.name !== stage.name || record.typeId !== stage.typeId || record.relationDigest !== stage.relationDigest || record.semanticNameDigest !== stage.semanticNameDigest) throw new Error(`Exact gate case ${expectedKey} changed identity or naming evidence during ${phase}.`)
}
}
if (fixture.roundtripNameDrift !== 0) roundtripNameDrift += 1
const knownObjects = new Set(fixture.stages.map((stage) => stage.name))
for (const stage of fixture.stages) {
@@ -96,10 +117,20 @@ for (const fixture of oracle.cases) {
elementMap2ParseFailures += 1
}
}
const metricsAfter = zeroMetrics()
const caseFailures = Object.fromEntries(Object.keys(metricsAfter).map((key) => [key, metricsAfter[key] - metricsBefore[key]]).filter(([, value]) => value !== 0))
if (Object.keys(caseFailures).length > 0) throw new Error(`Exact gate case ${fixture.id} failed closed: ${JSON.stringify(caseFailures)}.`)
caseReconciliations += 1
}
for (const scenario of roundtrip.scenarios || []) {
if (scenario.status !== 'pass' || (scenario.differences || []).length !== 0) roundtripNameDrift += 1
}
if (resave.status !== 'pass' || resave.summary?.nativeDesktopResaveCases !== gate.requiredCases || resave.summary?.roundtripNameDrift !== 0 || resave.summary?.resaveNameDrift !== 0 || caseReconciliations !== gate.requiredCases) resaveNameDrift += 1
if (stageCorrelation.status !== 'pass' || stageCorrelation.nativeDesktopResaveCovered !== true || stageCorrelation.summary?.mutationCases !== 5 || stageCorrelation.summary?.mutationPassed !== 5) throw new Error('Five-stage desktop correlation/mutation evidence is incomplete.')
wrongBindings += stageCorrelation.summary?.wrongBindings ?? 1
unexplainedRelations += stageCorrelation.summary?.unexplainedRelations ?? 1
roundtripNameDrift += stageCorrelation.summary?.roundtripNameDrift ?? 1
resaveNameDrift += stageCorrelation.summary?.resaveNameDrift ?? 1
if (indexedNameOnlyStages + privateTokenEvidenceRequiredStages !== nativeEvidenceStages || privateTokenEvidenceCompleteStages !== privateTokenEvidenceRequiredStages || internalBuilderEvidenceStages !== 42 || internalBuilderEvidenceMissingStages !== 0) throw new Error(`Exact native naming evidence is incomplete: indexedOnly=${indexedNameOnlyStages}, tokenRequired=${privateTokenEvidenceRequiredStages}, tokenComplete=${privateTokenEvidenceCompleteStages}, builder=${internalBuilderEvidenceStages}, builderMissing=${internalBuilderEvidenceMissingStages}.`)
if (roundtrip.status !== 'verified' || wrongBindings !== gate.requirements.wrongBindings || unexplainedRelations !== gate.requirements.unexplainedRelations || roundtripNameDrift !== gate.requirements.roundtripNameDrift || elementMap2ParseFailures !== gate.requirements.elementMap2ParseFailures || elementMap2SemanticFailures !== gate.requirements.elementMap2SemanticFailures || elementMap2TokenWriterFailures !== gate.requirements.elementMap2TokenWriterFailures || stringHasherParseFailures !== gate.requirements.stringHasherParseFailures || stringHasherSemanticFailures !== gate.requirements.stringHasherSemanticFailures || stringHasherEvidenceFailures !== gate.requirements.stringHasherEvidenceFailures || namingEvidenceMissing !== gate.requirements.namingEvidenceMissing || namingEvidenceBoundaryViolations !== gate.requirements.namingEvidenceBoundaryViolations) throw new Error(`Exact gate failed: wrongBindings=${wrongBindings}, unexplainedRelations=${unexplainedRelations}, roundtripNameDrift=${roundtripNameDrift}, elementMap2ParseFailures=${elementMap2ParseFailures}, elementMap2SemanticFailures=${elementMap2SemanticFailures}, elementMap2TokenWriterFailures=${elementMap2TokenWriterFailures}, stringHasherParseFailures=${stringHasherParseFailures}, stringHasherSemanticFailures=${stringHasherSemanticFailures}, stringHasherEvidenceFailures=${stringHasherEvidenceFailures}, namingEvidenceMissing=${namingEvidenceMissing}, namingEvidenceBoundaryViolations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-exact-history-elementmap-gate-pass', exactPromotionReady: false, cases: oracle.summary.cases, resources, stringHasherResources, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: indexedNameOnlyStages, privateTokenEvidenceRequiredStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, wrongBindings, unexplainedRelations, roundtripNameDrift, elementMap2ParseFailures, elementMap2SemanticFailures, elementMap2TokenWriterFailures, stringHasherParseFailures, stringHasherSemanticFailures, stringHasherEvidenceFailures, namingEvidenceMissing, namingEvidenceBoundaryViolations }, null, 2))
if (roundtrip.status !== 'verified' || wrongBindings !== gate.requirements.wrongBindings || unexplainedRelations !== gate.requirements.unexplainedRelations || roundtripNameDrift !== gate.requirements.roundtripNameDrift || resaveNameDrift !== gate.requirements.resaveNameDrift || elementMap2ParseFailures !== gate.requirements.elementMap2ParseFailures || elementMap2SemanticFailures !== gate.requirements.elementMap2SemanticFailures || elementMap2TokenWriterFailures !== gate.requirements.elementMap2TokenWriterFailures || stringHasherParseFailures !== gate.requirements.stringHasherParseFailures || stringHasherSemanticFailures !== gate.requirements.stringHasherSemanticFailures || stringHasherEvidenceFailures !== gate.requirements.stringHasherEvidenceFailures || namingEvidenceMissing !== gate.requirements.namingEvidenceMissing || namingEvidenceBoundaryViolations !== gate.requirements.namingEvidenceBoundaryViolations) throw new Error(`Exact gate failed: wrongBindings=${wrongBindings}, unexplainedRelations=${unexplainedRelations}, roundtripNameDrift=${roundtripNameDrift}, resaveNameDrift=${resaveNameDrift}, elementMap2ParseFailures=${elementMap2ParseFailures}, elementMap2SemanticFailures=${elementMap2SemanticFailures}, elementMap2TokenWriterFailures=${elementMap2TokenWriterFailures}, stringHasherParseFailures=${stringHasherParseFailures}, stringHasherSemanticFailures=${stringHasherSemanticFailures}, stringHasherEvidenceFailures=${stringHasherEvidenceFailures}, namingEvidenceMissing=${namingEvidenceMissing}, namingEvidenceBoundaryViolations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-exact-history-elementmap-gate-pass', exactPromotionReady: false, cases: oracle.summary.cases, caseReconciliations, categoryCases: mutationEvidence.categoryCases, mutationCases: mutationEvidence.mutationCases, mutationPassed: mutationEvidence.mutationPassed, mutationStageRecords: mutationEvidence.mutationStageRecords, mutationPhaseStageRecords: mutationEvidence.mutationPhaseStageRecords, mutationStageRestoreFailures: mutationEvidence.mutationStageRestoreFailures, mutationFinalPropagationFailures: mutationEvidence.mutationFinalPropagationFailures, mutationNamingRestoreDriftCases: mutationEvidence.mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages: mutationEvidence.mutationNamingRestoreDriftStages, nativeDesktopResaveCases: resave.summary.nativeDesktopResaveCases + 1, crossFeatureMutationCases: stageCorrelation.summary.mutationCases, crossFeatureMutationPassed: stageCorrelation.summary.mutationPassed, resources, stringHasherResources, nativeEvidenceStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: indexedNameOnlyStages, privateTokenEvidenceRequiredStages, privateTokenEvidenceCompleteStages, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, wrongBindings, unexplainedRelations, roundtripNameDrift, resaveNameDrift, elementMap2ParseFailures, elementMap2SemanticFailures, elementMap2TokenWriterFailures, stringHasherParseFailures, stringHasherSemanticFailures, stringHasherEvidenceFailures, namingEvidenceMissing, namingEvidenceBoundaryViolations }, null, 2))

View File

@@ -2,23 +2,42 @@ import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const inventory = JSON.parse(await readFile(resolve(root, 'config/freecad-gui-command-inventory.json'), 'utf8'))
const [inventory, contextBoundaries] = await Promise.all([
readFile(resolve(root, 'config/freecad-gui-command-inventory.json'), 'utf8').then(JSON.parse),
readFile(resolve(root, 'config/freecad-gui-command-context-boundaries.json'), 'utf8').then(JSON.parse),
])
const fail = (message) => { throw new Error(`FreeCAD GUI command inventory: ${message}`) }
if (inventory.schemaVersion !== 1 || inventory.baseline?.freecadVersion !== '1.1.1' || inventory.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline mismatch.')
if (!Number.isInteger(inventory.commandCount) || inventory.commandCount < 900 || !Array.isArray(inventory.commands) || inventory.commands.length !== inventory.commandCount) fail('runtime command inventory is incomplete.')
if (inventory.contextBoundaries !== 'config/freecad-gui-command-context-boundaries.json') fail('context-only command boundary ledger is not linked.')
if (inventory.runtimeEvidence?.report !== '.cache/freecad/reference-desktop-gui-commands.json' || !/^[0-9a-f]{64}$/.test(inventory.runtimeEvidence.reportSha256 || '')) fail('dedicated runtime GUI oracle provenance is incomplete.')
if (inventory.runtimeEvidence?.stateProbe !== 'native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard' || inventory.runtimeEvidence.stateCases?.join(',') !== 'noDocument,documentNoSelection,selectedPartBox' || inventory.runtimeEvidence.directIsActiveBoundary !== 'Std_Expressions-only-qaction-guard-for-locked-1.1.1-null-qaction-crash') fail('runtime command state boundary is incomplete.')
const ids = new Set()
for (const command of inventory.commands) {
if (!command.id || ids.has(command.id)) fail(`duplicate or empty command '${command.id}'.`)
ids.add(command.id)
if (!Array.isArray(command.sourceModules) || command.sourceModules.length === 0 || !Array.isArray(command.workbenches) || command.workbenches.length === 0) fail(`${command.id} is missing source/workbench ownership.`)
for (const caseName of ['emptySelection', 'selectedPartBox']) {
for (const caseName of ['noDocument', 'documentNoSelection', 'selectedPartBox']) {
const probeCase = command.cases?.[caseName]
if (!probeCase?.observed || !Array.isArray(probeCase.states) || probeCase.states.length === 0) fail(`${command.id} is missing ${caseName} evidence.`)
if (probeCase.states.some((state) => !['true', 'false', 'error', 'no-action'].includes(String(state)))) fail(`${command.id} has an invalid ${caseName} state.`)
}
}
if (!Array.isArray(inventory.sourceOnlyCommands)) fail('sourceOnlyCommands must be an array.')
const sourceOnlyIds = new Set()
for (const command of inventory.sourceOnlyCommands) {
if (!command.id || !Array.isArray(command.sourceModules) || !command.reason) fail('source-only command entry is incomplete.')
if (ids.has(command.id) || sourceOnlyIds.has(command.id)) fail(`source-only command '${command.id}' is duplicated or also runtime-probed.`)
if (!['module-not-built', 'source-registration-not-loaded'].includes(command.reason)) fail(`source-only command '${command.id}' has an unknown generator reason.`)
sourceOnlyIds.add(command.id)
}
console.log(JSON.stringify({ status: 'gui-command-inventory-pass', commandCount: inventory.commandCount, sourceOnlyCount: inventory.sourceOnlyCommands.length, workbenchCount: inventory.runtimeEvidence?.workbenchCount }, null, 2))
if (contextBoundaries.schemaVersion !== 1 || contextBoundaries.baseline?.freecadVersion !== '1.1.1' || contextBoundaries.baseline.commit !== inventory.baseline.commit) fail('context-only command boundary baseline mismatch.')
const isolated = contextBoundaries.isolatedWorkbenchOracle
if (isolated?.report !== inventory.runtimeEvidence.report || isolated.reportSha256 !== inventory.runtimeEvidence.reportSha256 || isolated.commandCount !== inventory.commandCount || isolated.commandUniverseSha256 !== '82901c576ef4b4cea1182f6505f5282a70b84ee8420105e83e58f65c0fb9685f' || isolated.shardCount !== 100 || isolated.bimFirstTimeWelcome !== 'suppressed-before-workbench-activation') fail('isolated workbench oracle boundary is incomplete.')
const contextCommands = contextBoundaries.contextOnlyCommands
if (!Array.isArray(contextCommands) || contextCommands.map((command) => command.id).join(',') !== 'Import_ReadBREP,NaviCubeDraggableCmd') fail('context-only command set changed without review.')
for (const command of contextCommands) {
if (ids.has(command.id) || sourceOnlyIds.has(command.id)) fail(`context-only command '${command.id}' leaked into a startup inventory.`)
if (!['explicit-module-import-command', 'context-menu-lazy-command'].includes(command.classification) || !command.runtimeExpectation || !command.exactTask || !Array.isArray(command.sourceEvidence) || command.sourceEvidence.length === 0) fail(`context-only command '${command.id}' lacks a complete boundary.`)
}
console.log(JSON.stringify({ status: 'gui-command-inventory-pass', commandCount: inventory.commandCount, sourceOnlyCount: inventory.sourceOnlyCommands.length, contextOnlyCount: contextCommands.length, workbenchCount: inventory.runtimeEvidence?.workbenchCount, documentSensitiveCount: inventory.commands.filter((command) => command.documentSensitive).length, selectionSensitiveCount: inventory.commands.filter((command) => command.selectionSensitive).length }, null, 2))

View File

@@ -0,0 +1,32 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const [report, plan] = await Promise.all([
readFile(resolve(root, 'config/freecad-gui-workflow-oracle.json'), 'utf8').then(JSON.parse),
readFile(resolve(root, 'config/freecad-gui-workflow-plan.json'), 'utf8').then(JSON.parse),
])
const fail = (message) => { throw new Error(`FreeCAD GUI workflow oracle: ${message}`) }
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (report.schemaVersion !== 1 || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== lockedCommit || report.baseline.profile !== 'desktop-xvfb-isolated-config') fail('baseline mismatch.')
if (report.family?.id !== plan.family?.id || report.family.primaryCommand !== plan.family.primaryCommand || report.family.exactTask !== plan.family.primaryExactTask) fail('selected family mapping is stale.')
if (report.generatedBy !== './npmw run probe:freecad-gui-workflow' || report.checkedBy !== './npmw run check:freecad-gui-workflow') fail('probe/check provenance is incomplete.')
const success = report.workflows?.success
if (success?.workflowId !== report.family.id || success.commandId !== report.family.primaryCommand || success.state !== 'success' || success.success !== true) fail('success workflow identity is invalid.')
if (success.before?.activeWorkbench !== 'PartDesignWorkbench' || success.before.commandRegistered !== true || success.before.commandActive !== true || success.before.actionCount < 1 || success.before.selection?.join(',') !== 'Sketch') fail('native command preconditions are incomplete.')
if (success.taskPanel?.opened?.activeDialog !== true || success.taskPanel.opened.inEdit !== 'Pad' || success.taskPanel.opened.fieldCount < 1 || success.taskPanel.preview?.name !== 'Pad' || success.taskPanel.preview.length !== 10 || success.taskPanel.preview.volume !== 120) fail('native Pad Task preview evidence is incomplete.')
if (success.after?.acceptClicked !== true || success.taskPanel?.closed?.activeDialog !== false || success.taskPanel.closed.inEdit !== '') fail('native Pad Task accept/close evidence is incomplete.')
const pad = success.after?.pad
if (success.after?.bodyTip !== 'Pad' || success.after.bodyGroup?.join(',') !== 'Sketch,Pad' || pad?.typeId !== 'PartDesign::Pad' || pad.length !== 10 || pad.shapeValid !== true || pad.solidCount !== 1 || pad.faceCount !== 6 || pad.edgeCount !== 12 || pad.vertexCount !== 8 || pad.volume !== 120 || pad.profile !== 'Sketch' || pad.state?.join(',') !== 'Up-to-date') fail('native Pad result semantics are incomplete.')
if ((!Number.isInteger(success.after.undoCount) || success.after.undoCount < 1) && (!Array.isArray(success.after.undoNames) || success.after.undoNames.length < 1)) fail('native Pad transaction evidence is missing.')
if (JSON.stringify(report.remainingStates) !== JSON.stringify(['disabled', 'failure', 'cancel', 'recovery']) || report.exactPromotionReady !== false) fail('remaining workflow boundary is incorrect.')
console.log(JSON.stringify({
status: 'freecad-gui-workflow-pass',
family: report.family.id,
command: report.family.primaryCommand,
capturedStates: Object.keys(report.workflows),
remainingStates: report.remainingStates,
bodyTip: success.after.bodyTip,
volume: pad.volume,
undoNames: success.after.undoNames,
}, null, 2))

View File

@@ -0,0 +1,60 @@
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 [workflow, inventory, exactPlan] = await Promise.all([
load('config/freecad-gui-workflow-plan.json'),
load('config/freecad-gui-command-inventory.json'),
load('config/freecad-web-exact-parity-plan.json'),
])
const fail = (message) => { throw new Error(`FreeCAD GUI workflow plan: ${message}`) }
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (workflow.schemaVersion !== 1 || workflow.baseline?.freecadVersion !== '1.1.1' || workflow.baseline?.commit !== lockedCommit) fail('baseline mismatch.')
if (inventory.baseline?.commit !== lockedCommit || exactPlan.baseline?.freecadCommit !== lockedCommit) fail('input baseline mismatch.')
const family = workflow.family
if (!family?.id || !family.title || !family.workbench || !family.primaryCommand || !family.primaryExactTask || !family.selectionReason) fail('selected family metadata is incomplete.')
if (!Array.isArray(family.commandIds) || family.commandIds.length === 0 || !family.commandIds.includes(family.primaryCommand)) fail('selected command family is incomplete.')
if (!Array.isArray(family.relatedExactTasks) || family.relatedExactTasks.length === 0) fail('related exact tasks are missing.')
const exactTaskIds = new Set(exactPlan.programs.flatMap((program) => program.tasks.map((task) => task.id)))
for (const taskId of [family.primaryExactTask, ...family.relatedExactTasks]) if (!exactTaskIds.has(taskId)) fail(`selected family references unknown exact task ${taskId}.`)
const commandById = new Map(inventory.commands.map((command) => [command.id, command]))
for (const commandId of family.commandIds) if (!commandById.has(commandId)) fail(`selected command ${commandId} is absent from the runtime inventory.`)
const primary = commandById.get(family.primaryCommand)
const sensitivity = workflow.sensitivityEvidence
if (sensitivity?.source !== 'config/freecad-gui-command-inventory.json' || sensitivity.commandId !== family.primaryCommand) fail('sensitivity provenance is incomplete.')
if (sensitivity.documentSensitive !== primary.documentSensitive || sensitivity.selectionSensitive !== primary.selectionSensitive) fail('selected sensitivity flags do not match the runtime inventory.')
for (const caseName of ['noDocument', 'documentNoSelection', 'selectedPartBox']) {
if (JSON.stringify(sensitivity.cases?.[caseName]) !== JSON.stringify(primary.cases?.[caseName]?.states)) fail(`${caseName} sensitivity evidence is stale.`)
}
const stateNames = ['success', 'disabled', 'failure', 'cancel', 'recovery']
for (const stateName of stateNames) {
const recipe = workflow.stateRecipes?.[stateName]
if (!recipe?.setup || !recipe.action || !recipe.expected) fail(`${stateName} workflow recipe is incomplete.`)
}
const expectedTaskIds = Array.from({ length: 7 }, (_, index) => `ORA-GUI-WF-${String(index).padStart(3, '0')}`)
if (!Array.isArray(workflow.progress) || workflow.progress.map((entry) => entry.taskId).join(',') !== expectedTaskIds.join(',')) fail('workflow progress must contain ordered ORA-GUI-WF-000 through 006 tasks.')
const statuses = workflow.progress.map((entry) => entry.status)
if (statuses.filter((status) => status === 'in_progress').length !== 1 || statuses.some((status) => !['completed', 'in_progress', 'pending'].includes(status))) fail('workflow progress must have exactly one in-progress task.')
const inProgressIndex = statuses.indexOf('in_progress')
if (statuses.slice(0, inProgressIndex).some((status) => status !== 'completed') || statuses.slice(inProgressIndex + 1).some((status) => status !== 'pending')) fail('workflow progress is not a contiguous serial queue.')
for (const [index, entry] of workflow.progress.entries()) {
if (!Array.isArray(entry.evidence)) fail(`${entry.taskId} evidence must be an array.`)
if (index < inProgressIndex && entry.evidence.length === 0) fail(`${entry.taskId} is completed without evidence.`)
}
console.log(JSON.stringify({
status: 'freecad-gui-workflow-plan-pass',
family: family.id,
primaryCommand: family.primaryCommand,
primaryExactTask: family.primaryExactTask,
completed: statuses.filter((status) => status === 'completed').length,
inProgress: workflow.progress[inProgressIndex].taskId,
pending: statuses.filter((status) => status === 'pending').length,
}, null, 2))

View File

@@ -5,16 +5,16 @@ 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 !== '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 !== '58 runtime property types and 523 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 !== 18 || support['native-editable-codec'].recordCount !== 4135 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 62 || support['opaque-fcstd-proxy'].recordCount !== 692) fail('property support partition is stale')
if (support?.['native-editable-codec']?.typeCount !== 22 || support['native-editable-codec'].recordCount !== 4304 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 58 || support['opaque-fcstd-proxy'].recordCount !== 523) 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')
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,NoModify,Ordered,Output,PropHidden,PropNoPersist,PropNoRecompute,PropOutput,PropReadOnly,PropTransient,ReadOnly,Transient' || report.propertyStatus.preservedOnly?.join(',') !== 'LockDynamic,PartialTrigger' || report.propertyStatus.preservedOnlyRecordCount !== 70) fail('property status behavior boundary 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]) {
const path = resolve(root, locked?.path ?? '')
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: 4818, opaqueProxyRecords: 692, representedStatusRecords: 5510, preservedOnlyStatusRecords: 70, exactPromotionReady: false }, null, 2))
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 4987, opaqueProxyRecords: 523, representedStatusRecords: 5510, preservedOnlyStatusRecords: 0, exactPromotionReady: false }, null, 2))

View File

@@ -3,15 +3,21 @@ 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 [sourceInventory, typeInventory, guiInventory, reference] = await Promise.all([
const [sourceInventory, typeInventory, guiInventory, reference, guiReference, boundaries, exactPlan] = await Promise.all([
load('config/freecad-source-inventory.json'),
load('config/freecad-type-property-inventory.json'),
load('config/freecad-gui-command-inventory.json'),
load('.cache/freecad/reference-desktop.json'),
load('.cache/freecad/reference-desktop-gui-commands.json'),
load('config/freecad-oracle-boundaries.json'),
load('config/freecad-web-exact-parity-plan.json'),
])
const fail = (message) => { throw new Error(`FreeCAD oracle coverage: ${message}`) }
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (reference.schemaVersion !== 1 || reference.freecadVersion !== '1.1.1' || reference.gitCommit !== lockedCommit) fail('reference desktop report is not the locked FreeCAD 1.1.1 baseline.')
if (guiReference.schemaVersion !== 1 || guiReference.freecadVersion !== '1.1.1' || guiReference.gitCommit !== lockedCommit || guiReference.guiUp !== true || guiReference.probeScope !== 'gui-commands') fail('GUI command report is not the dedicated locked FreeCAD 1.1.1 oracle.')
if (guiReference.determinism?.schemaVersion !== 1 || guiReference.determinism.normalizedProcessFields?.join(',') !== 'pointer-address,uuid,freecad-document-cache-run') fail('GUI command report lacks deterministic process-field normalization.')
if (boundaries.schemaVersion !== 1 || boundaries.baseline?.freecadVersion !== '1.1.1' || boundaries.baseline?.commit !== lockedCommit) fail('oracle boundary decisions do not match the locked baseline.')
if (sourceInventory.moduleCount !== 34 || reference.moduleCount !== 34 || reference.modules?.length !== 34) fail('source and desktop oracle must both enumerate 34 modules.')
if (typeInventory.moduleCount !== 34 || typeInventory.modules?.length !== 34) fail('type/property inventory must enumerate all 34 modules.')
@@ -26,17 +32,32 @@ for (const name of sourceModules.keys()) {
const missingReferenceModules = [...referenceModules.keys()].filter((name) => !sourceModules.has(name))
if (missingReferenceModules.length > 0) fail(`desktop oracle has unregistered modules: ${missingReferenceModules.join(', ')}.`)
const referenceCommands = new Map((reference.guiCommands?.commands ?? []).map((command) => [command.id, command]))
const referenceCommands = new Map((guiReference.guiCommands?.commands ?? []).map((command) => [command.id, command]))
const inventoryCommands = new Map(guiInventory.commands.map((command) => [command.id, command]))
if (referenceCommands.size !== guiInventory.commandCount || inventoryCommands.size !== guiInventory.commandCount) fail('GUI command counts or IDs are not unique and aligned.')
for (const [id, command] of inventoryCommands) {
const referenceCommand = referenceCommands.get(id)
if (!referenceCommand) fail(`GUI command ${id} is missing from the desktop oracle.`)
if (!Array.isArray(referenceCommand.observations) || referenceCommand.observations.length === 0) fail(`GUI command ${id} has no desktop observations.`)
if (referenceCommand.observations.some((observation) => !Object.hasOwn(observation, 'noDocument') || !Object.hasOwn(observation, 'documentNoSelection') || !Object.hasOwn(observation, 'selectedPartBox'))) fail(`GUI command ${id} lacks the complete document/selection state matrix.`)
if (command.status !== 'runtime-probed') fail(`GUI command ${id} is not marked runtime-probed.`)
for (const state of ['noDocument', 'documentNoSelection', 'selectedPartBox']) if (command.cases?.[state]?.observed !== true || !Array.isArray(command.cases[state].states) || command.cases[state].states.length === 0) fail(`GUI command ${id} inventory lacks ${state} evidence.`)
}
for (const id of referenceCommands.keys()) if (!inventoryCommands.has(id)) fail(`desktop GUI command ${id} is missing from the generated inventory.`)
for (const command of guiInventory.sourceOnlyCommands) if (!command.reason || !Array.isArray(command.sourceModules)) fail(`source-only command ${command.id} lacks an explicit reason.`)
const sourceOnlyCommands = new Map(guiInventory.sourceOnlyCommands.map((command) => [command.id, command]))
const sourceOnlyCommandDecisions = new Map((boundaries.sourceOnlyCommands ?? []).map((command) => [command.id, command]))
if (sourceOnlyCommandDecisions.size !== (boundaries.sourceOnlyCommands ?? []).length) fail('source-only command decisions contain duplicate IDs.')
const expectedSourceOnlyCommandIds = [...sourceOnlyCommands.keys()].sort()
const decidedSourceOnlyCommandIds = [...sourceOnlyCommandDecisions.keys()].sort()
if (JSON.stringify(expectedSourceOnlyCommandIds) !== JSON.stringify(decidedSourceOnlyCommandIds)) fail(`source-only command decisions drifted: expected ${expectedSourceOnlyCommandIds.join(', ')}, received ${decidedSourceOnlyCommandIds.join(', ')}.`)
const allowedCommandClassifications = new Set(['legacy-lazy-command', 'optional-addon-command-group', 'dynamic-command-template', 'optional-preference-command-group', 'optional-executable-command', 'context-lazy-command', 'source-template-command'])
const exactTaskIds = new Set(exactPlan.programs.flatMap((program) => program.tasks).map((task) => task.id))
for (const [id, decision] of sourceOnlyCommandDecisions) {
const inventoryCommand = sourceOnlyCommands.get(id)
if (!allowedCommandClassifications.has(decision.classification) || !decision.runtimeExpectation || !exactTaskIds.has(decision.exactTask) || !Array.isArray(decision.sourceEvidence) || decision.sourceEvidence.length === 0) fail(`source-only command ${id} lacks a complete classification.`)
if (JSON.stringify(decision.sourceModules) !== JSON.stringify(inventoryCommand.sourceModules)) fail(`source-only command ${id} module ownership differs from the generated inventory.`)
}
const runtimeObjects = reference.runtimeObjects
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types)) fail('desktop oracle is missing the complete Document.supportedTypes runtime inventory.')
@@ -55,8 +76,16 @@ const runtimeObjectCount = runtimeObjects.availableCount
const sourceUsageTypeIds = new Set((typeInventory.documentObjectUsages ?? []).map((record) => record.typeId))
if (sourceUsageTypeIds.size === 0 || !Array.isArray(typeInventory.typeIdDeclarations)) fail('source TypeId declaration and addObject usage indexes are missing.')
const sourceUsageMissingRuntime = [...sourceUsageTypeIds].filter((typeId) => !runtimeTypeIds.has(typeId)).sort()
const expectedNotBuilt = reference.modules.filter((module) => module.runtimeStatus === 'not-built').map((module) => module.name).sort()
const decidedNotBuilt = (boundaries.notBuiltModules ?? []).map((module) => module.name).sort()
if (JSON.stringify(expectedNotBuilt) !== JSON.stringify(decidedNotBuilt)) fail(`not-built module decisions drifted: expected ${expectedNotBuilt.join(', ')}, received ${decidedNotBuilt.join(', ')}.`)
for (const module of boundaries.notBuiltModules) if (!module.buildOption || !['proxy', 'browser-reimplementation', 'source-template'].includes(module.decision) || !module.exactTask || !module.boundary || !Array.isArray(module.sourceEvidence) || module.sourceEvidence.length === 0) fail(`not-built module ${module.name} lacks a complete decision.`)
const decidedSourceOnlyTypeIds = (boundaries.sourceOnlyTypeIds ?? []).map((entry) => entry.typeId).sort()
if (JSON.stringify(sourceUsageMissingRuntime) !== JSON.stringify(decidedSourceOnlyTypeIds)) fail(`source-only TypeId decisions drifted: expected ${sourceUsageMissingRuntime.join(', ')}, received ${decidedSourceOnlyTypeIds.join(', ')}.`)
for (const entry of boundaries.sourceOnlyTypeIds) if (!entry.classification || !entry.runtimeExpectation || !Array.isArray(entry.sourceEvidence) || entry.sourceEvidence.length === 0) fail(`source-only TypeId ${entry.typeId} lacks a complete classification.`)
const staticObjectCandidateCount = sourceUsageTypeIds.size
const moduleStatusCounts = Object.fromEntries([...new Set(reference.modules.map((module) => module.runtimeStatus))].sort().map((status) => [status, reference.modules.filter((module) => module.runtimeStatus === status).length]))
const sourceOnlyCommandClassificationCounts = Object.fromEntries([...allowedCommandClassifications].sort().map((classification) => [classification, boundaries.sourceOnlyCommands.filter((command) => command.classification === classification).length]).filter(([, count]) => count > 0))
console.log(JSON.stringify({
status: 'freecad-oracle-coverage-pass',
baseline: { freecadVersion: reference.freecadVersion, commit: reference.gitCommit },
@@ -65,6 +94,8 @@ console.log(JSON.stringify({
guiCommands: guiInventory.commandCount,
guiCommandsRuntimeAligned: true,
sourceOnlyCommands: guiInventory.sourceOnlyCommands.length,
sourceOnlyCommandsClassified: sourceOnlyCommandDecisions.size,
sourceOnlyCommandClassificationCounts,
registeredObjectTypeCount: runtimeObjects.candidateCount,
runtimeObjectCount,
unavailableObjectCount: runtimeObjects.unavailableCount,
@@ -73,7 +104,10 @@ console.log(JSON.stringify({
sourceDocumentObjectUsageCount: staticObjectCandidateCount,
sourceUsageMissingRuntimeCount: sourceUsageMissingRuntime.length,
sourceUsageMissingRuntime: sourceUsageMissingRuntime,
notBuiltModuleDecisions: boundaries.notBuiltModules.map(({ name, decision, exactTask }) => ({ name, decision, exactTask })),
sourceOnlyTypeIdDecisions: boundaries.sourceOnlyTypeIds.map(({ typeId, classification }) => ({ typeId, classification })),
sourceOnlyCommandDecisions: boundaries.sourceOnlyCommands.map(({ id, classification, exactTask }) => ({ id, classification, exactTask })),
staticObjectCandidateCount,
exactPromotionReady: false,
remaining: ['not-built modules require an explicit build/proxy decision', 'source-declared TypeId/property candidates outside the registered document-object set require a native API inventory'],
remaining: ['task-scoped proxy/browser implementations remain open', 'runtime GUI task, failure, cancel and recovery states require per-command workflow oracles'],
}, null, 2))

View File

@@ -7,7 +7,7 @@ const fail = (message) => { throw new Error(`FreeCAD Property status oracle: ${m
const byId = new Map((oracle.cases ?? []).map((entry) => [entry.id, entry]))
const changed = (id) => byId.get(id)?.changed
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-property-status-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass') fail('baseline is invalid.')
if (oracle.schemaVersion !== 2 || oracle.baselineId !== 'freecad-1.1.1-property-status-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass') fail('baseline is invalid.')
if (byId.size !== 5 || ['ordinary', 'runtime-output', 'type-output', 'runtime-no-recompute', 'type-no-recompute'].some((id) => !byId.has(id))) fail('change matrix is incomplete.')
if (changed('ordinary')?.documentTouched !== true || byId.get('ordinary')?.recomputeCount !== 1) fail('ordinary input change did not touch and recompute its owner.')
if (changed('runtime-output')?.mustExecute !== false || changed('runtime-output')?.documentTouched !== false) fail('runtime Output changed its owner recompute state.')
@@ -15,8 +15,17 @@ if (changed('type-output')?.mustExecute !== false || changed('type-output')?.doc
if (changed('runtime-no-recompute')?.documentTouched !== true || byId.get('runtime-no-recompute')?.recomputeCount !== 1) fail('runtime NoRecompute no longer matches the locked implementation behavior.')
if (changed('type-no-recompute')?.documentTouched !== true || byId.get('type-no-recompute')?.recomputeCount !== 0) fail('Prop_NoRecompute did not touch without recomputing its owner.')
if (oracle.lockDynamic?.propertyStillPresent !== true || oracle.lockDynamic.renamedPropertyPresent !== false || oracle.lockDynamic.failures?.remove !== null || !oracle.lockDynamic.failures?.rename) fail('LockDynamic did not preserve FreeCAD remove-false/rename-error behavior.')
if (oracle.lockDynamic.mutableRenameAndRemove !== true || oracle.lockDynamic.undoAvailable !== true) fail('Dynamic property mutation is not transactionally undoable.')
const dynamicEvents = (oracle.lockDynamic.observerEvents ?? []).map((event) => `${event.type}:${event.property ?? event.name ?? ''}`)
if (dynamicEvents.join(',') !== 'property.added:MutableValue,transaction.opened:dynamic-property-mutation,property.before-change:MutableValue,property.changed:MutableValue,property.removed:RenamedValue,transaction.committed:') fail('Dynamic property observer/transaction order drifted.')
const partialTrigger = oracle.partialTrigger
if (partialTrigger?.reportedStatus?.join(',') !== 'PartialTrigger' || partialTrigger.value !== true || partialTrigger.recomputeCount !== 1 || partialTrigger.undoAvailable !== true || partialTrigger.state?.documentTouched !== false) fail('PartialTrigger native mutation behavior drifted.')
const partialEvents = (partialTrigger.observerEvents ?? []).map((event) => `${event.type}:${event.property ?? event.name ?? ''}`)
if (partialEvents.join(',') !== 'property.before-change:PartialLoad,transaction.opened:partial-trigger-mutation,property.changed:PartialLoad,property.changed:Shape,transaction.committed:') fail('PartialTrigger observer/transaction order drifted.')
const xml = oracle.fcstd?.xml
const reopened = oracle.fcstd?.reopened
if (xml?.persistedProperty !== true || xml.runtimeTransientProperty !== true || xml.typeTransientProperty !== true || xml.noPersistProperty !== false || xml.runtimeTransientHasValue !== false || xml.typeTransientHasValue !== false) fail('FCStd Property persistence partition drifted.')
if (JSON.stringify(reopened?.properties) !== JSON.stringify(['Persisted', 'RuntimeTransient', 'TypeTransient']) || reopened.values?.Persisted !== 11 || reopened.values.RuntimeTransient !== 0 || reopened.values.TypeTransient !== 0) fail('FCStd reopen did not restore persisted/transient defaults exactly.')
console.log(JSON.stringify({ status: 'freecad-property-status-oracle-pass', cases: byId.size, outputSuppressesTouch: true, noRecomputePartition: true, lockDynamic: true, fcstdRoundtrip: true }, null, 2))
if (xml.floatConstraintNative !== true || xml.quantityConstraintNative !== true || xml.precisionNative !== true || xml.hiddenLinkNative !== true) fail('Native high-frequency App Property XML codecs drifted.')
if (reopened.codecValues?.FloatConstraint !== 0.25 || reopened.codecValues.QuantityConstraint !== 12.5 || reopened.codecValues.Precision !== 0.001 || reopened.codecValues.HiddenLink !== 'Target') fail('Native high-frequency App Property codecs did not reopen losslessly.')
console.log(JSON.stringify({ status: 'freecad-property-status-oracle-pass', cases: byId.size, outputSuppressesTouch: true, noRecomputePartition: true, lockDynamic: true, partialTrigger: true, observerTransactions: true, nativeCodecTypes: 4, fcstdRoundtrip: true }, null, 2))

View File

@@ -0,0 +1,92 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { validateCompositeMutationEvidence } from './freecad-composite-mutation-evidence.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const fail = (message) => { throw new Error(`FreeCAD TSN stage correlation: ${message}`) }
const [desktop, production, productionRegistry, composite, resave, mutations] = await Promise.all([
load('config/freecad-tsn-stage-correlation-oracle.json'),
load('config/freecad-isomorphic-provenance-verification.json'),
load('config/chrome-freecad-naming-production-verification.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
load('config/freecad-composite-history-resave-verification.json'),
load('config/freecad-parameter-mutation-report.json'),
])
const operations = ['cut', 'rotate', 'fillet', 'mirrored', 'linear-pattern']
if (desktop.schemaVersion !== 1 || desktop.baselineId !== 'freecad-1.1.1-tsn-stage-correlation' || desktop.freecadVersion !== '1.1.1' || desktop.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || desktop.status !== 'pass') fail('desktop oracle is not locked to FreeCAD 1.1.1.')
const harnessPath = resolve(root, desktop.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (desktop.harness?.path !== 'scripts/freecad-tsn-stage-correlation-oracle.py' || desktop.harness.bytes !== harnessBytes || desktop.harness.sha256 !== createHash('sha256').update(harnessContent).digest('hex')) fail('desktop oracle is stale for its executable harness.')
if (JSON.stringify(desktop.operations) !== JSON.stringify(operations) || JSON.stringify(production.cases?.longChain?.operations) !== JSON.stringify(operations)) fail('desktop and production five-stage operation order differs.')
if (desktop.stages?.length !== 5 || desktop.reopenedStages?.length !== 5 || desktop.resavedStages?.length !== 5) fail('desktop save/reopen/resave stage count is incomplete.')
for (let index = 0; index < operations.length; index += 1) {
const before = desktop.stages[index]
const reopened = desktop.reopenedStages[index]
const resaved = desktop.resavedStages[index]
if (before.operation !== operations[index] || reopened.operation !== before.operation || resaved.operation !== before.operation || !before.shape?.valid || before.mappedNameCount < 1 || before.historyRelations < 1) fail(`desktop stage ${operations[index]} has no valid topology association.`)
if (before.mappedNameDigest !== reopened.mappedNameDigest || before.mappedNameDigest !== resaved.mappedNameDigest) fail(`desktop stage ${operations[index]} changed mapped names after save/reopen/resave.`)
}
if (desktop.summary?.wrongBindings !== 0 || desktop.summary?.unexplainedRelations !== 0 || desktop.summary?.roundtripNameDrift !== 0 || desktop.summary?.resaveNameDrift !== 0) fail(`desktop zero-difference metrics failed: ${JSON.stringify(desktop.summary)}`)
if (desktop.nativeDesktopResaveCovered !== true || desktop.summary?.mutationCases !== 5 || desktop.summary?.mutationPassed !== 5 || desktop.mutations?.length !== 5) fail('desktop cross-feature mutation/resave corpus is incomplete.')
for (const [index, mutation] of desktop.mutations.entries()) if (mutation.operation !== operations[index] || mutation.status !== 'pass' || mutation.parameterChanged !== true || mutation.downstreamShapeChanged !== true || mutation.restoredExactly !== true) fail(`desktop ${operations[index]} mutation did not edit, recompute and restore the chain.`)
if (composite.summary?.cases !== 30 || composite.summary?.passed !== 30 || resave.summary?.nativeDesktopResaveCases !== 30 || resave.summary?.roundtripNameDrift !== 0 || resave.summary?.resaveNameDrift !== 0) fail('the locked 30-case native desktop resave corpus is incomplete.')
const compositeMutation = validateCompositeMutationEvidence(composite)
const resaveById = new Map((resave.cases || []).map((fixture) => [fixture.id, fixture]))
let compositeCaseReconciliations = 0
if (resaveById.size !== compositeMutation.mutationCases || resave.cases?.length !== compositeMutation.mutationCases) fail('the 30-case resave report is not joined one-to-one with mutation evidence.')
for (const fixture of composite.cases) {
const resaveFixture = resaveById.get(fixture.id)
if (!resaveFixture || resaveFixture.roundtripNameDrift !== 0 || resaveFixture.resaveNameDrift !== 0 || resaveFixture.nativeDesktopResaveCovered !== true) fail(`${fixture.id} lacks a passing native resave transaction.`)
for (const phase of ['initial', 'reopened', 'resaved']) {
const records = resaveFixture.stageCorrelations?.[phase]
if (!Array.isArray(records) || records.length !== fixture.stages.length) fail(`${fixture.id}/${phase} lacks all stage correlations.`)
for (const [ordinal, stage] of fixture.stages.entries()) {
const record = records[ordinal]
const key = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${stage.name}`
if (record.key !== key || record.ordinal !== ordinal || record.name !== stage.name || record.typeId !== stage.typeId || record.relationDigest !== stage.relationDigest || record.semanticNameDigest !== stage.semanticNameDigest) fail(`${key}/${phase} changed its locked stage evidence.`)
}
}
compositeCaseReconciliations += 1
}
if (compositeCaseReconciliations !== 30 || compositeMutation.mutationStageRecords !== 219 || compositeMutation.mutationStageRestoreFailures !== 0 || compositeMutation.mutationFinalPropagationFailures !== 0) fail('the 30-case mutation corpus is not fully reconciled.')
const mutationFamilyByOperation = new Map(mutations.families.flatMap((family) => (family.operationTypes || []).map((operation) => [operation, family])))
const nativeMutationFamilies = { cut: 'Part::Cut', fillet: 'Part::Fillet', mirrored: 'PartDesign::Mirrored', 'linear-pattern': 'PartDesign::LinearPattern' }
const nativeMutationCorrelation = operations.map((operation) => {
const requiredFamily = nativeMutationFamilies[operation]
const family = requiredFamily ? mutations.families.find(({ id }) => id === requiredFamily) : mutationFamilyByOperation.get(operation)
return { operation, familyId: family?.id ?? null, complete: family?.complete === true }
})
const missingNativeMutationOperations = nativeMutationCorrelation.filter(({ complete }) => !complete).map(({ operation }) => operation)
const proxyStageOperations = desktop.stages.filter(({ nativeBuilder }) => nativeBuilder !== true).map(({ operation }) => operation)
if (JSON.stringify(proxyStageOperations) !== '["rotate","mirrored","linear-pattern"]' || desktop.summary?.nativeBuilderStages !== 2 || desktop.summary?.proxyStages !== 3 || desktop.exactCorrelationReady !== false) fail('desktop native-builder/proxy boundary drifted.')
const registeredOperations = productionRegistry.operations || []
const coveredTransitions = operations.slice(1).map((operation, index) => `${operations[index]}->${operation}`)
const orderedOperationPairs = registeredOperations.length * registeredOperations.length
const blockers = [
`Native desktop builder stages missing from the sequential chain: ${proxyStageOperations.join(', ')}`,
`Native parameter-mutation family missing for production operation: ${missingNativeMutationOperations.join(', ')}`,
`${desktop.summary.mutationRestoreTopologyDriftCases}/5 edit/restore cases changed ${desktop.summary.mutationRestoreTopologyDriftStages} downstream semantic topology digests without wrong binding`,
`${compositeMutation.mutationNamingRestoreDriftCases}/30 native case mutations changed ${compositeMutation.mutationNamingRestoreDriftStages} recovered naming-stage digests while all 219 geometry stages restored`,
`Only ${coveredTransitions.length}/${orderedOperationPairs} ordered production operation pairs are classified and replayed; ${orderedOperationPairs - coveredTransitions.length} remain unclassified for type compatibility`,
]
console.log(JSON.stringify({
status: 'freecad-tsn-stage-correlation-fail-closed',
exactPromotionReady: false,
productionStages: production.cases.longChain.stages.length,
desktopStages: desktop.summary.stages,
nativeBuilderStages: desktop.summary.nativeBuilderStages,
proxyStageOperations,
compositeMutations: { cases: compositeMutation.mutationCases, passed: compositeMutation.mutationPassed, caseReconciliations: compositeCaseReconciliations, categoryCases: compositeMutation.categoryCases, stageRecords: compositeMutation.mutationStageRecords, phaseStageRecords: compositeMutation.mutationPhaseStageRecords, finalPropagationFailures: compositeMutation.mutationFinalPropagationFailures, geometryRestoreFailures: compositeMutation.mutationStageRestoreFailures, namingRestoreDriftCases: compositeMutation.mutationNamingRestoreDriftCases, namingRestoreDriftStages: compositeMutation.mutationNamingRestoreDriftStages },
mutations: { cases: desktop.summary.mutationCases, passed: desktop.summary.mutationPassed, nativeCorrelated: nativeMutationCorrelation.filter(({ complete }) => complete).length, missingNativeMutationOperations, restoreTopologyDriftCases: desktop.summary.mutationRestoreTopologyDriftCases, restoreTopologyDriftStages: desktop.summary.mutationRestoreTopologyDriftStages },
transitions: { registeredOperations: registeredOperations.length, orderedOperationPairs, covered: coveredTransitions.length, unclassified: orderedOperationPairs - coveredTransitions.length, coveredTransitions },
nativeDesktopResaveCases: resave.summary.nativeDesktopResaveCases + 1,
wrongBindings: desktop.summary.wrongBindings,
unexplainedRelations: desktop.summary.unexplainedRelations,
roundtripNameDrift: desktop.summary.roundtripNameDrift,
resaveNameDrift: desktop.summary.resaveNameDrift,
blockers,
}, null, 2))

View File

@@ -1,6 +1,7 @@
import hashlib
import json
import os
import re
import tempfile
import zipfile
@@ -34,8 +35,32 @@ def square_sketch(body, name, half_size=2.0, z=0.0):
def shape_summary(shape):
if shape is None or shape.isNull():
return {"valid": False, "solids": 0, "faces": 0, "edges": 0, "volume": 0.0}
return {"valid": bool(shape.isValid()), "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "volume": float(shape.Volume)}
return {"valid": False, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "area": 0.0, "brepSha256": None}
brep = shape.exportBrepToString()
bounds = shape.BoundBox
geometry = {
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": round(float(shape.Volume), 9),
"area": round(float(shape.Area), 9),
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
"vertexPoints": sorted([round(float(vertex.Point.x), 9), round(float(vertex.Point.y), 9), round(float(vertex.Point.z), 9)] for vertex in shape.Vertexes),
"edgeLengths": sorted(round(float(edge.Length), 9) for edge in shape.Edges),
"faceAreas": sorted(round(float(face.Area), 9) for face in shape.Faces),
}
return {
"valid": bool(shape.isValid()),
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": float(shape.Volume),
"area": float(shape.Area),
"brepSha256": hashlib.sha256(brep.encode("utf-8")).hexdigest(),
"geometryDigest": digest_json(geometry),
}
def history_entry(obj, name):
@@ -59,6 +84,81 @@ def relation_from_mapped_name(mapped_name):
return "preserved"
def normalize_mapped_name(mapped_name):
# StringHasher IDs are process-local. Preserve the token grammar while
# removing only the volatile hash-table slot assigned in this process.
return re.sub(r":H[0-9a-fA-F]+", ":H#", mapped_name or "")
def source_element_name(mapped_name):
match = re.search(r"(?:^|[;(])((?:Face|Edge|Vertex)\d+)", mapped_name or "")
if match:
return match.group(1)
local_token = re.search(r"(?:^|[;(])(#[a-zA-Z0-9]+:[0-9a-fA-F]+)", mapped_name or "")
if local_token:
return local_token.group(1)
sketch_token = re.search(r"(?:^|[;(])(g\d+(?:v\d+)?;SKT)", mapped_name or "")
if sketch_token:
return sketch_token.group(1)
indexed_token = re.search(r"(?:^|[;(])(#[0-9a-fA-F]+)", mapped_name or "")
return indexed_token.group(1) if indexed_token else ""
def linked_objects(obj):
result = set()
def collect(value):
if hasattr(value, "Name") and getattr(value, "Document", None) is obj.Document:
result.add(value.Name)
elif isinstance(value, (list, tuple)):
for item in value:
collect(item)
for property_name in obj.PropertiesList:
try:
property_type = obj.getTypeIdOfProperty(property_name)
except Exception:
continue
if "PropertyLink" not in property_type:
continue
try:
collect(getattr(obj, property_name))
except Exception:
pass
result.discard(obj.Name)
return sorted(result)
def relation_records(obj, names):
records = []
for entry in names:
history = entry.get("history") or []
sources = []
for item in history:
if item.get("object") == obj.Name or not item.get("object"):
continue
sources.append({
"sourceObject": item.get("object"),
"sourceTypeId": item.get("typeId"),
"sourceElement": source_element_name(item.get("mappedName")),
"mappedName": normalize_mapped_name(item.get("mappedName")),
"children": sorted(normalize_mapped_name(child) for child in (item.get("children") or [])),
})
records.append({
"resultName": entry["name"],
"mappedName": normalize_mapped_name(entry.get("mappedName")),
"indexedName": entry.get("indexedName") or "",
"relation": relation_from_mapped_name(entry.get("mappedName")),
"sources": sorted(sources, key=lambda source: (source["sourceObject"], source["sourceElement"], source["mappedName"])),
})
return records
def digest_json(value):
payload = json.dumps(value, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def native_mapping(obj, kind, index, name, mapped_name, mapped_ids, indexed_name, indexed_ids, history):
reference_name = mapped_name or indexed_name or name
source_refs = []
@@ -132,6 +232,7 @@ def stage_report(obj):
"history": history,
})
native_mapped_names.append(native_mapping(obj, kind, index, name, mapped, mapped_ids, indexed, indexed_ids, history))
relations = relation_records(obj, names)
native_evidence = {
"schemaVersion": 1,
"stageId": obj.Name,
@@ -159,10 +260,167 @@ def stage_report(obj):
"elementMapVersion": getattr(shape, "ElementMapVersion", None),
"elementMapSize": int(getattr(shape, "ElementMapSize", 0) or 0),
"names": names,
"linkedObjects": linked_objects(obj),
"relations": relations,
"relationDigest": digest_json(relations),
"semanticNameDigest": digest_json([{
"name": entry["name"],
"mappedName": normalize_mapped_name(entry.get("mappedName")),
"indexedName": entry.get("indexedName") or "",
} for entry in names]),
"nativeEvidence": native_evidence,
}
def correlation_snapshot(case_id, stages):
return [{
"key": "%s:%03d:%s" % (case_id, ordinal, stage["name"]),
"ordinal": ordinal,
"name": stage["name"],
"typeId": stage["typeId"],
"linkedObjects": stage["linkedObjects"],
"relationCount": len(stage["relations"]),
"relationDigest": stage["relationDigest"],
"semanticNameDigest": stage["semanticNameDigest"],
} for ordinal, stage in enumerate(stages)]
def parameter_value(value):
if hasattr(value, "Value"):
return float(value.Value)
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
return [float(value.x), float(value.y), float(value.z)]
if isinstance(value, (bool, int, float, str)) or value is None:
return value
return str(value)
def mutation_stage_snapshot(case_id, stages):
return [{
"key": "%s:%03d:%s" % (case_id, ordinal, stage["name"]),
"ordinal": ordinal,
"name": stage["name"],
"typeId": stage["typeId"],
"shape": stage["shape"],
"relationDigest": stage["relationDigest"],
"semanticNameDigest": stage["semanticNameDigest"],
} for ordinal, stage in enumerate(stages)]
def mutation_geometry_fingerprint(stage):
return stage["shape"]["geometryDigest"]
def mutation_naming_fingerprint(stage):
return (stage["relationDigest"], stage["semanticNameDigest"])
def collect_named_stages(document, stage_names):
stages = []
for stage_name in stage_names:
obj = document.getObject(stage_name)
stage = stage_report(obj) if obj is not None else None
if stage is None:
raise RuntimeError("mutation removed or invalidated stage %s" % stage_name)
stages.append(stage)
return stages
def capture_mutation(case_id, document, construction, before_stages):
final = construction["final"]
target = construction["mutationTarget"]
property_name = construction["mutationProperty"]
edited_value = construction["mutationValue"]
stage_names = [stage["name"] for stage in before_stages]
stage_ordinals = {stage_name: ordinal for ordinal, stage_name in enumerate(stage_names)}
if target.Name not in stage_ordinals or final.Name not in stage_ordinals:
raise RuntimeError("mutation target or final object has no captured Shape stage")
if property_name not in target.PropertiesList:
raise RuntimeError("mutation property %s.%s is not registered" % (target.Name, property_name))
editor_modes = list(target.getEditorMode(property_name))
property_type = target.getTypeIdOfProperty(property_name)
native_editable = "ReadOnly" not in editor_modes and "Hidden" not in editor_modes
original_value = getattr(target, property_name)
before_value = parameter_value(original_value)
try:
setattr(target, property_name, edited_value)
document.recompute()
edited_value_readback = parameter_value(getattr(target, property_name))
edited_stages = collect_named_stages(document, stage_names)
finally:
setattr(target, property_name, original_value)
document.recompute()
restored_value = parameter_value(getattr(target, property_name))
restored_stages = collect_named_stages(document, stage_names)
before_geometry = [mutation_geometry_fingerprint(stage) for stage in before_stages]
edited_geometry = [mutation_geometry_fingerprint(stage) for stage in edited_stages]
restored_geometry = [mutation_geometry_fingerprint(stage) for stage in restored_stages]
before_naming = [mutation_naming_fingerprint(stage) for stage in before_stages]
edited_naming = [mutation_naming_fingerprint(stage) for stage in edited_stages]
restored_naming = [mutation_naming_fingerprint(stage) for stage in restored_stages]
changed_ordinals = [ordinal for ordinal, (before, edited) in enumerate(zip(before_geometry, edited_geometry)) if before != edited]
restoration_drift_ordinals = [ordinal for ordinal, (before, restored) in enumerate(zip(before_geometry, restored_geometry)) if before != restored]
naming_changed_ordinals = [ordinal for ordinal, (before, edited) in enumerate(zip(before_naming, edited_naming)) if before != edited]
naming_restoration_drift_ordinals = [ordinal for ordinal, (before, restored) in enumerate(zip(before_naming, restored_naming)) if before != restored]
final_ordinal = stage_ordinals[final.Name]
target_ordinal = stage_ordinals[target.Name]
property_changed = before_value != edited_value_readback
final_shape_changed = before_geometry[final_ordinal] != edited_geometry[final_ordinal]
final_brep_changed = before_stages[final_ordinal]["shape"]["brepSha256"] != edited_stages[final_ordinal]["shape"]["brepSha256"]
property_restored = before_value == restored_value
all_stages_restored = len(restoration_drift_ordinals) == 0
edited_shapes_valid = all(stage["shape"]["valid"] for stage in edited_stages)
status = "pass" if native_editable and property_changed and final_shape_changed and property_restored and all_stages_restored and edited_shapes_valid else "failed"
return {
"schemaVersion": 1,
"category": construction["category"],
"contract": {
"targetObject": target.Name,
"targetTypeId": target.TypeId,
"targetStageOrdinal": target_ordinal,
"propertyPath": property_name,
"propertyType": property_type,
"editorModes": editor_modes,
"nativeEditable": native_editable and property_type.startswith("App::Property"),
"finalObject": final.Name,
"finalStageOrdinal": final_ordinal,
"requiresFinalPropagation": True,
"requiresAllStageRestore": True,
},
"values": {
"before": before_value,
"edited": edited_value_readback,
"restored": restored_value,
},
"phases": {
"before": mutation_stage_snapshot(case_id, before_stages),
"edited": mutation_stage_snapshot(case_id, edited_stages),
"restored": mutation_stage_snapshot(case_id, restored_stages),
},
"metrics": {
"propertyChanged": property_changed,
"finalShapeChanged": final_shape_changed,
"finalBrepChanged": final_brep_changed,
"propertyRestored": property_restored,
"allStagesRestored": all_stages_restored,
"allStageGeometryRestored": all_stages_restored,
"allStageNamingRestored": len(naming_restoration_drift_ordinals) == 0,
"editedShapesValid": edited_shapes_valid,
"changedStageOrdinals": changed_ordinals,
"changedStages": len(changed_ordinals),
"restorationDriftOrdinals": restoration_drift_ordinals,
"restorationDriftStages": len(restoration_drift_ordinals),
"namingChangedStageOrdinals": naming_changed_ordinals,
"namingChangedStages": len(naming_changed_ordinals),
"namingRestorationDriftOrdinals": naming_restoration_drift_ordinals,
"namingRestorationDriftStages": len(naming_restoration_drift_ordinals),
"stageRecords": len(before_stages),
"phaseStageRecords": len(before_stages) * 3,
},
"status": status,
}
def make_boolean(doc, operation, index):
base = doc.addObject("Part::Box", "Box%02d" % index)
base.Length, base.Width, base.Height = 10.0, 10.0, 10.0
@@ -176,7 +434,14 @@ def make_boolean(doc, operation, index):
else:
result = doc.addObject("Part::Cut", "Cut%02d" % index)
result.Base, result.Tool = base, tool
return result
return {
"category": "boolean",
"final": result,
"base": base,
"mutationTarget": tool,
"mutationProperty": "Radius",
"mutationValue": float(tool.Radius.Value) + 0.5,
}
def make_partdesign(doc, mode, index):
@@ -210,18 +475,37 @@ def make_partdesign(doc, mode, index):
elif mode == "pocket-twoside":
pocket.Length2 = 1.0
doc.recompute()
return pocket
return pad
return {
"category": "partdesign",
"final": pocket,
"mutationTarget": pad,
"mutationProperty": "Length",
"mutationValue": float(pad.Length.Value) + 0.75,
}
return {
"category": "partdesign",
"final": pad,
"mutationTarget": pad,
"mutationProperty": "Length",
"mutationValue": float(pad.Length.Value) + 0.75,
}
def make_composite(doc, index):
first = make_boolean(doc, "fuse" if index % 2 else "cut", index)
first_construction = make_boolean(doc, "fuse" if index % 2 else "cut", index)
first = first_construction["final"]
second = doc.addObject("Part::Box", "SecondBox%02d" % index)
second.Length, second.Width, second.Height = 4.0, 4.0, 4.0
second.Placement.Base = App.Vector(3.0, 3.0, 3.0)
final = doc.addObject("Part::Cut", "CompositeCut%02d" % index)
final.Base, final.Tool = first, second
return final
return {
"category": "composite-boolean",
"final": final,
"mutationTarget": first_construction["base"],
"mutationProperty": "Length",
"mutationValue": float(first_construction["base"].Length.Value) + 0.5,
}
def make_dressup(doc, mode, index):
@@ -236,17 +520,27 @@ def make_dressup(doc, mode, index):
feature = body.newObject("PartDesign::Chamfer", "Chamfer%02d" % index)
feature.Base, feature.Size = (box, ["Edge1"]), 1.0
doc.recompute()
return feature
return {
"category": "dress-up",
"final": feature,
"mutationTarget": feature,
"mutationProperty": "Radius" if mode == "fillet" else "Size",
"mutationValue": 1.25,
}
def collect_case(case_id, factory, index, directory):
name = "ElementMapOracle%02d" % index
doc = App.newDocument(name)
try:
final = factory(doc, index)
construction = factory(doc, index)
final = construction["final"]
doc.recompute()
stages = [entry for obj in doc.Objects if (entry := stage_report(obj)) is not None]
before_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in stages for entry in stage["names"]}
mutation = capture_mutation(case_id, doc, construction, stages)
stages = collect_named_stages(doc, [stage["name"] for stage in stages])
before_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in stages for entry in stage["names"]}
initial_correlations = correlation_snapshot(case_id, stages)
final_name = getattr(final, "Name", "")
path = os.path.join(directory, "%s.FCStd" % case_id)
doc.saveAs(path)
@@ -275,7 +569,8 @@ def collect_case(case_id, factory, index, directory):
reopened = App.openDocument(path)
reopened.recompute()
after_stages = [entry for obj in reopened.Objects if (entry := stage_report(obj)) is not None]
after_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in after_stages for entry in stage["names"]}
after_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in after_stages for entry in stage["names"]}
reopened_correlations = correlation_snapshot(case_id, after_stages)
roundtrip_drift = sum(1 for key, value in before_names.items() if after_names.get(key) != value)
resaved_path = os.path.join(directory, "%s-resaved.FCStd" % case_id)
reopened.saveAs(resaved_path)
@@ -283,10 +578,11 @@ def collect_case(case_id, factory, index, directory):
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_stages = [entry for obj in resaved.Objects if (entry := stage_report(obj)) is not None]
resaved_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in resaved_stages for entry in stage["names"]}
resaved_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in resaved_stages for entry in stage["names"]}
resaved_correlations = correlation_snapshot(case_id, resaved_stages)
resave_name_drift = sum(1 for key, value in before_names.items() if resaved_names.get(key) != value)
App.closeDocument(resaved.Name)
return {"id": case_id, "finalObject": final_name, "stages": stages, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "resaveNameDrift": resave_name_drift, "nativeDesktopResaveCovered": True, "status": "pass"}
return {"id": case_id, "category": construction["category"], "finalObject": final_name, "stages": stages, "mutation": mutation, "stageCorrelations": {"initial": initial_correlations, "reopened": reopened_correlations, "resaved": resaved_correlations}, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "resaveNameDrift": resave_name_drift, "nativeDesktopResaveCovered": True, "status": "pass" if mutation["status"] == "pass" else "failed"}
except Exception as error:
return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)}
finally:
@@ -312,11 +608,30 @@ with tempfile.TemporaryDirectory(prefix="freecad-elementmap-oracle-") as directo
version = App.Version()
report = {
"schemaVersion": 1,
"mutationContractVersion": 1,
"baselineId": "freecad-1.1.1-composite-history-elementmap2",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "pass" if len(cases) == 30 and all(case["status"] == "pass" for case in cases) else "failed",
"cases": cases,
"summary": {"cases": len(cases), "passed": sum(1 for case in cases if case["status"] == "pass"), "failed": sum(1 for case in cases if case["status"] != "pass"), "roundtripNameDrift": sum(case.get("roundtripNameDrift", 0) for case in cases), "resaveNameDrift": sum(case.get("resaveNameDrift", 0) for case in cases), "nativeDesktopResaveCases": sum(1 for case in cases if case.get("nativeDesktopResaveCovered") is True)},
"summary": {
"cases": len(cases),
"passed": sum(1 for case in cases if case["status"] == "pass"),
"failed": sum(1 for case in cases if case["status"] != "pass"),
"categoryCases": {category: sum(1 for case in cases if case.get("category") == category) for category in ("boolean", "partdesign", "composite-boolean", "dress-up")},
"stageCorrelations": sum(len(case.get("stages", [])) for case in cases),
"relationRecords": sum(sum(len(stage.get("relations", [])) for stage in case.get("stages", [])) for case in cases),
"mutationCases": sum(1 for case in cases if case.get("mutation")),
"mutationPassed": sum(1 for case in cases if case.get("mutation", {}).get("status") == "pass"),
"mutationStageRecords": sum(case.get("mutation", {}).get("metrics", {}).get("stageRecords", 0) for case in cases),
"mutationPhaseStageRecords": sum(case.get("mutation", {}).get("metrics", {}).get("phaseStageRecords", 0) for case in cases),
"mutationFinalPropagationFailures": sum(1 for case in cases if case.get("mutation", {}).get("metrics", {}).get("finalShapeChanged") is not True),
"mutationStageRestoreFailures": sum(case.get("mutation", {}).get("metrics", {}).get("restorationDriftStages", 0) for case in cases),
"mutationNamingRestoreDriftCases": sum(1 for case in cases if case.get("mutation", {}).get("metrics", {}).get("namingRestorationDriftStages", 0) > 0),
"mutationNamingRestoreDriftStages": sum(case.get("mutation", {}).get("metrics", {}).get("namingRestorationDriftStages", 0) for case in cases),
"roundtripNameDrift": sum(case.get("roundtripNameDrift", 0) for case in cases),
"resaveNameDrift": sum(case.get("resaveNameDrift", 0) for case in cases),
"nativeDesktopResaveCases": sum(1 for case in cases if case.get("nativeDesktopResaveCovered") is True),
},
}
print("FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,94 @@
const CASES = [
...['fuse', 'cut', 'common'].flatMap((operation) => [1, 2, 3].map((variant) => [`boolean-${operation}-${variant}`, 'boolean'])),
...['plain', 'midplane', 'reverse', 'taper', 'twoside', 'pocket', 'pocket-through', 'pocket-midplane', 'pocket-twoside', 'pocket-taper', 'pocket-up-to-face'].map((mode) => [`partdesign-${mode}`, 'partdesign']),
...[1, 2, 3, 4, 5, 6].map((variant) => [`composite-${String(variant).padStart(2, '0')}`, 'composite-boolean']),
...['fillet', 'chamfer'].flatMap((operation) => [1, 2].map((variant) => [`dressup-${operation}-${variant}`, 'dress-up'])),
]
export const expectedCompositeMutationCases = new Map(CASES)
export const expectedCompositeMutationCategoryCases = {
boolean: 9,
partdesign: 11,
'composite-boolean': 6,
'dress-up': 4,
}
const fail = (message) => { throw new Error(`FreeCAD composite mutation evidence: ${message}`) }
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)])) : value
const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
const stageFingerprint = (stage) => [stage?.shape?.geometryDigest, stage?.relationDigest, stage?.semanticNameDigest]
const geometryFingerprint = (stage) => stage?.shape?.geometryDigest
const namingFingerprint = (stage) => [stage?.relationDigest, stage?.semanticNameDigest]
const validatePhaseRecord = (fixture, nominalStage, ordinal, phase, record) => {
const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${nominalStage.name}`
if (record?.key !== expectedKey || record.ordinal !== ordinal || record.name !== nominalStage.name || record.typeId !== nominalStage.typeId) fail(`${fixture.id}/${phase}/${ordinal} changed its locked stage identity.`)
if (record.shape?.valid !== true || !sha256(record.shape?.brepSha256) || !sha256(record.shape?.geometryDigest) || !sha256(record.relationDigest) || !sha256(record.semanticNameDigest)) fail(`${fixture.id}/${phase}/${nominalStage.name} lacks valid geometry or naming digests.`)
}
export const validateCompositeMutationEvidence = (report) => {
if (report?.mutationContractVersion !== 1 || !Array.isArray(report.cases) || report.cases.length !== CASES.length) fail('the report does not declare the locked 30-case mutation contract.')
if (!same(report.cases.map(({ id }) => id), CASES.map(([id]) => id))) fail('case identities or ordering differ from the locked constructor inventory.')
const categoryCases = Object.fromEntries(Object.keys(expectedCompositeMutationCategoryCases).map((category) => [category, 0]))
let mutationCases = 0
let mutationPassed = 0
let mutationStageRecords = 0
let mutationPhaseStageRecords = 0
let mutationFinalPropagationFailures = 0
let mutationStageRestoreFailures = 0
let mutationNamingRestoreDriftCases = 0
let mutationNamingRestoreDriftStages = 0
const cases = []
for (const fixture of report.cases) {
const expectedCategory = expectedCompositeMutationCases.get(fixture.id)
if (fixture.category !== expectedCategory || fixture.status !== 'pass') fail(`${fixture.id} is not a passing ${expectedCategory} case.`)
categoryCases[fixture.category] += 1
const mutation = fixture.mutation
const contract = mutation?.contract
const metrics = mutation?.metrics
const phases = mutation?.phases
if (mutation?.schemaVersion !== 1 || mutation.category !== fixture.category || mutation.status !== 'pass' || !contract || !metrics || !phases) fail(`${fixture.id} has no passing mutation transaction.`)
if (contract.nativeEditable !== true || !Array.isArray(contract.editorModes) || contract.editorModes.includes('ReadOnly') || contract.editorModes.includes('Hidden') || !/^App::Property/.test(contract.propertyType || '') || typeof contract.propertyPath !== 'string' || !contract.propertyPath) fail(`${fixture.id} did not mutate a native editable FreeCAD property.`)
if (contract.finalObject !== fixture.finalObject || contract.requiresFinalPropagation !== true || contract.requiresAllStageRestore !== true) fail(`${fixture.id} changed its propagation or restoration contract.`)
if (!Number.isInteger(contract.targetStageOrdinal) || !Number.isInteger(contract.finalStageOrdinal) || contract.targetStageOrdinal < 0 || contract.finalStageOrdinal < 0 || contract.targetStageOrdinal >= fixture.stages.length || contract.finalStageOrdinal >= fixture.stages.length) fail(`${fixture.id} has invalid target/final stage ordinals.`)
if (fixture.stages[contract.targetStageOrdinal]?.name !== contract.targetObject || fixture.stages[contract.targetStageOrdinal]?.typeId !== contract.targetTypeId || fixture.stages[contract.finalStageOrdinal]?.name !== fixture.finalObject) fail(`${fixture.id} mutation targets are not bound to captured stages.`)
if (same(mutation.values?.before, mutation.values?.edited) || !same(mutation.values?.before, mutation.values?.restored)) fail(`${fixture.id} property edit or restoration readback is invalid.`)
if (!['before', 'edited', 'restored'].every((phase) => Array.isArray(phases[phase]) && phases[phase].length === fixture.stages.length)) fail(`${fixture.id} lacks a full three-phase stage matrix.`)
for (let ordinal = 0; ordinal < fixture.stages.length; ordinal += 1) {
const nominal = fixture.stages[ordinal]
for (const phase of ['before', 'edited', 'restored']) validatePhaseRecord(fixture, nominal, ordinal, phase, phases[phase][ordinal])
if (!same(stageFingerprint(phases.restored[ordinal]), stageFingerprint(nominal))) fail(`${fixture.id}/${nominal.name} recovered state differs from the saved nominal stage.`)
}
const changedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.edited[ordinal]) ? [] : [ordinal])
const restorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => geometryFingerprint(phases.before[ordinal]) === geometryFingerprint(phases.restored[ordinal]) ? [] : [ordinal])
const namingChangedStageOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.edited[ordinal])) ? [] : [ordinal])
const namingRestorationDriftOrdinals = fixture.stages.flatMap((_stage, ordinal) => same(namingFingerprint(phases.before[ordinal]), namingFingerprint(phases.restored[ordinal])) ? [] : [ordinal])
if (!same(metrics.changedStageOrdinals, changedStageOrdinals) || metrics.changedStages !== changedStageOrdinals.length || !changedStageOrdinals.includes(contract.finalStageOrdinal)) fail(`${fixture.id} did not propagate its native edit to the final object.`)
if (!same(metrics.restorationDriftOrdinals, restorationDriftOrdinals) || metrics.restorationDriftStages !== restorationDriftOrdinals.length || restorationDriftOrdinals.length !== 0) fail(`${fixture.id} did not restore every stage exactly.`)
if (!same(metrics.namingChangedStageOrdinals, namingChangedStageOrdinals) || metrics.namingChangedStages !== namingChangedStageOrdinals.length || !same(metrics.namingRestorationDriftOrdinals, namingRestorationDriftOrdinals) || metrics.namingRestorationDriftStages !== namingRestorationDriftOrdinals.length || metrics.allStageNamingRestored !== (namingRestorationDriftOrdinals.length === 0)) fail(`${fixture.id} naming edit/restoration metrics do not match its native phase evidence.`)
if (metrics.propertyChanged !== true || metrics.finalShapeChanged !== true || metrics.finalBrepChanged !== true || metrics.propertyRestored !== true || metrics.allStagesRestored !== true || metrics.allStageGeometryRestored !== true || metrics.editedShapesValid !== true) fail(`${fixture.id} mutation acceptance metrics are incomplete.`)
if (metrics.stageRecords !== fixture.stages.length || metrics.phaseStageRecords !== fixture.stages.length * 3) fail(`${fixture.id} mutation stage record counts are inconsistent.`)
mutationCases += 1
mutationPassed += 1
mutationStageRecords += metrics.stageRecords
mutationPhaseStageRecords += metrics.phaseStageRecords
mutationFinalPropagationFailures += metrics.finalShapeChanged === true ? 0 : 1
mutationStageRestoreFailures += metrics.restorationDriftStages
mutationNamingRestoreDriftCases += metrics.namingRestorationDriftStages > 0 ? 1 : 0
mutationNamingRestoreDriftStages += metrics.namingRestorationDriftStages
cases.push({ id: fixture.id, category: fixture.category, stages: fixture.stages.length, changedStages: metrics.changedStages, restoredStages: metrics.stageRecords, namingRestoreDriftStages: metrics.namingRestorationDriftStages })
}
const summary = { mutationCases, mutationPassed, mutationStageRecords, mutationPhaseStageRecords, mutationFinalPropagationFailures, mutationStageRestoreFailures, mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages, categoryCases }
for (const [key, value] of Object.entries(summary)) {
if (!same(report.summary?.[key], value)) fail(`summary ${key} differs from per-case evidence.`)
}
if (!same(categoryCases, expectedCompositeMutationCategoryCases) || mutationCases !== 30 || mutationPassed !== 30 || mutationStageRecords !== 219 || mutationPhaseStageRecords !== 657 || mutationFinalPropagationFailures !== 0 || mutationStageRestoreFailures !== 0) fail(`locked totals are incomplete: ${JSON.stringify(summary)}`)
return { ...summary, cases }
}

View File

@@ -0,0 +1,300 @@
import json
import os
import sys
import FreeCAD as App
import FreeCADGui as Gui
import Part
import Sketcher
from PySide import QtWidgets
MARKER = "FREECAD_GUI_WORKFLOW_RESULT="
def progress(phase):
print("FREECAD_GUI_WORKFLOW_PROGRESS=" + phase, file=sys.stderr, flush=True)
def emit(value, status=0):
result_file = os.environ.get("FREECAD_GUI_WORKFLOW_RESULT_FILE", "")
if result_file:
parent = os.path.dirname(result_file)
if parent:
os.makedirs(parent, exist_ok=True)
with open(result_file, "w", encoding="utf-8") as stream:
json.dump(value, stream, sort_keys=True, separators=(",", ":"))
stream.write("\n")
os._exit(status)
payload = (MARKER + json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
offset = 0
while offset < len(payload):
offset += os.write(sys.stdout.fileno(), payload[offset:])
os._exit(status)
def flush_gui():
Gui.updateGui()
QtWidgets.QApplication.processEvents()
def active_edit_name():
gui_document = Gui.activeDocument()
in_edit = gui_document.getInEdit() if gui_document else None
obj = getattr(in_edit, "Object", None) if in_edit else None
return str(getattr(obj, "Name", ""))
def task_panel_state(phase):
main_window = Gui.getMainWindow()
focus = QtWidgets.QApplication.focusWidget()
buttons = []
for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
if not box.isVisible():
continue
for button in box.buttons():
buttons.append({
"text": str(button.text()).replace("&", ""),
"objectName": str(button.objectName()),
"enabled": bool(button.isEnabled()),
"default": bool(button.isDefault()),
"role": str(box.buttonRole(button)),
})
known_button_ids = {id(button) for box in main_window.findChildren(QtWidgets.QDialogButtonBox) for button in box.buttons()}
for button in main_window.findChildren(QtWidgets.QPushButton):
if id(button) in known_button_ids or not button.isVisible():
continue
buttons.append({
"text": str(button.text()).replace("&", ""),
"objectName": str(button.objectName()),
"enabled": bool(button.isEnabled()),
"default": bool(button.isDefault()),
"role": "direct-push-button",
})
fields = []
field_types = (
QtWidgets.QLineEdit,
QtWidgets.QComboBox,
QtWidgets.QSpinBox,
QtWidgets.QDoubleSpinBox,
QtWidgets.QCheckBox,
)
seen = set()
for field_type in field_types:
for widget in main_window.findChildren(field_type):
identity = id(widget)
if identity in seen or not widget.isVisible():
continue
seen.add(identity)
fields.append({
"className": str(widget.metaObject().className()),
"objectName": str(widget.objectName()),
"enabled": bool(widget.isEnabled()),
"focus": widget is focus,
})
return {
"phase": phase,
"activeDialog": bool(Gui.Control.activeDialog()),
"inEdit": active_edit_name(),
"focus": None if focus is None else {
"className": str(focus.metaObject().className()),
"objectName": str(focus.objectName()),
},
"buttons": buttons,
"fieldCount": len(fields),
"fieldClasses": sorted({field["className"] for field in fields}),
}
def click_accept():
main_window = Gui.getMainWindow()
def finish_click(button):
button.click()
flush_gui()
if Gui.Control.activeDialog() or active_edit_name():
Gui.activeDocument().resetEdit()
flush_gui()
return True
candidates = []
for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
if not box.isVisible():
continue
for button in box.buttons():
if box.buttonRole(button) == QtWidgets.QDialogButtonBox.AcceptRole and button.isEnabled():
candidates.append(button)
if candidates:
return finish_click(candidates[0])
for button in main_window.findChildren(QtWidgets.QPushButton):
text = str(button.text()).replace("&", "").strip().lower()
if button.isVisible() and button.isEnabled() and text in ("ok", "accept", "done"):
return finish_click(button)
dialog = Gui.Control.activeDialog()
if dialog and hasattr(dialog, "accept"):
dialog.accept()
flush_gui()
return True
return False
def selection_names():
return [str(item.ObjectName) for item in Gui.Selection.getSelectionEx()]
def available_transactions(document, method_name):
method = getattr(document, method_name, None)
if not callable(method):
return []
try:
return [str(value) for value in method()]
except Exception:
return []
def profile_name(feature):
value = getattr(feature, "Profile", None)
if isinstance(value, tuple) and value:
return str(getattr(value[0], "Name", ""))
return str(getattr(value, "Name", ""))
def counter(document, name):
try:
return int(getattr(document, name))
except Exception:
return None
try:
progress("activate-workbench:start")
Gui.activateWorkbench("PartDesignWorkbench")
flush_gui()
progress("activate-workbench:done")
document = App.newDocument("PadGuiSuccess")
document.UndoMode = 1
body = document.addObject("PartDesign::Body", "Body")
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
points = [
App.Vector(0, 0, 0),
App.Vector(4, 0, 0),
App.Vector(4, 3, 0),
App.Vector(0, 3, 0),
]
for index in range(4):
sketch.addGeometry(Part.LineSegment(points[index], points[(index + 1) % 4]), False)
document.recompute()
Gui.activeView().setActiveObject("pdbody", body)
Gui.Selection.clearSelection()
Gui.Selection.addSelection(document.Name, sketch.Name)
flush_gui()
progress("fixture:done")
command = Gui.Command.get("PartDesign_Pad")
actions = list(command.getAction()) if command else []
before = {
"activeWorkbench": str(Gui.activeWorkbench().name()),
"commandRegistered": command is not None,
"commandActive": bool(command.isActive()) if command else False,
"actionCount": len(actions),
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
"selection": selection_names(),
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"undoNames": available_transactions(document, "getAvailableUndoNames"),
}
progress("run-command:start")
Gui.runCommand("PartDesign_Pad", 0)
flush_gui()
progress("run-command:done")
pad = document.getObject("Pad")
opened = task_panel_state("opened")
if pad is None:
raise RuntimeError("PartDesign_Pad did not create a Pad preview object")
pad.Length = 10.0
document.recompute()
flush_gui()
progress("preview:done")
preview = {
"name": str(pad.Name),
"typeId": str(pad.TypeId),
"length": round(float(pad.Length.Value), 9),
"shapeValid": bool(pad.Shape.isValid()),
"solidCount": len(pad.Shape.Solids),
"volume": round(float(pad.Shape.Volume), 9),
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"profile": profile_name(pad),
}
progress("accept:start")
acceptClicked = click_accept()
progress("accept:done")
document.recompute()
flush_gui()
closed = task_panel_state("closed")
pad = document.getObject("Pad")
after = {
"acceptClicked": acceptClicked,
"selection": selection_names(),
"objectNames": [str(obj.Name) for obj in document.Objects],
"bodyGroup": [str(obj.Name) for obj in body.Group],
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
"pad": {
"name": str(pad.Name),
"typeId": str(pad.TypeId),
"length": round(float(pad.Length.Value), 9),
"shapeValid": bool(pad.Shape.isValid()),
"solidCount": len(pad.Shape.Solids),
"faceCount": len(pad.Shape.Faces),
"edgeCount": len(pad.Shape.Edges),
"vertexCount": len(pad.Shape.Vertexes),
"volume": round(float(pad.Shape.Volume), 9),
"profile": profile_name(pad),
"state": [str(value) for value in pad.State],
},
"undoCount": counter(document, "UndoCount"),
"redoCount": counter(document, "RedoCount"),
"undoNames": available_transactions(document, "getAvailableUndoNames"),
"redoNames": available_transactions(document, "getAvailableRedoNames"),
}
version = App.Version()
result = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"freecadVersion": ".".join(str(value) for value in version[:3]),
"gitCommit": str(version[7]) if len(version) > 7 else "",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "success",
"before": before,
"taskPanel": {
"opened": opened,
"preview": preview,
"closed": closed,
},
"after": after,
"success": bool(
opened["activeDialog"]
and opened["inEdit"] == "Pad"
and acceptClicked
and not closed["activeDialog"]
and not closed["inEdit"]
and after["bodyTip"] == "Pad"
and after["pad"]["shapeValid"]
and after["pad"]["solidCount"] == 1
and abs(after["pad"]["volume"] - 120.0) < 1e-7
),
}
progress("result:done")
App.closeDocument(document.Name)
emit(result, 0 if result["success"] else 2)
except Exception as error:
progress("error:" + type(error).__name__)
emit({
"schemaVersion": 1,
"baselineId": "freecad-1.1.1",
"workflowId": "partdesign-pad-task",
"commandId": "PartDesign_Pad",
"state": "success",
"success": False,
"error": {"type": type(error).__name__, "message": str(error)},
}, 2)

View File

@@ -2,6 +2,7 @@ import json
import os
import tempfile
import zipfile
import xml.etree.ElementTree as ET
import FreeCAD as App
@@ -9,6 +10,43 @@ import FreeCAD as App
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
class PropertyObserver:
def __init__(self, document_name):
self.document_name = document_name
self.events = []
def clear(self):
self.events = []
def slotOpenTransaction(self, document, name):
if document.Name == self.document_name:
self.events.append({"type": "transaction.opened", "name": str(name)})
def slotCommitTransaction(self, document):
if document.Name == self.document_name:
self.events.append({"type": "transaction.committed"})
def slotAbortTransaction(self, document):
if document.Name == self.document_name:
self.events.append({"type": "transaction.aborted"})
def slotBeforeChangeObject(self, obj, prop):
if obj.Document and obj.Document.Name == self.document_name:
self.events.append({"type": "property.before-change", "object": obj.Name, "property": str(prop)})
def slotChangedObject(self, obj, prop):
if obj.Document and obj.Document.Name == self.document_name:
self.events.append({"type": "property.changed", "object": obj.Name, "property": str(prop)})
def slotAppendDynamicProperty(self, obj, prop):
if getattr(obj, "Document", None) and obj.Document.Name == self.document_name:
self.events.append({"type": "property.added", "object": obj.Name, "property": str(prop)})
def slotRemoveDynamicProperty(self, obj, prop):
if getattr(obj, "Document", None) and obj.Document.Name == self.document_name:
self.events.append({"type": "property.removed", "object": obj.Name, "property": str(prop)})
def object_state(obj):
return {
"state": [str(value) for value in obj.State],
@@ -48,10 +86,17 @@ def change_case(case_id, attr=0, runtime_status=None):
def locked_dynamic_case():
document = App.newDocument("PropertyStatus_LockDynamic")
observer = None
try:
document.UndoMode = 1
obj = document.addObject("App::FeaturePython", "Feature")
obj.addProperty("App::PropertyInteger", "LockedValue", "Oracle")
obj.setPropertyStatus("LockedValue", "LockDynamic")
observer = PropertyObserver(document.Name)
App.addDocumentObserver(observer)
document.openTransaction("dynamic-property-mutation")
obj.addProperty("App::PropertyInteger", "MutableValue", "Oracle")
obj.MutableValue = 3
failures = {}
for operation, callback in {
"remove": lambda: obj.removeProperty("LockedValue"),
@@ -62,13 +107,48 @@ def locked_dynamic_case():
failures[operation] = None
except Exception as error:
failures[operation] = {"type": type(error).__name__, "message": str(error)}
obj.renameProperty("MutableValue", "RenamedValue")
removed_mutable = bool(obj.removeProperty("RenamedValue"))
document.commitTransaction()
return {
"reportedStatus": [str(value) for value in obj.getPropertyStatus("LockedValue")],
"propertyStillPresent": "LockedValue" in obj.PropertiesList,
"renamedPropertyPresent": "RenamedValue" in obj.PropertiesList,
"failures": failures,
"mutableRenameAndRemove": removed_mutable and "MutableValue" not in obj.PropertiesList and "RenamedValue" not in obj.PropertiesList,
"observerEvents": observer.events,
"undoAvailable": document.UndoCount == 1,
}
finally:
if observer:
App.removeDocumentObserver(observer)
App.closeDocument(document.Name)
def partial_trigger_case():
document = App.newDocument("PropertyStatus_PartialTrigger")
observer = PropertyObserver(document.Name)
App.addDocumentObserver(observer)
try:
document.UndoMode = 1
obj = document.addObject("PartDesign::SubShapeBinder", "Binder")
document.recompute()
document.purgeTouched()
observer.clear()
document.openTransaction("partial-trigger-mutation")
obj.PartialLoad = True
recomputed = int(document.recompute())
document.commitTransaction()
return {
"reportedStatus": [str(value) for value in obj.getPropertyStatus("PartialLoad")],
"value": bool(obj.PartialLoad),
"recomputeCount": recomputed,
"state": object_state(obj),
"observerEvents": observer.events,
"undoAvailable": document.UndoCount == 1,
}
finally:
App.removeDocumentObserver(observer)
App.closeDocument(document.Name)
@@ -78,6 +158,7 @@ def archive_case():
document = App.newDocument("PropertyStatusArchive")
try:
obj = document.addObject("App::FeaturePython", "Feature")
target = document.addObject("App::FeaturePython", "Target")
obj.addProperty("App::PropertyInteger", "Persisted", "Oracle")
obj.Persisted = 11
obj.setPropertyStatus("Persisted", "Output")
@@ -88,6 +169,14 @@ def archive_case():
obj.TypeTransient = 13
obj.addProperty("App::PropertyInteger", "NoPersist", "Oracle", "", int(App.PropertyType.Prop_NoPersist))
obj.NoPersist = 14
obj.addProperty("App::PropertyFloatConstraint", "FloatConstraint", "Oracle")
obj.FloatConstraint = 0.25
obj.addProperty("App::PropertyQuantityConstraint", "QuantityConstraint", "Oracle")
obj.QuantityConstraint = 12.5
obj.addProperty("App::PropertyPrecision", "Precision", "Oracle")
obj.Precision = 0.001
obj.addProperty("App::PropertyLinkHidden", "HiddenLink", "Oracle")
obj.HiddenLink = target
document.recompute()
document.saveAs(path)
finally:
@@ -95,6 +184,16 @@ def archive_case():
with zipfile.ZipFile(path, "r") as archive:
document_xml = archive.read("Document.xml").decode("utf-8")
xml_root = ET.fromstring(document_xml)
xml_properties = {
element.attrib.get("name"): element
for element in xml_root.findall("./ObjectData/Object/Properties/Property")
}
def native_float_property(name, expected):
element = xml_properties.get(name)
child = element.find("Float") if element is not None else None
return bool(element is not None and element.attrib.get("type") == "App::Property" + name and child is not None and abs(float(child.attrib["value"]) - expected) < 1e-12)
reopened = App.openDocument(path)
try:
@@ -107,6 +206,10 @@ def archive_case():
"noPersistProperty": '<Property name="NoPersist"' in document_xml,
"runtimeTransientHasValue": '<Integer value="12"' in document_xml,
"typeTransientHasValue": '<Integer value="13"' in document_xml,
"floatConstraintNative": native_float_property("FloatConstraint", 0.25),
"quantityConstraintNative": native_float_property("QuantityConstraint", 12.5),
"precisionNative": native_float_property("Precision", 0.001),
"hiddenLinkNative": xml_properties.get("HiddenLink") is not None and xml_properties["HiddenLink"].attrib.get("type") == "App::PropertyLinkHidden" and xml_properties["HiddenLink"].find("Link") is not None and xml_properties["HiddenLink"].find("Link").attrib.get("value") == "Target",
},
"reopened": {
"properties": sorted(str(value) for value in obj.PropertiesList if value in {"Persisted", "RuntimeTransient", "TypeTransient", "NoPersist"}),
@@ -119,6 +222,12 @@ def archive_case():
name: [str(value) for value in obj.getPropertyStatus(name)]
for name in ["Persisted", "RuntimeTransient", "TypeTransient"]
},
"codecValues": {
"FloatConstraint": float(obj.FloatConstraint),
"QuantityConstraint": float(obj.QuantityConstraint.Value),
"Precision": float(obj.Precision),
"HiddenLink": obj.HiddenLink.Name,
},
},
}
finally:
@@ -133,13 +242,14 @@ cases = [
change_case("type-no-recompute", attr=int(App.PropertyType.Prop_NoRecompute)),
]
report = {
"schemaVersion": 1,
"schemaVersion": 2,
"baselineId": "freecad-1.1.1-property-status-oracle",
"freecadVersion": ".".join(str(value) for value in App.Version()[:3]),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"cases": cases,
"lockDynamic": locked_dynamic_case(),
"partialTrigger": partial_trigger_case(),
"fcstd": archive_case(),
}
print("FREECAD_PROPERTY_STATUS_ORACLE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -1,16 +1,41 @@
import importlib
import importlib.util
import faulthandler
import hashlib
import json
import os
import re
import sys
import FreeCAD as App
PROBE_SCOPE = os.environ.get("FREECAD_REFERENCE_SCOPE", "full")
if PROBE_SCOPE not in {"full", "gui-commands"}:
raise RuntimeError("Unknown FreeCAD reference probe scope: " + PROBE_SCOPE)
TRACEBACK_SECONDS = int(os.environ.get("FREECAD_REFERENCE_TRACEBACK_SECONDS", "0"))
if TRACEBACK_SECONDS > 0:
faulthandler.dump_traceback_later(TRACEBACK_SECONDS, repeat=True, file=2)
ISOLATED_GUI_CONFIG = os.environ.get("FREECAD_ORACLE_ISOLATED_CONFIG") == "1"
if PROBE_SCOPE == "gui-commands" and ISOLATED_GUI_CONFIG:
App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").SetBool("FirstTime", False)
def probe_progress(message):
print("FREECAD_REFERENCE_PROGRESS=" + message, file=sys.stderr, flush=True)
def property_metadata(obj):
def stable_text(value):
text = re.sub(r"0x[0-9a-fA-F]+", "<address>", str(value))
text = re.sub(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "<uuid>", text)
return re.sub(r"(FreeCAD_Doc_<uuid>_[^/_]+_)\d+", r"\1<run>", text)
def json_value(value):
if value is None or isinstance(value, (bool, int, float, str)):
if value is None or isinstance(value, (bool, int, float)):
return value
if isinstance(value, str):
return stable_text(value)
if isinstance(value, (list, tuple)):
return [json_value(item) for item in value]
if isinstance(value, dict):
@@ -18,7 +43,7 @@ def property_metadata(obj):
# FreeCAD quantities and placements keep a stable textual form while
# the report remains valid JSON for every registered property type.
try:
return str(value)
return stable_text(value)
except Exception:
return "<unserializable>"
@@ -168,7 +193,7 @@ def probe_module(name):
return result
def probe_gui_commands(document, selected_object):
def probe_gui_commands():
if not bool(getattr(App, "GuiUp", False)):
return {"available": False, "workbenches": [], "commands": []}
try:
@@ -180,61 +205,147 @@ def probe_gui_commands(document, selected_object):
workbench_names = sorted(Gui.listWorkbenches().keys())
except Exception:
workbench_names = []
command_observations = {}
workbench_filter = os.environ.get("FREECAD_GUI_WORKBENCH_FILTER")
if workbench_filter:
if workbench_filter not in workbench_names:
raise RuntimeError("Unknown GUI workbench filter: " + workbench_filter)
workbench_names = [workbench_filter]
command_registrations = {}
def action_enabled(command_id):
def command_active(command_id):
try:
command = Gui.Command.get(command_id)
if command is None:
return None
actions = command.getAction()
if not actions:
return None
return bool(actions[0].isEnabled())
# StdCmdExpression dereferences its private QAction in isActive().
# Other commands must use their native predicate; QAction enabled
# state is only a cached presentation value and misses context changes.
if command_id == "Std_Expressions":
actions = command.getAction()
return bool(actions[0].isEnabled()) if actions else None
return bool(command.isActive())
except Exception as error:
return {"error": str(error)}
workbenches = []
for workbench_name in workbench_names:
probe_progress("workbench:start:" + workbench_name)
try:
Gui.activateWorkbench(workbench_name)
Gui.updateGui()
command_ids = sorted(set(Gui.listCommands()))
Gui.Selection.clearSelection()
empty_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
Gui.Selection.addSelection(document.Name, selected_object.Name)
selected_selection = {command_id: action_enabled(command_id) for command_id in command_ids}
Gui.Selection.clearSelection()
observations = {
command_id: {
"emptySelection": empty_selection.get(command_id),
"selectedPartBox": selected_selection.get(command_id),
}
for command_id in command_ids
}
for command_id, observation in observations.items():
command_observations.setdefault(command_id, []).append({"workbench": workbench_name, **observation})
for command_id in command_ids:
command_registrations.setdefault(command_id, []).append(workbench_name)
workbenches.append({"name": workbench_name, "commandCount": len(command_ids), "status": "probed"})
probe_progress("workbench:done:" + workbench_name)
except Exception as error:
try:
Gui.Selection.clearSelection()
except Exception:
pass
workbenches.append({"name": workbench_name, "commandCount": 0, "status": "probe-failure", "error": str(error)})
probe_progress("workbench:failed:" + workbench_name)
# Command registration is workbench-scoped, but document/selection state is
# intrinsic to each command. Probe the state matrix once after all providers
# are loaded instead of repeating the cumulative command set per workbench.
for document_name in list(App.listDocuments().keys()):
App.closeDocument(document_name)
command_ids = sorted(command_registrations.keys())
command_universe_sha256 = hashlib.sha256("\n".join(command_ids).encode("utf-8")).hexdigest()
if os.environ.get("FREECAD_GUI_REGISTRATION_ONLY") == "1":
return {
"available": True,
"probeMode": "registration-only",
"stateProbe": "not-probed",
"stateCases": [],
"directIsActiveBoundary": "not-applicable",
"commandUniverseCount": len(command_ids),
"commandUniverseSha256": command_universe_sha256,
"stateCommandCount": 0,
"shard": None,
"workbenches": workbenches,
"commands": [
{
"id": command_id,
"registeredWorkbenches": command_registrations[command_id],
}
for command_id in command_ids
],
}
shard = None
state_command_ids = command_ids
shard_spec = os.environ.get("FREECAD_GUI_COMMAND_SHARD")
if shard_spec:
try:
shard_index, shard_count = [int(value) for value in shard_spec.split("/", 1)]
except Exception:
raise RuntimeError("FREECAD_GUI_COMMAND_SHARD must use zero-based INDEX/COUNT syntax")
if shard_count < 1 or shard_index < 0 or shard_index >= shard_count:
raise RuntimeError("FREECAD_GUI_COMMAND_SHARD is outside its declared range")
shard_start = len(command_ids) * shard_index // shard_count
shard_end = len(command_ids) * (shard_index + 1) // shard_count
state_command_ids = command_ids[shard_start:shard_end]
shard = {
"index": shard_index,
"count": shard_count,
"start": shard_start,
"end": shard_end,
}
Gui.Selection.clearSelection()
Gui.updateGui()
probe_progress("state:start:noDocument")
no_document = {command_id: command_active(command_id) for command_id in state_command_ids}
probe_progress("state:done:noDocument")
context_document = App.newDocument("CommandContext")
selected_object = context_document.addObject("Part::Box", "CommandProbeBox")
Gui.updateGui()
probe_progress("state:start:documentNoSelection")
document_no_selection = {command_id: command_active(command_id) for command_id in state_command_ids}
probe_progress("state:done:documentNoSelection")
Gui.Selection.addSelection(context_document.Name, selected_object.Name)
Gui.updateGui()
probe_progress("state:start:selectedPartBox")
selected_selection = {command_id: command_active(command_id) for command_id in state_command_ids}
probe_progress("state:done:selectedPartBox")
Gui.Selection.clearSelection()
App.closeDocument(context_document.Name)
return {
"available": True,
"probeMode": "state-matrix",
"stateProbe": "native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard",
"stateCases": ["noDocument", "documentNoSelection", "selectedPartBox"],
"directIsActiveBoundary": "Std_Expressions-only-qaction-guard-for-locked-1.1.1-null-qaction-crash",
"commandUniverseCount": len(command_ids),
"commandUniverseSha256": command_universe_sha256,
"stateCommandCount": len(state_command_ids),
"shard": shard,
"workbenches": workbenches,
"commands": [
{"id": command_id, "observations": observations}
for command_id, observations in sorted(command_observations.items())
{
"id": command_id,
"registeredWorkbenches": command_registrations[command_id],
"observations": [{
"workbench": "all-registered-runtime-context",
"noDocument": no_document.get(command_id),
"documentNoSelection": document_no_selection.get(command_id),
"selectedPartBox": selected_selection.get(command_id),
}],
}
for command_id in state_command_ids
],
}
document = App.newDocument("ReferenceProbe")
box_object = document.addObject("Part::Box", "CommandProbeBox")
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
gui_commands = probe_gui_commands(document, box_object)
runtime_objects = probe_registered_objects(document)
modules = []
if PROBE_SCOPE == "full":
document = App.newDocument("ReferenceProbe")
# Creating one native Part object loads the same runtime type registry used
# by the locked desktop baseline before supportedTypes() is enumerated.
document.addObject("Part::Box", "TypeRegistryProbeBox")
modules = [probe_module(name) for name in ALL_REFERENCE_MODULES]
App.closeDocument(document.Name)
gui_commands = probe_gui_commands()
version = App.Version()
result = {
"schemaVersion": 1,
@@ -245,20 +356,34 @@ result = {
"gitCommit": str(version[7]) if len(version) > 7 else "",
"guiUp": bool(getattr(App, "GuiUp", False)),
"buildPurpose": "desktop-reference-oracle" if bool(getattr(App, "GuiUp", False)) else "headless-reference-oracle",
"moduleCount": len(ALL_REFERENCE_MODULES),
"probeScope": PROBE_SCOPE,
"oracleSetup": {
"isolatedConfig": ISOLATED_GUI_CONFIG,
"bimFirstTimeWelcome": "suppressed-before-workbench-activation" if ISOLATED_GUI_CONFIG else "native-user-config",
},
"determinism": {
"schemaVersion": 1,
"normalizedProcessFields": ["pointer-address", "uuid", "freecad-document-cache-run"],
},
"moduleCount": len(modules),
"modules": modules,
"objects": [probe_object(document, type_id) for type_id in [
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
]],
"runtimeObjects": runtime_objects,
"guiCommands": gui_commands,
}
if PROBE_SCOPE == "full":
# Workbench activation registers additional GUI-owned document types.
# Enumerate them only in the independent object/property scope.
document = App.newDocument("RuntimeObjectProbe")
document.addObject("Part::Box", "TypeRegistryProbeBox")
result["runtimeObjects"] = probe_registered_objects(document)
result["objects"] = [probe_object(document, type_id) for type_id in [
"Part::Box", "Part::Cylinder", "Part::Sphere", "Part::Cone",
"Part::Feature", "PartDesign::Feature", "Sketcher::SketchObject",
]]
App.closeDocument(document.Name)
result["moduleStatusSummary"] = {
status: sum(1 for module in result["modules"] if module["runtimeStatus"] == status)
for status in sorted({module["runtimeStatus"] for module in result["modules"]})
}
App.closeDocument(document.Name)
print("FREECAD_REFERENCE_RESULT=" + json.dumps(result, sort_keys=True, separators=(",", ":")))
sys.stdout.flush()
sys.exit(0)

View File

@@ -0,0 +1,368 @@
import hashlib
import json
import os
import tempfile
import FreeCAD as App
import Part
import Sketcher
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
OPERATIONS = ["cut", "rotate", "fillet", "mirrored", "linear-pattern"]
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def add_rectangle(sketch, x_min, y_min, x_max, y_max):
points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
for index, start in enumerate(points):
end = points[(index + 1) % len(points)]
sketch.addGeometry(Part.LineSegment(App.Vector(start[0], start[1], 0), App.Vector(end[0], end[1], 0)), False)
def shape_snapshot(feature):
shape = feature.Shape
if shape.isNull():
raise RuntimeError("{} produced a null Shape".format(feature.Name))
brep = shape.exportBrepToString().encode("utf-8")
bounds = shape.BoundBox
return {
"valid": bool(shape.isValid()),
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": float(shape.Volume),
"area": float(shape.Area),
"brepSha256": hashlib.sha256(brep).hexdigest(),
"bounds": [float(bounds.XMin), float(bounds.YMin), float(bounds.ZMin), float(bounds.XMax), float(bounds.YMax), float(bounds.ZMax)],
}
def mapped_names(feature):
shape = feature.Shape
result = {}
for kind, count in (("Face", len(shape.Faces)), ("Edge", len(shape.Edges)), ("Vertex", len(shape.Vertexes))):
for index in range(1, count + 1):
name = "{}{}".format(kind, index)
try:
mapped, _ = shape.getElementMappedName(name, True)
except Exception:
mapped = ""
try:
indexed, _ = shape.getElementIndexedName(name, True)
except Exception:
indexed = ""
result[name] = mapped or indexed or name
return result
def mapped_name_digest(feature):
payload = json.dumps(mapped_names(feature), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def relation_kind(mapped_name):
if ":G" in mapped_name:
return "generated"
if ":M" in mapped_name:
return "modified"
return "preserved"
def semantic_topology_digest(feature):
relations = {}
for name in mapped_names(feature):
try:
history = feature.getElementHistory(name)
except Exception:
history = []
relations[name] = sorted({
"{}:{}:{}".format(getattr(source, "Name", ""), getattr(source, "TypeId", ""), relation_kind(mapped or ""))
for source, mapped, _children in history
if getattr(source, "Name", "")
})
payload = json.dumps(relations, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
def history_sources(feature):
sources = set()
relations = 0
for name in mapped_names(feature):
try:
history = feature.getElementHistory(name)
except Exception:
history = []
relations += len(history)
for source, _mapped, _children in history:
if getattr(source, "Name", ""):
sources.add(source.Name)
return sorted(sources), relations
def create_chain(document):
body = document.addObject("PartDesign::Body", "Body")
base = body.newObject("PartDesign::AdditiveBox", "Base")
base.Length, base.Width, base.Height = 20.0, 10.0, 4.0
document.recompute()
pocket_sketch = body.newObject("Sketcher::SketchObject", "PocketSketch")
add_rectangle(pocket_sketch, 7.0, 3.0, 11.0, 7.0)
pocket_sketch.Placement.Base.z = 4.0
pocket = body.newObject("PartDesign::Pocket", "CutStage")
pocket.Profile = pocket_sketch
pocket.Length = 2.0
document.recompute()
rotated = body.newObject("PartDesign::Feature", "RotateStage")
rotated.addProperty("App::PropertyLink", "SourceFeature", "Correlation")
rotated.addProperty("App::PropertyAngle", "Angle", "Correlation")
rotated.SourceFeature = pocket
rotated.Angle = 15.0
rotated_shape = pocket.Shape.copy()
transformed = rotated_shape.rotate(App.Vector(10, 5, 0), App.Vector(0, 0, 1), float(rotated.Angle))
if transformed is not None:
rotated_shape = transformed
rotated.Shape = rotated_shape
document.recompute()
fillet = body.newObject("PartDesign::Fillet", "FilletStage")
fillet.Base = (rotated, ["Edge1"])
fillet.Radius = 0.2
document.recompute()
mirrored = body.newObject("PartDesign::Feature", "MirroredStage")
mirrored.addProperty("App::PropertyLink", "SourceFeature", "Correlation")
mirrored.addProperty("App::PropertyVector", "PlaneNormal", "Correlation")
mirrored.SourceFeature = fillet
mirrored.PlaneNormal = App.Vector(1, 0, 0)
mirrored_shape = fillet.Shape.copy()
transformed = mirrored_shape.mirror(App.Vector(0, 0, 0), mirrored.PlaneNormal)
if transformed is not None:
mirrored_shape = transformed
mirrored.Shape = mirrored_shape
document.recompute()
pattern = body.newObject("PartDesign::Feature", "LinearPatternStage")
pattern.addProperty("App::PropertyLink", "SourceFeature", "Correlation")
pattern.addProperty("App::PropertyInteger", "Occurrences", "Correlation")
pattern.addProperty("App::PropertyLength", "Length", "Correlation")
pattern.SourceFeature = mirrored
pattern.Occurrences = 2
pattern.Length = 24.0
copies = []
for index in range(pattern.Occurrences):
copy = mirrored.Shape.copy()
transformed = copy.translate(App.Vector(float(pattern.Length) * index, 0, 0))
if transformed is not None:
copy = transformed
copies.append(copy)
pattern.Shape = Part.makeCompound(copies)
document.recompute()
return [pocket, rotated, fillet, mirrored, pattern]
def rebuild_derived(document, features):
pocket, rotated, fillet, mirrored, pattern = features
document.recompute()
rotated_shape = pocket.Shape.copy()
transformed = rotated_shape.rotate(App.Vector(10, 5, 0), App.Vector(0, 0, 1), float(rotated.Angle))
if transformed is not None:
rotated_shape = transformed
rotated.Shape = rotated_shape
document.recompute()
mirrored_shape = fillet.Shape.copy()
transformed = mirrored_shape.mirror(App.Vector(0, 0, 0), mirrored.PlaneNormal)
if transformed is not None:
mirrored_shape = transformed
mirrored.Shape = mirrored_shape
copies = []
for index in range(pattern.Occurrences):
copy = mirrored.Shape.copy()
transformed = copy.translate(App.Vector(float(pattern.Length) * index, 0, 0))
if transformed is not None:
copy = transformed
copies.append(copy)
pattern.Shape = Part.makeCompound(copies)
document.recompute()
def topology_snapshot(features):
return [{
"operation": operation,
"name": feature.Name,
"shape": shape_snapshot(feature),
"mappedNameCount": len(mapped_names(feature)),
"mappedNameDigest": mapped_name_digest(feature),
"semanticTopologyDigest": semantic_topology_digest(feature),
} for operation, feature in zip(OPERATIONS, features)]
def parameter_value(value):
if hasattr(value, "Value"):
return float(value.Value)
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
return [float(value.x), float(value.y), float(value.z)]
return value
def geometry_signature(snapshot):
shape = snapshot["shape"]
return [shape["valid"], shape["solids"], shape["faces"], shape["edges"], shape["vertices"], round(shape["volume"], 7), round(shape["area"], 7)]
def capture_mutation(document, features, operation, feature, property_name, edited_value):
original_value = getattr(feature, property_name)
before_parameter = parameter_value(original_value)
before = topology_snapshot(features)
try:
setattr(feature, property_name, edited_value)
rebuild_derived(document, features)
edited_parameter = parameter_value(getattr(feature, property_name))
edited = topology_snapshot(features)
finally:
setattr(feature, property_name, original_value)
rebuild_derived(document, features)
restored_parameter = parameter_value(getattr(feature, property_name))
restored = topology_snapshot(features)
before_digests = [stage["semanticTopologyDigest"] for stage in before]
restored_digests = [stage["semanticTopologyDigest"] for stage in restored]
before_final = before[-1]["shape"]["brepSha256"]
edited_final = edited[-1]["shape"]["brepSha256"]
restored_geometrically = [geometry_signature(stage) for stage in before] == [geometry_signature(stage) for stage in restored]
restored_topology_exactly = before_digests == restored_digests
passed = before_parameter != edited_parameter and before_final != edited_final and before_parameter == restored_parameter and restored_geometrically
return {
"operation": operation,
"featureName": feature.Name,
"typeId": feature.TypeId,
"propertyPath": property_name,
"beforeParameter": before_parameter,
"editedParameter": edited_parameter,
"restoredParameter": restored_parameter,
"parameterChanged": before_parameter != edited_parameter,
"downstreamShapeChanged": before_final != edited_final,
"restoredExactly": before_parameter == restored_parameter and restored_geometrically,
"restoredGeometrically": restored_geometrically,
"restoredTopologyExactly": restored_topology_exactly,
"restoreTopologyDriftStages": sum(1 for before_digest, restored_digest in zip(before_digests, restored_digests) if before_digest != restored_digest),
"beforeTopologyDigests": before_digests,
"editedTopologyDigests": [stage["semanticTopologyDigest"] for stage in edited],
"restoredTopologyDigests": restored_digests,
"status": "pass" if passed else "failed",
}
def stage_report(operation, feature):
sources, relations = history_sources(feature)
names = mapped_names(feature)
return {
"operation": operation,
"name": feature.Name,
"typeId": feature.TypeId,
"nativeBuilder": feature.TypeId != "PartDesign::Feature",
"shape": shape_snapshot(feature),
"mappedNameCount": len(names),
"mappedNameDigest": hashlib.sha256(json.dumps(names, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest(),
"historySources": sources,
"historyRelations": relations,
"status": str(feature.getStatusString()),
}
def collect():
document = App.newDocument("TsnStageCorrelationOracle")
try:
features = create_chain(document)
rebuild_derived(document, features)
pocket, rotated, fillet, mirrored, pattern = features
mutations = [
capture_mutation(document, features, "cut", pocket, "Length", 2.5),
capture_mutation(document, features, "rotate", rotated, "Angle", 22.5),
capture_mutation(document, features, "fillet", fillet, "Radius", 0.35),
capture_mutation(document, features, "mirrored", mirrored, "PlaneNormal", App.Vector(0, 0, 1)),
capture_mutation(document, features, "linear-pattern", pattern, "Occurrences", 3),
]
stages = [stage_report(operation, feature) for operation, feature in zip(OPERATIONS, features)]
with tempfile.TemporaryDirectory(prefix="freecad-tsn-stage-correlation-") as directory:
initial_path = os.path.join(directory, "tsn-stage-correlation.FCStd")
resaved_path = os.path.join(directory, "tsn-stage-correlation-resaved.FCStd")
document.saveAs(initial_path)
App.closeDocument(document.Name)
reopened = App.openDocument(initial_path)
reopened.recompute()
reopened_stages = [stage_report(operation, reopened.getObject(stage["name"])) for operation, stage in zip(OPERATIONS, stages)]
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_stages = [stage_report(operation, resaved.getObject(stage["name"])) for operation, stage in zip(OPERATIONS, stages)]
App.closeDocument(resaved.Name)
roundtrip_name_drift = sum(1 for before, after in zip(stages, reopened_stages) if before["mappedNameDigest"] != after["mappedNameDigest"])
resave_name_drift = sum(1 for before, after in zip(stages, resaved_stages) if before["mappedNameDigest"] != after["mappedNameDigest"])
known_sources = {"Base", "PocketSketch"}
wrong_bindings = 0
unexplained_relations = 0
for stage in stages:
known_sources.add(stage["name"])
unexplained_relations += sum(1 for source in stage["historySources"] if source not in known_sources)
if stage["mappedNameCount"] <= 0:
wrong_bindings += 1
with open(__file__, "rb") as harness_file:
harness_content = harness_file.read()
return {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-tsn-stage-correlation",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"operations": OPERATIONS,
"stages": stages,
"reopenedStages": reopened_stages,
"resavedStages": resaved_stages,
"mutations": mutations,
"summary": {
"stages": len(stages),
"nativeBuilderStages": sum(1 for stage in stages if stage["nativeBuilder"]),
"proxyStages": sum(1 for stage in stages if not stage["nativeBuilder"]),
"mutationCases": len(mutations),
"mutationPassed": sum(1 for mutation in mutations if mutation["status"] == "pass"),
"mutationRestoreTopologyDriftCases": sum(1 for mutation in mutations if mutation["restoredTopologyExactly"] is not True),
"mutationRestoreTopologyDriftStages": sum(mutation["restoreTopologyDriftStages"] for mutation in mutations),
"wrongBindings": wrong_bindings,
"unexplainedRelations": unexplained_relations,
"roundtripNameDrift": roundtrip_name_drift,
"resaveNameDrift": resave_name_drift,
},
"nativeDesktopResaveCovered": True,
"exactCorrelationReady": all(stage["nativeBuilder"] for stage in stages) and all(mutation["status"] == "pass" for mutation in mutations) and wrong_bindings == 0 and unexplained_relations == 0 and roundtrip_name_drift == 0 and resave_name_drift == 0,
"harness": {
"path": "scripts/freecad-tsn-stage-correlation-oracle.py",
"bytes": len(harness_content),
"sha256": hashlib.sha256(harness_content).hexdigest(),
},
"status": "pass",
}
finally:
for name in list(App.listDocuments().keys()):
App.closeDocument(name)
try:
report = collect()
except Exception as error:
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-tsn-stage-correlation",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"operations": OPERATIONS,
"status": "failed",
"errorType": type(error).__name__,
"error": str(error),
}
print("FREECAD_TSN_STAGE_CORRELATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,120 @@
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const outputPath = resolve(root, 'config/freecad-active-work-queue.json')
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [gui, workflowPlan, composite, correlation, production] = await Promise.all([
load('.cache/freecad/reference-desktop-gui-commands.json'),
load('config/freecad-gui-workflow-plan.json'),
load('config/freecad-composite-history-elementmap-oracle.json'),
load('config/freecad-tsn-stage-correlation-oracle.json'),
load('config/chrome-freecad-naming-production-verification.json'),
])
const guiShardTasks = gui.guiCommands.mergedShards.map((shard, index) => ({
id: `ORA-GUI-SHARD-${String(index).padStart(3, '0')}`,
title: `Probe GUI command indexes [${shard.start}, ${shard.end})`,
status: 'completed',
dependencies: index === 0 ? ['ORA-GUI-SETUP-002'] : [`ORA-GUI-SHARD-${String(index - 1).padStart(3, '0')}`],
evidence: [shard.path, shard.sha256],
exit: `${shard.end - shard.start} commands have three native state observations`,
}))
const workflowTitles = [
'Select one GUI command family from the sensitivity report',
'Capture the native success workflow',
'Capture the native disabled workflow',
'Capture the native failure workflow',
'Capture the native cancel workflow',
'Capture recovery after failure or cancel',
'Update the exact blocker and focused checks',
]
const workflowProgress = new Map(workflowPlan.progress.map((entry) => [entry.taskId, entry]))
const workflowTasks = workflowTitles.map((title, index) => ({
id: `ORA-GUI-WF-${String(index).padStart(3, '0')}`,
title,
status: workflowProgress.get(`ORA-GUI-WF-${String(index).padStart(3, '0')}`).status,
dependencies: [index === 0 ? 'ORA-GUI-CONTEXT-001' : `ORA-GUI-WF-${String(index - 1).padStart(3, '0')}`],
...(index === 0 ? { family: workflowPlan.family.id, primaryCommand: workflowPlan.family.primaryCommand, exactTask: workflowPlan.family.primaryExactTask } : {}),
...(workflowProgress.get(`ORA-GUI-WF-${String(index).padStart(3, '0')}`).evidence.length > 0 ? { evidence: workflowProgress.get(`ORA-GUI-WF-${String(index).padStart(3, '0')}`).evidence } : {}),
exit: index === 0 ? 'one command family and its exact task are recorded' : 'one native workflow fixture and its focused assertion pass',
}))
const driftCases = composite.cases.filter((fixture) => fixture.mutation.metrics.namingRestorationDriftStages > 0)
const driftTasks = driftCases.map((fixture, index) => ({
id: `TSN-DRIFT-${String(index).padStart(3, '0')}`,
title: `Classify recovered naming evolution for ${fixture.id}`,
status: 'pending',
dependencies: index === 0 ? ['ORA-GUI-WF-006'] : [`TSN-DRIFT-${String(index - 1).padStart(3, '0')}`],
caseId: fixture.id,
driftStages: fixture.mutation.metrics.namingRestorationDriftStages,
driftOrdinals: fixture.mutation.metrics.namingRestorationDriftOrdinals,
exit: 'the case is classified as stable semantics, allowed evolution, or an implementation defect',
}))
const productionDriftTasks = correlation.mutations
.filter((mutation) => mutation.restoreTopologyDriftStages > 0)
.map((mutation, index) => ({
id: `TSN-PROD-DRIFT-${String(index).padStart(3, '0')}`,
title: `Classify production restore topology drift for ${mutation.operation}`,
status: 'pending',
dependencies: [index === 0 ? driftTasks.at(-1).id : `TSN-PROD-DRIFT-${String(index - 1).padStart(3, '0')}`],
operation: mutation.operation,
driftStages: mutation.restoreTopologyDriftStages,
exit: 'every changed downstream stage has a native comparison and classification',
}))
const coveredTransitions = new Set(['cut->rotate', 'rotate->fillet', 'fillet->mirrored', 'mirrored->linear-pattern'])
const transitionTasks = production.operations.flatMap((from) => production.operations.map((to) => {
const pair = `${from}->${to}`
return {
id: `TSN-PAIR-${from}-${to}`,
title: `Classify ordered operation pair ${pair}`,
status: coveredTransitions.has(pair) ? 'completed' : 'pending',
dependencies: coveredTransitions.has(pair) ? [] : [productionDriftTasks.at(-1).id],
pair,
exit: 'native acceptance or rejection is recorded; accepted pairs include builder, mutation, naming, and resave evidence',
}
}))
const setupTasks = [
{ id: 'ORA-GUI-SETUP-000', title: 'Split GUI state probing from object/property probing', status: 'completed' },
{ id: 'ORA-GUI-SETUP-001', title: 'Diagnose the BIM first-run modal block', status: 'completed', dependencies: ['ORA-GUI-SETUP-000'] },
{ id: 'ORA-GUI-SETUP-002', title: 'Isolate FreeCAD config and suppress BIM first-run welcome before probing', status: 'completed', dependencies: ['ORA-GUI-SETUP-001'] },
]
const closureTasks = [
{ id: 'ORA-GUI-MERGE-000', title: 'Merge and verify all 100 GUI command shards', status: 'completed', dependencies: [guiShardTasks.at(-1).id] },
{ id: 'ORA-GUI-CONTEXT-000', title: 'Classify Import_ReadBREP as explicit module-import context', status: 'completed', dependencies: ['ORA-GUI-MERGE-000'] },
{ id: 'ORA-GUI-CONTEXT-001', title: 'Classify NaviCubeDraggableCmd as context-menu lazy registration', status: 'completed', dependencies: ['ORA-GUI-CONTEXT-000'] },
]
const milestones = [
{ id: 'ORA-GUI-BASELINE', exactTask: 'EX-ORA-01', status: 'completed', tasks: [...setupTasks, ...guiShardTasks, ...closureTasks] },
{ id: 'ORA-GUI-WORKFLOWS', exactTask: 'EX-ORA-01', status: 'in_progress', tasks: workflowTasks },
{ id: 'TSN-RECOVERY-DRIFT', exactTask: 'EX-TSN-04', status: 'pending', tasks: [...driftTasks, ...productionDriftTasks] },
{ id: 'TSN-ORDERED-PAIRS', exactTask: 'EX-TSN-04', status: 'pending', tasks: transitionTasks },
]
const allTasks = milestones.flatMap((milestone) => milestone.tasks)
const count = (status) => allTasks.filter((task) => task.status === status).length
const nextTask = allTasks.find((task) => task.status === 'in_progress')?.id
if (!nextTask) throw new Error('FreeCAD active work queue requires one in-progress task.')
const queue = {
schemaVersion: 1,
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
updatedAt: '2026-08-14',
generatedBy: 'scripts/generate-freecad-active-work-queue.mjs',
policy: {
maxInProgress: 1,
unit: 'one artifact, one focused check, one explicit exit condition',
transitionUnit: 'one ordered operation pair',
driftUnit: 'one native case',
},
nextTask,
summary: { tasks: allTasks.length, completed: count('completed'), inProgress: count('in_progress'), pending: count('pending') },
milestones,
}
const content = `${JSON.stringify(queue, null, 2)}\n`
if (process.argv.includes('--check')) {
const current = await readFile(outputPath, 'utf8').catch(() => '')
if (current !== content) throw new Error('FreeCAD active work queue is stale; run generate:freecad-active-work-queue.')
console.log(JSON.stringify({ status: 'freecad-active-work-queue-pass', nextTask: queue.nextTask, ...queue.summary }, null, 2))
} else {
await writeFile(outputPath, content)
console.log(JSON.stringify({ status: 'freecad-active-work-queue-generated', output: outputPath, nextTask: queue.nextTask, ...queue.summary }, null, 2))
}

View File

@@ -3,12 +3,13 @@ import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const reportPath = resolve(root, process.env.FREECAD_GUI_COMMAND_REPORT || '.cache/freecad/reference-desktop.json')
const reportPath = resolve(root, process.env.FREECAD_GUI_COMMAND_REPORT || '.cache/freecad/reference-desktop-gui-commands.json')
const outputPath = resolve(root, process.env.FREECAD_GUI_COMMAND_INVENTORY || 'config/freecad-gui-command-inventory.json')
const sourceInventory = JSON.parse(await readFile(resolve(root, 'config/freecad-source-inventory.json'), 'utf8'))
const reportBytes = await readFile(reportPath)
const report = JSON.parse(reportBytes)
if (report.baselineId !== 'freecad-1.1.1' || report.freecadVersion !== '1.1.1' || report.guiUp !== true) throw new Error('A verified desktop FreeCAD GUI report is required.')
if (report.probeScope !== 'gui-commands') throw new Error('The dedicated gui-commands oracle report is required.')
if (!report.guiCommands?.available || !Array.isArray(report.guiCommands.commands)) throw new Error('Desktop report does not contain GUI command observations.')
const sourceCommandModules = new Map()
@@ -21,17 +22,21 @@ const normalizeState = (state) => typeof state === 'boolean' ? state : state &&
const unique = (values) => [...new Set(values)].sort()
const commands = report.guiCommands.commands.map((command) => {
const observations = command.observations || []
const emptyStates = unique(observations.map((entry) => normalizeState(entry.emptySelection)))
const noDocumentStates = unique(observations.map((entry) => normalizeState(entry.noDocument)))
const documentNoSelectionStates = unique(observations.map((entry) => normalizeState(entry.documentNoSelection)))
const selectedStates = unique(observations.map((entry) => normalizeState(entry.selectedPartBox)))
const selectionSensitive = observations.some((entry) => typeof entry.emptySelection === 'boolean' && typeof entry.selectedPartBox === 'boolean' && entry.emptySelection !== entry.selectedPartBox)
const documentSensitive = observations.some((entry) => typeof entry.noDocument === 'boolean' && typeof entry.documentNoSelection === 'boolean' && entry.noDocument !== entry.documentNoSelection)
const selectionSensitive = observations.some((entry) => typeof entry.documentNoSelection === 'boolean' && typeof entry.selectedPartBox === 'boolean' && entry.documentNoSelection !== entry.selectedPartBox)
return {
id: command.id,
sourceModules: unique(sourceCommandModules.get(command.id) || ['GuiCore']),
workbenches: unique(observations.map((entry) => entry.workbench)),
workbenches: unique(command.registeredWorkbenches || observations.map((entry) => entry.workbench)),
cases: {
emptySelection: { states: emptyStates, observed: true },
noDocument: { states: noDocumentStates, observed: true },
documentNoSelection: { states: documentNoSelectionStates, observed: true },
selectedPartBox: { states: selectedStates, observed: true },
},
documentSensitive,
selectionSensitive,
status: 'runtime-probed',
}
@@ -51,10 +56,14 @@ const inventory = {
baseline: { freecadVersion: '1.1.1', commit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' },
generatedBy: 'scripts/generate-freecad-gui-command-inventory.mjs',
sourceInventory: 'config/freecad-source-inventory.json',
contextBoundaries: 'config/freecad-gui-command-context-boundaries.json',
runtimeEvidence: {
report: '.cache/freecad/reference-desktop.json',
report: '.cache/freecad/reference-desktop-gui-commands.json',
reportSha256: createHash('sha256').update(reportBytes).digest('hex'),
workbenchCount: report.guiCommands.workbenches.length,
stateProbe: report.guiCommands.stateProbe,
stateCases: report.guiCommands.stateCases,
directIsActiveBoundary: report.guiCommands.directIsActiveBoundary,
},
commandCount: commands.length,
commands,

View File

@@ -1,53 +1,113 @@
import { execFile } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { createServer as createViteServer } from 'vite'
import { promisify } from 'node:util'
import { createServer as createViteServer, preview as createVitePreview } from 'vite'
import { chromium } from '../cnc_wams_gpt6/linuxcnc-master/web/node_modules/playwright/index.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const reportPath = resolve(root, 'config/chrome-cam-linuxcnc-machine-verification.json')
const artifactManifestPath = resolve(root, 'config/linuxcnc-wasm-machine-artifact.json')
const linuxcncRoot = resolve(root, 'cnc_wams_gpt6/linuxcnc-master')
const linuxcncWebRoot = resolve(linuxcncRoot, 'web')
const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
if (!existsSync(executable)) throw new Error(`Chrome executable is unavailable: ${executable}`)
const vite = await createViteServer({
configFile: false,
root,
server: {
host: '127.0.0.1',
port: 5201,
strictPort: false,
watch: null,
proxy: {
'/linuxcnc-machine': {
target: process.env.LINUXCNC_WASM_URL || 'https://localhost:5190',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/linuxcnc-machine/, ''),
},
},
},
})
await vite.listen()
const address = vite.httpServer?.address()
if (!address || typeof address === 'string') throw new Error('CAM machine harness server did not expose a port.')
const browser = await chromium.launch({ headless: true, executablePath: executable, args: ['--no-sandbox', '--disable-dev-shm-usage', '--enable-unsafe-swiftshader'] })
let linuxcnc
let vite
let browser
let report
try {
if (!existsSync(executable)) throw new Error(`Chrome executable is unavailable: ${executable}`)
const manifest = JSON.parse(await readFile(artifactManifestPath, 'utf8'))
if (manifest.schemaVersion !== 1 || !/^[0-9a-f]{40}$/.test(manifest.worktreeRevision) || !Array.isArray(manifest.artifacts) || manifest.artifacts.length !== 7 || manifest.machineConfigPath !== manifest.artifacts.find(({ role }) => role === 'machine-config')?.path) {
throw new Error('LinuxCNC WASM machine artifact manifest is invalid.')
}
const actualRevision = (await promisify(execFile)('git', ['-C', linuxcncRoot, 'rev-parse', 'HEAD'])).stdout.trim()
if (actualRevision !== manifest.worktreeRevision) throw new Error(`LinuxCNC worktree revision mismatch: expected ${manifest.worktreeRevision}, got ${actualRevision}.`)
const artifactIdentity = []
for (const artifact of manifest.artifacts) {
const path = resolve(root, manifest.distRoot, artifact.path)
const [bytes, metadata] = await Promise.all([readFile(path), stat(path)])
const sha256 = createHash('sha256').update(bytes).digest('hex')
if (metadata.size !== artifact.bytes || sha256 !== artifact.sha256) throw new Error(`LinuxCNC WASM artifact mismatch: ${artifact.path}.`)
artifactIdentity.push({ role: artifact.role, path: artifact.path, runtimeLoaded: artifact.runtimeLoaded, bytes: metadata.size, sha256 })
}
// Own the LinuxCNC server lifecycle so a missing external preview cannot be
// misreported as a parser/controller timeout.
linuxcnc = await createVitePreview({
root: linuxcncWebRoot,
configFile: resolve(linuxcncWebRoot, 'vite.config.js'),
preview: { host: '127.0.0.1', port: 0, strictPort: false },
})
const linuxcncAddress = linuxcnc.httpServer.address()
if (!linuxcncAddress || typeof linuxcncAddress === 'string') throw new Error('LinuxCNC WASM preview did not expose a port.')
const linuxcncUrl = `https://127.0.0.1:${linuxcncAddress.port}`
vite = await createViteServer({
configFile: false,
root,
server: {
host: '127.0.0.1',
port: 5201,
strictPort: false,
watch: null,
proxy: {
'/linuxcnc-machine': {
target: linuxcncUrl,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/linuxcnc-machine/, ''),
},
},
},
})
await vite.listen()
const address = vite.httpServer?.address()
if (!address || typeof address === 'string') throw new Error('CAM machine harness server did not expose a port.')
browser = await chromium.launch({ headless: true, executablePath: executable, args: ['--no-sandbox', '--disable-dev-shm-usage', '--enable-unsafe-swiftshader'] })
const browserVersion = await browser.version()
if (browserVersion !== manifest.chromeVersion) throw new Error(`Chrome version mismatch: expected ${manifest.chromeVersion}, got ${browserVersion}.`)
const context = await browser.newContext({ ignoreHTTPSErrors: true })
const page = await context.newPage()
const networkStatuses = new Map()
page.on('response', (response) => {
const pathname = new URL(response.url()).pathname
if (!pathname.startsWith('/linuxcnc-machine/')) return
const relativePath = pathname.slice('/linuxcnc-machine/'.length) || 'index.html'
networkStatuses.set(relativePath, response.status())
})
await page.goto(`http://127.0.0.1:${address.port}/chrome-cam-linuxcnc-machine-harness.html`, { waitUntil: 'domcontentloaded' })
await page.waitForFunction(() => Boolean(window.__bitbybitCamLinuxcncMachineReport), null, { timeout: 240_000 })
// The harness performs independent dry-run and run submissions, each with a
// 180 second controller budget, plus initial machine startup.
await page.waitForFunction(() => Boolean(window.__bitbybitCamLinuxcncMachineReport), null, { timeout: 420_000 })
report = await page.evaluate(() => window.__bitbybitCamLinuxcncMachineReport)
report = { ...report, browserId: 'chrome', browser: { product: await browser.version(), userAgent: await page.evaluate(() => navigator.userAgent) } }
if (report.machine?.machineCase !== manifest.machineCase || report.machine?.dryRunMachineCase !== manifest.machineCase) throw new Error(`LinuxCNC browser loaded unexpected machine cases: run=${report.machine?.machineCase || '<missing>'}, dry-run=${report.machine?.dryRunMachineCase || '<missing>'}.`)
const loadedArtifacts = manifest.artifacts.filter(({ runtimeLoaded }) => runtimeLoaded).map(({ role, path }) => ({ role, path, status: networkStatuses.get(path) ?? 0 }))
const missingArtifacts = loadedArtifacts.filter(({ status }) => status !== 200)
if (missingArtifacts.length > 0) throw new Error(`Chrome did not load pinned LinuxCNC runtime artifacts: ${missingArtifacts.map(({ role, path, status }) => `${role}:${path}:${status}`).join(', ')}.`)
report = {
...report,
runtime: {
server: 'managed-vite-preview',
worktreeRevision: actualRevision,
machineCase: report.machine.machineCase,
artifacts: artifactIdentity,
loadedArtifacts,
},
browserId: 'chrome',
browser: { product: browserVersion, userAgent: await page.evaluate(() => navigator.userAgent) },
}
await context.close()
if (report.status !== 'pass') process.exitCode = 1
} catch (error) {
report = { schemaVersion: 1, status: 'failed', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error) }
process.exitCode = 1
} finally {
await Promise.allSettled([browser?.close(), vite?.close(), linuxcnc?.close()])
await mkdir(resolve(root, 'config'), { recursive: true })
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
await browser.close()
await vite.close()
}

View File

@@ -35,7 +35,7 @@ const resaveReport = {
freecadVersion: report.freecadVersion,
gitCommit: report.gitCommit,
status: report.status,
cases: report.cases.map(({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered }) => ({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered })),
cases: report.cases.map(({ id, stageCorrelations, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered }) => ({ id, stageCorrelations, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered })),
summary: {
cases: report.summary.cases,
passed: report.summary.passed,
@@ -43,12 +43,15 @@ const resaveReport = {
roundtripNameDrift: report.summary.roundtripNameDrift,
resaveNameDrift: report.summary.resaveNameDrift,
nativeDesktopResaveCases: report.summary.nativeDesktopResaveCases,
stageCorrelations: report.summary.stageCorrelations,
relationRecords: report.summary.relationRecords,
},
}
await writeFile(resaveOutputPath, `${JSON.stringify(resaveReport, null, 2)}\n`)
for (const caseReport of report.cases) {
delete caseReport.resaveNameDrift
delete caseReport.nativeDesktopResaveCovered
delete caseReport.stageCorrelations
}
delete report.summary.roundtripNameDrift
delete report.summary.resaveNameDrift

View File

@@ -0,0 +1,81 @@
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const shardCount = Number(process.env.FREECAD_GUI_COMMAND_SHARD_COUNT || 100)
if (!Number.isInteger(shardCount) || shardCount < 2 || shardCount > 256) throw new Error('FREECAD_GUI_COMMAND_SHARD_COUNT must be an integer from 2 through 256.')
const mergeOnly = process.argv.includes('--merge-only')
const shardArgument = process.argv.find((argument) => argument.startsWith('--shard='))
const requestedShard = shardArgument ? Number(shardArgument.slice('--shard='.length)) : null
if (mergeOnly && shardArgument) throw new Error('--merge-only and --shard cannot be used together.')
if (shardArgument && (!Number.isInteger(requestedShard) || requestedShard < 0 || requestedShard >= shardCount)) throw new Error(`--shard must be an integer from 0 through ${shardCount - 1}.`)
if (!mergeOnly) {
const shardIndexes = requestedShard == null ? Array.from({ length: shardCount }, (_, index) => index) : [requestedShard]
for (const index of shardIndexes) {
console.log(`[freecad-gui-command-shards] execute ${index + 1}/${shardCount}`)
const execution = spawnSync(process.execPath, ['scripts/run-freecad-reference-probe.mjs'], {
cwd: root,
env: {
...process.env,
FREECAD_ORACLE_PROFILE: 'desktop',
FREECAD_REFERENCE_SCOPE: 'gui-commands',
FREECAD_GUI_COMMAND_SHARD: `${index}/${shardCount}`,
},
stdio: 'inherit',
})
if (execution.status !== 0) throw new Error(`FreeCAD GUI command shard ${index}/${shardCount} failed with exit ${execution.status}.`)
}
}
if (requestedShard != null) {
console.log(JSON.stringify({ status: 'freecad-gui-command-shard-pass', shard: requestedShard, shardCount }, null, 2))
process.exit(0)
}
const shardReports = []
for (let index = 0; index < shardCount; index += 1) {
const relativePath = `.cache/freecad/reference-desktop-gui-commands-${index}-of-${shardCount}.json`
const bytes = await readFile(resolve(root, relativePath))
shardReports.push({ relativePath, bytes, report: JSON.parse(bytes) })
}
const first = shardReports[0].report
const universeCount = first.guiCommands?.commandUniverseCount
const universeSha256 = first.guiCommands?.commandUniverseSha256
const commands = []
let expectedStart = 0
for (const [index, { report }] of shardReports.entries()) {
const gui = report.guiCommands
if (report.baselineId !== 'freecad-1.1.1' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.probeScope !== 'gui-commands') throw new Error(`GUI command shard ${index} has the wrong baseline.`)
if (gui?.shard?.index !== index || gui.shard.count !== shardCount || gui.shard.start !== expectedStart || gui.shard.end <= gui.shard.start) throw new Error(`GUI command shard ${index} has a non-contiguous range.`)
if (gui.commandUniverseCount !== universeCount || gui.commandUniverseSha256 !== universeSha256) throw new Error(`GUI command shard ${index} has a different command universe.`)
if (gui.stateCommandCount !== gui.commands?.length || gui.stateCommandCount !== gui.shard.end - gui.shard.start) throw new Error(`GUI command shard ${index} has an incomplete state range.`)
if (JSON.stringify(report.modules) !== JSON.stringify(first.modules) || JSON.stringify(gui.workbenches) !== JSON.stringify(first.guiCommands.workbenches)) throw new Error(`GUI command shard ${index} changed module or workbench registration evidence.`)
commands.push(...gui.commands)
expectedStart = gui.shard.end
}
if (expectedStart !== universeCount || commands.length !== universeCount || new Set(commands.map((command) => command.id)).size !== universeCount) throw new Error('Merged GUI command shards do not cover the command universe exactly once.')
const mergedUniverseSha256 = createHash('sha256').update(commands.map((command) => command.id).join('\n')).digest('hex')
if (mergedUniverseSha256 !== universeSha256) throw new Error('Merged GUI command order does not match the declared command universe hash.')
const merged = {
...first,
guiCommands: {
...first.guiCommands,
stateCommandCount: commands.length,
shard: null,
commands,
mergedShards: shardReports.map(({ relativePath, bytes, report }) => ({
path: relativePath,
sha256: createHash('sha256').update(bytes).digest('hex'),
start: report.guiCommands.shard.start,
end: report.guiCommands.shard.end,
})),
},
}
const output = resolve(root, '.cache/freecad/reference-desktop-gui-commands.json')
await writeFile(output, `${JSON.stringify(merged, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-gui-command-shards-merged', shardCount, commandCount: commands.length, commandUniverseSha256: universeSha256, output }, null, 2))

View File

@@ -0,0 +1,81 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD')
const probe = resolve(root, 'scripts/freecad-gui-workflow-oracle.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const outputDirectory = resolve(root, '.cache/freecad/gui-workflow-oracle')
const resultFile = resolve(outputDirectory, 'partdesign-pad-success-result.json')
const reportPath = resolve(root, 'config/freecad-gui-workflow-oracle.json')
const configDirectory = resolve(outputDirectory, 'config')
const userConfig = resolve(configDirectory, 'user.cfg')
const systemConfig = resolve(configDirectory, 'system.cfg')
const marker = 'FREECAD_GUI_WORKFLOW_RESULT='
const timeoutMs = Number(process.env.FREECAD_GUI_WORKFLOW_TIMEOUT_MS || 120_000)
const fail = (message) => { throw new Error(`FreeCAD GUI workflow oracle: ${message}`) }
await mkdir(outputDirectory, { recursive: true })
mkdirSync(configDirectory, { recursive: true })
const emptyConfig = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n<FCParameters><FCParamGroup Name="Root"/></FCParameters>\n'
writeFileSync(userConfig, emptyConfig)
writeFileSync(systemConfig, emptyConfig)
const runtimeEnv = {
...process.env,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'),
MPLBACKEND: 'Agg',
FREECAD_GUI_WORKFLOW_RESULT_FILE: resultFile,
}
const execution = spawnSync('xvfb-run', ['-a', executable, '--user-cfg', userConfig, '--system-cfg', systemConfig, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), probe], {
cwd: root,
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
timeout: timeoutMs,
env: runtimeEnv,
})
const output = `${execution.stdout ?? ''}\n${execution.stderr ?? ''}`
const markerLine = output.split(/\r?\n/).find((line) => line.startsWith(marker))
const resultBytes = (() => { try { return readFileSync(resultFile, 'utf8') } catch { return '' } })()
if (execution.error || (execution.status !== 0 && !resultBytes && !markerLine)) fail(`native probe exited with ${execution.status}: ${[execution.error?.message, output.trim()].filter(Boolean).join('\n')}`)
const result = JSON.parse(resultBytes || markerLine.slice(marker.length))
if (result.freecadVersion !== '1.1.1' || result.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail(`native baseline mismatch: ${JSON.stringify(result)}`)
if (result.workflowId !== 'partdesign-pad-task' || result.commandId !== 'PartDesign_Pad' || result.state !== 'success' || result.success !== true) fail(`Pad success workflow failed: ${JSON.stringify(result)}`)
const report = {
schemaVersion: 1,
baseline: {
freecadVersion: result.freecadVersion,
commit: result.gitCommit,
profile: 'desktop-xvfb-isolated-config',
},
generatedBy: './npmw run probe:freecad-gui-workflow',
checkedBy: './npmw run check:freecad-gui-workflow',
family: {
id: result.workflowId,
primaryCommand: result.commandId,
exactTask: 'EX-UI-03',
source: '.cache/freecad/FreeCAD/src/Mod/PartDesign/Gui/Command.cpp',
},
workflows: {
success: result,
},
remainingStates: ['disabled', 'failure', 'cancel', 'recovery'],
exactPromotionReady: false,
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
status: 'freecad-gui-workflow-generated',
family: report.family.id,
command: report.family.primaryCommand,
state: result.state,
bodyTip: result.after.bodyTip,
volume: result.after.pad.volume,
taskOpened: result.taskPanel.opened.activeDialog,
taskClosed: !result.taskPanel.closed.activeDialog,
report: 'config/freecad-gui-workflow-oracle.json',
}, null, 2))

View File

@@ -16,10 +16,10 @@ if (!runtime?.available || runtime.candidateCount !== 352 || runtime.availableCo
const editableTypes = new Set([
'App::PropertyAngle', 'App::PropertyBool', 'App::PropertyDistance', 'App::PropertyEnumeration',
'App::PropertyFloat', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkList',
'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkHidden', 'App::PropertyLinkList',
'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyPlacement', 'App::PropertyString',
'App::PropertyStringList', 'App::PropertyVector',
'App::PropertyPrecision', 'App::PropertyQuantityConstraint', 'App::PropertyStringList', 'App::PropertyVector',
])
const specializedTypes = new Set([
'App::PropertyExpressionEngine', 'Part::PropertyGeometryList', 'Part::PropertyPartShape',
@@ -30,8 +30,9 @@ const numericStatusNames = new Map([
[25, 'PropTransient'], [26, 'PropHidden'], [27, 'PropOutput'],
])
const facadeBehaviorStatusNames = new Set([
'Hidden', 'Immutable', 'NoModify', 'Ordered', 'Output', 'PropHidden', 'PropNoPersist',
'PropNoRecompute', 'PropOutput', 'PropReadOnly', 'PropTransient', 'ReadOnly', 'Transient',
'Hidden', 'Immutable', 'LockDynamic', 'NoModify', 'Ordered', 'Output', 'PartialTrigger',
'PropHidden', 'PropNoPersist', 'PropNoRecompute', 'PropOutput', 'PropReadOnly', 'PropTransient',
'ReadOnly', 'Transient',
])
const normalizeStatus = (status) => status.map((entry) => typeof entry === 'number' ? (numericStatusNames.get(entry) ?? `UnknownBit${entry}`) : entry)
const properties = runtime.types.flatMap((object) => (object.properties ?? []).map((property) => ({ ...property, objectTypeId: object.typeId, status: normalizeStatus(property.status) })))
@@ -81,7 +82,7 @@ const report = {
},
harness: { path: 'scripts/run-freecad-native-property-semantics.mjs', bytes: (await stat(harnessPath)).size, sha256: createHash('sha256').update(harnessContent).digest('hex') },
exactPromotionReady: false,
exactBlocker: 'complete native document and property semantics',
exactBlocker: '58 runtime property types and 523 records remain opaque-only; complete native document and property semantics',
generatedAt: new Date().toISOString(),
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)

View File

@@ -24,4 +24,4 @@ const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD Property status oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-property-status-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, cases: report.cases.length, lockDynamic: report.lockDynamic.propertyStillPresent, fcstdRoundtrip: true }, null, 2))
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, cases: report.cases.length, lockDynamic: report.lockDynamic.propertyStillPresent, partialTrigger: report.partialTrigger.value, observerTransactions: true, fcstdRoundtrip: true }, null, 2))

View File

@@ -1,4 +1,5 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, writeFileSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -6,6 +7,33 @@ import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const profile = process.env.FREECAD_ORACLE_PROFILE || 'headless'
if (!['headless', 'desktop'].includes(profile)) throw new Error(`Unknown FreeCAD oracle profile: ${profile}`)
const scope = process.env.FREECAD_REFERENCE_SCOPE || 'full'
if (!['full', 'gui-commands'].includes(scope)) throw new Error(`Unknown FreeCAD oracle scope: ${scope}`)
if (scope === 'gui-commands' && profile !== 'desktop') throw new Error('The gui-commands oracle scope requires the desktop profile.')
const shardToken = process.env.FREECAD_GUI_COMMAND_SHARD
const shardMatch = shardToken?.match(/^(\d+)\/(\d+)$/)
if (shardToken && (!shardMatch || scope !== 'gui-commands')) throw new Error('FREECAD_GUI_COMMAND_SHARD requires gui-commands scope and zero-based INDEX/COUNT syntax.')
const requestedShard = shardMatch ? { index: Number(shardMatch[1]), count: Number(shardMatch[2]) } : null
if (requestedShard && (requestedShard.count < 1 || requestedShard.index >= requestedShard.count)) throw new Error('FREECAD_GUI_COMMAND_SHARD is outside its declared range.')
const workbenchFilter = process.env.FREECAD_GUI_WORKBENCH_FILTER
if (workbenchFilter && scope !== 'gui-commands') throw new Error('FREECAD_GUI_WORKBENCH_FILTER requires gui-commands scope.')
const registrationOnly = process.env.FREECAD_GUI_REGISTRATION_ONLY === '1'
if (registrationOnly && (!workbenchFilter || requestedShard)) throw new Error('FREECAD_GUI_REGISTRATION_ONLY requires one workbench filter and cannot use a command shard.')
const timeoutMs = Number(process.env.FREECAD_REFERENCE_TIMEOUT_MS || (profile === 'desktop' ? 300000 : 120000))
if (!Number.isInteger(timeoutMs) || timeoutMs < 10000 || timeoutMs > 900000) throw new Error('FREECAD_REFERENCE_TIMEOUT_MS must be an integer from 10000 through 900000.')
const configIdentity = requestedShard
? `gui-commands-${requestedShard.index}-of-${requestedShard.count}`
: workbenchFilter ? `gui-workbench-${workbenchFilter.replace(/[^A-Za-z0-9_.-]/g, '_')}`
: `${profile}-${scope}`
const oracleConfigDirectory = resolve(root, '.cache/freecad/oracle-config')
const oracleUserConfig = resolve(oracleConfigDirectory, `${configIdentity}-user.cfg`)
const oracleSystemConfig = resolve(oracleConfigDirectory, `${configIdentity}-system.cfg`)
if (profile === 'desktop') {
const emptyConfig = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n<FCParameters><FCParamGroup Name="Root"/></FCParameters>\n'
mkdirSync(oracleConfigDirectory, { recursive: true })
writeFileSync(oracleUserConfig, emptyConfig)
writeFileSync(oracleSystemConfig, emptyConfig)
}
const localOracle = resolve(root, profile === 'desktop' ? '.cache/freecad/install-desktop/bin/FreeCAD' : '.cache/freecad/install-native/bin/FreeCADCmd')
const fallbackOracle = profile === 'desktop' ? ['FreeCAD', 'freecad'] : ['FreeCADCmd', 'freecadcmd']
const referenceCommand = process.env.FREECAD_REFERENCE_CMD || process.env.FREECAD_CMD
@@ -22,7 +50,7 @@ if (!command) throw new Error('FreeCADCmd 1.1.1 is unavailable. Set FREECAD_CMD
// stdin loop and prevents a deterministic probe exit.
const sysroot = resolve(root, '.cache/freecad/sysroot')
const commandArgs = profile === 'desktop'
? ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-reference-probe.py')]
? ['--user-cfg', oracleUserConfig, '--system-cfg', oracleSystemConfig, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-reference-probe.py')]
: [resolve(root, 'scripts/freecad-reference-probe.py')]
const runtimeEnv = profile === 'desktop'
? {
@@ -31,6 +59,7 @@ const runtimeEnv = profile === 'desktop'
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'),
MPLBACKEND: 'Agg',
FREECAD_ORACLE_ISOLATED_CONFIG: '1',
}
: process.env
const desktopNeedsVirtualDisplay = profile === 'desktop' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY
@@ -39,62 +68,82 @@ const launchArgs = desktopNeedsVirtualDisplay ? ['-a', command, ...commandArgs]
const execution = spawnSync(launchCommand, launchArgs, {
cwd: root,
encoding: 'utf8',
timeout: 120000,
timeout: timeoutMs,
maxBuffer: 20 * 1024 * 1024,
env: runtimeEnv,
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const resultLine = output.split(/\r?\n/).find((line) => line.startsWith('FREECAD_REFERENCE_RESULT='))
if (execution.error || execution.status !== 0 || !resultLine) {
throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
throw new Error(`FreeCAD reference probe failed with status ${execution.status}: ${[execution.error?.message, output.trim()].filter(Boolean).join('\n')}`)
}
const result = JSON.parse(resultLine.slice('FREECAD_REFERENCE_RESULT='.length))
if (result.freecadVersion !== '1.1.1') throw new Error(`Expected FreeCAD 1.1.1, received ${result.freecadVersion}`)
if (result.determinism?.schemaVersion !== 1 || result.determinism.normalizedProcessFields?.join(',') !== 'pointer-address,uuid,freecad-document-cache-run') throw new Error('Reference probe lacks deterministic process-field normalization.')
if (result.probeScope !== scope) throw new Error(`Reference probe returned scope ${result.probeScope || '<missing>'}; expected ${scope}.`)
if (profile === 'desktop' && (result.oracleSetup?.isolatedConfig !== true || result.oracleSetup.bimFirstTimeWelcome !== 'suppressed-before-workbench-activation')) throw new Error('Desktop reference probe lacks the isolated modal-dialog boundary.')
if (profile === 'desktop' && !registrationOnly && (result.guiCommands?.probeMode !== 'state-matrix' || result.guiCommands.stateProbe !== 'native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard' || result.guiCommands.stateCases?.join(',') !== 'noDocument,documentNoSelection,selectedPartBox' || result.guiCommands.stateCommandCount !== result.guiCommands.commands?.length)) throw new Error('Desktop reference probe lacks the locked GUI state matrix.')
if (registrationOnly && (result.guiCommands?.probeMode !== 'registration-only' || result.guiCommands.stateCommandCount !== 0 || result.guiCommands.workbenches?.length !== 1 || result.guiCommands.workbenches[0].name !== workbenchFilter)) throw new Error('Desktop reference probe returned an invalid single-workbench registration report.')
if (profile === 'desktop' && (!Number.isInteger(result.guiCommands?.commandUniverseCount) || result.guiCommands.commandUniverseCount < result.guiCommands.stateCommandCount || !/^[0-9a-f]{64}$/.test(result.guiCommands.commandUniverseSha256 || ''))) throw new Error('Desktop reference probe lacks a stable GUI command universe.')
if (requestedShard && (result.guiCommands?.shard?.index !== requestedShard.index || result.guiCommands.shard.count !== requestedShard.count)) throw new Error('Desktop reference probe returned the wrong GUI command shard.')
if (!requestedShard && result.guiCommands?.shard != null) throw new Error('Unsharded desktop reference probe returned shard metadata.')
const moduleStatuses = new Set(['not-built', 'compiled-importable', 'compiled-import-failure', 'gui-only-unprobeable'])
if (!Array.isArray(result.modules) || result.modules.length !== 34 || result.moduleCount !== 34) throw new Error('Reference probe must report the 34 configured FreeCAD modules.')
for (const module of result.modules) {
if (typeof module.name !== 'string' || typeof module.compiled !== 'boolean' || typeof module.importable !== 'boolean' || typeof module.guiRequired !== 'boolean' || typeof module.guiAvailable !== 'boolean' || !moduleStatuses.has(module.runtimeStatus)) {
throw new Error(`Reference probe returned an invalid module status for ${module.name || '<unknown>'}.`)
if (scope === 'full') {
if (!Array.isArray(result.modules) || result.modules.length !== 34 || result.moduleCount !== 34) throw new Error('Full reference probe must report the 34 configured FreeCAD modules.')
for (const module of result.modules) {
if (typeof module.name !== 'string' || typeof module.compiled !== 'boolean' || typeof module.importable !== 'boolean' || typeof module.guiRequired !== 'boolean' || typeof module.guiAvailable !== 'boolean' || !moduleStatuses.has(module.runtimeStatus)) {
throw new Error(`Reference probe returned an invalid module status for ${module.name || '<unknown>'}.`)
}
}
} else if (result.moduleCount !== 0 || result.modules?.length !== 0) {
throw new Error('GUI command probe must not include the module oracle scope.')
}
const runtimeObjects = result.runtimeObjects
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types)) {
throw new Error('Reference probe must include the complete Document.supportedTypes runtime inventory.')
}
if (runtimeObjects.candidateCount !== runtimeObjects.types.length || runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) {
throw new Error('Reference probe returned inconsistent runtime object counts.')
}
const runtimeTypeIds = new Set()
for (const candidate of runtimeObjects.types) {
if (typeof candidate.typeId !== 'string' || !candidate.typeId || runtimeTypeIds.has(candidate.typeId) || !['available', 'unavailable'].includes(candidate.probeStatus)) {
throw new Error(`Reference probe returned an invalid runtime object candidate: ${candidate.typeId || '<unknown>'}.`)
if (scope === 'full') {
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types)) {
throw new Error('Reference probe must include the complete Document.supportedTypes runtime inventory.')
}
runtimeTypeIds.add(candidate.typeId)
if (candidate.available) {
if (candidate.probeStatus !== 'available' || typeof candidate.runtimeTypeId !== 'string' || !candidate.runtimeTypeId || !Array.isArray(candidate.properties)) {
throw new Error(`Reference probe returned incomplete metadata for ${candidate.typeId}.`)
if (runtimeObjects.candidateCount !== runtimeObjects.types.length || runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) {
throw new Error('Reference probe returned inconsistent runtime object counts.')
}
const runtimeTypeIds = new Set()
for (const candidate of runtimeObjects.types) {
if (typeof candidate.typeId !== 'string' || !candidate.typeId || runtimeTypeIds.has(candidate.typeId) || !['available', 'unavailable'].includes(candidate.probeStatus)) {
throw new Error(`Reference probe returned an invalid runtime object candidate: ${candidate.typeId || '<unknown>'}.`)
}
for (const property of candidate.properties) {
if (typeof property.name !== 'string' || typeof property.typeId !== 'string' || !Array.isArray(property.status) || !Object.hasOwn(property, 'default')) {
throw new Error(`Reference probe returned invalid property metadata for ${candidate.typeId}.`)
runtimeTypeIds.add(candidate.typeId)
if (candidate.available) {
if (candidate.probeStatus !== 'available' || typeof candidate.runtimeTypeId !== 'string' || !candidate.runtimeTypeId || !Array.isArray(candidate.properties)) {
throw new Error(`Reference probe returned incomplete metadata for ${candidate.typeId}.`)
}
for (const property of candidate.properties) {
if (typeof property.name !== 'string' || typeof property.typeId !== 'string' || !Array.isArray(property.status) || !Object.hasOwn(property, 'default')) {
throw new Error(`Reference probe returned invalid property metadata for ${candidate.typeId}.`)
}
}
} else if (candidate.probeStatus !== 'unavailable' || typeof candidate.error !== 'string' || !candidate.error) {
throw new Error(`Reference probe did not explain why ${candidate.typeId} is unavailable.`)
}
} else if (candidate.probeStatus !== 'unavailable' || typeof candidate.error !== 'string' || !candidate.error) {
throw new Error(`Reference probe did not explain why ${candidate.typeId} is unavailable.`)
}
}
await writeFile(resolve(root, `.cache/freecad/reference-${profile}.json`), `${JSON.stringify(result, null, 2)}\n`)
const reportName = requestedShard
? `reference-desktop-gui-commands-${requestedShard.index}-of-${requestedShard.count}.json`
: workbenchFilter ? `reference-desktop-gui-commands-${workbenchFilter.replace(/[^A-Za-z0-9_.-]/g, '_')}.json`
: scope === 'gui-commands' ? 'reference-desktop-gui-commands.json' : `reference-${profile}.json`
await writeFile(resolve(root, '.cache/freecad', reportName), `${JSON.stringify(result, null, 2)}\n`)
console.log(JSON.stringify({
command: launchCommand === command ? command : `${launchCommand} ${command}`,
profile,
scope,
shard: requestedShard,
freecadVersion: result.freecadVersion,
moduleStatusSummary: result.moduleStatusSummary,
guiCommandCount: result.guiCommands?.commands?.length ?? 0,
runtimeObjectSummary: {
candidateCount: runtimeObjects.candidateCount,
availableCount: runtimeObjects.availableCount,
unavailableCount: runtimeObjects.unavailableCount,
propertyCount: runtimeObjects.types.reduce((total, candidate) => total + (candidate.properties?.length ?? 0), 0),
candidateCount: runtimeObjects?.candidateCount ?? 0,
availableCount: runtimeObjects?.availableCount ?? 0,
unavailableCount: runtimeObjects?.unavailableCount ?? 0,
propertyCount: runtimeObjects?.types?.reduce((total, candidate) => total + (candidate.properties?.length ?? 0), 0) ?? 0,
},
report: `.cache/freecad/reference-${profile}.json`,
report: `.cache/freecad/${reportName}`,
}, null, 2))

View File

@@ -0,0 +1,33 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const script = resolve(root, 'scripts/freecad-tsn-stage-correlation-oracle.py')
const outputPath = resolve(root, 'config/freecad-tsn-stage-correlation-oracle.json')
if (!existsSync(executable)) throw new Error(`FreeCAD TSN stage-correlation executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), script], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 40 * 1024 * 1024,
env: {
...process.env,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'),
MPLBACKEND: 'Agg',
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_TSN_STAGE_CORRELATION_RESULT='
const markerIndex = output.indexOf(marker)
if (execution.error || execution.status !== 0 || markerIndex < 0) throw new Error(`FreeCAD TSN stage-correlation oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const resultLine = output.slice(markerIndex + marker.length).split(/\r?\n/, 1)[0]
const report = JSON.parse(resultLine)
if (report.status !== 'pass') throw new Error(`FreeCAD TSN stage-correlation oracle reported ${report.status}: ${report.error || 'unknown failure'}`)
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: 'freecad-tsn-stage-correlation-generated', ...report.summary, exactCorrelationReady: report.exactCorrelationReady }, null, 2))

View File

@@ -88,6 +88,7 @@ const lanes = {
'probe:freecad-partdesign-revolution-groove',
'probe:freecad-part-builders',
'probe:freecad-composite-history-elementmap',
'probe:freecad-tsn-stage-correlation',
'test:freecad-fcstd-native',
'generate:freecad-parameter-mutations',
'check:freecad-oracle-coverage',
@@ -98,6 +99,7 @@ const lanes = {
'check:freecad-sketcher-constraints',
'check:freecad-fcstd-roundtrip',
'check:freecad-composite-history-elementmap',
'check:freecad-tsn-stage-correlation',
'check:freecad-exact-history-elementmap-gate',
'check:freecad-native-naming-evidence',
],

View File

@@ -7,7 +7,7 @@ type MachineReport = {
stages: string[]
openCamLib?: { backend: string; sourceRevision: string; artifactSha256: string; triangleCount: number; inputPoints: number; outputPoints: number; sampling: number }
gcode?: { bytes: number; hasM428: boolean; hasM429: boolean; hasG93: boolean; hasG94: boolean; hasB: boolean; hasC359: boolean; hasC361: boolean }
machine?: { backend: string; status: string; dryRunStatus?: string; parserAuthority?: string; lines?: number; blocks?: number; trajectorySegments?: number; message?: string }
machine?: { backend: string; status: string; dryRunStatus?: string; parserAuthority?: string; machineCase?: string; dryRunMachineCase?: string; lines?: number; blocks?: number; trajectorySegments?: number; message?: string }
error?: string
}
@@ -50,9 +50,10 @@ const run = async (): Promise<MachineReport> => {
name: 'cad-ocl-camotics-xyzbc.ngc',
timeoutMs: 180_000,
}))
const trace = result.linuxcnc?.trace as { lines?: number; blocks?: number; trajectorySegments?: number; parserAuthority?: string } | undefined
report.machine = { backend: result.linuxcnc?.backend || '', status: result.linuxcnc?.status || '', dryRunStatus: dryRun.linuxcnc?.status || '', parserAuthority: trace?.parserAuthority, lines: trace?.lines, blocks: trace?.blocks, trajectorySegments: trace?.trajectorySegments, message: result.linuxcnc?.message }
report.status = report.openCamLib.backend === 'upstream-opencamlib-wasm' && report.openCamLib.triangleCount >= 2 && report.openCamLib.outputPoints >= report.openCamLib.inputPoints && Object.values(report.gcode).every(Boolean) && report.machine.backend === 'linuxcnc-wasm' && report.machine.status === 'accepted' && report.machine.dryRunStatus === 'dry-run' && report.machine.parserAuthority === 'linuxcnc-wasm' && Number(report.machine.lines) >= 10 && Number(report.machine.lines) < 1000 && Number(report.machine.blocks) >= 2 && Number(report.machine.blocks) < 1000 && Number(report.machine.trajectorySegments) >= 2 && Number(report.machine.trajectorySegments) < 1000 ? 'pass' : 'failed'
const trace = result.linuxcnc?.trace
const dryRunTrace = dryRun.linuxcnc?.trace
report.machine = { backend: result.linuxcnc?.backend || '', status: result.linuxcnc?.status || '', dryRunStatus: dryRun.linuxcnc?.status || '', parserAuthority: trace?.parserAuthority, machineCase: trace?.machineCase, dryRunMachineCase: dryRunTrace?.machineCase, lines: trace?.lines, blocks: trace?.blocks, trajectorySegments: trace?.trajectorySegments, message: result.linuxcnc?.message }
report.status = report.openCamLib.backend === 'upstream-opencamlib-wasm' && report.openCamLib.triangleCount >= 2 && report.openCamLib.outputPoints >= report.openCamLib.inputPoints && Object.values(report.gcode).every(Boolean) && report.machine.backend === 'linuxcnc-wasm' && report.machine.status === 'accepted' && report.machine.dryRunStatus === 'dry-run' && report.machine.parserAuthority === 'linuxcnc-wasm' && report.machine.machineCase === 'axis-vismach-5axis-table-rotary-tilting-xyzbc-trt' && report.machine.dryRunMachineCase === report.machine.machineCase && Number(report.machine.lines) >= 10 && Number(report.machine.lines) < 1000 && Number(report.machine.blocks) >= 2 && Number(report.machine.blocks) < 1000 && Number(report.machine.trajectorySegments) >= 2 && Number(report.machine.trajectorySegments) < 1000 ? 'pass' : 'failed'
} catch (error) {
report.error = error instanceof Error ? error.stack || error.message : String(error)
}

View File

@@ -39,7 +39,13 @@ export type LinuxcncWasmSubmission = {
status: 'accepted' | 'rejected' | 'dry-run' | 'unavailable'
programSha256?: string
message?: string
trace?: unknown
trace?: {
lines?: number
blocks?: number
trajectorySegments?: number
parserAuthority?: string
machineCase?: string
}
}
export type LinuxcncWasmAdapter = {
@@ -215,12 +221,13 @@ export const createLinuxcncIframeAdapter = (options: LinuxcncIframeAdapterOption
if (event.source !== frame.contentWindow || data?.type !== 'bitbybit-linuxcnc-result' || data.requestId !== requestId) return
window.clearTimeout(timer)
window.removeEventListener('message', onMessage)
const machineCase = frame.contentDocument?.querySelector<HTMLElement>('[data-case].active')?.dataset.case
resolve({
backend: 'linuxcnc-wasm',
status: data.status,
programSha256: data.programSha256,
message: data.message,
trace: { lines: data.lines, blocks: data.blocks, trajectorySegments: data.trajectorySegments, parserAuthority: data.parserAuthority },
trace: { lines: data.lines, blocks: data.blocks, trajectorySegments: data.trajectorySegments, parserAuthority: data.parserAuthority, machineCase },
})
}
window.addEventListener('message', onMessage)

View File

@@ -985,7 +985,7 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId
const quaternion = [axis[0] * sine, axis[1] * sine, axis[2] * sine, Math.cos(angle / 2)]
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><PropertyPlacement Px="${px}" Py="${py}" Pz="${pz}" Q0="${quaternion[0]}" Q1="${quaternion[1]}" Q2="${quaternion[2]}" Q3="${quaternion[3]}" A="${angle}" Ox="${axis[0]}" Oy="${axis[1]}" Oz="${axis[2]}"/></Property>`
}
if (property.type === 'App::PropertyLink') {
if (property.type === 'App::PropertyLink' || property.type === 'App::PropertyLinkHidden') {
if (property.value !== null && typeof property.value !== 'string') throw new TypeError(`FCStd Link property ${property.name} requires an object name.`)
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Link value="${xmlEscape(property.value ?? '')}"/></Property>`
}
@@ -1062,7 +1062,7 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId
if (typeof property.value !== 'boolean') throw new TypeError(`FCStd Bool property ${property.name} requires a boolean.`)
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Bool value="${property.value ? 'true' : 'false'}"/></Property>`
}
if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyFloat') {
if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision') {
if (typeof property.value !== 'number' || !Number.isFinite(property.value)) throw new TypeError(`FCStd numeric property ${property.name} requires a finite number.`)
return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Float value="${property.value}"/></Property>`
}
@@ -1893,7 +1893,7 @@ const sketchFromPropertySummaries = (objectId: string, summaries: FcstdPropertyS
}
export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdDecodedPropertyValue => {
const numericTypes = new Set(['App::PropertyLength', 'App::PropertyDistance', 'App::PropertyAngle', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyPercent'])
const numericTypes = new Set(['App::PropertyLength', 'App::PropertyDistance', 'App::PropertyAngle', 'App::PropertyQuantityConstraint', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyPrecision', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyPercent'])
if (numericTypes.has(property.typeId)) {
const value = Number(property.value)
return Number.isFinite(value) ? { value, decoded: true } : { value: property.value, decoded: false, error: `Property ${property.name} is not a finite number.` }
@@ -1915,7 +1915,7 @@ export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdD
value: { schemaVersion: 1, objectId: property.value, subElements: [...property.subElements] },
decoded: true,
}
if (property.typeId === 'App::PropertyLink') return { value: property.value || null, decoded: true }
if (property.typeId === 'App::PropertyLink' || property.typeId === 'App::PropertyLinkHidden') return { value: property.value || null, decoded: true }
if (property.typeId === 'App::PropertyLinkList' && 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 === 'Part::PropertyPartShape' && property.shapeResource) return { value: { ...property.shapeResource, format: 'brep' }, decoded: true }

View File

@@ -14,7 +14,7 @@ export type { ProjectMigrationTransaction, ProjectSchemaMigration } from './proj
export type { CreateEllipsoidInput, CreateHelixInput, CreatePrismInput, CreateWedgeInput, ExtrudeInput } from './types'
export type { DraftInput, ThicknessInput } from './types'
export type { LinkSubValue } from './types'
export { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, hasFreecadPropertyStatus, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyDynamicMutationLocked, isFreecadPropertyHidden, isFreecadPropertyNoPersist, isFreecadPropertyOutput, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, isFreecadPropertyTransient, normalizeFreecadPropertyStatusMask } from './propertyStatus'
export { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, hasFreecadPropertyStatus, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyDynamicMutationLocked, isFreecadPropertyHidden, isFreecadPropertyNoPersist, isFreecadPropertyOutput, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, isFreecadPropertyTransient, normalizeFreecadPropertyStatusMask, withFreecadPropertyStatus } from './propertyStatus'
export type { FreecadPropertyRecomputeEffect, FreecadPropertyStatus, FreecadPropertyStatusName } from './propertyStatus'
export { assessResourceQuota, planResourceSweep } from './resourcePolicy'
export type { ResourceQuotaAssessment, ResourceSweepPlan, ResourceSweepRecord } from './resourcePolicy'

View File

@@ -1,6 +1,7 @@
import { workbenchDefinitions, type WorkbenchId } from '../freecadManifest'
import type {
BitBybitWebCadFacade,
AddDynamicPropertyInput,
CommandState,
DocumentSnapshot,
Diagnostic,
@@ -20,6 +21,8 @@ import type {
PlacementValue,
ReorderBodyFeatureInput,
RemoveObjectInput,
RemoveDynamicPropertyInput,
RenameDynamicPropertyInput,
SetPropertyInput,
SetExpressionInput,
RecomputeResult,
@@ -47,7 +50,7 @@ import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTo
import { createCamJob } from './cam'
import { PUMP_HOUSING_DEMO_TEMPLATE, type DocumentTemplate } from './documentTemplates'
import { assertPartDesignParameterSet, parameterValuesForObject, validatePartDesignParameterSet } from './partDesignParameters'
import { freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyReadOnly } from './propertyStatus'
import { freecadPropertyRecomputeEffect, hasFreecadPropertyStatus, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyDynamicMutationLocked, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyReadOnly, withFreecadPropertyStatus } from './propertyStatus'
const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('ellipsoid') ? 'Part::Ellipsoid' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('torus') ? 'Part::Torus' : item.id.startsWith('helix') ? 'Part::Helix' : item.id.startsWith('prism') ? 'Part::Prism' : item.id.startsWith('wedge') ? 'Part::Wedge' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('groove') ? 'PartDesign::Groove' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('mirrored') ? 'PartDesign::Mirrored' : item.id.startsWith('multi-transform') ? 'PartDesign::MultiTransform' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'
@@ -536,7 +539,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) throw new Error(`${property.label} is read-only.`)
if (property.type === 'App::PropertyBool' && typeof value !== 'boolean') throw new TypeError(`${property.label} requires a boolean value.`)
if ((property.type === 'App::PropertyString' || property.type === 'App::PropertyEnumeration' || property.type === 'App::PropertyColor') && typeof value !== 'string') throw new TypeError(`${property.label} requires a string value.`)
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`)
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`)
if ((property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && !Number.isSafeInteger(value)) throw new TypeError(`${property.label} requires an integer value.`)
if (property.name === 'Occurrences' && ((value as number) < 2 || (value as number) > 100)) throw new RangeError('Occurrences must be between 2 and 100.')
if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyQuantityConstraint') && (value as number) < 0) throw new RangeError(`${property.label} cannot be negative.`)
@@ -576,7 +579,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
if (!/\.Map\.txt$/i.test(value.elementMapResource)) throw new TypeError(`${property.label} requires an ElementMap2 .Map.txt resource.`)
}
}
if (property.type === 'App::PropertyLink') {
if (property.type === 'App::PropertyLink' || property.type === 'App::PropertyLinkHidden') {
if (property.name === 'Support' && value && typeof value === 'object' && !Array.isArray(value)) {
validateAttachmentSupport(value)
const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
@@ -740,13 +743,14 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
const listeners = new Set<FacadeListener>()
const undoStack: FacadeState[] = []
const redoStack: FacadeState[] = []
const partialMutationWarnings = new Set<string>()
let requestSequence = 0
const emit = (event: FacadeEvent) => listeners.forEach((listener) => listener(event))
const emitState = () => emit({ type: 'state.changed', state: getState() })
const cloneSelection = (selection: SubshapeSelection | null | undefined): SubshapeSelection | null | undefined => selection ? { objectId: selection.objectId, ref: { ...selection.ref, candidates: selection.ref.candidates ? [...selection.ref.candidates] : undefined } } : selection
const getState = () => ({ ...state, selectedObjectIds: [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], selectedSubshape: cloneSelection(state.selectedSubshape), preselectedSubshape: cloneSelection(state.preselectedSubshape), diagnostics: state.diagnostics.map(cloneDiagnostic), document: cloneDocumentSnapshot(state.document), task: state.task ? { ...state.task, draft: Object.fromEntries(Object.entries(state.task.draft).map(([key, value]) => [key, key === 'transformations' ? clonePropertyValue(value as MultiTransformValue) : Array.isArray(value) ? [...value] : value])) } : null })
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty) autosave.schedule(next.document); emitState() }
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty && !next.document.partial) autosave.schedule(next.document); emitState() }
const notify = (message: string) => { state = { ...state, lastNotice: message }; emit({ type: 'notice', message }); emitState() }
const setActive = (id: WorkbenchId) => { state = { ...state, activeWorkbench: id }; emitState(); notify(`${id} workbench loaded`) }
const selectObjects = (objectIds: string[]) => {
@@ -1067,6 +1071,12 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
validatePropertyValue(state.document, sourceProperty, value)
if (Object.is(sourceProperty.value, value)) return
const beforeVersion = state.document.version
const transactionId = `txn-${++requestSequence}`
const partialDocument = state.document.partial === true
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
emit({ type: 'property.before-change', transactionId, documentId: state.document.id, documentVersion: beforeVersion, objectId, propertyName, partialDocument, partialTrigger })
const document = cloneDocumentSnapshot(state.document)
const object = document.objects[objectIndex]
object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(value), expression: undefined, expressionError: undefined }
@@ -1080,8 +1090,103 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
document.version += 1
document.dirty = isFreecadPropertyDocumentModifiedSuppressed(sourceProperty.nativeStatus) ? state.document.dirty : true
commit({ ...state, document })
emit({ type: 'property.changed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
emit({ type: 'transaction.committed', transactionId, operation: 'property.set', documentId: document.id, beforeVersion, afterVersion: document.version })
if (partialDocument && !partialTrigger && !partialMutationWarnings.has(document.id)) {
partialMutationWarnings.add(document.id)
const diagnostic: Diagnostic = {
id: `diag-${++requestSequence}`,
source: 'command',
severity: 'warning',
code: 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED',
message: `Changes to partial loaded document will not be saved: ${objectId}.${propertyName}`,
objectId,
documentId: document.id,
documentVersion: document.version,
}
state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: transactionId, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench } })
}
notify(`${sourceProperty.label} updated`)
}
const addDynamicProperty = ({ objectId, property: inputProperty }: AddDynamicPropertyInput): ObjectPropertySnapshot => {
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(inputProperty.name)) throw new RangeError(`Invalid property name '${inputProperty.name}'.`)
if (state.document.objects[objectIndex].properties.some((property) => property.name === inputProperty.name)) throw new RangeError(`Property ${objectId}.${inputProperty.name} already exists.`)
const property: ObjectPropertySnapshot = {
...inputProperty,
value: clonePropertyValue(inputProperty.value),
options: inputProperty.options ? [...inputProperty.options] : undefined,
nativeStatus: withFreecadPropertyStatus(inputProperty.nativeStatus, 'PropDynamic'),
}
validatePropertyValue(state.document, { ...property, readOnly: false, nativeStatus: undefined }, property.value)
const beforeVersion = state.document.version
const transactionId = `txn-${++requestSequence}`
const document = cloneDocumentSnapshot(state.document)
document.objects[objectIndex].properties.push(property)
document.dependencies = collectDependencyEdges(document)
document.version += 1
document.dirty = true
commit({ ...state, document })
const partialDocument = document.partial === true
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
emit({ type: 'property.added', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: property.name, partialDocument, partialTrigger })
emit({ type: 'transaction.committed', transactionId, operation: 'property.add', documentId: document.id, beforeVersion, afterVersion: document.version })
notify(`${property.label} property added`)
return { ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined }
}
const removeDynamicProperty = ({ objectId, propertyName }: RemoveDynamicPropertyInput): boolean => {
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
const propertyIndex = state.document.objects[objectIndex].properties.findIndex((property) => property.name === propertyName)
if (propertyIndex < 0) return false
const property = state.document.objects[objectIndex].properties[propertyIndex]
if (!hasFreecadPropertyStatus(property.nativeStatus, 'PropDynamic') || isFreecadPropertyDynamicMutationLocked(property.nativeStatus)) return false
const beforeVersion = state.document.version
const transactionId = `txn-${++requestSequence}`
const document = cloneDocumentSnapshot(state.document)
document.objects[objectIndex].properties.splice(propertyIndex, 1)
document.dependencies = collectDependencyEdges(document)
document.version += 1
document.dirty = true
commit({ ...state, document })
const partialDocument = document.partial === true
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
emit({ type: 'property.removed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
emit({ type: 'transaction.committed', transactionId, operation: 'property.remove', documentId: document.id, beforeVersion, afterVersion: document.version })
notify(`${property.label} property removed`)
return true
}
const renameDynamicProperty = ({ objectId, propertyName, newPropertyName }: RenameDynamicPropertyInput): boolean => {
const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
const sourceObject = state.document.objects[objectIndex]
const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName)
if (propertyIndex < 0 || !hasFreecadPropertyStatus(sourceObject.properties[propertyIndex].nativeStatus, 'PropDynamic')) throw new Error(`Property container has no dynamic property '${propertyName}'.`)
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(newPropertyName)) throw new RangeError(`Invalid property name '${newPropertyName}'.`)
if (sourceObject.properties.some((property) => property.name === newPropertyName)) throw new RangeError(`Property ${objectId}.${newPropertyName} already exists.`)
const sourceProperty = sourceObject.properties[propertyIndex]
if (isFreecadPropertyDynamicMutationLocked(sourceProperty.nativeStatus)) throw new Error(`Property ${propertyName} is locked`)
if (propertyName === newPropertyName) return true
const beforeVersion = state.document.version
const transactionId = `txn-${++requestSequence}`
const document = cloneDocumentSnapshot(state.document)
document.objects[objectIndex].properties[propertyIndex].name = newPropertyName
document.dependencies = collectDependencyEdges(document)
document.version += 1
document.dirty = true
commit({ ...state, document })
const partialDocument = document.partial === true
const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
emit({ type: 'property.renamed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: newPropertyName, previousPropertyName: propertyName, partialDocument, partialTrigger })
emit({ type: 'transaction.committed', transactionId, operation: 'property.rename', documentId: document.id, beforeVersion, afterVersion: document.version })
notify(`${sourceProperty.label} property renamed`)
return true
}
const reorderBodyFeature = ({ bodyId, objectId, beforeObjectId }: ReorderBodyFeatureInput) => {
const document = cloneDocumentSnapshot(state.document)
const body = document.tree.find((item) => item.id === bodyId && item.type === 'body')
@@ -1184,6 +1289,10 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
notify(`${sourceProperty.label} expression updated`)
}
const recomputeDocument = (): RecomputeResult => {
if (state.document.partial) {
notify(`Partial document ${state.document.label} must be fully loaded before recomputation`)
return { affected: [], order: [], levels: [], cycles: [], generation: state.document.recompute?.generation ?? 0, status: 'completed', errors: [] }
}
const document = cloneDocumentSnapshot(state.document)
const graph = new DependencyGraph(document.dependencies ?? [], document.objects.map((object) => object.id))
const recompute = document.recompute ?? createRecomputeSnapshot(document.objects.map((object) => object.id))
@@ -1221,12 +1330,23 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
state = { ...state, document, diagnostics }
const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })
if (document.dirty) autosave.schedule(document)
if (document.dirty && !document.partial) autosave.schedule(document)
emitState()
return { ...plan, generation, status: nextRecompute.status, errors }
}
const recomputeDocumentAsync = async (options: RecomputeExecutionOptions = {}) => {
const source = cloneDocumentSnapshot(state.document)
if (source.partial) {
notify(`Partial document ${source.label} must be fully loaded before recomputation`)
const recompute = source.recompute ?? createRecomputeSnapshot(source.objects.map((object) => object.id))
return {
generation: recompute.generation,
documentVersion: source.version,
status: 'completed' as const,
affected: [], order: [], levels: [], completed: [], suppressed: [], failed: [], skipped: [],
dirtyObjects: [...recompute.dirtyObjects], objectStates: { ...recompute.objectStates }, objectUpdates: [], errors: [],
}
}
const result = await recomputeCoordinator.run(source, options)
if ((result.status !== 'completed' && result.status !== 'failed') || state.document.id !== source.id || state.document.version !== source.version) return result
@@ -1293,7 +1413,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
state = { ...state, document, diagnostics }
const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${result.generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })
if (document.dirty) autosave.schedule(document)
if (document.dirty && !document.partial) autosave.schedule(document)
emitState()
return result
}
@@ -1492,7 +1612,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
const facade: BitBybitWebCadFacade = {
runtime,
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, reorderBodyFeature, removeObject, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, addDynamicProperty, removeDynamicProperty, renameDynamicProperty, reorderBodyFeature, removeObject, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId), state.selectedSubshape), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
selection: { getObjectId: () => state.selectedObjectId, getObjectIds: () => [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], getSubshape: () => cloneSelection(state.selectedSubshape) ?? null, getPreselection: () => cloneSelection(state.preselectedSubshape) ?? null, select, selectObjects, selectSubshape, preselectSubshape, clear: () => select('') },

View File

@@ -82,6 +82,12 @@ export const isFreecadPropertyTransient = (mask: number | undefined): boolean =>
export const isFreecadPropertyPersistenceSuppressed = (mask: number | undefined): boolean => hasFreecadPropertyStatus(mask, 'Transient', 'PropNoPersist', 'PropTransient')
export const isFreecadPropertyDocumentModifiedSuppressed = (mask: number | undefined): boolean => hasFreecadPropertyStatus(mask, 'NoModify')
export const isFreecadPropertyDynamicMutationLocked = (mask: number | undefined): boolean => hasFreecadPropertyStatus(mask, 'LockDynamic')
export const isFreecadPropertyPartialDocumentTrigger = (mask: number | undefined): boolean => hasFreecadPropertyStatus(mask, 'PartialTrigger')
export const withFreecadPropertyStatus = (mask: number | undefined, ...names: FreecadPropertyStatusName[]): number => {
const normalized = mask === undefined ? 0 : normalizeFreecadPropertyStatusMask(mask)
return (normalized | encodeFreecadPropertyStatus(names)) >>> 0
}
export type FreecadPropertyRecomputeEffect = 'none' | 'dependents' | 'owner'

View File

@@ -89,12 +89,23 @@ export type ShapeResourceValue = {
export type PropertyValue = string | number | boolean | string[] | number[] | VectorValue | PlacementValue | AttachmentSupportValue | MultiTransformValue | PathPropertyValue | ShapeResourceValue | TopoRefValue | LinkSubValue | LinkSubListValue | null
export type NativeEditablePropertyType =
| 'App::PropertyString' | 'App::PropertyLength' | 'App::PropertyDistance' | 'App::PropertyAngle'
| 'App::PropertyQuantityConstraint' | 'App::PropertyBool' | 'App::PropertyEnumeration'
| 'App::PropertyLink' | 'App::PropertyLinkHidden' | 'App::PropertyLinkSub' | 'App::PropertyLinkSubList'
| 'App::PropertyLinkList' | 'App::PropertyStringList' | 'App::PropertyFloatList'
| 'App::PropertyIntegerList' | 'App::PropertyVector' | 'App::PropertyPlacement'
| 'App::PropertyMultiTransform' | 'Path::PropertyPath' | 'Part::PropertyPartShape'
| 'App::PropertyColor' | 'App::PropertyPercent' | 'App::PropertyFloat'
| 'App::PropertyFloatConstraint' | 'App::PropertyPrecision' | 'App::PropertyInteger'
| 'App::PropertyIntegerConstraint'
export type ObjectPropertySnapshot = {
name: string
label: string
group: string
scope: 'data' | 'view'
type: 'App::PropertyString' | 'App::PropertyLength' | 'App::PropertyDistance' | 'App::PropertyAngle' | 'App::PropertyQuantityConstraint' | 'App::PropertyBool' | 'App::PropertyEnumeration' | 'App::PropertyLink' | 'App::PropertyLinkSub' | 'App::PropertyLinkSubList' | 'App::PropertyLinkList' | 'App::PropertyStringList' | 'App::PropertyFloatList' | 'App::PropertyIntegerList' | 'App::PropertyVector' | 'App::PropertyPlacement' | 'App::PropertyMultiTransform' | 'Path::PropertyPath' | 'Part::PropertyPartShape' | 'App::PropertyColor' | 'App::PropertyPercent' | 'App::PropertyFloat' | 'App::PropertyInteger' | 'App::PropertyIntegerConstraint'
type: NativeEditablePropertyType
value: PropertyValue
unit?: string
/** Raw App::Property::Status bitmask from the locked FreeCAD runtime. */
@@ -123,6 +134,8 @@ export type DocumentSnapshot = {
version: number
dirty: boolean
readOnly: boolean
/** Native PartialDoc state. Partial documents can be inspected but cannot be saved or recomputed. */
partial?: boolean
units: string
tree: ModelTreeItem[]
objects: DocumentObjectSnapshot[]
@@ -148,6 +161,20 @@ export type SetPropertyInput = {
value: PropertyValue
}
export type AddDynamicPropertyInput = {
objectId: string
property: ObjectPropertySnapshot
}
export type RemoveDynamicPropertyInput = {
objectId: string
propertyName: string
}
export type RenameDynamicPropertyInput = RemoveDynamicPropertyInput & {
newPropertyName: string
}
export type ReorderBodyFeatureInput = {
bodyId: string
objectId: string
@@ -816,12 +843,25 @@ export type FacadeState = {
diagnostics: Diagnostic[]
}
export type FacadePropertyMutation = {
transactionId: string
documentId: string
documentVersion: number
objectId: string
propertyName: string
previousPropertyName?: string
partialDocument: boolean
partialTrigger: boolean
}
export type FacadeEvent =
| { type: 'state.changed'; state: FacadeState }
| { type: 'notice'; message: string }
| { type: 'command.started' | 'command.completed' | 'command.failed'; commandId: string; context: FacadeRequestContext; message?: string }
| { type: 'diagnostic.focused'; diagnosticId: string; objectId: string }
| { type: 'diagnostic.added'; diagnostic: Diagnostic; context: FacadeRequestContext }
| ({ type: 'property.before-change' | 'property.changed' | 'property.added' | 'property.removed' | 'property.renamed' } & FacadePropertyMutation)
| { type: 'transaction.committed'; transactionId: string; operation: 'property.set' | 'property.add' | 'property.remove' | 'property.rename'; documentId: string; beforeVersion: number; afterVersion: number }
export type FacadeListener = (event: FacadeEvent) => void
export type Unsubscribe = () => void
@@ -867,6 +907,9 @@ export interface BitBybitWebCadFacade {
load(documentId: string): Promise<DocumentSnapshot | null>
markDirty(): void
setProperty(input: SetPropertyInput): void
addDynamicProperty(input: AddDynamicPropertyInput): ObjectPropertySnapshot
removeDynamicProperty(input: RemoveDynamicPropertyInput): boolean
renameDynamicProperty(input: RenameDynamicPropertyInput): boolean
reorderBodyFeature(input: ReorderBodyFeatureInput): DocumentSnapshot
removeObject(input: RemoveObjectInput): string[]
resolveTopologyReference(input: ResolveTopologyReferenceInput): TopoRefValue

View File

@@ -30,7 +30,7 @@ import { resolveMeshSubshape, resolveScreenBoxSelection } from '../src/facade/th
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
import { decodeFcstdPropertyValue, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive, serializeStringHasherTableResource, storeFcstdShapeResources } from '../src/facade/fcstd'
import { ATTACHMENT_MAP_MODES, composeAttachmentPlacement, identityAttachmentOffset, validateAttachmentMapMode, validateAttachmentOffset, validateAttachmentSupport } from '../src/facade/attachment'
import { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyHidden, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, normalizeFreecadPropertyStatusMask } from '../src/facade/propertyStatus'
import { decodeFreecadPropertyStatus, encodeFreecadPropertyStatus, freecadPropertyRecomputeEffect, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyHidden, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, isFreecadPropertyRecomputeSuppressed, normalizeFreecadPropertyStatusMask } from '../src/facade/propertyStatus'
import { redirectBodyTips, resolveBodyTip } from '../src/facade/bodyRules'
import { assertShapeHandleIntegrity, BitbybitGeometryRuntime, classifyPlanarProfile, collectGeometryImportText, MAX_GEOMETRY_IMPORT_TEXT_BYTES, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validateGeometryFileImport, validateGrooveInput, validateLoftInput, validateMirrorInput, validatePadInput, validatePipeInput, validatePlacementInput, validatePlanarProfile, validatePrismInput, validateRevolutionInput, validateSphereInput, validateTorusInput } from '../src/facade/geometryRuntime'
import type { DocumentObjectSnapshot, DocumentSnapshot, MultiTransformValue, NativeTopologyHistoryInput, ObjectTopologySnapshot, PlanarProfile, Point3, ShapeHandle, SubshapeTopology, TopoRefValue } from '../src/facade/types'
@@ -4509,6 +4509,7 @@ test('FreeCAD Property status masks preserve all source bits and enforce native
assert.equal(isFreecadPropertyRecomputeSuppressed(mask), true)
assert.equal(isFreecadPropertyPersistenceSuppressed(mask), true)
assert.equal(isFreecadPropertyDocumentModifiedSuppressed(mask), true)
assert.equal(isFreecadPropertyPartialDocumentTrigger(encodeFreecadPropertyStatus(['PartialTrigger'])), true)
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['NoRecompute'])), 'owner')
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['PropNoRecompute'])), 'dependents')
assert.equal(freecadPropertyRecomputeEffect(encodeFreecadPropertyStatus(['Output'])), 'none')
@@ -4539,6 +4540,79 @@ test('FreeCAD Property status masks preserve all source bits and enforce native
facade.geometry.dispose()
})
test('dynamic Property mutations are transactional, observable, and enforce LockDynamic', () => {
const document = recomputeDocumentFixture([{ sourceId: 'Follower', targetId: 'Box', relation: 'link', propertyName: 'Base' }])
document.tree = [{ id: 'Box', label: 'Box', type: 'feature' }, { id: 'Follower', label: 'Follower', type: 'feature' }]
document.objects = [
{ id: 'Box', typeId: 'App::FeaturePython', properties: [] },
{ id: 'Follower', typeId: 'Part::Feature', properties: [{ name: 'Base', label: 'Base', group: 'Base', scope: 'data', type: 'App::PropertyLink', value: 'Box', recompute: true }] },
]
document.recompute = { generation: 0, status: 'idle', objectStates: { Box: 'up-to-date', Follower: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }
const facade = createWebCadFacade({ runtimeMode: 'mock', initialDocument: document })
const observed: string[] = []
facade.subscribe((event) => {
if (event.type.startsWith('property.') || event.type === 'transaction.committed') observed.push(event.type)
})
const locked = facade.app.document.addDynamicProperty({ objectId: 'Box', property: {
name: 'LockedValue', label: 'Locked value', group: 'Oracle', scope: 'data', type: 'App::PropertyInteger', value: 1, recompute: true,
nativeStatus: encodeFreecadPropertyStatus(['LockDynamic']),
} })
assert.deepEqual(decodeFreecadPropertyStatus(locked.nativeStatus as number).names, ['LockDynamic', 'PropDynamic'])
facade.app.document.setProperty({ objectId: 'Box', propertyName: 'LockedValue', value: 2 })
assert.deepEqual(facade.app.document.getActive().recompute?.dirtyObjects, ['Box', 'Follower'])
facade.history.undo()
assert.equal(facade.app.document.getObject('Box')?.properties.find((property) => property.name === 'LockedValue')?.value, 1)
facade.history.redo()
assert.equal(facade.app.document.removeDynamicProperty({ objectId: 'Box', propertyName: 'LockedValue' }), false)
assert.throws(() => facade.app.document.renameDynamicProperty({ objectId: 'Box', propertyName: 'LockedValue', newPropertyName: 'RenamedLockedValue' }), /Property LockedValue is locked/)
facade.app.document.addDynamicProperty({ objectId: 'Box', property: { name: 'MutableValue', label: 'Mutable value', group: 'Oracle', scope: 'data', type: 'App::PropertyFloatConstraint', value: 3 } })
assert.equal(facade.app.document.renameDynamicProperty({ objectId: 'Box', propertyName: 'MutableValue', newPropertyName: 'RenamedValue' }), true)
assert.equal(facade.app.document.removeDynamicProperty({ objectId: 'Box', propertyName: 'RenamedValue' }), true)
assert.equal(facade.app.document.getObject('Box')?.properties.some((property) => property.name === 'RenamedValue'), false)
assert.deepEqual(observed, [
'property.added', 'transaction.committed',
'property.before-change', 'property.changed', 'transaction.committed',
'property.added', 'transaction.committed',
'property.renamed', 'transaction.committed',
'property.removed', 'transaction.committed',
])
facade.geometry.dispose()
})
test('PartialTrigger suppresses partial-document mutation warnings while recompute waits for a full load', async () => {
const document = recomputeDocumentFixture([{ sourceId: 'Follower', targetId: 'Binder', relation: 'link', propertyName: 'Base' }])
document.partial = true
document.dirty = false
document.tree = [{ id: 'Binder', label: 'Binder', type: 'feature' }, { id: 'Follower', label: 'Follower', type: 'feature' }]
document.objects = [
{ id: 'Binder', typeId: 'PartDesign::SubShapeBinder', properties: [
{ name: 'PartialLoad', label: 'Partial load', group: 'Base', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true, nativeStatus: encodeFreecadPropertyStatus(['PartialTrigger']) },
{ name: 'Ordinary', label: 'Ordinary', group: 'Base', scope: 'data', type: 'App::PropertyInteger', value: 1, recompute: true },
] },
{ id: 'Follower', typeId: 'Part::Feature', properties: [{ name: 'Base', label: 'Base', group: 'Base', scope: 'data', type: 'App::PropertyLink', value: 'Binder', recompute: true }] },
]
document.recompute = { generation: 4, status: 'idle', objectStates: { Binder: 'up-to-date', Follower: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] }
const facade = createWebCadFacade({ runtimeMode: 'mock', initialDocument: document })
const changes: Array<{ propertyName: string; partialTrigger: boolean }> = []
facade.subscribe((event) => { if (event.type === 'property.changed') changes.push({ propertyName: event.propertyName, partialTrigger: event.partialTrigger }) })
facade.app.document.setProperty({ objectId: 'Binder', propertyName: 'PartialLoad', value: true })
assert.equal(facade.diagnostics.list().some((diagnostic) => diagnostic.code === 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED'), false)
facade.app.document.setProperty({ objectId: 'Binder', propertyName: 'Ordinary', value: 2 })
assert.equal(facade.diagnostics.list().filter((diagnostic) => diagnostic.code === 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED').length, 1)
assert.deepEqual(changes, [{ propertyName: 'PartialLoad', partialTrigger: true }, { propertyName: 'Ordinary', partialTrigger: false }])
const sync = facade.app.document.recompute()
assert.deepEqual(sync.affected, [])
assert.equal(sync.generation, 4)
const asyncResult = await facade.app.document.recomputeAsync()
assert.equal(asyncResult.status, 'completed')
assert.deepEqual(asyncResult.affected, [])
assert.deepEqual(asyncResult.dirtyObjects, ['Binder', 'Follower'])
facade.geometry.dispose()
})
test('FCStd Property status preserves transient definitions while NoPersist stays out of the archive', () => {
const nativeStatus = encodeFreecadPropertyStatus(['Ordered', 'PropNoRecompute', 'PropReadOnly', 'PropHidden', 'PropOutput'])
const document = recomputeDocumentFixture()
@@ -4585,6 +4659,33 @@ test('FCStd typed property codec decodes numeric, boolean and LinkSub values wit
assert.deepEqual(decodeFcstdPropertyValue({ name: 'OptionalBase', typeId: 'App::PropertyLink', element: 'Link', value: '' }), { value: null, decoded: true })
})
test('FCStd high-frequency App Property codecs preserve constraint numerics and hidden links natively', () => {
const dynamicStatus = encodeFreecadPropertyStatus(['PropDynamic'])
const document = recomputeDocumentFixture()
document.tree = [{ id: 'Holder', label: 'Holder', type: 'feature' }, { id: 'Target', label: 'Target', type: 'feature' }]
document.objects = [
{ id: 'Holder', typeId: 'App::FeaturePython', properties: [
{ name: 'FloatConstraint', label: 'Float constraint', group: 'Oracle', scope: 'data', type: 'App::PropertyFloatConstraint', value: 0.25, nativeStatus: dynamicStatus },
{ name: 'QuantityConstraint', label: 'Quantity constraint', group: 'Oracle', scope: 'data', type: 'App::PropertyQuantityConstraint', value: 12.5, nativeStatus: dynamicStatus },
{ name: 'Precision', label: 'Precision', group: 'Oracle', scope: 'data', type: 'App::PropertyPrecision', value: 0.001, nativeStatus: dynamicStatus },
{ name: 'HiddenLink', label: 'Hidden link', group: 'Oracle', scope: 'data', type: 'App::PropertyLinkHidden', value: 'Target', nativeStatus: dynamicStatus },
] },
{ id: 'Target', typeId: 'App::FeaturePython', properties: [] },
]
document.dependencies = []
const archive = serializeFcstdMetadataArchive(document)
const xml = new TextDecoder().decode(unzipSync(archive)['Document.xml'])
assert.match(xml, /type="App::PropertyFloatConstraint"[^>]*><Float value="0\.25"\/><\/Property>/)
assert.match(xml, /type="App::PropertyQuantityConstraint"[^>]*><Float value="12\.5"\/><\/Property>/)
assert.match(xml, /type="App::PropertyPrecision"[^>]*><Float value="0\.001"\/><\/Property>/)
assert.match(xml, /type="App::PropertyLinkHidden"[^>]*><Link value="Target"\/><\/Property>/)
const properties = inspectFcstdArchive(archive).objects.find((object) => object.name === 'Holder')?.properties ?? []
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'FloatConstraint')!), { value: 0.25, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'QuantityConstraint')!), { value: 12.5, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'Precision')!), { value: 0.001, decoded: true })
assert.deepEqual(decodeFcstdPropertyValue(properties.find((property) => property.name === 'HiddenLink')!), { value: 'Target', decoded: true })
})
test('FCStd native Part primitive properties omit dynamic metadata and use Float payloads', () => {
const document = recomputeDocumentFixture([
{ sourceId: 'Extrusion', targetId: 'Profile', relation: 'link', propertyName: 'Base' },