From f97eae415366549213a26a985a511946b83ff667 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Thu, 13 Aug 2026 17:16:07 -0400 Subject: [PATCH] feat: add candidate FreeCAD naming SDK and attachment oracles Build and verify the candidate-only FreeCAD naming bridge and OCCT worker path, including three-stage StringHasher restoration. Add Datum, ShapeBinder, attachment-mode, and PartDesign structure oracles plus offline SDK build plans and CI boundary checks. --- config/boost-emscripten-user-config.jam | 1 + config/cmake/boost/BoostConfig.cmake | 33 + config/cmake/boost/BoostConfigVersion.cmake | 9 + config/cmake/fmt/fmt-config.cmake | 17 + config/compatibility-matrix.json | 16 +- ...scripten-freecad-candidate-toolchain.cmake | 13 + config/freecad-attachment-mode-oracle.json | 2340 +++++++++++++++++ config/freecad-naming-next-tasks.json | 87 + config/freecad-naming-sdk-plan.json | 277 ++ .../freecad-partdesign-structure-oracle.json | 346 +++ ...eecad-private-naming-source-readiness.json | 106 + config/freecad-wasm-sdk-bootstrap.cmake | 38 + config/freecad-wasm-sdk-build-plan.json | 112 + config/freecad-wasm-sdk-compat.h | 11 + config/freecad-web-exact-parity-plan.json | 2 +- config/offline-resources.json | 129 + config/wasm-compat/execinfo.h | 7 + docs/continuation-status.zh-CN.md | 50 +- docs/freecad-full-parity-plan.zh-CN.md | 6 +- .../freecad_naming_bridge.cpp | 783 ++++++ native/freecad-naming-bridge/pre.js | 26 + native/freecad-naming-bridge/smoke-test.ts | 185 ++ .../abi-candidate-smoke.ts | 74 + native/freecad-naming-probe/probe.cpp | 250 ++ .../shims/App/Application.h | 41 + .../freecad-naming-probe/shims/App/Document.h | 19 + .../shims/App/DocumentObject.h | 31 + .../shims/App/StringHasher.h | 27 - .../freecad-naming-probe/shims/Base/Console.h | 19 + .../shims/Base/Exception.h | 35 + .../shims/Base/Persistence.h | 38 + .../shims/Base/PyObjectBase.h | 3 + .../freecad-naming-probe/shims/Base/Reader.h | 45 + .../freecad-naming-probe/shims/Base/Stream.h | 43 + .../freecad-naming-probe/shims/Base/Writer.h | 40 + .../shims/CXX/Objects.hxx | 9 + native/freecad-naming-probe/shims/FCConfig.h | 11 + .../freecad-naming-probe/shims/ProbeAppHost.h | 5 + .../shims/StringHasherPy.h | 15 + .../freecad-naming-probe/shims/StringIDPy.h | 17 + native/freecad-naming-probe/smoke-test.mjs | 31 +- native/occt-history/README.md | 37 +- native/occt-history/build.sh | 41 +- .../freecad-naming-candidate-smoke.ts | 149 ++ .../freecad-wasm-sdk-manifest.example.json | 47 +- package.json | 31 +- scripts/boost-emscripten-compiler-wrapper.sh | 10 + scripts/build-boost-wasm.sh | 49 + scripts/build-cpython-wasm.sh | 85 + .../build-freecad-naming-bridge-candidate.sh | 88 + scripts/build-freecad-naming-sdk-candidate.sh | 85 + scripts/build-freecad-naming-source-probe.sh | 60 +- scripts/build-icu-wasm.sh | 57 + scripts/build-qt6-freecad-wasm.sh | 75 + scripts/build-xerces-c-wasm.sh | 67 + scripts/build-yaml-cpp-wasm.sh | 48 + ...chrome-freecad-naming-worker-candidate.mjs | 19 + .../check-freecad-attachment-mode-oracle.mjs | 66 + .../check-freecad-naming-bridge-candidate.mjs | 19 + scripts/check-freecad-naming-next-tasks.mjs | 22 + .../check-freecad-naming-sdk-readiness.mjs | 103 + scripts/check-freecad-naming-sdk.mjs | 62 +- ...ck-freecad-partdesign-structure-oracle.mjs | 52 + .../check-freecad-private-naming-boundary.mjs | 54 +- scripts/check-freecad-wasm-sdk-build-plan.mjs | 48 + scripts/check-quality-closure.mjs | 8 +- ...configure-freecad-naming-sdk-candidate.mjs | 105 + scripts/freecad-attachment-mode-oracle.py | 182 ++ scripts/freecad-naming-sdk-lib.mjs | 126 + .../freecad-partdesign-structure-oracle.py | 240 ++ .../generate-freecad-naming-sdk-manifest.mjs | 92 + scripts/offline-resource-lib.mjs | 2 +- scripts/prepare-freecad-occt8-overlay.sh | 166 ++ ...chrome-freecad-naming-worker-candidate.mjs | 106 + .../run-freecad-attachment-mode-oracle.mjs | 27 + ...un-freecad-partdesign-structure-oracle.mjs | 27 + scripts/run-real-verification.mjs | 19 +- src/facade/nativeNamingAbi.ts | 13 +- tests/freecadPrivateNamingBoundary.test.mjs | 179 +- 79 files changed, 7936 insertions(+), 77 deletions(-) create mode 100644 config/boost-emscripten-user-config.jam create mode 100644 config/cmake/boost/BoostConfig.cmake create mode 100644 config/cmake/boost/BoostConfigVersion.cmake create mode 100644 config/cmake/fmt/fmt-config.cmake create mode 100644 config/emscripten-freecad-candidate-toolchain.cmake create mode 100644 config/freecad-attachment-mode-oracle.json create mode 100644 config/freecad-naming-next-tasks.json create mode 100644 config/freecad-naming-sdk-plan.json create mode 100644 config/freecad-partdesign-structure-oracle.json create mode 100644 config/freecad-private-naming-source-readiness.json create mode 100644 config/freecad-wasm-sdk-bootstrap.cmake create mode 100644 config/freecad-wasm-sdk-build-plan.json create mode 100644 config/freecad-wasm-sdk-compat.h create mode 100644 config/wasm-compat/execinfo.h create mode 100644 native/freecad-naming-bridge/freecad_naming_bridge.cpp create mode 100644 native/freecad-naming-bridge/pre.js create mode 100644 native/freecad-naming-bridge/smoke-test.ts create mode 100644 native/freecad-naming-probe/abi-candidate-smoke.ts create mode 100644 native/freecad-naming-probe/shims/App/Application.h create mode 100644 native/freecad-naming-probe/shims/App/Document.h create mode 100644 native/freecad-naming-probe/shims/App/DocumentObject.h delete mode 100644 native/freecad-naming-probe/shims/App/StringHasher.h create mode 100644 native/freecad-naming-probe/shims/Base/Exception.h create mode 100644 native/freecad-naming-probe/shims/Base/Persistence.h create mode 100644 native/freecad-naming-probe/shims/Base/PyObjectBase.h create mode 100644 native/freecad-naming-probe/shims/Base/Reader.h create mode 100644 native/freecad-naming-probe/shims/Base/Stream.h create mode 100644 native/freecad-naming-probe/shims/Base/Writer.h create mode 100644 native/freecad-naming-probe/shims/CXX/Objects.hxx create mode 100644 native/freecad-naming-probe/shims/FCConfig.h create mode 100644 native/freecad-naming-probe/shims/ProbeAppHost.h create mode 100644 native/freecad-naming-probe/shims/StringHasherPy.h create mode 100644 native/freecad-naming-probe/shims/StringIDPy.h create mode 100644 native/occt-history/freecad-naming-candidate-smoke.ts create mode 100755 scripts/boost-emscripten-compiler-wrapper.sh create mode 100755 scripts/build-boost-wasm.sh create mode 100644 scripts/build-cpython-wasm.sh create mode 100755 scripts/build-freecad-naming-bridge-candidate.sh create mode 100755 scripts/build-freecad-naming-sdk-candidate.sh create mode 100644 scripts/build-icu-wasm.sh create mode 100644 scripts/build-qt6-freecad-wasm.sh create mode 100755 scripts/build-xerces-c-wasm.sh create mode 100644 scripts/build-yaml-cpp-wasm.sh create mode 100644 scripts/check-chrome-freecad-naming-worker-candidate.mjs create mode 100644 scripts/check-freecad-attachment-mode-oracle.mjs create mode 100644 scripts/check-freecad-naming-bridge-candidate.mjs create mode 100644 scripts/check-freecad-naming-next-tasks.mjs create mode 100644 scripts/check-freecad-naming-sdk-readiness.mjs create mode 100644 scripts/check-freecad-partdesign-structure-oracle.mjs create mode 100644 scripts/check-freecad-wasm-sdk-build-plan.mjs create mode 100644 scripts/configure-freecad-naming-sdk-candidate.mjs create mode 100644 scripts/freecad-attachment-mode-oracle.py create mode 100644 scripts/freecad-naming-sdk-lib.mjs create mode 100644 scripts/freecad-partdesign-structure-oracle.py create mode 100644 scripts/generate-freecad-naming-sdk-manifest.mjs create mode 100755 scripts/prepare-freecad-occt8-overlay.sh create mode 100644 scripts/run-chrome-freecad-naming-worker-candidate.mjs create mode 100644 scripts/run-freecad-attachment-mode-oracle.mjs create mode 100644 scripts/run-freecad-partdesign-structure-oracle.mjs diff --git a/config/boost-emscripten-user-config.jam b/config/boost-emscripten-user-config.jam new file mode 100644 index 0000000..e83b059 --- /dev/null +++ b/config/boost-emscripten-user-config.jam @@ -0,0 +1 @@ +using emscripten : 3.1.69 : ./boost-emscripten-compiler-wrapper.sh ; diff --git a/config/cmake/boost/BoostConfig.cmake b/config/cmake/boost/BoostConfig.cmake new file mode 100644 index 0000000..7b260f7 --- /dev/null +++ b/config/cmake/boost/BoostConfig.cmake @@ -0,0 +1,33 @@ +set(Boost_VERSION 108300) +set(Boost_VERSION_STRING "1.83.0") +set(Boost_INCLUDE_DIRS "${BOOST_WASM_ROOT}/include") +set(Boost_INCLUDE_DIR "${Boost_INCLUDE_DIRS}") +set(Boost_LIBRARIES "") + +foreach(component IN LISTS Boost_FIND_COMPONENTS) + string(TOLOWER "${component}" component_lower) + set(archive "${BOOST_WASM_ROOT}/lib/libboost_${component_lower}.a") + if(NOT EXISTS "${archive}") + set(Boost_${component}_FOUND FALSE) + set(Boost_FOUND FALSE) + if(Boost_FIND_REQUIRED_${component}) + message(FATAL_ERROR "Missing Boost wasm component ${component}: ${archive}") + endif() + continue() + endif() + if(NOT TARGET Boost::${component}) + add_library(Boost::${component} STATIC IMPORTED) + set_target_properties(Boost::${component} PROPERTIES + IMPORTED_LOCATION "${archive}" + INTERFACE_INCLUDE_DIRECTORIES "${Boost_INCLUDE_DIRS}" + ) + if(component_lower STREQUAL "thread") + set_property(TARGET Boost::${component} APPEND PROPERTY INTERFACE_LINK_LIBRARIES + "${BOOST_WASM_ROOT}/lib/libboost_atomic.a") + endif() + endif() + set(Boost_${component}_FOUND TRUE) + list(APPEND Boost_LIBRARIES Boost::${component}) +endforeach() + +set(Boost_FOUND TRUE) diff --git a/config/cmake/boost/BoostConfigVersion.cmake b/config/cmake/boost/BoostConfigVersion.cmake new file mode 100644 index 0000000..e3bf07f --- /dev/null +++ b/config/cmake/boost/BoostConfigVersion.cmake @@ -0,0 +1,9 @@ +set(PACKAGE_VERSION "1.83.0") +if(PACKAGE_FIND_VERSION VERSION_GREATER PACKAGE_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() diff --git a/config/cmake/fmt/fmt-config.cmake b/config/cmake/fmt/fmt-config.cmake new file mode 100644 index 0000000..a399d66 --- /dev/null +++ b/config/cmake/fmt/fmt-config.cmake @@ -0,0 +1,17 @@ +if(NOT FREECAD_WASM_OFFLINE_SYSROOT) + message(FATAL_ERROR "FREECAD_WASM_OFFLINE_SYSROOT is required by the candidate fmt package") +endif() +set(_freecad_wasm_fmt_include "${FREECAD_WASM_OFFLINE_SYSROOT}/fmt-10.1.1/include") +if(NOT EXISTS "${_freecad_wasm_fmt_include}/fmt/format.h") + message(FATAL_ERROR "Offline fmt headers are missing: ${_freecad_wasm_fmt_include}/fmt/format.h") +endif() +if(NOT TARGET fmt) + add_library(fmt INTERFACE) + target_include_directories(fmt INTERFACE "${_freecad_wasm_fmt_include}") + target_compile_definitions(fmt INTERFACE FMT_HEADER_ONLY=1) +endif() +if(NOT TARGET fmt::fmt) + add_library(fmt::fmt ALIAS fmt) +endif() +set(fmt_FOUND TRUE) +set(fmt_VERSION 10.1.1) diff --git a/config/compatibility-matrix.json b/config/compatibility-matrix.json index 4c30871..1bae714 100644 --- a/config/compatibility-matrix.json +++ b/config/compatibility-matrix.json @@ -102,7 +102,8 @@ }, "freecadNamingBuild": { "status": "contract-only", - "prerequisiteStatus": "qt6-wasm-core-and-locked-mapped-name-source-probe-pass", + "prerequisiteStatus": "real-isolated-freecad-private-naming-bridge-pass", + "sdkReadinessStatus": "candidate-complete", "sdkEnvironment": "FREECAD_WASM_SDK_DIR", "requiredManifest": "manifest.json", "requiredCallbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"], @@ -111,8 +112,21 @@ "requiredEmscriptenVersion": "3.1.69", "buildCommand": "./npmw run build:freecad-naming-worker", "qtCoreBuildCommand": "./npmw run build:qt6-wasm-core", + "qtFreecadSdkBuildCommand": "./npmw run build:qt6-freecad-wasm", + "yamlCppBuildCommand": "./npmw run build:yaml-cpp-wasm", + "icuBuildCommand": "./npmw run build:icu-wasm", + "pythonBuildCommand": "./npmw run build:cpython-wasm", + "xercesBuildCommand": "./npmw run build:xerces-c-wasm", + "boostBuildCommand": "./npmw run build:boost-wasm", "sourceProbeBuildCommand": "./npmw run build:freecad-naming-source-probe", "sourceProbeTestCommand": "./npmw run test:freecad-naming-source-probe", + "sdkReadinessCommand": "./npmw run check:freecad-naming-sdk-readiness", + "sdkManifestCommand": "./npmw run generate:freecad-naming-sdk-manifest", + "isolatedBridgeBuildCommand": "./npmw run build:freecad-naming-bridge-candidate", + "isolatedBridgeTestCommand": "./npmw run test:freecad-naming-bridge-candidate", + "candidateWorkerTestCommand": "./npmw run test:freecad-naming-worker-candidate", + "candidateChromeRunCommand": "./npmw run test:chrome-freecad-naming-worker-candidate", + "candidateChromeCheckCommand": "./npmw run check:chrome-freecad-naming-worker-candidate", "boundaryCheckCommand": "./npmw run check:freecad-private-naming-boundary", "checkCommand": "./npmw run check:freecad-naming-sdk", "statusBoundary": "Missing SDK or callback probe keeps EX-TSN-02=in_progress and systemExact=false" diff --git a/config/emscripten-freecad-candidate-toolchain.cmake b/config/emscripten-freecad-candidate-toolchain.cmake new file mode 100644 index 0000000..23e9701 --- /dev/null +++ b/config/emscripten-freecad-candidate-toolchain.cmake @@ -0,0 +1,13 @@ +# Candidate-only wrapper around the pinned Emscripten 3.1.69 platform file. +include("/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake") + +# FreeCAD declares Base/App/Part as SHARED. For this candidate, collect their +# wasm object files as static archives and link shared dependencies separately. +set(CMAKE_SHARED_LIBRARY_SUFFIX ".a") +set(CMAKE_C_CREATE_SHARED_LIBRARY " rcs ") +set(CMAKE_CXX_CREATE_SHARED_LIBRARY " rcs ") + +# FreeCAD's native compiler setup uses the CMake UNIX boolean to add +# --undefined,dynamic_lookup. Emscripten emits SHARED as a relocatable object, +# so that native-only linker branch must stay disabled for this candidate. +set(UNIX FALSE) diff --git a/config/freecad-attachment-mode-oracle.json b/config/freecad-attachment-mode-oracle.json new file mode 100644 index 0000000..a442a7a --- /dev/null +++ b/config/freecad-attachment-mode-oracle.json @@ -0,0 +1,2340 @@ +{ + "baselineId": "freecad-1.1.1-attachment-mode-oracle", + "engines": { + "line": { + "attacherType": "Attacher::AttachEngineLine", + "completeModeList": [ + "Deactivated", + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TangentU", + "TangentV", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "IntersectionPoint", + "Vertex", + "ProximityPoint1", + "ProximityPoint2", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "InertialCS", + "FaceNormal", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane", + "MidPoint" + ], + "implementedModes": [ + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "FaceNormal" + ], + "modeInfo": { + "Asymptote1": { + "modeIndex": 22, + "referenceCombinations": [ + [ + "Hyperbola" + ] + ] + }, + "Asymptote2": { + "modeIndex": 23, + "referenceCombinations": [ + [ + "Hyperbola" + ] + ] + }, + "AxisOfCurvature": { + "modeIndex": 19, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "AxisOfInertia1": { + "modeIndex": 42, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "AxisOfInertia2": { + "modeIndex": 43, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "AxisOfInertia3": { + "modeIndex": 44, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "Binormal": { + "modeIndex": 26, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "Directrix1": { + "modeIndex": 20, + "referenceCombinations": [ + [ + "Conic" + ] + ] + }, + "Directrix2": { + "modeIndex": 21, + "referenceCombinations": [ + [ + "Ellipse" + ], + [ + "Hyperbola" + ] + ] + }, + "FaceNormal": { + "modeIndex": 46, + "referenceCombinations": [ + [ + "Face", + "Vertex" + ], + [ + "Vertex", + "Face" + ] + ] + }, + "IntersectionLine": { + "modeIndex": 30, + "referenceCombinations": [ + [ + "Face", + "Face" + ] + ] + }, + "Normal": { + "modeIndex": 25, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "ObjectX": { + "modeIndex": 16, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectY": { + "modeIndex": 17, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectZ": { + "modeIndex": 18, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ProximityLine": { + "modeIndex": 31, + "referenceCombinations": [ + [ + "Any", + "Any" + ] + ] + }, + "Tangent": { + "modeIndex": 24, + "referenceCombinations": [ + [ + "Edge" + ], + [ + "Edge", + "Vertex" + ], + [ + "Vertex", + "Edge" + ] + ] + }, + "TwoPointLine": { + "modeIndex": 29, + "referenceCombinations": [ + [ + "Vertex", + "Vertex" + ], + [ + "Line" + ] + ] + } + }, + "typeId": "PartDesign::Line" + }, + "plane": { + "attacherType": "Attacher::AttachEnginePlane", + "completeModeList": [ + "Deactivated", + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TangentU", + "TangentV", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "IntersectionPoint", + "Vertex", + "ProximityPoint1", + "ProximityPoint2", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "InertialCS", + "FaceNormal", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane", + "MidPoint" + ], + "implementedModes": [ + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "InertialCS", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane" + ], + "modeInfo": { + "Concentric": { + "modeIndex": 11, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "FlatFace": { + "modeIndex": 5, + "referenceCombinations": [ + [ + "Plane" + ] + ] + }, + "Folding": { + "modeIndex": 15, + "referenceCombinations": [ + [ + "Line", + "Line", + "Line", + "Line" + ] + ] + }, + "FrenetNB": { + "modeIndex": 8, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "FrenetTB": { + "modeIndex": 10, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "FrenetTN": { + "modeIndex": 9, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "InertialCS": { + "modeIndex": 45, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "NormalToEdge": { + "modeIndex": 7, + "referenceCombinations": [ + [ + "Edge" + ], + [ + "Edge", + "Vertex" + ], + [ + "Vertex", + "Edge" + ] + ] + }, + "OXY": { + "modeIndex": 49, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OXZ": { + "modeIndex": 50, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OYX": { + "modeIndex": 52, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OYZ": { + "modeIndex": 51, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OZX": { + "modeIndex": 47, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OZY": { + "modeIndex": 48, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "ObjectXY": { + "modeIndex": 2, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectXZ": { + "modeIndex": 3, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectYZ": { + "modeIndex": 4, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ParallelPlane": { + "modeIndex": 53, + "referenceCombinations": [ + [ + "Plane|Placement", + "Vertex" + ], + [ + "Any|Placement", + "Vertex" + ] + ] + }, + "SectionOfRevolution": { + "modeIndex": 12, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "TangentPlane": { + "modeIndex": 6, + "referenceCombinations": [ + [ + "Face", + "Vertex" + ], + [ + "Vertex", + "Face" + ] + ] + }, + "ThreePointsNormal": { + "modeIndex": 14, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Line", + "Vertex" + ], + [ + "Vertex", + "Line" + ], + [ + "Line", + "Line" + ] + ] + }, + "ThreePointsPlane": { + "modeIndex": 13, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Line", + "Vertex" + ], + [ + "Vertex", + "Line" + ], + [ + "Line", + "Line" + ] + ] + }, + "Translate": { + "modeIndex": 1, + "referenceCombinations": [ + [ + "Vertex" + ] + ] + } + }, + "typeId": "PartDesign::Plane" + }, + "point": { + "attacherType": "Attacher::AttachEnginePoint", + "completeModeList": [ + "Deactivated", + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TangentU", + "TangentV", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "IntersectionPoint", + "Vertex", + "ProximityPoint1", + "ProximityPoint2", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "InertialCS", + "FaceNormal", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane", + "MidPoint" + ], + "implementedModes": [ + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "Vertex", + "ProximityPoint1", + "ProximityPoint2" + ], + "modeInfo": { + "CenterOfCurvature": { + "modeIndex": 36, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "CenterOfMass": { + "modeIndex": 37, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "Focus1": { + "modeIndex": 33, + "referenceCombinations": [ + [ + "Conic" + ] + ] + }, + "Focus2": { + "modeIndex": 34, + "referenceCombinations": [ + [ + "Ellipse" + ], + [ + "Hyperbola" + ] + ] + }, + "ObjectOrigin": { + "modeIndex": 32, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "OnEdge": { + "modeIndex": 35, + "referenceCombinations": [ + [ + "Edge" + ], + [ + "Edge", + "Vertex" + ], + [ + "Vertex", + "Edge" + ] + ] + }, + "ProximityPoint1": { + "modeIndex": 40, + "referenceCombinations": [ + [ + "Any", + "Any" + ] + ] + }, + "ProximityPoint2": { + "modeIndex": 41, + "referenceCombinations": [ + [ + "Any", + "Any" + ] + ] + }, + "Vertex": { + "modeIndex": 39, + "referenceCombinations": [ + [ + "Vertex" + ], + [ + "Line" + ] + ] + } + }, + "typeId": "PartDesign::Point" + }, + "sketch": { + "attacherType": "Attacher::AttachEnginePlane", + "completeModeList": [ + "Deactivated", + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TangentU", + "TangentV", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "IntersectionPoint", + "Vertex", + "ProximityPoint1", + "ProximityPoint2", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "InertialCS", + "FaceNormal", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane", + "MidPoint" + ], + "implementedModes": [ + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "InertialCS", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane" + ], + "modeInfo": { + "Concentric": { + "modeIndex": 11, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "FlatFace": { + "modeIndex": 5, + "referenceCombinations": [ + [ + "Plane" + ] + ] + }, + "Folding": { + "modeIndex": 15, + "referenceCombinations": [ + [ + "Line", + "Line", + "Line", + "Line" + ] + ] + }, + "FrenetNB": { + "modeIndex": 8, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "FrenetTB": { + "modeIndex": 10, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "FrenetTN": { + "modeIndex": 9, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Curve", + "Vertex" + ], + [ + "Vertex", + "Curve" + ] + ] + }, + "InertialCS": { + "modeIndex": 45, + "referenceCombinations": [ + [ + "Any" + ], + [ + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any" + ], + [ + "Any", + "Any", + "Any", + "Any" + ] + ] + }, + "NormalToEdge": { + "modeIndex": 7, + "referenceCombinations": [ + [ + "Edge" + ], + [ + "Edge", + "Vertex" + ], + [ + "Vertex", + "Edge" + ] + ] + }, + "OXY": { + "modeIndex": 49, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OXZ": { + "modeIndex": 50, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OYX": { + "modeIndex": 52, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OYZ": { + "modeIndex": 51, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OZX": { + "modeIndex": 47, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "OZY": { + "modeIndex": 48, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Vertex", + "Line" + ], + [ + "Vertex", + "Line", + "Vertex" + ], + [ + "Vertex", + "Line", + "Line" + ], + [ + "Vertex", + "Vertex" + ], + [ + "Vertex", + "Line" + ] + ] + }, + "ObjectXY": { + "modeIndex": 2, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectXZ": { + "modeIndex": 3, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ObjectYZ": { + "modeIndex": 4, + "referenceCombinations": [ + [ + "Any|Placement" + ], + [ + "Conic" + ] + ] + }, + "ParallelPlane": { + "modeIndex": 53, + "referenceCombinations": [ + [ + "Plane|Placement", + "Vertex" + ], + [ + "Any|Placement", + "Vertex" + ] + ] + }, + "SectionOfRevolution": { + "modeIndex": 12, + "referenceCombinations": [ + [ + "Curve" + ], + [ + "Circle" + ], + [ + "Curve", + "Vertex" + ], + [ + "Circle", + "Vertex" + ], + [ + "Vertex", + "Curve" + ], + [ + "Vertex", + "Circle" + ] + ] + }, + "TangentPlane": { + "modeIndex": 6, + "referenceCombinations": [ + [ + "Face", + "Vertex" + ], + [ + "Vertex", + "Face" + ] + ] + }, + "ThreePointsNormal": { + "modeIndex": 14, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Line", + "Vertex" + ], + [ + "Vertex", + "Line" + ], + [ + "Line", + "Line" + ] + ] + }, + "ThreePointsPlane": { + "modeIndex": 13, + "referenceCombinations": [ + [ + "Vertex", + "Vertex", + "Vertex" + ], + [ + "Line", + "Vertex" + ], + [ + "Vertex", + "Line" + ], + [ + "Line", + "Line" + ] + ] + }, + "Translate": { + "modeIndex": 1, + "referenceCombinations": [ + [ + "Vertex" + ] + ] + } + }, + "typeId": "Sketcher::SketchObject" + } + }, + "failures": { + "initial": { + "InvalidLine": { + "mapMode": "FlatFace", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode FlatFace is not implemented.", + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "PartDesign::Line" + }, + "InvalidPlane": { + "mapMode": "Vertex", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode Vertex is not implemented.", + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1" + ] + } + ], + "typeId": "PartDesign::Plane" + }, + "InvalidPoint": { + "mapMode": "FlatFace", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode FlatFace is not implemented.", + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "PartDesign::Point" + } + }, + "roundtrip": { + "InvalidLine": { + "mapMode": "FlatFace", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode FlatFace is not implemented." + }, + "InvalidPlane": { + "mapMode": "Vertex", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode Vertex is not implemented." + }, + "InvalidPoint": { + "mapMode": "FlatFace", + "state": [ + "Touched", + "Invalid" + ], + "status": "Attachment mode FlatFace is not implemented." + } + } + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "registry": { + "implementedUnionCount": 50, + "modeCount": 55, + "modes": [ + "Deactivated", + "Translate", + "ObjectXY", + "ObjectXZ", + "ObjectYZ", + "FlatFace", + "TangentPlane", + "NormalToEdge", + "FrenetNB", + "FrenetTN", + "FrenetTB", + "Concentric", + "SectionOfRevolution", + "ThreePointsPlane", + "ThreePointsNormal", + "Folding", + "ObjectX", + "ObjectY", + "ObjectZ", + "AxisOfCurvature", + "Directrix1", + "Directrix2", + "Asymptote1", + "Asymptote2", + "Tangent", + "Normal", + "Binormal", + "TangentU", + "TangentV", + "TwoPointLine", + "IntersectionLine", + "ProximityLine", + "ObjectOrigin", + "Focus1", + "Focus2", + "OnEdge", + "CenterOfCurvature", + "CenterOfMass", + "IntersectionPoint", + "Vertex", + "ProximityPoint1", + "ProximityPoint2", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3", + "InertialCS", + "FaceNormal", + "OZX", + "OZY", + "OXY", + "OXZ", + "OYZ", + "OYX", + "ParallelPlane", + "MidPoint" + ], + "unimplementedModes": [ + "Deactivated", + "TangentU", + "TangentV", + "IntersectionPoint", + "MidPoint" + ] + }, + "schemaVersion": 1, + "status": "pass", + "success": { + "edited": { + "line": { + "mapMode": "TwoPointLine", + "placement": { + "angleDegrees": 180, + "axis": [ + 0, + 1, + 0 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "TwoPointLine", + "ProximityLine", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3" + ], + "bestFitMode": "TwoPointLine", + "message": "OK", + "referenceTypes": [ + "Vertex", + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1", + "Vertex2" + ] + } + ], + "typeId": "PartDesign::Line" + }, + "plane": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "PartDesign::Plane" + }, + "point": { + "mapMode": "Vertex", + "placement": { + "angleDegrees": 0, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 9 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "CenterOfMass", + "Vertex" + ], + "bestFitMode": "CenterOfMass", + "message": "OK", + "referenceTypes": [ + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1" + ] + } + ], + "typeId": "PartDesign::Point" + }, + "sketch": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "Sketcher::SketchObject" + } + }, + "initial": { + "line": { + "mapMode": "TwoPointLine", + "placement": { + "angleDegrees": 180, + "axis": [ + 0, + 1, + 0 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "TwoPointLine", + "ProximityLine", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3" + ], + "bestFitMode": "TwoPointLine", + "message": "OK", + "referenceTypes": [ + "Vertex", + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1", + "Vertex2" + ] + } + ], + "typeId": "PartDesign::Line" + }, + "plane": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "PartDesign::Plane" + }, + "point": { + "mapMode": "Vertex", + "placement": { + "angleDegrees": 0, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 6 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "CenterOfMass", + "Vertex" + ], + "bestFitMode": "CenterOfMass", + "message": "OK", + "referenceTypes": [ + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1" + ] + } + ], + "typeId": "PartDesign::Point" + }, + "sketch": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "Sketcher::SketchObject" + } + }, + "roundtrip": { + "line": { + "mapMode": "TwoPointLine", + "placement": { + "angleDegrees": 180, + "axis": [ + 0, + 1, + 0 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "TwoPointLine", + "ProximityLine", + "AxisOfInertia1", + "AxisOfInertia2", + "AxisOfInertia3" + ], + "bestFitMode": "TwoPointLine", + "message": "OK", + "referenceTypes": [ + "Vertex", + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1", + "Vertex2" + ] + } + ], + "typeId": "PartDesign::Line" + }, + "plane": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "PartDesign::Plane" + }, + "point": { + "mapMode": "Vertex", + "placement": { + "angleDegrees": 0, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 9 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "CenterOfMass", + "Vertex" + ], + "bestFitMode": "CenterOfMass", + "message": "OK", + "referenceTypes": [ + "Vertex" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Vertex1" + ] + } + ], + "typeId": "PartDesign::Point" + }, + "sketch": { + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 120.00000000000001, + "axis": [ + 0.5773502691896257, + -0.5773502691896257, + -0.5773502691896257 + ], + "position": [ + 0, + 0, + 0 + ] + }, + "positionBySupport": true, + "state": [ + "Up-to-date" + ], + "status": "Valid", + "suggestion": { + "allApplicableModes": [ + "FlatFace", + "InertialCS" + ], + "bestFitMode": "FlatFace", + "message": "OK", + "referenceTypes": [ + "Plane" + ] + }, + "support": [ + { + "document": "AttachmentModeOracle", + "object": "SourceBox", + "subElements": [ + "Face1" + ] + } + ], + "typeId": "Sketcher::SketchObject" + } + } + }, + "tolerance": 1e-7 +} diff --git a/config/freecad-naming-next-tasks.json b/config/freecad-naming-next-tasks.json new file mode 100644 index 0000000..8c99fee --- /dev/null +++ b/config/freecad-naming-next-tasks.json @@ -0,0 +1,87 @@ +{ + "schemaVersion": 1, + "scope": "freecad-private-naming-and-parameter-followup", + "orderedTasks": [ + { + "id": "SDK-01", + "title": "Lock candidate FreeCAD wasm SDK baseline and build boundary", + "status": "completed", + "dependencies": [], + "deliverables": ["Pinned source/toolchain plan", "Candidate-only build-plan checker"], + "acceptance": ["FreeCAD commit and Emscripten version are verified", "Production publication remains false"], + "evidence": ["check:freecad-wasm-sdk-build-plan"] + }, + { + "id": "SDK-02A", + "title": "Build the Qt wasm modules required by FreeCAD", + "status": "completed", + "dependencies": ["SDK-01"], + "deliverables": ["Qt Core/Concurrent/Network/Xml archives", "Qt bundled Pcre2/ZLIB archives"], + "acceptance": ["Every archive contains only wasm32 objects", "Thread support is enabled for QtConcurrent"], + "evidence": ["check:freecad-naming-sdk-readiness"] + }, + { + "id": "SDK-02B", + "title": "Build remaining wasm dependencies and FreeCADBase/App/Part archives", + "status": "completed", + "dependencies": ["SDK-02A"], + "deliverables": ["Python/ICU/Xerces/yaml-cpp/fmt wasm dependencies", "FreeCADBase/FreeCADApp/Part static archives"], + "acceptance": ["No host ELF member is present", "CMake configuration performs no implicit network fetch"], + "evidence": ["build:freecad-naming-sdk-candidate", "check:freecad-naming-sdk-readiness"] + }, + { + "id": "SDK-04", + "title": "Implement an isolated real FreeCAD private naming bridge", + "status": "completed", + "dependencies": ["SDK-02B"], + "deliverables": ["Three versioned callback implementations", "MappedNameRef/StringHasher/ElementMap2 runtime evidence"], + "acceptance": ["Candidate bridge passes strict Web validation", "Candidate is not published to the production Worker"], + "evidence": ["build:freecad-naming-bridge-candidate", "test:freecad-naming-bridge-candidate", "check:freecad-naming-bridge-candidate", "check:freecad-private-naming-boundary"] + }, + { + "id": "SDK-03", + "title": "Generate and verify the complete SDK manifest", + "status": "completed", + "dependencies": ["SDK-02B", "SDK-04"], + "deliverables": ["Hashed manifest", "Private header and symbol audit"], + "acceptance": ["Manifest generator completes without missing inputs", "Complete SDK checker passes"], + "evidence": ["generate:freecad-naming-sdk-manifest", "check:freecad-naming-sdk"] + }, + { + "id": "SDK-05", + "title": "Link and probe a candidate OCCT Worker", + "status": "completed", + "dependencies": ["SDK-03", "SDK-04"], + "deliverables": ["Candidate Worker artifact", "Real Chrome callback and builder-history evidence"], + "acceptance": ["All three callbacks are exported together", "Invalid or incomplete evidence fails closed"], + "evidence": ["build:freecad-naming-worker", "test:freecad-naming-worker-candidate", "test:chrome-freecad-naming-worker-candidate", "check:chrome-freecad-naming-worker-candidate"] + }, + { + "id": "PAR-01", + "title": "Expand Datum/Attachment/ShapeBinder, multi-Body and parameter combinations", + "status": "in_progress", + "dependencies": ["SDK-05"], + "deliverables": ["Native oracle corpus", "Edit/recompute/recovery/FCStd round-trip matrix"], + "acceptance": ["Every family has success and failure evidence", "Support/Tip/references survive mutation without silent rebinding"], + "evidence": ["check:freecad-sketcher-partdesign-abi", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:chrome-partdesign-lifecycle"] + }, + { + "id": "QA-01", + "title": "Close CI, offline-resource and browser matrix for the new production path", + "status": "pending", + "dependencies": ["PAR-01"], + "deliverables": ["Execute/check-separated CI lanes", "Offline SDK resources", "Chrome/Firefox/WebKit reports"], + "acceptance": ["Static reports cannot substitute for live harnesses", "A clean offline restore can rebuild and verify the candidate"], + "evidence": ["ci:real:chrome", "ci:check:chrome", "offline:smoke", "check:browser-matrix"] + } + ], + "boundary": { + "freecadNamingBuildStatus": "contract-only", + "exTsn02": "in_progress", + "systemExact": false, + "productionWorker": { + "availability": "unavailable", + "callbacks": [] + } + } +} diff --git a/config/freecad-naming-sdk-plan.json b/config/freecad-naming-sdk-plan.json new file mode 100644 index 0000000..d9c7e39 --- /dev/null +++ b/config/freecad-naming-sdk-plan.json @@ -0,0 +1,277 @@ +{ + "schemaVersion": 1, + "scope": "pre-production-sdk-readiness", + "baseline": { + "freecadVersion": "1.1.1", + "sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "emscriptenVersion": "3.1.69", + "qtVersion": "6.8.2", + "target": "wasm32-emscripten" + }, + "includeDirs": [ + { + "name": "FreeCAD", + "path": ".cache/toolchains/freecad-naming-sdk/source-occt8/src", + "requiredHeaders": [ + "App/StringHasher.h", + "App/MappedName.h", + "App/MappedElement.h", + "App/ElementMap.h" + ] + }, + { + "name": "FreeCADBuild", + "path": ".cache/toolchains/freecad-naming-sdk/build-wasm/src", + "requiredHeaders": ["Base/PersistencePy.h", "Build/Version.h"] + }, + { + "name": "PyCXX", + "path": ".cache/toolchains/freecad-naming-sdk/source-occt8/src/3rdParty/PyCXX", + "requiredHeaders": ["CXX/Objects.hxx", "CXX/Extensions.hxx"] + }, + { + "name": "OCCT", + "path": ".cache/bitbybit/occt-history-build-offline/include/opencascade", + "requiredHeaders": ["Standard.hxx", "TopoDS_Shape.hxx"] + }, + { + "name": "OCCTDeprecatedAliases", + "path": ".cache/occt/occt/src/Deprecated/NCollectionAliases", + "requiredHeaders": ["TopTools_ListOfShape.hxx", "TopTools_HSequenceOfShape.hxx"] + }, + { + "name": "QtCore", + "path": ".cache/toolchains/qt6/install-wasm-freecad/include", + "requiredHeaders": [ + "QtCore/QByteArray", + "QtCore/QJsonDocument", + "QtConcurrent/QtConcurrent", + "QtNetwork/QNetworkAccessManager", + "QtXml/QDomDocument" + ] + }, + { + "name": "QtCoreModule", + "path": ".cache/toolchains/qt6/install-wasm-freecad/include/QtCore", + "requiredHeaders": ["QString", "QJsonDocument", "QProcessEnvironment"] + }, + { + "name": "Python", + "path": ".cache/toolchains/python/install-wasm/include/python3.13", + "requiredHeaders": ["Python.h", "pyconfig.h"] + }, + { + "name": "XercesC", + "path": ".cache/toolchains/xerces-c/install-wasm/include", + "requiredHeaders": ["xercesc/util/XercesVersion.hpp"] + }, + { + "name": "ICU", + "path": ".cache/toolchains/icu/install-wasm/include", + "requiredHeaders": ["unicode/uversion.h"] + }, + { + "name": "YamlCpp", + "path": ".cache/toolchains/yaml-cpp/install-wasm/include", + "requiredHeaders": ["yaml-cpp/yaml.h"] + }, + { + "name": "OfflineSysroot", + "path": ".cache/offline-sysroot/include", + "requiredHeaders": ["boost/signals2.hpp", "fmt/format.h", "eigen3/Eigen/Core"] + } + ], + "libraries": [ + { + "name": "FreeCADBase", + "path": ".cache/toolchains/freecad-naming-sdk/lib/libFreeCADBase.a", + "required": true, + "expectedSha256": "df02d5527285750a5a34012bb04a177e97719290f4704e8a20110207367698d6" + }, + { + "name": "FreeCADApp", + "path": ".cache/toolchains/freecad-naming-sdk/lib/libFreeCADApp.a", + "required": true, + "expectedSha256": "b81c3cb67934b6add3a810a0885baeb9b459cf370b460cb3906d1b20fbe0a500" + }, + { + "name": "Part", + "path": ".cache/toolchains/freecad-naming-sdk/lib/libPart.a", + "required": true, + "expectedSha256": "a5d0f1eabd8a3e7e3ad37807ca7cab90810d2fcfe50c2956020485a37bf27d69" + }, + { + "name": "QtCore", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Core.a", + "required": true, + "expectedSha256": "7cf42203ae54fc4706a5dba69a179b340d1a5bf27ce9db881f11d57c66fd3828" + }, + { + "name": "Python", + "path": ".cache/toolchains/python/install-wasm/lib/libpython3.13.a", + "required": true, + "expectedSha256": "2e332bc1141eeea51c94510ecc8f237e1f50ff9b3bc03df9cfc6142cd0489bd0" + } + ], + "linkDependencies": [ + { + "name": "QtConcurrent", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Concurrent.a", + "required": true, + "expectedSha256": "52a8ddbc72a420a7cd93593328f2e252cfd696a37b878c623f0ecfe5cb5ade81" + }, + { + "name": "QtNetwork", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Network.a", + "required": true, + "expectedSha256": "1c06acc8b5a18447951b3a53f36f555d80544d804eb9ff920fcac20d9ced019c" + }, + { + "name": "QtXml", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6Xml.a", + "required": true, + "expectedSha256": "8694057ac1ac468b3cd40da3bd16312662e434ba4b485780eb7a31ea44381f88" + }, + { + "name": "QtBundledPcre2", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6BundledPcre2.a", + "required": true, + "expectedSha256": "9dc72f77f02b13ae7f7de4bcd579a6f04acd6d72cd76d348db1cbfaf6d6d777b" + }, + { + "name": "QtBundledZLIB", + "path": ".cache/toolchains/qt6/install-wasm-freecad/lib/libQt6BundledZLIB.a", + "required": true, + "expectedSha256": "bf3c4d7a52c706abec70d83c94e32f0bf920e978aba9bb39fd87f94efcd2201f" + }, + { + "name": "yaml-cpp", + "path": ".cache/toolchains/yaml-cpp/install-wasm/lib/libyaml-cpp.a", + "required": true, + "expectedSha256": "213acb389e7e7ff6677e8170b8651a5285af1f8c5c8bf1dfc42591559f253fe5" + }, + { + "name": "ICUCommon", + "path": ".cache/toolchains/icu/install-wasm/lib/libicuuc.a", + "required": true, + "expectedSha256": "3cbe58215f7dc0596dacea9e8ac10974e8b8302b9472ff854f35e2e8c21fe7a2" + }, + { + "name": "ICUI18N", + "path": ".cache/toolchains/icu/install-wasm/lib/libicui18n.a", + "required": true, + "expectedSha256": "627169cfa4637fe02c5cd5d8358e3faf92f933191049f80f3381c3ab7d7719d7" + }, + { + "name": "ICUData", + "path": ".cache/toolchains/icu/install-wasm/lib/libicudata.a", + "required": true, + "expectedSha256": "b146f462e94d061862c27bb11fed116a1e4678f172f6e7551f3a57fedfd17822" + }, + { + "name": "XercesC", + "path": ".cache/toolchains/xerces-c/install-wasm/lib/libxerces-c.a", + "required": true, + "expectedSha256": "86fe0646cd7d2cb528708627527ce32cd963cf6b2510178f36c96c0ffa0b89ab" + }, + { + "name": "BoostProgramOptions", + "path": ".cache/toolchains/boost/install-wasm/lib/libboost_program_options.a", + "required": true, + "expectedSha256": "5c900c12f5323122d08b696ab095bcfe1b4b636c5a7ccb353962c4c956e11a24" + }, + { + "name": "BoostRegex", + "path": ".cache/toolchains/boost/install-wasm/lib/libboost_regex.a", + "required": true, + "expectedSha256": "8fa066fba93dcb06a7e71f21338b288598a1fc4880d8799aaa4934c2239f1f3f" + }, + { + "name": "BoostThread", + "path": ".cache/toolchains/boost/install-wasm/lib/libboost_thread.a", + "required": true, + "expectedSha256": "584dbdd2a186ec905d390b62ff3d24c81e4fb18d6b7f101e136ac760e8e4927c" + }, + { + "name": "BoostDateTime", + "path": ".cache/toolchains/boost/install-wasm/lib/libboost_date_time.a", + "required": true, + "expectedSha256": "859ed6fa84a44e4dc15dd95b5306d4b2f7c37d4057aa3301edf9804edb83bf29" + }, + { + "name": "BoostAtomic", + "path": ".cache/toolchains/boost/install-wasm/lib/libboost_atomic.a", + "required": true, + "expectedSha256": "e7e7b22bb8b380f2ea2ac083136f040908c7f9abcb758dbebf729ec23dc92432" + }, + { + "name": "PythonMpdecimal", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libmpdec.a", + "required": true, + "expectedSha256": "d69dac9933119c1eafb9d9a1f5e5a81c7e8ef51f0367c55b5349b2b014a07849" + }, + { + "name": "PythonExpat", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libexpat.a", + "required": true, + "expectedSha256": "6bfc695a31ee252a836e60ba34192dcf047ee4c7fe6a7b68c527e5fbcb714b66" + }, + { + "name": "PythonHaclSha2", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libHacl_Hash_SHA2.a", + "required": true, + "expectedSha256": "1e622af13d53951c4992f74d2af211dd75c59ec14f2a9d3a3eb3c39b2d9899a4" + }, + { + "name": "PythonZlib", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libz.a", + "required": true, + "expectedSha256": "42acf281029e51e07d468d2f323055c7c5558e5d6d78738ec1e1571acd4afd67" + }, + { + "name": "PythonBzip2", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libbz2.a", + "required": true, + "expectedSha256": "991ac951c995d3606ebedc74bc070273005ac9ebff26a7cc38518702d5268de9" + }, + { + "name": "PythonSqlite3", + "path": ".cache/toolchains/python/install-wasm/lib/python-deps/libsqlite3-mt.a", + "required": true, + "expectedSha256": "738c819dab8f31a97fc7c1104f73969fee9535222b67fd938450474f7b861fd6" + } + ], + "namingBridge": { + "path": "native/freecad-naming-bridge/freecad_naming_bridge.cpp", + "hostAdapter": "native/freecad-naming-bridge/pre.js", + "requiredExports": [ + "freecadNamingAbiVersion", + "freecadNamingCapabilitiesJson", + "freecadNamingEvidenceJson" + ] + }, + "compileOptions": { + "cxxStandard": "c++20", + "pthread": true, + "forceInclude": "config/freecad-wasm-sdk-compat.h", + "definitions": ["__linux__=1", "QT_NO_KEYWORDS", "HAVE_CONFIG_H", "PYCXX_6_2_COMPATIBILITY"] + }, + "runtimeAssets": [ + { + "name": "PythonWasmStdlib", + "path": ".cache/toolchains/python/install-wasm/share/python-wasm-stdlib", + "preloadTo": "/usr/local", + "requiredFiles": ["lib/python313.zip", "lib/python3.13/os.py"], + "expectedSha256": { + "lib/python313.zip": "e3fa6e7ceaf68a2f433a682713ac05288631b3c465114284d49e02e094bb0c12", + "lib/python3.13/os.py": "b6b68783d438ff044c096fd0c3d1dfc9479a9b632e0a450a376be6c01655aafe" + } + } + ], + "productionPublication": false, + "boundary": { + "freecadNamingBuildStatus": "contract-only", + "exTsn02": "in_progress", + "systemExact": false + } +} diff --git a/config/freecad-partdesign-structure-oracle.json b/config/freecad-partdesign-structure-oracle.json new file mode 100644 index 0000000..3870881 --- /dev/null +++ b/config/freecad-partdesign-structure-oracle.json @@ -0,0 +1,346 @@ +{ + "baselineId": "freecad-1.1.1-partdesign-structure-oracle", + "crossDocument": { + "deleted": { + "shapeBinderShapeNull": true, + "shapeBinderState": [ + "Up-to-date" + ], + "shapeBinderStatus": "Valid", + "shapeBinderSupport": [], + "subShapeBinderCachedArea": 48, + "subShapeBinderShapeNull": false, + "subShapeBinderState": [ + "Up-to-date" + ], + "subShapeBinderStatus": "Valid", + "subShapeBinderSupport": [] + }, + "edited": { + "shapeBinderShapeNull": true, + "shapeBinderState": [ + "Up-to-date" + ], + "sourceFaceArea": 48, + "sourceVolume": 192, + "subShapeBinderArea": 48, + "subShapeBinderState": [ + "Up-to-date" + ] + }, + "initial": { + "shapeBinderExternalLink": { + "accepted": false, + "error": "PropertyLinkSubList does not support external object", + "errorType": "ValueError" + }, + "shapeBinderShapeNull": true, + "shapeBinderSupport": [], + "shapeBinderSupportType": "App::PropertyLinkSubListGlobal", + "shapeBodyTip": null, + "sourceFaceArea": 30, + "sourceVolume": 120, + "subBodyTip": null, + "subShapeBinderArea": 30, + "subShapeBinderSourceDocument": "PartDesignExternalSource", + "subShapeBinderSupport": [ + { + "object": "ExternalBox", + "subElements": [ + "Face1" + ] + } + ], + "subShapeBinderSupportType": "App::PropertyXLinkSubList" + }, + "roundtrip": { + "shapeBinderShapeNull": true, + "shapeBinderSupport": [], + "shapeBodyTip": null, + "sourceFaceArea": 30, + "sourceVolume": 120, + "subBodyTip": null, + "subShapeBinderArea": 30, + "subShapeBinderSourceDocument": "PartDesignExternalSource", + "subShapeBinderSupport": [ + { + "object": "ExternalBox", + "subElements": [ + "Face1" + ] + } + ] + } + }, + "edited": { + "bodyTips": { + "BinderBody": null, + "DatumBody": null, + "SourceBody": "SourceBox", + "SubBinderBody": null + }, + "datumPlaneAttachmentOffset": { + "angleDegrees": 45, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 7 + ] + }, + "datumPlanePlacement": { + "angleDegrees": 45.00000000000001, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 7 + ] + }, + "shapeBinderVolume": 672, + "sourceVolume": 672, + "subShapeBinderLength": 28 + }, + "freecadVersion": "1.1.1", + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "initial": { + "bodyGroups": { + "BinderBody": [ + "ShapeBinder" + ], + "DatumBody": [ + "DatumPlane", + "DatumLine", + "DatumPoint" + ], + "SourceBody": [ + "SourceBox" + ], + "SubBinderBody": [ + "SubShapeBinder" + ] + }, + "bodyTips": { + "BinderBody": null, + "DatumBody": null, + "SourceBody": "SourceBox", + "SubBinderBody": null + }, + "datumLine": { + "mapMode": "ObjectX", + "state": [ + "Up-to-date" + ], + "support": [ + { + "object": "XY_Plane", + "subElements": [] + } + ] + }, + "datumPlane": { + "attachmentOffset": { + "angleDegrees": 29.999999999999996, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 5 + ] + }, + "mapMode": "FlatFace", + "placement": { + "angleDegrees": 29.999999999999993, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 5 + ] + }, + "state": [ + "Up-to-date" + ], + "support": [ + { + "object": "XY_Plane", + "subElements": [] + } + ] + }, + "datumPoint": { + "attachmentOffset": { + "angleDegrees": 0, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 1, + 2, + 3 + ] + }, + "mapMode": "ObjectOrigin", + "placement": { + "angleDegrees": 0, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 1, + 2, + 3 + ] + }, + "state": [ + "Up-to-date" + ], + "support": [ + { + "object": "XY_Plane", + "subElements": [] + } + ] + }, + "shapeBinder": { + "state": [ + "Up-to-date" + ], + "support": [ + { + "object": "SourceBox", + "subElements": [] + } + ], + "traceSupport": true + }, + "shapeBinderVolume": 480, + "sourceVolume": 480, + "subShapeBinder": { + "state": [ + "Up-to-date" + ], + "support": [ + { + "object": "SourceBox", + "subElements": [ + "Edge1", + "Edge2", + "Edge3", + "Edge4" + ] + } + ] + }, + "subShapeBinderLength": 28 + }, + "invalidSupport": { + "state": [ + "Touched", + "Invalid" + ], + "status": "AttachEngine3D: subshape not found SourceBox.Face999", + "support": [ + { + "object": "SourceBox", + "subElements": [ + "Face999" + ] + } + ], + "typeId": "PartDesign::Plane" + }, + "roundtrip": { + "bodyGroups": { + "BinderBody": [ + "ShapeBinder" + ], + "DatumBody": [ + "DatumPlane", + "DatumLine", + "DatumPoint", + "InvalidDatumPlane" + ], + "SourceBody": [ + "SourceBox" + ], + "SubBinderBody": [ + "SubShapeBinder" + ] + }, + "bodyTips": { + "BinderBody": null, + "DatumBody": null, + "SourceBody": "SourceBox", + "SubBinderBody": null + }, + "datumPlaneAttachmentOffset": { + "angleDegrees": 45, + "axis": [ + 0, + 0, + 1 + ], + "position": [ + 0, + 0, + 7 + ] + }, + "datumPlaneMapMode": "FlatFace", + "datumPlaneSupport": [ + { + "object": "XY_Plane", + "subElements": [] + } + ], + "invalidDatumState": [ + "Touched", + "Invalid" + ], + "invalidDatumStatus": "AttachEngine3D: subshape not found SourceBox.Face999", + "shapeBinderSupport": [ + { + "object": "SourceBox", + "subElements": [] + } + ], + "shapeBinderVolume": 672, + "sourceVolume": 672, + "subShapeBinderLength": 28, + "subShapeBinderSupport": [ + { + "object": "SourceBox", + "subElements": [ + "Edge1", + "Edge2", + "Edge3", + "Edge4" + ] + } + ] + }, + "schemaVersion": 1, + "status": "pass", + "tolerance": 1e-7 +} diff --git a/config/freecad-private-naming-source-readiness.json b/config/freecad-private-naming-source-readiness.json new file mode 100644 index 0000000..f249488 --- /dev/null +++ b/config/freecad-private-naming-source-readiness.json @@ -0,0 +1,106 @@ +{ + "schemaVersion": 1, + "scope": "standalone-wasm-source-prerequisite", + "baseline": { + "freecadVersion": "1.1.1", + "sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "emscriptenVersion": "3.1.69", + "qtVersion": "6.8.2" + }, + "linkedOriginalSources": [ + { + "path": "src/App/IndexedName.cpp", + "sha256": "73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435" + }, + { + "path": "src/App/MappedName.cpp", + "sha256": "90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc" + }, + { + "path": "src/App/StringHasher.cpp", + "sha256": "fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c" + }, + { + "path": "src/App/MappedElement.cpp", + "sha256": "f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5" + }, + { + "path": "src/App/ElementNamingUtils.cpp", + "sha256": "fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3" + }, + { + "path": "src/App/ElementMap.cpp", + "sha256": "e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52" + }, + { + "path": "src/Base/Handle.cpp", + "sha256": "8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2" + } + ], + "runtimeAssertions": [ + "IndexedName parsing", + "MappedName postfix tag parsing", + "StringHasher duplicate identity", + "StringHasher SHA-1 threshold", + "StringHasher mapped indexed-name references", + "ElementMap StringHasher encoding", + "ElementMap bidirectional lookup", + "ElementMap history tracing", + "ElementMap save and restore", + "MappedElement stable ordering", + "seven-object wasm static archive linkage" + ], + "hostAdapterScope": [ + "Base persistence streams", + "Base type-system declarations", + "Python wrapper objects", + "logging", + "Application and Document lifecycle hooks" + ], + "isolatedSourceArchive": { + "path": "native/freecad-naming-probe/dist/libFreeCADPrivateNamingProbe.a", + "target": "wasm32-emscripten", + "expectedObjectMembers": 7, + "hostAdapterBound": true, + "productionEligible": false + }, + "notLinkedProductionComponents": [ + "FreeCADBase static library", + "FreeCADApp static library", + "Part static library", + "Python static library", + "real Application and Document integration", + "FreeCAD private naming Worker bridge" + ], + "candidateAbi": { + "scope": "isolated-non-production", + "exports": [ + "freecadNamingCandidateAbiVersion", + "freecadNamingCandidateCapabilitiesJson", + "freecadNamingCandidateEvidenceJson" + ], + "strictWebValidator": "pass", + "nativeResources": [ + "MappedNameRef", + "StringHasher", + "ElementMap2" + ], + "occtBuilderContext": false, + "publishToWorker": false + }, + "production": { + "workerArtifact": "native/occt-history/dist/bitbybit-occt-history.wasm", + "workerLinked": false, + "exportedCallbacks": [], + "requiredCallbacks": [ + "freecadNamingAbiVersion", + "freecadNamingCapabilitiesJson", + "freecadNamingEvidenceJson" + ] + }, + "boundary": { + "freecadNamingBuildStatus": "contract-only", + "exTsn02": "in_progress", + "systemExact": false + } +} diff --git a/config/freecad-wasm-sdk-bootstrap.cmake b/config/freecad-wasm-sdk-bootstrap.cmake new file mode 100644 index 0000000..5bd17d4 --- /dev/null +++ b/config/freecad-wasm-sdk-bootstrap.cmake @@ -0,0 +1,38 @@ +# Candidate-only cross-build helpers. Keep the locked FreeCAD source tree clean +# and forbid its fallback FetchContent path from reaching the network. +set(FETCHCONTENT_FULLY_DISCONNECTED ON CACHE BOOL "Disable network fallback" FORCE) +set(fmt_DIR "${CMAKE_CURRENT_LIST_DIR}/cmake/fmt" CACHE PATH "Candidate header-only fmt package" FORCE) + +# FreeCAD 1.1.1 has no dedicated Emscripten branch in FCConfig.h. Emscripten's +# POSIX layer follows the Linux code paths for this headless candidate. +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + if(NOT FREECAD_WASM_OCCT_COMPAT_INCLUDE + OR NOT EXISTS "${FREECAD_WASM_OCCT_COMPAT_INCLUDE}/TopTools_ListOfShape.hxx") + message(FATAL_ERROR + "FREECAD_WASM_OCCT_COMPAT_INCLUDE must point to OCCT's Deprecated/NCollectionAliases directory") + endif() + add_compile_definitions(__linux__=1) + include_directories(BEFORE "${CMAKE_CURRENT_LIST_DIR}/wasm-compat") + include_directories(BEFORE SYSTEM "${FREECAD_WASM_OCCT_COMPAT_INCLUDE}") + # FreeCAD's native UNIX branch adds --undefined,dynamic_lookup, which is + # incompatible with Emscripten's relocatable-object emulation of SHARED. + set(UNIX FALSE) + # Qt disables its timezone backend on WASM. FreeCAD calls systemTimeZone() + # only when formatting diagnostic timestamps, so use deterministic UTC. + add_compile_options( + "$<$:-include${CMAKE_CURRENT_LIST_DIR}/freecad-wasm-sdk-compat.h>" + "$<$:-Wno-deprecated-declarations>" + ) +endif() + +# The headless SDK does not ship Qt LinguistTools, while FreeCAD Base/App invoke +# qt_add_translation unconditionally. Keep their resource targets valid without +# pulling host-generated .qm files into the wasm candidate. +set(FREECAD_WASM_EMBED_TRANSLATIONS OFF CACHE BOOL "Embed FreeCAD translations in the candidate wasm SDK") +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten" + AND NOT FREECAD_WASM_EMBED_TRANSLATIONS + AND NOT COMMAND qt_add_translation) + function(qt_add_translation output_variable) + set(${output_variable} "" PARENT_SCOPE) + endfunction() +endif() diff --git a/config/freecad-wasm-sdk-build-plan.json b/config/freecad-wasm-sdk-build-plan.json new file mode 100644 index 0000000..e395a08 --- /dev/null +++ b/config/freecad-wasm-sdk-build-plan.json @@ -0,0 +1,112 @@ +{ + "schemaVersion": 1, + "scope": "candidate-only-freecad-wasm-sdk-build", + "baseline": { + "freecadVersion": "1.1.1", + "sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "emscriptenVersion": "3.1.69", + "qtVersion": "6.8.2", + "target": "wasm32-emscripten" + }, + "source": { + "path": ".cache/freecad/FreeCAD", + "mode": "detached-pinned-commit", + "requiredClean": true + }, + "sourceOverlay": { + "path": ".cache/toolchains/freecad-naming-sdk/source-occt8", + "command": "npm run prepare:freecad-occt8-overlay", + "migration": "OCCT 8 migrate_raise_to_throw.py and replace_typedefs.py", + "productionPublication": false + }, + "toolchain": { + "emscriptenRoot": "EMSDK", + "qtPrefix": ".cache/toolchains/qt6/install-wasm-freecad", + "cmakeGenerator": "Ninja" + }, + "dependencyBuilds": [ + { + "name": "Qt6", + "version": "6.8.2", + "command": "npm run build:qt6-freecad-wasm", + "targetArchives": ["QtCore", "QtConcurrent", "QtNetwork", "QtXml"] + }, + { + "name": "yaml-cpp", + "version": "0.8.0", + "sourceCommit": "f7320141120f720aecc4c32be25586e7da9eb978", + "command": "npm run build:yaml-cpp-wasm", + "targetArchives": ["yaml-cpp"] + }, + { + "name": "ICU", + "version": "68.2", + "sourceSha256": "c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625", + "command": "npm run build:icu-wasm", + "targetArchives": ["icuuc", "icui18n", "icudata"] + }, + { + "name": "CPython", + "version": "3.13.5", + "sourceSha256": "93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640", + "command": "npm run build:cpython-wasm", + "targetArchives": ["python3.13"] + }, + { + "name": "Xerces-C", + "version": "3.2.4", + "sourceSha256": "075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9", + "command": "npm run build:xerces-c-wasm", + "targetArchives": ["xerces-c"] + }, + { + "name": "Boost", + "version": "1.83.0", + "sourceSha256": "6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e", + "command": "npm run build:boost-wasm", + "targetArchives": ["boost_program_options", "boost_regex", "boost_thread", "boost_date_time", "boost_atomic"] + } + ], + "configuration": { + "buildDirectory": ".cache/toolchains/freecad-naming-sdk/build-wasm", + "installDirectory": ".cache/toolchains/freecad-naming-sdk/install-wasm", + "buildType": "Release", + "buildGui": false, + "developerTests": false, + "python": "required", + "modules": ["Part"], + "productionPublication": false + }, + "requiredArchives": [ + "FreeCADBase", + "FreeCADApp", + "Part", + "Python" + ], + "knownConfigurationLimits": [ + "Qt Core/Concurrent/Network/Xml wasm archives are built locally with thread support for the candidate SDK.", + "yaml-cpp 0.8.0 is built as a wasm static archive for the required Material module.", + "CPython 3.13.5 development headers and its pthread-enabled wasm static archive are staged for the candidate SDK.", + "Xerces-C 3.2.4 is built as a wasm static archive against the staged ICU libraries.", + "The headless candidate omits Base/App Qt translation resources because Qt LinguistTools is not part of the target SDK.", + "Qt disables its timezone backend on WebAssembly; the candidate formats FreeCAD diagnostic timestamps in deterministic UTC.", + "FreeCAD's disabled Linux backtrace block still includes execinfo.h; a candidate-only empty header satisfies that unused include without exposing backtrace APIs.", + "OCCT 8.0 keeps FreeCAD 1.1.1's TopTools collection typedefs under Deprecated/NCollectionAliases; the candidate includes that pinned source compatibility directory explicitly.", + "OCCT 8.0 removed Standard_*::Raise; a disposable FreeCAD worktree overlay applies the pinned OCCT migrate_raise_to_throw.py phase without modifying the locked source checkout.", + "OCCT 8.0 folds legacy iterator typedefs into collection headers; the disposable overlay maps six removed include-only iterator headers after the official typedef migration.", + "OCCT 8.0 removes Standard_Failure RTTI; the disposable overlay replaces exception-only DynamicType calls with the documented ExceptionType API.", + "OCCT 8.0 removes BRepLProp_CurveTool; its parameter forwarding calls are mapped to BRepAdaptor_Curve's inherited FirstParameter and LastParameter methods.", + "OCCT 8.0 consolidates Geom2dLProp into GeomLProp; the overlay maps Geom2dLProp_CLProps2d to the signature-compatible GeomLProp_CLProps2d specialization.", + "OCCT 8.0 adds NCollection_List initializer-list construction; three ShapeMapper calls explicitly retain FreeCAD's original std::vector overload.", + "OCCT 8.0 makes Standard_Failure a std::exception; three identical trailing handlers are removed after their std::exception handler to avoid unreachable exception paths.", + "FreeCAD declares Base/App/Part as SHARED; the candidate toolchain archives their wasm objects with emar and links dependencies separately.", + "The upstream FreeCAD CMake project currently creates shared libraries and requires a full wasm dependency graph; this plan does not treat a host build as a wasm SDK." + ], + "boundary": { + "freecadNamingBuildStatus": "contract-only", + "exTsn02": "in_progress", + "systemExact": false, + "workerLinked": false, + "callbacks": [] + } +} diff --git a/config/freecad-wasm-sdk-compat.h b/config/freecad-wasm-sdk-compat.h new file mode 100644 index 0000000..44ded94 --- /dev/null +++ b/config/freecad-wasm-sdk-compat.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __EMSCRIPTEN__ +#if __has_include() +#include +#include +#endif + +// QTimeZone::UTC remains available when Qt's WASM timezone backend is off. +#define systemTimeZone() UTC +#endif diff --git a/config/freecad-web-exact-parity-plan.json b/config/freecad-web-exact-parity-plan.json index cac6131..b988f61 100644 --- a/config/freecad-web-exact-parity-plan.json +++ b/config/freecad-web-exact-parity-plan.json @@ -56,7 +56,7 @@ { "id": "EX-SK-02", "title": "Match Sketcher editing tools, autoconstraints, virtual space and UI lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-SK-01", "EX-UI-03"], "deliverables": ["Pointer and keyboard editor oracle", "All editing tools", "Task and focus lifecycle"], "acceptance": ["Every tool supports success, failure, cancel, undo and redo with matching selection"], "evidence": ["check:freecad-sketcher-editor", "check:chrome-sketcher-bspline"], "exactBlockedBy": ["The full GUI tool and focus matrix is incomplete"] }, { "id": "EX-PART-01", "title": "Complete all Part primitives, builders, booleans, healing and inspection", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01", "EX-TSN-04"], "deliverables": ["All Part commands and parameters", "Healing and tolerance tools", "Native history for local operations"], "acceptance": ["Success and failure outputs match native Shape, history and diagnostics"], "evidence": ["check:chrome-part-primitives", "check:freecad-golden-fixtures"], "exactBlockedBy": ["Current golden corpus does not enumerate every Part command and option"] }, { "id": "EX-PD-01", "title": "Complete all PartDesign features, parameters and additive/subtractive combinations", "priority": "P0", "status": "in_progress", "dependencies": ["EX-PART-01", "EX-TSN-04"], "deliverables": ["Complete PartDesign feature matrix", "Body Tip and feature-list semantics", "Per-feature native history and naming"], "acceptance": ["Every feature edit, failure recovery and save-reopen matches FreeCAD"], "evidence": ["check:partdesign-closure", "check:freecad-sketcher-partdesign-abi", "check:chrome-partdesign-lifecycle"], "exactBlockedBy": ["The parameter contract covers the supported Facade subset, not every registered PartDesign TypeId or cross-product"] }, - { "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Complete datum/binder and multi-body oracle is absent"] } + { "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip", "check:freecad-partdesign-structure", "check:freecad-attachment-modes"], "exactBlockedBy": ["Native same-document, first cross-document SubShapeBinder cases, and the 55-mode registry/engine partition pass; executable geometry for every implemented mode/reference combination, other XLink modes, relink recovery and Web Chrome replay remain incomplete"] } ] }, { diff --git a/config/offline-resources.json b/config/offline-resources.json index f8d59d5..bf06750 100644 --- a/config/offline-resources.json +++ b/config/offline-resources.json @@ -167,6 +167,126 @@ "libraryPath": "build-caches/qt6-wasm-core-build.tar.zst", "required": false }, + { + "id": "qt6-wasm-freecad-sdk", + "source": ".cache/toolchains/qt6/install-wasm-freecad", + "restore": ".cache/toolchains/qt6/install-wasm-freecad", + "libraryPath": "toolchains/qt6-wasm-freecad-6.8.2.tar.zst", + "required": true + }, + { + "id": "qt6-wasm-freecad-build", + "source": ".cache/toolchains/qt6/build-wasm-freecad", + "restore": ".cache/toolchains/qt6/build-wasm-freecad", + "libraryPath": "build-caches/qt6-wasm-freecad-build.tar.zst", + "required": false + }, + { + "id": "yaml-cpp-source", + "source": ".cache/toolchains/yaml-cpp/src", + "restore": ".cache/toolchains/yaml-cpp/src", + "libraryPath": "sources/yaml-cpp-0.8.0-f7320141.tar.zst", + "revision": "f7320141120f720aecc4c32be25586e7da9eb978", + "required": true + }, + { + "id": "yaml-cpp-wasm-sdk", + "source": ".cache/toolchains/yaml-cpp/install-wasm", + "restore": ".cache/toolchains/yaml-cpp/install-wasm", + "libraryPath": "toolchains/yaml-cpp-wasm-0.8.0.tar.zst", + "required": true + }, + { + "id": "yaml-cpp-wasm-build", + "source": ".cache/toolchains/yaml-cpp/build-wasm", + "restore": ".cache/toolchains/yaml-cpp/build-wasm", + "libraryPath": "build-caches/yaml-cpp-wasm-build.tar.zst", + "required": false + }, + { + "id": "icu-68.2-download", + "source": ".cache/toolchains/icu/downloads", + "restore": ".cache/toolchains/icu/downloads", + "libraryPath": "sources/icu-68.2-download.tar.zst", + "required": true + }, + { + "id": "icu-wasm-sdk", + "source": ".cache/toolchains/icu/install-wasm", + "restore": ".cache/toolchains/icu/install-wasm", + "libraryPath": "toolchains/icu-wasm-68.2.tar.zst", + "required": true + }, + { + "id": "emscripten-freecad-cache", + "source": ".cache/toolchains/emscripten-freecad-cache", + "restore": ".cache/toolchains/emscripten-freecad-cache", + "libraryPath": "build-caches/emscripten-freecad-cache.tar.zst", + "required": false + }, + { + "id": "cpython-3.13.5-download", + "source": ".cache/toolchains/python/downloads", + "restore": ".cache/toolchains/python/downloads", + "libraryPath": "sources/cpython-3.13.5-download.tar.zst", + "required": true + }, + { + "id": "cpython-wasm-sdk", + "source": ".cache/toolchains/python/install-wasm", + "restore": ".cache/toolchains/python/install-wasm", + "libraryPath": "toolchains/cpython-wasm-3.13.5.tar.zst", + "required": true + }, + { + "id": "cpython-wasm-build", + "source": ".cache/toolchains/python/build-wasm", + "restore": ".cache/toolchains/python/build-wasm", + "libraryPath": "build-caches/cpython-wasm-build.tar.zst", + "required": false + }, + { + "id": "xerces-c-3.2.4-download", + "source": ".cache/toolchains/xerces-c/downloads", + "restore": ".cache/toolchains/xerces-c/downloads", + "libraryPath": "sources/xerces-c-3.2.4-download.tar.zst", + "required": true + }, + { + "id": "xerces-c-wasm-sdk", + "source": ".cache/toolchains/xerces-c/install-wasm", + "restore": ".cache/toolchains/xerces-c/install-wasm", + "libraryPath": "toolchains/xerces-c-wasm-3.2.4.tar.zst", + "required": true + }, + { + "id": "xerces-c-wasm-build", + "source": ".cache/toolchains/xerces-c/build-wasm", + "restore": ".cache/toolchains/xerces-c/build-wasm", + "libraryPath": "build-caches/xerces-c-wasm-build.tar.zst", + "required": false + }, + { + "id": "boost-1.83.0-download", + "source": ".cache/toolchains/boost/downloads", + "restore": ".cache/toolchains/boost/downloads", + "libraryPath": "sources/boost-1.83.0-download.tar.zst", + "required": true + }, + { + "id": "boost-wasm-sdk", + "source": ".cache/toolchains/boost/install-wasm", + "restore": ".cache/toolchains/boost/install-wasm", + "libraryPath": "toolchains/boost-wasm-1.83.0.tar.zst", + "required": true + }, + { + "id": "boost-wasm-build", + "source": ".cache/toolchains/boost/src", + "restore": ".cache/toolchains/boost/src", + "libraryPath": "build-caches/boost-wasm-build.tar.zst", + "required": false + }, { "id": "opencamlib-build", "source": ".cache/opencamlib-wasm", @@ -210,6 +330,15 @@ "restore": ".cache/offline-sysroot/include/eigen3", "required": true }, + { + "id": "fmt-headers", + "sourceCandidates": [ + "/usr/include/fmt" + ], + "libraryPath": "system/headers/fmt-10.1.1", + "restore": ".cache/offline-sysroot/fmt-10.1.1/include/fmt", + "required": true + }, { "id": "debian-package-cache", "sourceCandidates": [ diff --git a/config/wasm-compat/execinfo.h b/config/wasm-compat/execinfo.h new file mode 100644 index 0000000..f4d302d --- /dev/null +++ b/config/wasm-compat/execinfo.h @@ -0,0 +1,7 @@ +#pragma once + +// FreeCAD includes this header on its Linux path even when its configure +// check has disabled HAVE_BACKTRACE_SYMBOLS. No backtrace API is declared. +#if !defined(__EMSCRIPTEN__) +#error "This compatibility header is only valid for the Emscripten candidate" +#endif diff --git a/docs/continuation-status.zh-CN.md b/docs/continuation-status.zh-CN.md index e4449bc..81b1a03 100644 --- a/docs/continuation-status.zh-CN.md +++ b/docs/continuation-status.zh-CN.md @@ -629,7 +629,7 @@ FCStd writer 现在把 Web Sketch 的 `Support`、`MapMode`、`AttachmentOffset` 锁定 FreeCAD 1.1.1 已实际打开、重算并另存 Web 生成的 `ExternalBox + Sketch`:`MapMode=FlatFace`、`AttachmentSupport=ExternalBox.Face1`、AttachmentOffset Z=0.25 mm,同时保留 10 个内部几何、11 个约束、1 个外部 Edge1 投影和外部 `PointOnObject` 约束。回存 FCStd 由 Web inspector 解码原生字段,并从隐藏元数据精确恢复 Face1 的 topologyVersion=3、generation=2 和 signature。Facade 回归升至 140/140,桌面 oracle 与生产构建通过。 -该增量关闭当前六种 Web Attachment 模式的 FCStd 边界证据,不宣称完整 FreeCAD AttachEngine 的 56 种模式、多引用 ThreePoints/Intersection 支撑、MapReversed/MapPathParameter 或局部 Attachment 几何执行全部等价。FC-06 仍受 Face 多投影、高级 Sketch 约束和批量 FCStd 黄金约束,83 项计划计数保持 30 completed、21 in_progress、32 pending、0 blocked。 +该增量关闭当前六种 Web Attachment 模式的 FCStd 边界证据,不宣称完整 FreeCAD AttachEngine 的 55 种持久化模式、多引用 ThreePoints/Intersection 支撑、MapReversed/MapPathParameter 或局部 Attachment 几何执行全部等价。FC-06 仍受 Face 多投影、高级 Sketch 约束和批量 FCStd 黄金约束,83 项计划计数保持 30 completed、21 in_progress、32 pending、0 blocked。 ## 82. 2026-08-04 FC-06 Sketcher SnellsLaw/Weight 原生往返增量 @@ -1790,6 +1790,52 @@ Sketcher 数据模型和 FCStd codec 已覆盖 FreeCAD 1.1.1 的完整原生几 QtBase 6.8.2 源码包已通过 USTC 国内镜像取得,并用 Qt 发布包官方 SHA-256 `012043ce6d411e6e8a91fdc4e05e6bedcfa10fcb1347d3c33908f7fdd10dfe05` 校验。新增 `build:qt6-wasm-core`,固定 Emscripten 3.1.69,构建静态 wasm32 `Qt6Core`、bundled Pcre2 和 zlib;离线模式只使用本机缓存,缺少已校验源码包时立即失败。源码包、展开源码、可用 SDK 和可选增量构建目录均已纳入项目离线资源清单。 -新增的 `build:freecad-naming-source-probe` 并未假装提供生产命名实现。它固定 FreeCAD 1.1.1 提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`,同时校验 `IndexedName.cpp` 与 `MappedName.cpp` 的逐文件 SHA-256,再把这两个原始编译单元与 wasm Qt6Core 编译为独立探针。运行时验证 IndexedName 解析、MappedName 追加和 tag 读取;探针明确检查不导出 `freecadNamingAbiVersion`、`freecadNamingCapabilitiesJson`、`freecadNamingEvidenceJson`。这证明的是锁定基础源码与工具链可交叉编译,不包含完整 StringHasher、FreeCADApp/Part 静态库或生产 Worker bridge。 +新增的 `build:freecad-naming-source-probe` 并未假装提供生产命名实现。它固定 FreeCAD 1.1.1 提交 `0108fd4b4850cc46e625b60e53cea7a7bbe69f8d`,同时校验 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp` 与 `Base/Handle.cpp` 的逐文件 SHA-256,再把四个原始编译单元与 wasm Qt6Core 编译为独立探针。运行时验证 IndexedName 解析、MappedName 追加/tag 读取、StringHasher 去重、SHA-1 阈值及索引映射名称引用;探针明确检查不导出 `freecadNamingAbiVersion`、`freecadNamingCapabilitiesJson`、`freecadNamingEvidenceJson`。这证明的是锁定 StringHasher 核心源码与工具链可交叉编译和运行,不包含 `ElementMap.cpp`、`MappedElement.cpp`、FreeCADApp/Part 静态库或生产 Worker bridge。 新增 `check:freecad-private-naming-boundary`,跨 ABI 合同、兼容性矩阵和 exact 计划校验同一状态,并接入默认 `verify`、真实 wasm lane 和离线 smoke。准确边界保持不变:随附 OCCT Worker 仍没有链接 FreeCAD 私有 C++ 命名实现,制品仍无三项私有命名回调;`shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false`。本批只完成可离线复现的前置构建和防止状态误提升的机器门禁。 + +## 231. 2026-08-12 ElementMap 原始源码 WASM 探针与隔离 ABI 候选 + +私有命名源码探针现已锁定并直接编译 FreeCAD 1.1.1 的 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp`、`MappedElement.cpp`、`ElementNamingUtils.cpp`、`ElementMap.cpp` 与 `Base/Handle.cpp` 七个原始编译单元。独立 WASM 实跑覆盖 StringHasher 去重/SHA-1 阈值/索引引用、ElementMap StringHasher 编码、双向查找、历史回溯、保存恢复、MappedElement 排序和 DocumentObject tag;Application/Document 生命周期、持久化流、Python 包装、类型系统和日志只在探针中使用最小宿主适配。 + +探针现在从真实 `ElementMap::save` 和 `StringHasher::getIDMap` 生成原生资源。ElementMap 保存体按 FreeCAD `ComplexGeoData` 外层协议补入 `BeginElementMap v1` 后,由 Web `parseElementMap2` 读得 `MapCount=1`、4 个名称和 1 个 token;StringHasher schema v2 读得 1 个条目,引用闭包无缺失。三个刻意隔离命名的 `freecadNamingCandidate*` 回调把这些资源送入现有 `probeFreeCadPrivateNamingAbi` 与 `captureFreeCadPrivateNamingEvidence`,严格 Web ABI 校验通过。候选只接受探针 `cut` 协议载荷,没有 OCCT builder context,也不会发布到生产 Worker。 + +`config/freecad-private-naming-source-readiness.json` 与 `check:freecad-private-naming-boundary` 已把真实源码、运行时断言、宿主适配、隔离候选和生产缺口分别建模;检查器拒绝候选使用生产回调名,并继续要求生产导出列表为空。真实 WASM lane 与离线 smoke 都会重建和执行源码探针及候选 ABI 测试,不允许只读取历史结果。 + +准确边界保持不变:`native/occt-history` 生产制品未链接探针,也未链接 FreeCADApp/Part/Python 静态库、真实 Application/Document 集成或生产 bridge,三项生产 Embind 命名回调仍不存在。因此 `shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false` 全部保持不变。下一步是生成可审计的 wasm FreeCADApp/Part/Python 静态 SDK,将真实 OCCT builder 阶段历史接入生产 bridge,再对每类 builder 执行命名证据和 FCStd 往返门禁。 + +## 232. 2026-08-12 FreeCAD 命名 WASM SDK 差距门与静态归档形态 + +七个已锁定的 FreeCAD 私有命名编译单元不再直接混入探针最终链接命令,而是分别生成 wasm object,再由 `emar` 封装为隔离的 `libFreeCADPrivateNamingProbe.a`。烟测解析真实 `ar` 成员并确认归档为 900882 bytes、7 个成员、7 个 wasm object、0 个 LLVM bitcode;主探针只从该归档解析命名符号后,StringHasher、ElementMap、MappedNameRef 与 Web ABI 全部断言继续通过。该归档仍使用 Persistence/Python/Application/Document 等宿主适配,机器清单固定 `hostAdapterBound=true`、`productionEligible=false`,不冒充 FreeCADBase/FreeCADApp。 + +新增 `config/freecad-naming-sdk-plan.json`、`check:freecad-naming-sdk-readiness` 和 `generate:freecad-naming-sdk-manifest`。就绪检查直接解析静态 `ar`,接受 wasm relocatable object/LLVM bitcode,明确拒绝宿主 ELF;当前本机验证 `libQt6Core.a` 为 219 个 wasm object,并验证其 Bundled Pcre2 27 个、Bundled Zlib 15 个实际链接依赖。FreeCAD 私有头与 QtCore 头齐全,但核心五库仅 `QtCore=1/5`,仍缺 FreeCADBase、FreeCADApp、Part、Python 和 production bridge。生成器在任何缺项下都拒绝写 manifest,完整 SDK 检查也复用相同归档目标校验,不允许仅靠文件非空和 SHA-256 混入 x86 库。 + +readiness 已进入默认 `verify`、真实 WASM 重建 lane 与离线 smoke;候选 SDK 目录要等首个真实 FreeCADBase/Python wasm 归档生成后再加入离线资源库,当前不制造空资源记录。准确边界不变:生产 OCCT Worker 未使用上述探针归档,`availability=unavailable`、`callbacks=[]`;`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false`。下一步仍是获取或构建锁定 Emscripten 3.1.69 的 FreeCADBase/FreeCADApp/Part/Python 静态库,实现真实 Document/OCCT builder bridge 后再做 production artifact probe。 + +## 233. 2026-08-13 完整候选 SDK、真实私有命名桥与隔离 Worker + +候选 SDK 已从差距门推进为完整可校验闭包。FreeCADBase、FreeCADApp、Part、QtCore 和 CPython 3.13 五个核心 wasm 静态库全部通过 `ar` 成员目标检查;21 个链接依赖包含 Qt/ICU/Xerces/Boost/yaml-cpp 以及 CPython 实际拉入的 mpdecimal、expat、HACL SHA2、zlib、bzip2、sqlite3。CPython 构建固定 `SOURCE_DATE_EPOCH`,显式生成 `pybuilddir.txt` 和 wasm stdlib;连续两次构建得到相同的 `libpython3.13.a` 与 `python313.zip` 哈希。manifest 现在同时哈希 bridge、host adapter、force-include 头和 Python 运行时文件,并固定 C++20、pthread 和四项兼容宏,缺任一项都拒绝生成或加载。 + +新增真实隔离 bridge,不再依赖探针 shims。它初始化 FreeCAD `App::Application`,直接调用锁定 1.1.1 的 `ElementMap::encodeElementName/setElementName/beforeSave/save`、`MappedName` 和 `StringHasher`,导出三项版本化 ABI。Node 实跑验证首阶段 postfix、链式 `$#id:index`/3 项 StringHasher 闭包,以及缺 history 的结构化 fail-closed;JS/WASM/DATA 的哈希证据写入本机候选报告,且明确 `productionPublication=false`。 + +SDK-05 又建立了独立 pthread OCCT 构建和 `.cache/candidates/freecad-naming-worker` 候选模块。Node 与 Chrome 均在同一个 wasm 中先执行 OCCT 8.0.0 cut history,再调用 FreeCAD 私有命名 ABI;Chrome 在 COOP/COEP 隔离环境中验证三回调、链式 StringHasher 闭包和无 history 拒绝。执行命令与只读报告检查已拆分,检查器会重新计算当前 JS/WASM/DATA 哈希,静态旧报告不能替代真实 harness。本批 WBS 为 SDK-01/02A/02B/03/04/05 completed,PAR-01 in_progress。 + +候选桥随后补齐非空 StringHasher 的下一阶段恢复。它只接受 schema/nativeVersion 正确、ID 连续有序、依赖只指向既有 ID、能够由 FreeCAD 公共 `StringHasher` API 逐字段重建的表;二进制/单向哈希、缺表、ID 篡改和多输入不一致表继续 fail-closed。隔离桥、合并 Worker 和真实 Chrome 都执行三阶段链,条目数为 `0→3→5`;隔离负例确认篡改 ID 与不一致表均被拒绝。该能力仍仅存在于候选目录,未发布到生产 Worker。 + +准确生产边界没有改变:候选构建强制 `OCCT_HISTORY_PUBLISH=0`,没有覆盖仓库随附的 `native/occt-history` 或 `public/native/occt-history`。因此生产 `availability=unavailable`、`callbacks=[]`、`shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress`、`systemExact=false` 继续成立。下一项是 PAR-01:扩展 Datum/Attachment/ShapeBinder、多 Body、完整参数 mutation/编辑恢复/FCStd 往返,并在覆盖每类 builder 后才评估生产发布。 + +## 234. 2026-08-13 PAR-01 首批 Datum/Binder/多 Body 原生结构 oracle + +新增 `freecad-partdesign-structure-oracle`,直接在锁定 FreeCAD 1.1.1 中创建四个 Body、Plane/Line/Point 三类 Datum、ShapeBinder 与 SubShapeBinder。初始证据锁定 SourceBody.Tip=SourceBox,而只包含 Datum 或 Binder 的三个 Body.Tip 均为 null;这些非实体对象不会错误接管 Tip。DatumPlane 使用 XY_Plane/FlatFace 与 Z=5、绕 Z 30° 的 AttachmentOffset,DatumLine/Point 分别使用 ObjectX/ObjectOrigin;四个 Body 的 Group 顺序均被采集。 + +源 AdditiveBox 的 Length 编辑使体积 `480→672`,跨 Body ShapeBinder 同步 `480→672`;选定 Edge1..4 的 SubShapeBinder 长度保持 28。DatumPlane offset 编辑为 Z=7/45° 后,其实际 Placement 同步更新。另建 `Face999` 支撑的 DatumPlane,FreeCAD 明确返回 `Touched + Invalid` 与 `AttachEngine3D: subshape not found SourceBox.Face999`,没有把坏引用静默重绑。 + +该文档真实保存为 FCStd、关闭、重新打开并重算;四个 Body 的 Group/Tip、Datum 支撑/MapMode/Offset、两类 Binder 引用与形状、无效支持诊断全部保持。执行与检查命令已分离为 `probe:freecad-partdesign-structure` 和 `check:freecad-partdesign-structure`,并纳入真实 oracle lane 和默认 Sketcher/PartDesign 门禁。 + +同一 oracle 又增加双 FCStd 跨文档场景。原生 `ShapeBinder.Support` 虽报告为 `App::PropertyLinkSubListGlobal`,但直接赋外部文档对象仍明确返回 `PropertyLinkSubList does not support external object`;`SubShapeBinder.Support` 为 `App::PropertyXLinkSubList`,在源/消费文档均已落盘后接受 `ExternalBox.Face1`。关闭并按源→消费顺序重开后 XLink 仍指向源文档,源 Width 编辑使 Face1 与 SubShapeBinder 面积同步 `30→48`。删除源对象时 FreeCAD 清空 XLink,但保留面积 48 的最后形状缓存,且状态仍为 `Up-to-date/Valid`;检查器锁定了这一非直觉生命周期语义。EX-PD-02 仍为 pending:完整 Attachment 组合、其他 XLink/重新链接恢复与 Web Chrome 生命周期尚未闭合。 + +## 235. 2026-08-13 PAR-01 Attachment 注册表与引擎分区 oracle + +新增独立 `freecad-attachment-mode-oracle`,从锁定 FreeCAD 1.1.1 运行时读取 `MapMode` 的永久枚举顺序和 `Attacher` 元数据。真实注册表为 55 项;Plane 与 Sketch 的 `AttachEnginePlane` 各实现 23 项,Line 的 `AttachEngineLine` 实现 18 项,Point 的 `AttachEnginePoint` 实现 9 项,实现并集为 50 项。`Deactivated` 是关闭状态,`TangentU`、`TangentV`、`IntersectionPoint`、`MidPoint` 未出现在这四类引擎的实现集合。报告为每个实现模式保留永久索引和完整 `ReferenceCombinations`,从而将“枚举可写”与“当前引擎实际实现”分开。 + +代表性执行场景覆盖 Plane/Sketch 的 `FlatFace`、Line 的 `TwoPointLine` 和 Point 的 `Vertex`,并锁定引用类型建议、Placement、状态与 `positionBySupport`。源 Box 高度从 6 改为 9 后,Vertex Point 的 Z 同步 `6→9`;FCStd 关闭重开后保持。反向案例证明 Plane 写入 `Vertex`、Line/Point 写入 `FlatFace` 虽能保留枚举,却都进入 `Touched + Invalid` 并返回 `Attachment mode ... is not implemented.`,且失败状态同样通过 FCStd 往返。新执行/检查命令已进入真实 oracle lane、Sketcher/PartDesign 聚合门禁及 PAR-01 证据。EX-PD-02 仍保持 pending:尚需为 50 个实现槽位的全部引用组合构造可执行几何,并完成其他 XLink、重新链接恢复和 Web Chrome 回放。 diff --git a/docs/freecad-full-parity-plan.zh-CN.md b/docs/freecad-full-parity-plan.zh-CN.md index df0bb9c..b6408d7 100644 --- a/docs/freecad-full-parity-plan.zh-CN.md +++ b/docs/freecad-full-parity-plan.zh-CN.md @@ -393,10 +393,14 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片:Facade 验 ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAG;object tag 会从 `Document.xml` 的 `Object.id` 导入,按稳定值写回 FCStd,并通过 SQLite schema v7、OPFS 重开和 checkpoint 往返。`build:freecad-naming-worker` 是独立的 fail-closed 构建入口,只接受锁定提交、Emscripten 3.1.69、wasm Qt/Python、FreeCADBase/FreeCADApp/Part 静态库及三回调 bridge 的哈希清单;普通 `build:occt-history` 继续是 OCCT-only。`check:occt-history-artifact` 会实际实例化 WASM 并探测三回调,当前结果为 unavailable,因此 `EX-TSN-02=in_progress` 与 `systemExact=false` 保持不变。 -Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256;离线模式缺包时直接失败,不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp`、`MappedName.cpp` 两个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。该探针使用最小编译适配头,不包含完整 `StringHasher`、FreeCADApp/Part 静态库或生产 bridge,也明确禁止导出三项生产命名回调,因此只消除工具链和基础源码可编译性风险,不构成 `FreeCADPrivateNamingABI` 的生产实现。 +Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256;离线模式缺包时直接失败,不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp`、`MappedName.cpp`、`StringHasher.cpp`、`MappedElement.cpp`、`ElementNamingUtils.cpp`、`ElementMap.cpp`、`Base/Handle.cpp` 七个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。运行时覆盖名称解析、StringHasher 去重/SHA-1 阈值/索引引用、ElementMap 编码/双向查找/历史/保存恢复以及 MappedElement 稳定排序;真实 ElementMap 保存体和 StringHasher 表还会经过 Web 侧 ElementMap2 parser、引用闭包及严格 `FreeCADPrivateNamingABI` 校验。探针只导出隔离的 `freecadNamingCandidate*` 名称,持久化流、Python 包装、类型系统、日志和 Application/Document 生命周期仍由适配头提供;FreeCADApp/Part/Python 静态库、真实文档集成、OCCT builder 上下文和生产 bridge 均未链接。因此它证明的是锁定私有源码子集与 ABI 数据可交叉编译/运行,不构成生产命名实现。 `check:freecad-private-naming-boundary` 同时读取 ABI 合同、兼容性矩阵和 exact 计划,固定校验 `shippedWorkerImplementation=not-linked`、`freecadNamingBuild.status=contract-only`、`EX-TSN-02=in_progress` 与 `systemExact=false`;该检查已进入默认 `verify`、真实 wasm lane 和离线 smoke。只有生产 OCCT Worker 实际链接锁定的 FreeCAD 私有静态库/bridge,且制品探针取得三项回调和完整命名证据后,才能在后续批次修改这些状态。 +生产 SDK 前置已有完整候选闭包。七个锁定命名编译单元仍保留 `libFreeCADPrivateNamingProbe.a` 作为源码先决条件;实际 bridge 则链接真实 FreeCADBase/FreeCADApp/Part、QtCore、CPython 及 21 个传递依赖,并初始化真实 `App::Application`。`check:freecad-naming-sdk-readiness` 解析所有静态 `ar` 成员并拒绝宿主 ELF;`generate:freecad-naming-sdk-manifest` 还锁定 C++20、pthread、兼容头、host adapter 和预加载的 Python wasm stdlib。完整 `check:freecad-naming-sdk` 对库、源码和运行时文件逐项复算 SHA-256。 + +隔离 bridge 的 Node 烟测已用真实 `ElementMap/MappedName/StringHasher` 生成首阶段和链式证据,并验证 `$#id:index` 与 StringHasher 闭包。独立 pthread OCCT 候选模块进一步在 Node 和真实 Chrome 中执行 OCCT cut history,再从同一 wasm 调用三项 FreeCAD 私有命名回调;Chrome 执行与报告检查分开,检查器会拒绝与当前 JS/WASM/DATA 哈希不一致的旧报告。该候选强制写入 `.cache/candidates` 且 `OCCT_HISTORY_PUBLISH=0`,没有替换仓库随附生产 Worker。因此候选 SDK/Worker 验证完成不等于生产链接完成,`shippedWorkerImplementation=not-linked`、`EX-TSN-02=in_progress` 与 `systemExact=false` 仍保持不变。 + Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同:Sketcher 覆盖 FreeCAD 1.1.1 FCStd 表面的 9 类 geometry、19 类 constraint 和 10 类 InternalAlignment 语义;PartDesign 覆盖 Facade 已支持的 21 个 family、202 个属性槽和 72 个语义分区,并在对象创建及属性编辑时执行跨字段校验。`check:freecad-sketcher-partdesign-abi` 会从实现重算这些计数并拒绝陈旧报告;该“完整”限定于 supported Web Facade,不包含尚未实现的 FreeCAD PartDesign TypeId,也不表示 basic TypeScript solver 已求解全部圆锥曲线约束。 逐 feature 等级目前由 `config/compatibility-matrix.json` 的 `facadeCapabilities.geometry.featureLevels` 管理:Pad/Pocket/Revolution/Groove/Boolean 为有 native history 子集的 `compatible`,Fillet/Chamfer/MultiTransform 保持 `experimental`。`systemExactEvaluation.exact` 必须在所有 feature 的阶段 Shape/history、私有 token 证据和同构来源都闭合后才允许改为 true;当前值固定为 false,阻断项为私有 FreeCAD token 算法、没有原生 builder stage、以及无唯一同构来源。 diff --git a/native/freecad-naming-bridge/freecad_naming_bridge.cpp b/native/freecad-naming-bridge/freecad_naming_bridge.cpp new file mode 100644 index 0000000..3f9b80f --- /dev/null +++ b/native/freecad-naming-bridge/freecad_naming_bridge.cpp @@ -0,0 +1,783 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#include + +extern "C" ssize_t readlink(const char* path, char* buffer, size_t size) +{ + constexpr const char* executable = "/freecad/bin/freecad-naming-bridge"; + if (std::strcmp(path, "/proc/self/exe") != 0) { + return -1; + } + const size_t length = std::strlen(executable); + if (size < length) { + return -1; + } + std::memcpy(buffer, executable, length); + return static_cast(length); +} + +extern "C" char* realpath(const char* path, char* resolvedPath) +{ + static constexpr const char* allowed[] = { + "/freecad-user", + "/freecad-user/config", + "/freecad-user/data", + "/freecad-user/cache", + "/freecad-user/temp", + }; + if (!path || std::none_of(std::begin(allowed), std::end(allowed), [path](const char* entry) { + return std::strcmp(path, entry) == 0; + })) { + return nullptr; + } + const size_t length = std::strlen(path); + if (length >= PATH_MAX) { + return nullptr; + } + char* output = resolvedPath ? resolvedPath : static_cast(std::malloc(length + 1)); + if (!output) { + return nullptr; + } + std::memcpy(output, path, length + 1); + return output; +} + +extern "C" int getpwuid_r(uid_t uid, + struct passwd* password, + char* buffer, + size_t bufferSize, + struct passwd** result) +{ + constexpr const char identity[] = "freecad\0/freecad-user\0/bin/false"; + if (bufferSize < sizeof(identity)) { + *result = nullptr; + return -1; + } + std::memcpy(buffer, identity, sizeof(identity)); + password->pw_name = buffer; + password->pw_passwd = const_cast(""); + password->pw_uid = uid; + password->pw_gid = getgid(); + password->pw_gecos = buffer; + password->pw_dir = buffer + sizeof("freecad"); + password->pw_shell = password->pw_dir + sizeof("/freecad-user"); + *result = password; + return 0; +} + +#endif + +namespace +{ +constexpr int ABI_VERSION = 1; +constexpr const char* FREECAD_VERSION = "1.1.1"; +constexpr const char* FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"; + +void ensureApplication() +{ + static bool initialized = false; + if (initialized) { + return; + } + char executable[] = "freecad-naming-bridge"; + char console[] = "--console"; + char* argv[] = {executable, console}; + App::Application::init(2, argv); + initialized = true; +} + +QJsonObject requireObject(const QJsonObject& object, const char* key) +{ + const QJsonValue value = object.value(key); + if (!value.isObject()) { + throw std::runtime_error(std::string("FreeCAD naming request requires object ") + key); + } + return value.toObject(); +} + +QString requireString(const QJsonObject& object, const char* key) +{ + const QJsonValue value = object.value(key); + if (!value.isString() || value.toString().isEmpty()) { + throw std::runtime_error(std::string("FreeCAD naming request requires string ") + key); + } + return value.toString(); +} + +long requirePositiveTag(const QJsonObject& object, const char* key) +{ + const double value = object.value(key).toDouble(-1.0); + if (!std::isfinite(value) || value <= 0.0 || std::floor(value) != value + || value > static_cast(0x7fffffff)) { + throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key); + } + return static_cast(value); +} + +int requireIndex(const QJsonObject& object, const char* key) +{ + const double value = object.value(key).toDouble(-1.0); + if (!std::isfinite(value) || value < 0.0 || std::floor(value) != value + || value > static_cast(0x7ffffffe)) { + throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ") + + key); + } + return static_cast(value); +} + +QString titleKind(const QString& value) +{ + if (value == "face") { + return "Face"; + } + if (value == "edge") { + return "Edge"; + } + if (value == "vertex") { + return "Vertex"; + } + throw std::runtime_error("FreeCAD naming history supports only face, edge, and vertex"); +} + +QString canonicalOperation(const QString& operation) +{ + QString output; + output.reserve(operation.size()); + for (const QChar character : operation) { + if (character.isLetterOrNumber()) { + output.append(character.toUpper()); + } + else if (character == '-') { + output.append('_'); + } + else { + throw std::runtime_error("FreeCAD naming operation contains an unsupported character"); + } + } + if (output.isEmpty()) { + throw std::runtime_error("FreeCAD naming operation is empty"); + } + return output; +} + +QJsonObject parseNameToken(const std::string& raw) +{ + if (raw.empty() || (raw[0] != ':' && raw[0] != '$' && raw[0] != ';')) { + throw std::runtime_error("FreeCAD ElementMap emitted an invalid name token"); + } + std::vector parts; + std::size_t start = 0; + while (true) { + const std::size_t dot = raw.find('.', start); + parts.push_back(raw.substr(start, dot == std::string::npos ? dot : dot - start)); + if (dot == std::string::npos) { + break; + } + start = dot + 1; + } + const QString marker = QString(QChar(raw[0])); + QJsonObject token {{"raw", QString::fromStdString(raw)}, {"marker", marker}}; + QJsonArray suffix; + if (raw[0] == ':') { + if (parts.size() < 3) { + throw std::runtime_error("FreeCAD ElementMap emitted a truncated indexed token"); + } + bool postfixOk = false; + bool indexOk = false; + const int postfixIndex = QString::fromStdString(parts[0].substr(1)).toInt(&postfixOk, 16); + const int elementIndex = QString::fromStdString(parts[1]).toInt(&indexOk, 16); + if (!postfixOk || !indexOk) { + throw std::runtime_error("FreeCAD ElementMap emitted a malformed indexed token"); + } + token.insert("postfixIndex", postfixIndex); + token.insert("elementIndex", elementIndex); + for (std::size_t index = 2; index < parts.size(); ++index) { + suffix.append(QString::fromStdString(parts[index])); + } + } + else { + token.insert("name", QString::fromStdString(parts[0].substr(1))); + for (std::size_t index = 1; index < parts.size(); ++index) { + suffix.append(QString::fromStdString(parts[index])); + } + } + token.insert("suffix", suffix); + return token; +} + +QJsonObject parseElementMap(const std::string& saved) +{ + std::istringstream stream(saved); + unsigned rootId = 0; + int postfixCount = 0; + std::string label; + if (!(stream >> rootId >> label >> postfixCount) || rootId == 0 || label != "PostfixCount" + || postfixCount < 0) { + throw std::runtime_error("FreeCAD ElementMap root header is invalid"); + } + QJsonArray postfixes; + for (int index = 0; index < postfixCount; ++index) { + std::string postfix; + if (!(stream >> postfix)) { + throw std::runtime_error("FreeCAD ElementMap postfix list is truncated"); + } + postfixes.append(QString::fromStdString(postfix)); + } + int mapCount = 0; + if (!(stream >> label >> mapCount) || label != "MapCount" || mapCount <= 0) { + throw std::runtime_error("FreeCAD ElementMap map count is invalid"); + } + QJsonArray maps; + int rootMapIndex = 0; + for (int mapOrdinal = 0; mapOrdinal < mapCount; ++mapOrdinal) { + int mapIndex = 0; + unsigned mapId = 0; + int typeCount = 0; + if (!(stream >> label >> mapIndex >> mapId >> typeCount) || label != "ElementMap" + || mapIndex <= 0 || typeCount < 0) { + throw std::runtime_error("FreeCAD ElementMap map header is invalid"); + } + rootMapIndex = std::max(rootMapIndex, mapIndex); + QJsonArray sections; + for (int typeOrdinal = 0; typeOrdinal < typeCount; ++typeOrdinal) { + std::string sectionName; + int childCount = 0; + if (!(stream >> sectionName >> label >> childCount) || label != "ChildCount" + || childCount != 0) { + throw std::runtime_error("Candidate bridge does not accept child ElementMap records"); + } + int nameCount = 0; + if (!(stream >> label >> nameCount) || label != "NameCount" || nameCount < 0) { + throw std::runtime_error("FreeCAD ElementMap name count is invalid"); + } + QJsonArray names; + for (int nameOrdinal = 0; nameOrdinal < nameCount; ++nameOrdinal) { + QJsonArray tokens; + std::string raw; + std::string token; + while (stream >> token) { + if (token == "0") { + break; + } + if (!raw.empty()) { + raw += ' '; + } + raw += token; + tokens.append(parseNameToken(token)); + } + if (!stream) { + throw std::runtime_error("FreeCAD ElementMap name entry is truncated"); + } + names.append(QJsonObject { + {"tokens", tokens}, + {"trailing", "0"}, + {"raw", QString::fromStdString(raw.empty() ? "0" : raw + " 0")}, + }); + } + sections.append(QJsonObject { + {"name", QString::fromStdString(sectionName)}, + {"children", QJsonArray {}}, + {"names", names}, + }); + } + if (!(stream >> label) || label != "EndMap") { + throw std::runtime_error("FreeCAD ElementMap map terminator is missing"); + } + maps.append(QJsonObject { + {"index", mapIndex}, + {"id", static_cast(mapId)}, + {"typeCount", typeCount}, + {"sections", sections}, + }); + } + return QJsonObject { + {"schemaVersion", 2}, + {"nativeVersion", 1}, + {"rootId", static_cast(rootId)}, + {"postfixes", postfixes}, + {"maps", maps}, + {"rootMapIndex", rootMapIndex}, + }; +} + +QJsonObject stringHasherJson(const App::StringHasherRef& hasher) +{ + QJsonArray entries; + for (const auto& [id, reference] : hasher->getIDMap()) { + const App::StringID& stringId = reference.deref(); + int flags = 0; + flags |= stringId.isBinary() ? 1 << 0 : 0; + flags |= stringId.isHashed() ? 1 << 1 : 0; + flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0; + flags |= stringId.isPostfixed() ? 1 << 3 : 0; + flags |= stringId.isIndexed() ? 1 << 4 : 0; + flags |= stringId.isPrefixID() ? 1 << 5 : 0; + flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0; + flags |= stringId.isPersistent() ? 1 << 7 : 0; + QJsonArray relatedIds; + for (const auto& related : reference.relatedIDs()) { + relatedIds.append(static_cast(related.value())); + } + entries.append(QJsonObject { + {"id", static_cast(id)}, + {"flags", flags}, + {"relatedIds", relatedIds}, + {"data", QString::fromUtf8(stringId.data())}, + {"postfix", QString::fromUtf8(stringId.postfix())}, + }); + } + return QJsonObject {{"schemaVersion", 2}, {"nativeVersion", 1}, {"entries", entries}}; +} + +int stringIdFlags(const App::StringID& stringId) +{ + int flags = 0; + flags |= stringId.isBinary() ? 1 << 0 : 0; + flags |= stringId.isHashed() ? 1 << 1 : 0; + flags |= stringId.isPostfixEncoded() ? 1 << 2 : 0; + flags |= stringId.isPostfixed() ? 1 << 3 : 0; + flags |= stringId.isIndexed() ? 1 << 4 : 0; + flags |= stringId.isPrefixID() ? 1 << 5 : 0; + flags |= stringId.isPrefixIDIndex() ? 1 << 6 : 0; + flags |= stringId.isPersistent() ? 1 << 7 : 0; + return flags; +} + +long requireStringId(const QJsonValue& value, const char* label) +{ + const double number = value.toDouble(-1.0); + if (!std::isfinite(number) || number <= 0.0 || std::floor(number) != number + || number > static_cast(0x7fffffff)) { + throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label); + } + return static_cast(number); +} + +void restoreStringHasherTable(const QJsonObject& table, const App::StringHasherRef& hasher) +{ + if (table.value("schemaVersion").toInt() != 2 + || table.value("nativeVersion").toInt() != 1) { + throw std::runtime_error("Prior StringHasher schema or native version is unsupported"); + } + const QJsonArray entries = table.value("entries").toArray(); + if (entries.size() > 1000000) { + throw std::runtime_error("Prior StringHasher entry count exceeds the candidate limit"); + } + long previousId = 0; + for (const QJsonValue entryValue : entries) { + if (!entryValue.isObject()) { + throw std::runtime_error("Prior StringHasher entry must be an object"); + } + const QJsonObject entry = entryValue.toObject(); + const long id = requireStringId(entry.value("id"), "entry ID"); + if (id != previousId + 1) { + throw std::runtime_error("Prior StringHasher IDs must be contiguous and ordered"); + } + const int flags = entry.value("flags").toInt(-1); + constexpr int knownFlags = 0xff; + if (flags < 0 || (flags & ~knownFlags) != 0) { + throw std::runtime_error("Prior StringHasher entry contains unknown flags"); + } + if ((flags & ((1 << 0) | (1 << 1))) != 0) { + throw std::runtime_error( + "Candidate bridge cannot losslessly restore binary or one-way-hashed StringHasher entries"); + } + if (!entry.value("data").isString() || !entry.value("postfix").isString() + || !entry.value("relatedIds").isArray()) { + throw std::runtime_error("Prior StringHasher entry payload is incomplete"); + } + const QByteArray data = entry.value("data").toString().toUtf8(); + const QByteArray postfix = entry.value("postfix").toString().toUtf8(); + const QJsonArray relatedValues = entry.value("relatedIds").toArray(); + std::vector relatedIds; + relatedIds.reserve(relatedValues.size()); + for (const QJsonValue relatedValue : relatedValues) { + const long relatedId = requireStringId(relatedValue, "related ID"); + if (relatedId >= id || !hasher->getID(relatedId)) { + throw std::runtime_error("Prior StringHasher related ID is unresolved or forward-referenced"); + } + relatedIds.push_back(relatedId); + } + + App::StringIDRef restored; + if ((flags & (1 << 3)) == 0) { + if (flags != 0 && flags != (1 << 7)) { + throw std::runtime_error("Prior non-postfixed StringHasher flags cannot be reconstructed"); + } + restored = hasher->getID(data, App::StringHasher::Option::None); + } + else { + QByteArray mappedData = data; + if ((flags & ((1 << 4) | (1 << 6))) != 0) { + mappedData += '1'; + } + Data::MappedName mapped(mappedData); + if (!postfix.isEmpty()) { + mapped += postfix; + } + int internalCount = 0; + internalCount += (flags & (1 << 2)) != 0 ? 1 : 0; + internalCount += (flags & (1 << 4)) != 0 ? 1 : 0; + if (internalCount > static_cast(relatedIds.size())) { + throw std::runtime_error("Prior StringHasher internal dependencies are truncated"); + } + Data::ElementIDRefs externalRefs; + for (std::size_t index = static_cast(internalCount); + index < relatedIds.size(); ++index) { + externalRefs.push_back(hasher->getID(relatedIds[index])); + } + restored = hasher->getID(mapped, externalRefs); + } + if (!restored || restored.value() != id) { + throw std::runtime_error("Prior StringHasher entry did not restore to its persistent ID"); + } + if ((flags & (1 << 7)) != 0) { + restored.setPersistent(true); + } + const App::StringID& actual = restored.deref(); + std::vector actualRelated; + for (const App::StringIDRef& related : actual.relatedIDs()) { + actualRelated.push_back(related.value()); + } + if (stringIdFlags(actual) != flags || actual.data() != data || actual.postfix() != postfix + || actualRelated != relatedIds) { + throw std::runtime_error("Prior StringHasher entry changed during strict restoration"); + } + previousId = id; + } +} + +void restorePriorStringHasher(const QJsonArray& inputs, const App::StringHasherRef& hasher) +{ + QByteArray canonical; + QJsonObject selected; + for (const QJsonValue inputValue : inputs) { + const QJsonObject evidence = inputValue.toObject().value("namingEvidence").toObject(); + if (evidence.isEmpty()) { + continue; + } + const QJsonValue tableValue = evidence.value("stringHasher"); + if (!tableValue.isObject()) { + const QJsonArray mappedNames = evidence.value("mappedNames").toArray(); + const bool requiresHasher = std::any_of( + mappedNames.begin(), mappedNames.end(), [](const QJsonValue& mappedValue) { + const QJsonObject reference = mappedValue.toObject().value("reference").toObject(); + return !reference.value("stringIds").toArray().isEmpty() + || reference.contains("prefixStringId"); + }); + if (requiresHasher) { + throw std::runtime_error("Prior mapped-name IDs require StringHasher evidence"); + } + continue; + } + const QJsonObject table = tableValue.toObject(); + const QByteArray encoded = QJsonDocument(table).toJson(QJsonDocument::Compact); + if (canonical.isEmpty()) { + canonical = encoded; + selected = table; + } + else if (canonical != encoded) { + throw std::runtime_error("Candidate bridge cannot merge inconsistent prior StringHasher tables"); + } + } + if (!selected.isEmpty()) { + restoreStringHasherTable(selected, hasher); + } +} + +QJsonObject findSourceInput(const QJsonArray& inputs, const QString& source) +{ + for (const QJsonValue value : inputs) { + const QJsonObject input = value.toObject(); + if (input.value("inputId").toString() == source || input.value("role").toString() == source) { + return input; + } + } + if (source == "object" && !inputs.isEmpty()) { + return inputs.first().toObject(); + } + if (source == "tool" && inputs.size() > 1) { + return inputs.at(1).toObject(); + } + throw std::runtime_error("FreeCAD naming history references an unknown input"); +} + +struct SourceName +{ + Data::MappedName name; + QString persistentId; +}; + +SourceName sourceNameFor(const QJsonObject& input, + const QString& kind, + int sourceIndex, + const App::StringHasherRef& hasher) +{ + const QString indexed = titleKind(kind) + QString::number(sourceIndex + 1); + const QJsonValue evidenceValue = input.value("namingEvidence"); + if (!evidenceValue.isObject()) { + return {Data::MappedName(indexed.toStdString()), indexed}; + } + const QJsonArray mappedNames = evidenceValue.toObject().value("mappedNames").toArray(); + for (const QJsonValue value : mappedNames) { + const QJsonObject mapped = value.toObject(); + if (mapped.value("kind").toString() != kind + || mapped.value("resultIndex").toInt(-1) != sourceIndex) { + continue; + } + const QJsonObject reference = requireObject(mapped, "reference"); + const QJsonArray stringIds = reference.value("stringIds").toArray(); + for (const QJsonValue idValue : stringIds) { + if (!hasher->getID(requireStringId(idValue, "mapped-name string ID"))) { + throw std::runtime_error("Prior mapped-name string ID is absent from StringHasher"); + } + } + if (reference.contains("prefixStringId")) { + const long prefixId = requireStringId(reference.value("prefixStringId"), + "mapped-name prefix ID"); + if (!hasher->getID(prefixId)) { + throw std::runtime_error("Prior mapped-name prefix ID is absent from StringHasher"); + } + } + Data::MappedName name(requireString(reference, "name").toStdString()); + const QString postfix = reference.value("postfix").toString(); + if (!postfix.isEmpty()) { + name += postfix.toStdString(); + } + return {name, mapped.value("resultPersistentId").toString(indexed)}; + } + throw std::runtime_error("Prior naming evidence has no matching source subshape"); +} + +QJsonObject mappedReference(const Data::MappedName& name, const Data::ElementIDRefs& stringIds) +{ + QJsonArray ids; + for (const App::StringIDRef& stringId : stringIds) { + ids.append(static_cast(stringId.value())); + } + QJsonObject reference { + {"name", QString::fromUtf8(name.dataBytes())}, + {"postfix", QString::fromUtf8(name.postfixBytes())}, + {"stringIds", ids}, + }; + const App::StringID::IndexID prefix = App::StringID::fromString(name.dataBytes()); + if (prefix) { + reference.insert("prefixStringId", static_cast(prefix.id)); + } + return reference; +} + +std::string evidenceJsonImpl(const std::string& requestJson) +{ + ensureApplication(); + QJsonParseError parseError; + const QJsonDocument document = + QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) { + throw std::runtime_error("FreeCAD naming request is not valid JSON"); + } + const QJsonObject request = document.object(); + if (request.value("schemaVersion").toInt() != ABI_VERSION) { + throw std::runtime_error("FreeCAD naming request schemaVersion is unsupported"); + } + const QString stageId = requireString(request, "stageId"); + const QString resultObjectId = requireString(request, "resultObjectId"); + const long resultTag = requirePositiveTag(request, "resultObjectTag"); + const QString operation = requireString(request, "operation"); + const QString operationPostfix = canonicalOperation(operation); + const QJsonArray inputs = request.value("inputs").toArray(); + const QJsonObject history = requireObject(request, "history"); + const QJsonArray records = history.value("records").toArray(); + if (inputs.isEmpty() || records.isEmpty()) { + throw std::runtime_error("FreeCAD naming request requires inputs and native history records"); + } + + App::StringHasherRef hasher(new App::StringHasher()); + hasher->setSaveAll(true); + restorePriorStringHasher(inputs, hasher); + auto elementMap = std::make_shared(); + elementMap->hasher = hasher; + QJsonArray mappedNames; + std::set resultKeys; + std::map sourceOrdinals; + + for (const QJsonValue recordValue : records) { + const QJsonObject record = recordValue.toObject(); + const QString relation = requireString(record, "relation"); + if (relation == "deleted") { + continue; + } + if (relation != "modified" && relation != "generated") { + throw std::runtime_error("FreeCAD naming history relation is unsupported"); + } + const QString sourceKind = requireString(record, "kind"); + const QString resultKind = record.value("resultKind").toString(sourceKind); + titleKind(sourceKind); + const QString resultType = titleKind(resultKind); + const int sourceIndex = requireIndex(record, "sourceIndex"); + const QString source = record.value("sourceId").toString( + requireString(record, "source")); + const QJsonObject input = findSourceInput(inputs, source); + const QString sourceObjectId = requireString(input, "objectId"); + const long sourceTag = requirePositiveTag(input, "objectTag"); + const SourceName sourceName = sourceNameFor(input, sourceKind, sourceIndex, hasher); + QJsonArray resultIndexes = record.value("resultIndexes").toArray(); + if (resultIndexes.isEmpty() && record.contains("resultIndex")) { + resultIndexes.append(record.value("resultIndex")); + } + if (resultIndexes.isEmpty()) { + throw std::runtime_error("Non-deleted FreeCAD naming history requires result indexes"); + } + const std::string ordinalKey = (sourceObjectId + '|' + sourceKind + '|' + + QString::number(sourceIndex) + '|' + relation) + .toStdString(); + for (const QJsonValue resultValue : resultIndexes) { + const int resultIndex = requireIndex(QJsonObject {{"value", resultValue}}, "value"); + const std::string resultKey = + (resultKind + ':' + QString::number(resultIndex)).toStdString(); + if (!resultKeys.insert(resultKey).second) { + throw std::runtime_error("FreeCAD naming history maps the same result subshape twice"); + } + const int ordinal = ++sourceOrdinals[ordinalKey]; + std::ostringstream postfix; + postfix << (relation == "modified" ? Data::POSTFIX_MOD : Data::POSTFIX_GEN); + if (ordinal > 1) { + postfix << ordinal; + } + Data::MappedName encoded(sourceName.name); + Data::ElementIDRefs stringIds; + elementMap->encodeElementName( + resultType.at(0).toLatin1(), + encoded, + postfix, + &stringIds, + resultTag, + operationPostfix.toUtf8().constData(), + sourceTag); + const Data::IndexedName resultIndexed( + (resultType + QString::number(resultIndex + 1)).toUtf8()); + const Data::MappedName stored = + elementMap->setElementName(resultIndexed, encoded, resultTag, &stringIds); + if (!stored) { + throw std::runtime_error("FreeCAD ElementMap rejected a native history mapping"); + } + mappedNames.append(QJsonObject { + {"kind", resultKind}, + {"resultIndex", resultIndex}, + {"resultPersistentId", resultType + QString::number(resultIndex + 1)}, + {"relation", relation}, + {"reference", mappedReference(stored, stringIds)}, + {"sourceRefs", QJsonArray {QJsonObject { + {"objectId", sourceObjectId}, + {"persistentId", sourceName.persistentId}, + {"stageId", input.value("stageId")}, + }}}, + }); + } + } + if (mappedNames.isEmpty()) { + throw std::runtime_error("FreeCAD naming history has no result subshape evidence"); + } + + elementMap->beforeSave(hasher); + std::ostringstream saved; + elementMap->save(saved); + const QJsonObject response { + {"schemaVersion", ABI_VERSION}, + {"stageId", stageId}, + {"resultObjectId", resultObjectId}, + {"status", "native-evidence"}, + {"mappedNames", mappedNames}, + {"stringHasher", stringHasherJson(hasher)}, + {"elementMap2", parseElementMap(saved.str())}, + }; + return QJsonDocument(response).toJson(QJsonDocument::Compact).toStdString(); +} + +std::string evidenceJson(const std::string& requestJson) +{ + try { + return evidenceJsonImpl(requestJson); + } + catch (const Base::Exception& exception) { + return QJsonDocument(QJsonObject { + {"schemaVersion", ABI_VERSION}, + {"status", "error"}, + {"error", QString::fromStdString("FreeCAD private naming failure: " + + exception.getMessage())}, + }).toJson(QJsonDocument::Compact).toStdString(); + } + catch (const std::exception& exception) { + return QJsonDocument(QJsonObject { + {"schemaVersion", ABI_VERSION}, + {"status", "error"}, + {"error", QString::fromUtf8(exception.what())}, + }).toJson(QJsonDocument::Compact).toStdString(); + } + catch (...) { + return R"({"schemaVersion":1,"status":"error","error":"Unknown native exception"})"; + } +} + +int freecadNamingAbiVersion() +{ + return ABI_VERSION; +} + +std::string freecadNamingCapabilitiesJson() +{ + return QJsonDocument(QJsonObject { + {"schemaVersion", ABI_VERSION}, + {"freecadVersion", FREECAD_VERSION}, + {"sourceCommit", FREECAD_COMMIT}, + {"mappedNameRef", true}, + {"stringHasher", true}, + {"elementMap2", true}, + {"operations", QJsonArray { + "fuse", "cut", "common", "rotate", "pad", "pocket", "loft", "pipe", + "revolution", "groove", "fillet", "chamfer", "hole", "draft", "thickness", + "linear-pattern", "polar-pattern", "mirrored", "multi-transform", + }}, + {"maxRequestBytes", 16 * 1024 * 1024}, + {"maxResponseBytes", 32 * 1024 * 1024}, + }).toJson(QJsonDocument::Compact).toStdString(); +} +} // namespace + +EMSCRIPTEN_BINDINGS(freecad_private_naming_bridge) +{ + emscripten::function("freecadNamingAbiVersion", &freecadNamingAbiVersion); + emscripten::function("freecadNamingCapabilitiesJson", &freecadNamingCapabilitiesJson); + emscripten::function("freecadNamingEvidenceJson", &evidenceJson); +} diff --git a/native/freecad-naming-bridge/pre.js b/native/freecad-naming-bridge/pre.js new file mode 100644 index 0000000..cf92973 --- /dev/null +++ b/native/freecad-naming-bridge/pre.js @@ -0,0 +1,26 @@ +Module.preRun = Module.preRun || []; +Module.preRun.push(() => { + for (const path of [ + '/freecad-user', + '/freecad-user/config', + '/freecad-user/data', + '/freecad-user/cache', + '/freecad-user/temp', + ]) { + try { + FS.mkdir(path) + } catch (error) { + if (!FS.analyzePath(path).exists) throw error + } + } + Object.assign(ENV, { + HOME: '/freecad-user', + FREECAD_USER_HOME: '/freecad-user', + FREECAD_USER_DATA: '/freecad-user/data', + FREECAD_USER_TEMP: '/freecad-user/temp', + XDG_CONFIG_HOME: '/freecad-user/config', + XDG_DATA_HOME: '/freecad-user/data', + XDG_CACHE_HOME: '/freecad-user/cache', + TMPDIR: '/freecad-user/temp', + }) +}) diff --git a/native/freecad-naming-bridge/smoke-test.ts b/native/freecad-naming-bridge/smoke-test.ts new file mode 100644 index 0000000..abcb30c --- /dev/null +++ b/native/freecad-naming-bridge/smoke-test.ts @@ -0,0 +1,185 @@ +import createBridge from './dist/freecad-private-naming-bridge.js' +import { fileURLToPath } from 'node:url' +import { createHash } from 'node:crypto' +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { + captureFreeCadPrivateNamingEvidence, + createFreeCadPrivateNamingAbiRequest, + probeFreeCadPrivateNamingAbi, + type NativeFreeCadNamingAbiModule, +} from '../../src/facade/nativeNamingAbi' + +const distUrl = new URL('./dist/', import.meta.url) +const bridge = await createBridge({ locateFile: (path: string) => fileURLToPath(new URL(path, distUrl)) }) as NativeFreeCadNamingAbiModule +const probe = probeFreeCadPrivateNamingAbi(bridge) +if (probe.availability !== 'available') throw new Error(`Isolated bridge ABI probe failed: ${probe.reason}`) + +const request = createFreeCadPrivateNamingAbiRequest({ + requestId: 'isolated-bridge-1', + documentId: 'isolated-document', + documentVersion: 1, + operationId: 'isolated-cut-1', + operation: 'cut', + stageId: 'isolated:stage:1', + resultObjectId: 'isolated:result:1', + resultObjectTag: 99, + inputs: [ + { inputId: 'object', objectId: 'source-object', role: 'object', step: 'ISO-10303-21; object', objectTag: 42 }, + { inputId: 'tool', objectId: 'source-tool', role: 'tool', step: 'ISO-10303-21; tool', objectTag: 43 }, + ], + stages: [{ stageId: 'isolated:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], + resultStep: 'ISO-10303-21; result', + history: { + provider: 'occt-native', + occtVersion: '8.0.0', + hasModified: true, + hasGenerated: false, + hasDeleted: false, + records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }], + }, +}) +const first = captureFreeCadPrivateNamingEvidence(bridge, request, probe) +if (!first || first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || !first.elementMap2 || !first.stringHasher) throw new Error('Isolated bridge returned incomplete first-stage evidence.') +if (!first.mappedNames[0].reference.postfix?.includes(';:M;CUT;:H2a:7,F')) throw new Error(`Unexpected first-stage FreeCAD postfix: ${first.mappedNames[0].reference.postfix}`) + +const chained = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({ + requestId: 'isolated-bridge-2', + documentId: 'isolated-document', + documentVersion: 2, + operationId: 'isolated-cut-2', + operation: 'cut', + stageId: 'isolated:stage:2', + resultObjectId: 'isolated:result:2', + resultObjectTag: 100, + inputs: [{ inputId: 'object', objectId: 'isolated:result:1', role: 'object', stageId: 'isolated:stage:1', step: 'ISO-10303-21; first result', objectTag: 99, namingEvidence: first }], + stages: [{ stageId: 'isolated:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + resultStep: 'ISO-10303-21; second result', + history: { + provider: 'occt-native', + occtVersion: '8.0.0', + hasModified: true, + hasGenerated: false, + hasDeleted: false, + records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 1 }], + }, +}), probe) +if (!chained || chained.elementMap2?.maps.length !== 1) throw new Error('Isolated bridge did not preserve chained ElementMap2 evidence.') +const chainedReference = chained.mappedNames?.[0].reference +const prefixStringId = chainedReference?.prefixStringId +const stringHasherIds = new Set(chained.stringHasher?.entries.map(({ id }) => id)) +const chainedTokens = chained.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens))) +if (!chainedReference?.name.startsWith('#') || !Number.isSafeInteger(prefixStringId) || !chainedReference.stringIds?.includes(prefixStringId!) || !stringHasherIds.has(prefixStringId!) || !chainedTokens.some((token) => token.marker === '$' && token.name === chainedReference.name)) throw new Error('Isolated bridge did not emit a closed native hashed MappedNameRef/StringHasher/ElementMap2 chain.') + +const third = captureFreeCadPrivateNamingEvidence(bridge, createFreeCadPrivateNamingAbiRequest({ + requestId: 'isolated-bridge-3', + documentId: 'isolated-document', + documentVersion: 3, + operationId: 'isolated-cut-3', + operation: 'cut', + stageId: 'isolated:stage:3', + resultObjectId: 'isolated:result:3', + resultObjectTag: 101, + inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }], + stages: [{ stageId: 'isolated:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + resultStep: 'ISO-10303-21; third result', + history: { + provider: 'occt-native', + occtVersion: '8.0.0', + hasModified: true, + hasGenerated: false, + hasDeleted: false, + records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }], + }, +}), probe) +const thirdReference = third?.mappedNames?.[0]?.reference +const thirdHasherIds = new Set(third?.stringHasher?.entries.map(({ id }) => id)) +if (!third || third.status !== 'native-evidence' || !thirdReference?.name.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdHasherIds.has(id)) || (third.stringHasher?.entries.length ?? 0) <= (chained.stringHasher?.entries.length ?? 0)) throw new Error('Isolated bridge did not restore prior StringHasher evidence for a third naming stage.') + +let tamperedHasherRejected = false +try { + const validRequest = createFreeCadPrivateNamingAbiRequest({ + requestId: 'isolated-bridge-tampered', + documentId: 'isolated-document', + documentVersion: 3, + operationId: 'isolated-cut-tampered', + operation: 'cut', + stageId: 'isolated:stage:tampered', + resultObjectId: 'isolated:result:tampered', + resultObjectTag: 102, + inputs: [{ inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }], + stages: [{ stageId: 'isolated:stage:tampered', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + resultStep: 'ISO-10303-21; tampered result', + history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] }, + }) + const tamperedRequest = structuredClone(validRequest) + const tamperedEntry = tamperedRequest.inputs[0].namingEvidence?.stringHasher?.entries[0] + if (!tamperedEntry) throw new Error('Chained evidence has no StringHasher entry to tamper.') + tamperedEntry.id += 1 + const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(tamperedRequest))) as { status?: string; error?: string } + if (response.status !== 'error' || !response.error?.includes('StringHasher IDs must be contiguous and ordered')) throw new Error(`Unexpected tampered StringHasher response: ${JSON.stringify(response)}`) + tamperedHasherRejected = true +} catch (error) { + throw error +} +if (!tamperedHasherRejected) throw new Error('Isolated bridge accepted tampered prior StringHasher evidence.') + +let inconsistentTablesRejected = false +{ + const inconsistentRequest = createFreeCadPrivateNamingAbiRequest({ + requestId: 'isolated-bridge-inconsistent', + documentId: 'isolated-document', + documentVersion: 3, + operationId: 'isolated-cut-inconsistent', + operation: 'cut', + stageId: 'isolated:stage:inconsistent', + resultObjectId: 'isolated:result:inconsistent', + resultObjectTag: 103, + inputs: [ + { inputId: 'object', objectId: 'isolated:result:2', role: 'object', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result', objectTag: 100, namingEvidence: chained }, + { inputId: 'tool', objectId: 'isolated:result:2-copy', role: 'tool', stageId: 'isolated:stage:2', step: 'ISO-10303-21; second result copy', objectTag: 104, namingEvidence: chained }, + ], + stages: [{ stageId: 'isolated:stage:inconsistent', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], + resultStep: 'ISO-10303-21; inconsistent result', + history: { provider: 'occt-native', occtVersion: '8.0.0', hasModified: true, hasGenerated: false, hasDeleted: false, records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 1, resultIndex: 2 }] }, + }) + const secondTable = inconsistentRequest.inputs[1].namingEvidence?.stringHasher + if (!secondTable?.entries[0]) throw new Error('Chained evidence has no StringHasher table to make inconsistent.') + secondTable.entries[0].data += '-different' + const response = JSON.parse(bridge.freecadNamingEvidenceJson!(JSON.stringify(inconsistentRequest))) as { status?: string; error?: string } + if (response.status !== 'error' || !response.error?.includes('cannot merge inconsistent prior StringHasher tables')) throw new Error(`Unexpected inconsistent StringHasher response: ${JSON.stringify(response)}`) + inconsistentTablesRejected = true +} + +let rejected = false +try { + captureFreeCadPrivateNamingEvidence(bridge, { ...request, history: { ...request.history, records: [] } }, probe) +} catch (error) { + if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error + rejected = true +} +if (!rejected) throw new Error('Isolated bridge accepted a request without native history records.') + +const result = { + status: 'freecad-private-naming-isolated-bridge-pass', + descriptor: probe.descriptor, + firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length }, + chainedStage: { mappedNames: chained.mappedNames.length, stringHasherEntries: chained.stringHasher.entries.length }, + thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length }, + tamperedHasherRejected, + inconsistentTablesRejected, + productionPublication: false, + productionWorkerLinked: false, +} +const reportPath = process.env.FREECAD_NAMING_BRIDGE_REPORT +if (reportPath) { + const artifacts = await Promise.all(['freecad-private-naming-bridge.js', 'freecad-private-naming-bridge.wasm', 'freecad-private-naming-bridge.data'].map(async (name) => { + const path = fileURLToPath(new URL(name, distUrl)) + const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)]) + return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') } + })) + const absoluteReportPath = resolve(reportPath) + await mkdir(dirname(absoluteReportPath), { recursive: true }) + await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`) +} +console.log(JSON.stringify(result, null, 2)) diff --git a/native/freecad-naming-probe/abi-candidate-smoke.ts b/native/freecad-naming-probe/abi-candidate-smoke.ts new file mode 100644 index 0000000..af92699 --- /dev/null +++ b/native/freecad-naming-probe/abi-candidate-smoke.ts @@ -0,0 +1,74 @@ +import createProbe from './dist/freecad-private-naming-source-probe.js' +import { parseElementMap2 } from '../../src/facade/elementMap2' +import { + captureFreeCadPrivateNamingEvidence, + createFreeCadPrivateNamingAbiRequest, + probeFreeCadPrivateNamingAbi, + type NativeFreeCadNamingAbiModule, +} from '../../src/facade/nativeNamingAbi' + +type CandidateProbe = { + freecadNamingCandidateAbiVersion(): number + freecadNamingCandidateCapabilitiesJson(): string + freecadNamingCandidateEvidenceJson(requestJson: string): string +} + +const native = await createProbe() as CandidateProbe +const candidate: NativeFreeCadNamingAbiModule = { + freecadNamingAbiVersion: () => native.freecadNamingCandidateAbiVersion(), + freecadNamingCapabilitiesJson: () => native.freecadNamingCandidateCapabilitiesJson(), + freecadNamingEvidenceJson: (requestJson) => { + const response = JSON.parse(native.freecadNamingCandidateEvidenceJson(requestJson)) as Record + const elementMap2Text = response.elementMap2Text + if (typeof elementMap2Text !== 'string') throw new TypeError('Candidate response omitted native ElementMap2 text.') + delete response.elementMap2Text + response.elementMap2 = parseElementMap2(elementMap2Text) + return JSON.stringify(response) + }, +} + +const abiProbe = probeFreeCadPrivateNamingAbi(candidate) +if (abiProbe.availability !== 'available') throw new Error(`Candidate ABI probe failed: ${abiProbe.reason}`) +const request = createFreeCadPrivateNamingAbiRequest({ + requestId: 'source-candidate-request', + documentId: 'source-candidate-document', + documentVersion: 1, + operationId: 'source-candidate-cut', + operation: 'cut', + stageId: 'source-candidate:stage:0', + resultObjectId: 'source-candidate:result', + resultObjectTag: 99, + inputs: [ + { inputId: 'object', objectId: 'source-candidate:object', role: 'object', step: 'ISO-10303-21; probe object', objectTag: 42 }, + { inputId: 'tool', objectId: 'source-candidate:tool', role: 'tool', step: 'ISO-10303-21; probe tool', objectTag: 43 }, + ], + stages: [{ stageId: 'source-candidate:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], + resultStep: 'ISO-10303-21; probe result', + history: { + provider: 'occt-native', + occtVersion: '8.0.0', + hasModified: true, + hasGenerated: false, + hasDeleted: false, + resultStep: 'ISO-10303-21; probe result', + records: [{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 2 }], + }, +}) +const evidence = captureFreeCadPrivateNamingEvidence(candidate, request, abiProbe) +if (!evidence || evidence.status !== 'native-evidence') throw new Error('Candidate ABI returned no native evidence.') +if (evidence.mappedNames?.[0]?.reference.name !== '#1' || evidence.mappedNames[0].reference.postfix !== ';:H2a,F') throw new Error('Candidate ABI mapped-name reference is unexpected.') +if (evidence.stringHasher?.entries.length !== 1 || evidence.elementMap2?.maps.length !== 1) throw new Error('Candidate ABI native resource evidence is incomplete.') + +console.log(JSON.stringify({ + status: 'freecad-private-naming-abi-candidate-pass', + descriptor: abiProbe.descriptor, + evidence: { + stageId: evidence.stageId, + resultObjectId: evidence.resultObjectId, + mappedNames: evidence.mappedNames.length, + stringHasherEntries: evidence.stringHasher.entries.length, + elementMaps: evidence.elementMap2.maps.length, + }, + productionCallbacksExported: false, + productionWorkerLinked: false, +}, null, 2)) diff --git a/native/freecad-naming-probe/probe.cpp b/native/freecad-naming-probe/probe.cpp index 20ababd..0ed218b 100644 --- a/native/freecad-naming-probe/probe.cpp +++ b/native/freecad-naming-probe/probe.cpp @@ -1,10 +1,21 @@ #include #include +#include +#include #include +#include +#include +#include +#include +#include + +#include +#include #include #include +#include namespace { @@ -26,9 +37,248 @@ std::string freecadPrivateNamingSourceProbe() } return name.toString(); } + +std::string freecadPrivateStringHasherSourceProbe() +{ + App::StringHasher hasher; + hasher.setSaveAll(true); + + const App::StringIDRef plain = hasher.getID("stable-name"); + const App::StringIDRef duplicate = hasher.getID("stable-name"); + if (!plain || plain.value() != duplicate.value() || plain.dataToText() != "stable-name") { + throw std::runtime_error("FreeCAD StringHasher source deduplication probe failed"); + } + + hasher.setThreshold(8); + const App::StringIDRef hashed = hasher.getID("hashable-source-value", -1, true); + if (!hashed.isHashed() || hashed.deref().data().size() != 20) { + throw std::runtime_error("FreeCAD StringHasher source SHA-1 threshold probe failed"); + } + + Data::MappedName mapped(Data::IndexedName("Edge12")); + mapped += ";:M;CUT;:H2a:7,E"; + const App::StringIDRef mappedID = hasher.getID(mapped, {}); + if (!mappedID || mappedID.getIndex() != 12 || mappedID.relatedIDs().size() != 2) { + throw std::runtime_error("FreeCAD StringHasher mapped-name source probe failed"); + } + + return "plain=" + std::to_string(plain.value()) + ";hashed=" + + std::to_string(hashed.value()) + ";mapped=" + std::to_string(mappedID.value()) + + ";index=" + std::to_string(mappedID.getIndex()) + ";related=" + + std::to_string(mappedID.relatedIDs().size()); +} + +std::string freecadPrivateElementMapSourceProbe() +{ + App::StringHasherRef hasher(new App::StringHasher()); + hasher->setSaveAll(true); + + auto elementMap = std::make_shared(); + elementMap->hasher = hasher; + Data::MappedName encoded("Edge12;:M;CUT"); + Data::ElementIDRefs stringIDs; + std::ostringstream postfix; + elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true); + if (!encoded.startsWith("#") || encoded.find(";:H2a,F") < 0 || stringIDs.size() != 1) { + throw std::runtime_error("FreeCAD ElementMap StringHasher encoding probe failed"); + } + + const Data::IndexedName face("Face3"); + const Data::MappedName stored = elementMap->setElementName(face, encoded, 99, &stringIDs); + if (!stored || elementMap->find(stored) != face || elementMap->find(face) != stored) { + throw std::runtime_error("FreeCAD ElementMap bidirectional lookup probe failed"); + } + + Data::MappedName original; + std::vector history; + const long historyTag = elementMap->getElementHistory(stored, 99, &original, &history); + if (historyTag != 42 || original.toString() != "Edge12;:M;CUT") { + throw std::runtime_error("FreeCAD ElementMap history probe failed"); + } + + elementMap->beforeSave(hasher); + std::ostringstream saved; + elementMap->save(saved); + auto restored = std::make_shared(); + std::istringstream input(saved.str()); + restored = restored->restore(hasher, input); + if (!restored || restored->find(face) != stored || restored->find(stored) != face) { + throw std::runtime_error("FreeCAD ElementMap save/restore probe failed"); + } + + std::vector ordered { + Data::MappedName("#b"), + Data::MappedName("Edge10"), + Data::MappedName("#a"), + Data::MappedName("Edge2"), + }; + std::sort(ordered.begin(), ordered.end(), Data::ElementNameComparator {}); + if (ordered[0].toString() != "Edge2" || ordered[1].toString() != "Edge10" + || ordered[2].toString() != "#a" || ordered[3].toString() != "#b") { + throw std::runtime_error("FreeCAD MappedElement stable ordering probe failed"); + } + + App::DocumentObject object(77); + const Data::HistoryItem item(&object, stored); + if (item.tag != 77 || Data::oldElementName("Body.;mapped.Face3") != "Body.Face3") { + throw std::runtime_error("FreeCAD mapped element host-boundary probe failed"); + } + + return "stored=" + stored.toString() + ";tag=" + std::to_string(historyTag) + + ";original=" + original.toString() + ";serialized=" + + std::to_string(saved.str().size()) + ";restored=" + std::to_string(restored->size()); +} + +std::string freecadPrivateElementMapResourcesProbe() +{ + App::StringHasherRef hasher(new App::StringHasher()); + hasher->setSaveAll(true); + auto elementMap = std::make_shared(); + elementMap->hasher = hasher; + + Data::MappedName encoded("Edge12;:M;CUT"); + Data::ElementIDRefs stringIDs; + std::ostringstream postfix; + elementMap->encodeElementName('F', encoded, postfix, &stringIDs, 99, nullptr, 42, true); + elementMap->setElementName(Data::IndexedName("Face3"), encoded, 99, &stringIDs); + elementMap->beforeSave(hasher); + + std::ostringstream elementMapStream; + elementMap->save(elementMapStream); + QJsonArray referenceStringIDs; + for (const auto& stringID : stringIDs) { + referenceStringIDs.append(static_cast(stringID.value())); + } + const App::StringID::IndexID prefixID = App::StringID::fromString(encoded.dataBytes()); + if (!prefixID || prefixID.index != 0 || referenceStringIDs.isEmpty()) { + throw std::runtime_error("FreeCAD ElementMap MappedNameRef probe failed"); + } + const QJsonObject mappedNameReference { + {"name", QString::fromUtf8(encoded.dataBytes())}, + {"postfix", QString::fromUtf8(encoded.postfixBytes())}, + {"prefixStringId", static_cast(prefixID.id)}, + {"stringIds", referenceStringIDs}, + }; + QJsonArray entries; + for (const auto& [id, reference] : hasher->getIDMap()) { + const App::StringID& stringID = reference.deref(); + int flags = 0; + flags |= stringID.isBinary() ? 1 << 0 : 0; + flags |= stringID.isHashed() ? 1 << 1 : 0; + flags |= stringID.isPostfixEncoded() ? 1 << 2 : 0; + flags |= stringID.isPostfixed() ? 1 << 3 : 0; + flags |= stringID.isIndexed() ? 1 << 4 : 0; + flags |= stringID.isPrefixID() ? 1 << 5 : 0; + flags |= stringID.isPrefixIDIndex() ? 1 << 6 : 0; + flags |= stringID.isPersistent() ? 1 << 7 : 0; + QJsonArray relatedIDs; + for (const auto& related : reference.relatedIDs()) { + relatedIDs.append(static_cast(related.value())); + } + entries.append(QJsonObject { + {"id", static_cast(id)}, + {"flags", flags}, + {"relatedIds", relatedIDs}, + {"data", QString::fromUtf8(stringID.data())}, + {"postfix", QString::fromUtf8(stringID.postfix())}, + }); + } + + return QJsonDocument(QJsonObject { + {"elementMapText", QString::fromStdString("BeginElementMap v1\n" + elementMapStream.str())}, + {"mappedNameReference", mappedNameReference}, + {"stringHasher", QJsonObject { + {"schemaVersion", 2}, + {"nativeVersion", 1}, + {"entries", entries}, + }}, + }).toJson(QJsonDocument::Compact).toStdString(); +} + +int freecadNamingCandidateAbiVersion() +{ + return 1; +} + +std::string freecadNamingCandidateCapabilitiesJson() +{ + return QJsonDocument(QJsonObject { + {"schemaVersion", 1}, + {"freecadVersion", "1.1.1"}, + {"sourceCommit", "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"}, + {"mappedNameRef", true}, + {"stringHasher", true}, + {"elementMap2", true}, + {"operations", QJsonArray {"cut"}}, + }).toJson(QJsonDocument::Compact).toStdString(); +} + +std::string freecadNamingCandidateEvidenceJson(const std::string& requestJson) +{ + QJsonParseError requestError; + const QJsonDocument requestDocument = + QJsonDocument::fromJson(QByteArray::fromStdString(requestJson), &requestError); + const QJsonObject request = requestDocument.object(); + if (requestError.error != QJsonParseError::NoError || request.value("schemaVersion").toInt() != 1 + || request.value("operation").toString() != "cut" + || request.value("stageId").toString().isEmpty() + || request.value("resultObjectId").toString().isEmpty()) { + throw std::runtime_error("FreeCAD naming candidate request is invalid"); + } + + QJsonParseError resourcesError; + const QJsonDocument resourcesDocument = QJsonDocument::fromJson( + QByteArray::fromStdString(freecadPrivateElementMapResourcesProbe()), &resourcesError); + if (resourcesError.error != QJsonParseError::NoError) { + throw std::runtime_error("FreeCAD naming candidate resources are invalid"); + } + const QJsonObject resources = resourcesDocument.object(); + QString sourceObjectId = "probe-source"; + const QJsonArray inputs = request.value("inputs").toArray(); + if (!inputs.isEmpty() && !inputs[0].toObject().value("objectId").toString().isEmpty()) { + sourceObjectId = inputs[0].toObject().value("objectId").toString(); + } + + const QJsonObject reference = resources.value("mappedNameReference").toObject(); + if (reference.isEmpty()) { + throw std::runtime_error("FreeCAD naming candidate omitted its native MappedNameRef"); + } + const QJsonObject mappedName { + {"kind", "face"}, + {"resultIndex", 2}, + {"resultPersistentId", "probe-face-3"}, + {"relation", "modified"}, + {"reference", reference}, + {"sourceRefs", QJsonArray {QJsonObject { + {"objectId", sourceObjectId}, + {"persistentId", "Edge12"}, + }}}, + }; + + return QJsonDocument(QJsonObject { + {"schemaVersion", 1}, + {"stageId", request.value("stageId")}, + {"resultObjectId", request.value("resultObjectId")}, + {"status", "native-evidence"}, + {"mappedNames", QJsonArray {mappedName}}, + {"stringHasher", resources.value("stringHasher")}, + {"elementMap2Text", resources.value("elementMapText")}, + }).toJson(QJsonDocument::Compact).toStdString(); +} } // namespace EMSCRIPTEN_BINDINGS(freecad_private_naming_source_probe) { emscripten::function("freecadPrivateNamingSourceProbe", &freecadPrivateNamingSourceProbe); + emscripten::function("freecadPrivateStringHasherSourceProbe", + &freecadPrivateStringHasherSourceProbe); + emscripten::function("freecadPrivateElementMapSourceProbe", + &freecadPrivateElementMapSourceProbe); + emscripten::function("freecadPrivateElementMapResourcesProbe", + &freecadPrivateElementMapResourcesProbe); + emscripten::function("freecadNamingCandidateAbiVersion", &freecadNamingCandidateAbiVersion); + emscripten::function("freecadNamingCandidateCapabilitiesJson", + &freecadNamingCandidateCapabilitiesJson); + emscripten::function("freecadNamingCandidateEvidenceJson", + &freecadNamingCandidateEvidenceJson); } diff --git a/native/freecad-naming-probe/shims/App/Application.h b/native/freecad-naming-probe/shims/App/Application.h new file mode 100644 index 0000000..ec69a60 --- /dev/null +++ b/native/freecad-naming-probe/shims/App/Application.h @@ -0,0 +1,41 @@ +#ifndef SRC_APP_APPLICATION_H_ +#define SRC_APP_APPLICATION_H_ + +#include +#include + +namespace App { + +class Document; + +class ProbeSignal +{ +public: + template + void connect(Callback&&) + {} +}; + +class Application +{ +public: + Document* getActiveDocument() const + { + return nullptr; + } + + ProbeSignal signalStartSaveDocument; + ProbeSignal signalFinishSaveDocument; + ProbeSignal signalStartRestoreDocument; + ProbeSignal signalFinishRestoreDocument; +}; + +inline Application& GetApplication() +{ + static Application application; + return application; +} + +} // namespace App + +#endif diff --git a/native/freecad-naming-probe/shims/App/Document.h b/native/freecad-naming-probe/shims/App/Document.h new file mode 100644 index 0000000..c9651e9 --- /dev/null +++ b/native/freecad-naming-probe/shims/App/Document.h @@ -0,0 +1,19 @@ +#ifndef SRC_APP_DOCUMENT_H_ +#define SRC_APP_DOCUMENT_H_ + +#include + +namespace App { + +class Document +{ +public: + DocumentObject* getObjectByID(long) const + { + return nullptr; + } +}; + +} // namespace App + +#endif diff --git a/native/freecad-naming-probe/shims/App/DocumentObject.h b/native/freecad-naming-probe/shims/App/DocumentObject.h new file mode 100644 index 0000000..27d9da5 --- /dev/null +++ b/native/freecad-naming-probe/shims/App/DocumentObject.h @@ -0,0 +1,31 @@ +#ifndef SRC_APP_DOCUMENTOBJECT_H_ +#define SRC_APP_DOCUMENTOBJECT_H_ + +#include + +namespace App { + +class DocumentObject +{ +public: + explicit DocumentObject(long id = 0) + : objectId(id) + {} + + long getID() const + { + return objectId; + } + + std::string getFullName() const + { + return "ProbeObject"; + } + +private: + long objectId; +}; + +} // namespace App + +#endif diff --git a/native/freecad-naming-probe/shims/App/StringHasher.h b/native/freecad-naming-probe/shims/App/StringHasher.h deleted file mode 100644 index aaf212f..0000000 --- a/native/freecad-naming-probe/shims/App/StringHasher.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#define APP_STRING_ID_H - -#include - -namespace App -{ -class StringIDRef -{ -public: - void toBytes(QByteArray& bytes) const - { - bytes.clear(); - } - - bool operator<(const StringIDRef&) const - { - return false; - } - - bool operator==(const StringIDRef&) const - { - return true; - } -}; -} // namespace App diff --git a/native/freecad-naming-probe/shims/Base/Console.h b/native/freecad-naming-probe/shims/Base/Console.h index db80a3a..c21374f 100644 --- a/native/freecad-naming-probe/shims/Base/Console.h +++ b/native/freecad-naming-probe/shims/Base/Console.h @@ -1,3 +1,22 @@ #pragma once +#include + +struct FreeCadNamingProbeLogInstance +{ + bool isEnabled(int) const + { + return false; + } +}; + +inline FreeCadNamingProbeLogInstance freecad_naming_probe_log_instance; + +#define FC_LOGLEVEL_LOG 3 +#define FC_LOGLEVEL_TRACE 4 +#define FC_LOG_INSTANCE freecad_naming_probe_log_instance #define FC_LOG_LEVEL_INIT(...) +#define FC_WARN(message) ((void)0) +#define FC_ERR(message) ((void)0) +#define FC_LOG(message) ((void)0) +#define FC_TRACE(message) ((void)0) diff --git a/native/freecad-naming-probe/shims/Base/Exception.h b/native/freecad-naming-probe/shims/Base/Exception.h new file mode 100644 index 0000000..ea34fc5 --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/Exception.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace Base { + +class Exception : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; + + void reportException() const {} +}; + +class RuntimeError : public Exception +{ +public: + using Exception::Exception; +}; + +class ValueError : public Exception +{ +public: + using Exception::Exception; +}; + +} // namespace Base + +#define FC_THROWM(type, message) \ + do { \ + std::ostringstream freecad_naming_probe_exception_stream; \ + freecad_naming_probe_exception_stream << message; \ + throw type(freecad_naming_probe_exception_stream.str()); \ + } while (false) diff --git a/native/freecad-naming-probe/shims/Base/Persistence.h b/native/freecad-naming-probe/shims/Base/Persistence.h new file mode 100644 index 0000000..5d85b5d --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/Persistence.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace Base { + +class Reader; +class Writer; +class XMLReader; + +class BaseClass +{ +public: + virtual ~BaseClass() = default; + virtual PyObject* getPyObject() + { + return nullptr; + } +}; + +class Persistence : public BaseClass +{ +public: + ~Persistence() override = default; + + virtual unsigned int getMemSize() const = 0; + virtual void Save(Writer&) const = 0; + virtual void Restore(XMLReader&) = 0; + virtual void SaveDocFile(Writer&) const = 0; + virtual void RestoreDocFile(Reader&) = 0; +}; + +} // namespace Base + +#define TYPESYSTEM_HEADER() +#define TYPESYSTEM_HEADER_WITH_OVERRIDE() +#define TYPESYSTEM_SOURCE(...) +#define TYPESYSTEM_SOURCE_ABSTRACT(...) diff --git a/native/freecad-naming-probe/shims/Base/PyObjectBase.h b/native/freecad-naming-probe/shims/Base/PyObjectBase.h new file mode 100644 index 0000000..9cf6fa2 --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/PyObjectBase.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/native/freecad-naming-probe/shims/Base/Reader.h b/native/freecad-naming-probe/shims/Base/Reader.h new file mode 100644 index 0000000..a89934f --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/Reader.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace Base { + +class Reader : public std::istream +{ +public: + Reader() + : std::istream(nullptr) + {} +}; + +class XMLReader +{ +public: + template + T getAttribute(const char*) const + { + return T {}; + } + + bool hasAttribute(const char*) const + { + return false; + } + + void readElement(const char*) {} + void readEndElement(const char*) {} + void addFile(const char*, void*) {} + + std::istream& beginCharStream() + { + return stream; + } + + int FileVersion = 1; + +private: + std::istringstream stream; +}; + +} // namespace Base diff --git a/native/freecad-naming-probe/shims/Base/Stream.h b/native/freecad-naming-probe/shims/Base/Stream.h new file mode 100644 index 0000000..5aec1fb --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/Stream.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace Base { + +class TextOutputStream +{ +public: + explicit TextOutputStream(std::ostream& stream) + : output(stream) + {} + + TextOutputStream& operator<<(const char* value) + { + output << value; + return *this; + } + +private: + std::ostream& output; +}; + +class TextInputStream +{ +public: + explicit TextInputStream(std::istream& stream) + : input(stream) + {} + + TextInputStream& operator>>(std::string& value) + { + input >> value; + return *this; + } + +private: + std::istream& input; +}; + +} // namespace Base diff --git a/native/freecad-naming-probe/shims/Base/Writer.h b/native/freecad-naming-probe/shims/Base/Writer.h new file mode 100644 index 0000000..615e91d --- /dev/null +++ b/native/freecad-naming-probe/shims/Base/Writer.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace Base { + +class Writer +{ +public: + std::ostream& Stream() + { + return stream; + } + + const char* ind() const + { + return ""; + } + + const char* addFile(const char* name, const void*) + { + return name; + } + + std::ostream& beginCharStream() + { + return stream; + } + + std::ostream& endCharStream() + { + return stream; + } + +private: + std::ostringstream stream; +}; + +} // namespace Base diff --git a/native/freecad-naming-probe/shims/CXX/Objects.hxx b/native/freecad-naming-probe/shims/CXX/Objects.hxx new file mode 100644 index 0000000..1fd9551 --- /dev/null +++ b/native/freecad-naming-probe/shims/CXX/Objects.hxx @@ -0,0 +1,9 @@ +#pragma once + +struct _object {}; +using PyObject = _object; + +inline _object freecad_naming_probe_py_none; + +#define Py_None (&freecad_naming_probe_py_none) +#define Py_INCREF(object) ((void)(object)) diff --git a/native/freecad-naming-probe/shims/FCConfig.h b/native/freecad-naming-probe/shims/FCConfig.h new file mode 100644 index 0000000..51b7a33 --- /dev/null +++ b/native/freecad-naming-probe/shims/FCConfig.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#ifndef AppExport +#define AppExport +#endif + +#ifndef BaseExport +#define BaseExport +#endif diff --git a/native/freecad-naming-probe/shims/ProbeAppHost.h b/native/freecad-naming-probe/shims/ProbeAppHost.h new file mode 100644 index 0000000..919fc03 --- /dev/null +++ b/native/freecad-naming-probe/shims/ProbeAppHost.h @@ -0,0 +1,5 @@ +#pragma once + +#include +#include +#include diff --git a/native/freecad-naming-probe/shims/StringHasherPy.h b/native/freecad-naming-probe/shims/StringHasherPy.h new file mode 100644 index 0000000..409d2e7 --- /dev/null +++ b/native/freecad-naming-probe/shims/StringHasherPy.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace App { + +class StringHasher; + +class StringHasherPy : public _object +{ +public: + explicit StringHasherPy(StringHasher*) {} +}; + +} // namespace App diff --git a/native/freecad-naming-probe/shims/StringIDPy.h b/native/freecad-naming-probe/shims/StringIDPy.h new file mode 100644 index 0000000..24d34d3 --- /dev/null +++ b/native/freecad-naming-probe/shims/StringIDPy.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace App { + +class StringID; + +class StringIDPy : public _object +{ +public: + explicit StringIDPy(StringID*) {} + + int _index = 0; +}; + +} // namespace App diff --git a/native/freecad-naming-probe/smoke-test.mjs b/native/freecad-naming-probe/smoke-test.mjs index 01f584d..82ef7a7 100644 --- a/native/freecad-naming-probe/smoke-test.mjs +++ b/native/freecad-naming-probe/smoke-test.mjs @@ -1,8 +1,22 @@ import createProbe from './dist/freecad-private-naming-source-probe.js' +import { inspectWasmStaticArchive } from '../../scripts/freecad-naming-sdk-lib.mjs' + +const archive = await inspectWasmStaticArchive(new URL('./dist/libFreeCADPrivateNamingProbe.a', import.meta.url)) +if (archive.memberCount !== 7 || archive.wasmObjectCount !== 7 || archive.llvmBitcodeCount !== 0) { + throw new Error(`Unexpected FreeCAD private naming source archive: ${JSON.stringify(archive)}`) +} const probe = await createProbe() const result = probe.freecadPrivateNamingSourceProbe() if (result !== 'Edge12;:M;CUT;:H2a:7,E') throw new Error(`Unexpected FreeCAD private naming source probe result: ${result}`) +const stringHasherResult = probe.freecadPrivateStringHasherSourceProbe() +if (stringHasherResult !== 'plain=1;hashed=2;mapped=5;index=12;related=2') { + throw new Error(`Unexpected FreeCAD StringHasher source probe result: ${stringHasherResult}`) +} +const elementMapResult = probe.freecadPrivateElementMapSourceProbe() +if (elementMapResult !== 'stored=#1;:H2a,F;tag=42;original=Edge12;:M;CUT;serialized=115;restored=1') { + throw new Error(`Unexpected FreeCAD ElementMap source probe result: ${elementMapResult}`) +} for (const forbidden of ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']) { if (typeof probe[forbidden] === 'function') throw new Error(`Source prerequisite probe must not export production ABI callback ${forbidden}.`) } @@ -10,7 +24,22 @@ console.log(JSON.stringify({ status: 'source-prerequisite-pass', freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', - linkedSources: ['App/IndexedName.cpp', 'App/MappedName.cpp'], + linkedSources: [ + 'App/IndexedName.cpp', + 'App/MappedName.cpp', + 'App/StringHasher.cpp', + 'App/MappedElement.cpp', + 'App/ElementNamingUtils.cpp', + 'App/ElementMap.cpp', + 'Base/Handle.cpp', + ], + sourceArchive: { + name: 'libFreeCADPrivateNamingProbe.a', + ...archive, + productionEligible: false, + }, result, + stringHasherResult, + elementMapResult, productionWorkerLinked: false, }, null, 2)) diff --git a/native/occt-history/README.md b/native/occt-history/README.md index 9f433e0..afd3422 100644 --- a/native/occt-history/README.md +++ b/native/occt-history/README.md @@ -44,19 +44,48 @@ establishes ABI linkage; exact promotion additionally requires valid MappedNameRef, StringHasher, ElementMap2, stage history, and round-trip evidence. The repository also carries a prerequisite-only source probe. It builds QtBase -6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp` and -`MappedName.cpp` sources without exporting the production naming callbacks: +6.8.2 `Qt6Core` for wasm, then compiles the locked FreeCAD `IndexedName.cpp`, +`MappedName.cpp`, `StringHasher.cpp`, `MappedElement.cpp`, +`ElementNamingUtils.cpp`, `ElementMap.cpp`, and `Base/Handle.cpp` sources without +exporting the production naming callbacks: ```bash ./npmw run build:qt6-wasm-core ./npmw run build:freecad-naming-source-probe ./npmw run test:freecad-naming-source-probe +./npmw run check:freecad-naming-sdk-readiness ./npmw run check:freecad-private-naming-boundary ``` Passing this probe establishes that the private source subset and toolchain are -cross-compilable. It does not link the production OCCT Worker, does not close -EX-TSN-02, and does not change `systemExact=false`. +cross-compilable. Its runtime test covers mapped-name parsing, StringHasher +deduplication, SHA-1 threshold handling, indexed mapped-name references, +ElementMap encoding/lookup/history/save/restore, and stable mapped-element +ordering. It also feeds native MappedNameRef, StringHasher, and ElementMap2 +resources through the strict Web ABI validator using deliberately separate +`freecadNamingCandidate*` callback names. + +The seven locked FreeCAD source units are first compiled into the isolated +`libFreeCADPrivateNamingProbe.a` archive. The smoke test checks that it contains +exactly seven wasm object members before linking the runnable probe. This +archive remains host-adapter-bound and is not a substitute for FreeCADBase or +FreeCADApp. + +Persistence, Python wrappers, type-system, logging, and Application/Document +lifecycle interfaces remain standalone host adapters. FreeCADApp/Part/Python +static libraries, real Application/Document integration, OCCT builder context, +and the production bridge are not linked. The candidate callbacks are never +published to the production Worker. The probe therefore does not close +EX-TSN-02 and does not change `systemExact=false`. + +`check:freecad-naming-sdk-readiness` audits the pre-production SDK plan without +publishing anything. It parses every present `.a` archive and rejects native +ELF members; the current local resource set verifies QtCore plus its bundled +Pcre2/Zlib wasm dependencies, but still lacks FreeCADBase, FreeCADApp, Part, +Python, and the production bridge. `generate:freecad-naming-sdk-manifest` is +fail-closed and writes no manifest until all inputs are present. The complete +SDK checker applies the same wasm-archive rule, so an x86 static library cannot +satisfy the manifest by hash alone. The versioned JSON request carries document `objectId`, stable positive `objectTag`, prior naming evidence, result object identity, result tag, stage diff --git a/native/occt-history/build.sh b/native/occt-history/build.sh index 14c1ab1..f0c2907 100755 --- a/native/occt-history/build.sh +++ b/native/occt-history/build.sh @@ -20,6 +20,7 @@ if [[ -z "${OCCT_BUILD_DIR:-}" ]]; then fi DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}" PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}" +PUBLISH_ARTIFACT="${OCCT_HISTORY_PUBLISH:-1}" JOBS="${JOBS:-$(nproc)}" FREECAD_WASM_SDK_DIR="${FREECAD_WASM_SDK_DIR:-}" @@ -40,11 +41,18 @@ if ! command -v emcmake >/dev/null || ! command -v em++ >/dev/null; then exit 1 fi +OCCT_CXX_FLAGS="-fexceptions" +OCCT_CXX_RELEASE_FLAGS="-O0 -DNDEBUG -fexceptions" +if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then + OCCT_CXX_FLAGS+=" -pthread" + OCCT_CXX_RELEASE_FLAGS+=" -pthread" +fi + emcmake cmake -S "${OCCT_SOURCE_DIR}" -B "${OCCT_BUILD_DIR}" -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_MAKE_PROGRAM="$(command -v ninja)" \ - -DCMAKE_CXX_FLAGS="-fexceptions" \ - -DCMAKE_CXX_FLAGS_RELEASE="-O0 -DNDEBUG -fexceptions" \ + -DCMAKE_CXX_FLAGS="${OCCT_CXX_FLAGS}" \ + -DCMAKE_CXX_FLAGS_RELEASE="${OCCT_CXX_RELEASE_FLAGS}" \ -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF \ -DBUILD_LIBRARY_TYPE=Static \ -DBUILD_MODULE_FoundationClasses=ON \ @@ -74,18 +82,29 @@ SOURCES=("${ROOT_DIR}/native/occt-history/occt_history.cpp") INCLUDES=("-I${OCCT_BUILD_DIR}/include/opencascade" "-I${OCCT_SOURCE_DIR}/src") DEFINES=() FREECAD_LIBRARIES=() +FREECAD_LINK_OPTIONS=() if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then BRIDGE_SOURCE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(p.isAbsolute(m.namingBridge.source) ? m.namingBridge.source : p.resolve(process.argv[1],m.namingBridge.source))' "${FREECAD_WASM_SDK_DIR}")" SOURCES+=("${BRIDGE_SOURCE}") while IFS= read -r include_dir; do INCLUDES+=("-I${include_dir}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const d of m.includeDirs) console.log(p.isAbsolute(d) ? d : p.resolve(root,d))' "${FREECAD_WASM_SDK_DIR}") while IFS= read -r library; do FREECAD_LIBRARIES+=("${library}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const l of m.libraries) console.log(p.isAbsolute(l.path) ? l.path : p.resolve(root,l.path))' "${FREECAD_WASM_SDK_DIR}") + while IFS= read -r definition; do DEFINES+=("-D${definition}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); for (const d of m.compileOptions.definitions) console.log(d)' "${FREECAD_WASM_SDK_DIR}") + CXX_STANDARD="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(m.compileOptions.cxxStandard)' "${FREECAD_WASM_SDK_DIR}")" + FORCE_INCLUDE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.compileOptions.forceInclude; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")" + HOST_ADAPTER="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); const v=m.namingBridge.hostAdapter; console.log(p.isAbsolute(v) ? v : p.resolve(root,v))' "${FREECAD_WASM_SDK_DIR}")" + FREECAD_LINK_OPTIONS+=("-include" "${FORCE_INCLUDE}" "-pthread" "--pre-js" "${HOST_ADAPTER}" "-sPTHREAD_POOL_SIZE=2") + while IFS=$'\t' read -r asset_path preload_to; do + FREECAD_LINK_OPTIONS+=("--preload-file" "${asset_path}@${preload_to}") + done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const a of m.runtimeAssets) { const v=p.isAbsolute(a.path) ? a.path : p.resolve(root,a.path); console.log(`${v}\t${a.preloadTo}`) }' "${FREECAD_WASM_SDK_DIR}") DEFINES+=("-DBITBYBIT_FREECAD_NAMING_LINKED=1") +else + CXX_STANDARD="c++17" fi -em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \ +em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" "${FREECAD_LINK_OPTIONS[@]}" \ -I"${OCCT_BUILD_DIR}/include/opencascade" \ -I"${OCCT_SOURCE_DIR}/src" \ - -std=c++17 -O2 -fexceptions --bind \ + -std="${CXX_STANDARD}" -O2 -fexceptions --bind \ -Wl,--start-group \ "${OCCT_BUILD_DIR}/lin32/clang/lib/libTKBO.a" \ "${OCCT_BUILD_DIR}/lin32/clang/lib/libTKFillet.a" \ @@ -112,7 +131,13 @@ em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \ -o "${DIST_DIR}/bitbybit-occt-history.js" cp "${ROOT_DIR}/native/occt-history/package.json" "${DIST_DIR}/package.json" -mkdir -p "${PUBLIC_DIR}" -cp "${DIST_DIR}/bitbybit-occt-history.js" "${PUBLIC_DIR}/bitbybit-occt-history.js" -cp "${DIST_DIR}/bitbybit-occt-history.wasm" "${PUBLIC_DIR}/bitbybit-occt-history.wasm" -sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm" +if [[ "${PUBLISH_ARTIFACT}" == "1" ]]; then + mkdir -p "${PUBLIC_DIR}" + for artifact in "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data,worker.js}; do + [[ -f "${artifact}" ]] && cp "${artifact}" "${PUBLIC_DIR}/$(basename "${artifact}")" + done +elif [[ "${PUBLISH_ARTIFACT}" != "0" ]]; then + echo "OCCT_HISTORY_PUBLISH must be 0 or 1." >&2 + exit 1 +fi +sha256sum "${DIST_DIR}"/bitbybit-occt-history.{js,wasm,data} 2>/dev/null || sha256sum "${DIST_DIR}/bitbybit-occt-history.js" "${DIST_DIR}/bitbybit-occt-history.wasm" diff --git a/native/occt-history/freecad-naming-candidate-smoke.ts b/native/occt-history/freecad-naming-candidate-smoke.ts new file mode 100644 index 0000000..7c84bde --- /dev/null +++ b/native/occt-history/freecad-naming-candidate-smoke.ts @@ -0,0 +1,149 @@ +import { pathToFileURL } from 'node:url' +import { createHash } from 'node:crypto' +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' +import { dirname } from 'node:path' +import { resolve } from 'node:path' +import { + captureFreeCadPrivateNamingEvidence, + createFreeCadPrivateNamingAbiRequest, + probeFreeCadPrivateNamingAbi, + type NativeFreeCadNamingAbiModule, +} from '../../src/facade/nativeNamingAbi' + +type CandidateModule = NativeFreeCadNamingAbiModule & { + makeBox(x: number, y: number, z: number): unknown + shapeToStep(shape: unknown): string + booleanHistoryFromStep(objectStep: string, toolStep: string, operation: 'cut'): { + provider: 'occt-native' + occtVersion: string + hasModified: boolean + hasGenerated: boolean + hasDeleted: boolean + resultStep: string + records: Array<{ + relation: 'modified' | 'generated' | 'deleted' + source: string + kind: 'face' | 'edge' | 'vertex' + resultKind?: 'face' | 'edge' | 'vertex' + sourceIndex: number + resultIndex?: number + resultIndexes?: number[] + }> + } +} + +const dist = resolve(process.env.FREECAD_NAMING_WORKER_DIST ?? '.cache/candidates/freecad-naming-worker') +const moduleUrl = pathToFileURL(resolve(dist, 'bitbybit-occt-history.js')).href +const createCandidate = (await import(moduleUrl)).default as (options: { locateFile(path: string): string }) => Promise +const candidate = await createCandidate({ locateFile: (path) => resolve(dist, path) }) +const probe = probeFreeCadPrivateNamingAbi(candidate) +if (probe.availability !== 'available') throw new Error(`Candidate Worker naming ABI probe failed: ${probe.reason}`) + +const object = candidate.makeBox(10, 10, 10) +const tool = candidate.makeBox(5, 5, 5) +const objectStep = candidate.shapeToStep(object) +const toolStep = candidate.shapeToStep(tool) +const history = candidate.booleanHistoryFromStep(objectStep, toolStep, 'cut') +const record = history.records.find((entry) => entry.relation !== 'deleted' + && Number.isSafeInteger(entry.sourceIndex) + && (Number.isSafeInteger(entry.resultIndex) || entry.resultIndexes?.some(Number.isSafeInteger))) +if (!record) throw new Error('Candidate Worker OCCT cut returned no usable native history record.') +const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex! : record.resultIndexes!.find(Number.isSafeInteger)! +const selectedRecord = { ...record, resultIndex, resultIndexes: undefined } +const evidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ + requestId: 'candidate-worker-cut-1', + documentId: 'candidate-worker-document', + documentVersion: 1, + operationId: 'candidate-worker-cut', + operation: 'cut', + stageId: 'candidate-worker:stage:1', + resultObjectId: 'candidate-worker:result:1', + resultObjectTag: 100, + inputs: [ + { inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }, + { inputId: 'tool', objectId: 'candidate-tool', role: 'tool', objectTag: 43, step: toolStep }, + ], + stages: [{ stageId: 'candidate-worker:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], + resultStep: history.resultStep, + history: { ...history, records: [selectedRecord] }, +}), probe) +if (!evidence?.elementMap2 || !evidence.stringHasher || evidence.mappedNames?.length !== 1) throw new Error('Candidate Worker returned incomplete FreeCAD naming evidence for OCCT cut history.') +const secondEvidence = captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ + requestId: 'candidate-worker-cut-2', + documentId: 'candidate-worker-document', + documentVersion: 2, + operationId: 'candidate-worker-cut-2', + operation: 'cut', + stageId: 'candidate-worker:stage:2', + resultObjectId: 'candidate-worker:result:2', + resultObjectTag: 101, + inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:1', role: 'object', stageId: 'candidate-worker:stage:1', objectTag: 100, step: history.resultStep, namingEvidence: evidence }], + stages: [{ stageId: 'candidate-worker:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + resultStep: history.resultStep, + history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] }, +}), probe) +const thirdEvidence = secondEvidence && captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ + requestId: 'candidate-worker-cut-3', + documentId: 'candidate-worker-document', + documentVersion: 3, + operationId: 'candidate-worker-cut-3', + operation: 'cut', + stageId: 'candidate-worker:stage:3', + resultObjectId: 'candidate-worker:result:3', + resultObjectTag: 102, + inputs: [{ inputId: 'object', objectId: 'candidate-worker:result:2', role: 'object', stageId: 'candidate-worker:stage:2', objectTag: 101, step: history.resultStep, namingEvidence: secondEvidence }], + stages: [{ stageId: 'candidate-worker:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + resultStep: history.resultStep, + history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind ?? record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] }, +}), probe) +if (!secondEvidence?.stringHasher || !thirdEvidence?.stringHasher || secondEvidence.stringHasher.entries.length < 1 || thirdEvidence.stringHasher.entries.length <= secondEvidence.stringHasher.entries.length) throw new Error('Candidate Worker did not restore non-empty StringHasher evidence across three stages.') + +let rejected = false +try { + captureFreeCadPrivateNamingEvidence(candidate, createFreeCadPrivateNamingAbiRequest({ + requestId: 'candidate-worker-invalid', + documentId: 'candidate-worker-document', + documentVersion: 2, + operationId: 'candidate-worker-invalid', + operation: 'cut', + stageId: 'candidate-worker:invalid', + resultObjectId: 'candidate-worker:invalid-result', + resultObjectTag: 101, + inputs: [{ inputId: 'object', objectId: 'candidate-object', role: 'object', objectTag: 42, step: objectStep }], + stages: [{ stageId: 'candidate-worker:invalid', operation: 'cut', inputIds: ['object'], ordinal: 0 }], + history: { ...history, records: [] }, + }), probe) +} catch (error) { + if (!(error instanceof Error) || !error.message.includes('requires inputs and native history records')) throw error + rejected = true +} +if (!rejected) throw new Error('Candidate Worker accepted incomplete native history.') + +const result = { + status: 'freecad-naming-candidate-worker-pass', + occtVersion: history.occtVersion, + callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'], + namingEvidence: { + mappedNames: evidence.mappedNames.length, + stringHasherEntries: evidence.stringHasher.entries.length, + elementMaps: evidence.elementMap2.maps.length, + }, + chainedStage: { mappedNames: secondEvidence.mappedNames.length, stringHasherEntries: secondEvidence.stringHasher.entries.length }, + thirdStage: { mappedNames: thirdEvidence.mappedNames.length, stringHasherEntries: thirdEvidence.stringHasher.entries.length }, + invalidHistoryRejected: true, + candidateOnly: true, + productionPublication: false, + productionWorkerLinked: false, +} +const reportPath = process.env.FREECAD_NAMING_WORKER_REPORT +if (reportPath) { + const artifacts = await Promise.all(['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'].map(async (name) => { + const path = resolve(dist, name) + const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)]) + return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') } + })) + const absoluteReportPath = resolve(reportPath) + await mkdir(dirname(absoluteReportPath), { recursive: true }) + await writeFile(absoluteReportPath, `${JSON.stringify({ schemaVersion: 1, ...result, artifacts }, null, 2)}\n`) +} +console.log(JSON.stringify(result, null, 2)) diff --git a/native/occt-history/freecad-wasm-sdk-manifest.example.json b/native/occt-history/freecad-wasm-sdk-manifest.example.json index 3ce1755..e847483 100644 --- a/native/occt-history/freecad-wasm-sdk-manifest.example.json +++ b/native/occt-history/freecad-wasm-sdk-manifest.example.json @@ -14,15 +14,60 @@ { "name": "FreeCADApp", "path": "lib/libFreeCADApp.a", "sha256": "replace-with-64-lowercase-hex-digits" }, { "name": "Part", "path": "lib/libPart.a", "sha256": "replace-with-64-lowercase-hex-digits" }, { "name": "QtCore", "path": "lib/libQt6Core.a", "sha256": "replace-with-64-lowercase-hex-digits" }, - { "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" } + { "name": "QtConcurrent", "path": "lib/libQt6Concurrent.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "QtNetwork", "path": "lib/libQt6Network.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "QtXml", "path": "lib/libQt6Xml.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "QtBundledPcre2", "path": "lib/libQt6BundledPcre2.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "QtBundledZLIB", "path": "lib/libQt6BundledZLIB.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "yaml-cpp", "path": "lib/libyaml-cpp.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "ICUCommon", "path": "lib/libicuuc.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "ICUI18N", "path": "lib/libicui18n.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "ICUData", "path": "lib/libicudata.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "XercesC", "path": "lib/libxerces-c.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "BoostProgramOptions", "path": "lib/libboost_program_options.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "BoostRegex", "path": "lib/libboost_regex.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "BoostThread", "path": "lib/libboost_thread.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "BoostDateTime", "path": "lib/libboost_date_time.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "BoostAtomic", "path": "lib/libboost_atomic.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonMpdecimal", "path": "lib/python-deps/libmpdec.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonExpat", "path": "lib/python-deps/libexpat.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonHaclSha2", "path": "lib/python-deps/libHacl_Hash_SHA2.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonZlib", "path": "lib/python-deps/libz.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonBzip2", "path": "lib/python-deps/libbz2.a", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "name": "PythonSqlite3", "path": "lib/python-deps/libsqlite3-mt.a", "sha256": "replace-with-64-lowercase-hex-digits" } ], "namingBridge": { "source": "src/freecad_naming_bridge.cpp", "sha256": "replace-with-64-lowercase-hex-digits", + "hostAdapter": "runtime/freecad_naming_pre.js", + "hostAdapterSha256": "replace-with-64-lowercase-hex-digits", "exports": [ "freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson" ] + }, + "compileOptions": { + "cxxStandard": "c++20", + "pthread": true, + "forceInclude": "include/freecad-wasm-sdk-compat.h", + "forceIncludeSha256": "replace-with-64-lowercase-hex-digits", + "definitions": ["__linux__=1", "QT_NO_KEYWORDS", "HAVE_CONFIG_H", "PYCXX_6_2_COMPATIBILITY"] + }, + "runtimeAssets": [{ + "name": "PythonWasmStdlib", + "path": "runtime/python", + "preloadTo": "/usr/local", + "files": [ + { "path": "lib/python313.zip", "sha256": "replace-with-64-lowercase-hex-digits" }, + { "path": "lib/python3.13/os.py", "sha256": "replace-with-64-lowercase-hex-digits" } + ] + }], + "productionPublication": false, + "boundary": { + "freecadNamingBuildStatus": "contract-only", + "exTsn02": "in_progress", + "systemExact": false } } diff --git a/package.json b/package.json index 9cd035c..88ffbe0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,16 @@ "test:golden:freecad:failures": "node scripts/run-freecad-golden-failures.mjs", "test:golden:freecad:family-failures": "node scripts/run-freecad-golden-failures.mjs --manifest=fixtures/freecad-golden/feature-families/failures/manifest.json", "check:occt-history-artifact": "node scripts/check-occt-history-browser-artifact.mjs", + "check:freecad-naming-sdk-readiness": "node scripts/check-freecad-naming-sdk-readiness.mjs", + "check:freecad-wasm-sdk-build-plan": "node scripts/check-freecad-wasm-sdk-build-plan.mjs", + "check:freecad-naming-next-tasks": "node scripts/check-freecad-naming-next-tasks.mjs", + "configure:freecad-naming-sdk-candidate": "node scripts/configure-freecad-naming-sdk-candidate.mjs", + "build:freecad-naming-sdk-candidate": "bash scripts/build-freecad-naming-sdk-candidate.sh", + "build:freecad-naming-bridge-candidate": "bash scripts/build-freecad-naming-bridge-candidate.sh", + "test:freecad-naming-bridge-candidate": "tsx native/freecad-naming-bridge/smoke-test.ts", + "check:freecad-naming-bridge-candidate": "node scripts/check-freecad-naming-bridge-candidate.mjs", "check:freecad-naming-sdk": "node scripts/check-freecad-naming-sdk.mjs", + "generate:freecad-naming-sdk-manifest": "node scripts/generate-freecad-naming-sdk-manifest.mjs", "check:freecad-private-naming-boundary": "node scripts/check-freecad-private-naming-boundary.mjs", "test:browser-occt": "node scripts/run-browser-occt-harness.mjs", "test:freecad:threeway": "node scripts/run-freecad-threeway-boolean.mjs", @@ -63,7 +72,7 @@ "build:freecad-native": "node scripts/build-freecad-native.mjs", "check:freecad-desktop-oracle": "node scripts/check-freecad-desktop-oracle.mjs", "probe:freecad-sketcher-constraints": "node scripts/run-freecad-sketcher-constraint-oracle.mjs", - "check:freecad-sketcher-constraints": "node scripts/check-freecad-sketcher-constraint-oracle.mjs && node scripts/check-freecad-partdesign-profile-oracle.mjs && node scripts/check-freecad-partdesign-base-oracle.mjs && node scripts/check-freecad-partdesign-loft-oracle.mjs && node scripts/check-freecad-partdesign-dressup-oracle.mjs && node scripts/check-freecad-partdesign-transform-oracle.mjs && node scripts/check-freecad-partdesign-failure-oracle.mjs && node scripts/check-freecad-partdesign-revolution-groove-oracle.mjs && node scripts/check-freecad-part-builders-oracle.mjs && node scripts/check-freecad-sketcher-editor-oracle.mjs && npm run check:freecad-sketcher-partdesign-abi", + "check:freecad-sketcher-constraints": "node scripts/check-freecad-sketcher-constraint-oracle.mjs && node scripts/check-freecad-partdesign-profile-oracle.mjs && node scripts/check-freecad-partdesign-base-oracle.mjs && node scripts/check-freecad-partdesign-loft-oracle.mjs && node scripts/check-freecad-partdesign-dressup-oracle.mjs && node scripts/check-freecad-partdesign-transform-oracle.mjs && node scripts/check-freecad-partdesign-structure-oracle.mjs && node scripts/check-freecad-attachment-mode-oracle.mjs && node scripts/check-freecad-partdesign-failure-oracle.mjs && node scripts/check-freecad-partdesign-revolution-groove-oracle.mjs && node scripts/check-freecad-part-builders-oracle.mjs && node scripts/check-freecad-sketcher-editor-oracle.mjs && npm run check:freecad-sketcher-partdesign-abi", "check:freecad-sketcher-partdesign-abi": "tsx scripts/check-freecad-sketcher-partdesign-abi-contract.mjs", "probe:freecad-partdesign-profiles": "node scripts/run-freecad-partdesign-profile-oracle.mjs", "check:freecad-partdesign-profiles": "node scripts/check-freecad-partdesign-profile-oracle.mjs", @@ -75,6 +84,10 @@ "check:freecad-partdesign-dressup": "node scripts/check-freecad-partdesign-dressup-oracle.mjs", "probe:freecad-partdesign-transform": "node scripts/run-freecad-partdesign-transform-oracle.mjs", "check:freecad-partdesign-transform": "node scripts/check-freecad-partdesign-transform-oracle.mjs", + "probe:freecad-partdesign-structure": "node scripts/run-freecad-partdesign-structure-oracle.mjs", + "check:freecad-partdesign-structure": "node scripts/check-freecad-partdesign-structure-oracle.mjs", + "probe:freecad-attachment-modes": "node scripts/run-freecad-attachment-mode-oracle.mjs", + "check:freecad-attachment-modes": "node scripts/check-freecad-attachment-mode-oracle.mjs", "probe:freecad-sketcher-editor": "node scripts/run-freecad-sketcher-editor-oracle.mjs", "check:freecad-sketcher-editor": "node scripts/check-freecad-sketcher-editor-oracle.mjs", "test:freecad-fcstd-native": "tsx scripts/run-freecad-fcstd-native-oracle.ts", @@ -82,10 +95,20 @@ "check:freecad-cam-path": "node scripts/check-freecad-cam-path-oracle.mjs", "test:freecad-cam-path-report": "node --test tests/freecadCamPathOracle.test.mjs", "build:occt-history": "bash native/occt-history/build.sh", - "build:freecad-naming-worker": "FREECAD_WASM_NAMING_REQUIRED=1 bash native/occt-history/build.sh", + "build:freecad-naming-worker": "FREECAD_WASM_NAMING_REQUIRED=1 FREECAD_WASM_SDK_DIR=.cache/toolchains/freecad-naming-sdk OCCT_BUILD_DIR=.cache/bitbybit/occt-history-build-freecad-pthread OCCT_HISTORY_DIST_DIR=.cache/candidates/freecad-naming-worker OCCT_HISTORY_PUBLISH=0 bash native/occt-history/build.sh", + "test:freecad-naming-worker-candidate": "FREECAD_NAMING_WORKER_REPORT=.cache/toolchains/freecad-naming-sdk/candidate-worker-report.json tsx native/occt-history/freecad-naming-candidate-smoke.ts", + "test:chrome-freecad-naming-worker-candidate": "node scripts/run-chrome-freecad-naming-worker-candidate.mjs", + "check:chrome-freecad-naming-worker-candidate": "node scripts/check-chrome-freecad-naming-worker-candidate.mjs", "build:qt6-wasm-core": "bash scripts/build-qt6-wasm-core.sh", + "build:qt6-freecad-wasm": "bash scripts/build-qt6-freecad-wasm.sh", + "build:yaml-cpp-wasm": "bash scripts/build-yaml-cpp-wasm.sh", + "build:icu-wasm": "bash scripts/build-icu-wasm.sh", + "build:cpython-wasm": "bash scripts/build-cpython-wasm.sh", + "build:xerces-c-wasm": "bash scripts/build-xerces-c-wasm.sh", + "build:boost-wasm": "bash scripts/build-boost-wasm.sh", + "prepare:freecad-occt8-overlay": "bash scripts/prepare-freecad-occt8-overlay.sh", "build:freecad-naming-source-probe": "bash scripts/build-freecad-naming-source-probe.sh", - "test:freecad-naming-source-probe": "node native/freecad-naming-probe/smoke-test.mjs", + "test:freecad-naming-source-probe": "node native/freecad-naming-probe/smoke-test.mjs && tsx native/freecad-naming-probe/abi-candidate-smoke.ts", "test:freecad-private-naming-boundary": "node --test tests/freecadPrivateNamingBoundary.test.mjs", "test:occt-history": "node native/occt-history/smoke-test.mjs", "build:planegcs": "bash native/planegcs/build.sh", @@ -274,7 +297,7 @@ "test:browser-matrix": "node scripts/run-browser-matrix.mjs", "check:browser-matrix": "node scripts/check-browser-matrix.mjs", "check:runtime": "node scripts/check-runtime.mjs", - "verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:freecad-naming-sdk && npm run check:freecad-private-naming-boundary && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature", + "verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:freecad-naming-sdk-readiness && npm run check:freecad-wasm-sdk-build-plan && npm run check:freecad-naming-next-tasks && npm run check:freecad-naming-sdk && npm run check:freecad-private-naming-boundary && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature", "test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs", "check:chrome-native-thickness-history": "node scripts/check-chrome-native-thickness-history.mjs", "test:chrome-native-linear-pattern-history": "node scripts/run-chrome-native-linear-pattern-history.mjs", diff --git a/scripts/boost-emscripten-compiler-wrapper.sh b/scripts/boost-emscripten-compiler-wrapper.sh new file mode 100755 index 0000000..10b1525 --- /dev/null +++ b/scripts/boost-emscripten-compiler-wrapper.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == "-r" && "${2:-}" == "-o" && -n "${3:-}" ]]; then + output="$3" + shift 3 + exec emar rcs "${output}" "$@" +fi + +exec emcc "$@" diff --git a/scripts/build-boost-wasm.sh b/scripts/build-boost-wasm.sh new file mode 100755 index 0000000..e4a4877 --- /dev/null +++ b/scripts/build-boost-wasm.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BOOST_ROOT="${BOOST_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/boost}" +DOWNLOAD_DIR="${BOOST_ROOT}/downloads" +SOURCE_DIR="${BOOST_ROOT}/src" +INSTALL_DIR="${BOOST_ROOT}/install-wasm" +ARCHIVE="${DOWNLOAD_DIR}/boost_1_83_0.tar.bz2" +DOWNLOAD_URL="${BOOST_DOWNLOAD_URL:-https://mirrors.aliyun.com/blfs/conglomeration/boost/boost_1_83_0.tar.bz2}" +EXPECTED_SHA256="6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e" +JOBS="${JOBS:-$(nproc)}" + +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "Boost wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi +mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${INSTALL_DIR}" +if [[ ! -f "${ARCHIVE}" ]]; then + curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}" + mv "${ARCHIVE}.part" "${ARCHIVE}" +fi +[[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_SHA256}" ]] || { echo "Boost 1.83.0 archive SHA-256 mismatch." >&2; exit 1; } +if [[ ! -x "${SOURCE_DIR}/bootstrap.sh" ]]; then + tar -xjf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1 +fi +if [[ ! -x "${SOURCE_DIR}/b2" ]]; then + (cd "${SOURCE_DIR}" && ./bootstrap.sh --with-libraries=program_options,regex,thread,date_time) +fi +cp -f "${ROOT_DIR}/scripts/boost-emscripten-compiler-wrapper.sh" "${SOURCE_DIR}/boost-emscripten-compiler-wrapper.sh" +chmod +x "${SOURCE_DIR}/boost-emscripten-compiler-wrapper.sh" +(cd "${SOURCE_DIR}" && ./b2 --user-config="${ROOT_DIR}/config/boost-emscripten-user-config.jam" \ + toolset=emscripten-3.1.69 link=static threading=multi variant=release \ + --with-program_options --with-regex --with-thread --with-date_time \ + --prefix="${INSTALL_DIR}" cxxflags=-pthread linkflags=-pthread install -j"${JOBS}") +for name in program_options regex thread date_time atomic; do + cp -f "${INSTALL_DIR}/lib/libboost_${name}.bc" "${INSTALL_DIR}/lib/libboost_${name}.a" +done + +cd "${ROOT_DIR}" +node --input-type=module - "${INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[2] +const archives = {} +for (const name of ['program_options', 'regex', 'thread', 'date_time', 'atomic']) { + archives[name] = await inspectWasmStaticArchive(`${install}/lib/libboost_${name}.a`) +} +console.log(JSON.stringify({ status: 'boost-wasm-pass', version: '1.83.0', pthread: true, archives }, null, 2)) +NODE diff --git a/scripts/build-cpython-wasm.sh b/scripts/build-cpython-wasm.sh new file mode 100644 index 0000000..7d03ea2 --- /dev/null +++ b/scripts/build-cpython-wasm.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_ROOT="${CPYTHON_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/python}" +DOWNLOAD_DIR="${PYTHON_ROOT}/downloads" +SOURCE_DIR="${PYTHON_ROOT}/src" +BUILD_DIR="${PYTHON_ROOT}/build-wasm" +INSTALL_DIR="${PYTHON_ROOT}/install-wasm" +EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache" +ARCHIVE="${DOWNLOAD_DIR}/Python-3.13.5.tar.xz" +DOWNLOAD_URL="${CPYTHON_DOWNLOAD_URL:-https://repo.huaweicloud.com/python/3.13.5/Python-3.13.5.tar.xz}" +EXPECTED_MD5="dbaa8833aa736eddbb18a6a6ae0c10fa" +EXPECTED_SHA256="93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640" +SOURCE_DATE_EPOCH_VALUE="1735689600" +JOBS="${JOBS:-$(nproc)}" + +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "CPython wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi +if [[ "$(python3.13 --version | awk '{print $2}')" != "3.13.5" ]]; then + echo "CPython wasm cross-build requires host Python 3.13.5." >&2 + exit 1 +fi +mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${BUILD_DIR}" "${INSTALL_DIR}/include/python3.13" "${INSTALL_DIR}/lib" +if [[ ! -f "${ARCHIVE}" ]]; then + curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}" + mv "${ARCHIVE}.part" "${ARCHIVE}" +fi +[[ "$(md5sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_MD5}" ]] || { echo "CPython archive MD5 mismatch." >&2; exit 1; } +[[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" == "${EXPECTED_SHA256}" ]] || { echo "CPython archive SHA-256 mismatch." >&2; exit 1; } +if [[ ! -f "${SOURCE_DIR}/configure" ]]; then + tar -xJf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1 +fi + +( + cd "${BUILD_DIR}" + SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" \ + CONFIG_SITE="${SOURCE_DIR}/Tools/wasm/config.site-wasm32-emscripten" \ + EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" emconfigure "${SOURCE_DIR}/configure" -C \ + --host=wasm32-unknown-emscripten \ + --build="$("${SOURCE_DIR}/config.guess")" \ + --with-emscripten-target=node \ + --enable-wasm-pthreads \ + --disable-wasm-dynamic-linking \ + --with-build-python=/usr/bin/python3.13 \ + --with-ensurepip=no \ + --disable-test-modules \ + --without-mimalloc +) +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" \ + emmake make -C "${BUILD_DIR}" -j"${JOBS}" libpython3.13.a pybuilddir.txt +PYTHON_SYSCONFIG_DATA="${BUILD_DIR}/$(tr -d '\n' < "${BUILD_DIR}/pybuilddir.txt")/_sysconfigdata__emscripten_wasm32-emscripten.py" +[[ -s "${PYTHON_SYSCONFIG_DATA}" ]] || { echo "CPython wasm sysconfig data is missing." >&2; exit 1; } +touch --date="@${SOURCE_DATE_EPOCH_VALUE}" "${PYTHON_SYSCONFIG_DATA}" +rm -f "${BUILD_DIR}/usr/local/lib/python313.zip" +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH_VALUE}" EM_FROZEN_CACHE=1 EM_CACHE="${EM_CACHE_DIR}" \ + emmake make -C "${BUILD_DIR}" wasm_stdlib +cp -a "${SOURCE_DIR}/Include/." "${INSTALL_DIR}/include/python3.13/" +cp -f "${BUILD_DIR}/pyconfig.h" "${INSTALL_DIR}/include/python3.13/pyconfig.h" +cp -f "${BUILD_DIR}/libpython3.13.a" "${INSTALL_DIR}/lib/libpython3.13.a" +mkdir -p "${INSTALL_DIR}/lib/python-deps" "${INSTALL_DIR}/share/python-wasm-stdlib" +cp -f "${BUILD_DIR}/Modules/_decimal/libmpdec/libmpdec.a" "${INSTALL_DIR}/lib/python-deps/libmpdec.a" +cp -f "${BUILD_DIR}/Modules/expat/libexpat.a" "${INSTALL_DIR}/lib/python-deps/libexpat.a" +cp -f "${BUILD_DIR}/Modules/_hacl/libHacl_Hash_SHA2.a" "${INSTALL_DIR}/lib/python-deps/libHacl_Hash_SHA2.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libz.a" "${INSTALL_DIR}/lib/python-deps/libz.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libbz2.a" "${INSTALL_DIR}/lib/python-deps/libbz2.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libsqlite3-mt.a" "${INSTALL_DIR}/lib/python-deps/libsqlite3-mt.a" +cp -a "${BUILD_DIR}/usr/local/." "${INSTALL_DIR}/share/python-wasm-stdlib/" + +cd "${ROOT_DIR}" +node --input-type=module - "${INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[2] +const dependencies = ['libmpdec.a', 'libexpat.a', 'libHacl_Hash_SHA2.a', 'libz.a', 'libbz2.a', 'libsqlite3-mt.a'] +console.log(JSON.stringify({ + status: 'cpython-wasm-pass', + version: '3.13.5', + pthread: true, + archive: await inspectWasmStaticArchive(`${install}/lib/libpython3.13.a`), + dependencies: Object.fromEntries(await Promise.all(dependencies.map(async (name) => [name, await inspectWasmStaticArchive(`${install}/lib/python-deps/${name}`)]))), + wasmStdlib: `${install}/share/python-wasm-stdlib/lib/python313.zip`, +}, null, 2)) +NODE diff --git a/scripts/build-freecad-naming-bridge-candidate.sh b/scripts/build-freecad-naming-bridge-candidate.sh new file mode 100755 index 0000000..377c5e3 --- /dev/null +++ b/scripts/build-freecad-naming-bridge-candidate.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SDK_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk" +SOURCE_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/source-occt8" +BUILD_ROOT="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/build-wasm" +OCCT_BUILD="${ROOT_DIR}/.cache/bitbybit/occt-history-build-offline" +EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache" +DIST_DIR="${ROOT_DIR}/native/freecad-naming-bridge/dist" +BRIDGE_SOURCE="${ROOT_DIR}/native/freecad-naming-bridge/freecad_naming_bridge.cpp" +QT="${ROOT_DIR}/.cache/toolchains/qt6/install-wasm-freecad" +PYTHON="${ROOT_DIR}/.cache/toolchains/python/install-wasm" +ICU="${ROOT_DIR}/.cache/toolchains/icu/install-wasm" +XERCES="${ROOT_DIR}/.cache/toolchains/xerces-c/install-wasm" +BOOST="${ROOT_DIR}/.cache/toolchains/boost/install-wasm" +YAML="${ROOT_DIR}/.cache/toolchains/yaml-cpp/install-wasm" + +cd "${ROOT_DIR}" +node scripts/check-freecad-naming-sdk-readiness.mjs >/dev/null +for path in \ + "${SDK_ROOT}/lib/libFreeCADBase.a" \ + "${SDK_ROOT}/lib/libFreeCADApp.a" \ + "${SDK_ROOT}/lib/libPart.a" \ + "${PYTHON}/lib/python-deps/libmpdec.a" \ + "${PYTHON}/lib/python-deps/libexpat.a" \ + "${PYTHON}/lib/python-deps/libHacl_Hash_SHA2.a" \ + "${PYTHON}/lib/python-deps/libz.a" \ + "${PYTHON}/lib/python-deps/libbz2.a" \ + "${PYTHON}/lib/python-deps/libsqlite3-mt.a" \ + "${PYTHON}/share/python-wasm-stdlib/lib/python313.zip" \ + "${BRIDGE_SOURCE}"; do + [[ -s "${path}" ]] || { echo "Missing isolated bridge input: ${path}" >&2; exit 1; } +done +mkdir -p "${DIST_DIR}" + +mapfile -t OCCT_LIBRARIES < <(find "${OCCT_BUILD}/lin32/clang/lib" -maxdepth 1 -type f -name '*.a' | sort) +EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" em++ "${BRIDGE_SOURCE}" \ + -I"${SOURCE_ROOT}/src" \ + -I"${BUILD_ROOT}/src" \ + -I"${SOURCE_ROOT}/src/3rdParty/PyCXX" \ + -I"${OCCT_BUILD}/include/opencascade" \ + -I"${ROOT_DIR}/.cache/occt/occt/src/Deprecated/NCollectionAliases" \ + -I"${QT}/include" -I"${QT}/include/QtCore" \ + -I"${PYTHON}/include/python3.13" \ + -I"${XERCES}/include" -I"${ICU}/include" -I"${YAML}/include" \ + -I"${ROOT_DIR}/.cache/offline-sysroot/include" \ + -include "${ROOT_DIR}/config/freecad-wasm-sdk-compat.h" \ + -D__linux__=1 -DQT_NO_KEYWORDS -DHAVE_CONFIG_H -DPYCXX_6_2_COMPATIBILITY \ + -std=c++20 -O1 -fexceptions -pthread --bind \ + -Wl,--start-group \ + "${SDK_ROOT}/lib/libPart.a" \ + "${SDK_ROOT}/lib/libFreeCADApp.a" \ + "${SDK_ROOT}/lib/libFreeCADBase.a" \ + "${OCCT_LIBRARIES[@]}" \ + "${PYTHON}/lib/libpython3.13.a" \ + "${PYTHON}/lib/python-deps/libmpdec.a" \ + "${PYTHON}/lib/python-deps/libexpat.a" \ + "${PYTHON}/lib/python-deps/libHacl_Hash_SHA2.a" \ + "${PYTHON}/lib/python-deps/libz.a" \ + "${PYTHON}/lib/python-deps/libbz2.a" \ + "${PYTHON}/lib/python-deps/libsqlite3-mt.a" \ + "${XERCES}/lib/libxerces-c.a" \ + "${YAML}/lib/libyaml-cpp.a" \ + "${BOOST}/lib/libboost_program_options.a" \ + "${BOOST}/lib/libboost_regex.a" \ + "${BOOST}/lib/libboost_thread.a" \ + "${BOOST}/lib/libboost_date_time.a" \ + "${BOOST}/lib/libboost_atomic.a" \ + "${ICU}/lib/libicui18n.a" "${ICU}/lib/libicuuc.a" "${ICU}/lib/libicudata.a" \ + "${QT}/lib/libQt6Concurrent.a" "${QT}/lib/libQt6Network.a" "${QT}/lib/libQt6Xml.a" \ + "${QT}/lib/libQt6Core.a" "${QT}/lib/libQt6BundledPcre2.a" "${QT}/lib/libQt6BundledZLIB.a" \ + -Wl,--end-group \ + -sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=node \ + -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=268435456 -sMAXIMUM_MEMORY=2147483648 \ + -sNO_EXIT_RUNTIME=1 -sPTHREAD_POOL_SIZE=2 \ + --preload-file "${PYTHON}/share/python-wasm-stdlib@/usr/local" \ + --pre-js "${ROOT_DIR}/native/freecad-naming-bridge/pre.js" \ + -o "${DIST_DIR}/freecad-private-naming-bridge.js" + +FREECAD_NAMING_BRIDGE_REPORT="${SDK_ROOT}/isolated-bridge-report.json" \ + npx tsx native/freecad-naming-bridge/smoke-test.ts +sha256sum \ + "${DIST_DIR}/freecad-private-naming-bridge.js" \ + "${DIST_DIR}/freecad-private-naming-bridge.wasm" \ + "${DIST_DIR}/freecad-private-naming-bridge.data" +[[ ! -e "${ROOT_DIR}/public/native/freecad-naming-bridge" ]] || { echo "Isolated bridge must not be published under public/." >&2; exit 1; } +echo "Isolated bridge evidence: ${SDK_ROOT}/isolated-bridge-report.json" diff --git a/scripts/build-freecad-naming-sdk-candidate.sh b/scripts/build-freecad-naming-sdk-candidate.sh new file mode 100755 index 0000000..50dac40 --- /dev/null +++ b/scripts/build-freecad-naming-sdk-candidate.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SDK_ROOT="${FREECAD_WASM_SDK_OUTPUT_DIR:-${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk}" +BUILD_DIR="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/build-wasm" +REPORT_PATH="${SDK_ROOT}/candidate-build-report.json" +JOBS="${JOBS:-$(nproc)}" + +cd "${ROOT_DIR}" +node scripts/check-freecad-wasm-sdk-build-plan.mjs >/dev/null +if [[ "${FREECAD_WASM_REUSE_OVERLAY:-0}" != "1" ]]; then + bash scripts/prepare-freecad-occt8-overlay.sh +fi +if [[ "${FREECAD_WASM_REUSE_CONFIGURE:-0}" != "1" ]]; then + node scripts/configure-freecad-naming-sdk-candidate.mjs +fi + +cmake --build "${BUILD_DIR}" --target Part --parallel "${JOBS}" +mkdir -p "${SDK_ROOT}/lib" +cp -f "${BUILD_DIR}/lib/libFreeCADBase.so" "${SDK_ROOT}/lib/libFreeCADBase.a" +cp -f "${BUILD_DIR}/lib/libFreeCADApp.so" "${SDK_ROOT}/lib/libFreeCADApp.a" +cp -f "${BUILD_DIR}/Mod/Part/Part.so" "${SDK_ROOT}/lib/libPart.a" + +node --input-type=module - "${SDK_ROOT}" "${REPORT_PATH}" <<'NODE' +import { writeFile } from 'node:fs/promises' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' +import { relative, resolve } from 'node:path' +import { inspectWasmStaticArchive, sha256File } from './scripts/freecad-naming-sdk-lib.mjs' + +const run = promisify(execFile) +const root = process.cwd() +const sdkRoot = resolve(process.argv[2]) +const reportPath = resolve(process.argv[3]) +const specs = [ + { name: 'FreeCADBase', file: 'libFreeCADBase.a', requiredMembers: [] }, + { + name: 'FreeCADApp', + file: 'libFreeCADApp.a', + requiredMembers: ['ElementMap.cpp.o', 'MappedElement.cpp.o', 'MappedName.cpp.o', 'ElementNamingUtils.cpp.o', 'StringHasher.cpp.o'], + }, + { name: 'Part', file: 'libPart.a', requiredMembers: ['TopoShape.cpp.o', 'TopoShapeExpansion.cpp.o', 'TopoShapeMapper.cpp.o'] }, +] +const archives = [] +for (const spec of specs) { + const path = resolve(sdkRoot, 'lib', spec.file) + const inspected = await inspectWasmStaticArchive(path) + const members = (await run('emar', ['t', path], { maxBuffer: 16 * 1024 * 1024 })).stdout.trim().split('\n').filter(Boolean) + const missingMembers = spec.requiredMembers.filter((member) => !members.includes(member)) + if (missingMembers.length > 0) throw new Error(`${spec.name} archive is missing required members: ${missingMembers.join(', ')}`) + archives.push({ + name: spec.name, + path: relative(root, path), + sha256: await sha256File(path), + ...inspected, + requiredMembers: spec.requiredMembers, + }) +} +const report = { + schemaVersion: 1, + status: 'candidate-archives-built', + availability: 'candidate-archives-only', + target: 'wasm32-emscripten', + freecadVersion: '1.1.1', + sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', + emscriptenVersion: '3.1.69', + archives, + productionPublication: false, + productionManifestGenerated: false, + boundary: { + freecadNamingBuildStatus: 'contract-only', + exTsn02: 'in_progress', + systemExact: false, + workerLinked: false, + availability: 'unavailable', + callbacks: [], + }, +} +await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ ...report, report: relative(root, reportPath) }, null, 2)) +NODE + +git -C "${ROOT_DIR}/.cache/freecad/FreeCAD" diff --quiet +git -C "${ROOT_DIR}/.cache/freecad/FreeCAD" diff --cached --quiet diff --git a/scripts/build-freecad-naming-source-probe.sh b/scripts/build-freecad-naming-source-probe.sh index 24d75cc..5bd259b 100755 --- a/scripts/build-freecad-naming-source-probe.sh +++ b/scripts/build-freecad-naming-source-probe.sh @@ -6,18 +6,41 @@ FREECAD_SOURCE_DIR="${FREECAD_SOURCE_DIR:-${ROOT_DIR}/.cache/freecad/FreeCAD}" QT6_WASM_DIR="${QT6_WASM_DIR:-${ROOT_DIR}/.cache/toolchains/qt6/install-wasm}" OFFLINE_INCLUDE_DIR="${OFFLINE_INCLUDE_DIR:-${ROOT_DIR}/.cache/offline-sysroot/include}" DIST_DIR="${ROOT_DIR}/native/freecad-naming-probe/dist" +OBJECT_DIR="${DIST_DIR}/objects" +SOURCE_ARCHIVE="${DIST_DIR}/libFreeCADPrivateNamingProbe.a" SHIM_DIR="${ROOT_DIR}/native/freecad-naming-probe/shims" FREECAD_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" INDEXED_NAME_SHA256="73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435" MAPPED_NAME_SHA256="90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc" +STRING_HASHER_SHA256="fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c" +HANDLE_SHA256="8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2" +MAPPED_ELEMENT_SHA256="f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5" +ELEMENT_NAMING_UTILS_SHA256="fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3" +ELEMENT_MAP_SHA256="e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52" if [[ ! -d "${FREECAD_SOURCE_DIR}/.git" ]] || [[ "$(git -C "${FREECAD_SOURCE_DIR}" rev-parse HEAD)" != "${FREECAD_COMMIT}" ]]; then echo "FreeCAD private naming probe requires locked commit ${FREECAD_COMMIT}." >&2 exit 1 fi +if ! git -C "${FREECAD_SOURCE_DIR}" diff --quiet HEAD -- \ + src/App/IndexedName.cpp src/App/IndexedName.h \ + src/App/MappedName.cpp src/App/MappedName.h \ + src/App/MappedElement.cpp src/App/MappedElement.h \ + src/App/ElementNamingUtils.cpp src/App/ElementNamingUtils.h \ + src/App/ElementMap.cpp src/App/ElementMap.h \ + src/App/StringHasher.cpp src/App/StringHasher.h \ + src/Base/Handle.cpp src/Base/Handle.h; then + echo "FreeCAD private naming probe requires unmodified locked source inputs." >&2 + exit 1 +fi printf '%s %s\n' \ "${INDEXED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/IndexedName.cpp" \ - "${MAPPED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" | sha256sum -c - + "${MAPPED_NAME_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" \ + "${STRING_HASHER_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/StringHasher.cpp" \ + "${HANDLE_SHA256}" "${FREECAD_SOURCE_DIR}/src/Base/Handle.cpp" \ + "${MAPPED_ELEMENT_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/MappedElement.cpp" \ + "${ELEMENT_NAMING_UTILS_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/ElementNamingUtils.cpp" \ + "${ELEMENT_MAP_SHA256}" "${FREECAD_SOURCE_DIR}/src/App/ElementMap.cpp" | sha256sum -c - if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then echo "FreeCAD private naming probe requires Emscripten 3.1.69." >&2 exit 1 @@ -37,19 +60,38 @@ if [[ ! -s "${OFFLINE_INCLUDE_DIR}/boost/signals2/signal.hpp" ]]; then exit 1 fi -mkdir -p "${DIST_DIR}" -em++ \ - "${ROOT_DIR}/native/freecad-naming-probe/probe.cpp" \ - "${FREECAD_SOURCE_DIR}/src/App/IndexedName.cpp" \ - "${FREECAD_SOURCE_DIR}/src/App/MappedName.cpp" \ +mkdir -p "${DIST_DIR}" "${OBJECT_DIR}" +COMPILE_FLAGS=( -I"${SHIM_DIR}" \ - -include "${SHIM_DIR}/App/StringHasher.h" \ + -include "${SHIM_DIR}/ProbeAppHost.h" \ -I"${FREECAD_SOURCE_DIR}/src" \ -I"${QT6_WASM_DIR}/include" \ -I"${QT6_WASM_DIR}/include/QtCore" \ -I"${OFFLINE_INCLUDE_DIR}" \ - -std=c++17 -Oz -fexceptions --bind \ + -std=c++17 -Oz -fexceptions +) +SOURCE_OBJECTS=() +for source in \ + "App/IndexedName.cpp" \ + "App/MappedName.cpp" \ + "App/StringHasher.cpp" \ + "App/MappedElement.cpp" \ + "App/ElementNamingUtils.cpp" \ + "App/ElementMap.cpp" \ + "Base/Handle.cpp"; do + object="${OBJECT_DIR}/$(basename "${source}" .cpp).o" + em++ "${COMPILE_FLAGS[@]}" -c "${FREECAD_SOURCE_DIR}/src/${source}" -o "${object}" + SOURCE_OBJECTS+=("${object}") +done +ARCHIVE_TMP="${SOURCE_ARCHIVE}.$$.tmp" +emar rcs "${ARCHIVE_TMP}" "${SOURCE_OBJECTS[@]}" +mv "${ARCHIVE_TMP}" "${SOURCE_ARCHIVE}" + +em++ \ + "${ROOT_DIR}/native/freecad-naming-probe/probe.cpp" \ + "${COMPILE_FLAGS[@]}" --bind \ -Wl,--start-group \ + "${SOURCE_ARCHIVE}" \ "${QT6_WASM_DIR}/lib/libQt6Core.a" \ "${QT6_WASM_DIR}/lib/libQt6BundledPcre2.a" \ "${QT6_WASM_DIR}/lib/libQt6BundledZLIB.a" \ @@ -58,4 +100,4 @@ em++ \ -sALLOW_MEMORY_GROWTH=1 -sNO_EXIT_RUNTIME=1 \ -o "${DIST_DIR}/freecad-private-naming-source-probe.js" -sha256sum "${DIST_DIR}/freecad-private-naming-source-probe.js" "${DIST_DIR}/freecad-private-naming-source-probe.wasm" +sha256sum "${SOURCE_ARCHIVE}" "${DIST_DIR}/freecad-private-naming-source-probe.js" "${DIST_DIR}/freecad-private-naming-source-probe.wasm" diff --git a/scripts/build-icu-wasm.sh b/scripts/build-icu-wasm.sh new file mode 100644 index 0000000..af5cd30 --- /dev/null +++ b/scripts/build-icu-wasm.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ICU_ROOT="${ICU_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/icu}" +ICU_DOWNLOAD_DIR="${ICU_ROOT}/downloads" +ICU_SOURCE_DIR="${ICU_ROOT}/src" +ICU_INSTALL_DIR="${ICU_ROOT}/install-wasm" +EM_CACHE_DIR="${ROOT_DIR}/.cache/toolchains/emscripten-freecad-cache" +ARCHIVE="${ICU_DOWNLOAD_DIR}/icu4c-68_2-src.tgz" +DOWNLOAD_URL="${ICU_DOWNLOAD_URL:-https://mirrors.aliyun.com/blfs/conglomeration/icu/icu4c-68_2-src.tgz}" +EXPECTED_MD5="c21cbdfe31a1e325afe765a16f907d20" +EXPECTED_SHA256="c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625" +PORT_URL="https://github.com/unicode-org/icu/releases/download/release-68-2/icu4c-68_2-src.zip" + +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "ICU wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi +mkdir -p "${ICU_DOWNLOAD_DIR}" "${ICU_SOURCE_DIR}" "${ICU_INSTALL_DIR}/include" "${ICU_INSTALL_DIR}/lib" +if [[ ! -f "${ARCHIVE}" ]]; then + curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}" + mv "${ARCHIVE}.part" "${ARCHIVE}" +fi +if [[ "$(md5sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_MD5}" ]]; then + echo "ICU 68.2 archive MD5 mismatch." >&2 + exit 1 +fi +if [[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_SHA256}" ]]; then + echo "ICU 68.2 archive SHA-256 mismatch." >&2 + exit 1 +fi +if [[ ! -f "${ICU_SOURCE_DIR}/source/common/unicode/utypes.h" ]]; then + tar -xzf "${ARCHIVE}" -C "${ICU_SOURCE_DIR}" --strip-components=1 +fi + +mkdir -p "${EM_CACHE_DIR}/ports/icu/icu" +cp -a "${ICU_SOURCE_DIR}/." "${EM_CACHE_DIR}/ports/icu/icu/" +printf '%s\n' "${PORT_URL}" > "${EM_CACHE_DIR}/ports/icu/.emscripten_url" +EM_FROZEN_CACHE=0 EM_CACHE="${EM_CACHE_DIR}" em++ -xc++ /dev/null -pthread -sUSE_ICU=1 -o "${ICU_ROOT}/icu-port-smoke.js" + +cp -a "${EM_CACHE_DIR}/sysroot/include/unicode" "${ICU_INSTALL_DIR}/include/" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_common-mt.a" "${ICU_INSTALL_DIR}/lib/libicuuc.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_i18n-mt.a" "${ICU_INSTALL_DIR}/lib/libicui18n.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_stubdata-mt.a" "${ICU_INSTALL_DIR}/lib/libicudata.a" +cp -f "${EM_CACHE_DIR}/sysroot/lib/wasm32-emscripten/libicu_io-mt.a" "${ICU_INSTALL_DIR}/lib/libicuio.a" + +cd "${ROOT_DIR}" +node --input-type=module - "${ICU_INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[2] +const archives = {} +for (const name of ['icuuc', 'icui18n', 'icudata', 'icuio']) { + archives[name] = await inspectWasmStaticArchive(`${install}/lib/lib${name}.a`) +} +console.log(JSON.stringify({ status: 'icu-wasm-pass', version: '68.2', pthread: true, archives }, null, 2)) +NODE diff --git a/scripts/build-qt6-freecad-wasm.sh b/scripts/build-qt6-freecad-wasm.sh new file mode 100644 index 0000000..073a568 --- /dev/null +++ b/scripts/build-qt6-freecad-wasm.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +QT_ROOT="${QT6_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/qt6}" +QT_SOURCE_DIR="${QT_ROOT}/src" +QT_BUILD_DIR="${QT_ROOT}/build-wasm-freecad" +QT_INSTALL_DIR="${QT_ROOT}/install-wasm-freecad" +EMSDK_SHIM_DIR="${QT_ROOT}/emsdk-shim" +JOBS="${JOBS:-$(nproc)}" + +if [[ ! -x "${QT_SOURCE_DIR}/configure" ]]; then + echo "Qt 6.8.2 source is missing; run build:qt6-wasm-core or restore the offline Qt source first." >&2 + exit 1 +fi +if ! command -v emcc >/dev/null || ! command -v em++ >/dev/null; then + echo "Emscripten emcc/em++ are required." >&2 + exit 1 +fi +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "FreeCAD Qt wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi + +mkdir -p "${QT_BUILD_DIR}" "${QT_INSTALL_DIR}" +if [[ ! -f "${QT_BUILD_DIR}/build.ninja" ]]; then + ( + cd "${QT_BUILD_DIR}" + EMSDK="${EMSDK_SHIM_DIR}" "${QT_SOURCE_DIR}/configure" \ + -platform wasm-emscripten \ + -qt-host-path /usr \ + -prefix /qt \ + -extprefix "${QT_INSTALL_DIR}" \ + -static -release -optimize-size \ + -feature-thread \ + -no-gui -no-widgets -no-dbus \ + -nomake examples -nomake tests -nomake benchmarks -nomake manual-tests \ + -no-pch -no-sbom \ + -- -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DQT_BUILD_TOOLS_WHEN_CROSSCOMPILING=OFF \ + -DQT_FEATURE_wasm_simd128=OFF \ + -DQT_FEATURE_wasm_exceptions=OFF \ + -DQt6HostInfo_DIR=/usr/lib/x86_64-linux-gnu/cmake/Qt6HostInfo + ) +fi +if ! rg -q '^QT_FEATURE_thread:INTERNAL=ON$' "${QT_BUILD_DIR}/CMakeCache.txt"; then + echo "Existing FreeCAD Qt wasm build does not enable thread support: ${QT_BUILD_DIR}" >&2 + exit 1 +fi + +cmake --build "${QT_BUILD_DIR}" --target Core Concurrent Network Xml QTlsBackendCertOnlyPlugin QTlsBackendCertOnlyPlugin_init --parallel "${JOBS}" +cmake --install "${QT_BUILD_DIR}" --component Devel +for module in corelib concurrent network xml; do cmake --install "${QT_BUILD_DIR}/src/${module}"; done +cmake --install "${QT_BUILD_DIR}/src/plugins/tls/certonly" +mkdir -p "${QT_INSTALL_DIR}/mkspecs/common" +cmake -E copy_directory "${QT_SOURCE_DIR}/mkspecs/wasm-emscripten" "${QT_INSTALL_DIR}/mkspecs/wasm-emscripten" +cmake -E copy_directory "${QT_SOURCE_DIR}/mkspecs/common/wasm" "${QT_INSTALL_DIR}/mkspecs/common/wasm" +for helper in QtFeatureCommon.cmake "${QT_SOURCE_DIR}"/cmake/QtPublic*.cmake; do + cp -f "${QT_SOURCE_DIR}/cmake/${helper##*/}" "${QT_INSTALL_DIR}/lib/cmake/Qt6/${helper##*/}" +done +for library in Core Concurrent Network Xml BundledPcre2 BundledZLIB; do + cp -f "${QT_BUILD_DIR}/lib/libQt6${library}.a" "${QT_INSTALL_DIR}/lib/libQt6${library}.a" +done + +node --input-type=module - "${ROOT_DIR}" "${QT_INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[3] +const result = {} +for (const name of ['Core', 'Concurrent', 'Network', 'Xml', 'BundledPcre2', 'BundledZLIB']) { + result[name] = await inspectWasmStaticArchive(`${install}/lib/libQt6${name}.a`) +} +console.log(JSON.stringify({ status: 'qt6-freecad-wasm-pass', threadSupport: true, mkspec: `${install}/mkspecs/wasm-emscripten`, modules: result }, null, 2)) +NODE diff --git a/scripts/build-xerces-c-wasm.sh b/scripts/build-xerces-c-wasm.sh new file mode 100755 index 0000000..cc06b67 --- /dev/null +++ b/scripts/build-xerces-c-wasm.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +XERCES_ROOT="${XERCES_C_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/xerces-c}" +DOWNLOAD_DIR="${XERCES_ROOT}/downloads" +SOURCE_DIR="${XERCES_ROOT}/src" +BUILD_DIR="${XERCES_ROOT}/build-wasm" +INSTALL_DIR="${XERCES_ROOT}/install-wasm" +ICU_ROOT="${ICU_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/icu}" +ARCHIVE="${DOWNLOAD_DIR}/xerces-c-3.2.4.tar.xz" +DOWNLOAD_URL="${XERCES_C_DOWNLOAD_URL:-https://archive.apache.org/dist/xerces/c/3/sources/xerces-c-3.2.4.tar.xz}" +EXPECTED_SHA256="075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9" +JOBS="${JOBS:-$(nproc)}" + +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "Xerces-C wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi +for archive in libicuuc.a libicudata.a; do + if [[ ! -f "${ICU_ROOT}/install-wasm/lib/${archive}" ]]; then + echo "Xerces-C wasm SDK requires the staged ICU archive: ${archive}" >&2 + exit 1 + fi +done +mkdir -p "${DOWNLOAD_DIR}" "${SOURCE_DIR}" "${BUILD_DIR}" "${INSTALL_DIR}/lib" +if [[ ! -f "${ARCHIVE}" ]]; then + curl --fail --location --retry 3 --connect-timeout 15 --output "${ARCHIVE}.part" "${DOWNLOAD_URL}" + mv "${ARCHIVE}.part" "${ARCHIVE}" +fi +if [[ "$(sha256sum "${ARCHIVE}" | awk '{print $1}')" != "${EXPECTED_SHA256}" ]]; then + echo "Xerces-C 3.2.4 archive SHA-256 mismatch." >&2 + exit 1 +fi +if [[ ! -f "${SOURCE_DIR}/CMakeLists.txt" ]]; then + tar -xJf "${ARCHIVE}" -C "${SOURCE_DIR}" --strip-components=1 +fi + +emcmake cmake --fresh -S "${SOURCE_DIR}" -B "${BUILD_DIR}" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \ + -DBUILD_SHARED_LIBS=OFF \ + -Dnetwork=OFF \ + -Dtranscoder=icu \ + -Dmsgloader=inmemory \ + -Dthreads=ON \ + -Dsse2=OFF \ + -DICU_ROOT="${ICU_ROOT}/install-wasm" \ + -DICU_INCLUDE_DIR="${ICU_ROOT}/install-wasm/include" \ + -DICU_UC_LIBRARY_RELEASE="${ICU_ROOT}/install-wasm/lib/libicuuc.a" \ + -DICU_DATA_LIBRARY_RELEASE="${ICU_ROOT}/install-wasm/lib/libicudata.a" +cmake --build "${BUILD_DIR}" --target xerces-c --parallel "${JOBS}" +# Upstream assigns samples and the core library to the same runtime component. +# Install development files separately and stage the already-built archive directly. +cmake --install "${BUILD_DIR}" --component development +cp -f "${BUILD_DIR}/src/libxerces-c.a" "${INSTALL_DIR}/lib/libxerces-c.a" + +cd "${ROOT_DIR}" +node --input-type=module - "${INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[2] +console.log(JSON.stringify({ + status: 'xerces-c-wasm-pass', + version: '3.2.4', + archive: await inspectWasmStaticArchive(`${install}/lib/libxerces-c.a`), +}, null, 2)) +NODE diff --git a/scripts/build-yaml-cpp-wasm.sh b/scripts/build-yaml-cpp-wasm.sh new file mode 100644 index 0000000..08f9d37 --- /dev/null +++ b/scripts/build-yaml-cpp-wasm.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +YAML_ROOT="${YAML_CPP_WASM_CACHE_DIR:-${ROOT_DIR}/.cache/toolchains/yaml-cpp}" +YAML_SOURCE_DIR="${YAML_ROOT}/src" +YAML_BUILD_DIR="${YAML_ROOT}/build-wasm" +YAML_INSTALL_DIR="${YAML_ROOT}/install-wasm" +JOBS="${JOBS:-$(nproc)}" +LOCKED_COMMIT="f7320141120f720aecc4c32be25586e7da9eb978" + +if [[ ! -f "${YAML_SOURCE_DIR}/CMakeLists.txt" ]]; then + echo "yaml-cpp 0.8.0 source is missing: ${YAML_SOURCE_DIR}" >&2 + exit 1 +fi +if [[ "$(git -C "${YAML_SOURCE_DIR}" rev-parse HEAD)" != "${LOCKED_COMMIT}" ]]; then + echo "yaml-cpp source must be locked to ${LOCKED_COMMIT}." >&2 + exit 1 +fi +if [[ -n "$(git -C "${YAML_SOURCE_DIR}" status --porcelain)" ]]; then + echo "yaml-cpp source checkout must be clean." >&2 + exit 1 +fi +if [[ "$(emcc --version | sed -n '1s/.* \([0-9][0-9.]*\) .*/\1/p')" != "3.1.69" ]]; then + echo "yaml-cpp wasm SDK must use Emscripten 3.1.69." >&2 + exit 1 +fi + +emcmake cmake --fresh -S "${YAML_SOURCE_DIR}" -B "${YAML_BUILD_DIR}" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${YAML_INSTALL_DIR}" \ + -DYAML_BUILD_SHARED_LIBS=OFF \ + -DYAML_CPP_BUILD_TESTS=OFF \ + -DYAML_CPP_BUILD_TOOLS=OFF \ + -DYAML_CPP_INSTALL=ON \ + -DYAML_CPP_FORMAT_SOURCE=OFF +cmake --build "${YAML_BUILD_DIR}" --parallel "${JOBS}" +cmake --install "${YAML_BUILD_DIR}" + +cd "${ROOT_DIR}" +node --input-type=module - "${YAML_INSTALL_DIR}" <<'NODE' +import { inspectWasmStaticArchive } from './scripts/freecad-naming-sdk-lib.mjs' +const install = process.argv[2] +console.log(JSON.stringify({ + status: 'yaml-cpp-wasm-pass', + archive: await inspectWasmStaticArchive(`${install}/lib/libyaml-cpp.a`), +}, null, 2)) +NODE diff --git a/scripts/check-chrome-freecad-naming-worker-candidate.mjs b/scripts/check-chrome-freecad-naming-worker-candidate.mjs new file mode 100644 index 0000000..23023d9 --- /dev/null +++ b/scripts/check-chrome-freecad-naming-worker-candidate.mjs @@ -0,0 +1,19 @@ +import { createHash } from 'node:crypto' +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, '.cache/toolchains/freecad-naming-sdk/chrome-candidate-worker-report.json'), 'utf8')) +const fail = (message) => { throw new Error(`Chrome FreeCAD naming candidate Worker: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'pass' || report.browserId !== 'chrome' || report.crossOriginIsolated !== true) fail('live Chrome report is missing or failed.') +if (JSON.stringify(report.callbacks) !== JSON.stringify(['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'])) fail('callback set is incomplete.') +if (report.chainedStage?.mappedNames !== 1 || report.chainedStage?.stringHasherEntries < 1 || report.thirdStage?.mappedNames !== 1 || report.thirdStage?.stringHasherEntries <= report.chainedStage.stringHasherEntries || report.invalidHistoryRejected !== true) fail('three-stage native evidence closure or fail-closed result is incomplete.') +if (report.candidateOnly !== true || report.productionPublication !== false || report.productionWorkerLinked !== false) fail('candidate report crossed the production publication boundary.') +for (const artifact of report.artifacts ?? []) { + const path = resolve(root, '.cache/candidates/freecad-naming-worker', artifact.name) + const [content, size] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)]) + const sha256 = createHash('sha256').update(content).digest('hex') + if (artifact.bytes !== size || artifact.sha256 !== sha256) fail(`stale artifact evidence for ${artifact.name}.`) +} +if (report.artifacts?.length !== 3) fail('artifact evidence must include JS, WASM and DATA.') +console.log(JSON.stringify({ status: 'chrome-freecad-naming-candidate-worker-check-pass', generatedAt: report.generatedAt, callbacks: report.callbacks, chainedStage: report.chainedStage, thirdStage: report.thirdStage, candidateOnly: true, productionPublication: false, productionWorkerLinked: false }, null, 2)) diff --git a/scripts/check-freecad-attachment-mode-oracle.mjs b/scripts/check-freecad-attachment-mode-oracle.mjs new file mode 100644 index 0000000..55d11f9 --- /dev/null +++ b/scripts/check-freecad-attachment-mode-oracle.mjs @@ -0,0 +1,66 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-attachment-mode-oracle.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD attachment mode oracle: ${message}`) } +const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance +const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected) + +const modes = [ + 'Deactivated', 'Translate', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'TangentPlane', 'NormalToEdge', + 'FrenetNB', 'FrenetTN', 'FrenetTB', 'Concentric', 'SectionOfRevolution', 'ThreePointsPlane', 'ThreePointsNormal', 'Folding', + 'ObjectX', 'ObjectY', 'ObjectZ', 'AxisOfCurvature', 'Directrix1', 'Directrix2', 'Asymptote1', 'Asymptote2', 'Tangent', 'Normal', + 'Binormal', 'TangentU', 'TangentV', 'TwoPointLine', 'IntersectionLine', 'ProximityLine', 'ObjectOrigin', 'Focus1', 'Focus2', + 'OnEdge', 'CenterOfCurvature', 'CenterOfMass', 'IntersectionPoint', 'Vertex', 'ProximityPoint1', 'ProximityPoint2', + 'AxisOfInertia1', 'AxisOfInertia2', 'AxisOfInertia3', 'InertialCS', 'FaceNormal', 'OZX', 'OZY', 'OXY', 'OXZ', 'OYZ', 'OYX', + 'ParallelPlane', 'MidPoint', +] +const implemented = { + plane: ['Translate', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'TangentPlane', 'NormalToEdge', 'FrenetNB', 'FrenetTN', 'FrenetTB', 'Concentric', 'SectionOfRevolution', 'ThreePointsPlane', 'ThreePointsNormal', 'Folding', 'InertialCS', 'OZX', 'OZY', 'OXY', 'OXZ', 'OYZ', 'OYX', 'ParallelPlane'], + line: ['ObjectX', 'ObjectY', 'ObjectZ', 'AxisOfCurvature', 'Directrix1', 'Directrix2', 'Asymptote1', 'Asymptote2', 'Tangent', 'Normal', 'Binormal', 'TwoPointLine', 'IntersectionLine', 'ProximityLine', 'AxisOfInertia1', 'AxisOfInertia2', 'AxisOfInertia3', 'FaceNormal'], + point: ['ObjectOrigin', 'Focus1', 'Focus2', 'OnEdge', 'CenterOfCurvature', 'CenterOfMass', 'Vertex', 'ProximityPoint1', 'ProximityPoint2'], +} +implemented.sketch = implemented.plane +const engineTypes = { plane: 'Attacher::AttachEnginePlane', line: 'Attacher::AttachEngineLine', point: 'Attacher::AttachEnginePoint', sketch: 'Attacher::AttachEnginePlane' } +const objectTypes = { plane: 'PartDesign::Plane', line: 'PartDesign::Line', point: 'PartDesign::Point', sketch: 'Sketcher::SketchObject' } + +if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-attachment-mode-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.') +if (oracle.registry?.modeCount !== 55 || !same(oracle.registry.modes, modes) || oracle.registry.implementedUnionCount !== 50 || !same(oracle.registry.unimplementedModes, ['Deactivated', 'TangentU', 'TangentV', 'IntersectionPoint', 'MidPoint'])) fail('persistent mode registry or implemented union changed.') + +for (const name of Object.keys(engineTypes)) { + const engine = oracle.engines?.[name] + if (engine?.typeId !== objectTypes[name] || engine.attacherType !== engineTypes[name] || !same(engine.completeModeList, modes) || !same(engine.implementedModes, implemented[name])) fail(`${name} engine identity or implemented modes changed.`) + for (const mode of implemented[name]) { + const info = engine.modeInfo?.[mode] + if (info?.modeIndex !== modes.indexOf(mode) || !Array.isArray(info.referenceCombinations) || info.referenceCombinations.length === 0) fail(`${name}.${mode} mode metadata is incomplete.`) + } +} +if (!same(oracle.engines.plane.modeInfo.FlatFace.referenceCombinations, [['Plane']]) || !same(oracle.engines.line.modeInfo.TwoPointLine.referenceCombinations, [['Vertex', 'Vertex'], ['Line']]) || !same(oracle.engines.point.modeInfo.Vertex.referenceCombinations, [['Vertex'], ['Line']])) fail('representative reference combination contract changed.') + +const expectedSuccess = { + plane: { typeId: 'PartDesign::Plane', mode: 'FlatFace', refs: ['Plane'], support: ['Face1'] }, + line: { typeId: 'PartDesign::Line', mode: 'TwoPointLine', refs: ['Vertex', 'Vertex'], support: ['Vertex1', 'Vertex2'] }, + point: { typeId: 'PartDesign::Point', mode: 'Vertex', refs: ['Vertex'], support: ['Vertex1'] }, + sketch: { typeId: 'Sketcher::SketchObject', mode: 'FlatFace', refs: ['Plane'], support: ['Face1'] }, +} +for (const phase of ['initial', 'edited', 'roundtrip']) { + for (const [name, expected] of Object.entries(expectedSuccess)) { + const result = oracle.success?.[phase]?.[name] + if (result?.typeId !== expected.typeId || result.mapMode !== expected.mode || result.positionBySupport !== true || !same(result.state, ['Up-to-date']) || result.status !== 'Valid' || result.suggestion?.message !== 'OK' || !result.suggestion.allApplicableModes.includes(expected.mode) || !same(result.suggestion.referenceTypes, expected.refs) || !same(result.support?.[0]?.subElements, expected.support) || result.support?.[0]?.object !== 'SourceBox') fail(`${phase} ${name} success evidence is invalid.`) + } +} +if (!near(oracle.success.initial.point.placement.position?.[2], 6) || !near(oracle.success.edited.point.placement.position?.[2], 9) || !near(oracle.success.roundtrip.point.placement.position?.[2], 9)) fail('source mutation did not move the attached point through edit and round-trip.') + +const failureContract = { + InvalidPlane: ['PartDesign::Plane', 'Vertex', 'Attachment mode Vertex is not implemented.'], + InvalidLine: ['PartDesign::Line', 'FlatFace', 'Attachment mode FlatFace is not implemented.'], + InvalidPoint: ['PartDesign::Point', 'FlatFace', 'Attachment mode FlatFace is not implemented.'], +} +for (const [name, [typeId, mode, status]] of Object.entries(failureContract)) { + const initial = oracle.failures?.initial?.[name] + const roundtrip = oracle.failures?.roundtrip?.[name] + if (initial?.typeId !== typeId || initial.mapMode !== mode || !same(initial.state, ['Touched', 'Invalid']) || initial.status !== status || roundtrip?.mapMode !== mode || !same(roundtrip.state, ['Touched', 'Invalid']) || roundtrip.status !== status) fail(`${name} unsupported-mode failure did not survive FCStd round-trip.`) +} + +console.log(JSON.stringify({ status: 'freecad-attachment-mode-oracle-pass', baselineId: oracle.baselineId, persistentModes: oracle.registry.modeCount, implementedModes: { plane: implemented.plane.length, line: implemented.line.length, point: implemented.point.length, sketch: implemented.sketch.length }, representativeSuccesses: Object.keys(expectedSuccess), unsupportedFailures: Object.keys(failureContract), pointMutationZ: [oracle.success.initial.point.placement.position[2], oracle.success.edited.point.placement.position[2]], fcstdRoundtrip: true }, null, 2)) diff --git a/scripts/check-freecad-naming-bridge-candidate.mjs b/scripts/check-freecad-naming-bridge-candidate.mjs new file mode 100644 index 0000000..3cb5dcf --- /dev/null +++ b/scripts/check-freecad-naming-bridge-candidate.mjs @@ -0,0 +1,19 @@ +import { createHash } from 'node:crypto' +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const report = JSON.parse(await readFile(resolve(root, '.cache/toolchains/freecad-naming-sdk/isolated-bridge-report.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD naming isolated bridge candidate: ${message}`) } +if (report.schemaVersion !== 1 || report.status !== 'freecad-private-naming-isolated-bridge-pass') fail('execution report is missing or failed.') +if (report.firstStage?.mappedNames !== 1 || report.firstStage?.stringHasherEntries !== 0 || report.chainedStage?.mappedNames !== 1 || report.chainedStage?.stringHasherEntries < 1 || report.thirdStage?.mappedNames !== 1 || report.thirdStage?.stringHasherEntries <= report.chainedStage.stringHasherEntries) fail('three-stage StringHasher restoration evidence is incomplete.') +if (report.tamperedHasherRejected !== true || report.inconsistentTablesRejected !== true) fail('strict StringHasher fail-closed evidence is incomplete.') +if (report.productionPublication !== false || report.productionWorkerLinked !== false) fail('candidate report crossed the production publication boundary.') +for (const artifact of report.artifacts ?? []) { + const path = resolve(root, 'native/freecad-naming-bridge/dist', artifact.name) + const [content, size] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)]) + const sha256 = createHash('sha256').update(content).digest('hex') + if (artifact.bytes !== size || artifact.sha256 !== sha256) fail(`stale artifact evidence for ${artifact.name}.`) +} +if (report.artifacts?.length !== 3) fail('artifact evidence must include JS, WASM and DATA.') +console.log(JSON.stringify({ status: 'freecad-naming-isolated-bridge-candidate-check-pass', stringHasherStages: [report.firstStage.stringHasherEntries, report.chainedStage.stringHasherEntries, report.thirdStage.stringHasherEntries], tamperedHasherRejected: true, inconsistentTablesRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false }, null, 2)) diff --git a/scripts/check-freecad-naming-next-tasks.mjs b/scripts/check-freecad-naming-next-tasks.mjs new file mode 100644 index 0000000..0e5c385 --- /dev/null +++ b/scripts/check-freecad-naming-next-tasks.mjs @@ -0,0 +1,22 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')) +const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-naming-next-tasks.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD naming next tasks: ${message}`) } +const expectedIds = ['SDK-01', 'SDK-02A', 'SDK-02B', 'SDK-04', 'SDK-03', 'SDK-05', 'PAR-01', 'QA-01'] +if (plan.schemaVersion !== 1 || plan.scope !== 'freecad-private-naming-and-parameter-followup') fail('unsupported schema or scope.') +if (JSON.stringify(plan.orderedTasks?.map(({ id }) => id)) !== JSON.stringify(expectedIds)) fail('ordered task list is incomplete or reordered.') +const taskById = new Map(plan.orderedTasks.map((task) => [task.id, task])) +for (const task of plan.orderedTasks) { + if (!['pending', 'in_progress', 'completed'].includes(task.status)) fail(`${task.id} has an invalid status.`) + if (!Array.isArray(task.deliverables) || !task.deliverables.length || !Array.isArray(task.acceptance) || !task.acceptance.length || !Array.isArray(task.evidence) || !task.evidence.length) fail(`${task.id} is missing its executable contract.`) + for (const dependency of task.dependencies ?? []) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task ${dependency}.`) + for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing npm script ${evidence}.`) +} +if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'in_progress') fail('current SDK/parameter task status is stale.') +if (JSON.stringify(taskById.get('SDK-03').dependencies) !== JSON.stringify(['SDK-02B', 'SDK-04'])) fail('SDK-03 dependencies must retain both the archive set and isolated bridge.') +if (taskById.get('SDK-04').dependencies?.length !== 1 || taskById.get('SDK-04').dependencies[0] !== 'SDK-02B') fail('SDK-04 must start directly from the completed archive set.') +if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'unavailable' || plan.boundary?.productionWorker?.callbacks?.length !== 0) fail('production boundary changed before the candidate Worker gate.') +console.log(JSON.stringify({ status: 'freecad-naming-next-tasks-pass', tasks: plan.orderedTasks.length, completed: plan.orderedTasks.filter(({ status }) => status === 'completed').length, inProgress: plan.orderedTasks.filter(({ status }) => status === 'in_progress').map(({ id }) => id), boundary: plan.boundary }, null, 2)) diff --git a/scripts/check-freecad-naming-sdk-readiness.mjs b/scripts/check-freecad-naming-sdk-readiness.mjs new file mode 100644 index 0000000..247f024 --- /dev/null +++ b/scripts/check-freecad-naming-sdk-readiness.mjs @@ -0,0 +1,103 @@ +import { readFile } from 'node:fs/promises' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' +import { resolve } from 'node:path' +import { + REQUIRED_FREECAD_NAMING_CALLBACKS, + REQUIRED_FREECAD_NAMING_DEFINITIONS, + REQUIRED_FREECAD_NAMING_LIBRARIES, + exists, + inspectIncludeDirectory, + inspectPlannedLibrary, + inspectPlannedRuntimeAsset, + loadSdkPlan, + resolveFrom, + sha256File, +} from './freecad-naming-sdk-lib.mjs' + +const root = resolve(new URL('..', import.meta.url).pathname) +const execFileAsync = promisify(execFile) +const plan = await loadSdkPlan(root) +const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK readiness: ${message}`) } +if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.') +if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.') +if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.') +if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.') +if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.') +if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.') +if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.') + +const libraries = [] +for (const library of plan.libraries) libraries.push(await inspectPlannedLibrary(root, library)) +const linkDependencies = [] +for (const library of plan.linkDependencies ?? []) linkDependencies.push(await inspectPlannedLibrary(root, library)) +const includeDirs = [] +for (const includeDir of plan.includeDirs ?? []) includeDirs.push(await inspectIncludeDirectory(root, includeDir)) +const runtimeAssets = [] +for (const asset of plan.runtimeAssets ?? []) runtimeAssets.push(await inspectPlannedRuntimeAsset(root, asset)) +const freeCadInclude = plan.includeDirs.find(({ name }) => name === 'FreeCAD') +let sourceCommit = null +if (freeCadInclude && await exists(resolveFrom(root, freeCadInclude.path))) { + try { + sourceCommit = (await execFileAsync('git', ['-C', resolveFrom(root, freeCadInclude.path), 'rev-parse', 'HEAD'])).stdout.trim() + } catch (error) { + fail(`cannot inspect FreeCAD source commit: ${error instanceof Error ? error.message : String(error)}`) + } + if (sourceCommit !== plan.baseline.sourceCommit) fail(`FreeCAD source commit mismatch: expected ${plan.baseline.sourceCommit}, got ${sourceCommit}.`) +} +const bridgePath = resolveFrom(root, plan.namingBridge.path) +const hostAdapterPath = resolveFrom(root, plan.namingBridge.hostAdapter) +const forceIncludePath = resolveFrom(root, plan.compileOptions.forceInclude) +const bridgePresent = await exists(bridgePath) +const hostAdapterPresent = await exists(hostAdapterPath) +const forceIncludePresent = await exists(forceIncludePath) +let bridgeExports = [] +if (bridgePresent) { + const source = await readFile(bridgePath, 'utf8') + bridgeExports = REQUIRED_FREECAD_NAMING_CALLBACKS.filter((callback) => source.includes(callback)) + if (bridgeExports.length !== REQUIRED_FREECAD_NAMING_CALLBACKS.length) fail('present naming bridge source omits required callback names.') +} +if (hostAdapterPresent) { + const source = await readFile(hostAdapterPath, 'utf8') + if (!source.includes('Module.preRun') || !source.includes('FREECAD_USER_HOME')) fail('host adapter does not initialize the FreeCAD virtual user environment.') +} +const missingLibraries = libraries.filter(({ status }) => status !== 'verified').map(({ name }) => name) +const missingLinkDependencies = linkDependencies.filter(({ status }) => status !== 'verified').map(({ name }) => name) +const missingIncludes = includeDirs.filter(({ status }) => status !== 'verified').map(({ name }) => name) +const missingRuntimeAssets = runtimeAssets.filter(({ status }) => status !== 'verified').map(({ name }) => name) +const blockers = [ + ...missingLibraries.map((name) => `${name} wasm static library`), + ...missingLinkDependencies.map((name) => `${name} wasm link dependency`), + ...missingIncludes.map((name) => `${name} headers`), + ...missingRuntimeAssets.map((name) => `${name} runtime asset`), + ...(!bridgePresent ? ['FreeCAD private naming Worker bridge'] : []), + ...(!hostAdapterPresent ? ['FreeCAD naming host adapter'] : []), + ...(!forceIncludePresent ? ['FreeCAD wasm force-include header'] : []), +] +console.log(JSON.stringify({ + status: blockers.length === 0 ? 'sdk-readiness-complete' : 'sdk-readiness-incomplete', + availability: blockers.length === 0 ? 'candidate-complete' : 'unavailable', + baseline: plan.baseline, + libraries, + linkDependencies, + includeDirs, + runtimeAssets, + sourceCommit, + namingBridge: { + path: plan.namingBridge.path, + status: bridgePresent ? 'verified' : 'missing', + sha256: bridgePresent ? await sha256File(bridgePath) : null, + hostAdapter: plan.namingBridge.hostAdapter, + hostAdapterSha256: hostAdapterPresent ? await sha256File(hostAdapterPath) : null, + exports: bridgeExports, + }, + compileOptions: { + ...plan.compileOptions, + forceIncludeSha256: forceIncludePresent ? await sha256File(forceIncludePath) : null, + }, + blockers, + readyLibraries: libraries.length - missingLibraries.length, + requiredLibraries: libraries.length, + publishToWorker: false, + boundary: plan.boundary, +}, null, 2)) diff --git a/scripts/check-freecad-naming-sdk.mjs b/scripts/check-freecad-naming-sdk.mjs index ef7d232..a618e39 100644 --- a/scripts/check-freecad-naming-sdk.mjs +++ b/scripts/check-freecad-naming-sdk.mjs @@ -1,6 +1,14 @@ -import { access, readFile } from 'node:fs/promises' -import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' import { resolve, isAbsolute } from 'node:path' +import { + REQUIRED_FREECAD_NAMING_CALLBACKS, + REQUIRED_FREECAD_NAMING_DEFINITIONS, + REQUIRED_FREECAD_NAMING_LIBRARIES, + REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES, + exists, + inspectWasmStaticArchive, + sha256File, +} from './freecad-naming-sdk-lib.mjs' const root = resolve(new URL('..', import.meta.url).pathname) const sdkRoot = process.env.FREECAD_WASM_SDK_DIR ? resolve(root, process.env.FREECAD_WASM_SDK_DIR) : '' @@ -8,8 +16,6 @@ const required = (value, name) => { if (typeof value !== 'string' || !value.trim()) throw new Error(`FreeCAD WASM SDK manifest requires ${name}.`) return value.trim() } -const exists = async (path) => access(path).then(() => true).catch(() => false) -const sha256 = async (path) => createHash('sha256').update(await readFile(path)).digest('hex') const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) } if (!sdkRoot) { @@ -26,16 +32,27 @@ if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freec if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.') if (required(manifest.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.') if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.') +if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'contract-only' || manifest.boundary?.exTsn02 !== 'in_progress' || manifest.boundary?.systemExact !== false) fail('manifest must preserve the candidate-only contract-only/in_progress/non-exact boundary.') const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.') const resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir)) const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.') const libraryNames = new Set(libraries.map((library) => library?.name)) -for (const name of ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`) +if (libraryNames.size !== libraries.length) fail('library names must be unique.') +for (const name of REQUIRED_FREECAD_NAMING_LIBRARIES) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`) +for (const name of REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES) if (!libraryNames.has(name)) fail(`libraries must include ${name}.`) const namingBridge = manifest.namingBridge && typeof manifest.namingBridge === 'object' ? manifest.namingBridge : fail('namingBridge is required.') const bridgePath = required(namingBridge.source, 'namingBridge.source') const bridge = isAbsolute(bridgePath) ? bridgePath : resolve(sdkRoot, bridgePath) if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`) -if (!Array.isArray(namingBridge.exports) || !['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.') +const hostAdapterPath = required(namingBridge.hostAdapter, 'namingBridge.hostAdapter') +const hostAdapter = isAbsolute(hostAdapterPath) ? hostAdapterPath : resolve(sdkRoot, hostAdapterPath) +if (!await exists(hostAdapter)) fail(`missing naming host adapter: ${hostAdapter}`) +if (!Array.isArray(namingBridge.exports) || !REQUIRED_FREECAD_NAMING_CALLBACKS.every((name) => namingBridge.exports.includes(name))) fail('namingBridge.exports must declare all three versioned ABI callbacks.') +const compileOptions = manifest.compileOptions && typeof manifest.compileOptions === 'object' ? manifest.compileOptions : fail('compileOptions is required.') +if (compileOptions.cxxStandard !== 'c++20' || compileOptions.pthread !== true || JSON.stringify(compileOptions.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compileOptions must lock C++20, pthread and the FreeCAD wasm compatibility definitions.') +const forceIncludePath = required(compileOptions.forceInclude, 'compileOptions.forceInclude') +const forceInclude = isAbsolute(forceIncludePath) ? forceIncludePath : resolve(sdkRoot, forceIncludePath) +if (!await exists(forceInclude)) fail(`missing force-include header: ${forceInclude}`) for (const path of resolvedIncludeDirs) { if (!await exists(path)) fail(`missing include directory: ${path}`) } @@ -46,15 +63,40 @@ for (const library of libraries) { const path = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue) if (!path.endsWith('.a')) fail(`library ${name} must be a static .a archive.`) if (!await exists(path)) fail(`missing static library ${name}: ${path}`) - if ((await readFile(path)).byteLength === 0) fail(`static library ${name} is empty.`) if (typeof library.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(library.sha256)) fail(`library ${name} requires a lowercase SHA-256.`) - const actualHash = await sha256(path) + let archive + try { archive = await inspectWasmStaticArchive(path) } catch (error) { fail(`library ${name} is not a wasm static archive: ${error instanceof Error ? error.message : String(error)}.`) } + if (archive.target !== 'wasm32-emscripten') fail(`library ${name} has unexpected target ${archive.target}.`) + const actualHash = await sha256File(path) if (actualHash !== library.sha256) fail(`library ${name} hash mismatch: expected ${library.sha256}, got ${actualHash}.`) } for (const header of ['App/StringHasher.h', 'App/MappedName.h', 'App/ElementMap.h']) { if (!(await Promise.all(resolvedIncludeDirs.map((includeDir) => exists(resolve(includeDir, header))))).some(Boolean)) fail(`missing locked FreeCAD private header in includeDirs: ${header}`) } -const bridgeSha = await sha256(bridge) +const bridgeSha = await sha256File(bridge) if (typeof namingBridge.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.sha256)) fail('namingBridge.sha256 requires a lowercase SHA-256.') if (namingBridge.sha256 !== bridgeSha) fail(`naming bridge hash mismatch: expected ${namingBridge.sha256}, got ${bridgeSha}.`) -console.log(JSON.stringify({ status: 'sdk-ready', availability: 'available', systemExact: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, exports: [...namingBridge.exports] } }, null, 2)) +const hostAdapterSha = await sha256File(hostAdapter) +if (typeof namingBridge.hostAdapterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(namingBridge.hostAdapterSha256) || namingBridge.hostAdapterSha256 !== hostAdapterSha) fail(`naming host adapter hash mismatch: expected ${String(namingBridge.hostAdapterSha256)}, got ${hostAdapterSha}.`) +const forceIncludeSha = await sha256File(forceInclude) +if (typeof compileOptions.forceIncludeSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(compileOptions.forceIncludeSha256) || compileOptions.forceIncludeSha256 !== forceIncludeSha) fail(`force-include header hash mismatch: expected ${String(compileOptions.forceIncludeSha256)}, got ${forceIncludeSha}.`) +const runtimeAssets = Array.isArray(manifest.runtimeAssets) ? manifest.runtimeAssets : fail('runtimeAssets must be an array.') +if (!runtimeAssets.some((asset) => asset?.name === 'PythonWasmStdlib')) fail('runtimeAssets must include PythonWasmStdlib.') +for (const asset of runtimeAssets) { + if (!asset || typeof asset !== 'object') fail('runtimeAssets entries must be objects.') + const name = required(asset.name, 'runtimeAssets[].name') + const pathValue = required(asset.path, `runtimeAssets[${name}].path`) + required(asset.preloadTo, `runtimeAssets[${name}].preloadTo`) + const assetRoot = isAbsolute(pathValue) ? pathValue : resolve(sdkRoot, pathValue) + if (!await exists(assetRoot)) fail(`missing runtime asset ${name}: ${assetRoot}`) + if (!Array.isArray(asset.files) || asset.files.length === 0) fail(`runtime asset ${name} requires hashed files.`) + for (const file of asset.files) { + const relativePath = required(file?.path, `runtimeAssets[${name}].files[].path`) + const filePath = resolve(assetRoot, relativePath) + if (!await exists(filePath)) fail(`missing runtime asset file ${name}/${relativePath}.`) + if (typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(file.sha256)) fail(`runtime asset file ${name}/${relativePath} requires a lowercase SHA-256.`) + const actualHash = await sha256File(filePath) + if (actualHash !== file.sha256) fail(`runtime asset file ${name}/${relativePath} hash mismatch: expected ${file.sha256}, got ${actualHash}.`) + } +} +console.log(JSON.stringify({ status: 'sdk-ready', availability: 'candidate-complete', systemExact: false, productionPublication: false, sdkRoot, freecadVersion: manifest.freecadVersion, sourceCommit: manifest.sourceCommit, emscriptenVersion: manifest.emscriptenVersion, includeDirs, libraries: libraries.map(({ name, path, sha256 }) => ({ name, path, sha256 })), namingBridge: { source: bridgePath, sha256: bridgeSha, hostAdapter: hostAdapterPath, hostAdapterSha256: hostAdapterSha, exports: [...namingBridge.exports] }, compileOptions, runtimeAssets, boundary: manifest.boundary }, null, 2)) diff --git a/scripts/check-freecad-partdesign-structure-oracle.mjs b/scripts/check-freecad-partdesign-structure-oracle.mjs new file mode 100644 index 0000000..882a893 --- /dev/null +++ b/scripts/check-freecad-partdesign-structure-oracle.mjs @@ -0,0 +1,52 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-partdesign-structure-oracle.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD PartDesign structure oracle: ${message}`) } +const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance +const expectedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' +if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-partdesign-structure-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== expectedCommit || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.') + +const expectedInitialGroups = { + SourceBody: ['SourceBox'], + DatumBody: ['DatumPlane', 'DatumLine', 'DatumPoint'], + BinderBody: ['ShapeBinder'], + SubBinderBody: ['SubShapeBinder'], +} +const expectedInitialTips = { SourceBody: 'SourceBox', DatumBody: null, BinderBody: null, SubBinderBody: null } +for (const [body, group] of Object.entries(expectedInitialGroups)) { + if (JSON.stringify(oracle.initial?.bodyGroups?.[body]) !== JSON.stringify(group) || oracle.initial?.bodyTips?.[body] !== expectedInitialTips[body]) fail(`initial ${body} grouping or Tip state is invalid.`) +} +if (!near(oracle.initial.sourceVolume, 480) || !near(oracle.initial.shapeBinderVolume, 480) || !near(oracle.initial.subShapeBinderLength, 28)) fail('initial source or binder shape evidence is invalid.') +if (oracle.initial.datumPlane?.mapMode !== 'FlatFace' || JSON.stringify(oracle.initial.datumPlane.support) !== JSON.stringify([{ object: 'XY_Plane', subElements: [] }]) || !near(oracle.initial.datumPlane.attachmentOffset.position?.[2], 5) || !near(oracle.initial.datumPlane.attachmentOffset.angleDegrees, 30) || !near(oracle.initial.datumPlane.placement.position?.[2], 5) || !near(oracle.initial.datumPlane.placement.angleDegrees, 30) || JSON.stringify(oracle.initial.datumPlane.state) !== JSON.stringify(['Up-to-date'])) fail('DatumPlane support, offset or placement evidence is invalid.') +if (oracle.initial.datumLine?.mapMode !== 'ObjectX' || JSON.stringify(oracle.initial.datumLine.state) !== JSON.stringify(['Up-to-date']) || oracle.initial.datumPoint?.mapMode !== 'ObjectOrigin' || JSON.stringify(oracle.initial.datumPoint.attachmentOffset.position) !== JSON.stringify([1, 2, 3]) || JSON.stringify(oracle.initial.datumPoint.state) !== JSON.stringify(['Up-to-date'])) fail('DatumLine or DatumPoint evidence is invalid.') +if (JSON.stringify(oracle.initial.shapeBinder?.support) !== JSON.stringify([{ object: 'SourceBox', subElements: [] }]) || oracle.initial.shapeBinder.traceSupport !== true || JSON.stringify(oracle.initial.shapeBinder.state) !== JSON.stringify(['Up-to-date'])) fail('ShapeBinder support evidence is invalid.') +const expectedSubSupport = [{ object: 'SourceBox', subElements: ['Edge1', 'Edge2', 'Edge3', 'Edge4'] }] +if (JSON.stringify(oracle.initial.subShapeBinder?.support) !== JSON.stringify(expectedSubSupport) || JSON.stringify(oracle.initial.subShapeBinder.state) !== JSON.stringify(['Up-to-date'])) fail('SubShapeBinder support evidence is invalid.') + +if (!near(oracle.edited?.sourceVolume, 672) || !near(oracle.edited?.shapeBinderVolume, 672) || !near(oracle.edited?.subShapeBinderLength, 28) || !near(oracle.edited?.datumPlaneAttachmentOffset?.position?.[2], 7) || !near(oracle.edited?.datumPlaneAttachmentOffset?.angleDegrees, 45) || !near(oracle.edited?.datumPlanePlacement?.position?.[2], 7) || !near(oracle.edited?.datumPlanePlacement?.angleDegrees, 45) || Object.entries(expectedInitialTips).some(([body, tip]) => oracle.edited?.bodyTips?.[body] !== tip)) fail('edited recompute evidence is invalid.') +if (oracle.invalidSupport?.typeId !== 'PartDesign::Plane' || JSON.stringify(oracle.invalidSupport.support) !== JSON.stringify([{ object: 'SourceBox', subElements: ['Face999'] }]) || JSON.stringify(oracle.invalidSupport.state) !== JSON.stringify(['Touched', 'Invalid']) || oracle.invalidSupport.status !== 'AttachEngine3D: subshape not found SourceBox.Face999') fail('invalid attachment support evidence is invalid.') + +const expectedRoundtripGroups = { ...expectedInitialGroups, DatumBody: [...expectedInitialGroups.DatumBody, 'InvalidDatumPlane'] } +for (const [body, group] of Object.entries(expectedRoundtripGroups)) { + if (JSON.stringify(oracle.roundtrip?.bodyGroups?.[body]) !== JSON.stringify(group) || oracle.roundtrip?.bodyTips?.[body] !== expectedInitialTips[body]) fail(`FCStd round-trip ${body} grouping or Tip state is invalid.`) +} +if (!near(oracle.roundtrip.sourceVolume, 672) || !near(oracle.roundtrip.shapeBinderVolume, 672) || !near(oracle.roundtrip.subShapeBinderLength, 28)) fail('FCStd round-trip shape evidence is invalid.') +if (JSON.stringify(oracle.roundtrip.shapeBinderSupport) !== JSON.stringify([{ object: 'SourceBox', subElements: [] }]) || JSON.stringify(oracle.roundtrip.subShapeBinderSupport) !== JSON.stringify(expectedSubSupport) || JSON.stringify(oracle.roundtrip.datumPlaneSupport) !== JSON.stringify([{ object: 'XY_Plane', subElements: [] }]) || oracle.roundtrip.datumPlaneMapMode !== 'FlatFace' || !near(oracle.roundtrip.datumPlaneAttachmentOffset.position?.[2], 7) || !near(oracle.roundtrip.datumPlaneAttachmentOffset.angleDegrees, 45) || JSON.stringify(oracle.roundtrip.invalidDatumState) !== JSON.stringify(['Touched', 'Invalid']) || oracle.roundtrip.invalidDatumStatus !== oracle.invalidSupport.status) fail('FCStd round-trip reference or failure evidence is invalid.') + +const externalSupport = [{ object: 'ExternalBox', subElements: ['Face1'] }] +const crossInitial = oracle.crossDocument?.initial +if (crossInitial?.shapeBinderExternalLink?.accepted !== false || crossInitial.shapeBinderExternalLink.errorType !== 'ValueError' || crossInitial.shapeBinderExternalLink.error !== 'PropertyLinkSubList does not support external object' || crossInitial.shapeBinderSupportType !== 'App::PropertyLinkSubListGlobal' || crossInitial.subShapeBinderSupportType !== 'App::PropertyXLinkSubList') fail('cross-document binder property boundary is invalid.') +if (!near(crossInitial.sourceVolume, 120) || !near(crossInitial.sourceFaceArea, 30) || crossInitial.shapeBinderShapeNull !== true || !near(crossInitial.subShapeBinderArea, 30) || JSON.stringify(crossInitial.shapeBinderSupport) !== '[]' || JSON.stringify(crossInitial.subShapeBinderSupport) !== JSON.stringify(externalSupport) || crossInitial.subShapeBinderSourceDocument !== 'PartDesignExternalSource' || crossInitial.shapeBodyTip !== null || crossInitial.subBodyTip !== null) fail('cross-document initial shape, support or Tip evidence is invalid.') + +const crossRoundtrip = oracle.crossDocument?.roundtrip +if (!near(crossRoundtrip?.sourceVolume, 120) || !near(crossRoundtrip?.sourceFaceArea, 30) || crossRoundtrip?.shapeBinderShapeNull !== true || !near(crossRoundtrip?.subShapeBinderArea, 30) || JSON.stringify(crossRoundtrip?.shapeBinderSupport) !== '[]' || JSON.stringify(crossRoundtrip?.subShapeBinderSupport) !== JSON.stringify(externalSupport) || crossRoundtrip?.subShapeBinderSourceDocument !== 'PartDesignExternalSource' || crossRoundtrip?.shapeBodyTip !== null || crossRoundtrip?.subBodyTip !== null) fail('cross-document FCStd round-trip evidence is invalid.') + +const crossEdited = oracle.crossDocument?.edited +if (!near(crossEdited?.sourceVolume, 192) || !near(crossEdited?.sourceFaceArea, 48) || crossEdited?.shapeBinderShapeNull !== true || !near(crossEdited?.subShapeBinderArea, 48) || JSON.stringify(crossEdited?.shapeBinderState) !== JSON.stringify(['Up-to-date']) || JSON.stringify(crossEdited?.subShapeBinderState) !== JSON.stringify(['Up-to-date'])) fail('cross-document source mutation evidence is invalid.') + +const crossDeleted = oracle.crossDocument?.deleted +if (JSON.stringify(crossDeleted?.shapeBinderSupport) !== '[]' || JSON.stringify(crossDeleted?.subShapeBinderSupport) !== '[]' || crossDeleted?.shapeBinderShapeNull !== true || crossDeleted?.subShapeBinderShapeNull !== false || !near(crossDeleted?.subShapeBinderCachedArea, 48) || JSON.stringify(crossDeleted?.shapeBinderState) !== JSON.stringify(['Up-to-date']) || JSON.stringify(crossDeleted?.subShapeBinderState) !== JSON.stringify(['Up-to-date']) || crossDeleted?.shapeBinderStatus !== 'Valid' || crossDeleted?.subShapeBinderStatus !== 'Valid') fail('cross-document source deletion evidence is invalid.') + +console.log(JSON.stringify({ status: 'freecad-partdesign-structure-oracle-pass', baselineId: oracle.baselineId, bodies: Object.keys(oracle.initial.bodyGroups).length, datumTypes: ['PartDesign::Plane', 'PartDesign::Line', 'PartDesign::Point'], binderTypes: ['PartDesign::ShapeBinder', 'PartDesign::SubShapeBinder'], mutation: { sourceVolume: [oracle.initial.sourceVolume, oracle.edited.sourceVolume], shapeBinderVolume: [oracle.initial.shapeBinderVolume, oracle.edited.shapeBinderVolume], datumPlaneZ: [oracle.initial.datumPlane.attachmentOffset.position[2], oracle.edited.datumPlaneAttachmentOffset.position[2]] }, crossDocument: { shapeBinder: 'external-link-rejected', subShapeBinderArea: [crossInitial.subShapeBinderArea, crossEdited.subShapeBinderArea], deletion: 'support-cleared-shape-cache-retained' }, invalidSupport: oracle.invalidSupport.status, fcstdRoundtrip: true }, null, 2)) diff --git a/scripts/check-freecad-private-naming-boundary.mjs b/scripts/check-freecad-private-naming-boundary.mjs index 88074ab..42e9598 100644 --- a/scripts/check-freecad-private-naming-boundary.mjs +++ b/scripts/check-freecad-private-naming-boundary.mjs @@ -3,10 +3,15 @@ import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8')) -const [abi, matrix, plan] = await Promise.all([ +const [abi, matrix, plan, readiness, sdkPlan, nextTasks, buildScript, smokeTest] = await Promise.all([ load('config/freecad-sketcher-partdesign-abi-contract.json'), load('config/compatibility-matrix.json'), load('config/freecad-web-exact-parity-plan.json'), + load('config/freecad-private-naming-source-readiness.json'), + load('config/freecad-naming-sdk-plan.json'), + load('config/freecad-naming-next-tasks.json'), + readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8'), + readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8'), ]) const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) } @@ -16,16 +21,63 @@ if (!task) fail('EX-TSN-02 is missing from the exact parity plan.') if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'not-linked') fail('the shipped ABI contract must remain not-linked until the production artifact exports verified callbacks.') if (abi.claim?.exactFreeCadParity !== false) fail('the supported facade contract cannot claim exact FreeCAD parity.') if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'contract-only') fail('the FreeCAD naming build must remain contract-only before a locked SDK and callback probe exist.') +if (matrix.nativeOcctHistory?.freecadNamingBuild?.prerequisiteStatus !== 'real-isolated-freecad-private-naming-bridge-pass') fail('the prerequisite status must identify the real isolated FreeCAD naming bridge.') +if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must identify the complete candidate SDK without implying production linkage.') if (task.status !== 'in_progress') fail(`EX-TSN-02 must remain in_progress, received ${String(task.status)}.`) if (matrix.systemExactEvaluation?.exact !== false) fail('systemExact must remain false.') const blockers = matrix.systemExactEvaluation?.blockers ?? [] if (!blockers.includes('shipped Worker lacks the FreeCAD-linked private naming ABI implementation')) fail('systemExact blockers must retain the missing FreeCAD-linked Worker implementation.') if (!task.exactBlockedBy?.includes('The shipped Worker does not link the FreeCAD private naming implementation for every builder')) fail('EX-TSN-02 must retain its production Worker linkage blocker.') +if (readiness.scope !== 'standalone-wasm-source-prerequisite') fail('source readiness must remain a standalone prerequisite, not a production implementation.') +if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain pre-production and must not publish a Worker.') +if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'contract-only' || sdkPlan.boundary?.exTsn02 !== 'in_progress' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.') +if (nextTasks.boundary?.freecadNamingBuildStatus !== 'contract-only' || nextTasks.boundary?.exTsn02 !== 'in_progress' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'unavailable' || nextTasks.boundary?.productionWorker?.callbacks?.length !== 0) fail('next-task plan disagrees with the production boundary.') +if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessCommand !== './npmw run check:freecad-naming-sdk-readiness') fail('compatibility matrix must expose the SDK readiness command.') +if (readiness.production?.workerLinked !== false) fail('the readiness manifest cannot claim that the production Worker is linked.') +if (readiness.production?.exportedCallbacks?.length !== 0) fail('the prerequisite probe cannot declare production callback exports.') +if (readiness.boundary?.freecadNamingBuildStatus !== 'contract-only' || readiness.boundary?.exTsn02 !== 'in_progress' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.') +if (readiness.candidateAbi?.scope !== 'isolated-non-production') fail('the candidate ABI must remain isolated and non-production.') +if (readiness.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.') +if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.') +if (readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI cannot claim OCCT builder context or Worker publication.') +for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) { + if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`) +} +for (const callback of readiness.candidateAbi?.exports ?? []) { + if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`) +} +for (const callback of readiness.production?.requiredCallbacks ?? []) { + if (!smokeTest.includes(callback)) fail(`the source smoke test must reject production callback ${callback}.`) +} +for (const source of readiness.linkedOriginalSources ?? []) { + if (!buildScript.includes(source.path.split('/').at(-1)) || !buildScript.includes(source.sha256)) fail(`the source probe build does not pin ${source.path}.`) +} +for (const expected of ['src/App/StringHasher.cpp', 'src/App/MappedElement.cpp', 'src/App/ElementNamingUtils.cpp', 'src/App/ElementMap.cpp', 'src/Base/Handle.cpp']) { + if (!readiness.linkedOriginalSources?.some((source) => source.path === expected)) fail(`the source readiness manifest must include ${expected}.`) +} +for (const expected of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'real Application and Document integration', 'FreeCAD private naming Worker bridge']) { + if (!readiness.notLinkedProductionComponents?.includes(expected)) fail(`the source readiness manifest must retain ${expected} as not linked.`) +} + console.log(JSON.stringify({ status: 'boundary-pass', shippedWorkerImplementation: 'not-linked', task: { id: task.id, status: task.status }, systemExact: matrix.systemExactEvaluation.exact, sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status, + prerequisiteStatus: matrix.nativeOcctHistory.freecadNamingBuild.prerequisiteStatus, + sdkReadinessStatus: matrix.nativeOcctHistory.freecadNamingBuild.sdkReadinessStatus, + linkedOriginalSources: readiness.linkedOriginalSources.map((source) => source.path), + candidateAbi: { + scope: readiness.candidateAbi.scope, + strictWebValidator: readiness.candidateAbi.strictWebValidator, + publishToWorker: readiness.candidateAbi.publishToWorker, + }, + sdkReadiness: { + scope: sdkPlan.scope, + productionPublication: sdkPlan.productionPublication, + requiredLibraries: sdkPlan.libraries.map(({ name }) => name), + }, + workerLinked: readiness.production.workerLinked, }, null, 2)) diff --git a/scripts/check-freecad-wasm-sdk-build-plan.mjs b/scripts/check-freecad-wasm-sdk-build-plan.mjs new file mode 100644 index 0000000..6e4fa62 --- /dev/null +++ b/scripts/check-freecad-wasm-sdk-build-plan.mjs @@ -0,0 +1,48 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' + +const root = resolve(new URL('..', import.meta.url).pathname) +const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-wasm-sdk-build-plan.json'), 'utf8')) +const fail = (message) => { throw new Error(`FreeCAD wasm SDK build plan: ${message}`) } +const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' +if (plan.schemaVersion !== 1 || plan.scope !== 'candidate-only-freecad-wasm-sdk-build') fail('unsupported schema or scope.') +if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.') +if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.') +if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== false || plan.boundary?.callbacks?.length !== 0) fail('production boundary must remain contract-only, unlinked, and callback-free.') +if (JSON.stringify(plan.requiredArchives) !== JSON.stringify(['FreeCADBase', 'FreeCADApp', 'Part', 'Python'])) fail('required archive list is incomplete or reordered.') +const expectedDependencies = [ + ['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'], + ['yaml-cpp', '0.8.0', 'npm run build:yaml-cpp-wasm'], + ['ICU', '68.2', 'npm run build:icu-wasm'], + ['CPython', '3.13.5', 'npm run build:cpython-wasm'], + ['Xerces-C', '3.2.4', 'npm run build:xerces-c-wasm'], + ['Boost', '1.83.0', 'npm run build:boost-wasm'], +] +if (JSON.stringify(plan.dependencyBuilds?.map(({ name, version, command }) => [name, version, command])) !== JSON.stringify(expectedDependencies)) fail('dependency build order, versions, or commands changed.') +for (const dependency of plan.dependencyBuilds) { + if (!Array.isArray(dependency.targetArchives) || dependency.targetArchives.length === 0) fail(`${dependency.name} has no target archive contract.`) +} +if (plan.dependencyBuilds.find(({ name }) => name === 'yaml-cpp')?.sourceCommit !== 'f7320141120f720aecc4c32be25586e7da9eb978') fail('yaml-cpp source commit is not locked.') +if (plan.dependencyBuilds.find(({ name }) => name === 'ICU')?.sourceSha256 !== 'c79193dee3907a2199b8296a93b52c5cb74332c26f3d167269487680d479d625') fail('ICU source hash is not locked.') +if (plan.dependencyBuilds.find(({ name }) => name === 'CPython')?.sourceSha256 !== '93e583f243454e6e9e4588ca2c2662206ad961659863277afcdb96801647d640') fail('CPython source hash is not locked.') +if (plan.dependencyBuilds.find(({ name }) => name === 'Xerces-C')?.sourceSha256 !== '075bc57940da0f9be6dd183c550c8ce0b9833e4550dc382048377a1a5e3b2bd9') fail('Xerces-C source hash is not locked.') +if (plan.dependencyBuilds.find(({ name }) => name === 'Boost')?.sourceSha256 !== '6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e') fail('Boost source hash is not locked.') +const sourceDir = resolve(root, plan.source.path) +const execFileAsync = promisify(execFile) +let sourceCommit = null +try { + sourceCommit = (await execFileAsync('git', ['-C', sourceDir, 'rev-parse', 'HEAD'])).stdout.trim() +} catch (error) { + fail(`cannot inspect source checkout: ${error instanceof Error ? error.message : String(error)}`) +} +if (sourceCommit !== lockedCommit) fail(`source checkout is ${sourceCommit}, expected ${lockedCommit}.`) +let dirty = '' +try { dirty = (await execFileAsync('git', ['-C', sourceDir, 'status', '--porcelain'])).stdout.trim() } catch (error) { fail(`cannot inspect source status: ${error instanceof Error ? error.message : String(error)}`) } +if (plan.source.requiredClean && dirty) fail('source checkout has uncommitted changes.') +const emcc = process.env.EMCC || 'emcc' +let emccVersion = '' +try { emccVersion = (await execFileAsync(emcc, ['--version'])).stdout.split('\n')[0].trim() } catch (error) { fail(`cannot inspect Emscripten: ${error instanceof Error ? error.message : String(error)}`) } +if (!emccVersion.includes('3.1.69')) fail(`Emscripten version is not 3.1.69: ${emccVersion}`) +console.log(JSON.stringify({ status: 'freecad-wasm-sdk-build-plan-pass', sourceCommit, sourceClean: dirty.length === 0, emcc: emccVersion, productionPublication: false, boundary: plan.boundary }, null, 2)) diff --git a/scripts/check-quality-closure.mjs b/scripts/check-quality-closure.mjs index 27bf9a4..93c316e 100644 --- a/scripts/check-quality-closure.mjs +++ b/scripts/check-quality-closure.mjs @@ -5,7 +5,7 @@ const root = resolve(new URL('..', import.meta.url).pathname) const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse) const [ fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures, - sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignFailures, partdesignRevolutionGroove, partBuilders, + sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, partdesignFailures, partdesignRevolutionGroove, partBuilders, fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script, ] = await Promise.all([ load('config/qa04-fcstd-fuzz-verification.json'), @@ -20,6 +20,8 @@ const [ load('config/freecad-partdesign-loft-oracle.json'), load('config/freecad-partdesign-dressup-oracle.json'), load('config/freecad-partdesign-transform-oracle.json'), + load('config/freecad-partdesign-structure-oracle.json'), + load('config/freecad-attachment-mode-oracle.json'), load('config/freecad-partdesign-failure-oracle.json'), load('config/freecad-partdesign-revolution-groove-oracle.json'), load('config/freecad-part-builders-oracle.json'), @@ -44,6 +46,8 @@ if (successFixtures.scenarios?.length !== 100 || failureFixtures.failures?.lengt if (sketchOracle.status !== 'pass' || sketchOracle.summary?.constraintTypes !== 19 || sketchOracle.summary.successPassed !== 19 || sketchOracle.summary.failurePassed !== 19) throw new Error('QA-02 Sketcher oracle evidence is incomplete.') const partdesignCases = [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].reduce((total, report) => total + (report.summary?.passed ?? 0), 0) if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].some((report) => report.status !== 'pass' || report.freecadVersion !== '1.1.1')) throw new Error('QA-02 PartDesign oracle evidence is incomplete.') +if (partdesignStructure.status !== 'pass' || partdesignStructure.freecadVersion !== '1.1.1' || Object.keys(partdesignStructure.initial?.bodyGroups ?? {}).length !== 4 || partdesignStructure.crossDocument?.initial?.shapeBinderExternalLink?.accepted !== false || partdesignStructure.crossDocument?.initial?.subShapeBinderSupportType !== 'App::PropertyXLinkSubList' || partdesignStructure.crossDocument?.edited?.subShapeBinderArea !== 48 || partdesignStructure.crossDocument?.deleted?.subShapeBinderCachedArea !== 48) throw new Error('QA-02 PartDesign structure and cross-document evidence is incomplete.') +if (attachmentModes.status !== 'pass' || attachmentModes.freecadVersion !== '1.1.1' || attachmentModes.registry?.modeCount !== 55 || attachmentModes.registry?.implementedUnionCount !== 50 || attachmentModes.engines?.plane?.implementedModes?.length !== 23 || attachmentModes.engines?.line?.implementedModes?.length !== 18 || attachmentModes.engines?.point?.implementedModes?.length !== 9 || attachmentModes.success?.roundtrip?.point?.placement?.position?.[2] !== 9 || Object.keys(attachmentModes.failures?.roundtrip ?? {}).length !== 3) throw new Error('QA-02 Attachment mode registry, execution or round-trip evidence is incomplete.') if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.') if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.') if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.') @@ -68,7 +72,7 @@ console.log(JSON.stringify({ tasks: 8, unitSuites: testFiles.length, unitTests: testCases, - freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount }, + freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount }, fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models }, chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes }, browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])), diff --git a/scripts/configure-freecad-naming-sdk-candidate.mjs b/scripts/configure-freecad-naming-sdk-candidate.mjs new file mode 100644 index 0000000..b1e3755 --- /dev/null +++ b/scripts/configure-freecad-naming-sdk-candidate.mjs @@ -0,0 +1,105 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' +import { resolve } from 'node:path' + +const root = resolve(new URL('..', import.meta.url).pathname) +const run = promisify(execFile) +const sourceDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/source-occt8') +const buildDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/build-wasm') +const installDir = resolve(root, '.cache/toolchains/freecad-naming-sdk/install-wasm') +const qtPrefix = resolve(root, '.cache/toolchains/qt6/install-wasm-freecad') +const yamlPrefix = resolve(root, '.cache/toolchains/yaml-cpp/install-wasm') +const icuPrefix = resolve(root, '.cache/toolchains/icu/install-wasm') +const pythonPrefix = resolve(root, '.cache/toolchains/python/install-wasm') +const xercesPrefix = resolve(root, '.cache/toolchains/xerces-c/install-wasm') +const occtBuild = resolve(root, '.cache/bitbybit/occt-history-build-offline') +const occtSource = resolve(root, '.cache/occt/occt') +const offlineSysroot = resolve(root, '.cache/offline-sysroot') +const boostPrefix = resolve(root, '.cache/toolchains/boost/install-wasm') +const reportPath = resolve(root, '.cache/toolchains/freecad-naming-sdk/configure-report.json') +const command = [ + 'emcmake', 'cmake', '--fresh', '-S', sourceDir, '-B', buildDir, '-G', 'Ninja', + '-DCMAKE_BUILD_TYPE=Release', + `-DCMAKE_INSTALL_PREFIX=${installDir}`, + `-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${resolve(root, 'config/freecad-wasm-sdk-bootstrap.cmake')}`, + '-DBUILD_GUI=OFF', '-DBUILD_PART=ON', '-DBUILD_MATERIAL=ON', '-DBUILD_BIM=OFF', '-DBUILD_FLAT_MESH=OFF', + '-DBUILD_PART_DESIGN=OFF', '-DBUILD_SKETCHER=OFF', + '-DBUILD_MESH=OFF', '-DBUILD_MESH_PART=OFF', '-DBUILD_FEM=OFF', '-DBUILD_CAM=OFF', '-DBUILD_ASSEMBLY=OFF', + '-DBUILD_DRAFT=OFF', '-DBUILD_DRAWING=OFF', '-DBUILD_HELP=OFF', '-DBUILD_IDF=OFF', '-DBUILD_IMPORT=OFF', + '-DBUILD_INSPECTION=OFF', '-DBUILD_OPENSCAD=OFF', '-DBUILD_PLOT=OFF', '-DBUILD_POINTS=OFF', + '-DBUILD_REVERSEENGINEERING=OFF', '-DBUILD_ROBOT=OFF', '-DBUILD_SHOW=OFF', '-DBUILD_SPREADSHEET=OFF', + '-DBUILD_START=OFF', '-DBUILD_SURFACE=OFF', '-DBUILD_TECHDRAW=OFF', '-DBUILD_TEST=OFF', '-DBUILD_TUX=OFF', + '-DBUILD_WEB=OFF', '-DBUILD_ADDONMGR=OFF', '-DBUILD_MEASURE=OFF', '-DBUILD_CLOUD=OFF', '-DBUILD_JTREADER=OFF', + '-DBUILD_VR=OFF', '-DBUILD_TEMPLATE=OFF', '-DBUILD_SANDBOX=OFF', '-DENABLE_DEVELOPER_TESTS=OFF', + '-DFREECAD_USE_FREETYPE=OFF', '-DFREECAD_USE_EXTERNAL_FMT=ON', '-DFREECAD_CHECK_PIVY=OFF', + '-DINSTALL_TO_SITEPACKAGES=OFF', '-DFREECAD_USE_CCACHE=OFF', '-DBUILD_DYNAMIC_LINK_PYTHON=OFF', + `-DFREECAD_WASM_OFFLINE_SYSROOT=${offlineSysroot}`, + `-DCMAKE_PREFIX_PATH=${qtPrefix};${yamlPrefix};${icuPrefix};${xercesPrefix}`, + `-DQt6_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6')}`, + `-DQt6Core_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Core')}`, + `-DQt6Concurrent_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Concurrent')}`, + `-DQt6Network_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Network')}`, + `-DQt6Xml_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6Xml')}`, + `-DQt6ZlibPrivate_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6ZlibPrivate')}`, + `-DQt6BundledZLIB_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6BundledZLIB')}`, + `-DQt6BundledPcre2_DIR=${resolve(qtPrefix, 'lib/cmake/Qt6BundledPcre2')}`, + `-DZLIB_INCLUDE_DIR=${resolve(qtPrefix, 'include/QtZlib')}`, + `-DZLIB_LIBRARY=${resolve(qtPrefix, 'lib/libQt6BundledZLIB.a')}`, + `-Dyaml-cpp_DIR=${resolve(yamlPrefix, 'lib/cmake/yaml-cpp')}`, + `-DICU_ROOT=${icuPrefix}`, + `-DICU_INCLUDE_DIR=${resolve(icuPrefix, 'include')}`, + `-DICU_UC_LIBRARY_RELEASE=${resolve(icuPrefix, 'lib/libicuuc.a')}`, + `-DICU_I18N_LIBRARY_RELEASE=${resolve(icuPrefix, 'lib/libicui18n.a')}`, + `-DXercesC_INCLUDE_DIR=${resolve(xercesPrefix, 'include')}`, + `-DXercesC_LIBRARY=${resolve(xercesPrefix, 'lib/libxerces-c.a')}`, + '-DPython3_EXECUTABLE=/usr/bin/python3.13', + `-DPython3_INCLUDE_DIR=${resolve(pythonPrefix, 'include/python3.13')}`, + `-DPython3_INCLUDE_DIRS=${resolve(pythonPrefix, 'include/python3.13')}`, + `-DPython3_LIBRARY=${resolve(pythonPrefix, 'lib/libpython3.13.a')}`, + '-DOCCT_CMAKE_FALLBACK=ON', + `-DOCC_INCLUDE_DIR=${resolve(occtBuild, 'include/opencascade')}`, + `-DOCC_LIBRARY=${resolve(occtBuild, 'lin32/clang/lib/libTKernel.a')}`, + `-DFREECAD_WASM_OCCT_COMPAT_INCLUDE=${resolve(occtSource, 'src/Deprecated/NCollectionAliases')}`, + `-DEIGEN3_INCLUDE_DIR=${resolve(offlineSysroot, 'include/eigen3')}`, + `-DBOOST_WASM_ROOT=${boostPrefix}`, + `-DBoost_DIR=${resolve(root, 'config/cmake/boost')}`, + '-DBoost_USE_STATIC_LIBS=ON', + '-DQT_HOST_PATH=/usr', + '-DQT_HOST_PATH_CMAKE_DIR=/usr/lib/x86_64-linux-gnu/cmake', + '-DFETCHCONTENT_FULLY_DISCONNECTED=ON', + `-DCMAKE_TOOLCHAIN_FILE=${resolve(root, 'config/emscripten-freecad-candidate-toolchain.cmake')}`, + '-DCMAKE_AR=emar', '-DCMAKE_RANLIB=emranlib', + '-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER', '-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY', '-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY', +] +const startedAt = new Date().toISOString() +let status = 'configured' +let exitCode = 0 +let output = '' +try { + const result = await run(command[0], command.slice(1), { cwd: root, maxBuffer: 16 * 1024 * 1024 }) + output = `${result.stdout}${result.stderr}` +} catch (error) { + status = 'blocked' + exitCode = error.status ?? 1 + output = `${error.stdout ?? ''}${error.stderr ?? ''}` +} +const missing = [] +if (status === 'blocked') { + if (output.includes('Could not find a valid Qt installation') || output.includes('Could NOT find Qt6')) missing.push('Qt6 wasm package configuration') + if (output.includes('Could NOT find Python3')) missing.push('Emscripten Python3 development target') + if (output.includes('Could NOT find ICU') || output.includes('Failed to find all ICU components')) missing.push('ICU wasm static libraries') + if (output.includes('Could NOT find XercesC') || output.includes('Failed to find XercesC')) missing.push('Xerces-C wasm static library') + if (output.includes('Could NOT find ZLIB')) missing.push('ZLIB wasm static library') + if (output.includes('Could NOT find OCC') || output.includes('OpenCASCADE not found')) missing.push('OpenCASCADE wasm SDK') + if (output.includes('Could NOT find Eigen3') || output.includes('Eigen3 not found')) missing.push('Eigen3 headers') + if (output.includes('Could NOT find Boost') || output.includes('provided by "Boost"')) missing.push('Boost wasm static libraries') + if (output.includes('Could NOT find yaml-cpp') || output.includes('provided by "yaml-cpp"')) missing.push('yaml-cpp wasm package') + if (output.includes('Unknown CMake command "qt_add_translation"')) missing.push('Qt translation cross-build adapter') + if (output.includes('Qt6::Core') && output.includes('mkspecs/wasm-emscripten') && output.includes('non-existent path')) missing.push('Qt6 wasm mkspec installation') + if (missing.length === 0) missing.push('unclassified CMake configure failure') +} +await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true }) +await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'contract-only', exTsn02: 'in_progress', systemExact: false, workerLinked: false, callbacks: [] } }, null, 2)}\n`) +console.log(JSON.stringify({ status, exitCode, report: reportPath, missing, productionPublication: false }, null, 2)) +if (status !== 'configured') process.exit(exitCode) diff --git a/scripts/freecad-attachment-mode-oracle.py b/scripts/freecad-attachment-mode-oracle.py new file mode 100644 index 0000000..6afef29 --- /dev/null +++ b/scripts/freecad-attachment-mode-oracle.py @@ -0,0 +1,182 @@ +import json +import os +import tempfile + +import FreeCAD as App + + +def version_text(): + return ".".join(str(value) for value in App.Version()[:3]) + + +def placement_json(placement): + axis = placement.Rotation.Axis + base = placement.Base + return { + "position": [float(base.x), float(base.y), float(base.z)], + "axis": [float(axis.x), float(axis.y), float(axis.z)], + "angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793), + } + + +def support_json(value): + result = [] + for obj, sub_elements in value: + subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements) + result.append({ + "document": obj.Document.Name, + "object": obj.Name, + "subElements": [str(sub) for sub in subs if str(sub)], + }) + return result + + +def engine_json(obj): + attacher = obj.Attacher + implemented = list(attacher.ImplementedModes) + return { + "typeId": obj.TypeId, + "attacherType": attacher.AttacherType, + "completeModeList": list(attacher.CompleteModeList), + "implementedModes": implemented, + "modeInfo": { + mode: { + "modeIndex": int(attacher.getModeInfo(mode)["ModeIndex"]), + "referenceCombinations": attacher.getModeInfo(mode)["ReferenceCombinations"], + } + for mode in implemented + }, + } + + +def object_state(obj): + suggestion = obj.Attacher.suggestModes() + return { + "typeId": obj.TypeId, + "mapMode": str(obj.MapMode), + "support": support_json(obj.AttachmentSupport), + "placement": placement_json(obj.Placement), + "state": [str(value) for value in obj.State], + "status": str(obj.getStatusString()), + "positionBySupport": bool(obj.positionBySupport()), + "suggestion": { + "message": suggestion["message"], + "bestFitMode": suggestion["bestFitMode"], + "allApplicableModes": suggestion["allApplicableModes"], + "referenceTypes": suggestion["references_Types"], + }, + } + + +document = App.newDocument("AttachmentModeOracle") +source_body = document.addObject("PartDesign::Body", "SourceBody") +source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox") +source_box.Length = 4 +source_box.Width = 5 +source_box.Height = 6 +document.recompute() + +plane_body = document.addObject("PartDesign::Body", "PlaneBody") +datum_plane = plane_body.newObject("PartDesign::Plane", "DatumPlane") +datum_plane.AttachmentSupport = [(source_box, "Face1")] +datum_plane.MapMode = "FlatFace" + +line_body = document.addObject("PartDesign::Body", "LineBody") +datum_line = line_body.newObject("PartDesign::Line", "DatumLine") +datum_line.AttachmentSupport = [(source_box, ("Vertex1", "Vertex2"))] +datum_line.MapMode = "TwoPointLine" + +point_body = document.addObject("PartDesign::Body", "PointBody") +datum_point = point_body.newObject("PartDesign::Point", "DatumPoint") +datum_point.AttachmentSupport = [(source_box, "Vertex1")] +datum_point.MapMode = "Vertex" + +sketch_body = document.addObject("PartDesign::Body", "SketchBody") +sketch = sketch_body.newObject("Sketcher::SketchObject", "Sketch") +sketch.AttachmentSupport = [(source_box, "Face1")] +sketch.MapMode = "FlatFace" + +invalid_plane_body = document.addObject("PartDesign::Body", "InvalidPlaneBody") +invalid_plane = invalid_plane_body.newObject("PartDesign::Plane", "InvalidPlane") +invalid_plane.AttachmentSupport = [(source_box, "Vertex1")] +invalid_plane.MapMode = "Vertex" + +invalid_line_body = document.addObject("PartDesign::Body", "InvalidLineBody") +invalid_line = invalid_line_body.newObject("PartDesign::Line", "InvalidLine") +invalid_line.AttachmentSupport = [(source_box, "Face1")] +invalid_line.MapMode = "FlatFace" + +invalid_point_body = document.addObject("PartDesign::Body", "InvalidPointBody") +invalid_point = invalid_point_body.newObject("PartDesign::Point", "InvalidPoint") +invalid_point.AttachmentSupport = [(source_box, "Face1")] +invalid_point.MapMode = "FlatFace" + +document.recompute() +engine_objects = { + "plane": datum_plane, + "line": datum_line, + "point": datum_point, + "sketch": sketch, +} +engines = {name: engine_json(obj) for name, obj in engine_objects.items()} +success_initial = {name: object_state(obj) for name, obj in engine_objects.items()} +failures = { + obj.Name: { + "typeId": obj.TypeId, + "mapMode": str(obj.MapMode), + "support": support_json(obj.AttachmentSupport), + "state": [str(value) for value in obj.State], + "status": str(obj.getStatusString()), + } + for obj in [invalid_plane, invalid_line, invalid_point] +} + +source_box.Height = 9 +document.recompute() +success_edited = {name: object_state(obj) for name, obj in engine_objects.items()} + +with tempfile.TemporaryDirectory(prefix="freecad-attachment-mode-") as temp_dir: + path = os.path.join(temp_dir, "AttachmentModeOracle.FCStd") + document.saveAs(path) + App.closeDocument(document.Name) + reopened = App.openDocument(path) + reopened.recompute() + roundtrip = { + name: object_state(reopened.getObject(object_name)) + for name, object_name in { + "plane": "DatumPlane", + "line": "DatumLine", + "point": "DatumPoint", + "sketch": "Sketch", + }.items() + } + roundtrip_failures = { + object_name: { + "mapMode": str(reopened.getObject(object_name).MapMode), + "state": [str(value) for value in reopened.getObject(object_name).State], + "status": str(reopened.getObject(object_name).getStatusString()), + } + for object_name in ["InvalidPlane", "InvalidLine", "InvalidPoint"] + } + App.closeDocument(reopened.Name) + +all_modes = engines["plane"]["completeModeList"] +implemented_union = sorted({mode for engine in engines.values() for mode in engine["implementedModes"]}) +report = { + "schemaVersion": 1, + "baselineId": "freecad-1.1.1-attachment-mode-oracle", + "freecadVersion": version_text(), + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "status": "pass", + "tolerance": 1e-7, + "registry": { + "modeCount": len(all_modes), + "modes": all_modes, + "implementedUnionCount": len(implemented_union), + "unimplementedModes": [mode for mode in all_modes if mode not in implemented_union], + }, + "engines": engines, + "success": {"initial": success_initial, "edited": success_edited, "roundtrip": roundtrip}, + "failures": {"initial": failures, "roundtrip": roundtrip_failures}, +} +print("FREECAD_ATTACHMENT_MODE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":"))) diff --git a/scripts/freecad-naming-sdk-lib.mjs b/scripts/freecad-naming-sdk-lib.mjs new file mode 100644 index 0000000..e660b36 --- /dev/null +++ b/scripts/freecad-naming-sdk-lib.mjs @@ -0,0 +1,126 @@ +import { access, open, readFile } from 'node:fs/promises' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { isAbsolute, resolve } from 'node:path' + +export const REQUIRED_FREECAD_NAMING_LIBRARIES = ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python'] +export const REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES = ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3'] +export const REQUIRED_FREECAD_NAMING_CALLBACKS = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'] +export const REQUIRED_FREECAD_NAMING_DEFINITIONS = ['__linux__=1', 'QT_NO_KEYWORDS', 'HAVE_CONFIG_H', 'PYCXX_6_2_COMPATIBILITY'] + +export const exists = async (path) => access(path).then(() => true).catch(() => false) + +export const resolveFrom = (base, path) => isAbsolute(path) ? path : resolve(base, path) + +export const sha256File = async (path) => new Promise((resolveHash, reject) => { + const hash = createHash('sha256') + const stream = createReadStream(path) + stream.on('error', reject) + stream.on('data', (chunk) => hash.update(chunk)) + stream.on('end', () => resolveHash(hash.digest('hex'))) +}) + +const readAt = async (handle, length, position) => { + const buffer = Buffer.alloc(length) + const { bytesRead } = await handle.read(buffer, 0, length, position) + return buffer.subarray(0, bytesRead) +} + +const isWasmObject = (bytes) => bytes.length >= 8 + && bytes.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d])) + && bytes.subarray(4, 8).equals(Buffer.from([0x01, 0x00, 0x00, 0x00])) +const isElfObject = (bytes) => bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) +const isLlvmBitcode = (bytes) => bytes.length >= 4 + && (bytes.subarray(0, 4).equals(Buffer.from([0x42, 0x43, 0xc0, 0xde])) + || bytes.subarray(0, 4).equals(Buffer.from([0xde, 0xc0, 0x17, 0x0b]))) + +export async function inspectWasmStaticArchive(path) { + const handle = await open(path, 'r') + try { + const stat = await handle.stat() + if (stat.size < 68) throw new Error('archive is empty or truncated') + const magic = await readAt(handle, 8, 0) + if (magic.toString('ascii') !== '!\n') throw new Error('file is not a regular static ar archive') + let offset = 8 + let memberCount = 0 + let wasmObjectCount = 0 + let llvmBitcodeCount = 0 + let hostObjectCount = 0 + let unsupportedMemberCount = 0 + while (offset + 60 <= stat.size) { + const header = await readAt(handle, 60, offset) + if (header.length !== 60 || header.subarray(58, 60).toString('ascii') !== '`\n') throw new Error(`invalid ar member header at byte ${offset}`) + const rawName = header.subarray(0, 16).toString('ascii').trim() + const sizeText = header.subarray(48, 58).toString('ascii').trim() + if (!/^\d+$/.test(sizeText)) throw new Error(`invalid ar member size at byte ${offset}`) + const memberSize = Number(sizeText) + const dataOffset = offset + 60 + if (!Number.isSafeInteger(memberSize) || memberSize < 0 || dataOffset + memberSize > stat.size) throw new Error(`ar member exceeds archive size at byte ${offset}`) + const special = rawName === '/' || rawName === '//' || rawName === '/SYM64/' + if (!special) { + memberCount += 1 + const extendedName = rawName.startsWith('#1/') ? Number(rawName.slice(3)) : 0 + if (!Number.isSafeInteger(extendedName) || extendedName < 0 || extendedName > memberSize) throw new Error(`invalid BSD ar filename at byte ${offset}`) + const prefix = await readAt(handle, Math.min(8, memberSize - extendedName), dataOffset + extendedName) + if (isWasmObject(prefix)) wasmObjectCount += 1 + else if (isLlvmBitcode(prefix)) llvmBitcodeCount += 1 + else if (isElfObject(prefix)) hostObjectCount += 1 + else unsupportedMemberCount += 1 + } + offset = dataOffset + memberSize + (memberSize % 2) + } + if (memberCount === 0) throw new Error('archive has no object members') + if (hostObjectCount > 0) throw new Error(`archive contains ${hostObjectCount} host ELF object member(s)`) + if (unsupportedMemberCount > 0) throw new Error(`archive contains ${unsupportedMemberCount} unsupported member(s)`) + if (wasmObjectCount + llvmBitcodeCount === 0) throw new Error('archive has no wasm or LLVM bitcode object members') + return { target: 'wasm32-emscripten', bytes: stat.size, memberCount, wasmObjectCount, llvmBitcodeCount } + } finally { + await handle.close() + } +} + +export async function loadSdkPlan(projectRoot) { + const path = resolve(projectRoot, 'config/freecad-naming-sdk-plan.json') + return JSON.parse(await readFile(path, 'utf8')) +} + +export async function inspectPlannedLibrary(projectRoot, library) { + const path = resolveFrom(projectRoot, library.path) + if (!await exists(path)) return { name: library.name, path: library.path, status: 'missing' } + const archive = await inspectWasmStaticArchive(path) + const sha256 = await sha256File(path) + if (library.expectedSha256 && sha256 !== library.expectedSha256) throw new Error(`${library.name} hash mismatch: expected ${library.expectedSha256}, got ${sha256}`) + return { name: library.name, path: library.path, status: 'verified', sha256, ...archive } +} + +export async function inspectIncludeDirectory(projectRoot, entry) { + const path = resolveFrom(projectRoot, entry.path) + const missingHeaders = [] + for (const header of entry.requiredHeaders ?? []) if (!await exists(resolve(path, header))) missingHeaders.push(header) + return { name: entry.name, path: entry.path, status: missingHeaders.length === 0 ? 'verified' : 'missing', missingHeaders } +} + +export async function inspectPlannedRuntimeAsset(projectRoot, asset) { + const root = resolveFrom(projectRoot, asset.path) + const files = [] + const missingFiles = [] + for (const relativePath of asset.requiredFiles ?? []) { + const path = resolve(root, relativePath) + if (!await exists(path)) { + missingFiles.push(relativePath) + continue + } + const sha256 = await sha256File(path) + const expectedSha256 = asset.expectedSha256?.[relativePath] + if (expectedSha256 && sha256 !== expectedSha256) throw new Error(`${asset.name}/${relativePath} hash mismatch: expected ${expectedSha256}, got ${sha256}`) + files.push({ path: relativePath, sha256 }) + } + return { + name: asset.name, + path: asset.path, + preloadTo: asset.preloadTo, + status: missingFiles.length === 0 ? 'verified' : 'missing', + files, + missingFiles, + } +} diff --git a/scripts/freecad-partdesign-structure-oracle.py b/scripts/freecad-partdesign-structure-oracle.py new file mode 100644 index 0000000..c816efe --- /dev/null +++ b/scripts/freecad-partdesign-structure-oracle.py @@ -0,0 +1,240 @@ +import json +import os +import tempfile + +import FreeCAD as App + + +def version_text(): + return ".".join(str(value) for value in App.Version()[:3]) + + +def names(objects): + return [obj.Name for obj in objects] + + +def link_sub_values(value): + output = [] + for obj, sub_elements in value: + if isinstance(sub_elements, str): + subs = [sub_elements] if sub_elements else [] + else: + subs = [str(sub) for sub in sub_elements if str(sub)] + output.append({"object": obj.Name, "subElements": subs}) + return output + + +def placement_json(placement): + axis = placement.Rotation.Axis + base = placement.Base + return { + "position": [float(base.x), float(base.y), float(base.z)], + "axis": [float(axis.x), float(axis.y), float(axis.z)], + "angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793), + } + + +document = App.newDocument("PartDesignStructureOracle") +source_body = document.addObject("PartDesign::Body", "SourceBody") +source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox") +source_box.Length = 10 +source_box.Width = 8 +source_box.Height = 6 + +datum_body = document.addObject("PartDesign::Body", "DatumBody") +datum_plane = datum_body.newObject("PartDesign::Plane", "DatumPlane") +datum_plane.AttachmentSupport = [(document.XY_Plane, "")] +datum_plane.MapMode = "FlatFace" +datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 5), App.Rotation(App.Vector(0, 0, 1), 30)) +datum_line = datum_body.newObject("PartDesign::Line", "DatumLine") +datum_line.AttachmentSupport = [(document.XY_Plane, "")] +datum_line.MapMode = "ObjectX" +datum_point = datum_body.newObject("PartDesign::Point", "DatumPoint") +datum_point.AttachmentSupport = [(document.XY_Plane, "")] +datum_point.MapMode = "ObjectOrigin" +datum_point.AttachmentOffset = App.Placement(App.Vector(1, 2, 3), App.Rotation()) + +binder_body = document.addObject("PartDesign::Body", "BinderBody") +shape_binder = binder_body.newObject("PartDesign::ShapeBinder", "ShapeBinder") +shape_binder.Support = [(source_box, "")] +shape_binder.TraceSupport = True + +sub_binder_body = document.addObject("PartDesign::Body", "SubBinderBody") +sub_binder = sub_binder_body.newObject("PartDesign::SubShapeBinder", "SubShapeBinder") +sub_binder.Support = [(source_box, ("Edge1", "Edge2", "Edge3", "Edge4"))] + +document.recompute() +initial = { + "sourceVolume": float(source_box.Shape.Volume), + "shapeBinderVolume": float(shape_binder.Shape.Volume), + "subShapeBinderLength": float(sub_binder.Shape.Length), + "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]}, + "bodyGroups": {body.Name: names(body.Group) for body in [source_body, datum_body, binder_body, sub_binder_body]}, + "datumPlane": {"mapMode": datum_plane.MapMode, "support": link_sub_values(datum_plane.AttachmentSupport), "attachmentOffset": placement_json(datum_plane.AttachmentOffset), "placement": placement_json(datum_plane.Placement), "state": [str(value) for value in datum_plane.State]}, + "datumLine": {"mapMode": datum_line.MapMode, "support": link_sub_values(datum_line.AttachmentSupport), "state": [str(value) for value in datum_line.State]}, + "datumPoint": {"mapMode": datum_point.MapMode, "support": link_sub_values(datum_point.AttachmentSupport), "attachmentOffset": placement_json(datum_point.AttachmentOffset), "placement": placement_json(datum_point.Placement), "state": [str(value) for value in datum_point.State]}, + "shapeBinder": {"support": link_sub_values(shape_binder.Support), "traceSupport": bool(shape_binder.TraceSupport), "state": [str(value) for value in shape_binder.State]}, + "subShapeBinder": {"support": link_sub_values(sub_binder.Support), "state": [str(value) for value in sub_binder.State]}, +} + +source_box.Length = 14 +datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 7), App.Rotation(App.Vector(0, 0, 1), 45)) +document.recompute() +edited = { + "sourceVolume": float(source_box.Shape.Volume), + "shapeBinderVolume": float(shape_binder.Shape.Volume), + "subShapeBinderLength": float(sub_binder.Shape.Length), + "datumPlaneAttachmentOffset": placement_json(datum_plane.AttachmentOffset), + "datumPlanePlacement": placement_json(datum_plane.Placement), + "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]}, +} + +invalid_plane = datum_body.newObject("PartDesign::Plane", "InvalidDatumPlane") +invalid_plane.AttachmentSupport = [(source_box, "Face999")] +invalid_plane.MapMode = "FlatFace" +document.recompute() +invalid_support = { + "typeId": invalid_plane.TypeId, + "support": link_sub_values(invalid_plane.AttachmentSupport), + "state": [str(value) for value in invalid_plane.State], + "status": str(invalid_plane.getStatusString()), +} + +with tempfile.TemporaryDirectory(prefix="freecad-partdesign-structure-") as temp_dir: + path = os.path.join(temp_dir, "PartDesignStructureOracle.FCStd") + document.recompute() + document.saveAs(path) + App.closeDocument(document.Name) + reopened_document = App.openDocument(path) + reopened_document.recompute() + reopened_source = reopened_document.getObject("SourceBox") + reopened_plane = reopened_document.getObject("DatumPlane") + reopened_shape_binder = reopened_document.getObject("ShapeBinder") + reopened_sub_binder = reopened_document.getObject("SubShapeBinder") + reopened_invalid = reopened_document.getObject("InvalidDatumPlane") + reopened_bodies = [reopened_document.getObject(name) for name in ["SourceBody", "DatumBody", "BinderBody", "SubBinderBody"]] + roundtrip = { + "sourceVolume": float(reopened_source.Shape.Volume), + "shapeBinderVolume": float(reopened_shape_binder.Shape.Volume), + "subShapeBinderLength": float(reopened_sub_binder.Shape.Length), + "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), + "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), + "datumPlaneSupport": link_sub_values(reopened_plane.AttachmentSupport), + "datumPlaneMapMode": reopened_plane.MapMode, + "datumPlaneAttachmentOffset": placement_json(reopened_plane.AttachmentOffset), + "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in reopened_bodies}, + "bodyGroups": {body.Name: names(body.Group) for body in reopened_bodies}, + "invalidDatumState": [str(value) for value in reopened_invalid.State], + "invalidDatumStatus": str(reopened_invalid.getStatusString()), + } + App.closeDocument(reopened_document.Name) + +with tempfile.TemporaryDirectory(prefix="freecad-partdesign-cross-document-") as temp_dir: + source_path = os.path.join(temp_dir, "PartDesignExternalSource.FCStd") + consumer_path = os.path.join(temp_dir, "PartDesignExternalConsumer.FCStd") + external_source = App.newDocument("PartDesignExternalSource") + external_source_body = external_source.addObject("PartDesign::Body", "ExternalSourceBody") + external_box = external_source_body.newObject("PartDesign::AdditiveBox", "ExternalBox") + external_box.Length = 4 + external_box.Width = 5 + external_box.Height = 6 + external_source.recompute() + external_source.saveAs(source_path) + external_consumer = App.newDocument("PartDesignExternalConsumer") + external_consumer.saveAs(consumer_path) + external_shape_body = external_consumer.addObject("PartDesign::Body", "ExternalShapeBody") + external_shape_binder = external_shape_body.newObject("PartDesign::ShapeBinder", "ExternalShapeBinder") + try: + external_shape_binder.Support = [(external_box, "")] + shape_binder_external_link = {"accepted": True, "errorType": None, "error": None} + except Exception as error: + shape_binder_external_link = {"accepted": False, "errorType": type(error).__name__, "error": str(error)} + external_sub_body = external_consumer.addObject("PartDesign::Body", "ExternalSubBody") + external_sub_binder = external_sub_body.newObject("PartDesign::SubShapeBinder", "ExternalSubShapeBinder") + external_sub_binder.Support = [(external_box, ("Face1",))] + external_consumer.recompute() + cross_document_initial = { + "sourceVolume": float(external_box.Shape.Volume), + "shapeBinderExternalLink": shape_binder_external_link, + "shapeBinderSupportType": external_shape_binder.getTypeIdOfProperty("Support"), + "subShapeBinderSupportType": external_sub_binder.getTypeIdOfProperty("Support"), + "shapeBinderShapeNull": bool(external_shape_binder.Shape.isNull()), + "subShapeBinderArea": float(external_sub_binder.Shape.Area), + "sourceFaceArea": float(external_box.Shape.Face1.Area), + "shapeBinderSupport": link_sub_values(external_shape_binder.Support), + "subShapeBinderSupport": link_sub_values(external_sub_binder.Support), + "subShapeBinderSourceDocument": external_sub_binder.Support[0][0].Document.Name, + "shapeBodyTip": external_shape_body.Tip.Name if external_shape_body.Tip else None, + "subBodyTip": external_sub_body.Tip.Name if external_sub_body.Tip else None, + } + external_source.save() + external_consumer.save() + App.closeDocument(external_consumer.Name) + App.closeDocument(external_source.Name) + reopened_source = App.openDocument(source_path) + reopened_consumer = App.openDocument(consumer_path) + reopened_source.recompute() + reopened_consumer.recompute() + reopened_box = reopened_source.getObject("ExternalBox") + reopened_shape_body = reopened_consumer.getObject("ExternalShapeBody") + reopened_sub_body = reopened_consumer.getObject("ExternalSubBody") + reopened_shape_binder = reopened_consumer.getObject("ExternalShapeBinder") + reopened_sub_binder = reopened_consumer.getObject("ExternalSubShapeBinder") + cross_document_roundtrip = { + "sourceVolume": float(reopened_box.Shape.Volume), + "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), + "subShapeBinderArea": float(reopened_sub_binder.Shape.Area), + "sourceFaceArea": float(reopened_box.Shape.Face1.Area), + "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), + "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), + "subShapeBinderSourceDocument": reopened_sub_binder.Support[0][0].Document.Name, + "shapeBodyTip": reopened_shape_body.Tip.Name if reopened_shape_body.Tip else None, + "subBodyTip": reopened_sub_body.Tip.Name if reopened_sub_body.Tip else None, + } + reopened_box.Width = 8 + reopened_source.recompute() + reopened_consumer.recompute() + cross_document_edited = { + "sourceVolume": float(reopened_box.Shape.Volume), + "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), + "subShapeBinderArea": float(reopened_sub_binder.Shape.Area), + "sourceFaceArea": float(reopened_box.Shape.Face1.Area), + "shapeBinderState": [str(value) for value in reopened_shape_binder.State], + "subShapeBinderState": [str(value) for value in reopened_sub_binder.State], + } + reopened_source.removeObject("ExternalBox") + reopened_source.recompute() + reopened_consumer.recompute() + cross_document_deleted = { + "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), + "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), + "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), + "subShapeBinderShapeNull": bool(reopened_sub_binder.Shape.isNull()), + "subShapeBinderCachedArea": float(reopened_sub_binder.Shape.Area), + "shapeBinderState": [str(value) for value in reopened_shape_binder.State], + "subShapeBinderState": [str(value) for value in reopened_sub_binder.State], + "shapeBinderStatus": str(reopened_shape_binder.getStatusString()), + "subShapeBinderStatus": str(reopened_sub_binder.getStatusString()), + } + App.closeDocument(reopened_consumer.Name) + App.closeDocument(reopened_source.Name) + +report = { + "schemaVersion": 1, + "baselineId": "freecad-1.1.1-partdesign-structure-oracle", + "freecadVersion": version_text(), + "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", + "status": "pass", + "tolerance": 1e-7, + "initial": initial, + "edited": edited, + "invalidSupport": invalid_support, + "roundtrip": roundtrip, + "crossDocument": { + "initial": cross_document_initial, + "roundtrip": cross_document_roundtrip, + "edited": cross_document_edited, + "deleted": cross_document_deleted, + }, +} +print("FREECAD_PARTDESIGN_STRUCTURE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":"))) diff --git a/scripts/generate-freecad-naming-sdk-manifest.mjs b/scripts/generate-freecad-naming-sdk-manifest.mjs new file mode 100644 index 0000000..5bdbbee --- /dev/null +++ b/scripts/generate-freecad-naming-sdk-manifest.mjs @@ -0,0 +1,92 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { resolve, relative } from 'node:path' +import { + REQUIRED_FREECAD_NAMING_CALLBACKS, + REQUIRED_FREECAD_NAMING_DEFINITIONS, + inspectIncludeDirectory, + inspectPlannedLibrary, + inspectPlannedRuntimeAsset, + loadSdkPlan, + resolveFrom, + exists, + sha256File, +} from './freecad-naming-sdk-lib.mjs' + +const root = resolve(new URL('..', import.meta.url).pathname) +const outputRoot = process.env.FREECAD_WASM_SDK_OUTPUT_DIR + ? resolve(root, process.env.FREECAD_WASM_SDK_OUTPUT_DIR) + : resolve(root, '.cache/toolchains/freecad-naming-sdk') +const plan = await loadSdkPlan(root) +const fail = (message) => { throw new Error(`FreeCAD WASM SDK manifest generation: ${message}`) } + +const libraries = [] +for (const library of [...(plan.libraries ?? []), ...(plan.linkDependencies ?? [])]) { + const inspected = await inspectPlannedLibrary(root, library) + if (inspected.status !== 'verified') fail(`missing ${library.name} wasm static library: ${resolveFrom(root, library.path)}`) + libraries.push({ + name: library.name, + path: relative(outputRoot, resolveFrom(root, library.path)), + sha256: inspected.sha256, + }) +} +const includeDirs = [] +for (const includeDir of plan.includeDirs ?? []) { + const inspected = await inspectIncludeDirectory(root, includeDir) + if (inspected.status !== 'verified') fail(`include directory ${includeDir.name} is incomplete: ${inspected.missingHeaders.join(', ')}`) + includeDirs.push(relative(outputRoot, resolveFrom(root, includeDir.path))) +} +const bridge = resolveFrom(root, plan.namingBridge.path) +if (!await exists(bridge)) fail(`missing naming bridge source: ${bridge}`) +const hostAdapter = resolveFrom(root, plan.namingBridge.hostAdapter) +if (!await exists(hostAdapter)) fail(`missing naming host adapter: ${hostAdapter}`) +const forceInclude = resolveFrom(root, plan.compileOptions.forceInclude) +if (!await exists(forceInclude)) fail(`missing force-include header: ${forceInclude}`) +if (plan.compileOptions.cxxStandard !== 'c++20' || plan.compileOptions.pthread !== true || JSON.stringify(plan.compileOptions.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options do not match the locked SDK contract') +const bridgeSha256 = await sha256File(bridge) +const runtimeAssets = [] +for (const asset of plan.runtimeAssets ?? []) { + const inspected = await inspectPlannedRuntimeAsset(root, asset) + if (inspected.status !== 'verified') fail(`runtime asset ${asset.name} is incomplete: ${inspected.missingFiles.join(', ')}`) + runtimeAssets.push({ + name: asset.name, + path: relative(outputRoot, resolveFrom(root, asset.path)), + preloadTo: asset.preloadTo, + files: inspected.files, + }) +} +const manifest = { + schemaVersion: 1, + freecadVersion: plan.baseline.freecadVersion, + sourceCommit: plan.baseline.sourceCommit, + emscriptenVersion: plan.baseline.emscriptenVersion, + qtTarget: plan.baseline.target, + pythonTarget: plan.baseline.target, + includeDirs, + libraries, + namingBridge: { + source: relative(outputRoot, bridge), + sha256: bridgeSha256, + hostAdapter: relative(outputRoot, hostAdapter), + hostAdapterSha256: await sha256File(hostAdapter), + exports: [...REQUIRED_FREECAD_NAMING_CALLBACKS], + }, + compileOptions: { + cxxStandard: plan.compileOptions.cxxStandard, + pthread: plan.compileOptions.pthread, + forceInclude: relative(outputRoot, forceInclude), + forceIncludeSha256: await sha256File(forceInclude), + definitions: [...plan.compileOptions.definitions], + }, + runtimeAssets, + productionPublication: false, + boundary: { ...plan.boundary }, +} +await mkdir(outputRoot, { recursive: true }) +const output = resolve(outputRoot, 'manifest.json') +await writeFile(output, `${JSON.stringify(manifest, null, 2)}\n`) +console.log(JSON.stringify({ + status: 'freecad-naming-sdk-manifest-generated', + output, + productionPublished: false, + systemExact: false, +}, null, 2)) diff --git a/scripts/offline-resource-lib.mjs b/scripts/offline-resource-lib.mjs index bbe1ad9..4c922c5 100644 --- a/scripts/offline-resource-lib.mjs +++ b/scripts/offline-resource-lib.mjs @@ -607,7 +607,7 @@ async function smoke() { FREECAD_SOURCE_OFFLINE: '1', OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'), } - for (const script of ['check:runtime', 'check:freecad-source', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) { + for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) { print(`[offline:smoke] npm run ${script}`) await run(resolve(root, 'npmw'), ['run', script], { env: environment }) } diff --git a/scripts/prepare-freecad-occt8-overlay.sh b/scripts/prepare-freecad-occt8-overlay.sh new file mode 100755 index 0000000..353641a --- /dev/null +++ b/scripts/prepare-freecad-occt8-overlay.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FREECAD_SOURCE_DIR="${ROOT_DIR}/.cache/freecad/FreeCAD" +OVERLAY_DIR="${ROOT_DIR}/.cache/toolchains/freecad-naming-sdk/source-occt8" +OCCT_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/migrate_raise_to_throw.py" +OCCT_TYPEDEF_MIGRATION="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/replace_typedefs.py" +OCCT_TYPEDEF_MAP="${ROOT_DIR}/.cache/occt/occt/adm/scripts/migration_800/collected_typedefs.json" +OCCT_TYPEDEF_RESULTS="adm/scripts/migration_800/replacement_results.json" +LOCKED_COMMIT="0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" + +[[ -f "${FREECAD_SOURCE_DIR}/CMakeLists.txt" ]] || { echo "FreeCAD source is missing: ${FREECAD_SOURCE_DIR}" >&2; exit 1; } +[[ -f "${OCCT_MIGRATION}" ]] || { echo "OCCT 8 migration script is missing: ${OCCT_MIGRATION}" >&2; exit 1; } +[[ -f "${OCCT_TYPEDEF_MIGRATION}" && -f "${OCCT_TYPEDEF_MAP}" ]] || { echo "OCCT 8 typedef migration inputs are missing" >&2; exit 1; } +git -C "${ROOT_DIR}/.cache/occt/occt" diff --quiet -- "${OCCT_TYPEDEF_RESULTS}" || { echo "OCCT migration result file is already modified" >&2; exit 1; } +cleanup_migration_result() { + git -C "${ROOT_DIR}/.cache/occt/occt" restore -- "${OCCT_TYPEDEF_RESULTS}" +} +trap cleanup_migration_result EXIT +[[ "$(git -C "${FREECAD_SOURCE_DIR}" rev-parse HEAD)" == "${LOCKED_COMMIT}" ]] || { echo "FreeCAD source commit is not locked" >&2; exit 1; } +git -C "${FREECAD_SOURCE_DIR}" diff --quiet +git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet + +if [[ -e "${OVERLAY_DIR}" && ! -f "${OVERLAY_DIR}/.git" ]]; then + echo "Refusing to replace non-worktree overlay: ${OVERLAY_DIR}" >&2 + exit 1 +fi +if [[ -f "${OVERLAY_DIR}/.git" ]]; then + git -C "${FREECAD_SOURCE_DIR}" worktree remove --force "${OVERLAY_DIR}" +fi +git -C "${FREECAD_SOURCE_DIR}" worktree prune +git -C "${FREECAD_SOURCE_DIR}" worktree add --detach "${OVERLAY_DIR}" "${LOCKED_COMMIT}" + +# The OCCT 8 script locates its own source root, so import it as a module and +# apply the documented Raise-to-throw phase only to FreeCAD's source overlay. +PYTHONDONTWRITEBYTECODE=1 python3 - "${OCCT_MIGRATION}" "${OVERLAY_DIR}/src" <<'PY' +import importlib.util +import pathlib +import sys + +script = pathlib.Path(sys.argv[1]) +source = pathlib.Path(sys.argv[2]) +spec = importlib.util.spec_from_file_location("occt8_raise_migration", script) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +files = module.find_files_with_raise(source) +for path in files: + module.process_file(path) +print(f"Applied OCCT 8 Raise-to-throw migration to {len(files)} FreeCAD source files") +PY +PYTHONDONTWRITEBYTECODE=1 python3 "${OCCT_TYPEDEF_MIGRATION}" \ + "${OVERLAY_DIR}/src" --input "${OCCT_TYPEDEF_MAP}" --jobs "${JOBS:-4}" +# replace_typedefs.py always writes a diagnostic beside the pinned tool. Its +# content is not an SDK input, so restore it immediately after the overlay run. +cleanup_migration_result + +# OCCT 8 moved iterator typedefs into their collection headers. The official +# typedef migration replaces usages but intentionally leaves include-only +# references untouched, so map the removed OCCT 7 header names explicitly. +while IFS='|' read -r removed_header collection_header; do + mapfile -d '' include_files < <( + rg -l -0 -F "#include <${removed_header}>" "${OVERLAY_DIR}/src" || true + ) + if (( ${#include_files[@]} > 0 )); then + sed -i "s|#include <${removed_header}>|#include <${collection_header}>|g" "${include_files[@]}" + fi +done <<'EOF' +BRepCheck_ListIteratorOfListOfStatus.hxx|BRepCheck_ListOfStatus.hxx +TColStd_ListIteratorOfListOfTransient.hxx|TColStd_ListOfTransient.hxx +TColStd_MapIteratorOfMapOfTransient.hxx|TColStd_MapOfTransient.hxx +TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape.hxx|TopTools_DataMapOfIntegerListOfShape.hxx +TopTools_DataMapIteratorOfDataMapOfShapeShape.hxx|TopTools_DataMapOfShapeShape.hxx +TopTools_ListIteratorOfListOfShape.hxx|TopTools_ListOfShape.hxx +EOF + +# Standard_Failure no longer participates in OCCT RTTI. OCCT 8 documents +# ExceptionType() as the source-compatible exception class-name API. +while IFS='|' read -r legacy_expression replacement_expression; do + mapfile -d '' exception_files < <( + rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true + ) + if (( ${#exception_files[@]} > 0 )); then + sed -i "s|${legacy_expression}|${replacement_expression}|g" "${exception_files[@]}" + fi +done <<'EOF' +e.DynamicType()->get_type_name()|e.ExceptionType() +e.DynamicType()->Name()|e.ExceptionType() +ex.DynamicType()->Name()|ex.ExceptionType() +EOF + +# BRepAdaptor_Curve exposes these inherited parameter accessors directly in +# OCCT 8; the removed BRepLProp_CurveTool only forwarded these calls here. +while IFS='|' read -r legacy_expression replacement_expression; do + mapfile -d '' curve_tool_files < <( + rg -l -0 -F "${legacy_expression}" "${OVERLAY_DIR}/src" || true + ) + if (( ${#curve_tool_files[@]} > 0 )); then + sed -i "s|${legacy_expression}|${replacement_expression}|g" "${curve_tool_files[@]}" + fi +done <<'EOF' +BRepLProp_CurveTool::FirstParameter(adapt)|adapt.FirstParameter() +BRepLProp_CurveTool::LastParameter(adapt)|adapt.LastParameter() +EOF +mapfile -d '' curve_tool_include_files < <( + rg -l -0 '^[[:space:]]*#[[:space:]]*include[[:space:]]*' \ + "${OVERLAY_DIR}/src" || true +) +if (( ${#curve_tool_include_files[@]} > 0 )); then + sed -i -E '\|^[[:space:]]*#[[:space:]]*include[[:space:]]*|d' \ + "${curve_tool_include_files[@]}" +fi + +# OCCT 8 consolidates Geom2dLProp into GeomLProp while preserving the 2D +# specialization's constructor and methods under the unified class name. +mapfile -d '' geom2d_lprop_files < <( + rg -l -0 -F 'Geom2dLProp_CLProps2d' "${OVERLAY_DIR}/src" || true +) +if (( ${#geom2d_lprop_files[@]} > 0 )); then + sed -i \ + -e 's|#include |#include |g' \ + -e 's|\bGeom2dLProp_CLProps2d\b|GeomLProp_CLProps2d|g' \ + "${geom2d_lprop_files[@]}" +fi + +# OCCT 8 adds an initializer-list constructor to NCollection_List, making +# these calls ambiguous with FreeCAD's std::vector overload. The +# latter was the only viable overload with the pinned FreeCAD/OCCT 7 sources. +TOPO_SHAPE_EXPANSION="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapeExpansion.cpp" +sed -i \ + -e 's|mapper.populate(MappingStatus::Modified, e, {e1, e2, e3, e4});|mapper.populate(MappingStatus::Modified, e, std::vector{e1, e2, e3, e4});|' \ + -e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::FirstVertex(e1)});|mapper.populate(MappingStatus::Generated, v, std::vector{TopExp::FirstVertex(e1)});|' \ + -e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::LastVertex(e4)});|mapper.populate(MappingStatus::Generated, v, std::vector{TopExp::LastVertex(e4)});|' \ + "${TOPO_SHAPE_EXPANSION}" + +# Standard_Failure inherits std::exception in OCCT 8 and what() returns the +# same message as the deprecated GetMessageString(). Remove the three now +# unreachable duplicate fallbacks that immediately follow an identical +# std::exception handler. +TOPO_SHAPE_PY="${OVERLAY_DIR}/src/Mod/Part/App/TopoShapePyImp.cpp" +perl -0pi -e ' + s|( catch \(const std::exception& e\) \{\n PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n return nullptr;\n \})\n catch \(Standard_Failure& e\) \{\n(?:\n)? PyErr_SetString\(PartExceptionOCCError, e\.GetMessageString\(\)\);\n return nullptr;\n \}|$1|g +' "${TOPO_SHAPE_PY}" + +if git -C "${OVERLAY_DIR}" status --porcelain | awk '{print $2}' | grep -Ev '^src/' >/dev/null; then + echo "OCCT 8 migration changed files outside the FreeCAD src directory" >&2 + exit 1 +fi +remaining="$( (rg -l 'Standard_[A-Za-z0-9_]+::Raise\s*\(' "${OVERLAY_DIR}/src" -g '*.{h,hpp,hxx,c,cpp,cxx,lxx,pxx}' || true) | wc -l)" +[[ "${remaining}" == "0" ]] || { echo "OCCT 8 migration left ${remaining} source files with legacy Raise calls" >&2; exit 1; } +removed_iterator_headers="$( (rg -l '#include <(BRepCheck_ListIteratorOfListOfStatus|TColStd_ListIteratorOfListOfTransient|TColStd_MapIteratorOfMapOfTransient|TopTools_DataMapIteratorOfDataMapOfIntegerListOfShape|TopTools_DataMapIteratorOfDataMapOfShapeShape|TopTools_ListIteratorOfListOfShape)\.hxx>' "${OVERLAY_DIR}/src" || true) | wc -l)" +[[ "${removed_iterator_headers}" == "0" ]] || { echo "OCCT 8 migration left ${removed_iterator_headers} files with removed iterator headers" >&2; exit 1; } +legacy_exception_rtti="$( (rg -l '\b(e|ex)\.DynamicType\(\)->(Name|get_type_name)\(\)' "${OVERLAY_DIR}/src" || true) | wc -l)" +[[ "${legacy_exception_rtti}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_exception_rtti} files with legacy exception RTTI" >&2; exit 1; } +legacy_curve_tool="$( (rg -l '\bBRepLProp_CurveTool\b' "${OVERLAY_DIR}/src" || true) | wc -l)" +[[ "${legacy_curve_tool}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_curve_tool} files using removed BRepLProp_CurveTool" >&2; exit 1; } +legacy_geom2d_lprop="$( (rg -l '\bGeom2dLProp_CLProps2d\b' "${OVERLAY_DIR}/src" || true) | wc -l)" +[[ "${legacy_geom2d_lprop}" == "0" ]] || { echo "OCCT 8 migration left ${legacy_geom2d_lprop} files using removed Geom2dLProp_CLProps2d" >&2; exit 1; } +ambiguous_shape_mapper_calls="$( (rg -n 'mapper\.populate\(MappingStatus::(Modified|Generated), (e|v), \{' "${TOPO_SHAPE_EXPANSION}" || true) | wc -l)" +[[ "${ambiguous_shape_mapper_calls}" == "0" ]] || { echo "OCCT 8 migration left ${ambiguous_shape_mapper_calls} ambiguous ShapeMapper calls" >&2; exit 1; } +unreachable_occt_exception_fallbacks="$( (rg -U -n 'catch \(const std::exception& e\) \{\n PyErr_SetString\(PartExceptionOCCError, e\.what\(\)\);\n return nullptr;\n \}\n catch \(Standard_Failure& e\)' "${TOPO_SHAPE_PY}" || true) | wc -l)" +[[ "${unreachable_occt_exception_fallbacks}" == "0" ]] || { echo "OCCT 8 migration left ${unreachable_occt_exception_fallbacks} unreachable Standard_Failure handlers" >&2; exit 1; } + +git -C "${FREECAD_SOURCE_DIR}" diff --quiet +git -C "${FREECAD_SOURCE_DIR}" diff --cached --quiet +echo "FreeCAD OCCT 8 candidate overlay ready: ${OVERLAY_DIR}" diff --git a/scripts/run-chrome-freecad-naming-worker-candidate.mjs b/scripts/run-chrome-freecad-naming-worker-candidate.mjs new file mode 100644 index 0000000..a9ffb96 --- /dev/null +++ b/scripts/run-chrome-freecad-naming-worker-candidate.mjs @@ -0,0 +1,106 @@ +import { createHash } from 'node:crypto' +import { createReadStream, existsSync } from 'node:fs' +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' +import { createServer } from 'node:http' +import { extname, join, normalize, resolve } from 'node:path' +import { spawn } from 'node:child_process' +import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs' + +const root = resolve(new URL('..', import.meta.url).pathname) +const candidateRoot = resolve(root, '.cache/candidates/freecad-naming-worker') +const reportPath = resolve(root, '.cache/toolchains/freecad-naming-sdk/chrome-candidate-worker-report.json') +const chrome = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome' +const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data'] +for (const name of artifactNames) if (!existsSync(resolve(candidateRoot, name))) throw new Error(`Missing candidate Worker artifact: ${name}`) +if (!existsSync(chrome)) throw new Error(`Chrome executable is unavailable: ${chrome}`) + +const artifacts = await Promise.all(artifactNames.map(async (name) => { + const path = resolve(candidateRoot, name) + const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)]) + return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') } +})) +let resolveReport +let rejectReport +const reportPromise = new Promise((resolveValue, rejectValue) => { resolveReport = resolveValue; rejectReport = rejectValue }) +const html = `FreeCAD naming candidate Worker` +const contentTypes = { '.js': 'text/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream' } +const server = createServer((request, response) => { + response.setHeader('Cross-Origin-Opener-Policy', 'same-origin') + response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') + response.setHeader('Cross-Origin-Resource-Policy', 'same-origin') + if (request.method === 'POST' && request.url === '/__report') { + let body = '' + request.setEncoding('utf8') + request.on('data', (chunk) => { body += chunk }) + request.on('end', () => { + try { resolveReport(JSON.parse(body)); response.writeHead(204); response.end() } + catch (error) { rejectReport(error); response.writeHead(400); response.end(String(error)) } + }) + return + } + if (request.url === '/' || request.url === '/index.html') { response.setHeader('content-type', 'text/html'); response.end(html); return } + if (!request.url?.startsWith('/candidate/')) { response.writeHead(404); response.end('not found'); return } + const file = normalize(join(candidateRoot, decodeURIComponent(request.url.slice('/candidate/'.length)))) + if (!file.startsWith(candidateRoot) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return } + response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream') + createReadStream(file).pipe(response) +}) +await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer)) +const port = server.address().port +const profile = await createChromeProfile('freecad-naming-candidate') +const child = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] }) +let stderr = '' +child.stderr.on('data', (chunk) => { stderr += String(chunk) }) +const timeout = setTimeout(() => { rejectReport(new Error(`Chrome candidate Worker harness timed out. ${stderr.slice(-2000)}`)); child.kill('SIGTERM') }, 240_000) +let report +try { + report = await reportPromise + report = { ...report, artifacts, generatedAt: new Date().toISOString() } + await mkdir(resolve(reportPath, '..'), { recursive: true }) + await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`) + console.log(JSON.stringify(report, null, 2)) + if (report.status !== 'pass') process.exitCode = 1 +} finally { + clearTimeout(timeout) + child.kill('SIGTERM') + await new Promise((resolveServer) => server.close(resolveServer)) + await removeChromeProfile(profile) +} diff --git a/scripts/run-freecad-attachment-mode-oracle.mjs b/scripts/run-freecad-attachment-mode-oracle.mjs new file mode 100644 index 0000000..9df2ca0 --- /dev/null +++ b/scripts/run-freecad-attachment-mode-oracle.mjs @@ -0,0 +1,27 @@ +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') +if (!existsSync(executable)) throw new Error(`FreeCAD attachment mode oracle executable is missing: ${executable}`) +const sysroot = resolve(root, '.cache/freecad/sysroot') +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-attachment-mode-oracle.py')], { + cwd: root, + encoding: 'utf8', + timeout: 180_000, + maxBuffer: 20 * 1024 * 1024, + env: { + ...process.env, + PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, + LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, + }, +}) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +const marker = 'FREECAD_ATTACHMENT_MODE_RESULT=' +const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker)) +if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD attachment mode 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-attachment-mode-oracle.json'), `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, modes: report.registry.modeCount, engines: Object.fromEntries(Object.entries(report.engines).map(([name, engine]) => [name, engine.implementedModes.length])), successCases: Object.keys(report.success.initial).length, failureCases: Object.keys(report.failures.initial).length, fcstdRoundtrip: true }, null, 2)) diff --git a/scripts/run-freecad-partdesign-structure-oracle.mjs b/scripts/run-freecad-partdesign-structure-oracle.mjs new file mode 100644 index 0000000..b80fc80 --- /dev/null +++ b/scripts/run-freecad-partdesign-structure-oracle.mjs @@ -0,0 +1,27 @@ +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') +if (!existsSync(executable)) throw new Error(`FreeCAD PartDesign structure oracle executable is missing: ${executable}`) +const sysroot = resolve(root, '.cache/freecad/sysroot') +const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-partdesign-structure-oracle.py')], { + cwd: root, + encoding: 'utf8', + timeout: 180_000, + maxBuffer: 20 * 1024 * 1024, + env: { + ...process.env, + PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, + LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, + }, +}) +const output = `${execution.stdout || ''}\n${execution.stderr || ''}` +const marker = 'FREECAD_PARTDESIGN_STRUCTURE_RESULT=' +const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker)) +if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PartDesign structure 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-partdesign-structure-oracle.json'), `${JSON.stringify(report, null, 2)}\n`) +console.log(JSON.stringify(report, null, 2)) diff --git a/scripts/run-real-verification.mjs b/scripts/run-real-verification.mjs index 12cf7bb..d08e0d6 100644 --- a/scripts/run-real-verification.mjs +++ b/scripts/run-real-verification.mjs @@ -28,7 +28,22 @@ const chromeTests = Object.keys(scripts) .sort() const wasmBuilds = process.env.CI_REAL_REBUILD_WASM === '1' - ? ['fetch:freecad-source', 'build:occt-history', 'build:planegcs'] + ? [ + 'fetch:freecad-source', + 'build:qt6-wasm-core', + 'build:qt6-freecad-wasm', + 'build:yaml-cpp-wasm', + 'build:icu-wasm', + 'build:cpython-wasm', + 'build:xerces-c-wasm', + 'build:boost-wasm', + 'build:freecad-naming-source-probe', + 'test:freecad-naming-source-probe', + 'check:freecad-naming-sdk-readiness', + 'check:freecad-wasm-sdk-build-plan', + 'build:occt-history', + 'build:planegcs', + ] : [] if (wasmBuilds.length > 0) { for (const variable of ['OCCT_SOURCE_DIR', 'EMSDK']) { @@ -63,6 +78,8 @@ const lanes = { 'probe:freecad-partdesign-loft', 'probe:freecad-partdesign-dressup', 'probe:freecad-partdesign-transform', + 'probe:freecad-partdesign-structure', + 'probe:freecad-attachment-modes', 'probe:freecad-partdesign-failures', 'probe:freecad-partdesign-revolution-groove', 'probe:freecad-part-builders', diff --git a/src/facade/nativeNamingAbi.ts b/src/facade/nativeNamingAbi.ts index 5ceaa31..7737469 100644 --- a/src/facade/nativeNamingAbi.ts +++ b/src/facade/nativeNamingAbi.ts @@ -33,6 +33,12 @@ export type NativeFreeCadNamingAbiModule = { freecadNamingEvidenceJson?(requestJson: string): string } +type FreeCadPrivateNamingAbiError = { + schemaVersion: 1 + status: 'error' + error: string +} + export type FreeCadPrivateNamingAbiRequest = { schemaVersion: 1 requestId: string @@ -124,7 +130,12 @@ export const captureFreeCadPrivateNamingEvidence = ( const responseBytes = encoder.encode(responseJson).byteLength const responseLimit = boundedLimit(probe.descriptor.maxResponseBytes, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES) if (responseBytes > responseLimit) throw new RangeError(`FreeCAD naming ABI response exceeds ${responseLimit} bytes.`) - const evidence = assertNativeNamingEvidence(JSON.parse(responseJson) as NativeStageNamingEvidence) + const parsed = JSON.parse(responseJson) as NativeStageNamingEvidence | FreeCadPrivateNamingAbiError + if (parsed && parsed.status === 'error') { + if (parsed.schemaVersion !== FREECAD_PRIVATE_NAMING_ABI_VERSION || typeof parsed.error !== 'string' || !parsed.error.trim()) throw new TypeError('FreeCAD naming ABI returned a malformed error response.') + throw new Error(`FreeCAD naming ABI failed: ${parsed.error}`) + } + const evidence = assertNativeNamingEvidence(parsed) if (evidence.stageId !== request.stageId || evidence.resultObjectId !== request.resultObjectId) throw new RangeError('FreeCAD naming ABI response does not match its stage and result object context.') if (evidence.status !== 'native-evidence' && evidence.status !== 'ambiguous') throw new TypeError(`FreeCAD naming ABI cannot return non-native status '${evidence.status}'.`) if (!evidence.stringHasher || !evidence.elementMap2) throw new TypeError('FreeCAD naming ABI response requires both StringHasher and ElementMap2 evidence.') diff --git a/tests/freecadPrivateNamingBoundary.test.mjs b/tests/freecadPrivateNamingBoundary.test.mjs index 32dcbb4..59f87e4 100644 --- a/tests/freecadPrivateNamingBoundary.test.mjs +++ b/tests/freecadPrivateNamingBoundary.test.mjs @@ -1,12 +1,21 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { readFile } from 'node:fs/promises' -import { resolve } from 'node:path' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' +import { tmpdir } from 'node:os' +import { inspectWasmStaticArchive } from '../scripts/freecad-naming-sdk-lib.mjs' const root = resolve(new URL('..', import.meta.url).pathname) +const arArchive = (member, bytes) => { + const field = (value, length) => String(value).padEnd(length, ' ') + const header = `${field(`${member}/`, 16)}${field(0, 12)}${field(0, 6)}${field(0, 6)}${field('100644', 8)}${field(bytes.length, 10)}` + '`\n' + return Buffer.concat([Buffer.from('!\n'), Buffer.from(header), bytes, ...(bytes.length % 2 ? [Buffer.from('\n')] : [])]) +} + test('Qt wasm Core build pins the released QtBase source and project Emscripten baseline', async () => { const source = await readFile(resolve(root, 'scripts/build-qt6-wasm-core.sh'), 'utf8') + const freecadQt = await readFile(resolve(root, 'scripts/build-qt6-freecad-wasm.sh'), 'utf8') assert.match(source, /QT_VERSION="6\.8\.2"/) assert.match(source, /012043ce6d411e6e8a91fdc4e05e6bedcfa10fcb1347d3c33908f7fdd10dfe05/) assert.match(source, /Emscripten 3\.1\.69/) @@ -14,19 +23,45 @@ test('Qt wasm Core build pins the released QtBase source and project Emscripten assert.match(source, /WEB_FREECAD_OFFLINE/) assert.match(source, /cmake --install "\$\{QT_BUILD_DIR\}\/src\/corelib"/) assert.doesNotMatch(source, /cmake --install[^\n]+\|\| true/) + assert.match(freecadQt, /Core Concurrent Network Xml/) + assert.match(freecadQt, /feature-thread/) + assert.match(freecadQt, /mkspecs\/wasm-emscripten/) + assert.match(freecadQt, /inspectWasmStaticArchive/) }) test('FreeCAD source probe pins the exact private source inputs and does not export the production ABI', async () => { const build = await readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8') const smoke = await readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8') + const candidate = await readFile(resolve(root, 'native/freecad-naming-probe/abi-candidate-smoke.ts'), 'utf8') assert.match(build, /0108fd4b4850cc46e625b60e53cea7a7bbe69f8d/) assert.match(build, /73e0e60a9d6ee06851252e2071ebdd58698f8f99733903232529b3a194164435/) assert.match(build, /90173aba5f331ac9453589833f7e5fc63c5c80a35f1600dbc71211606b1c9dbc/) + assert.match(build, /fd32f35c9c2a0c21a60fa634ec37ec594d6f331924c5bba7fdbbcb351c3b612c/) + assert.match(build, /8f02d27a8b672e9a56764f85e394265b4fcf023aba097f040b03dcb38bc393d2/) + assert.match(build, /StringHasher\.cpp/) + assert.match(build, /Base\/Handle\.cpp/) + assert.match(build, /f5448d9de693e319460d5e3ef52c63a54e0948963e4b92786aac38c77bdcd7a5/) + assert.match(build, /fc3223857ca6d2990d2b48f437f75841cdf23e1e79fa18ef18296f40acd3cdc3/) + assert.match(build, /e1ceaedb624688ddffa6a834c1b32539e8ba5bbdc0ec31401ea087e6a9c3eb52/) + assert.match(build, /MappedElement\.cpp/) + assert.match(build, /ElementNamingUtils\.cpp/) + assert.match(build, /ElementMap\.cpp/) + assert.match(build, /libFreeCADPrivateNamingProbe\.a/) + assert.match(build, /emar rcs/) + assert.match(build, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/) assert.doesNotMatch(build, /public\/native\/freecad-naming-probe/) for (const callback of ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']) { assert.match(smoke, new RegExp(callback)) } assert.match(smoke, /productionWorkerLinked: false/) + assert.match(smoke, /freecadPrivateStringHasherSourceProbe/) + assert.match(smoke, /freecadPrivateElementMapSourceProbe/) + assert.match(smoke, /archive\.memberCount !== 7/) + assert.match(smoke, /productionEligible: false/) + assert.match(candidate, /captureFreeCadPrivateNamingEvidence/) + assert.match(candidate, /parseElementMap2/) + assert.match(candidate, /productionCallbacksExported: false/) + assert.match(candidate, /productionWorkerLinked: false/) }) test('private naming boundary checker cross-validates all authoritative status files', async () => { @@ -35,8 +70,148 @@ test('private naming boundary checker cross-validates all authoritative status f 'config/freecad-sketcher-partdesign-abi-contract.json', 'config/compatibility-matrix.json', 'config/freecad-web-exact-parity-plan.json', + 'config/freecad-private-naming-source-readiness.json', + 'config/freecad-naming-sdk-plan.json', ]) assert.match(source, new RegExp(path.replaceAll('.', '\\.'))) assert.match(source, /shippedWorkerImplementation !== 'not-linked'/) assert.match(source, /task\.status !== 'in_progress'/) assert.match(source, /systemExactEvaluation\?\.exact !== false/) + assert.match(source, /production\?\.workerLinked !== false/) + assert.match(source, /candidateAbi\?\.scope !== 'isolated-non-production'/) + assert.match(source, /callback\.startsWith\('freecadNamingCandidate'\)/) + assert.match(source, /FreeCAD private naming Worker bridge/) + assert.match(source, /sdkPlan\.productionPublication !== false/) +}) + +test('SDK readiness verifies the complete candidate wasm closure and keeps it pre-production', async () => { + const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-naming-sdk-plan.json'), 'utf8')) + const readiness = await readFile(resolve(root, 'scripts/check-freecad-naming-sdk-readiness.mjs'), 'utf8') + const sdkBuildPlan = await readFile(resolve(root, 'config/freecad-wasm-sdk-build-plan.json'), 'utf8') + const sdkBootstrap = await readFile(resolve(root, 'config/freecad-wasm-sdk-bootstrap.cmake'), 'utf8') + const sdkCompat = await readFile(resolve(root, 'config/freecad-wasm-sdk-compat.h'), 'utf8') + const sdkExecinfo = await readFile(resolve(root, 'config/wasm-compat/execinfo.h'), 'utf8') + const sdkToolchain = await readFile(resolve(root, 'config/emscripten-freecad-candidate-toolchain.cmake'), 'utf8') + const sdkConfigure = await readFile(resolve(root, 'scripts/configure-freecad-naming-sdk-candidate.mjs'), 'utf8') + const sdkBuild = await readFile(resolve(root, 'scripts/build-freecad-naming-sdk-candidate.sh'), 'utf8') + const sdkOverlay = await readFile(resolve(root, 'scripts/prepare-freecad-occt8-overlay.sh'), 'utf8') + const sdkCheck = await readFile(resolve(root, 'scripts/check-freecad-naming-sdk.mjs'), 'utf8') + const sdkManifest = await readFile(resolve(root, 'scripts/generate-freecad-naming-sdk-manifest.mjs'), 'utf8') + const bridgeBuild = await readFile(resolve(root, 'scripts/build-freecad-naming-bridge-candidate.sh'), 'utf8') + const bridgeSmoke = await readFile(resolve(root, 'native/freecad-naming-bridge/smoke-test.ts'), 'utf8') + const bridgeCheck = await readFile(resolve(root, 'scripts/check-freecad-naming-bridge-candidate.mjs'), 'utf8') + const workerBuild = await readFile(resolve(root, 'native/occt-history/build.sh'), 'utf8') + const workerSmoke = await readFile(resolve(root, 'native/occt-history/freecad-naming-candidate-smoke.ts'), 'utf8') + const chromeRun = await readFile(resolve(root, 'scripts/run-chrome-freecad-naming-worker-candidate.mjs'), 'utf8') + const chromeCheck = await readFile(resolve(root, 'scripts/check-chrome-freecad-naming-worker-candidate.mjs'), 'utf8') + assert.equal(plan.productionPublication, false) + assert.deepEqual(plan.libraries.map(({ name }) => name), ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']) + assert.deepEqual(plan.linkDependencies.map(({ name }) => name), ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3']) + assert.equal(plan.compileOptions.cxxStandard, 'c++20') + assert.equal(plan.compileOptions.pthread, true) + assert.deepEqual(plan.runtimeAssets.map(({ name }) => name), ['PythonWasmStdlib']) + assert.match(readiness, /inspectPlannedLibrary/) + assert.match(readiness, /publishToWorker: false/) + assert.match(readiness, /inspectPlannedRuntimeAsset/) + assert.match(sdkCheck, /inspectWasmStaticArchive/) + assert.match(sdkCheck, /hostAdapterSha256/) + assert.match(sdkCheck, /forceIncludeSha256/) + assert.match(sdkCheck, /PythonWasmStdlib/) + assert.match(sdkManifest, /runtimeAssets/) + assert.match(sdkManifest, /productionPublication: false/) + assert.match(sdkBuildPlan, /candidate-only-freecad-wasm-sdk-build/) + assert.match(sdkBuildPlan, /productionPublication": false/) + assert.match(sdkBootstrap, /FREECAD_WASM_EMBED_TRANSLATIONS OFF/) + assert.match(sdkBootstrap, /function\(qt_add_translation output_variable\)/) + assert.match(sdkBootstrap, /add_compile_definitions\(__linux__=1\)/) + assert.match(sdkBootstrap, /FREECAD_WASM_OCCT_COMPAT_INCLUDE/) + assert.match(sdkBootstrap, /TopTools_ListOfShape\.hxx/) + assert.match(sdkBootstrap, /set\(UNIX FALSE\)/) + assert.match(sdkToolchain, /Platform\/Emscripten\.cmake/) + assert.match(sdkToolchain, /set\(UNIX FALSE\)/) + assert.match(sdkToolchain, /CMAKE_CXX_CREATE_SHARED_LIBRARY/) + assert.match(sdkBootstrap, /freecad-wasm-sdk-compat\.h/) + assert.match(sdkCompat, /#include /) + assert.match(sdkCompat, /#include /) + assert.match(sdkCompat, /#define systemTimeZone\(\) UTC/) + assert.doesNotMatch(sdkExecinfo, /backtrace\s*\(/) + assert.match(sdkConfigure, /Qt6_DIR/) + assert.match(sdkConfigure, /Python3_INCLUDE_DIRS/) + assert.match(sdkConfigure, /FREECAD_WASM_OFFLINE_SYSROOT/) + assert.match(sdkConfigure, /src\/Deprecated\/NCollectionAliases/) + assert.match(sdkConfigure, /source-occt8/) + assert.match(sdkOverlay, /migrate_raise_to_throw\.py/) + assert.match(sdkOverlay, /replace_typedefs\.py/) + assert.match(sdkOverlay, /collected_typedefs\.json/) + assert.match(sdkOverlay, /TopTools_ListIteratorOfListOfShape\.hxx\|TopTools_ListOfShape\.hxx/) + assert.match(sdkOverlay, /removed_iterator_headers/) + assert.match(sdkOverlay, /e\.DynamicType\(\)->get_type_name\(\)\|e\.ExceptionType\(\)/) + assert.match(sdkOverlay, /legacy_exception_rtti/) + assert.match(sdkOverlay, /BRepLProp_CurveTool::FirstParameter\(adapt\)\|adapt\.FirstParameter\(\)/) + assert.match(sdkOverlay, /legacy_curve_tool/) + assert.match(sdkOverlay, /Geom2dLProp_CLProps2d\.hxx>\|#include /) + assert.match(sdkOverlay, /legacy_geom2d_lprop/) + assert.match(sdkOverlay, /std::vector\{e1, e2, e3, e4\}/) + assert.match(sdkOverlay, /ambiguous_shape_mapper_calls/) + assert.match(sdkOverlay, /Standard_Failure inherits std::exception/) + assert.match(sdkOverlay, /unreachable_occt_exception_fallbacks/) + assert.match(sdkOverlay, /worktree add --detach/) + assert.match(sdkOverlay, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/) + assert.match(sdkConfigure, /FreeCADBase|BUILD_PART/) + assert.match(sdkConfigure, /callbacks: \[\]/) + assert.match(sdkBuild, /cmake --build "\$\{BUILD_DIR\}" --target Part/) + assert.match(sdkBuild, /libFreeCADBase\.a/) + assert.match(sdkBuild, /ElementMap\.cpp\.o/) + assert.match(sdkBuild, /productionManifestGenerated: false/) + assert.match(sdkBuild, /availability: 'unavailable'/) + assert.match(bridgeBuild, /libHacl_Hash_SHA2\.a/) + assert.match(bridgeBuild, /--preload-file/) + assert.match(bridgeBuild, /FREECAD_NAMING_BRIDGE_REPORT/) + assert.match(bridgeBuild, /public\/native\/freecad-naming-bridge/) + assert.match(bridgeSmoke, /freecad-private-naming-isolated-bridge-pass/) + assert.match(bridgeSmoke, /thirdStage/) + assert.match(bridgeSmoke, /tamperedHasherRejected/) + assert.match(bridgeSmoke, /inconsistentTablesRejected/) + assert.match(bridgeSmoke, /productionWorkerLinked: false/) + assert.match(bridgeCheck, /thirdStage\?\.stringHasherEntries <= report\.chainedStage\.stringHasherEntries/) + assert.match(bridgeCheck, /stale artifact evidence/) + assert.match(workerBuild, /OCCT_HISTORY_PUBLISH/) + assert.match(workerBuild, /m\.runtimeAssets/) + assert.match(workerBuild, /m\.compileOptions\.definitions/) + assert.match(workerBuild, /OCCT_CXX_FLAGS\+=" -pthread"/) + assert.match(workerSmoke, /booleanHistoryFromStep/) + assert.match(workerSmoke, /thirdStage/) + assert.match(workerSmoke, /invalidHistoryRejected: true/) + assert.match(chromeRun, /crossOriginIsolated/) + assert.match(chromeRun, /thirdStage/) + assert.match(chromeRun, /productionWorkerLinked: false/) + assert.match(chromeCheck, /thirdStage\?\.stringHasherEntries <= report\.chainedStage\.stringHasherEntries/) + assert.match(chromeCheck, /stale artifact evidence/) + const fixtureDir = await mkdtemp(join(tmpdir(), 'freecad-naming-sdk-')) + try { + const wasmArchive = join(fixtureDir, 'libwasm.a') + const hostArchive = join(fixtureDir, 'libhost.a') + await writeFile(wasmArchive, arArchive('wasm.o', Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]))) + await writeFile(hostArchive, arArchive('host.o', Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00]))) + const wasm = await inspectWasmStaticArchive(wasmArchive) + assert.equal(wasm.target, 'wasm32-emscripten') + assert.equal(wasm.wasmObjectCount, 1) + await assert.rejects(inspectWasmStaticArchive(hostArchive), /host ELF object/) + } finally { + await rm(fixtureDir, { recursive: true, force: true }) + } +}) + +test('real wasm and offline smoke lanes rebuild and execute the source prerequisite', async () => { + const real = await readFile(resolve(root, 'scripts/run-real-verification.mjs'), 'utf8') + const offline = await readFile(resolve(root, 'scripts/offline-resource-lib.mjs'), 'utf8') + for (const source of [real, offline]) { + assert.match(source, /build:freecad-naming-source-probe/) + assert.match(source, /test:freecad-naming-source-probe/) + assert.match(source, /check:freecad-naming-sdk-readiness/) + assert.match(source, /check:freecad-wasm-sdk-build-plan/) + } + assert.match(real, /build:qt6-wasm-core/) + assert.match(real, /probe:freecad-attachment-modes/) + assert.match(real, /name\.startsWith\('test:chrome-'\)/) + assert.match(real, /requestedPhase === 'execute' \? chromeExecution : chromeChecks/) })