feat: prepare FreeCAD private naming worker linkage
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-11 23:16:22 -04:00
parent aa607451ad
commit 97967041e2
30 changed files with 505 additions and 84 deletions

View File

@@ -5,13 +5,16 @@
"status": "pass",
"migration": {
"sourceSchemaVersion": 1,
"migratedSchemaVersion": 6,
"migratedSchemaVersion": 7,
"migratedDocumentVersion": 3,
"migratedLabel": "Legacy OPFS project",
"migratedObjectCount": 1,
"migratedNativeObjectTag": 1,
"recomputeDefaulted": true,
"reopenedSchemaVersion": 6,
"reopenedSchemaVersion": 7,
"reopenedVersion": 4,
"reopenedNativeObjectTag": 17,
"checkpointNativeObjectTag": 17,
"reopenedLabel": "Migrated OPFS project",
"checkpointVersion": 4,
"recoveryIntegrity": "ok",
@@ -28,9 +31,9 @@
]
},
"cleanup": {
"databasePath": "/bitbybit-rel02-301b6b76-9ab6-44ef-bf40-f2e6052e58ae.sqlite3",
"databasePath": "/bitbybit-rel02-976dea78-76e4-4a2b-afce-c0a2da583550.sqlite3",
"removedFiles": [
"bitbybit-rel02-301b6b76-9ab6-44ef-bf40-f2e6052e58ae.sqlite3"
"bitbybit-rel02-976dea78-76e4-4a2b-afce-c0a2da583550.sqlite3"
]
}
}

View File

@@ -100,6 +100,18 @@
"exactBlockers": ["shipped Worker lacks the FreeCAD-linked private naming ABI implementation", "browser builder naming evidence transport", "non-unique isomorphic source"]
}
},
"freecadNamingBuild": {
"status": "contract-only",
"sdkEnvironment": "FREECAD_WASM_SDK_DIR",
"requiredManifest": "manifest.json",
"requiredCallbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"],
"requiredFreecadVersion": "1.1.1",
"requiredSourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"requiredEmscriptenVersion": "3.1.69",
"buildCommand": "./npmw run build:freecad-naming-worker",
"checkCommand": "./npmw run check:freecad-naming-sdk",
"statusBoundary": "Missing SDK or callback probe keeps EX-TSN-02=in_progress and systemExact=false"
},
"browserWorker": {
"entry": "src/facade/nativeHistoryWorkerEntry.ts",
"client": "src/facade/nativeHistoryWorkerClient.ts",
@@ -117,7 +129,7 @@
"facadeCapabilities": {
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "object-placement-recompute", "placement-validation", "mirror-plane", "mirrored-whole-shape", "multi-transform-whole-shape", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "loft", "pipe", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "native-cross-feature-history-routing", "step-export", "stl-export"], "featureLevels": { "pad": { "level": "compatible", "exactBlockedBy": ["native MappedNameRef/StringHasher evidence when provider omits it"] }, "pocket": { "level": "compatible", "exactBlockedBy": ["native MappedNameRef/StringHasher evidence when provider omits it"] }, "revolution": { "level": "compatible", "exactBlockedBy": ["native MappedNameRef/StringHasher evidence when provider omits it"] }, "groove": { "level": "compatible", "exactBlockedBy": ["native MappedNameRef/StringHasher evidence when provider omits it", "OCCT/FreeCAD private token generation"] }, "boolean": { "level": "compatible", "exactBlockedBy": ["native MappedNameRef/StringHasher evidence when provider omits it", "isomorphic source ambiguity"] }, "fillet": { "level": "experimental", "exactBlockedBy": ["complete cross-feature native stage history"] }, "chamfer": { "level": "experimental", "exactBlockedBy": ["complete cross-feature native stage history"] }, "multi-transform": { "level": "experimental", "exactBlockedBy": ["feature-list native history", "private naming tokens"] } } },
"document": { "level": "experimental", "operations": ["typed-properties", "property-vector", "property-placement", "property-multi-transform", "property-link-list", "property-string-list", "property-link-sub", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "versioned-toporef", "toporef-migration", "generation-topology-snapshot", "linksub-external-geometry-migration", "ambiguous-deleted-topology-diagnostics", "adjacency-fingerprint-disambiguation", "toporef-candidate-replacement-transaction", "signature-topology-history-fallback", "native-topology-history-adapter", "native-cross-feature-history-routing", "runtime-mapped-name-ref-evidence", "string-hasher-evidence-validation", "element-map2-multi-stage-name-mapping", "isomorphic-candidate-persistence", "element-map-name-contract", "undo-redo"] },
"persistence": { "level": "experimental", "provider": "SQLite WASM + OPFS with transient fallback", "operations": ["schema-v6", "object-topology-snapshots", "transactional-migrations", "migration-rollback", "serialized-write-queue", "five-version-checkpoints", "checkpoint-load", "recovery-report", "content-addressed-resources", "quota-preflight", "resource-sweep-plan", "opfs-orphan-sweep", "cross-tab-lock-notice"] },
"persistence": { "level": "experimental", "provider": "SQLite WASM + OPFS with transient fallback", "operations": ["schema-v7", "stable-native-object-tags", "object-topology-snapshots", "transactional-migrations", "migration-rollback", "serialized-write-queue", "five-version-checkpoints", "checkpoint-load", "recovery-report", "content-addressed-resources", "quota-preflight", "resource-sweep-plan", "opfs-orphan-sweep", "cross-tab-lock-notice"] },
"sketcher": { "level": "experimental", "provider": "typescript-basic; FreeCAD 1.1.1 planegcs WASM core installed", "operations": ["point-line-circle-arc-model", "ellipse-bspline-periodic-model", "construction-geometry", "geometry-validation", "stable-geometry-signature", "stable-external-geometry-model", "basic-constraints", "advanced-constraint-proxy", "diameter-symmetric-tangent", "basic-solver", "versioned-solver-protocol", "solver-capability-probe", "cancel-stale-isolation", "replay-contract", "planegcs-native-system-solve", "planegcs-horizontal-distance", "planegcs-vertical-distance", "planegcs-distance-x", "planegcs-distance-y", "planegcs-angle", "planegcs-circle-radius", "planegcs-circle-diameter", "planegcs-equal-lines", "planegcs-equal-circles", "planegcs-tangent-circles", "planegcs-point-symmetry", "planegcs-point-on-object", "planegcs-point-on-arc", "planegcs-point-on-ellipse", "planegcs-point-on-cubic-bspline", "planegcs-block-freeze", "planegcs-parallel-lines", "planegcs-perpendicular-lines", "planegcs-coincident-lines", "planegcs-snells-law-lines", "planegcs-ellipse-internal-alignment", "planegcs-cubic-bspline-weight", "planegcs-dedicated-worker-smoke", "persistence"] },
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "document-property-projection", "live-topology-element-map", "proxy-report", "script-isolation"] },
"viewport": { "level": "experimental", "provider": "Three.js WebGL2", "operations": ["mesh-rendering", "multi-object-mesh-registry", "view-orientation", "fit-and-zoom", "subshape-triangle-mapping", "subshape-click-selection", "subshape-hover-preselection", "subshape-hover-highlight", "occt-analytic-picking-topology", "brep-edge-polyline-picking", "edge-pixel-tolerance-picking", "vertex-pixel-tolerance-picking", "diagnostic-selection-flash", "multi-object-selection-set", "modifier-box-selection-gesture", "window-contained-box-selection", "crossing-intersection-box-selection", "multi-object-selection-highlight", "object-visibility-projection", "selection-resource-release"] },

View File

@@ -32,7 +32,7 @@
{ "id": "EX-KER-01", "title": "Match OCCT construction, validation, tolerances and failure diagnostics", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-02"], "deliverables": ["Per-operation native differential", "Tolerance and placement contract", "Failure-code map"], "acceptance": ["Shape validity, topology, mass properties and diagnostics match locked FreeCAD fixtures", "Worker ownership returns to zero after every case"], "evidence": ["check:browser-occt", "check:chrome-geometry-features"], "exactBlockedBy": ["Not all FreeCAD builder flags and failure branches are represented"] },
{ "id": "EX-TSN-00", "title": "Fail closed when native naming or builder evidence is absent", "priority": "P0", "status": "completed", "dependencies": [], "deliverables": ["Runtime naming evidence contract", "final-shape-only and ambiguous persistence", "ElementMap2 writer evidence guard"], "acceptance": ["Final geometry cannot mint a private FreeCAD token", "Unproven stage relations and isomorphic sources cannot become stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
{ "id": "EX-TSN-01", "title": "Capture native stage Shape and Generated/Modified/Deleted for every builder", "priority": "P0", "status": "completed", "dependencies": ["EX-KER-01", "EX-TSN-00"], "deliverables": ["All builder-stage captures", "Multi-input and one-to-many lineage", "Local feature recompute history"], "acceptance": ["Every feature result relation has a producing native stage", "No final-result index is applied to an intermediate Shape"], "evidence": ["check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-evidence"], "exactBlockedBy": [] },
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi"], "exactBlockedBy": ["The shipped Worker does not link the FreeCAD private naming implementation for every builder"] },
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "check:freecad-naming-sdk", "check:occt-history-artifact"], "exactBlockedBy": ["The shipped Worker does not link the FreeCAD private naming implementation for every builder"] },
{ "id": "EX-TSN-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["Symmetric cases without unique native provenance remain intentionally ambiguous"] },
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "pending", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["Exhaustive cross-feature naming corpus is incomplete"] }
]

View File

@@ -1,82 +1,98 @@
{
"schemaVersion": 1,
"status": "pass",
"currentSchemaVersion": 6,
"currentSchemaVersion": 7,
"migrationVersions": [
1,
2,
3,
4,
5,
6
6,
7
],
"migrationSha256": "0308954a28a2cd5d98888cd9c97c25170760e00f8cb16133a51414c5ebedb5a0",
"migrationSha256": "ab987867c4935f60198ec19fb69259f6fc95167005bbb4f40e11723a52392f1d",
"forward": [
{
"from": 0,
"to": 6,
"to": 7,
"applied": [
1,
2,
3,
4,
5,
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 1,
"to": 6,
"to": 7,
"applied": [
2,
3,
4,
5,
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 2,
"to": 6,
"to": 7,
"applied": [
3,
4,
5,
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 3,
"to": 6,
"to": 7,
"applied": [
4,
5,
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 4,
"to": 6,
"to": 7,
"applied": [
5,
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 5,
"to": 6,
"to": 7,
"applied": [
6
6,
7
],
"committed": true,
"rolledBack": false
},
{
"from": 6,
"to": 7,
"applied": [
7
],
"committed": true,
"rolledBack": false
@@ -138,6 +154,19 @@
],
"committed": false,
"rolledBack": true
},
{
"failedAt": 7,
"restoredVersions": [
1,
2,
3,
4,
5,
6
],
"committed": false,
"rolledBack": true
}
],
"idempotentRerun": {

View File

@@ -22,25 +22,25 @@
"bytes": 556597,
"sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105"
},
{
"path": "assets/index-B7ENiszF.js",
"bytes": 1134628,
"sha256": "53bd3b2876d56238158be315cfdf8d7143767c46c0a429271d9985c36f003ca4"
},
{
"path": "assets/index-BnRqhr3m.css",
"bytes": 65976,
"sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec"
},
{
"path": "assets/index-ChdGmY7w.js",
"bytes": 1130165,
"sha256": "c508b701c47d2dea412c96f3a529f99b42bd7b137e647d958b993c3268dfc3e6"
"path": "assets/nativeHistoryWorkerEntry-C7hZKjun.js",
"bytes": 21764,
"sha256": "a4fbc5e765e1d919c8f066b6b07e2064f62e3c291496972a2b4006069a17902b"
},
{
"path": "assets/nativeHistoryWorkerEntry-gNsJxM2G.js",
"bytes": 20437,
"sha256": "39c26691ba3275ce5fbb3a43239d5511430bc0222259f0c76e3aa9314f2e87cf"
},
{
"path": "assets/persistenceWorker-CyD5EUVX.js",
"bytes": 241190,
"sha256": "65a24ca0125444e3047f409d3b1c3667321e392a0ca9ddf64df6b728a1544c66"
"path": "assets/persistenceWorker-DQjay47f.js",
"bytes": 242249,
"sha256": "fa86b0667f07d1986e2d6dd50ee08ac291f83de07ab132db2ec13f306d49402b"
},
{
"path": "assets/planegcsWorkerEntry-BeAnOOBp.js",
@@ -219,8 +219,8 @@
},
{
"path": "chrome-opfs-migration-harness.html",
"bytes": 7032,
"sha256": "53203065bbc2937f63b7e0a9d17dd9a4e27396bbcef0766a87d5b8be45c8fb49"
"bytes": 7518,
"sha256": "553387802e86572a416bdd78a34d5e54855f6390f531f728c4749b5901e6c945"
},
{
"path": "chrome-partdesign-lifecycle-harness.html",
@@ -330,7 +330,7 @@
{
"path": "index.html",
"bytes": 678,
"sha256": "ce1c0c151dfb290cff01268efaec2efd0cdc354548ffccb5cb1030b02249870f"
"sha256": "019c7f7b5255a12f94a19ae72edc50a45343e81779bed1b68842a934b31da01a"
},
{
"path": "manifest.webmanifest",
@@ -380,7 +380,7 @@
{
"path": "sw.js",
"bytes": 1390,
"sha256": "a8d35592358f8580b3be765a36897969c63ebfb7c777a71296481e0d8118fad7"
"sha256": "5389aba344f93a8948bfed30aa20c0a1c38cdba7d9ebf120964d0897bba7e8cc"
},
{
"path": "vendor/camotics/camotics-sweep.wasm",
@@ -402,7 +402,7 @@
"status": "signed",
"algorithm": "Ed25519",
"keyId": "bitbybit-local-release-2026",
"payloadSha256": "be48e099205c1a86339eb4560e62ed88e12ffd0e2e45e036f3a723abd3bb078a",
"signature": "AAyAo8hzt1I48LaHUArN7lEg11zWrl3EkNKF6zr92x2SZHFhXq/5m8KZ0hqJxNU4INM6bC/laqanCJ9GhRbkCQ=="
"payloadSha256": "20fb0fbc84390befc6fa22358b25cc139fb9dde8829643b5fc4ab3e804c44181",
"signature": "QyhRB17NFPFu9Nda1rU2KW+Lv5yROFM9Lo0vY9hOexecfwzb3PWw+XWHymLc1/hYjBm3Hw7dPdQ8Ou2KapWmBg=="
}
}

View File

@@ -333,7 +333,7 @@ Bitbybit 必须被充分用作几何、Worker、IO、三角化和缓存的首选
属性领域合同现覆盖标量、枚举、颜色、Vector、Placement、ObjectLink、ObjectLinkList/StringList 和版本化 SubshapeLink。Placement/Vector 使用结构化值并做有限数、角度范围及非零旋转轴校验,复合值深拷贝进入 Undo/Redo 与持久化。Facade 的对象级几何重算已在 Shape 生成后执行 Placement并对恒等变换、畸形结构、变换失败释放和最近有效缓存保留给出测试证据这不是 FreeCAD Body/Tip、Support、AttachmentOffset 或特征局部坐标链的等价实现File/Shape 摘要和多选 mixed 编辑也仍未完成。
TSN 已完成面/边/顶点量化签名、邻接指纹消歧和匹配 API并挂接到实际重算 generation。成功 Shape 会产生对象拓扑快照、迁移 matches、`signature-fallback`/`occt-native` 历史和 schema v1 ElementMap提交阶段自动写回 LinkSub 与草图外部几何,稳定引用更新 generation歧义/删除引用保持显式状态、标脏所有者并产生修复诊断。候选替换 Facade 事务验证当前拓扑和 kind 后原子写入 stable TopoRef重建 DAG、进入 Undo/Redo属性面板和诊断页均通过 Facade 调用。schema v6 保存快照。Three.js 已支持多对象 Mesh 注册、OCCT analytic 面/边/顶点点击、预选、完整边折线高亮、window/crossing 框选和诊断聚焦闪烁;普通单向 Pad/Pocket/Revolution/Groove 已接入已有 OCCT `Generated/Modified/Deleted` provider其余复合模式仍显式回退。
TSN 已完成面/边/顶点量化签名、邻接指纹消歧和匹配 API并挂接到实际重算 generation。成功 Shape 会产生对象拓扑快照、迁移 matches、`signature-fallback`/`occt-native` 历史和 schema v1 ElementMap提交阶段自动写回 LinkSub 与草图外部几何,稳定引用更新 generation歧义/删除引用保持显式状态、标脏所有者并产生修复诊断。候选替换 Facade 事务验证当前拓扑和 kind 后原子写入 stable TopoRef重建 DAG、进入 Undo/Redo属性面板和诊断页均通过 Facade 调用。schema v7 保存拓扑快照和稳定原生对象标签。Three.js 已支持多对象 Mesh 注册、OCCT analytic 面/边/顶点点击、预选、完整边折线高亮、window/crossing 框选和诊断聚焦闪烁;普通单向 Pad/Pocket/Revolution/Groove 已接入已有 OCCT `Generated/Modified/Deleted` provider其余复合模式仍显式回退。
Sketcher 已有 Facade 事务模型和基础求解器适配器Sketch 几何/约束快照进入 `DocumentObjectSnapshot`,新增几何、约束和求解会更新 `ConstraintStatus`、历史、自动保存与 SQLite schema v4基础约束现覆盖 Diameter、Symmetric、Tangent。该适配器的职责是验证数据模型和提供可替换协议它不宣称已经等价于 FreeCAD `planegcs`。SK-03 的 planegcs WASM、SK-07 的完整约束分类、SK-09 的交互回放和 SK-10 的 B-spline 仍是兼容门禁。
@@ -359,7 +359,7 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片Facade 验
| FC-03 ZIP 安全读取 | 先读中央目录再解压;限制 archive/entry/total 字节、压缩比、条目数拒绝路径穿越、重复路径、加密项、ZIP64 和 XML entity | 缺 Document.xml、路径穿越、压缩炸弹测试通过 | 尚未恢复 BRep/Shape、视图、表达式和参数化对象 |
| FC-04/FC-07 只读报告 | `project.fcstd.inspect()` 解析 Document.xml 对象声明、Label、属性数量和类型未知类型为 proxyPython 类型为 blocked脚本资源只告警且不执行 | recognized/proxy/blocked 分类和 Facade-only 测试通过 | `metadata-compatible` 仅表示 XML 元数据可读,不表示 FCStd 可编辑或 round-trip |
| P2-04 Project list/load | `project.list()` 暴露 SQLite/内存快照摘要,`app.document.load()` 将选定快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态、保留 Undo 边界并按完整对象图异步重算Projects 页面按 documentId 调用 Facade API | 列表摘要、保存→修改→加载后属性恢复、选择清空、任务清空和加载重算调用通过测试 | FCStd 导入仍是只读 metadata/proxy跨文档 ShapeHandle 和 Shape 持久化仍待完成 |
| P2-01/P2-02 Migration/write pressure | schema v6 与统一事务 migration runnerv6 新增对象拓扑快照 JSON失败 rollback写队列严格串行并在失败后继续 | migration 排序/跳过/回滚测试、拓扑快照 fallback 往返及 1000 次队列压力通过 | SQLite v4→v5→v6 的真实 OPFS 浏览器升级、崩溃注入与发布回滚演练仍待完成 |
| P2-01/P2-02 Migration/write pressure | schema v7 与统一事务 migration runnerv6 新增对象拓扑快照 JSONv7 新增稳定原生对象标签;失败 rollback写队列严格串行并在失败后继续 | migration 排序/跳过/回滚测试、真实 Chrome OPFS v1→v7、标签重开/checkpoint 往返及 1000 次队列压力通过 | 跨发布降级、断电 journal 恢复和用户数据备份仍待完成 |
| P2-03 Resource/recovery fallback | SQLite Worker 在无 OPFS 时使用内存资源表OPFS 模式增加配额预留、重复内容免写、缺失文件重建和孤儿/零引用/缺失文件对账清扫Facade 暴露 sweep 报告 | quota/sweep 纯策略、fallback API、Worker 类型检查和生产构建通过 | 真实 OPFS 孤儿清扫、QuotaExceeded、大文件压力及浏览器崩溃注入仍待完成 |
| P2-04 Version checkpoints | 文档规范化保存与完整 JSON checkpoint 同事务;每文档保留最近 5 版Recovery 列出版本Facade 可加载指定/最新 checkpoint | 7 版保存后仅保留 3..7、指定版本/最新版加载、过期版本删除和深克隆测试通过 | 当前测试使用 transient fallbackSQLite/OPFS 断电点恢复和保留策略 UI 待完成 |
| PART-07 Shape check | `check-shape` 使用 Facade `geometry.topology()` 生成面/边/顶点数量和 `SHAPE_CHECK_PASSED` 信息诊断;空缓存、空拓扑和 Worker 错误分别返回结构化失败诊断 | 无 Shape、错误选择前置和拓扑调用边界通过测试/构建 | 当前是网格派生拓扑检查,不等价于 OCCT `BRepCheck_Analyzer`,完整修复建议和自交诊断仍待实现 |
@@ -368,7 +368,7 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片Facade 验
| P3-07 MultiTransform 实验切片 | `multi-transform` 使用结构化有序 Linear/Polar/Mirrored 步骤Task 与属性面板共用编辑器;重算逐步扩展 whole-shape 实例并最终 union限制最多 6 步和 100 个组合实例 | 6 实例组合、方向/平面映射、故障注入、中间 Shape 完整回收、最近有效缓存、深拷贝、Undo/Redo、fallback 保存恢复与任务提交测试通过 | 不是 FreeCAD 局部 Transform feature history未实现特征列表、Datum/TopoRef 轴面、单实体规则与原生拓扑历史,保持 `experimental` |
| P3-05 Hole 基础切片 | `hole` 创建 `PartDesign::Hole`,用 32 边圆形 Profile 或圆锥刀具调用现有 OCCT Pocket/Cut支持 Diameter、DepthType/Depth、Position/Direction、Reversed、Flat/Angled DrillPoint、DrillForDepth、Tapered/TaperedAngle 以及 Dimension/ThroughAll校验正尺寸并纳入 Base 链接和 Body.Tip | 圆孔 Profile、有限深度调用、位置/方向、90 度基准锥度、包含钻尖总深度、非法直径/方向诊断、Task 属性提交和 Tip 更新测试通过Counterbore/Countersink/Counterdrill、Angled drill point 和 Tapered 由 Bitbybit 圆柱/圆锥刀具加 Boolean Cut 组合执行,并有 Chrome 非空 mesh 与 0/0 release 证据 | 仍未实现 Threaded/ModelThread、标准尺寸、Support/Profile 面定位和完整局部特征历史;螺纹保持显式 `HOLE_THREADING_UNSUPPORTED`,能力保持 `experimental` |
| TSN-02/05/06/08 TopoRef 基础迁移 | `topologyReferences` 提供 schema v1 创建、序列化、安全解析、解析和跨版本迁移;重复几何通过邻接指纹唯一消歧,一旧多新不会错误继承;匹配按 kind 隔离并用形状尺度归一化几何评分支持纯平移;对象快照附带 schema v1 ElementMap新增原生 ElementMap2 v1 token AST/schema v2 writer以及 StringHasher v1 AST/schema v2 codec | round-trip、瞬时索引拒绝、resolved/ambiguous/deleted、重复拆分、邻接唯一/对称回归、FaceN/EdgeN/VertexN 写出和持久化通过FreeCAD 1.1.1 的 30 模型、91 个 ElementMap2、11 个 StringHasher 表完成解析、写回和引用闭合 | Web 拓扑仍不能凭最终几何生成 FreeCAD 私有算法未提供的历史 token/StringID对称 Boolean 的同构子形需要原生候选证据才能消歧 |
| TSN-08/09 LinkSub 文档接线 | `PropertyValue` 支持 schema v1 TopoRef重算 generation 自动迁移 LinkSub/草图外部几何,歧义/删除不强绑schema v6 保存对象拓扑快照ElementMapFacade 候选替换事务验证当前同 kind persistentId重建 DAG 并支持 Undo/Redo属性/诊断 UI 可替换Three.js 多 Mesh 场景已将对象/OCCT analytic 面边点拾取、预选、完整曲线折线高亮、诊断闪烁和 window/crossing 框选回调 FacadePocket 暴露 `UpToFace`Pocket/Groove/Hole/MultiTransform 及双侧线性/角度特征可携带内部 builder 的 STEP/topology/history stageElementMap2 writer 可从 MappedNameRef/StringID 证据生成 FreeCAD 1.1.1 tokenFCStd reader/writer 会校验 StringHasher 引用闭包 | 非法瞬时索引/候选拒绝、DAG 边、稳定/歧义/删除迁移、邻接消歧、候选替换、Undo/Redo、保存加载、无三角对角线的 BRep 边拾取、多选集合、真实双对象穿越框、诊断聚焦事件、live ElementMap、Pocket 双侧、Revolution/Groove 双角 Chrome 回放及 30 模型 naming-evidence gate 通过 | 未提供逐阶段 Shape/history 或 MappedNameRef/StringHasher 证据的算法继续保守回退;同构子形无唯一原生证据时不宣称 exact |
| TSN-08/09 LinkSub 文档接线 | `PropertyValue` 支持 schema v1 TopoRef重算 generation 自动迁移 LinkSub/草图外部几何,歧义/删除不强绑schema v7 保存对象拓扑快照ElementMap 与稳定原生对象标签Facade 候选替换事务验证当前同 kind persistentId重建 DAG 并支持 Undo/Redo属性/诊断 UI 可替换Three.js 多 Mesh 场景已将对象/OCCT analytic 面边点拾取、预选、完整曲线折线高亮、诊断闪烁和 window/crossing 框选回调 FacadePocket 暴露 `UpToFace`Pocket/Groove/Hole/MultiTransform 及双侧线性/角度特征可携带内部 builder 的 STEP/topology/history stageElementMap2 writer 可从 MappedNameRef/StringID 证据生成 FreeCAD 1.1.1 tokenFCStd reader/writer 会校验 StringHasher 引用闭包 | 非法瞬时索引/候选拒绝、DAG 边、稳定/歧义/删除迁移、邻接消歧、候选替换、Undo/Redo、保存加载、无三角对角线的 BRep 边拾取、多选集合、真实双对象穿越框、诊断聚焦事件、live ElementMap、Pocket 双侧、Revolution/Groove 双角 Chrome 回放及 30 模型 naming-evidence gate 通过 | 未提供逐阶段 Shape/history 或 MappedNameRef/StringHasher 证据的算法继续保守回退;同构子形无唯一原生证据时不宣称 exact |
| TSN-03/07 历史适配器回退 | Bitbybit 1.1.1 绑定仅有 `HasGenerated()`,没有逐 Shape 的 Generated/Modified/IsDeleted独立 native provider 已接入普通及双侧 Pad/Pocket/Revolution/Groove 参数路由与既有 Boolean/Pattern/Dress-up 子集;协议新增显式 input/stage、sourceStage/resultStage、一对多 resultIndexes、有序 feature sides 和 rotate stage可向后兼容旧 object/tool 载荷stage capture 按各自产生结果解释 index 并以邻接唯一性约束同构候选 | 平移修改、新增/删除/重复面、邻接唯一消歧、跨对象竞争、generation 写回、采集失败隔离、Up-to-face 有效距离、Two sides/Midplane/Two angles 路由通过Groove 双角按 FreeCAD profile offset + 单 sweep + Cut 得到 native valid 单 solid30 模型 history/ElementMap2/StringHasher exact gate 与 Facade `182/182` 通过 | provider 仍通过 STEP/BREP 跨 WASM同构候选和未提供私有 builder/命名证据的算法仍需逐项黄金模型后才能声明 exact |
原生历史 provider 适配器现已进入重算执行器:当 Geometry Worker 暴露 `topologyHistory()` 且 Base/Tool/Profile 有拓扑快照时Facade 将真实输入/输出 Shape 和记录交给 `captureNativeTopologyHistoryStages()`,再由 `composeNativeTopologyHistoryLineage()` 把上游对象 stage 接入当前关系;普通 Pad/Pocket/Revolution/Groove、Up-to-face 有效长度以及双侧/Midplane/Two angles 会进入同一参数协议。Pocket、Hole、MultiTransform 和双侧 builder 保存中间 STEP/拓扑/记录Groove TwoAngles 按 FreeCAD 语义保存 `rotate -> revolution -> cut` 的 BREP/history并已通过 native BRepCheck Chrome 黄金门。provider 不声明组成 operation 时保留单响应兼容路径。exact 判定仍必须同时拥有逐阶段 Shape/history 与 MappedNameRef/StringHasher 命名证据,任一缺失都不能由最终几何补推。
@@ -391,6 +391,8 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片Facade 验
`FreeCADPrivateNamingABI v1` 已定义为版本化 Embind/JSON 合同,并接入 direct provider 与真实 Worker请求携带 operation、输入 STEP、最终 STEP/BRep、阶段 DAG 和原生 history响应必须同时提供 `MappedNameRef`、StringHasher 与 ElementMap2且逐项通过 stage/result 绑定、大小上限、schema、StringID 和引用闭包校验。生产运行时声明 `freecad-private-v1-optional-worker`mock 明确为 `not-exposed`;未导出三项回调、版本/锁定提交不符、证据不闭合或 operation 未声明时均 fail-closed 到 `final-shape-only`,禁止合成 token。当前仓库随附的 `native/occt-history` 仍是 OCCT-only artifact没有链接 FreeCAD 私有实现,因此这里只完成 ABI 与传输边界,不把浏览器实时私有命名提升为 exact。
ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAGobject tag 会从 `Document.xml``Object.id` 导入,按稳定值写回 FCStd并通过 SQLite schema v7、OPFS 重开和 checkpoint 往返。`build:freecad-naming-worker` 是独立的 fail-closed 构建入口只接受锁定提交、Emscripten 3.1.69、wasm Qt/Python、FreeCADBase/FreeCADApp/Part 静态库及三回调 bridge 的哈希清单;普通 `build:occt-history` 继续是 OCCT-only。`check:occt-history-artifact` 会实际实例化 WASM 并探测三回调,当前结果为 unavailable因此 `EX-TSN-02=in_progress``systemExact=false` 保持不变。
Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同Sketcher 覆盖 FreeCAD 1.1.1 FCStd 表面的 9 类 geometry、19 类 constraint 和 10 类 InternalAlignment 语义PartDesign 覆盖 Facade 已支持的 21 个 family、202 个属性槽和 72 个语义分区,并在对象创建及属性编辑时执行跨字段校验。`check:freecad-sketcher-partdesign-abi` 会从实现重算这些计数并拒绝陈旧报告;该“完整”限定于 supported Web Facade不包含尚未实现的 FreeCAD PartDesign TypeId也不表示 basic TypeScript solver 已求解全部圆锥曲线约束。
逐 feature 等级目前由 `config/compatibility-matrix.json``facadeCapabilities.geometry.featureLevels` 管理Pad/Pocket/Revolution/Groove/Boolean 为有 native history 子集的 `compatible`Fillet/Chamfer/MultiTransform 保持 `experimental``systemExactEvaluation.exact` 必须在所有 feature 的阶段 Shape/history、私有 token 证据和同构来源都闭合后才允许改为 true当前值固定为 false阻断项为私有 FreeCAD token 算法、没有原生 builder stage、以及无唯一同构来源。

View File

@@ -13,6 +13,18 @@ find_package(OpenCASCADE REQUIRED CONFIG PATHS "${OCCT_DIR}" NO_DEFAULT_PATH)
add_executable(bitbybit_occt_history occt_history.cpp)
target_include_directories(bitbybit_occt_history PRIVATE "${OCCT_DIR}/include/opencascade")
target_link_libraries(bitbybit_occt_history PRIVATE TKDESTEP TKXSBase TKDE TKBO TKBRep TKTopAlgo TKPrim TKShHealing TKGeomAlgo TKGeomBase TKG3d TKG2d TKMath TKernel)
if(FREECAD_WASM_SDK_DIR)
if(NOT FREECAD_NAMING_BRIDGE_SOURCE OR NOT EXISTS "${FREECAD_NAMING_BRIDGE_SOURCE}")
message(FATAL_ERROR "FREECAD_NAMING_BRIDGE_SOURCE must point to the verified FreeCAD private naming bridge source.")
endif()
if(NOT FREECAD_NAMING_INCLUDE_DIRS OR NOT FREECAD_NAMING_LIBRARIES)
message(FATAL_ERROR "FREECAD_NAMING_INCLUDE_DIRS and FREECAD_NAMING_LIBRARIES are required for a FreeCAD-linked Worker.")
endif()
target_sources(bitbybit_occt_history PRIVATE "${FREECAD_NAMING_BRIDGE_SOURCE}")
target_include_directories(bitbybit_occt_history PRIVATE ${FREECAD_NAMING_INCLUDE_DIRS})
target_link_libraries(bitbybit_occt_history PRIVATE ${FREECAD_NAMING_LIBRARIES})
target_compile_definitions(bitbybit_occt_history PRIVATE BITBYBIT_FREECAD_NAMING_LINKED=1)
endif()
target_compile_options(bitbybit_occt_history PRIVATE -fexceptions -O2)
target_link_options(bitbybit_occt_history PRIVATE
--bind

View File

@@ -19,6 +19,35 @@ The build also publishes the JS/WASM pair to
clean checkout must run the build after restoring the pinned OCCT source and
Emscripten toolchain.
## FreeCAD private naming build boundary
The default build is intentionally OCCT-only. It does not export
`freecadNamingAbiVersion`, `freecadNamingCapabilitiesJson`, or
`freecadNamingEvidenceJson`, and it must not be described as exact FreeCAD
naming support.
A separate fail-closed build lane is available for an actual FreeCAD WASM SDK:
```bash
FREECAD_WASM_SDK_DIR=/absolute/path/to/sdk ./npmw run check:freecad-naming-sdk
FREECAD_WASM_SDK_DIR=/absolute/path/to/sdk ./npmw run build:freecad-naming-worker
./npmw run check:occt-history-artifact
```
The SDK layout is defined by `freecad-wasm-sdk-manifest.example.json`. The
checker requires the locked FreeCAD 1.1.1 commit, Emscripten 3.1.69, wasm Qt
and Python targets, private App headers, hashed static FreeCAD/Part/Qt/Python
libraries, and a hashed bridge source exporting all three callbacks. If any
input is absent, `build:freecad-naming-worker` exits before rebuilding or
publishing the OCCT-only artifact. A successful artifact probe still only
establishes ABI linkage; exact promotion additionally requires valid
MappedNameRef, StringHasher, ElementMap2, stage history, and round-trip evidence.
The versioned JSON request carries document `objectId`, stable positive
`objectTag`, prior naming evidence, result object identity, result tag, stage
DAG, STEP/BRep, and OCCT Generated/Modified/Deleted history. No Web fallback is
allowed to synthesize a FreeCAD private token.
The exported `booleanHistory(object, tool, operation)` function supports
`fuse`, `cut`, and `common`. It returns the native result shape, per-input
`modified`/`generated`/`deleted` records, a validity/topology/quality summary,

View File

@@ -7,6 +7,15 @@ OCCT_BUILD_DIR="${OCCT_BUILD_DIR:-${ROOT_DIR}/.cache/bitbybit/occt-history-build
DIST_DIR="${OCCT_HISTORY_DIST_DIR:-${ROOT_DIR}/native/occt-history/dist}"
PUBLIC_DIR="${OCCT_HISTORY_PUBLIC_DIR:-${ROOT_DIR}/public/native/occt-history}"
JOBS="${JOBS:-$(nproc)}"
FREECAD_WASM_SDK_DIR="${FREECAD_WASM_SDK_DIR:-}"
if [[ "${FREECAD_WASM_NAMING_REQUIRED:-0}" == "1" && -z "${FREECAD_WASM_SDK_DIR}" ]]; then
echo "FREECAD_WASM_SDK_DIR is required for build:freecad-naming-worker; refusing to produce an OCCT-only artifact." >&2
exit 1
fi
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
FREECAD_WASM_SDK_DIR="${FREECAD_WASM_SDK_DIR}" node "${ROOT_DIR}/scripts/check-freecad-naming-sdk.mjs" >/dev/null
fi
if [[ ! -f "${OCCT_SOURCE_DIR}/CMakeLists.txt" ]]; then
echo "OCCT source checkout not found: ${OCCT_SOURCE_DIR}" >&2
@@ -47,7 +56,19 @@ cmake --build "${OCCT_BUILD_DIR}" --target \
--parallel "${JOBS}"
mkdir -p "${DIST_DIR}"
em++ "${ROOT_DIR}/native/occt-history/occt_history.cpp" \
SOURCES=("${ROOT_DIR}/native/occt-history/occt_history.cpp")
INCLUDES=("-I${OCCT_BUILD_DIR}/include/opencascade" "-I${OCCT_SOURCE_DIR}/src")
DEFINES=()
FREECAD_LIBRARIES=()
if [[ -n "${FREECAD_WASM_SDK_DIR}" ]]; then
BRIDGE_SOURCE="$(node -e 'const fs=require("node:fs"); const p=require("node:path"); const m=JSON.parse(fs.readFileSync(p.resolve(process.argv[1],"manifest.json"),"utf8")); console.log(p.isAbsolute(m.namingBridge.source) ? m.namingBridge.source : p.resolve(process.argv[1],m.namingBridge.source))' "${FREECAD_WASM_SDK_DIR}")"
SOURCES+=("${BRIDGE_SOURCE}")
while IFS= read -r include_dir; do INCLUDES+=("-I${include_dir}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const d of m.includeDirs) console.log(p.isAbsolute(d) ? d : p.resolve(root,d))' "${FREECAD_WASM_SDK_DIR}")
while IFS= read -r library; do FREECAD_LIBRARIES+=("${library}"); done < <(node -e 'const fs=require("node:fs"); const p=require("node:path"); const root=p.resolve(process.argv[1]); const m=JSON.parse(fs.readFileSync(p.resolve(root,"manifest.json"),"utf8")); for (const l of m.libraries) console.log(p.isAbsolute(l.path) ? l.path : p.resolve(root,l.path))' "${FREECAD_WASM_SDK_DIR}")
DEFINES+=("-DBITBYBIT_FREECAD_NAMING_LINKED=1")
fi
em++ "${SOURCES[@]}" "${INCLUDES[@]}" "${DEFINES[@]}" \
-I"${OCCT_BUILD_DIR}/include/opencascade" \
-I"${OCCT_SOURCE_DIR}/src" \
-std=c++17 -O2 -fexceptions --bind \
@@ -69,6 +90,7 @@ em++ "${ROOT_DIR}/native/occt-history/occt_history.cpp" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKG2d.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKMath.a" \
"${OCCT_BUILD_DIR}/lin32/clang/lib/libTKernel.a" \
"${FREECAD_LIBRARIES[@]}" \
-Wl,--end-group \
-sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web,worker,node \
-sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=134217728 -sMAXIMUM_MEMORY=2147483648 \

View File

@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"freecadVersion": "1.1.1",
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"emscriptenVersion": "3.1.69",
"qtTarget": "wasm32-emscripten",
"pythonTarget": "wasm32-emscripten",
"includeDirs": [
"include",
"include/FreeCAD"
],
"libraries": [
{ "name": "FreeCADBase", "path": "lib/libFreeCADBase.a", "sha256": "replace-with-64-lowercase-hex-digits" },
{ "name": "FreeCADApp", "path": "lib/libFreeCADApp.a", "sha256": "replace-with-64-lowercase-hex-digits" },
{ "name": "Part", "path": "lib/libPart.a", "sha256": "replace-with-64-lowercase-hex-digits" },
{ "name": "QtCore", "path": "lib/libQt6Core.a", "sha256": "replace-with-64-lowercase-hex-digits" },
{ "name": "Python", "path": "lib/libpython3.a", "sha256": "replace-with-64-lowercase-hex-digits" }
],
"namingBridge": {
"source": "src/freecad_naming_bridge.cpp",
"sha256": "replace-with-64-lowercase-hex-digits",
"exports": [
"freecadNamingAbiVersion",
"freecadNamingCapabilitiesJson",
"freecadNamingEvidenceJson"
]
}
}

View File

@@ -48,6 +48,7 @@
"test:golden:freecad:failures": "node scripts/run-freecad-golden-failures.mjs",
"test:golden:freecad:family-failures": "node scripts/run-freecad-golden-failures.mjs --manifest=fixtures/freecad-golden/feature-families/failures/manifest.json",
"check:occt-history-artifact": "node scripts/check-occt-history-browser-artifact.mjs",
"check:freecad-naming-sdk": "node scripts/check-freecad-naming-sdk.mjs",
"test:browser-occt": "node scripts/run-browser-occt-harness.mjs",
"test:freecad:threeway": "node scripts/run-freecad-threeway-boolean.mjs",
"check:browser-occt": "node scripts/check-occt-browser-verification.mjs",
@@ -80,6 +81,7 @@
"check:freecad-cam-path": "node scripts/check-freecad-cam-path-oracle.mjs",
"test:freecad-cam-path-report": "node --test tests/freecadCamPathOracle.test.mjs",
"build:occt-history": "bash native/occt-history/build.sh",
"build:freecad-naming-worker": "FREECAD_WASM_NAMING_REQUIRED=1 bash native/occt-history/build.sh",
"test:occt-history": "node native/occt-history/smoke-test.mjs",
"build:planegcs": "bash native/planegcs/build.sh",
"test:planegcs": "node native/planegcs/smoke-test.mjs",
@@ -261,7 +263,7 @@
"test:browser-matrix": "node scripts/run-browser-matrix.mjs",
"check:browser-matrix": "node scripts/check-browser-matrix.mjs",
"check:runtime": "node scripts/check-runtime.mjs",
"verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature",
"verify": "npm run check:runtime && npm run check:baseline && npm run check:bitbybit-history && npm run check:execution-plan && npm run check:freecad-sketcher-constraints && npm run check:freecad-inventory && npm run check:freecad-type-properties && npm run check:freecad-gui-commands && npm run check:freecad-entrypoints && npm run check:freecad-golden-fixtures && npm run check:freecad-semantic-comparator && npm run check:freecad-fcstd-roundtrip && npm run check:freecad-composite-history-elementmap && npm run check:freecad-exact-history-elementmap-gate && npm run check:freecad-native-naming-evidence && npm run check:fcstd-hardening && npm run check:freecad-naming-sdk && npm run check:occt-history-artifact && npm run check:planegcs-artifact && npm run check:browser-occt && npm run check:chrome-topology && npm run check:chrome-planegcs && npm run check:chrome-fault-injection && npm run check:chrome-opfs-migration && npm run check:chrome-persistence && npm run check:chrome-app-e2e && npm run check:chrome-partdesign-lifecycle && npm run check:pwa && npm run check:chrome-offline && npm run check:chrome-geometry-features && npm run check:chrome-native-history && npm run check:chrome-native-pad-history && npm run check:chrome-native-pocket-history && npm run check:chrome-native-loft-history && npm run check:chrome-native-pipe-history && npm run check:chrome-native-revolution-history && npm run check:chrome-native-groove-history && npm run check:chrome-native-fillet-history && npm run check:chrome-native-chamfer-history && npm run check:chrome-native-hole-history && npm run check:chrome-native-draft-history && npm run check:chrome-native-thickness-history && npm run check:chrome-native-linear-pattern-history && npm run check:chrome-native-polar-pattern-history && npm run check:chrome-native-mirrored-history && npm run check:chrome-native-multi-transform-history && npm run check:chrome-sketcher-diagnostics && npm run check:chrome-profile-validation && npm run check:chrome-sketcher-editor && npm run check:chrome-sketcher-bspline && npm run check:chrome-sketcher-stress && npm run check:chrome-part-primitives && npm run check:chrome-partdesign-transform && npm run check:chrome-partdesign-loft && npm run check:partdesign-closure && npm run check:chrome-fcstd-semantic && npm run check:chrome-fcstd-roundtrip && npm run check:fcstd-closure && npm run check:chrome-spreadsheet && npm run check:chrome-plot && npm run check:chrome-draft && npm run check:chrome-techdraw && npm run check:chrome-assembly && npm run check:chrome-mesh && npm run check:chrome-inspection && npm run check:chrome-bim && npm run check:chrome-cam && npm run check:cam-parity && npm run check:cam-pipeline && npm run check:opencamlib-wasm && npm run check:camotics-native && npm run check:camotics-gui-tpl && npm run check:camotics-wasm && npm run check:chrome-camotics-wasm && npm run check:chrome-cam-native-simulation && npm run check:chrome-cam-linuxcnc-machine && npm run check:chrome-fem && npm run check:chrome-robot && npm run check:chrome-data && npm run check:chrome-addon && npm run check:chrome-script && npm run check:chrome-surface && npm run check:chrome-qa08 && npm run check:chrome-secondary-formats && npm run check:chrome-performance && npm run check:chrome-production-document && npm run check:document-closure && npm run check:chrome-engineering && npm run check:chrome-security && npm run check:chrome-fcstd-golden && npm run check:sbom && npm run check:project-migration && npm run check:security-baseline && npm run check:dependency-audit && npm run check:freecad:threeway && npm run check:occt-upstream-drift && npm run check:facade-boundary && npm run test:golden && npm run test:spreadsheet && npm run test:plot && npm run test:draft && npm run test:techdraw && npm run test:assembly && npm run test:mesh && npm run test:inspection && npm run test:bim && npm run test:cam && npm run test:camotics-wasm && npm run test:fem && npm run test:robot && npm run test:data && npm run test:addon && npm run test:script && npm run test:surface && npm run test:locale && npm run test:secondary-formats && npm run test:performance && npm run test:production-document && npm run test:engineering && npm run test:security-preflight && npm run test:fcstd-roundtrip && npm run test:facade && npm run test:fcstd-fuzz && npm run test:geometry-input-fuzz && npm run test:sketch-solver-fuzz && npm run check:quality-closure && npm run test:planegcs && npm run test:project-migration && npm run test:sketcher-stress && npm run test:topology-replay && npm run build && npm run check:built-pwa && npm run generate:release-artifacts && npm run check:release-artifacts && npm run check:release-closure && npm run test:release-signature",
"test:chrome-native-thickness-history": "node scripts/run-chrome-native-thickness-history.mjs",
"check:chrome-native-thickness-history": "node scripts/check-chrome-native-thickness-history.mjs",
"test:chrome-native-linear-pattern-history": "node scripts/run-chrome-native-linear-pattern-history.mjs",

View File

@@ -54,7 +54,7 @@ try {
const loaded = await persistence.load(documentId)
const restoredResource = await persistence.resource.get(resourceHash)
const recovery = await persistence.recovery(documentId)
const migratedDocument = loaded ? { ...loaded, version: loaded.version + 1, dirty: false, label: 'Migrated OPFS project' } : null
const migratedDocument = loaded ? { ...loaded, version: loaded.version + 1, dirty: false, label: 'Migrated OPFS project', objects: loaded.objects.map((object, index) => ({ ...object, nativeObjectTag: 17 + index })) } : null
if (!migratedDocument) throw new Error('Legacy document did not load after migration.')
await persistence.save(migratedDocument)
await persistence.dispose()
@@ -73,9 +73,12 @@ try {
migratedDocumentVersion: loaded?.version ?? null,
migratedLabel: loaded?.label ?? null,
migratedObjectCount: loaded?.objects.length ?? 0,
migratedNativeObjectTag: loaded?.objects[0]?.nativeObjectTag ?? null,
recomputeDefaulted: loaded?.recompute?.status === 'idle',
reopenedSchemaVersion: reopenedCapabilities.schemaVersion,
reopenedVersion: reopenedDocument?.version ?? null,
reopenedNativeObjectTag: reopenedDocument?.objects[0]?.nativeObjectTag ?? null,
checkpointNativeObjectTag: checkpoint?.objects[0]?.nativeObjectTag ?? null,
reopenedLabel: reopenedDocument?.label ?? null,
checkpointVersion: checkpoint?.version ?? null,
recoveryIntegrity: recovery.integrity,
@@ -85,7 +88,7 @@ try {
}
report.rollback = { rejected: rollbackError.includes('interruption after schema version 3'), interruptedAfterMarker: rollbackError.includes('interruption after schema version 3'), error: rollbackError, appliedVersionsAfterFailure: afterRollback.appliedVersions || [] }
report.cleanup = { databasePath, removedFiles: [] }
report.status = report.crossOriginIsolated && migrated.mode === 'sqlite-opfs' && migrated.schemaVersion === 6 && report.rollback.rejected && report.rollback.interruptedAfterMarker && report.rollback.appliedVersionsAfterFailure.length === 1 && report.rollback.appliedVersionsAfterFailure[0] === 1 && report.migration.migratedDocumentVersion === 3 && report.migration.migratedObjectCount === 1 && report.migration.recomputeDefaulted && report.migration.reopenedSchemaVersion === 6 && report.migration.reopenedVersion === 4 && report.migration.checkpointVersion === 4 && report.migration.recoveryIntegrity === 'ok' && report.migration.reopenedRecoveryIntegrity === 'ok' && report.migration.resourceRoundTrip && report.migration.resourceReleased ? 'pass' : 'failed'
report.status = report.crossOriginIsolated && migrated.mode === 'sqlite-opfs' && migrated.schemaVersion === 7 && report.rollback.rejected && report.rollback.interruptedAfterMarker && report.rollback.appliedVersionsAfterFailure.length === 1 && report.rollback.appliedVersionsAfterFailure[0] === 1 && report.migration.migratedDocumentVersion === 3 && report.migration.migratedObjectCount === 1 && report.migration.migratedNativeObjectTag === 1 && report.migration.recomputeDefaulted && report.migration.reopenedSchemaVersion === 7 && report.migration.reopenedVersion === 4 && report.migration.reopenedNativeObjectTag === 17 && report.migration.checkpointNativeObjectTag === 17 && report.migration.checkpointVersion === 4 && report.migration.recoveryIntegrity === 'ok' && report.migration.reopenedRecoveryIntegrity === 'ok' && report.migration.resourceRoundTrip && report.migration.resourceReleased ? 'pass' : 'failed'
} catch (error) {
report.status = 'failed'
report.error = error instanceof Error ? `${error.name}: ${error.message}` : String(error)

View File

@@ -5,7 +5,7 @@ const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/chrome-opfs-migration-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.browserId !== 'chrome' || report.status !== 'pass' || report.crossOriginIsolated !== true) throw new Error('Chrome REL-02 OPFS migration evidence is not passing.')
const migration = report.migration
if (!migration || migration.sourceSchemaVersion !== 1 || migration.migratedSchemaVersion !== 6 || migration.migratedDocumentVersion !== 3 || migration.migratedObjectCount !== 1 || migration.recomputeDefaulted !== true || migration.reopenedSchemaVersion !== 6 || migration.reopenedVersion !== 4 || migration.checkpointVersion !== 4 || migration.recoveryIntegrity !== 'ok' || migration.reopenedRecoveryIntegrity !== 'ok' || migration.resourceRoundTrip !== true || migration.resourceReleased !== true) throw new Error('Chrome REL-02 forward migration, reopen, checkpoint, or resource evidence is invalid.')
if (!migration || migration.sourceSchemaVersion !== 1 || migration.migratedSchemaVersion !== 7 || migration.migratedDocumentVersion !== 3 || migration.migratedObjectCount !== 1 || migration.migratedNativeObjectTag !== 1 || migration.recomputeDefaulted !== true || migration.reopenedSchemaVersion !== 7 || migration.reopenedVersion !== 4 || migration.reopenedNativeObjectTag !== 17 || migration.checkpointNativeObjectTag !== 17 || migration.checkpointVersion !== 4 || migration.recoveryIntegrity !== 'ok' || migration.reopenedRecoveryIntegrity !== 'ok' || migration.resourceRoundTrip !== true || migration.resourceReleased !== true) throw new Error('Chrome REL-02 forward migration, native object tag, reopen, checkpoint, or resource evidence is invalid.')
if (!report.rollback || report.rollback.rejected !== true || report.rollback.interruptedAfterMarker !== true || !/interruption after schema version 3/.test(report.rollback.error || '') || JSON.stringify(report.rollback.appliedVersionsAfterFailure) !== '[1]') throw new Error('Chrome REL-02 SQLite interruption rollback evidence is invalid.')
if (!report.cleanup || !Array.isArray(report.cleanup.removedFiles) || report.cleanup.removedFiles.length === 0 || report.cleanup.removedFiles.some((name) => typeof name !== 'string' || !name.startsWith(report.cleanup.databasePath.slice(1)))) throw new Error('Chrome REL-02 OPFS database cleanup evidence is invalid.')
console.log(JSON.stringify({ status: 'chrome-opfs-migration-pass', browserId: report.browserId, migration, cleanup: report.cleanup }, null, 2))

View File

@@ -0,0 +1,60 @@
import { access, readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { resolve, isAbsolute } from 'node:path'
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) : ''
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) {
console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the shipped Worker remains OCCT-only.' }, null, 2))
process.exit(0)
}
const manifestPath = resolve(sdkRoot, 'manifest.json')
if (!await exists(manifestPath)) fail(`missing manifest: ${manifestPath}`)
let manifest
try { manifest = JSON.parse(await readFile(manifestPath, 'utf8')) } catch (error) { fail(`manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`) }
if (manifest.schemaVersion !== 1) fail(`unsupported manifest schema ${String(manifest.schemaVersion)}.`)
if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freecadVersion must be 1.1.1.')
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.')
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}.`)
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.')
for (const path of resolvedIncludeDirs) {
if (!await exists(path)) fail(`missing include directory: ${path}`)
}
for (const library of libraries) {
if (!library || typeof library !== 'object') fail('libraries entries must be objects.')
const name = required(library.name, 'libraries[].name')
const pathValue = required(library.path, `libraries[${name}].path`)
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)
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)
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))

View File

@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto'
import { access, readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(new URL('..', import.meta.url).pathname)
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm']
@@ -27,4 +28,56 @@ const workerClient = await readFile(resolve(root, 'src/facade/nativeHistoryWorke
if (!workerClient.includes("'/native/occt-history/bitbybit-occt-history.js'")) throw new Error('Native history Worker client URL does not match the published artifact.')
const files = []
for (const name of artifactNames) files.push({ name, sha256: hash(await readFile(resolve(publicRoot, name))) })
console.log(JSON.stringify({ status: 'artifact-pass', files }, null, 2))
let namingAbi = { availability: 'unavailable', callbacks: [] }
const factoryModule = await import(pathToFileURL(resolve(distRoot, 'bitbybit-occt-history.js')).href)
const factory = factoryModule.default
if (typeof factory !== 'function') throw new TypeError('OCCT history artifact default export is not a factory.')
const nativeModule = await factory({ locateFile: (fileName) => resolve(distRoot, fileName.split('/').at(-1)) })
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'].filter((name) => typeof nativeModule[name] === 'function')
if (callbacks.length !== 0 && callbacks.length !== 3) throw new Error(`FreeCAD naming ABI is partially exported: ${callbacks.join(', ')}.`)
if (callbacks.length === 3) {
const abiVersion = nativeModule.freecadNamingAbiVersion()
const descriptor = JSON.parse(nativeModule.freecadNamingCapabilitiesJson())
if (abiVersion !== 1 || descriptor.schemaVersion !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || descriptor.mappedNameRef !== true || descriptor.stringHasher !== true || descriptor.elementMap2 !== true || !Array.isArray(descriptor.operations) || !descriptor.operations.includes('cut')) throw new Error('FreeCAD naming ABI descriptor is not the locked v1 contract with Cut support.')
let object
let tool
let evidence
try {
object = nativeModule.makeBox(10, 10, 10)
tool = nativeModule.makeBox(5, 5, 5)
const objectStep = nativeModule.shapeToStep(object)
const toolStep = nativeModule.shapeToStep(tool)
const history = nativeModule.booleanHistoryFromStep(objectStep, toolStep, 'cut')
const request = {
schemaVersion: 1,
requestId: 'artifact-private-naming-probe',
documentId: 'artifact-probe-document',
documentVersion: 1,
operationId: 'artifact-probe-cut',
operation: 'cut',
stageId: 'artifact-probe:stage:0',
resultObjectId: 'artifact-probe:result',
resultObjectTag: 3,
inputs: [
{ inputId: 'object', objectId: 'artifact-probe:object', role: 'object', step: objectStep, objectTag: 1 },
{ inputId: 'tool', objectId: 'artifact-probe:tool', role: 'tool', step: toolStep, objectTag: 2 },
],
stages: [{ stageId: 'artifact-probe:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
resultStep: history.resultStep,
resultBrep: history.resultBrep,
history,
}
evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request)))
} finally {
object?.delete?.()
tool?.delete?.()
}
if (!evidence || evidence.schemaVersion !== 1 || evidence.stageId !== 'artifact-probe:stage:0' || evidence.resultObjectId !== 'artifact-probe:result' || !['native-evidence', 'ambiguous'].includes(evidence.status) || !Array.isArray(evidence.mappedNames) || evidence.mappedNames.length === 0 || !evidence.stringHasher || !evidence.elementMap2) throw new Error('FreeCAD naming ABI did not return complete stage-bound Cut evidence.')
namingAbi = { availability: 'available', callbacks, abiVersion, descriptor, evidenceProbe: { operation: 'cut', status: evidence.status, mappedNames: evidence.mappedNames.length, stringHasher: true, elementMap2: true } }
}
const abiContract = JSON.parse(await readFile(resolve(root, 'config/freecad-sketcher-partdesign-abi-contract.json'), 'utf8'))
const expectedImplementation = abiContract.privateNamingAbi?.shippedWorkerImplementation
if (expectedImplementation === 'not-linked' && namingAbi.availability !== 'unavailable') throw new Error('OCCT history artifact exports FreeCAD naming callbacks but the shipped-worker contract still says not-linked.')
if (expectedImplementation === 'freecad-linked' && namingAbi.availability !== 'available') throw new Error('Shipped-worker contract claims FreeCAD linkage but the artifact ABI probe is unavailable.')
if (!['not-linked', 'freecad-linked'].includes(expectedImplementation)) throw new Error('Shipped-worker FreeCAD naming implementation status is invalid.')
console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'probe-only; FreeCAD evidence still requires locked descriptor and response validation' : 'occt-only; EX-TSN-02 remains in_progress and systemExact=false' }, null, 2))

View File

@@ -59,7 +59,7 @@ if (sketchFuzz.status !== 'sketch-solver-fuzz-pass' || sketchFuzz.models !== 200
if (performance.status !== 'pass' || performance.benchmark?.objects !== 1000 || performance.benchmark.triangles !== 1_000_000 || performance.benchmark.tableCells !== 100_000 || performance.benchmark.pass !== true || performance.afterRelease?.shapeCount !== 0) throw new Error('QA-05 performance and ownership evidence is incomplete.')
if (fault.status !== 'pass' || fault.lifecycle?.completed !== 'completed' || fault.lifecycle.cancelled !== 'cancelled' || fault.lifecycle.crashInjected !== true || fault.lifecycle.recovered !== 'completed' || fault.lifecycle.stale !== 'stale' || fault.opfs?.markerRemoved !== true) throw new Error('QA-06 Worker/WASM fault evidence is incomplete.')
if (opfsMigration.status !== 'pass' || opfsMigration.migration?.migratedSchemaVersion !== 6 || opfsMigration.migration.recoveryIntegrity !== 'ok' || opfsMigration.rollback?.rejected !== true) throw new Error('QA-06 OPFS recovery evidence is incomplete.')
if (opfsMigration.status !== 'pass' || opfsMigration.migration?.migratedSchemaVersion !== 7 || opfsMigration.migration.reopenedNativeObjectTag !== 17 || opfsMigration.migration.recoveryIntegrity !== 'ok' || opfsMigration.rollback?.rejected !== true) throw new Error('QA-06 OPFS recovery evidence is incomplete.')
if (security.status !== 'pass' || security.checks?.rejectedCases !== 4 || security.checks.pass !== true || addon.security?.forgedRejected !== true || addon.security.permissionRejected !== true || script.security?.noDynamicExecution !== true || script.security.deniedCapabilities?.length !== 4) throw new Error('QA-07 security evidence is incomplete.')
if (qa08.status !== 'pass' || qa08.locales?.supported !== 3 || qa08.accessibility?.pass !== true || app.keyboard?.named !== 8 || app.screenReader?.namedControls !== app.screenReader?.controls || app.screenReader?.unnamedControls !== 0 || app.mobile?.bodyHorizontalOverflow > 1) throw new Error('QA-08 accessibility/i18n evidence is incomplete.')

View File

@@ -30,6 +30,6 @@ await access(resolve(root, 'docs/release-runbook.zh-CN.md')).catch(() => fail('r
if (release.schemaVersion !== 1 || release.signature?.status !== 'signed' || release.signature.algorithm !== 'Ed25519' || !release.signature.keyId || !/^[0-9a-f]{64}$/.test(release.signature.payloadSha256 || '') || release.build?.files?.length === 0) fail('release artifact manifest is not signed and complete.')
if (sbom.bomFormat !== 'CycloneDX' || sbom.components?.length !== audit.metadata?.dependencies?.total) fail('SBOM is missing, stale, or inconsistent with the locked audit inventory.')
if (offline.status !== 'pass' || offline.serviceWorker?.offlineFallback !== true || offline.serviceWorker?.staleCacheRemoved !== true) fail('offline deployment evidence is incomplete.')
if (migration.status !== 'pass' || migration.currentSchemaVersion !== 6 || migration.forward?.length !== 6 || migration.rollback?.length !== 6) fail('migration/rollback evidence is incomplete.')
if (migration.status !== 'pass' || migration.currentSchemaVersion !== 7 || migration.forward?.length !== 7 || migration.rollback?.length !== 7) fail('migration/rollback evidence is incomplete.')
if (performance.status !== 'pass' || performance.benchmark?.pass !== true || security.status !== 'pass' || security.checks?.pass !== true || quality.status !== 'quality-closure-pass') fail('quality evidence is incomplete.')
console.log(JSON.stringify({ status: 'release-closure-pass', gates: matrix.gates.length, modules: coverage.modules.length, moduleCounts: matrix.moduleCounts, signed: true, sbomComponents: sbom.components.length, migrationVersions: migration.currentSchemaVersion }, null, 2))

View File

@@ -52,6 +52,7 @@ export type FcstdObjectSummary = {
name: string
label: string
typeId: string
nativeObjectTag?: number
propertyCount: number
properties: FcstdPropertySummary[]
support: FcstdObjectSupport
@@ -1103,16 +1104,32 @@ export const serializeFcstdMetadataArchive = (document: DocumentSnapshot, option
const objectIds = document.objects.map((object) => object.id.trim())
if (objectIds.some((objectId) => !objectId)) throw new TypeError('FCStd metadata writer requires non-empty object ids.')
if (new Set(objectIds).size !== objectIds.length) throw new TypeError('FCStd metadata writer requires unique object ids.')
const explicitObjectTags = new Set<number>()
for (const object of document.objects) {
if (object.nativeObjectTag === undefined) continue
if (!Number.isSafeInteger(object.nativeObjectTag) || object.nativeObjectTag <= 0) throw new RangeError(`FCStd object ${object.id} nativeObjectTag must be a positive safe integer.`)
if (explicitObjectTags.has(object.nativeObjectTag)) throw new RangeError(`FCStd nativeObjectTag ${object.nativeObjectTag} is assigned to more than one object.`)
explicitObjectTags.add(object.nativeObjectTag)
}
let nextObjectTag = 1
const objectTags = new Map(document.objects.map((object) => {
if (object.nativeObjectTag !== undefined) return [object.id, object.nativeObjectTag] as const
while (explicitObjectTags.has(nextObjectTag)) nextObjectTag += 1
const tag = nextObjectTag
explicitObjectTags.add(tag)
nextObjectTag += 1
return [object.id, tag] as const
}))
const objectIdSet = new Set(objectIds)
const dependencies = new Map(objectIds.map((objectId) => [objectId, [] as string[]]))
for (const edge of document.dependencies ?? []) {
if (!objectIdSet.has(edge.sourceId) || !objectIdSet.has(edge.targetId)) throw new Error(`FCStd dependency ${edge.sourceId} -> ${edge.targetId} references an undeclared object.`)
if (edge.relation !== 'view') dependencies.get(edge.sourceId)?.push(edge.targetId)
}
const objectDeclarations = document.objects.map((object, objectIndex) => {
const objectDeclarations = document.objects.map((object) => {
const targets = dependencies.get(object.id) ?? []
const dependencyXml = targets.length === 0 ? `<ObjectDeps Name="${xmlEscape(object.id)}" Count="0"/>` : `<ObjectDeps Name="${xmlEscape(object.id)}" Count="${targets.length}">${targets.map((target) => `<Dep Name="${xmlEscape(target)}"/>`).join('')}</ObjectDeps>`
return `${dependencyXml}<Object name="${xmlEscape(object.id)}" type="${xmlEscape(nativeFcstdTypeId(object.typeId))}" id="${objectIndex + 1}" Touched="1"/>`
return `${dependencyXml}<Object name="${xmlEscape(object.id)}" type="${xmlEscape(nativeFcstdTypeId(object.typeId))}" id="${objectTags.get(object.id)}" Touched="1"/>`
}).join('')
const objectData = document.objects.map((object) => {
const sketchGeneratedPropertyNames = object.sketch ? new Set(['Geometry', 'Constraints', 'ExternalGeo', 'ExternalGeometry', 'ExternalTypes', 'WebGeometryIds', 'WebSyntheticConstraintIds', 'WebExternalIds', 'WebExternalProjectionIds', 'WebExternalSources']) : new Set<string>()
@@ -2014,9 +2031,18 @@ const parseDocumentXml = (bytes: Uint8Array, limits: FcstdArchiveLimits) => {
const dataByName = new Map(objectData.map((data) => [attribute(data, 'name'), data]))
const documentProperties = asArray((((root.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
const labelProperty = documentProperties.find((property) => attribute(property, 'name') === 'Label')
const nativeObjectTags = new Set<number>()
const objects = objectDeclarations.map((declaration): FcstdObjectSummary => {
const name = attribute(declaration, 'name') || '<unnamed>'
const typeId = attribute(declaration, 'type') || 'App::DocumentObject'
const objectTagText = attribute(declaration, 'id')
let nativeObjectTag: number | undefined
if (objectTagText) {
if (!/^\d+$/.test(objectTagText) || !Number.isSafeInteger(Number(objectTagText)) || Number(objectTagText) <= 0) throw new Error(`FCStd object ${name} has an invalid native id.`)
nativeObjectTag = Number(objectTagText)
if (nativeObjectTags.has(nativeObjectTag)) throw new Error(`FCStd object native id ${nativeObjectTag} is duplicated.`)
nativeObjectTags.add(nativeObjectTag)
}
const data = dataByName.get(name)
const propertiesContainer = data?.Properties as Record<string, unknown> | undefined
const properties = asArray(((propertiesContainer?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
@@ -2029,7 +2055,7 @@ const parseDocumentXml = (bytes: Uint8Array, limits: FcstdArchiveLimits) => {
const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
const summaries = propertySummaries(properties).map((property) => expressions.has(property.name) ? { ...property, expression: expressions.get(property.name) } : property)
const sketch = typeId === 'Sketcher::SketchObject' ? sketchFromPropertySummaries(name, summaries) : undefined
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, propertyCount: properties.length, properties: summaries, support, extensions, ...(sketch ? { sketch } : {}) }
return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, ...(nativeObjectTag === undefined ? {} : { nativeObjectTag }), propertyCount: properties.length, properties: summaries, support, extensions, ...(sketch ? { sketch } : {}) }
})
return {
schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
@@ -2348,6 +2374,7 @@ const createProxyDocument = (inspection: Omit<FcstdInspection, 'proxyDocument'>)
return {
id: object.name,
typeId: object.typeId,
...(object.nativeObjectTag === undefined ? {} : { nativeObjectTag: object.nativeObjectTag }),
properties: [
createProxyProperty('Label', 'Label', object.label),
createProxyProperty('TypeId', 'Type', object.typeId),

View File

@@ -1324,9 +1324,12 @@ export class BitbybitGeometryRuntime {
const stageTransport = input.stages?.length ? {
inputs: input.inputs.map((source, index) => ({
inputId: source.inputId ?? `${input.operationId}:input:${index}`,
objectId: source.objectId,
role: source.role,
stageId: source.stageId,
step: stepByObjectId.get(source.objectId)!.text,
objectTag: source.objectTag,
namingEvidence: source.namingEvidence,
})),
stages: input.stages.map((stage) => ({
stageId: stage.stageId,
@@ -1339,12 +1342,12 @@ export class BitbybitGeometryRuntime {
ordinal: stage.ordinal,
})),
} : {}
const captureHistory = (request: Parameters<NativeOcctHistoryCoordinator['capture']>[1]) => nativeHistory.coordinator.capture(nativeHistory.provider, { ...request, ...stageTransport })
const mapHistoryRecords = (response: Parameters<typeof mapNativeOcctHistoryRecords>[0], sourceIds: { object: string; tool: string } | Record<string, string>) => mapNativeOcctHistoryRecords(response, sourceIds, input)
type NativeStageRequest = Pick<NativeOcctHistoryRequest, 'objectStep'> & Partial<Pick<NativeOcctHistoryRequest, 'toolStep' | 'direction' | 'axisOrigin' | 'angle'>>
const finalResultObjectId = () => input.stages?.length
? input.stages[input.stages.length - 1].resultObjectId ?? `${input.operationId}:result`
: `${input.operationId}:result`
const captureHistory = (request: Parameters<NativeOcctHistoryCoordinator['capture']>[1]) => nativeHistory.coordinator.capture(nativeHistory.provider, { ...request, ...stageTransport, resultObjectId: finalResultObjectId(), resultObjectTag: input.resultObjectTag })
const mapHistoryRecords = (response: Parameters<typeof mapNativeOcctHistoryRecords>[0], sourceIds: { object: string; tool: string } | Record<string, string>) => mapNativeOcctHistoryRecords(response, sourceIds, input)
type NativeStageRequest = Pick<NativeOcctHistoryRequest, 'objectStep'> & Partial<Pick<NativeOcctHistoryRequest, 'toolStep' | 'direction' | 'axisOrigin' | 'angle'>>
const namingEvidenceForResponse = (response: NativeOcctHistoryResponse, stageId: string, resultObjectId: string) => response.namingEvidence
? assertNativeNamingEvidence({ ...response.namingEvidence, stageId, resultObjectId })
: createFinalShapeOnlyNamingEvidence(stageId, resultObjectId)
@@ -1358,11 +1361,24 @@ export class BitbybitGeometryRuntime {
inputs: NativeTopologyHistoryInput['inputs']
request: NativeStageRequest
}) => {
const transportInputs = stage.inputs.map((source, index) => ({
inputId: `${stage.stageId}:input:${index}`,
objectId: source.objectId,
role: index === 0 ? 'object' : 'tool',
stageId: source.stageId,
step: index === 0 ? stage.request.objectStep : stage.request.toolStep ?? stage.request.objectStep,
objectTag: source.objectTag,
namingEvidence: source.namingEvidence,
}))
const execution = await nativeHistory.coordinator.capture(nativeHistory.provider, {
documentId: input.documentId,
documentVersion: input.documentVersion,
operationId: stage.stageId,
operation: stage.operation,
resultObjectId: stage.resultObjectId,
...(stage.resultObjectId === finalResultObjectId() && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}),
inputs: transportInputs,
stages: [{ stageId: stage.stageId, operation: stage.operation, inputIds: transportInputs.map(({ inputId }) => inputId), ordinal: stage.ordinal }],
...stage.request,
})
if (execution.status !== 'completed' || !execution.response) throw new Error(`Native OCCT ${stage.operation} stage history ${execution.status}.`)
@@ -1445,8 +1461,8 @@ export class BitbybitGeometryRuntime {
ordinal: 2,
sourceIds: { object: firstObjectId, tool: secondObjectId },
inputs: [
{ objectId: firstObjectId, shape: profileInput.shape, stageId: firstStageId },
{ objectId: secondObjectId, shape: profileInput.shape, stageId: secondStageId },
{ objectId: firstObjectId, shape: profileInput.shape, stageId: firstStageId, namingEvidence: first.capture.namingEvidence },
{ objectId: secondObjectId, shape: profileInput.shape, stageId: secondStageId, namingEvidence: second.capture.namingEvidence },
],
request: { objectStep: first.resultStep, toolStep: second.resultStep },
})
@@ -1498,8 +1514,8 @@ export class BitbybitGeometryRuntime {
ordinal: 2,
sourceIds: { object: toolOneObjectId, tool: toolTwoObjectId },
inputs: [
{ objectId: toolOneObjectId, shape: profileInput.shape, stageId: toolOneStageId },
{ objectId: toolTwoObjectId, shape: profileInput.shape, stageId: toolTwoStageId },
{ objectId: toolOneObjectId, shape: profileInput.shape, stageId: toolOneStageId, namingEvidence: toolOne.capture.namingEvidence },
{ objectId: toolTwoObjectId, shape: profileInput.shape, stageId: toolTwoStageId, namingEvidence: toolTwo.capture.namingEvidence },
],
request: { objectStep: toolOne.resultStep, toolStep: toolTwo.resultStep },
})
@@ -1512,7 +1528,7 @@ export class BitbybitGeometryRuntime {
sourceIds: { object: baseInput.objectId, tool: fusedToolObjectId },
inputs: [
baseInput,
{ objectId: fusedToolObjectId, shape: profileInput.shape, stageId: fuseStageId },
{ objectId: fusedToolObjectId, shape: profileInput.shape, stageId: fuseStageId, namingEvidence: fusedTool.capture.namingEvidence },
],
request: { objectStep: baseStep, toolStep: fusedTool.resultStep },
})
@@ -1554,7 +1570,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: finalResultObjectId(),
ordinal: 1,
sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId },
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId }],
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }],
request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle },
})
return withStageCaptures([rotated.capture, revolution.capture])
@@ -1592,7 +1608,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: revolutionToolObjectId,
ordinal: 1,
sourceIds: { object: rotatedProfileObjectId, tool: rotatedProfileObjectId },
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId }],
inputs: [{ objectId: rotatedProfileObjectId, shape: profileInput.shape, stageId: rotateStageId, namingEvidence: rotated.capture.namingEvidence }],
request: { objectStep: rotated.resultStep, axisOrigin, direction: parameters.direction, angle: parameters.totalAngle },
})
const cut = await captureStage({
@@ -1602,7 +1618,7 @@ export class BitbybitGeometryRuntime {
resultObjectId: finalResultObjectId(),
ordinal: 2,
sourceIds: { object: baseInput.objectId, tool: revolutionToolObjectId },
inputs: [baseInput, { objectId: revolutionToolObjectId, shape: profileInput.shape, stageId: revolutionStageId }],
inputs: [baseInput, { objectId: revolutionToolObjectId, shape: profileInput.shape, stageId: revolutionStageId, namingEvidence: revolution.capture.namingEvidence }],
request: { objectStep: baseStep, toolStep: revolution.resultStep },
})
return withStageCaptures([rotated.capture, revolution.capture, cut.capture])
@@ -1639,6 +1655,9 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: toolStageId,
operation: 'pad',
resultObjectId: toolObjectId,
inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }],
stages: [{ stageId: toolStageId, operation: 'pad', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }],
objectStep: profileStep.text,
direction: sides[0].direction,
})
@@ -1661,6 +1680,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolResponse.resultStep,
})
@@ -1745,6 +1771,9 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: toolStageId,
operation: 'revolution',
resultObjectId: toolObjectId,
inputs: [{ inputId: `${toolStageId}:input:0`, objectId: profileInput.objectId, role: 'object', step: profileStep.text, objectTag: profileInput.objectTag, namingEvidence: profileInput.namingEvidence }],
stages: [{ stageId: toolStageId, operation: 'revolution', inputIds: [`${toolStageId}:input:0`], ordinal: 0 }],
objectStep: profileStep.text,
axisOrigin,
direction: sides[0].direction,
@@ -1769,6 +1798,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolResponse.resultStep, namingEvidence: toolResponse.namingEvidence },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolResponse.resultStep,
})
@@ -1909,6 +1945,13 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: cutStageId,
operation: 'cut',
resultObjectId,
...(input.resultObjectTag === undefined ? {} : { resultObjectTag: input.resultObjectTag }),
inputs: [
{ inputId: `${cutStageId}:input:0`, objectId: baseInput.objectId, role: 'object', step: baseStep.text, objectTag: baseInput.objectTag, namingEvidence: baseInput.namingEvidence },
{ inputId: `${cutStageId}:input:1`, objectId: toolObjectId, role: 'tool', stageId: toolStageId, step: toolStep.text, namingEvidence: createFinalShapeOnlyNamingEvidence(toolStageId, toolObjectId, 'Hole tool was synthesized by the geometry runtime; provider did not capture its builder naming evidence.') },
],
stages: [{ stageId: cutStageId, operation: 'cut', inputIds: [`${cutStageId}:input:0`, `${cutStageId}:input:1`], ordinal: 1 }],
objectStep: baseStep.text,
toolStep: toolStep.text,
})
@@ -2050,6 +2093,7 @@ export class BitbybitGeometryRuntime {
let previousStep = baseStep.text
let previousObjectId = baseInput.objectId
let previousStageId: string | undefined
let previousNamingEvidence = baseInput.namingEvidence
const declaredResultObjectId = input.stages?.length ? input.stages[input.stages.length - 1].resultObjectId : undefined
for (let index = 0; index < input.transforms.length; index += 1) {
const step = input.transforms[index]
@@ -2062,6 +2106,10 @@ export class BitbybitGeometryRuntime {
documentVersion: input.documentVersion,
operationId: `${input.operationId}:native-stage:${index}`,
operation,
resultObjectId,
...(index === input.transforms.length - 1 && input.resultObjectTag !== undefined ? { resultObjectTag: input.resultObjectTag } : {}),
inputs: [{ inputId: `${stageId}:input:0`, objectId: previousObjectId, role: 'object', stageId: previousStageId, step: previousStep, ...(index === 0 && baseInput.objectTag !== undefined ? { objectTag: baseInput.objectTag } : {}), namingEvidence: previousNamingEvidence }],
stages: [{ stageId, operation, inputIds: [`${stageId}:input:0`], ordinal: index }],
objectStep: previousStep,
...(step.type === 'linear' ? { direction: step.direction } : {}),
...(step.type === 'polar' ? { axisOrigin: step.axisOrigin, direction: step.direction, angle: step.angle } : {}),
@@ -2085,6 +2133,7 @@ export class BitbybitGeometryRuntime {
previousStep = response.resultStep
previousObjectId = resultObjectId
previousStageId = stageId
previousNamingEvidence = namingEvidenceForResponse(response, stageId, resultObjectId)
}
const records = captures[captures.length - 1].records as NativeTopologyHistoryRecords
Object.defineProperty(records, 'stageCaptures', { value: captures, enumerable: false, configurable: false, writable: false })

View File

@@ -81,7 +81,7 @@ export { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, nativeNamingAbiCapabilities, native
export type { NativeNamingAbiCapabilities, NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryInputTransport, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest, NativeOcctHistoryStageTransport } from './nativeHistoryProtocol'
export { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, FREECAD_PRIVATE_NAMING_ABI_VERSION, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES, probeFreeCadPrivateNamingAbi } from './nativeNamingAbi'
export type { FreeCadPrivateNamingAbiDescriptor, FreeCadPrivateNamingAbiProbe, FreeCadPrivateNamingAbiRequest, NativeFreeCadNamingAbiModule } from './nativeNamingAbi'
export { assertNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
export { assertNativeNamingEvidence, cloneNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
export type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence'
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'
export type { NativeOcctHistoryWorkerOptions } from './nativeHistoryWorkerClient'

View File

@@ -390,7 +390,7 @@ const expressionReferences = (expression: string): string[] => {
const createDocumentFromTemplate = (template: DocumentTemplate): DocumentSnapshot => {
const tree = template.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
const objects = tree.map(createObjectSnapshot)
const objects = tree.map((item, index) => ({ ...createObjectSnapshot(item), nativeObjectTag: index + 1 }))
const document: DocumentSnapshot = { id: template.id, label: template.label, version: template.version, dirty: template.dirty, readOnly: template.readOnly, units: template.units, tree, objects }
document.dependencies = collectDependencyEdges(document)
document.recompute = createRecomputeSnapshot(objects.map((object) => object.id))
@@ -799,6 +799,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
tree.push(item)
}
const objectSnapshot = createObjectSnapshot(item)
objectSnapshot.nativeObjectTag = document.objects.reduce((maximum, object) => Math.max(maximum, object.nativeObjectTag ?? 0), 0) + 1
if (commandId === 'create-body') {
const tip = objectSnapshot.properties.find((property) => property.name === 'Tip')
if (tip) tip.value = null

View File

@@ -1,5 +1,6 @@
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse, NativeOcctHistoryStepProvider, NativeOcctMultiTransformStep } from './nativeHistoryProvider'
import { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi } from './nativeNamingAbi'
import type { NativeStageNamingEvidence } from './nativeNamingEvidence'
export const NATIVE_OCCT_HISTORY_PROTOCOL_VERSION = 1 as const
@@ -61,6 +62,8 @@ export type NativeOcctHistoryRequest = {
documentId: string
documentVersion: number
operationId: string
/** Document object produced by this native stage; distinct from the request operation ID. */
resultObjectId?: string
operation: NativeOcctHistoryOperation
transformKind?: 'linear' | 'polar' | 'mirrored'
transforms?: NativeOcctMultiTransformStep[]
@@ -70,6 +73,7 @@ export type NativeOcctHistoryRequest = {
inputs?: NativeOcctHistoryInputTransport[]
stages?: NativeOcctHistoryStageTransport[]
resultStepByStage?: Record<string, string>
resultObjectTag?: number
direction?: [number, number, number]
axisOrigin?: [number, number, number]
angle?: number
@@ -87,9 +91,13 @@ export type NativeOcctHistoryRequest = {
export type NativeOcctHistoryInputTransport = {
inputId: string
/** Stable document object identity represented by this transport input. */
objectId?: string
role?: string
stageId?: string
step: string
objectTag?: number
namingEvidence?: NativeStageNamingEvidence
}
export type NativeOcctHistoryStageTransport = {
@@ -129,6 +137,7 @@ const booleanOperations: readonly NativeOcctHistoryOperation[] = ['fuse', 'cut',
const assertRequest = (request: NativeOcctHistoryRequest) => {
if (request.protocolVersion !== NATIVE_OCCT_HISTORY_PROTOCOL_VERSION) throw new RangeError(`Unsupported native OCCT history protocol version: ${request.protocolVersion}.`)
if (request.resultObjectId !== undefined && !request.resultObjectId.trim()) throw new TypeError('Native OCCT history resultObjectId must be non-empty when provided.')
if (!request.requestId.trim() || !request.documentId.trim() || !request.operationId.trim()) throw new TypeError('Native OCCT history request IDs must be non-empty strings.')
if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Native OCCT history documentVersion must be a non-negative integer.')
if (!['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'].includes(request.operation)) throw new RangeError(`Unsupported native OCCT history operation: ${String(request.operation)}.`)
@@ -162,6 +171,11 @@ const assertRequest = (request: NativeOcctHistoryRequest) => {
if (!step.startsWith('ISO-10303-21;')) throw new TypeError(`Native OCCT result step for '${stageId}' requires STEP text.`)
}
}
if (request.resultObjectTag !== undefined && (!Number.isSafeInteger(request.resultObjectTag) || request.resultObjectTag <= 0)) throw new RangeError('Native OCCT history resultObjectTag must be a positive safe integer.')
for (const input of request.inputs ?? []) {
if (input.objectId !== undefined && !input.objectId.trim()) throw new TypeError(`Native OCCT history input ${input.inputId} objectId must be non-empty when provided.`)
if (input.objectTag !== undefined && (!Number.isSafeInteger(input.objectTag) || input.objectTag <= 0)) throw new RangeError(`Native OCCT history input ${input.inputId} objectTag must be a positive safe integer.`)
}
if (booleanOperations.includes(request.operation)) {
if (!request.toolStep?.startsWith('ISO-10303-21;')) throw new TypeError('Native Boolean history transport requires object and tool STEP text.')
} else if (request.operation === 'pocket') {
@@ -359,8 +373,9 @@ export class DirectNativeOcctHistoryProvider implements NativeOcctHistoryProvide
operationId: request.operationId,
operation: request.operation,
stageId,
resultObjectId: request.operationId,
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, role, stageId: inputStageId, step }) => ({ inputId, step, ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}) })),
resultObjectId: request.resultObjectId ?? request.operationId,
...(request.resultObjectTag === undefined ? {} : { resultObjectTag: request.resultObjectTag }),
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, objectId, role, stageId: inputStageId, step, objectTag, namingEvidence }) => ({ inputId, step, ...(objectId ? { objectId } : {}), ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}), ...(objectTag === undefined ? {} : { objectTag }), ...(namingEvidence ? { namingEvidence } : {}) })),
stages: orderedStages.map((stage) => ({ ...stage, inputIds: [...stage.inputIds] })),
...(stagedHistory.resultStep ? { resultStep: stagedHistory.resultStep } : {}),
...(stagedHistory.resultBrep ? { resultBrep: stagedHistory.resultBrep } : {}),

View File

@@ -94,8 +94,9 @@ scope.onmessage = ({ data }: MessageEvent<WorkerRequest>) => {
operationId: request.operationId,
operation: request.operation,
stageId,
resultObjectId: request.operationId,
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, role, stageId: inputStageId, step }) => ({ inputId, step, ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}) })),
resultObjectId: request.resultObjectId ?? request.operationId,
...(request.resultObjectTag === undefined ? {} : { resultObjectTag: request.resultObjectTag }),
inputs: (request.inputs ?? [{ inputId: 'object', role: 'object', step: request.objectStep }, ...(request.toolStep ? [{ inputId: 'tool', role: 'tool', step: request.toolStep }] : [])]).map(({ inputId, objectId, role, stageId: inputStageId, step, objectTag, namingEvidence }) => ({ inputId, step, ...(objectId ? { objectId } : {}), ...(role ? { role } : {}), ...(inputStageId ? { stageId: inputStageId } : {}), ...(objectTag === undefined ? {} : { objectTag }), ...(namingEvidence ? { namingEvidence } : {}) })),
stages: orderedStages.map((stage) => ({ ...stage, inputIds: [...stage.inputIds] })),
...(history.resultStep ? { resultStep: history.resultStep } : {}),
...(history.resultBrep ? { resultBrep: history.resultBrep } : {}),

View File

@@ -1,5 +1,5 @@
import { migrateElementMap2Schema, validateElementMap2 } from './elementMap2'
import { assertNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { assertNativeNamingEvidence, cloneNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { migrateStringHasherSchema, validateElementMap2StringHasherEvidence, validateStringHasherTable } from './stringHasher'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider'
@@ -42,7 +42,8 @@ export type FreeCadPrivateNamingAbiRequest = {
operation: NativeOcctHistoryOperation
stageId: string
resultObjectId: string
inputs: Array<{ inputId: string; role?: string; stageId?: string; step: string }>
resultObjectTag?: number
inputs: Array<{ inputId: string; objectId?: string; role?: string; stageId?: string; step: string; objectTag?: number; namingEvidence?: NativeStageNamingEvidence }>
stages: Array<{ stageId: string; operation?: NativeOcctHistoryOperation; inputIds: string[]; resultStageId?: string; ordinal: number }>
resultStep?: string
resultBrep?: string
@@ -91,12 +92,15 @@ export const probeFreeCadPrivateNamingAbi = (module: NativeFreeCadNamingAbiModul
export const createFreeCadPrivateNamingAbiRequest = (input: Omit<FreeCadPrivateNamingAbiRequest, 'schemaVersion'>): FreeCadPrivateNamingAbiRequest => {
if (!input.requestId.trim() || !input.documentId.trim() || !input.operationId.trim() || !input.stageId.trim() || !input.resultObjectId.trim()) throw new TypeError('FreeCAD naming ABI request IDs must be non-empty.')
if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('FreeCAD naming ABI documentVersion must be a non-negative integer.')
if (input.resultObjectTag !== undefined && (!Number.isSafeInteger(input.resultObjectTag) || input.resultObjectTag <= 0)) throw new RangeError('FreeCAD naming ABI resultObjectTag must be a positive safe integer.')
return {
schemaVersion: 1,
...input,
inputs: input.inputs.map((entry) => {
if (!entry.inputId.trim() || !entry.step.trim()) throw new TypeError('FreeCAD naming ABI inputs require non-empty inputId and STEP text.')
return { ...entry }
if (entry.objectId !== undefined && !entry.objectId.trim()) throw new TypeError(`FreeCAD naming ABI input ${entry.inputId} objectId must be non-empty when provided.`)
if (entry.objectTag !== undefined && (!Number.isSafeInteger(entry.objectTag) || entry.objectTag <= 0)) throw new RangeError(`FreeCAD naming ABI input ${entry.inputId} objectTag must be a positive safe integer.`)
return { ...entry, ...(entry.namingEvidence ? { namingEvidence: cloneNativeNamingEvidence(assertNativeNamingEvidence(entry.namingEvidence)) } : {}) }
}),
stages: input.stages.map((entry) => ({ ...entry, inputIds: [...entry.inputIds] })),
...(input.resultStepByStage ? { resultStepByStage: { ...input.resultStepByStage } } : {}),

View File

@@ -1,5 +1,5 @@
import type { AnyElementMap2Document, ElementMap2MappedNameReference } from './elementMap2'
import { migrateStringHasherSchema, validateStringHasherTable, type AnyStringHasherTable } from './stringHasher'
import { cloneElementMap2, migrateElementMap2Schema, type AnyElementMap2Document, type ElementMap2MappedNameReference } from './elementMap2'
import { cloneStringHasherTable, migrateStringHasherSchema, validateStringHasherTable, type AnyStringHasherTable } from './stringHasher'
export type NativeMappedNameRelation = 'preserved' | 'modified' | 'generated' | 'deleted' | 'ambiguous'
@@ -119,6 +119,22 @@ export const assertNativeNamingEvidence = <T extends NativeStageNamingEvidence>(
return evidence
}
export const cloneNativeNamingEvidence = (evidence: NativeStageNamingEvidence): NativeStageNamingEvidence => ({
...evidence,
mappedNames: evidence.mappedNames?.map((mapped) => ({
...mapped,
reference: {
...mapped.reference,
indexedName: mapped.reference.indexedName ? { ...mapped.reference.indexedName } : undefined,
stringIds: mapped.reference.stringIds ? [...mapped.reference.stringIds] : undefined,
},
sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })),
candidates: mapped.candidates?.map((candidate) => ({ ...candidate })),
})),
stringHasher: evidence.stringHasher ? cloneStringHasherTable(evidence.stringHasher) : undefined,
elementMap2: evidence.elementMap2 ? cloneElementMap2(migrateElementMap2Schema(evidence.elementMap2)) : undefined,
})
export const createFinalShapeOnlyNamingEvidence = (stageId: string, resultObjectId: string, reason = 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.'): NativeStageNamingEvidence => ({
schemaVersion: 1,
stageId,

View File

@@ -90,6 +90,13 @@ const initialize = async (requestedPath?: string, migrationFailureVersion?: numb
const saveDocument = (document: DocumentSnapshot) => {
if (!database) throw new Error('Persistence database is not initialized.')
const nativeObjectTags = new Set<number>()
for (const object of document.objects) {
if (object.nativeObjectTag === undefined) continue
if (!Number.isSafeInteger(object.nativeObjectTag) || object.nativeObjectTag <= 0) throw new RangeError(`Object ${object.id} nativeObjectTag must be a positive safe integer.`)
if (nativeObjectTags.has(object.nativeObjectTag)) throw new RangeError(`nativeObjectTag ${object.nativeObjectTag} is assigned to more than one object.`)
nativeObjectTags.add(object.nativeObjectTag)
}
const now = Date.now()
database.exec('BEGIN;')
try {
@@ -98,7 +105,7 @@ const saveDocument = (document: DocumentSnapshot) => {
database.exec({ sql: 'DELETE FROM objects WHERE document_id = ?', bind: [document.id] })
const parentByChild = new Map<string, string>()
for (const item of document.tree) for (const childId of item.children || []) parentByChild.set(childId, item.id)
document.tree.forEach((item, ordinal) => { const object = document.objects.find((candidate) => candidate.id === item.id); database?.exec({ sql: 'INSERT INTO objects(id, document_id, parent_id, label, object_type, state, detail, children_json, ordinal, sketch_json, topology_json) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', bind: [item.id, document.id, parentByChild.get(item.id) || null, item.label, item.type, item.state || null, item.detail || null, JSON.stringify(item.children || []), ordinal, object?.sketch ? JSON.stringify(object.sketch) : null, object?.topology ? JSON.stringify(object.topology) : null] }) })
document.tree.forEach((item, ordinal) => { const object = document.objects.find((candidate) => candidate.id === item.id); database?.exec({ sql: 'INSERT INTO objects(id, document_id, parent_id, label, object_type, state, detail, children_json, ordinal, sketch_json, topology_json, native_object_tag) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', bind: [item.id, document.id, parentByChild.get(item.id) || null, item.label, item.type, item.state || null, item.detail || null, JSON.stringify(item.children || []), ordinal, object?.sketch ? JSON.stringify(object.sketch) : null, object?.topology ? JSON.stringify(object.topology) : null, object?.nativeObjectTag ?? null] }) })
for (const object of document.objects) for (const property of object.properties) database.exec({ sql: 'INSERT INTO object_properties(document_id, object_id, name, value_json, property_type, updated_at) VALUES(?, ?, ?, ?, ?, ?)', bind: [document.id, object.id, property.name, JSON.stringify(property), property.type, now] })
database.exec({ sql: 'DELETE FROM dependencies WHERE document_id = ?', bind: [document.id] })
for (const edge of document.dependencies ?? []) database.exec({ sql: 'INSERT INTO dependencies(document_id, source_id, target_id, relation, property_name, reference) VALUES(?, ?, ?, ?, ?, ?)', bind: [document.id, edge.sourceId, edge.targetId, edge.relation, edge.propertyName ?? null, edge.reference ?? null] })
@@ -132,7 +139,7 @@ const loadDocument = (documentId: string): DocumentSnapshot | null => {
const documents = database.exec({ sql: 'SELECT id, label, version, dirty, read_only, units, recompute_json FROM documents WHERE id = ?', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const row = documents[0]
if (!row) return null
const objects = database.exec({ sql: 'SELECT id, label, object_type, state, detail, children_json, sketch_json, topology_json FROM objects WHERE document_id = ? ORDER BY ordinal', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const objects = database.exec({ sql: 'SELECT id, label, object_type, state, detail, children_json, sketch_json, topology_json, native_object_tag FROM objects WHERE document_id = ? ORDER BY ordinal', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
const propertyRows = database.exec({ sql: 'SELECT object_id, value_json FROM object_properties WHERE document_id = ? ORDER BY object_id, name', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string>>
const dependencyRows = database.exec({ sql: 'SELECT source_id, target_id, relation, property_name, reference FROM dependencies WHERE document_id = ?', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | null>>
const propertiesByObject = new Map<string, ObjectPropertySnapshot[]>()
@@ -142,11 +149,29 @@ const loadDocument = (documentId: string): DocumentSnapshot | null => {
propertiesByObject.set(String(propertyRow.object_id), properties)
}
const tree: ModelTreeItem[] = objects.map((object) => ({ id: String(object.id), label: String(object.label), type: String(object.object_type) as ModelTreeItem['type'], state: object.state ? String(object.state) as ModelTreeItem['state'] : undefined, detail: object.detail ? String(object.detail) : undefined, children: object.children_json ? JSON.parse(String(object.children_json)) as string[] : undefined }))
const usedNativeObjectTags = new Set<number>()
const persistedNativeObjectTags = new Map<string, number>()
for (const object of objects) {
if (object.native_object_tag === null || object.native_object_tag === undefined) continue
const nativeObjectTag = Number(object.native_object_tag)
if (!Number.isSafeInteger(nativeObjectTag) || nativeObjectTag <= 0) throw new Error(`Persisted object ${String(object.id)} has an invalid native object tag.`)
if (usedNativeObjectTags.has(nativeObjectTag)) throw new Error(`Persisted native object tag ${nativeObjectTag} is duplicated.`)
usedNativeObjectTags.add(nativeObjectTag)
persistedNativeObjectTags.set(String(object.id), nativeObjectTag)
}
let nextNativeObjectTag = 1
const objectSnapshots: DocumentObjectSnapshot[] = tree.map((item) => {
const properties = propertiesByObject.get(item.id) ?? []
const typeId = properties.find((property) => property.name === 'TypeId')?.value
const row = objects.find((candidate) => String(candidate.id) === item.id)
return { id: item.id, typeId: typeof typeId === 'string' ? typeId : item.type, properties, sketch: row?.sketch_json ? JSON.parse(String(row.sketch_json)) : undefined, topology: row?.topology_json ? JSON.parse(String(row.topology_json)) : undefined }
let nativeObjectTag = persistedNativeObjectTags.get(item.id)
if (nativeObjectTag === undefined) {
while (usedNativeObjectTags.has(nextNativeObjectTag)) nextNativeObjectTag += 1
nativeObjectTag = nextNativeObjectTag
usedNativeObjectTags.add(nativeObjectTag)
nextNativeObjectTag += 1
}
return { id: item.id, typeId: typeof typeId === 'string' ? typeId : item.type, nativeObjectTag, properties, sketch: row?.sketch_json ? JSON.parse(String(row.sketch_json)) : undefined, topology: row?.topology_json ? JSON.parse(String(row.topology_json)) : undefined }
})
return {
id: String(row.id),

View File

@@ -1,4 +1,4 @@
export const PROJECT_SCHEMA_VERSION = 6
export const PROJECT_SCHEMA_VERSION = 7
export type ProjectSchemaMigration = { version: number; sql: string }
@@ -141,4 +141,5 @@ export const PROJECT_SCHEMA_MIGRATIONS = [
{ version: 4, sql: 'ALTER TABLE objects ADD COLUMN sketch_json TEXT;' },
{ version: 5, sql: 'CREATE TABLE IF NOT EXISTS document_checkpoints (document_id TEXT NOT NULL, version INTEGER NOT NULL, snapshot_json TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (document_id, version), FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE); CREATE INDEX IF NOT EXISTS checkpoints_document_created ON document_checkpoints(document_id, created_at DESC);' },
{ version: 6, sql: 'ALTER TABLE objects ADD COLUMN topology_json TEXT;' },
{ version: 7, sql: 'ALTER TABLE objects ADD COLUMN native_object_tag INTEGER;' },
] as const

View File

@@ -580,8 +580,16 @@ const topologyForObject = async (
documentVersion: context.documentVersion,
operationId,
operation: nativeOperation,
inputs: orderedNativeInputs.map(({ source, shape: inputShape }, index) => ({ objectId: source.id, shape: inputShape, inputId: `${object.id}:input:${index}`, role: index === 0 ? 'object' : 'tool' })),
inputs: orderedNativeInputs.map(({ source, shape: inputShape }, index) => ({
objectId: source.id,
shape: inputShape,
inputId: `${object.id}:input:${index}`,
role: index === 0 ? 'object' : 'tool',
...(source.nativeObjectTag === undefined ? {} : { objectTag: source.nativeObjectTag }),
namingEvidence: source.topology?.history?.namingEvidence?.at(-1),
})),
stages: [{ stageId: `${object.id}:stage:0`, operation: nativeOperation, inputObjectIds: orderedNativeInputs.map(({ source }) => source.id), resultObjectId: object.id, ordinal: 0 }],
...(object.nativeObjectTag === undefined ? {} : { resultObjectTag: object.nativeObjectTag }),
...(nativeOperation === 'fillet' ? { radius: numericProperty('Radius', 1) } : {}),
...(nativeOperation === 'chamfer' ? { distance: numericProperty('Size', numericProperty('Distance', 1)) } : {}),
...(nativeOperation === 'hole' ? {

View File

@@ -108,6 +108,8 @@ export type ObjectPropertySnapshot = {
export type DocumentObjectSnapshot = {
id: string
typeId: string
/** Stable positive App::DocumentObject tag persisted by FreeCAD's Document.xml. */
nativeObjectTag?: number
properties: ObjectPropertySnapshot[]
sketch?: SketchSnapshot
topology?: ObjectTopologySnapshot
@@ -407,8 +409,10 @@ export type NativeTopologyHistoryInput = GeometryDocumentContext & {
ruled?: boolean
offset?: number
joinType?: 'Arc' | 'Intersection'
inputs: Array<{ objectId: string; shape: ShapeHandle; inputId?: string; role?: string; stageId?: string }>
inputs: Array<{ objectId: string; shape: ShapeHandle; inputId?: string; role?: string; stageId?: string; objectTag?: number; namingEvidence?: NativeStageNamingEvidence }>
stages?: NativeTopologyHistoryStage[]
/** Stable FreeCAD owner tag used when encoding the result ElementMap. */
resultObjectTag?: number
result: ShapeHandle
}

View File

@@ -259,8 +259,8 @@ test('viewport box selection distinguishes enclosed window objects from crossing
})
test('project schema is versioned and covers the FreeCAD document graph', () => {
assert.equal(PROJECT_SCHEMA_VERSION, 6)
assert.deepEqual(PROJECT_SCHEMA_MIGRATIONS.map((migration) => migration.version), [1, 2, 3, 4, 5, 6])
assert.equal(PROJECT_SCHEMA_VERSION, 7)
assert.deepEqual(PROJECT_SCHEMA_MIGRATIONS.map((migration) => migration.version), [1, 2, 3, 4, 5, 6, 7])
for (const table of ['projects', 'documents', 'objects', 'object_properties', 'dependencies', 'transactions', 'resources', 'document_checkpoints']) assert.match(PROJECT_SCHEMA_SQL, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`))
assert.match(PROJECT_SCHEMA_SQL, /CREATE INDEX IF NOT EXISTS objects_document_ordinal/)
})
@@ -719,7 +719,7 @@ test('versioned FreeCAD private naming ABI is capability-gated and attaches vali
freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['cut'] }),
freecadNamingEvidenceJson: (requestJson: string) => {
capturedRequest = JSON.parse(requestJson) as Record<string, unknown>
return JSON.stringify({ schemaVersion: 1, stageId: 'cut-native:stage:0', resultObjectId: 'cut-native', status: 'native-evidence', mappedNames: [{ kind: 'edge', resultIndex: 0, resultPersistentId: 'edge-native-0', relation: 'modified', reference: { name: 'Edge1', stringIds: [0x36] }, sourceRefs: [{ objectId: 'object', persistentId: 'Edge1' }] }], stringHasher, elementMap2 })
return JSON.stringify({ schemaVersion: 1, stageId: capturedRequest.stageId, resultObjectId: capturedRequest.resultObjectId, status: 'native-evidence', mappedNames: [{ kind: 'edge', resultIndex: 0, resultPersistentId: 'edge-native-0', relation: 'modified', reference: { name: 'Edge1', stringIds: [0x36] }, sourceRefs: [{ objectId: 'object', persistentId: 'Edge1' }] }], stringHasher, elementMap2 })
},
}
assert.deepEqual(probeFreeCadPrivateNamingAbi(module).availability, 'available')
@@ -731,6 +731,16 @@ test('versioned FreeCAD private naming ABI is capability-gated and attaches vali
assert.equal(capturedRequest?.operation, 'cut')
assert.equal((capturedRequest?.history as { records: unknown[] }).records.length, 1)
const inputEvidence = createFinalShapeOnlyNamingEvidence('source:stage:0', 'source')
await provider.capture({ protocolVersion: 1, requestId: 'naming-tags', documentId: 'doc', documentVersion: 2, operationId: 'cut-native', resultObjectId: 'cut-result', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool', resultObjectTag: 23, inputs: [{ inputId: 'object', objectId: 'base', role: 'object', step: 'ISO-10303-21; object', objectTag: 17, namingEvidence: inputEvidence }, { inputId: 'tool', objectId: 'tool', role: 'tool', step: 'ISO-10303-21; tool', objectTag: 19 }] }, new AbortController().signal)
assert.equal(capturedRequest?.resultObjectId, 'cut-result')
assert.equal(capturedRequest?.resultObjectTag, 23)
assert.equal((capturedRequest?.inputs as Array<{ objectTag?: number; namingEvidence?: { stageId: string } }>)[0].objectTag, 17)
assert.equal((capturedRequest?.inputs as Array<{ objectId?: string }>)[0].objectId, 'base')
assert.equal((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string } }>)[0].namingEvidence?.stageId, 'source:stage:0')
assert.notEqual((capturedRequest?.inputs as Array<{ namingEvidence?: unknown }>)[0].namingEvidence, inputEvidence)
await assert.rejects(() => provider.capture({ protocolVersion: 1, requestId: 'naming-invalid-tag', documentId: 'doc', documentVersion: 2, operationId: 'cut-native', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool', resultObjectTag: 0 }, new AbortController().signal), /positive safe integer/)
const invalid = { ...module, freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: 'wrong', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['cut'] }) }
assert.equal(probeFreeCadPrivateNamingAbi(invalid).availability, 'unavailable')
})
@@ -4367,6 +4377,9 @@ test('FCStd metadata writer rejects ambiguous object identity', () => {
const document = createMockFacade().app.document.getActive()
assert.throws(() => serializeFcstdMetadataArchive({ ...document, objects: [...document.objects, { ...document.objects[0] }] }), /unique object ids/)
assert.throws(() => serializeFcstdMetadataArchive({ ...document, objects: [{ ...document.objects[0], id: ' ' }] }), /non-empty object ids/)
assert.throws(() => serializeFcstdMetadataArchive({ ...document, objects: document.objects.slice(0, 2).map((object) => ({ ...object, nativeObjectTag: 9 })) }), /assigned to more than one object/)
const duplicateNativeTag = zipSync({ 'Document.xml': strToU8('<Document><Objects><Object name="A" type="Part::Feature" id="4"/><Object name="B" type="Part::Feature" id="4"/></Objects></Document>') })
assert.throws(() => inspectFcstdArchive(duplicateNativeTag), /native id 4 is duplicated/)
})
test('FCStd inspection inventories object extensions without instantiating them', () => {
@@ -4379,7 +4392,7 @@ test('FCStd metadata writer round-trips Document and GuiDocument while preservin
const document = {
id: 'fcstd-writer-doc', label: 'Writer fixture', version: 1, dirty: false, readOnly: false, units: 'mm',
tree: [{ id: 'body', label: 'Body', type: 'body' as const, state: 'active' as const }],
objects: [{ id: 'body', typeId: 'PartDesign::Body', properties: [
objects: [{ id: 'body', typeId: 'PartDesign::Body', nativeObjectTag: 37, properties: [
{ name: 'Label', label: 'Label', type: 'App::PropertyString', value: 'Body', expression: 'Spreadsheet.Width * 2', recompute: false },
{ name: 'Length', label: 'Length', type: 'App::PropertyLength', value: 12.5 },
{ name: 'Visible', label: 'Visible', type: 'App::PropertyBool', value: true },
@@ -4391,6 +4404,8 @@ test('FCStd metadata writer round-trips Document and GuiDocument while preservin
const inspection = inspectFcstdArchive(archive)
assert.equal(inspection.label, 'Writer fixture')
assert.equal(inspection.objects[0].typeId, 'PartDesign::Body')
assert.equal(inspection.objects[0].nativeObjectTag, 37)
assert.equal(inspection.proxyDocument.objects[0].nativeObjectTag, 37)
assert.equal(inspection.objects[0].properties[0].value, 'Body')
assert.equal(inspection.objects[0].properties[0].expression, 'Spreadsheet.Width * 2')
assert.equal(inspection.objects[0].properties.find((property) => property.name === 'Length')?.element, 'Float')