feat: add candidate FreeCAD naming SDK and attachment oracles
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

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.
This commit is contained in:
2026-08-13 17:16:07 -04:00
parent 58c0807219
commit f97eae4153
79 changed files with 7936 additions and 77 deletions

View File

@@ -0,0 +1 @@
using emscripten : 3.1.69 : ./boost-emscripten-compiler-wrapper.sh ;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 "<CMAKE_AR> rcs <TARGET> <OBJECTS>")
set(CMAKE_CXX_CREATE_SHARED_LIBRARY "<CMAKE_AR> rcs <TARGET> <OBJECTS>")
# 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)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(
"$<$<COMPILE_LANGUAGE:CXX>:-include${CMAKE_CURRENT_LIST_DIR}/freecad-wasm-sdk-compat.h>"
"$<$<COMPILE_LANGUAGE:CXX>:-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()

View File

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

View File

@@ -0,0 +1,11 @@
#pragma once
#ifdef __EMSCRIPTEN__
#if __has_include(<Standard_Macro.hxx>)
#include <TopTools_HSequenceOfShape.hxx>
#include <TopTools_ListOfShape.hxx>
#endif
// QTimeZone::UTC remains available when Qt's WASM timezone backend is off.
#define systemTimeZone() UTC
#endif

View File

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

View File

@@ -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": [

View File

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

View File

@@ -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 tagApplication/Document 生命周期、持久化流、Python 包装、类型系统和日志只在探针中使用最小宿主适配。
探针现在从真实 `ElementMap::save``StringHasher::getIDMap` 生成原生资源。ElementMap 保存体按 FreeCAD `ComplexGeoData` 外层协议补入 `BeginElementMap v1` 后,由 Web `parseElementMap2` 读得 `MapCount=1`、4 个名称和 1 个 tokenStringHasher 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-closedJS/WASM/DATA 的哈希证据写入本机候选报告,且明确 `productionPublication=false`
SDK-05 又建立了独立 pthread OCCT 构建和 `.cache/candidates/freecad-naming-worker` 候选模块。Node 与 Chrome 均在同一个 wasm 中先执行 OCCT 8.0.0 cut history再调用 FreeCAD 私有命名 ABIChrome 在 COOP/COEP 隔离环境中验证三回调、链式 StringHasher 闭包和无 history 拒绝。执行命令与只读报告检查已拆分,检查器会重新计算当前 JS/WASM/DATA 哈希,静态旧报告不能替代真实 harness。本批 WBS 为 SDK-01/02A/02B/03/04/05 completedPAR-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° 的 AttachmentOffsetDatumLine/Point 分别使用 ObjectX/ObjectOrigin四个 Body 的 Group 顺序均被采集。
源 AdditiveBox 的 Length 编辑使体积 `480→672`,跨 Body ShapeBinder 同步 `480→672`;选定 Edge1..4 的 SubShapeBinder 长度保持 28。DatumPlane offset 编辑为 Z=7/45° 后,其实际 Placement 同步更新。另建 `Face999` 支撑的 DatumPlaneFreeCAD 明确返回 `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 回放。

View File

@@ -393,10 +393,14 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片Facade 验
ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAGobject 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、以及无唯一同构来源。

View File

@@ -0,0 +1,783 @@
#include <emscripten/bind.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
#include <App/Application.h>
#include <App/ElementMap.h>
#include <App/ElementNamingUtils.h>
#include <App/IndexedName.h>
#include <App/MappedName.h>
#include <App/StringHasher.h>
#include <Base/Exception.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <pwd.h>
#include <limits.h>
#include <sys/types.h>
#include <unistd.h>
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<ssize_t>(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<char*>(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<char*>("");
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<double>(0x7fffffff)) {
throw std::runtime_error(std::string("FreeCAD naming request requires positive tag ") + key);
}
return static_cast<long>(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<double>(0x7ffffffe)) {
throw std::runtime_error(std::string("FreeCAD naming request requires non-negative index ")
+ key);
}
return static_cast<int>(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<std::string> 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<qint64>(mapId)},
{"typeCount", typeCount},
{"sections", sections},
});
}
return QJsonObject {
{"schemaVersion", 2},
{"nativeVersion", 1},
{"rootId", static_cast<qint64>(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<qint64>(related.value()));
}
entries.append(QJsonObject {
{"id", static_cast<qint64>(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<double>(0x7fffffff)) {
throw std::runtime_error(std::string("Prior StringHasher requires positive ") + label);
}
return static_cast<long>(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<long> 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<int>(relatedIds.size())) {
throw std::runtime_error("Prior StringHasher internal dependencies are truncated");
}
Data::ElementIDRefs externalRefs;
for (std::size_t index = static_cast<std::size_t>(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<long> 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<qint64>(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<qint64>(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<Data::ElementMap>();
elementMap->hasher = hasher;
QJsonArray mappedNames;
std::set<std::string> resultKeys;
std::map<std::string, int> 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);
}

View File

@@ -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',
})
})

View File

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

View File

@@ -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<string, unknown>
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))

View File

@@ -1,10 +1,21 @@
#include <emscripten/bind.h>
#include <App/IndexedName.h>
#include <App/ElementMap.h>
#include <App/ElementNamingUtils.h>
#include <App/MappedName.h>
#include <App/MappedElement.h>
#include <App/StringHasher.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <algorithm>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
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<Data::ElementMap>();
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<Data::MappedName> 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<Data::ElementMap>();
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<Data::MappedName> 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<Data::ElementMap>();
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<qint64>(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<qint64>(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<qint64>(related.value()));
}
entries.append(QJsonObject {
{"id", static_cast<qint64>(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);
}

View File

@@ -0,0 +1,41 @@
#ifndef SRC_APP_APPLICATION_H_
#define SRC_APP_APPLICATION_H_
#include <set>
#include <string>
namespace App {
class Document;
class ProbeSignal
{
public:
template<typename Callback>
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

View File

@@ -0,0 +1,19 @@
#ifndef SRC_APP_DOCUMENT_H_
#define SRC_APP_DOCUMENT_H_
#include <App/DocumentObject.h>
namespace App {
class Document
{
public:
DocumentObject* getObjectByID(long) const
{
return nullptr;
}
};
} // namespace App
#endif

View File

@@ -0,0 +1,31 @@
#ifndef SRC_APP_DOCUMENTOBJECT_H_
#define SRC_APP_DOCUMENTOBJECT_H_
#include <string>
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

View File

@@ -1,27 +0,0 @@
#pragma once
#define APP_STRING_ID_H
#include <QByteArray>
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

View File

@@ -1,3 +1,22 @@
#pragma once
#include <Base/Exception.h>
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)

View File

@@ -0,0 +1,35 @@
#pragma once
#include <sstream>
#include <stdexcept>
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)

View File

@@ -0,0 +1,38 @@
#pragma once
#include <CXX/Objects.hxx>
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(...)

View File

@@ -0,0 +1,3 @@
#pragma once
#include <CXX/Objects.hxx>

View File

@@ -0,0 +1,45 @@
#pragma once
#include <istream>
#include <sstream>
namespace Base {
class Reader : public std::istream
{
public:
Reader()
: std::istream(nullptr)
{}
};
class XMLReader
{
public:
template<typename T>
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

View File

@@ -0,0 +1,43 @@
#pragma once
#include <istream>
#include <ostream>
#include <string>
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

View File

@@ -0,0 +1,40 @@
#pragma once
#include <ostream>
#include <sstream>
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

View File

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

View File

@@ -0,0 +1,11 @@
#pragma once
#include <map>
#ifndef AppExport
#define AppExport
#endif
#ifndef BaseExport
#define BaseExport
#endif

View File

@@ -0,0 +1,5 @@
#pragma once
#include <App/DocumentObject.h>
#include <App/Document.h>
#include <App/Application.h>

View File

@@ -0,0 +1,15 @@
#pragma once
#include <CXX/Objects.hxx>
namespace App {
class StringHasher;
class StringHasherPy : public _object
{
public:
explicit StringHasherPy(StringHasher*) {}
};
} // namespace App

View File

@@ -0,0 +1,17 @@
#pragma once
#include <CXX/Objects.hxx>
namespace App {
class StringID;
class StringIDPy : public _object
{
public:
explicit StringIDPy(StringID*) {}
int _index = 0;
};
} // namespace App

View File

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

View File

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

View File

@@ -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"
if [[ "${PUBLISH_ARTIFACT}" == "1" ]]; then
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"
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"

View File

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

View File

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

View File

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

View File

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

49
scripts/build-boost-wasm.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

57
scripts/build-icu-wasm.sh Normal file
View File

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

View File

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

67
scripts/build-xerces-c-wasm.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 }])),

View File

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

View File

@@ -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=(",", ":")))

View File

@@ -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') !== '!<arch>\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,
}
}

View File

@@ -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=(",", ":")))

View File

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

View File

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

View File

@@ -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:]]*<BRepLProp_CurveTool\.hxx>' \
"${OVERLAY_DIR}/src" || true
)
if (( ${#curve_tool_include_files[@]} > 0 )); then
sed -i -E '\|^[[:space:]]*#[[:space:]]*include[[:space:]]*<BRepLProp_CurveTool\.hxx>|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 <Geom2dLProp_CLProps2d.hxx>|#include <GeomLProp_CLProps.hxx>|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<TopoShape> 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<TopoShape>{e1, e2, e3, e4});|' \
-e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::FirstVertex(e1)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{TopExp::FirstVertex(e1)});|' \
-e 's|mapper.populate(MappingStatus::Generated, v, {TopExp::LastVertex(e4)});|mapper.populate(MappingStatus::Generated, v, std::vector<TopoShape>{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}"

View File

@@ -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 = `<!doctype html><meta charset="utf-8"><title>FreeCAD naming candidate Worker</title><script type="module">
const send = async (report) => fetch('/__report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
try {
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Chrome candidate harness requires cross-origin isolation and SharedArrayBuffer.')
const createCandidate = (await import('/candidate/bitbybit-occt-history.js')).default
const candidate = await createCandidate({ locateFile: (path) => new URL('/candidate/' + path, location.href).href })
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
if (!callbacks.every((name) => typeof candidate[name] === 'function')) throw new Error('Candidate module omits a FreeCAD naming callback.')
const descriptor = JSON.parse(candidate.freecadNamingCapabilitiesJson())
if (candidate.freecadNamingAbiVersion() !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Candidate naming ABI descriptor is not locked.')
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('Chrome OCCT cut returned no usable history record.')
const resultIndex = Number.isSafeInteger(record.resultIndex) ? record.resultIndex : record.resultIndexes.find(Number.isSafeInteger)
const firstRequest = { schemaVersion: 1, requestId: 'chrome-candidate-1', documentId: 'chrome-candidate-document', documentVersion: 1, operationId: 'chrome-cut-1', operation: 'cut', stageId: 'chrome:stage:1', resultObjectId: 'chrome:result:1', resultObjectTag: 99, inputs: [{ inputId: 'object', objectId: 'source-object', role: 'object', step: objectStep, objectTag: 42 }, { inputId: 'tool', objectId: 'source-tool', role: 'tool', step: toolStep, objectTag: 43 }], stages: [{ stageId: 'chrome:stage:1', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], resultStep: history.resultStep, history: { ...history, records: [{ ...record, resultIndex, resultIndexes: undefined }] } }
const first = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(firstRequest)))
if (first.status !== 'native-evidence' || first.mappedNames?.length !== 1 || first.elementMap2?.maps?.length !== 1 || !Array.isArray(first.stringHasher?.entries)) throw new Error('Chrome first-stage naming evidence is incomplete.')
const secondRequest = { ...firstRequest, requestId: 'chrome-candidate-2', documentVersion: 2, operationId: 'chrome-cut-2', stageId: 'chrome:stage:2', resultObjectId: 'chrome:result:2', resultObjectTag: 100, inputs: [{ inputId: 'object', objectId: 'chrome:result:1', role: 'object', stageId: 'chrome:stage:1', step: history.resultStep, objectTag: 99, namingEvidence: first }], stages: [{ stageId: 'chrome:stage:2', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex, resultIndex: resultIndex + 1 }] } }
const second = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(secondRequest)))
const reference = second.mappedNames?.[0]?.reference
const hasherIds = new Set(second.stringHasher?.entries?.map((entry) => entry.id))
const tokens = second.elementMap2?.maps?.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens))) ?? []
if (second.status !== 'native-evidence' || !reference?.name?.startsWith('#') || !Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !hasherIds.has(reference.prefixStringId) || !tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error('Chrome chained MappedNameRef/StringHasher/ElementMap2 closure is invalid.')
const thirdRequest = { ...secondRequest, requestId: 'chrome-candidate-3', documentVersion: 3, operationId: 'chrome-cut-3', stageId: 'chrome:stage:3', resultObjectId: 'chrome:result:3', resultObjectTag: 101, inputs: [{ inputId: 'object', objectId: 'chrome:result:2', role: 'object', stageId: 'chrome:stage:2', step: history.resultStep, objectTag: 100, namingEvidence: second }], stages: [{ stageId: 'chrome:stage:3', operation: 'cut', inputIds: ['object'], ordinal: 0 }], history: { ...history, records: [{ relation: 'modified', source: 'object', kind: record.resultKind || record.kind, sourceIndex: resultIndex + 1, resultIndex: resultIndex + 2 }] } }
const third = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify(thirdRequest)))
const thirdIds = new Set(third.stringHasher?.entries?.map((entry) => entry.id))
const thirdReference = third.mappedNames?.[0]?.reference
if (third.status !== 'native-evidence' || !thirdReference?.name?.startsWith('#') || !thirdReference.stringIds?.every((id) => thirdIds.has(id)) || third.stringHasher?.entries?.length <= second.stringHasher?.entries?.length) throw new Error('Chrome candidate did not restore non-empty StringHasher evidence across three stages.')
const invalid = JSON.parse(candidate.freecadNamingEvidenceJson(JSON.stringify({ ...firstRequest, history: { ...history, records: [] } })))
if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Chrome candidate did not fail closed for missing history.')
await send({ schemaVersion: 1, status: 'pass', browserId: 'chrome', crossOriginIsolated, occtVersion: history.occtVersion, callbacks, firstStage: { mappedNames: first.mappedNames.length, stringHasherEntries: first.stringHasher.entries.length }, chainedStage: { mappedNames: second.mappedNames.length, stringHasherEntries: second.stringHasher.entries.length }, thirdStage: { mappedNames: third.mappedNames.length, stringHasherEntries: third.stringHasher.entries.length }, invalidHistoryRejected: true, candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
} catch (error) {
await send({ schemaVersion: 1, status: 'failed', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error), candidateOnly: true, productionPublication: false, productionWorkerLinked: false })
}
</script>`
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('!<arch>\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 <TopTools_ListOfShape\.hxx>/)
assert.match(sdkCompat, /#include <TopTools_HSequenceOfShape\.hxx>/)
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 <GeomLProp_CLProps\.hxx>/)
assert.match(sdkOverlay, /legacy_geom2d_lprop/)
assert.match(sdkOverlay, /std::vector<TopoShape>\{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/)
})