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