feat: complete production naming and reference lifecycle gates
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled

This commit is contained in:
2026-08-14 10:10:30 -04:00
parent f97eae4153
commit f64b78865c
75 changed files with 57290 additions and 15675 deletions

View File

@@ -0,0 +1,692 @@
{
"schemaVersion": 1,
"status": "pass",
"implementation": "freecad-linked",
"productionWorkerLinked": true,
"occtVersion": "8.0.0",
"freecadVersion": "1.1.1",
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"callbacks": [
"freecadNamingAbiVersion",
"freecadNamingCapabilitiesJson",
"freecadNamingEvidenceJson"
],
"operations": [
"fuse",
"cut",
"common",
"rotate",
"pad",
"pocket",
"loft",
"pipe",
"revolution",
"groove",
"fillet",
"chamfer",
"hole",
"draft",
"thickness",
"linear-pattern",
"polar-pattern",
"mirrored",
"multi-transform"
],
"cases": [
{
"operation": "fuse",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 52,
"externallySourcedRecords": 52,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 7,
"ambiguousResults": 16,
"provenanceResults": 23,
"mappedNames": 7,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "cut",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 42,
"externallySourcedRecords": 42,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 9,
"provenanceResults": 12,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "common",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 123,
"externallySourcedRecords": 123,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 5,
"ambiguousResults": 4,
"provenanceResults": 9,
"mappedNames": 5,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "rotate",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 78,
"externallySourcedRecords": 78,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "pad",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 25,
"externallySourcedRecords": 25,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 9,
"ambiguousResults": 8,
"provenanceResults": 17,
"mappedNames": 9,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "pocket",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 42,
"externallySourcedRecords": 42,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 9,
"provenanceResults": 12,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "loft",
"status": "pass",
"namingStatus": "ambiguous",
"callbackExecuted": false,
"historyRecords": 24,
"externallySourcedRecords": 24,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 0,
"ambiguousResults": 7,
"provenanceResults": 7,
"mappedNames": 0,
"stringHasherEntries": 0,
"elementMaps": 0
},
{
"operation": "pipe",
"status": "pass",
"namingStatus": "ambiguous",
"callbackExecuted": false,
"historyRecords": 28,
"externallySourcedRecords": 28,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 4,
"modifiedRelationPrecedence": 0,
"selectedRecords": 0,
"ambiguousResults": 10,
"provenanceResults": 10,
"mappedNames": 0,
"stringHasherEntries": 0,
"elementMaps": 0
},
{
"operation": "revolution",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 23,
"externallySourcedRecords": 23,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 4,
"ambiguousResults": 8,
"provenanceResults": 12,
"mappedNames": 4,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "groove",
"status": "pass",
"namingStatus": "native-evidence",
"callbackExecuted": true,
"historyRecords": 2,
"externallySourcedRecords": 2,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 1,
"ambiguousResults": 0,
"provenanceResults": 1,
"mappedNames": 1,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "fillet",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 150,
"externallySourcedRecords": 150,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "chamfer",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 150,
"externallySourcedRecords": 150,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "hole",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 30,
"externallySourcedRecords": 15,
"internalRecordsExcluded": 15,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 3,
"provenanceResults": 6,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "draft",
"status": "pass",
"namingStatus": "native-evidence",
"callbackExecuted": true,
"historyRecords": 5,
"externallySourcedRecords": 5,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 5,
"ambiguousResults": 0,
"provenanceResults": 5,
"mappedNames": 5,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "thickness",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 38,
"externallySourcedRecords": 38,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 8,
"provenanceResults": 14,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "linear-pattern",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 90,
"externallySourcedRecords": 45,
"internalRecordsExcluded": 45,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 4,
"ambiguousResults": 12,
"provenanceResults": 16,
"mappedNames": 4,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "polar-pattern",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 68,
"externallySourcedRecords": 34,
"internalRecordsExcluded": 34,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 10,
"ambiguousResults": 12,
"provenanceResults": 22,
"mappedNames": 10,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "mirrored",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 66,
"externallySourcedRecords": 33,
"internalRecordsExcluded": 33,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 8,
"ambiguousResults": 12,
"provenanceResults": 20,
"mappedNames": 8,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "multi-transform",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 311,
"externallySourcedRecords": 311,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 8,
"selectedRecords": 22,
"ambiguousResults": 68,
"provenanceResults": 90,
"mappedNames": 22,
"stringHasherEntries": 0,
"elementMaps": 1
}
],
"callbackExecutedCases": 17,
"ambiguityOnlyCases": 2,
"isomorphicCorpus": {
"operation": "common",
"fixture": "coincident-equal-boxes",
"status": "pass",
"historyRecords": 156,
"uniqueResults": 0,
"ambiguousResults": 26,
"candidateSets": [
{
"result": "vertex:0",
"candidates": [
"production-symmetric-object:Vertex17:",
"production-symmetric-object:Vertex19:",
"production-symmetric-object:Vertex1:",
"production-symmetric-object:Vertex33:",
"production-symmetric-object:Vertex35:",
"production-symmetric-object:Vertex3:",
"production-symmetric-tool:Vertex17:",
"production-symmetric-tool:Vertex19:",
"production-symmetric-tool:Vertex1:",
"production-symmetric-tool:Vertex33:",
"production-symmetric-tool:Vertex35:",
"production-symmetric-tool:Vertex3:"
]
},
{
"result": "vertex:1",
"candidates": [
"production-symmetric-object:Vertex20:",
"production-symmetric-object:Vertex21:",
"production-symmetric-object:Vertex2:",
"production-symmetric-object:Vertex41:",
"production-symmetric-object:Vertex43:",
"production-symmetric-object:Vertex7:",
"production-symmetric-tool:Vertex20:",
"production-symmetric-tool:Vertex21:",
"production-symmetric-tool:Vertex2:",
"production-symmetric-tool:Vertex41:",
"production-symmetric-tool:Vertex43:",
"production-symmetric-tool:Vertex7:"
]
},
{
"result": "vertex:3",
"candidates": [
"production-symmetric-object:Vertex25:",
"production-symmetric-object:Vertex27:",
"production-symmetric-object:Vertex34:",
"production-symmetric-object:Vertex39:",
"production-symmetric-object:Vertex4:",
"production-symmetric-object:Vertex5:",
"production-symmetric-tool:Vertex25:",
"production-symmetric-tool:Vertex27:",
"production-symmetric-tool:Vertex34:",
"production-symmetric-tool:Vertex39:",
"production-symmetric-tool:Vertex4:",
"production-symmetric-tool:Vertex5:"
]
},
{
"result": "vertex:5",
"candidates": [
"production-symmetric-object:Vertex28:",
"production-symmetric-object:Vertex29:",
"production-symmetric-object:Vertex42:",
"production-symmetric-object:Vertex47:",
"production-symmetric-object:Vertex6:",
"production-symmetric-object:Vertex8:",
"production-symmetric-tool:Vertex28:",
"production-symmetric-tool:Vertex29:",
"production-symmetric-tool:Vertex42:",
"production-symmetric-tool:Vertex47:",
"production-symmetric-tool:Vertex6:",
"production-symmetric-tool:Vertex8:"
]
},
{
"result": "vertex:8",
"candidates": [
"production-symmetric-object:Vertex11:",
"production-symmetric-object:Vertex18:",
"production-symmetric-object:Vertex23:",
"production-symmetric-object:Vertex36:",
"production-symmetric-object:Vertex37:",
"production-symmetric-object:Vertex9:",
"production-symmetric-tool:Vertex11:",
"production-symmetric-tool:Vertex18:",
"production-symmetric-tool:Vertex23:",
"production-symmetric-tool:Vertex36:",
"production-symmetric-tool:Vertex37:",
"production-symmetric-tool:Vertex9:"
]
},
{
"result": "vertex:9",
"candidates": [
"production-symmetric-object:Vertex10:",
"production-symmetric-object:Vertex15:",
"production-symmetric-object:Vertex22:",
"production-symmetric-object:Vertex24:",
"production-symmetric-object:Vertex44:",
"production-symmetric-object:Vertex45:",
"production-symmetric-tool:Vertex10:",
"production-symmetric-tool:Vertex15:",
"production-symmetric-tool:Vertex22:",
"production-symmetric-tool:Vertex24:",
"production-symmetric-tool:Vertex44:",
"production-symmetric-tool:Vertex45:"
]
},
{
"result": "vertex:11",
"candidates": [
"production-symmetric-object:Vertex12:",
"production-symmetric-object:Vertex13:",
"production-symmetric-object:Vertex26:",
"production-symmetric-object:Vertex31:",
"production-symmetric-object:Vertex38:",
"production-symmetric-object:Vertex40:",
"production-symmetric-tool:Vertex12:",
"production-symmetric-tool:Vertex13:",
"production-symmetric-tool:Vertex26:",
"production-symmetric-tool:Vertex31:",
"production-symmetric-tool:Vertex38:",
"production-symmetric-tool:Vertex40:"
]
},
{
"result": "vertex:13",
"candidates": [
"production-symmetric-object:Vertex14:",
"production-symmetric-object:Vertex16:",
"production-symmetric-object:Vertex30:",
"production-symmetric-object:Vertex32:",
"production-symmetric-object:Vertex46:",
"production-symmetric-object:Vertex48:",
"production-symmetric-tool:Vertex14:",
"production-symmetric-tool:Vertex16:",
"production-symmetric-tool:Vertex30:",
"production-symmetric-tool:Vertex32:",
"production-symmetric-tool:Vertex46:",
"production-symmetric-tool:Vertex48:"
]
},
{
"result": "edge:0",
"candidates": [
"production-symmetric-object:Edge10:",
"production-symmetric-object:Edge1:",
"production-symmetric-tool:Edge10:",
"production-symmetric-tool:Edge1:"
]
},
{
"result": "edge:1",
"candidates": [
"production-symmetric-object:Edge17:",
"production-symmetric-object:Edge2:",
"production-symmetric-tool:Edge17:",
"production-symmetric-tool:Edge2:"
]
},
{
"result": "edge:2",
"candidates": [
"production-symmetric-object:Edge14:",
"production-symmetric-object:Edge3:",
"production-symmetric-tool:Edge14:",
"production-symmetric-tool:Edge3:"
]
},
{
"result": "edge:3",
"candidates": [
"production-symmetric-object:Edge21:",
"production-symmetric-object:Edge4:",
"production-symmetric-tool:Edge21:",
"production-symmetric-tool:Edge4:"
]
},
{
"result": "edge:4",
"candidates": [
"production-symmetric-object:Edge12:",
"production-symmetric-object:Edge5:",
"production-symmetric-tool:Edge12:",
"production-symmetric-tool:Edge5:"
]
},
{
"result": "edge:5",
"candidates": [
"production-symmetric-object:Edge19:",
"production-symmetric-object:Edge6:",
"production-symmetric-tool:Edge19:",
"production-symmetric-tool:Edge6:"
]
},
{
"result": "edge:6",
"candidates": [
"production-symmetric-object:Edge16:",
"production-symmetric-object:Edge7:",
"production-symmetric-tool:Edge16:",
"production-symmetric-tool:Edge7:"
]
},
{
"result": "edge:7",
"candidates": [
"production-symmetric-object:Edge23:",
"production-symmetric-object:Edge8:",
"production-symmetric-tool:Edge23:",
"production-symmetric-tool:Edge8:"
]
},
{
"result": "edge:8",
"candidates": [
"production-symmetric-object:Edge18:",
"production-symmetric-object:Edge9:",
"production-symmetric-tool:Edge18:",
"production-symmetric-tool:Edge9:"
]
},
{
"result": "edge:10",
"candidates": [
"production-symmetric-object:Edge11:",
"production-symmetric-object:Edge22:",
"production-symmetric-tool:Edge11:",
"production-symmetric-tool:Edge22:"
]
},
{
"result": "edge:12",
"candidates": [
"production-symmetric-object:Edge13:",
"production-symmetric-object:Edge20:",
"production-symmetric-tool:Edge13:",
"production-symmetric-tool:Edge20:"
]
},
{
"result": "edge:14",
"candidates": [
"production-symmetric-object:Edge15:",
"production-symmetric-object:Edge24:",
"production-symmetric-tool:Edge15:",
"production-symmetric-tool:Edge24:"
]
},
{
"result": "face:0",
"candidates": [
"production-symmetric-object:Face1:",
"production-symmetric-tool:Face1:"
]
},
{
"result": "face:1",
"candidates": [
"production-symmetric-object:Face2:",
"production-symmetric-tool:Face2:"
]
},
{
"result": "face:2",
"candidates": [
"production-symmetric-object:Face3:",
"production-symmetric-tool:Face3:"
]
},
{
"result": "face:3",
"candidates": [
"production-symmetric-object:Face4:",
"production-symmetric-tool:Face4:"
]
},
{
"result": "face:4",
"candidates": [
"production-symmetric-object:Face5:",
"production-symmetric-tool:Face5:"
]
},
{
"result": "face:5",
"candidates": [
"production-symmetric-object:Face6:",
"production-symmetric-tool:Face6:"
]
}
],
"arrayOrderFallbackUsed": false
},
"chainedStage": {
"mappedNames": 1,
"stringHasherEntries": 3
},
"invalidHistoryRejected": true,
"systemExact": false,
"runtime": "chrome",
"browserId": "chrome",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36",
"crossOriginIsolated": true,
"artifacts": [
{
"name": "bitbybit-occt-history.js",
"bytes": 359739,
"sha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0"
},
{
"name": "bitbybit-occt-history.wasm",
"bytes": 37482285,
"sha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515"
},
{
"name": "bitbybit-occt-history.data",
"bytes": 3322679,
"sha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
}
],
"harness": {
"path": "scripts/freecad-naming-production-matrix.mjs",
"bytes": 19819,
"sha256": "01532d8d3197aacddc0c18d4d8e41b4aee3b88bb4eb6636d8d0457a194651125"
},
"generatedAt": "2026-08-14T05:27:30.061Z"
}

File diff suppressed because it is too large Load Diff

View File

@@ -81,8 +81,10 @@
"artifact": { "artifact": {
"javascript": "native/occt-history/dist/bitbybit-occt-history.js", "javascript": "native/occt-history/dist/bitbybit-occt-history.js",
"wasm": "native/occt-history/dist/bitbybit-occt-history.wasm", "wasm": "native/occt-history/dist/bitbybit-occt-history.wasm",
"javascriptSha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557", "data": "native/occt-history/dist/bitbybit-occt-history.data",
"wasmSha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6" "javascriptSha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0",
"wasmSha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515",
"dataSha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
}, },
"protocol": "native result and history records stay inside the provider; cross-WASM handles are forbidden; STEP text is the explicit transport boundary", "protocol": "native result and history records stay inside the provider; cross-WASM handles are forbidden; STEP text is the explicit transport boundary",
"historyProtocol": { "historyProtocol": {
@@ -97,11 +99,11 @@
"status": "explicit-runtime-contract", "status": "explicit-runtime-contract",
"mappedNameRef": "optional provider evidence; final-shape-only stages are marked and cannot mint FreeCAD tokens", "mappedNameRef": "optional provider evidence; final-shape-only stages are marked and cannot mint FreeCAD tokens",
"stringHasher": "optional opaque native table; validated and preserved losslessly", "stringHasher": "optional opaque native table; validated and preserved losslessly",
"exactBlockers": ["shipped Worker lacks the FreeCAD-linked private naming ABI implementation", "browser builder naming evidence transport", "non-unique isomorphic source"] "exactBlockers": ["exhaustive cross-feature naming and FCStd round-trip corpus", "complete native document and property semantics"]
} }
}, },
"freecadNamingBuild": { "freecadNamingBuild": {
"status": "contract-only", "status": "production-linked",
"prerequisiteStatus": "real-isolated-freecad-private-naming-bridge-pass", "prerequisiteStatus": "real-isolated-freecad-private-naming-bridge-pass",
"sdkReadinessStatus": "candidate-complete", "sdkReadinessStatus": "candidate-complete",
"sdkEnvironment": "FREECAD_WASM_SDK_DIR", "sdkEnvironment": "FREECAD_WASM_SDK_DIR",
@@ -110,7 +112,8 @@
"requiredFreecadVersion": "1.1.1", "requiredFreecadVersion": "1.1.1",
"requiredSourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", "requiredSourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"requiredEmscriptenVersion": "3.1.69", "requiredEmscriptenVersion": "3.1.69",
"buildCommand": "./npmw run build:freecad-naming-worker", "buildCommand": "./npmw run build:occt-history",
"candidateBuildCommand": "./npmw run build:freecad-naming-worker",
"qtCoreBuildCommand": "./npmw run build:qt6-wasm-core", "qtCoreBuildCommand": "./npmw run build:qt6-wasm-core",
"qtFreecadSdkBuildCommand": "./npmw run build:qt6-freecad-wasm", "qtFreecadSdkBuildCommand": "./npmw run build:qt6-freecad-wasm",
"yamlCppBuildCommand": "./npmw run build:yaml-cpp-wasm", "yamlCppBuildCommand": "./npmw run build:yaml-cpp-wasm",
@@ -127,16 +130,19 @@
"candidateWorkerTestCommand": "./npmw run test:freecad-naming-worker-candidate", "candidateWorkerTestCommand": "./npmw run test:freecad-naming-worker-candidate",
"candidateChromeRunCommand": "./npmw run test:chrome-freecad-naming-worker-candidate", "candidateChromeRunCommand": "./npmw run test:chrome-freecad-naming-worker-candidate",
"candidateChromeCheckCommand": "./npmw run check:chrome-freecad-naming-worker-candidate", "candidateChromeCheckCommand": "./npmw run check:chrome-freecad-naming-worker-candidate",
"productionNodeTestCommand": "./npmw run test:freecad-naming-production",
"productionChromeRunCommand": "./npmw run test:chrome-freecad-naming-production",
"productionCheckCommand": "./npmw run check:freecad-naming-production",
"boundaryCheckCommand": "./npmw run check:freecad-private-naming-boundary", "boundaryCheckCommand": "./npmw run check:freecad-private-naming-boundary",
"checkCommand": "./npmw run check:freecad-naming-sdk", "checkCommand": "./npmw run check:freecad-naming-sdk",
"statusBoundary": "Missing SDK or callback probe keeps EX-TSN-02=in_progress and systemExact=false" "statusBoundary": "Production Node and Chrome matrices prove all 19 callbacks/builders for EX-TSN-02; systemExact remains false until the broader exact plan closes"
}, },
"browserWorker": { "browserWorker": {
"entry": "src/facade/nativeHistoryWorkerEntry.ts", "entry": "src/facade/nativeHistoryWorkerEntry.ts",
"client": "src/facade/nativeHistoryWorkerClient.ts", "client": "src/facade/nativeHistoryWorkerClient.ts",
"moduleUrl": "/native/occt-history/bitbybit-occt-history.js", "moduleUrl": "/native/occt-history/bitbybit-occt-history.js",
"runtimeConfiguration": "BitbybitGeometryRuntime.configureNativeHistory(provider, timeoutMs)", "runtimeConfiguration": "BitbybitGeometryRuntime.configureNativeHistory(provider, timeoutMs)",
"artifactPublishCommand": "./npmw run build:occt-history", "artifactPublishCommand": "./npmw run build:occt-history",
"status": "default-application-worker-with-fallback", "status": "default-application-worker-with-fallback",
"browserVerification": "config/occt-browser-verification.json", "browserVerification": "config/occt-browser-verification.json",
"threeWayVerification": "config/freecad-bitbybit-native-threeway.json", "threeWayVerification": "config/freecad-bitbybit-native-threeway.json",
@@ -155,13 +161,13 @@
"verification": { "level": "experimental", "provider": "FreeCADCmd 1.1.1 native headless Part oracle", "operations": ["locked-source-commit", "reproducible-native-build", "declarative-golden-contract", "part-geometry-replay", "numeric-tolerance-comparison", "execution-plan-validation", "source-inventory-validation", "type-property-inventory-validation"] } "verification": { "level": "experimental", "provider": "FreeCADCmd 1.1.1 native headless Part oracle", "operations": ["locked-source-commit", "reproducible-native-build", "declarative-golden-contract", "part-geometry-replay", "numeric-tolerance-comparison", "execution-plan-validation", "source-inventory-validation", "type-property-inventory-validation"] }
}, },
"systemExactEvaluation": { "systemExactEvaluation": {
"evaluatedAt": "2026-08-11", "evaluatedAt": "2026-08-14",
"exact": false, "exact": false,
"featureExactCount": 0, "featureExactCount": 0,
"plan": "config/freecad-web-exact-parity-plan.json", "plan": "config/freecad-web-exact-parity-plan.json",
"promotionTask": "EX-REL-01", "promotionTask": "EX-REL-01",
"taskStatus": { "completed": 2, "inProgress": 37, "pending": 13, "blocked": 0 }, "taskStatus": { "completed": 5, "inProgress": 36, "pending": 11, "blocked": 0 },
"blockers": ["shipped Worker lacks the FreeCAD-linked private naming ABI implementation", "browser builder naming evidence transport", "non-unique isomorphic source"] "blockers": ["exhaustive cross-feature naming and FCStd round-trip corpus", "complete native document and property semantics"]
}, },
"rules": [ "rules": [
"UI status does not imply geometry or file compatibility.", "UI status does not imply geometry or file compatibility.",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,197 @@
{
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-composite-history-elementmap2",
"freecadVersion": "1.1.1",
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"status": "pass",
"cases": [
{
"id": "boolean-fuse-1",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-fuse-2",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-fuse-3",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-cut-1",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-cut-2",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-cut-3",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-common-1",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-common-2",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "boolean-common-3",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-plain",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-midplane",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-reverse",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-taper",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-twoside",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket-through",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket-midplane",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket-twoside",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket-taper",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "partdesign-pocket-up-to-face",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-01",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-02",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-03",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-04",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-05",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "composite-06",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "dressup-fillet-1",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "dressup-fillet-2",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "dressup-chamfer-1",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
},
{
"id": "dressup-chamfer-2",
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCovered": true
}
],
"summary": {
"cases": 30,
"passed": 30,
"failed": 0,
"roundtripNameDrift": 0,
"resaveNameDrift": 0,
"nativeDesktopResaveCases": 30
}
}

View File

@@ -0,0 +1,195 @@
{
"schemaVersion": 1,
"status": "pass",
"runtime": "node",
"implementation": "freecad-linked",
"productionWorkerLinked": true,
"cases": {
"symmetricCommon": {
"status": "ambiguous",
"stable": 0,
"ambiguous": 26,
"candidates": 156,
"stringHasherEntries": 0,
"elementMaps": 0,
"persistedCandidateSets": true
},
"mixedCut": {
"status": "ambiguous",
"stable": 3,
"ambiguous": 9,
"candidates": 18,
"stringHasherEntries": 0,
"elementMaps": 1
},
"downstreamRotate": {
"status": "ambiguous",
"stable": 9,
"ambiguous": 35,
"candidates": 135,
"stringHasherEntries": 3,
"elementMaps": 1,
"candidateObjects": [
"cut-object",
"cut-result",
"cut-tool"
]
},
"longChain": {
"operations": [
"cut",
"rotate",
"fillet",
"mirrored",
"linear-pattern"
],
"stages": [
{
"operation": "cut",
"historyRecords": 42,
"status": "ambiguous",
"stable": 3,
"ambiguous": 9,
"candidates": 18,
"stringHasherEntries": 0,
"elementMaps": 1,
"candidateObjects": [
"cut-object",
"cut-tool"
],
"persistedInputEvidence": false
},
{
"operation": "rotate",
"historyRecords": 135,
"status": "ambiguous",
"stable": 9,
"ambiguous": 35,
"candidates": 135,
"stringHasherEntries": 3,
"elementMaps": 1,
"candidateObjects": [
"cut-object",
"cut-result",
"cut-tool"
],
"referencedInputCandidateObjects": [
"cut-object",
"cut-tool"
],
"referencedCandidateObjectsRetained": true,
"persistedInputEvidence": true
},
{
"operation": "fillet",
"historyRecords": 261,
"status": "ambiguous",
"stable": 9,
"ambiguous": 35,
"candidates": 226,
"stringHasherEntries": 6,
"elementMaps": 1,
"candidateObjects": [
"cut-object",
"cut-result",
"cut-tool",
"rotate-result"
],
"referencedInputCandidateObjects": [
"cut-object",
"cut-result",
"cut-tool"
],
"referencedCandidateObjectsRetained": true,
"persistedInputEvidence": true
},
{
"operation": "mirrored",
"historyRecords": 326,
"status": "ambiguous",
"stable": 11,
"ambiguous": 40,
"candidates": 121,
"stringHasherEntries": 9,
"elementMaps": 1,
"candidateObjects": [
"cut-object",
"cut-result",
"cut-tool",
"fillet-result",
"rotate-result"
],
"referencedInputCandidateObjects": [
"cut-object",
"cut-result",
"cut-tool",
"rotate-result"
],
"referencedCandidateObjectsRetained": true,
"persistedInputEvidence": true
},
{
"operation": "linear-pattern",
"historyRecords": 1090,
"status": "ambiguous",
"stable": 55,
"ambiguous": 89,
"candidates": 211,
"stringHasherEntries": 17,
"elementMaps": 1,
"candidateObjects": [
"cut-result",
"cut-tool",
"fillet-result",
"mirrored-result",
"rotate-result"
],
"referencedInputCandidateObjects": [
"cut-result",
"cut-tool",
"fillet-result",
"rotate-result"
],
"referencedCandidateObjectsRetained": true,
"persistedInputEvidence": true
}
],
"persistedFinalEvidence": true,
"referencedCandidateObjectsRetained": true,
"stringHasherMonotonic": true
}
},
"fcstdAmbiguityRejected": true,
"fcstdStableLinkRoundtrip": {
"subElements": [
"Face1"
],
"inspectRewritePreserved": true,
"nativeDesktopResaveCovered": false
},
"artifacts": [
{
"name": "bitbybit-occt-history.js",
"bytes": 359739,
"sha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0"
},
{
"name": "bitbybit-occt-history.wasm",
"bytes": 37482285,
"sha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515"
},
{
"name": "bitbybit-occt-history.data",
"bytes": 3322679,
"sha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
}
],
"harness": {
"path": "scripts/run-freecad-isomorphic-provenance.ts",
"bytes": 16135,
"sha256": "c61a23201585217309ceb88dcff4f7ea4d966e0d4140090a0ca1992c7d8a44ed"
},
"exactBlocker": "exhaustive cross-feature naming and native FreeCAD FCStd save-reopen-resave corpus",
"systemExact": false,
"generatedAt": "2026-08-14T12:21:09.187Z"
}

View File

@@ -59,11 +59,11 @@
{ {
"id": "PAR-01", "id": "PAR-01",
"title": "Expand Datum/Attachment/ShapeBinder, multi-Body and parameter combinations", "title": "Expand Datum/Attachment/ShapeBinder, multi-Body and parameter combinations",
"status": "in_progress", "status": "completed",
"dependencies": ["SDK-05"], "dependencies": ["SDK-05"],
"deliverables": ["Native oracle corpus", "Edit/recompute/recovery/FCStd round-trip matrix"], "deliverables": ["Native oracle corpus", "Edit/recompute/recovery/FCStd round-trip matrix"],
"acceptance": ["Every family has success and failure evidence", "Support/Tip/references survive mutation without silent rebinding"], "acceptance": ["Every family has success and failure evidence", "Support/Tip/references survive mutation without silent rebinding"],
"evidence": ["check:freecad-sketcher-partdesign-abi", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:chrome-partdesign-lifecycle"] "evidence": ["check:freecad-sketcher-partdesign-abi", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:freecad-attachment-combinations", "check:freecad-xlink-relink", "check:chrome-partdesign-lifecycle", "check:chrome-partdesign-reference-lifecycle"]
}, },
{ {
"id": "QA-01", "id": "QA-01",
@@ -76,12 +76,12 @@
} }
], ],
"boundary": { "boundary": {
"freecadNamingBuildStatus": "contract-only", "freecadNamingBuildStatus": "production-linked",
"exTsn02": "in_progress", "exTsn02": "completed",
"systemExact": false, "systemExact": false,
"productionWorker": { "productionWorker": {
"availability": "unavailable", "availability": "available",
"callbacks": [] "callbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"]
} }
} }
} }

View File

@@ -0,0 +1,689 @@
{
"schemaVersion": 1,
"status": "pass",
"implementation": "freecad-linked",
"productionWorkerLinked": true,
"occtVersion": "8.0.0",
"freecadVersion": "1.1.1",
"sourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"callbacks": [
"freecadNamingAbiVersion",
"freecadNamingCapabilitiesJson",
"freecadNamingEvidenceJson"
],
"operations": [
"fuse",
"cut",
"common",
"rotate",
"pad",
"pocket",
"loft",
"pipe",
"revolution",
"groove",
"fillet",
"chamfer",
"hole",
"draft",
"thickness",
"linear-pattern",
"polar-pattern",
"mirrored",
"multi-transform"
],
"cases": [
{
"operation": "fuse",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 52,
"externallySourcedRecords": 52,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 7,
"ambiguousResults": 16,
"provenanceResults": 23,
"mappedNames": 7,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "cut",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 42,
"externallySourcedRecords": 42,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 9,
"provenanceResults": 12,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "common",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 123,
"externallySourcedRecords": 123,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 5,
"ambiguousResults": 4,
"provenanceResults": 9,
"mappedNames": 5,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "rotate",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 78,
"externallySourcedRecords": 78,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "pad",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 25,
"externallySourcedRecords": 25,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 9,
"ambiguousResults": 8,
"provenanceResults": 17,
"mappedNames": 9,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "pocket",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 42,
"externallySourcedRecords": 42,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 9,
"provenanceResults": 12,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "loft",
"status": "pass",
"namingStatus": "ambiguous",
"callbackExecuted": false,
"historyRecords": 24,
"externallySourcedRecords": 24,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 0,
"ambiguousResults": 7,
"provenanceResults": 7,
"mappedNames": 0,
"stringHasherEntries": 0,
"elementMaps": 0
},
{
"operation": "pipe",
"status": "pass",
"namingStatus": "ambiguous",
"callbackExecuted": false,
"historyRecords": 28,
"externallySourcedRecords": 28,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 4,
"modifiedRelationPrecedence": 0,
"selectedRecords": 0,
"ambiguousResults": 10,
"provenanceResults": 10,
"mappedNames": 0,
"stringHasherEntries": 0,
"elementMaps": 0
},
{
"operation": "revolution",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 23,
"externallySourcedRecords": 23,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 4,
"ambiguousResults": 8,
"provenanceResults": 12,
"mappedNames": 4,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "groove",
"status": "pass",
"namingStatus": "native-evidence",
"callbackExecuted": true,
"historyRecords": 2,
"externallySourcedRecords": 2,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 1,
"ambiguousResults": 0,
"provenanceResults": 1,
"mappedNames": 1,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "fillet",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 150,
"externallySourcedRecords": 150,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "chamfer",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 150,
"externallySourcedRecords": 150,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 20,
"provenanceResults": 26,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "hole",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 30,
"externallySourcedRecords": 15,
"internalRecordsExcluded": 15,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 3,
"ambiguousResults": 3,
"provenanceResults": 6,
"mappedNames": 3,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "draft",
"status": "pass",
"namingStatus": "native-evidence",
"callbackExecuted": true,
"historyRecords": 5,
"externallySourcedRecords": 5,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 5,
"ambiguousResults": 0,
"provenanceResults": 5,
"mappedNames": 5,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "thickness",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 38,
"externallySourcedRecords": 38,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 6,
"ambiguousResults": 8,
"provenanceResults": 14,
"mappedNames": 6,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "linear-pattern",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 90,
"externallySourcedRecords": 45,
"internalRecordsExcluded": 45,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 4,
"ambiguousResults": 12,
"provenanceResults": 16,
"mappedNames": 4,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "polar-pattern",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 68,
"externallySourcedRecords": 34,
"internalRecordsExcluded": 34,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 10,
"ambiguousResults": 12,
"provenanceResults": 22,
"mappedNames": 10,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "mirrored",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 66,
"externallySourcedRecords": 33,
"internalRecordsExcluded": 33,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 0,
"selectedRecords": 8,
"ambiguousResults": 12,
"provenanceResults": 20,
"mappedNames": 8,
"stringHasherEntries": 0,
"elementMaps": 1
},
{
"operation": "multi-transform",
"status": "pass",
"namingStatus": "mixed-native-and-ambiguous",
"callbackExecuted": true,
"historyRecords": 311,
"externallySourcedRecords": 311,
"internalRecordsExcluded": 0,
"duplicateRecordsCollapsed": 0,
"modifiedRelationPrecedence": 8,
"selectedRecords": 22,
"ambiguousResults": 68,
"provenanceResults": 90,
"mappedNames": 22,
"stringHasherEntries": 0,
"elementMaps": 1
}
],
"callbackExecutedCases": 17,
"ambiguityOnlyCases": 2,
"isomorphicCorpus": {
"operation": "common",
"fixture": "coincident-equal-boxes",
"status": "pass",
"historyRecords": 156,
"uniqueResults": 0,
"ambiguousResults": 26,
"candidateSets": [
{
"result": "vertex:0",
"candidates": [
"production-symmetric-object:Vertex17:",
"production-symmetric-object:Vertex19:",
"production-symmetric-object:Vertex1:",
"production-symmetric-object:Vertex33:",
"production-symmetric-object:Vertex35:",
"production-symmetric-object:Vertex3:",
"production-symmetric-tool:Vertex17:",
"production-symmetric-tool:Vertex19:",
"production-symmetric-tool:Vertex1:",
"production-symmetric-tool:Vertex33:",
"production-symmetric-tool:Vertex35:",
"production-symmetric-tool:Vertex3:"
]
},
{
"result": "vertex:1",
"candidates": [
"production-symmetric-object:Vertex20:",
"production-symmetric-object:Vertex21:",
"production-symmetric-object:Vertex2:",
"production-symmetric-object:Vertex41:",
"production-symmetric-object:Vertex43:",
"production-symmetric-object:Vertex7:",
"production-symmetric-tool:Vertex20:",
"production-symmetric-tool:Vertex21:",
"production-symmetric-tool:Vertex2:",
"production-symmetric-tool:Vertex41:",
"production-symmetric-tool:Vertex43:",
"production-symmetric-tool:Vertex7:"
]
},
{
"result": "vertex:3",
"candidates": [
"production-symmetric-object:Vertex25:",
"production-symmetric-object:Vertex27:",
"production-symmetric-object:Vertex34:",
"production-symmetric-object:Vertex39:",
"production-symmetric-object:Vertex4:",
"production-symmetric-object:Vertex5:",
"production-symmetric-tool:Vertex25:",
"production-symmetric-tool:Vertex27:",
"production-symmetric-tool:Vertex34:",
"production-symmetric-tool:Vertex39:",
"production-symmetric-tool:Vertex4:",
"production-symmetric-tool:Vertex5:"
]
},
{
"result": "vertex:5",
"candidates": [
"production-symmetric-object:Vertex28:",
"production-symmetric-object:Vertex29:",
"production-symmetric-object:Vertex42:",
"production-symmetric-object:Vertex47:",
"production-symmetric-object:Vertex6:",
"production-symmetric-object:Vertex8:",
"production-symmetric-tool:Vertex28:",
"production-symmetric-tool:Vertex29:",
"production-symmetric-tool:Vertex42:",
"production-symmetric-tool:Vertex47:",
"production-symmetric-tool:Vertex6:",
"production-symmetric-tool:Vertex8:"
]
},
{
"result": "vertex:8",
"candidates": [
"production-symmetric-object:Vertex11:",
"production-symmetric-object:Vertex18:",
"production-symmetric-object:Vertex23:",
"production-symmetric-object:Vertex36:",
"production-symmetric-object:Vertex37:",
"production-symmetric-object:Vertex9:",
"production-symmetric-tool:Vertex11:",
"production-symmetric-tool:Vertex18:",
"production-symmetric-tool:Vertex23:",
"production-symmetric-tool:Vertex36:",
"production-symmetric-tool:Vertex37:",
"production-symmetric-tool:Vertex9:"
]
},
{
"result": "vertex:9",
"candidates": [
"production-symmetric-object:Vertex10:",
"production-symmetric-object:Vertex15:",
"production-symmetric-object:Vertex22:",
"production-symmetric-object:Vertex24:",
"production-symmetric-object:Vertex44:",
"production-symmetric-object:Vertex45:",
"production-symmetric-tool:Vertex10:",
"production-symmetric-tool:Vertex15:",
"production-symmetric-tool:Vertex22:",
"production-symmetric-tool:Vertex24:",
"production-symmetric-tool:Vertex44:",
"production-symmetric-tool:Vertex45:"
]
},
{
"result": "vertex:11",
"candidates": [
"production-symmetric-object:Vertex12:",
"production-symmetric-object:Vertex13:",
"production-symmetric-object:Vertex26:",
"production-symmetric-object:Vertex31:",
"production-symmetric-object:Vertex38:",
"production-symmetric-object:Vertex40:",
"production-symmetric-tool:Vertex12:",
"production-symmetric-tool:Vertex13:",
"production-symmetric-tool:Vertex26:",
"production-symmetric-tool:Vertex31:",
"production-symmetric-tool:Vertex38:",
"production-symmetric-tool:Vertex40:"
]
},
{
"result": "vertex:13",
"candidates": [
"production-symmetric-object:Vertex14:",
"production-symmetric-object:Vertex16:",
"production-symmetric-object:Vertex30:",
"production-symmetric-object:Vertex32:",
"production-symmetric-object:Vertex46:",
"production-symmetric-object:Vertex48:",
"production-symmetric-tool:Vertex14:",
"production-symmetric-tool:Vertex16:",
"production-symmetric-tool:Vertex30:",
"production-symmetric-tool:Vertex32:",
"production-symmetric-tool:Vertex46:",
"production-symmetric-tool:Vertex48:"
]
},
{
"result": "edge:0",
"candidates": [
"production-symmetric-object:Edge10:",
"production-symmetric-object:Edge1:",
"production-symmetric-tool:Edge10:",
"production-symmetric-tool:Edge1:"
]
},
{
"result": "edge:1",
"candidates": [
"production-symmetric-object:Edge17:",
"production-symmetric-object:Edge2:",
"production-symmetric-tool:Edge17:",
"production-symmetric-tool:Edge2:"
]
},
{
"result": "edge:2",
"candidates": [
"production-symmetric-object:Edge14:",
"production-symmetric-object:Edge3:",
"production-symmetric-tool:Edge14:",
"production-symmetric-tool:Edge3:"
]
},
{
"result": "edge:3",
"candidates": [
"production-symmetric-object:Edge21:",
"production-symmetric-object:Edge4:",
"production-symmetric-tool:Edge21:",
"production-symmetric-tool:Edge4:"
]
},
{
"result": "edge:4",
"candidates": [
"production-symmetric-object:Edge12:",
"production-symmetric-object:Edge5:",
"production-symmetric-tool:Edge12:",
"production-symmetric-tool:Edge5:"
]
},
{
"result": "edge:5",
"candidates": [
"production-symmetric-object:Edge19:",
"production-symmetric-object:Edge6:",
"production-symmetric-tool:Edge19:",
"production-symmetric-tool:Edge6:"
]
},
{
"result": "edge:6",
"candidates": [
"production-symmetric-object:Edge16:",
"production-symmetric-object:Edge7:",
"production-symmetric-tool:Edge16:",
"production-symmetric-tool:Edge7:"
]
},
{
"result": "edge:7",
"candidates": [
"production-symmetric-object:Edge23:",
"production-symmetric-object:Edge8:",
"production-symmetric-tool:Edge23:",
"production-symmetric-tool:Edge8:"
]
},
{
"result": "edge:8",
"candidates": [
"production-symmetric-object:Edge18:",
"production-symmetric-object:Edge9:",
"production-symmetric-tool:Edge18:",
"production-symmetric-tool:Edge9:"
]
},
{
"result": "edge:10",
"candidates": [
"production-symmetric-object:Edge11:",
"production-symmetric-object:Edge22:",
"production-symmetric-tool:Edge11:",
"production-symmetric-tool:Edge22:"
]
},
{
"result": "edge:12",
"candidates": [
"production-symmetric-object:Edge13:",
"production-symmetric-object:Edge20:",
"production-symmetric-tool:Edge13:",
"production-symmetric-tool:Edge20:"
]
},
{
"result": "edge:14",
"candidates": [
"production-symmetric-object:Edge15:",
"production-symmetric-object:Edge24:",
"production-symmetric-tool:Edge15:",
"production-symmetric-tool:Edge24:"
]
},
{
"result": "face:0",
"candidates": [
"production-symmetric-object:Face1:",
"production-symmetric-tool:Face1:"
]
},
{
"result": "face:1",
"candidates": [
"production-symmetric-object:Face2:",
"production-symmetric-tool:Face2:"
]
},
{
"result": "face:2",
"candidates": [
"production-symmetric-object:Face3:",
"production-symmetric-tool:Face3:"
]
},
{
"result": "face:3",
"candidates": [
"production-symmetric-object:Face4:",
"production-symmetric-tool:Face4:"
]
},
{
"result": "face:4",
"candidates": [
"production-symmetric-object:Face5:",
"production-symmetric-tool:Face5:"
]
},
{
"result": "face:5",
"candidates": [
"production-symmetric-object:Face6:",
"production-symmetric-tool:Face6:"
]
}
],
"arrayOrderFallbackUsed": false
},
"chainedStage": {
"mappedNames": 1,
"stringHasherEntries": 3
},
"invalidHistoryRejected": true,
"systemExact": false,
"runtime": "node",
"artifacts": [
{
"name": "bitbybit-occt-history.js",
"bytes": 359739,
"sha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0"
},
{
"name": "bitbybit-occt-history.wasm",
"bytes": 37482285,
"sha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515"
},
{
"name": "bitbybit-occt-history.data",
"bytes": 3322679,
"sha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
}
],
"harness": {
"path": "scripts/freecad-naming-production-matrix.mjs",
"bytes": 19819,
"sha256": "01532d8d3197aacddc0c18d4d8e41b4aee3b88bb4eb6636d8d0457a194651125"
},
"generatedAt": "2026-08-14T12:21:03.731Z"
}

View File

@@ -270,8 +270,8 @@
], ],
"productionPublication": false, "productionPublication": false,
"boundary": { "boundary": {
"freecadNamingBuildStatus": "contract-only", "freecadNamingBuildStatus": "production-linked",
"exTsn02": "in_progress", "exTsn02": "completed",
"systemExact": false "systemExact": false
} }
} }

View File

@@ -0,0 +1,893 @@
{
"schemaVersion": 1,
"status": "pass",
"baseline": {
"freecadVersion": "1.1.1",
"commit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
},
"source": {
"path": ".cache/freecad/reference-desktop.json",
"bytes": 4030859,
"sha256": "995b495cd287cb43bed3d6a7cb69ae1f36d021a331ddf7c418304ece023e911e"
},
"runtime": {
"registeredObjectTypes": 352,
"instantiableObjectTypes": 348,
"unavailableObjectTypes": 4,
"unavailableObjects": [
{
"typeId": "Fem::DocumentObject",
"error": "No document object found of type 'Fem::DocumentObject'"
},
{
"typeId": "Part::Boolean",
"error": "No document object found of type 'Part::Boolean'"
},
{
"typeId": "Part::Datum",
"error": "No document object found of type 'Part::Datum'"
},
{
"typeId": "Part::Primitive",
"error": "No document object found of type 'Part::Primitive'"
}
],
"propertyRecords": 5510,
"propertyTypes": 85
},
"supportSummary": {
"native-editable-codec": {
"typeCount": 18,
"recordCount": 4135
},
"native-specialized-codec": {
"typeCount": 5,
"recordCount": 683
},
"opaque-fcstd-proxy": {
"typeCount": 62,
"recordCount": 692
}
},
"types": [
{
"typeId": "App::PropertyAcceleration",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyAngle",
"support": "native-editable-codec",
"recordCount": 135,
"objectTypeCount": 78,
"statusNames": [
"Hidden",
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyArea",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": [
"PropOutput",
"PropReadOnly"
]
},
{
"typeId": "App::PropertyBool",
"support": "native-editable-codec",
"recordCount": 1018,
"objectTypeCount": 348,
"statusNames": [
"Hidden",
"LockDynamic",
"NoModify",
"Output",
"PartialTrigger",
"PropHidden",
"PropNoPersist",
"PropOutput",
"PropReadOnly",
"PropTransient",
"ReadOnly"
]
},
{
"typeId": "App::PropertyBoolList",
"support": "opaque-fcstd-proxy",
"recordCount": 7,
"objectTypeCount": 7,
"statusNames": [
"Hidden",
"Immutable",
"LockDynamic"
]
},
{
"typeId": "App::PropertyColor",
"support": "opaque-fcstd-proxy",
"recordCount": 8,
"objectTypeCount": 8,
"statusNames": []
},
{
"typeId": "App::PropertyColorList",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyDirection",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyDistance",
"support": "native-editable-codec",
"recordCount": 166,
"objectTypeCount": 56,
"statusNames": [
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyEnumeration",
"support": "native-editable-codec",
"recordCount": 304,
"objectTypeCount": 150,
"statusNames": [
"Hidden",
"LockDynamic",
"PropHidden",
"PropOutput",
"ReadOnly"
]
},
{
"typeId": "App::PropertyExpressionEngine",
"support": "native-specialized-codec",
"recordCount": 348,
"objectTypeCount": 348,
"statusNames": [
"PropHidden"
]
},
{
"typeId": "App::PropertyFile",
"support": "opaque-fcstd-proxy",
"recordCount": 16,
"objectTypeCount": 12,
"statusNames": []
},
{
"typeId": "App::PropertyFileIncluded",
"support": "opaque-fcstd-proxy",
"recordCount": 20,
"objectTypeCount": 15,
"statusNames": [
"PropOutput",
"ReadOnly"
]
},
{
"typeId": "App::PropertyFloat",
"support": "native-editable-codec",
"recordCount": 178,
"objectTypeCount": 120,
"statusNames": [
"Hidden",
"LockDynamic",
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyFloatConstraint",
"support": "opaque-fcstd-proxy",
"recordCount": 80,
"objectTypeCount": 64,
"statusNames": [
"Hidden",
"ReadOnly"
]
},
{
"typeId": "App::PropertyFloatList",
"support": "native-editable-codec",
"recordCount": 14,
"objectTypeCount": 9,
"statusNames": [
"Output",
"ReadOnly"
]
},
{
"typeId": "App::PropertyFont",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyForce",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 2,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "App::PropertyHeatFlux",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyInteger",
"support": "native-editable-codec",
"recordCount": 88,
"objectTypeCount": 47,
"statusNames": [
"PropHidden",
"PropNoRecompute",
"PropOutput",
"PropReadOnly",
"PropTransient",
"ReadOnly"
]
},
{
"typeId": "App::PropertyIntegerConstraint",
"support": "native-editable-codec",
"recordCount": 28,
"objectTypeCount": 24,
"statusNames": [
"LockDynamic"
]
},
{
"typeId": "App::PropertyIntegerList",
"support": "native-editable-codec",
"recordCount": 9,
"objectTypeCount": 7,
"statusNames": [
"PropHidden",
"ReadOnly"
]
},
{
"typeId": "App::PropertyIntegerSet",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyLength",
"support": "native-editable-codec",
"recordCount": 164,
"objectTypeCount": 66,
"statusNames": [
"PropOutput",
"ReadOnly"
]
},
{
"typeId": "App::PropertyLink",
"support": "native-editable-codec",
"recordCount": 174,
"objectTypeCount": 152,
"statusNames": [
"Hidden",
"LockDynamic",
"PropHidden"
]
},
{
"typeId": "App::PropertyLinkHidden",
"support": "opaque-fcstd-proxy",
"recordCount": 63,
"objectTypeCount": 63,
"statusNames": [
"PropHidden",
"PropOutput",
"PropReadOnly",
"PropTransient"
]
},
{
"typeId": "App::PropertyLinkList",
"support": "native-editable-codec",
"recordCount": 81,
"objectTypeCount": 68,
"statusNames": [
"Hidden",
"Immutable",
"LockDynamic",
"PropHidden",
"PropNoPersist",
"PropOutput",
"PropReadOnly",
"Transient"
]
},
{
"typeId": "App::PropertyLinkListHidden",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": [
"PropHidden"
]
},
{
"typeId": "App::PropertyLinkSub",
"support": "native-editable-codec",
"recordCount": 105,
"objectTypeCount": 51,
"statusNames": [
"PropHidden",
"PropReadOnly"
]
},
{
"typeId": "App::PropertyLinkSubHidden",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 4,
"statusNames": [
"LockDynamic",
"PropHidden"
]
},
{
"typeId": "App::PropertyLinkSubList",
"support": "native-editable-codec",
"recordCount": 134,
"objectTypeCount": 101,
"statusNames": [
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyLinkSubListGlobal",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyMap",
"support": "opaque-fcstd-proxy",
"recordCount": 9,
"objectTypeCount": 9,
"statusNames": []
},
{
"typeId": "App::PropertyMaterial",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyMaterialList",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "App::PropertyMatrix",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 4,
"statusNames": []
},
{
"typeId": "App::PropertyMoment",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 1,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "App::PropertyPath",
"support": "opaque-fcstd-proxy",
"recordCount": 8,
"objectTypeCount": 8,
"statusNames": [
"PropHidden",
"PropOutput",
"PropTransient"
]
},
{
"typeId": "App::PropertyPlacement",
"support": "native-editable-codec",
"recordCount": 311,
"objectTypeCount": 241,
"statusNames": [
"Hidden",
"LockDynamic",
"PropNoRecompute",
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyPlacementList",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": [
"LockDynamic"
]
},
{
"typeId": "App::PropertyPosition",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 3,
"statusNames": [
"PropOutput",
"PropReadOnly"
]
},
{
"typeId": "App::PropertyPower",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyPrecision",
"support": "opaque-fcstd-proxy",
"recordCount": 14,
"objectTypeCount": 6,
"statusNames": []
},
{
"typeId": "App::PropertyPressure",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyPythonObject",
"support": "opaque-fcstd-proxy",
"recordCount": 55,
"objectTypeCount": 53,
"statusNames": [
"PropHidden",
"PropOutput",
"PropReadOnly",
"PropTransient"
]
},
{
"typeId": "App::PropertyQuantity",
"support": "opaque-fcstd-proxy",
"recordCount": 5,
"objectTypeCount": 3,
"statusNames": []
},
{
"typeId": "App::PropertyQuantityConstraint",
"support": "opaque-fcstd-proxy",
"recordCount": 12,
"objectTypeCount": 7,
"statusNames": [
"PropOutput",
"ReadOnly"
]
},
{
"typeId": "App::PropertyRotation",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "App::PropertySpeed",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyStiffness",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyStiffnessDensity",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyString",
"support": "native-editable-codec",
"recordCount": 1094,
"objectTypeCount": 348,
"statusNames": [
"Hidden",
"LockDynamic",
"Output",
"PropHidden",
"PropOutput",
"PropReadOnly",
"PropTransient",
"ReadOnly"
]
},
{
"typeId": "App::PropertyStringList",
"support": "native-editable-codec",
"recordCount": 25,
"objectTypeCount": 23,
"statusNames": [
"Hidden",
"PropOutput",
"PropReadOnly",
"PropTransient"
]
},
{
"typeId": "App::PropertyTemperature",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 3,
"statusNames": []
},
{
"typeId": "App::PropertyThermalTransferCoefficient",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "App::PropertyUUID",
"support": "opaque-fcstd-proxy",
"recordCount": 5,
"objectTypeCount": 5,
"statusNames": []
},
{
"typeId": "App::PropertyVector",
"support": "native-editable-codec",
"recordCount": 107,
"objectTypeCount": 60,
"statusNames": [
"LockDynamic",
"PropHidden",
"PropOutput",
"PropReadOnly",
"ReadOnly"
]
},
{
"typeId": "App::PropertyVectorDistance",
"support": "opaque-fcstd-proxy",
"recordCount": 7,
"objectTypeCount": 6,
"statusNames": []
},
{
"typeId": "App::PropertyVectorList",
"support": "opaque-fcstd-proxy",
"recordCount": 46,
"objectTypeCount": 28,
"statusNames": [
"LockDynamic",
"PropHidden",
"PropOutput",
"PropReadOnly"
]
},
{
"typeId": "App::PropertyXLink",
"support": "opaque-fcstd-proxy",
"recordCount": 11,
"objectTypeCount": 7,
"statusNames": [
"Hidden",
"LockDynamic",
"PropHidden"
]
},
{
"typeId": "App::PropertyXLinkList",
"support": "opaque-fcstd-proxy",
"recordCount": 11,
"objectTypeCount": 11,
"statusNames": []
},
{
"typeId": "App::PropertyXLinkSub",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": [
"PropHidden",
"PropNoPersist",
"PropReadOnly"
]
},
{
"typeId": "App::PropertyXLinkSubList",
"support": "opaque-fcstd-proxy",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": [
"ReadOnly"
]
},
{
"typeId": "Fem::PropertyFemMesh",
"support": "opaque-fcstd-proxy",
"recordCount": 6,
"objectTypeCount": 6,
"statusNames": [
"PropNoRecompute"
]
},
{
"typeId": "Fem::PropertyPostDataObject",
"support": "opaque-fcstd-proxy",
"recordCount": 13,
"objectTypeCount": 13,
"statusNames": []
},
{
"typeId": "Inspection::PropertyDistanceList",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "Materials::PropertyMaterial",
"support": "opaque-fcstd-proxy",
"recordCount": 142,
"objectTypeCount": 142,
"statusNames": []
},
{
"typeId": "Mesh::PropertyCurvatureList",
"support": "opaque-fcstd-proxy",
"recordCount": 1,
"objectTypeCount": 1,
"statusNames": []
},
{
"typeId": "Mesh::PropertyMeshKernel",
"support": "opaque-fcstd-proxy",
"recordCount": 25,
"objectTypeCount": 25,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "Part::PropertyFilletEdges",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 3,
"statusNames": []
},
{
"typeId": "Part::PropertyGeometryList",
"support": "native-specialized-codec",
"recordCount": 5,
"objectTypeCount": 3,
"statusNames": [
"Ordered",
"PropHidden"
]
},
{
"typeId": "Part::PropertyPartShape",
"support": "native-specialized-codec",
"recordCount": 322,
"objectTypeCount": 142,
"statusNames": [
"Hidden",
"Output",
"Transient"
]
},
{
"typeId": "Part::PropertyShapeHistory",
"support": "opaque-fcstd-proxy",
"recordCount": 6,
"objectTypeCount": 6,
"statusNames": [
"PropHidden",
"PropOutput",
"PropTransient"
]
},
{
"typeId": "Part::PropertyTopoShapeList",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 3,
"statusNames": [
"Ordered"
]
},
{
"typeId": "Path::PropertyPath",
"support": "native-specialized-codec",
"recordCount": 6,
"objectTypeCount": 6,
"statusNames": []
},
{
"typeId": "Points::PropertyPointKernel",
"support": "opaque-fcstd-proxy",
"recordCount": 5,
"objectTypeCount": 5,
"statusNames": []
},
{
"typeId": "Robot::PropertyTrajectory",
"support": "opaque-fcstd-proxy",
"recordCount": 4,
"objectTypeCount": 4,
"statusNames": []
},
{
"typeId": "Sketcher::PropertyConstraintList",
"support": "native-specialized-codec",
"recordCount": 2,
"objectTypeCount": 2,
"statusNames": []
},
{
"typeId": "Spreadsheet::PropertyColumnWidths",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 3,
"statusNames": [
"PropHidden",
"PropOutput",
"PropReadOnly"
]
},
{
"typeId": "Spreadsheet::PropertyRowHeights",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 3,
"statusNames": [
"PropHidden",
"PropOutput",
"PropReadOnly"
]
},
{
"typeId": "Spreadsheet::PropertySheet",
"support": "opaque-fcstd-proxy",
"recordCount": 3,
"objectTypeCount": 3,
"statusNames": [
"PropHidden"
]
},
{
"typeId": "TechDraw::PropertyCenterLineList",
"support": "opaque-fcstd-proxy",
"recordCount": 10,
"objectTypeCount": 10,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "TechDraw::PropertyCosmeticEdgeList",
"support": "opaque-fcstd-proxy",
"recordCount": 10,
"objectTypeCount": 10,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "TechDraw::PropertyCosmeticVertexList",
"support": "opaque-fcstd-proxy",
"recordCount": 10,
"objectTypeCount": 10,
"statusNames": [
"PropOutput"
]
},
{
"typeId": "TechDraw::PropertyGeomFormatList",
"support": "opaque-fcstd-proxy",
"recordCount": 10,
"objectTypeCount": 10,
"statusNames": [
"PropOutput"
]
}
],
"propertyStatus": {
"observed": [
"Hidden",
"Immutable",
"LockDynamic",
"NoModify",
"Ordered",
"Output",
"PartialTrigger",
"PropHidden",
"PropNoPersist",
"PropNoRecompute",
"PropOutput",
"PropReadOnly",
"PropTransient",
"ReadOnly",
"Transient"
],
"facadeNative": [
"Hidden",
"ReadOnly"
],
"proxyOnly": [
"Immutable",
"LockDynamic",
"NoModify",
"Ordered",
"Output",
"PartialTrigger",
"PropHidden",
"PropNoPersist",
"PropNoRecompute",
"PropOutput",
"PropReadOnly",
"PropTransient",
"Transient"
],
"proxyOnlyRecordCount": 2639,
"unknownNumericBits": []
},
"harness": {
"path": "scripts/run-freecad-native-property-semantics.mjs",
"bytes": 5658,
"sha256": "35eb62dc7988ac5f373fb25d1558ab830ecb42b261009fbd79638ee428800331"
},
"exactPromotionReady": false,
"exactBlocker": "complete native document and property semantics",
"generatedAt": "2026-08-14T12:20:59.613Z"
}

View File

@@ -64,14 +64,17 @@
"hostAdapterBound": true, "hostAdapterBound": true,
"productionEligible": false "productionEligible": false
}, },
"notLinkedProductionComponents": [ "productionLinkedComponents": [
"FreeCADBase static library", "FreeCADBase static library",
"FreeCADApp static library", "FreeCADApp static library",
"Part static library", "Part static library",
"Python static library", "Python static library",
"real Application and Document integration",
"FreeCAD private naming Worker bridge" "FreeCAD private naming Worker bridge"
], ],
"remainingExactComponents": [
"complete Application and Document parity",
"exhaustive FCStd edit and round-trip corpus"
],
"candidateAbi": { "candidateAbi": {
"scope": "isolated-non-production", "scope": "isolated-non-production",
"exports": [ "exports": [
@@ -90,8 +93,12 @@
}, },
"production": { "production": {
"workerArtifact": "native/occt-history/dist/bitbybit-occt-history.wasm", "workerArtifact": "native/occt-history/dist/bitbybit-occt-history.wasm",
"workerLinked": false, "workerLinked": true,
"exportedCallbacks": [], "exportedCallbacks": [
"freecadNamingAbiVersion",
"freecadNamingCapabilitiesJson",
"freecadNamingEvidenceJson"
],
"requiredCallbacks": [ "requiredCallbacks": [
"freecadNamingAbiVersion", "freecadNamingAbiVersion",
"freecadNamingCapabilitiesJson", "freecadNamingCapabilitiesJson",
@@ -99,8 +106,8 @@
] ]
}, },
"boundary": { "boundary": {
"freecadNamingBuildStatus": "contract-only", "freecadNamingBuildStatus": "production-linked",
"exTsn02": "in_progress", "exTsn02": "completed",
"systemExact": false "systemExact": false
} }
} }

View File

@@ -7,7 +7,7 @@
"claim": { "claim": {
"scope": "supported-web-facade", "scope": "supported-web-facade",
"exactFreeCadParity": false, "exactFreeCadParity": false,
"reason": "The contract exhausts the Sketcher serialization surface and supported PartDesign facade families; the shipped OCCT-only Worker is not linked with FreeCAD private naming code." "reason": "The contract exhausts the Sketcher serialization surface and supported PartDesign facade families, and the production OCCT Worker links the locked FreeCAD private naming ABI; broader native oracle and round-trip gates remain open."
}, },
"sketcher": { "sketcher": {
"geometryTypes": [ "geometryTypes": [
@@ -92,7 +92,7 @@
"ElementMap2" "ElementMap2"
], ],
"runtimeProfile": "freecad-private-v1-optional-worker", "runtimeProfile": "freecad-private-v1-optional-worker",
"shippedWorkerImplementation": "not-linked", "shippedWorkerImplementation": "freecad-linked",
"fallback": "final-shape-only", "fallback": "final-shape-only",
"syntheticTokenGeneration": "forbidden" "syntheticTokenGeneration": "forbidden"
} }

View File

@@ -103,10 +103,10 @@
"The upstream FreeCAD CMake project currently creates shared libraries and requires a full wasm dependency graph; this plan does not treat a host build as a wasm SDK." "The upstream FreeCAD CMake project currently creates shared libraries and requires a full wasm dependency graph; this plan does not treat a host build as a wasm SDK."
], ],
"boundary": { "boundary": {
"freecadNamingBuildStatus": "contract-only", "freecadNamingBuildStatus": "production-linked",
"exTsn02": "in_progress", "exTsn02": "completed",
"systemExact": false, "systemExact": false,
"workerLinked": false, "workerLinked": true,
"callbacks": [] "callbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"]
} }
} }

View File

@@ -32,16 +32,16 @@
{ "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-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-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-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", "check:freecad-naming-sdk", "check:freecad-private-naming-boundary", "check:occt-history-artifact"], "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": "completed", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "check:freecad-naming-sdk", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-private-naming-boundary", "check:occt-history-artifact"], "exactBlockedBy": [] },
{ "id": "EX-TSN-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "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-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "completed", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "test:facade", "check:freecad-isomorphic-provenance", "check:freecad-naming-production", "check:chrome-freecad-naming-production", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "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"] } { "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-isomorphic-provenance", "check:freecad-composite-history-elementmap", "check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["The 30-case native save-reopen-resave corpus now has zero mapped-name drift, but the exhaustive cross-feature transition matrix and production-chain desktop resave correlation remain incomplete"] }
] ]
}, },
{ {
"id": "EX03", "id": "EX03",
"title": "Application document, property and recompute semantics", "title": "Application document, property and recompute semantics",
"tasks": [ "tasks": [
{ "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["The runtime property inventory has unpromoted static candidates"] }, { "id": "EX-DOC-01", "title": "Implement every native property type, flag, editor and serialization rule", "priority": "P0", "status": "in_progress", "dependencies": ["EX-ORA-01"], "deliverables": ["Complete Property schema", "Status and editor metadata", "Binary and XML codecs"], "acceptance": ["Every runtime property fixture round-trips without type coercion or unknown loss"], "evidence": ["check:freecad-type-properties", "check:freecad-native-property-semantics", "check:freecad-fcstd-roundtrip"], "exactBlockedBy": ["Of 85 runtime property types and 5510 records, 62 types/692 records remain opaque-only and 2639 records use status semantics not represented natively by the Facade"] },
{ "id": "EX-DOC-02", "title": "Match transactions, DAG recompute, partial failure and document observers", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01", "EX-TSN-04"], "deliverables": ["Transaction and observer event oracle", "Partial recompute scheduler", "Failure recovery contract"], "acceptance": ["Undo, redo, abort, stale and cyclic dependency behavior matches FreeCAD"], "evidence": ["check:document-closure", "test:facade"], "exactBlockedBy": ["Observer ordering and every partial-recompute branch are not yet oracle-backed"] }, { "id": "EX-DOC-02", "title": "Match transactions, DAG recompute, partial failure and document observers", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01", "EX-TSN-04"], "deliverables": ["Transaction and observer event oracle", "Partial recompute scheduler", "Failure recovery contract"], "acceptance": ["Undo, redo, abort, stale and cyclic dependency behavior matches FreeCAD"], "evidence": ["check:document-closure", "test:facade"], "exactBlockedBy": ["Observer ordering and every partial-recompute branch are not yet oracle-backed"] },
{ "id": "EX-DOC-03", "title": "Complete expressions, links, extensions, groups and App object lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01"], "deliverables": ["Expression dependency grammar", "Link and extension lifecycle", "Group and clone semantics"], "acceptance": ["Copy, clone, delete, relink and expression edits survive native round-trip"], "evidence": ["check:fcstd-closure", "check:chrome-production-document"], "exactBlockedBy": ["FeaturePython-backed extensions remain non-executable"] }, { "id": "EX-DOC-03", "title": "Complete expressions, links, extensions, groups and App object lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-01"], "deliverables": ["Expression dependency grammar", "Link and extension lifecycle", "Group and clone semantics"], "acceptance": ["Copy, clone, delete, relink and expression edits survive native round-trip"], "evidence": ["check:fcstd-closure", "check:chrome-production-document"], "exactBlockedBy": ["FeaturePython-backed extensions remain non-executable"] },
{ "id": "EX-DOC-04", "title": "Complete multi-document, external link, merge and recovery workflows", "priority": "P1", "status": "pending", "dependencies": ["EX-DOC-02", "EX-DOC-03"], "deliverables": ["Cross-document Link corpus", "Merge and conflict semantics", "Crash and autosave recovery"], "acceptance": ["Open, close, relink, merge and recovery match the native document graph"], "evidence": ["check:chrome-persistence", "check:project-migration"], "exactBlockedBy": ["Current application is project-first and does not implement every desktop multi-document workflow"] }, { "id": "EX-DOC-04", "title": "Complete multi-document, external link, merge and recovery workflows", "priority": "P1", "status": "pending", "dependencies": ["EX-DOC-02", "EX-DOC-03"], "deliverables": ["Cross-document Link corpus", "Merge and conflict semantics", "Crash and autosave recovery"], "acceptance": ["Open, close, relink, merge and recovery match the native document graph"], "evidence": ["check:chrome-persistence", "check:project-migration"], "exactBlockedBy": ["Current application is project-first and does not implement every desktop multi-document workflow"] },
@@ -56,14 +56,14 @@
{ "id": "EX-SK-02", "title": "Match Sketcher editing tools, autoconstraints, virtual space and UI lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-SK-01", "EX-UI-03"], "deliverables": ["Pointer and keyboard editor oracle", "All editing tools", "Task and focus lifecycle"], "acceptance": ["Every tool supports success, failure, cancel, undo and redo with matching selection"], "evidence": ["check:freecad-sketcher-editor", "check:chrome-sketcher-bspline"], "exactBlockedBy": ["The full GUI tool and focus matrix is incomplete"] }, { "id": "EX-SK-02", "title": "Match Sketcher editing tools, autoconstraints, virtual space and UI lifecycle", "priority": "P0", "status": "in_progress", "dependencies": ["EX-SK-01", "EX-UI-03"], "deliverables": ["Pointer and keyboard editor oracle", "All editing tools", "Task and focus lifecycle"], "acceptance": ["Every tool supports success, failure, cancel, undo and redo with matching selection"], "evidence": ["check:freecad-sketcher-editor", "check:chrome-sketcher-bspline"], "exactBlockedBy": ["The full GUI tool and focus matrix is incomplete"] },
{ "id": "EX-PART-01", "title": "Complete all Part primitives, builders, booleans, healing and inspection", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01", "EX-TSN-04"], "deliverables": ["All Part commands and parameters", "Healing and tolerance tools", "Native history for local operations"], "acceptance": ["Success and failure outputs match native Shape, history and diagnostics"], "evidence": ["check:chrome-part-primitives", "check:freecad-golden-fixtures"], "exactBlockedBy": ["Current golden corpus does not enumerate every Part command and option"] }, { "id": "EX-PART-01", "title": "Complete all Part primitives, builders, booleans, healing and inspection", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01", "EX-TSN-04"], "deliverables": ["All Part commands and parameters", "Healing and tolerance tools", "Native history for local operations"], "acceptance": ["Success and failure outputs match native Shape, history and diagnostics"], "evidence": ["check:chrome-part-primitives", "check:freecad-golden-fixtures"], "exactBlockedBy": ["Current golden corpus does not enumerate every Part command and option"] },
{ "id": "EX-PD-01", "title": "Complete all PartDesign features, parameters and additive/subtractive combinations", "priority": "P0", "status": "in_progress", "dependencies": ["EX-PART-01", "EX-TSN-04"], "deliverables": ["Complete PartDesign feature matrix", "Body Tip and feature-list semantics", "Per-feature native history and naming"], "acceptance": ["Every feature edit, failure recovery and save-reopen matches FreeCAD"], "evidence": ["check:partdesign-closure", "check:freecad-sketcher-partdesign-abi", "check:chrome-partdesign-lifecycle"], "exactBlockedBy": ["The parameter contract covers the supported Facade subset, not every registered PartDesign TypeId or cross-product"] }, { "id": "EX-PD-01", "title": "Complete all PartDesign features, parameters and additive/subtractive combinations", "priority": "P0", "status": "in_progress", "dependencies": ["EX-PART-01", "EX-TSN-04"], "deliverables": ["Complete PartDesign feature matrix", "Body Tip and feature-list semantics", "Per-feature native history and naming"], "acceptance": ["Every feature edit, failure recovery and save-reopen matches FreeCAD"], "evidence": ["check:partdesign-closure", "check:freecad-sketcher-partdesign-abi", "check:chrome-partdesign-lifecycle"], "exactBlockedBy": ["The parameter contract covers the supported Facade subset, not every registered PartDesign TypeId or cross-product"] },
{ "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip", "check:freecad-partdesign-structure", "check:freecad-attachment-modes"], "exactBlockedBy": ["Native same-document, first cross-document SubShapeBinder cases, and the 55-mode registry/engine partition pass; executable geometry for every implemented mode/reference combination, other XLink modes, relink recovery and Web Chrome replay remain incomplete"] } { "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "completed", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:chrome-partdesign-reference-lifecycle", "check:freecad-fcstd-roundtrip", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:freecad-attachment-combinations", "check:freecad-xlink-relink"], "exactBlockedBy": [] }
] ]
}, },
{ {
"id": "EX05", "id": "EX05",
"title": "FCStd and all import/export entrypoints", "title": "FCStd and all import/export entrypoints",
"tasks": [ "tasks": [
{ "id": "EX-FC-01", "title": "Round-trip every native FCStd object, property, extension and resource", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-04", "EX-TSN-04"], "deliverables": ["Complete native archive model", "Unknown object preservation", "FreeCAD-Web-FreeCAD mutation corpus"], "acceptance": ["No supported object, property, GUI state or resource drifts after mutation and resave"], "evidence": ["check:fcstd-closure", "check:chrome-fcstd-roundtrip"], "exactBlockedBy": ["FCStd currently supports a verified subset plus read-only proxies"] }, { "id": "EX-FC-01", "title": "Round-trip every native FCStd object, property, extension and resource", "priority": "P0", "status": "in_progress", "dependencies": ["EX-DOC-04", "EX-TSN-04"], "deliverables": ["Complete native archive model", "Unknown object preservation", "FreeCAD-Web-FreeCAD mutation corpus"], "acceptance": ["No supported object, property, GUI state or resource drifts after mutation and resave"], "evidence": ["check:freecad-native-property-semantics", "check:freecad-isomorphic-provenance", "check:fcstd-closure", "check:chrome-fcstd-roundtrip"], "exactBlockedBy": ["FCStd supports 23 native property codecs plus opaque preservation for 62 runtime types; exhaustive native mutation and resave remains open"] },
{ "id": "EX-FMT-01", "title": "Close BREP, STEP and IGES import/export semantics", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01"], "deliverables": ["Assembly/color/name import", "Unit and tolerance handling", "Deterministic exports"], "acceptance": ["Native and Web topology, metadata and diagnostics match for each format"] , "evidence": ["check:chrome-secondary-formats", "check:browser-occt"], "exactBlockedBy": ["Advanced STEP/IGES metadata and assembly cases are incomplete"] }, { "id": "EX-FMT-01", "title": "Close BREP, STEP and IGES import/export semantics", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01"], "deliverables": ["Assembly/color/name import", "Unit and tolerance handling", "Deterministic exports"], "acceptance": ["Native and Web topology, metadata and diagnostics match for each format"] , "evidence": ["check:chrome-secondary-formats", "check:browser-occt"], "exactBlockedBy": ["Advanced STEP/IGES metadata and assembly cases are incomplete"] },
{ "id": "EX-FMT-02", "title": "Close STL, OBJ, PLY, DXF, SVG and PDF round-trips", "priority": "P1", "status": "in_progress", "dependencies": ["EX-MESH-01", "EX-DRAFT-01", "EX-TD-01"], "deliverables": ["Mesh and 2D format codecs", "Style/layer metadata", "Import-export failure corpus"], "acceptance": ["All supported entities and metadata round-trip or fail with a registered difference"] , "evidence": ["check:chrome-secondary-formats", "check:document-closure"], "exactBlockedBy": ["Several formats remain read-only or omit advanced entity metadata"] }, { "id": "EX-FMT-02", "title": "Close STL, OBJ, PLY, DXF, SVG and PDF round-trips", "priority": "P1", "status": "in_progress", "dependencies": ["EX-MESH-01", "EX-DRAFT-01", "EX-TD-01"], "deliverables": ["Mesh and 2D format codecs", "Style/layer metadata", "Import-export failure corpus"], "acceptance": ["All supported entities and metadata round-trip or fail with a registered difference"] , "evidence": ["check:chrome-secondary-formats", "check:document-closure"], "exactBlockedBy": ["Several formats remain read-only or omit advanced entity metadata"] },
{ "id": "EX-FMT-03", "title": "Close IFC, CSV and G-code native object semantics", "priority": "P1", "status": "in_progress", "dependencies": ["EX-BIM-01", "EX-SS-01", "EX-CAM-01"], "deliverables": ["IFC schema and property sets", "Spreadsheet CSV dialect parity", "Path/G-code object round-trip"], "acceptance": ["Import, edit, export and native reopen preserve the declared object graph"] , "evidence": ["check:chrome-bim", "check:document-closure", "check:cam-parity"], "exactBlockedBy": ["IFC and G-code entrypoint inventory still records unsupported desktop semantics"] }, { "id": "EX-FMT-03", "title": "Close IFC, CSV and G-code native object semantics", "priority": "P1", "status": "in_progress", "dependencies": ["EX-BIM-01", "EX-SS-01", "EX-CAM-01"], "deliverables": ["IFC schema and property sets", "Spreadsheet CSV dialect parity", "Path/G-code object round-trip"], "acceptance": ["Import, edit, export and native reopen preserve the declared object graph"] , "evidence": ["check:chrome-bim", "check:document-closure", "check:cam-parity"], "exactBlockedBy": ["IFC and G-code entrypoint inventory still records unsupported desktop semantics"] },

View File

@@ -0,0 +1,544 @@
{
"baselineId": "freecad-1.1.1-xlink-relink-oracle",
"edited": {
"binder": {
"area": 54,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
}
]
},
"dimensions": {
"xLinkLength": 8,
"xLinkListHeights": [
6,
4
],
"xLinkSubListAreas": [
54,
28,
28
],
"xLinkSubWidth": 9
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": {
"document": "XLinkSource",
"object": "SourceBox"
},
"xLinkList": [
{
"document": "XLinkSource",
"object": "SourceBox"
},
{
"document": "XLinkSource",
"object": "SecondBox"
}
],
"xLinkSub": {
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
"xLinkSubList": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
{
"document": "XLinkSource",
"object": "SecondBox",
"subElements": [
"Face1",
"Face2"
]
}
]
},
"sourceFaceArea": 54
},
"finalRoundtrip": {
"binder": {
"area": 54,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
}
]
},
"dimensions": {
"xLinkLength": 8,
"xLinkListHeights": [
6,
4
],
"xLinkSubListAreas": [
54,
28,
28
],
"xLinkSubWidth": 9
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": {
"document": "XLinkSource",
"object": "SourceBox"
},
"xLinkList": [
{
"document": "XLinkSource",
"object": "SourceBox"
},
{
"document": "XLinkSource",
"object": "SecondBox"
}
],
"xLinkSub": {
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
"xLinkSubList": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
{
"document": "XLinkSource",
"object": "SecondBox",
"subElements": [
"Face1",
"Face2"
]
}
]
},
"sourceFaceArea": 54
},
"freecadVersion": "1.1.1",
"gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
"initial": {
"binder": {
"area": 30,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
}
]
},
"dimensions": {
"xLinkLength": 4,
"xLinkListHeights": [
6,
2
],
"xLinkSubListAreas": [
30,
14,
14
],
"xLinkSubWidth": 5
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": {
"document": "XLinkSource",
"object": "SourceBox"
},
"xLinkList": [
{
"document": "XLinkSource",
"object": "SourceBox"
},
{
"document": "XLinkSource",
"object": "SecondBox"
}
],
"xLinkSub": {
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
"xLinkSubList": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
{
"document": "XLinkSource",
"object": "SecondBox",
"subElements": [
"Face1",
"Face2"
]
}
]
},
"sourceDocuments": [
"XLinkConsumer",
"XLinkSource"
]
},
"missingSource": {
"binder": {
"area": 30,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": []
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": null,
"xLinkList": [],
"xLinkSub": null,
"xLinkSubList": []
},
"sourceOpen": false
},
"propertyTypes": [
"App::PropertyXLink",
"App::PropertyXLinkSub",
"App::PropertyXLinkList",
"App::PropertyXLinkSubList"
],
"relinkAfterClose": {
"binder": {
"area": 30,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
}
]
},
"dimensions": {
"xLinkLength": 4,
"xLinkListHeights": [
6,
2
],
"xLinkSubListAreas": [
30,
14,
14
],
"xLinkSubWidth": 5
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": {
"document": "XLinkSource",
"object": "SourceBox"
},
"xLinkList": [
{
"document": "XLinkSource",
"object": "SourceBox"
},
{
"document": "XLinkSource",
"object": "SecondBox"
}
],
"xLinkSub": {
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
"xLinkSubList": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
{
"document": "XLinkSource",
"object": "SecondBox",
"subElements": [
"Face1",
"Face2"
]
}
]
},
"sourceOpen": true
},
"relinkAfterMissing": {
"binder": {
"area": 30,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
}
]
},
"dimensions": {
"xLinkLength": 4,
"xLinkListHeights": [
6,
2
],
"xLinkSubListAreas": [
30,
14,
14
],
"xLinkSubWidth": 5
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": {
"document": "XLinkSource",
"object": "SourceBox"
},
"xLinkList": [
{
"document": "XLinkSource",
"object": "SourceBox"
},
{
"document": "XLinkSource",
"object": "SecondBox"
}
],
"xLinkSub": {
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
"xLinkSubList": [
{
"document": "XLinkSource",
"object": "SourceBox",
"subElements": [
"Face1"
]
},
{
"document": "XLinkSource",
"object": "SecondBox",
"subElements": [
"Face1",
"Face2"
]
}
]
},
"sourceOpen": true
},
"schemaVersion": 1,
"sourceClosed": {
"binder": {
"area": 30,
"shapeNull": false,
"state": [
"Up-to-date"
],
"status": "Valid",
"support": []
},
"holder": {
"propertyStatus": {
"XLink": [
"21"
],
"XLinkList": [
"21"
],
"XLinkSub": [
"21"
],
"XLinkSubList": [
"21"
]
},
"state": [
"Up-to-date"
],
"status": "Valid",
"xLink": null,
"xLinkList": [],
"xLinkSub": null,
"xLinkSubList": []
},
"sourceOpen": false
},
"status": "pass",
"tolerance": 1e-7
}

View File

@@ -17,8 +17,10 @@
"nativeOcct": { "nativeOcct": {
"version": "8.0.0", "version": "8.0.0",
"sourceCommit": "bb75017e915f6509a4bfa8b551f853352754c2da", "sourceCommit": "bb75017e915f6509a4bfa8b551f853352754c2da",
"javascriptSha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557", "buildStatus": "production-linked",
"wasmSha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6" "javascriptSha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0",
"wasmSha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515",
"dataSha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
}, },
"golden": { "golden": {
"manifestScenarioCount": 100, "manifestScenarioCount": 100,

View File

@@ -111,6 +111,13 @@
"libraryPath": "artifacts/occt-history-dist.tar.zst", "libraryPath": "artifacts/occt-history-dist.tar.zst",
"required": true "required": true
}, },
{
"id": "freecad-naming-wasm-sdk",
"source": ".cache/toolchains/freecad-naming-sdk",
"restore": ".cache/toolchains/freecad-naming-sdk",
"libraryPath": "toolchains/freecad-naming-wasm-sdk-1.1.1.tar.zst",
"required": true
},
{ {
"id": "planegcs-dist", "id": "planegcs-dist",
"source": "native/planegcs/dist", "source": "native/planegcs/dist",

View File

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

View File

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

View File

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

View File

@@ -22,20 +22,20 @@
"bytes": 556597, "bytes": 556597,
"sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105" "sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105"
}, },
{
"path": "assets/index-B7ENiszF.js",
"bytes": 1134628,
"sha256": "53bd3b2876d56238158be315cfdf8d7143767c46c0a429271d9985c36f003ca4"
},
{ {
"path": "assets/index-BnRqhr3m.css", "path": "assets/index-BnRqhr3m.css",
"bytes": 65976, "bytes": 65976,
"sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec" "sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec"
}, },
{ {
"path": "assets/nativeHistoryWorkerEntry-C7hZKjun.js", "path": "assets/index-BZhOOUrh.js",
"bytes": 21764, "bytes": 1135258,
"sha256": "a4fbc5e765e1d919c8f066b6b07e2064f62e3c291496972a2b4006069a17902b" "sha256": "f8d20cf0ceeb131295d21f8d9809a542aa8871a50e74a712fdc065be307fd021"
},
{
"path": "assets/nativeHistoryWorkerEntry-BEDB1h7o.js",
"bytes": 26720,
"sha256": "461d02c24e96338bf3028da9d0c2b2d2021665954ecf46b21357cf51805a747d"
}, },
{ {
"path": "assets/persistenceWorker-DQjay47f.js", "path": "assets/persistenceWorker-DQjay47f.js",
@@ -232,6 +232,11 @@
"bytes": 190, "bytes": 190,
"sha256": "3cb76a9187f3f0cda082bfcf28c78c5bbf6abfc78a3cdb57d8560c2a7632fa51" "sha256": "3cb76a9187f3f0cda082bfcf28c78c5bbf6abfc78a3cdb57d8560c2a7632fa51"
}, },
{
"path": "chrome-partdesign-reference-lifecycle-harness.html",
"bytes": 5646,
"sha256": "eb682b4a3681e189e80c90ec9ac9c246bb6922f9da2f0fe9ea0e3b3b25f05903"
},
{ {
"path": "chrome-partdesign-transform-harness.html", "path": "chrome-partdesign-transform-harness.html",
"bytes": 200, "bytes": 200,
@@ -330,22 +335,27 @@
{ {
"path": "index.html", "path": "index.html",
"bytes": 678, "bytes": 678,
"sha256": "019c7f7b5255a12f94a19ae72edc50a45343e81779bed1b68842a934b31da01a" "sha256": "b78bfb7e023a1f03371b5f2c322cb367a9a05c622e45a06e37670c87cc68cd3a"
}, },
{ {
"path": "manifest.webmanifest", "path": "manifest.webmanifest",
"bytes": 344, "bytes": 344,
"sha256": "cc7f6ffa211d1a79486885182e2e129640b0bf4367a18a2d2d08bd2c6cff3758" "sha256": "cc7f6ffa211d1a79486885182e2e129640b0bf4367a18a2d2d08bd2c6cff3758"
}, },
{
"path": "native/occt-history/bitbybit-occt-history.data",
"bytes": 3322679,
"sha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
},
{ {
"path": "native/occt-history/bitbybit-occt-history.js", "path": "native/occt-history/bitbybit-occt-history.js",
"bytes": 237925, "bytes": 359739,
"sha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557" "sha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0"
}, },
{ {
"path": "native/occt-history/bitbybit-occt-history.wasm", "path": "native/occt-history/bitbybit-occt-history.wasm",
"bytes": 23091986, "bytes": 37482285,
"sha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6" "sha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515"
}, },
{ {
"path": "native/occt-history/browser-harness-worker.js", "path": "native/occt-history/browser-harness-worker.js",
@@ -380,7 +390,7 @@
{ {
"path": "sw.js", "path": "sw.js",
"bytes": 1390, "bytes": 1390,
"sha256": "5389aba344f93a8948bfed30aa20c0a1c38cdba7d9ebf120964d0897bba7e8cc" "sha256": "020250b7a88f8106b0666ad9d7070aefab28892371e4c670ab3d164b9e0922b6"
}, },
{ {
"path": "vendor/camotics/camotics-sweep.wasm", "path": "vendor/camotics/camotics-sweep.wasm",
@@ -402,7 +412,7 @@
"status": "signed", "status": "signed",
"algorithm": "Ed25519", "algorithm": "Ed25519",
"keyId": "bitbybit-local-release-2026", "keyId": "bitbybit-local-release-2026",
"payloadSha256": "c8734e374954b614941eb295623252af2094e9de7e415a46a399936d1cc391a9", "payloadSha256": "44d3d76c20d3a5d4e691b2d98cc5dc6fa1df910e492bf811fa64133fb2f6177a",
"signature": "vtnNIPTf3sCXN2F0c5lRVO7E76DXuolr+B5dZ8YQ34rWeWbJvgLqFVIqQ3IHke9nQVUhwYOR7SR2EyG3gucBAQ==" "signature": "m2GxJ+PLZ2RcDSI9w0hUMF+ArOLKydE6ilbgRXKgJlj8qiIRrgvaFghAZQL23NGws++j/9Xv78EsaXRvZC8oDA=="
} }
} }

View File

@@ -1832,10 +1832,56 @@ SDK-05 又建立了独立 pthread OCCT 构建和 `.cache/candidates/freecad-nami
该文档真实保存为 FCStd、关闭、重新打开并重算四个 Body 的 Group/Tip、Datum 支撑/MapMode/Offset、两类 Binder 引用与形状、无效支持诊断全部保持。执行与检查命令已分离为 `probe:freecad-partdesign-structure``check:freecad-partdesign-structure`,并纳入真实 oracle lane 和默认 Sketcher/PartDesign 门禁。 该文档真实保存为 FCStd、关闭、重新打开并重算四个 Body 的 Group/Tip、Datum 支撑/MapMode/Offset、两类 Binder 引用与形状、无效支持诊断全部保持。执行与检查命令已分离为 `probe:freecad-partdesign-structure``check:freecad-partdesign-structure`,并纳入真实 oracle lane 和默认 Sketcher/PartDesign 门禁。
同一 oracle 又增加双 FCStd 跨文档场景。原生 `ShapeBinder.Support` 虽报告为 `App::PropertyLinkSubListGlobal`,但直接赋外部文档对象仍明确返回 `PropertyLinkSubList does not support external object``SubShapeBinder.Support``App::PropertyXLinkSubList`,在源/消费文档均已落盘后接受 `ExternalBox.Face1`。关闭并按源→消费顺序重开后 XLink 仍指向源文档,源 Width 编辑使 Face1 与 SubShapeBinder 面积同步 `30→48`。删除源对象时 FreeCAD 清空 XLink但保留面积 48 的最后形状缓存,且状态仍为 `Up-to-date/Valid`检查器锁定了这一非直觉生命周期语义。EX-PD-02 仍为 pending完整 Attachment 组合、其他 XLink/重新链接恢复与 Web Chrome 生命周期尚未闭合 同一 oracle 又增加双 FCStd 跨文档场景。原生 `ShapeBinder.Support` 虽报告为 `App::PropertyLinkSubListGlobal`,但直接赋外部文档对象仍明确返回 `PropertyLinkSubList does not support external object``SubShapeBinder.Support``App::PropertyXLinkSubList`,在源/消费文档均已落盘后接受 `ExternalBox.Face1`。关闭并按源→消费顺序重开后 XLink 仍指向源文档,源 Width 编辑使 Face1 与 SubShapeBinder 面积同步 `30→48`。删除源对象时 FreeCAD 清空 XLink但保留面积 48 的最后形状缓存,且状态仍为 `Up-to-date/Valid`;检查器锁定了这一非直觉生命周期语义。本节完成时 EX-PD-02 仍为 pending;第 236 节已补齐当时剩余的完整 Attachment 组合、其他 XLink/重新链接恢复与 Web Chrome 生命周期。
## 235. 2026-08-13 PAR-01 Attachment 注册表与引擎分区 oracle ## 235. 2026-08-13 PAR-01 Attachment 注册表与引擎分区 oracle
新增独立 `freecad-attachment-mode-oracle`,从锁定 FreeCAD 1.1.1 运行时读取 `MapMode` 的永久枚举顺序和 `Attacher` 元数据。真实注册表为 55 项Plane 与 Sketch 的 `AttachEnginePlane` 各实现 23 项Line 的 `AttachEngineLine` 实现 18 项Point 的 `AttachEnginePoint` 实现 9 项,实现并集为 50 项。`Deactivated` 是关闭状态,`TangentU``TangentV``IntersectionPoint``MidPoint` 未出现在这四类引擎的实现集合。报告为每个实现模式保留永久索引和完整 `ReferenceCombinations`,从而将“枚举可写”与“当前引擎实际实现”分开。 新增独立 `freecad-attachment-mode-oracle`,从锁定 FreeCAD 1.1.1 运行时读取 `MapMode` 的永久枚举顺序和 `Attacher` 元数据。真实注册表为 55 项Plane 与 Sketch 的 `AttachEnginePlane` 各实现 23 项Line 的 `AttachEngineLine` 实现 18 项Point 的 `AttachEnginePoint` 实现 9 项,实现并集为 50 项。`Deactivated` 是关闭状态,`TangentU``TangentV``IntersectionPoint``MidPoint` 未出现在这四类引擎的实现集合。报告为每个实现模式保留永久索引和完整 `ReferenceCombinations`,从而将“枚举可写”与“当前引擎实际实现”分开。
代表性执行场景覆盖 Plane/Sketch 的 `FlatFace`、Line 的 `TwoPointLine` 和 Point 的 `Vertex`并锁定引用类型建议、Placement、状态与 `positionBySupport`。源 Box 高度从 6 改为 9 后Vertex Point 的 Z 同步 `6→9`FCStd 关闭重开后保持。反向案例证明 Plane 写入 `Vertex`、Line/Point 写入 `FlatFace` 虽能保留枚举,却都进入 `Touched + Invalid` 并返回 `Attachment mode ... is not implemented.`,且失败状态同样通过 FCStd 往返。新执行/检查命令已进入真实 oracle lane、Sketcher/PartDesign 聚合门禁及 PAR-01 证据。EX-PD-02 仍保持 pending尚需为 50 个实现槽位的全部引用组合构造可执行几何,并完成其他 XLink、重新链接恢复和 Web Chrome 回放。 代表性执行场景覆盖 Plane/Sketch 的 `FlatFace`、Line 的 `TwoPointLine` 和 Point 的 `Vertex`并锁定引用类型建议、Placement、状态与 `positionBySupport`。源 Box 高度从 6 改为 9 后Vertex Point 的 Z 同步 `6→9`FCStd 关闭重开后保持。反向案例证明 Plane 写入 `Vertex`、Line/Point 写入 `FlatFace` 虽能保留枚举,却都进入 `Touched + Invalid` 并返回 `Attachment mode ... is not implemented.`,且失败状态同样通过 FCStd 往返。新执行/检查命令已进入真实 oracle lane、Sketcher/PartDesign 聚合门禁及 PAR-01 证据。本节登记的剩余项已由第 236 节关闭。
## 236. 2026-08-13 EX-PD-02 Attachment/XLink/重连与 Chrome 生命周期闭合
新增 `freecad-attachment-combination-oracle`,不再把 50 个实现模式的注册表元数据当作执行覆盖。锁定 FreeCAD 1.1.1 为 Plane、Sketch、Line、Point 四类对象逐项展开每个 `ImplementedModes` 的全部 `ReferenceCombinations`,形成 `85 + 85 + 44 + 22 = 236` 个执行槽位。每个槽位使用独立 Box、平面、直线、圆、椭圆、双曲线、抛物线、一般曲线和顶点几何初始状态均为 `Up-to-date/Valid``suggestModes` 接受目标模式且 Placement 有限。所有源几何统一平移 `(1.25,-0.75,2)`236 个 Placement 逐轴同步;保存 FCStd、关闭并重开后支持引用、位置、旋转轴和角度均无漂移。`probe:freecad-attachment-combinations` 负责真实执行,`check:freecad-attachment-combinations` 只检查当前报告,两者已纳入真实 oracle lane。
新增 `freecad-xlink-relink-oracle` 覆盖 `App::PropertyXLink``App::PropertyXLinkSub``App::PropertyXLinkList``App::PropertyXLinkSubList`。原生 FreeCAD 要求外链所有者先保存;四类属性标志均为 `21`。源文档关闭或源 FCStd 临时移走时Python 可见链接清空,而 SubShapeBinder 保留最后面积 30 的有效缓存;源文件恢复原路径并重新打开后,四类链接与 Binder 支持自动恢复。恢复后 SourceBox `Length/Width` 与 SecondBox `Height` 编辑通过四类属性传播Binder Face1 面积从 `30→54`;源和消费文档再次保存、关闭、按序重开后仍保持。真实执行与检查命令为 `probe:freecad-xlink-relink``check:freecad-xlink-relink`
Web 新增独立 `ExternalPartDesignLinkRegistry`,按原生状态建模四类外链、`open/closed/missing`、状态标志 `21`、Binder 最后形状缓存、同 document id/同路径重连以及可序列化 undo/redo。真实 Chrome harness 在 COOP/COEP 隔离环境中由 OCCT Worker 创建两代四个 Box以质量属性校验尺寸和面积随后通过 OPFS 保存 missing 状态、重开、重连、编辑、undo/redo、再次保存并验证重开后的历史栈。结束时 OPFS 证据目录删除ShapeHandle 与 kernel reference 均为 `0/0`。执行与静态检查分别为 `test:chrome-partdesign-reference-lifecycle``check:chrome-partdesign-reference-lifecycle`,已进入默认 `verify`、Chrome execute/check lane 和 PartDesign 聚合闭合门。
因此 `PAR-01=completed``EX-PD-02=completed`exact 计划为 `3 completed / 37 in_progress / 12 pending / 0 blocked`。该关闭只对应 Attachment、Datum、Binder、多 Body 与跨文档引用的已定义验收范围;更大的 `EX-PD-01``EX-DOC-04` 仍按各自全 TypeId/全多文档范围保持开放。准确生产命名边界未改变:`availability=unavailable``callbacks=[]``shippedWorkerImplementation=not-linked``freecadNamingBuild.status=contract-only``EX-TSN-02=in_progress``systemExact=false`
## 237. 2026-08-13 EX-TSN-02 生产 FreeCAD 私有命名 Worker 链接闭合
生产 `native/occt-history` 已改为强制读取完整 FreeCAD wasm SDK manifest并把锁定 FreeCAD 1.1.1 的 FreeCADBase、FreeCADApp、Part、Qt/Python 依赖和真实 `ElementMap/MappedName/StringHasher` bridge 链接到 OCCT Worker。默认 `build:occt-history` 必须同时满足 `FREECAD_WASM_NAMING_REQUIRED=1``OCCT_HISTORY_PUBLISH=1`;缺 SDK 或哈希不一致会直接失败。OCCT-only 构建被拆为不发布的 `build:occt-history-only-candidate`,不能覆盖生产路径。
新增共享 19-operation 生产矩阵,覆盖 Fuse/Cut/Common、Rotate、Pad/Pocket、Loft/Pipe、Revolution/Groove、Fillet/Chamfer/Hole/Draft/Thickness 以及 Linear/Polar/Mirrored/MultiTransform。Node 与真实 Chrome 均从已发布生产模块执行实际 OCCT builder history再调用 `freecadNamingAbiVersion``freecadNamingCapabilitiesJson``freecadNamingEvidenceJson`。第 238 节进一步修正了本段最初按结果“取第一条”的错误统计:当前 17 类样本具有至少一个唯一来源并实际执行 C++ 回调Loft/Pipe 为全歧义来源而不调用私有 token 生成;链式 Cut 仍将 StringHasher 扩展到 3 项,缺失 history 继续结构化拒绝。两份报告同时锁定生产 JS/WASM/DATA 和共享 harness SHA-256执行命令与静态 check 分离,旧报告不能替代实时 Chrome。
命名传输现在只保留能解析到外部 `inputId/role` 的历史来源C++ bridge 继续严格拒绝未知来源,不再把内部 builder 形体冒充为外部输入。Node 与 Chrome 报告逐项记录 `externallySourcedRecords/internalRecordsExcluded`Hole 排除 15 条内部圆柱记录LinearPattern、PolarPattern、Mirrored 分别排除内部复制体记录。双输入 Boolean/Loft/Pipe 的独立 tool 输入仍完整保留。相同结果的关系现在先按 provenance 分组:同一来源的重复关系折叠,唯一来源才进入严格 ElementMap多来源保留候选集绝不再依赖数组顺序。
离线资源仍以 `native/occt-history/dist` 为唯一归档源;新增 `publish:occt-history` 在离线恢复后先实例化并校验锁定 ABI再同步 JS/WASM/DATA 到 public。真实 WASM lane 负责重建生产链接并执行 Node 矩阵Chrome lane 自动执行实时生产矩阵并在 check 阶段只读校验报告。
因此当前准确边界为 `availability=available`、三项生产回调齐全、`shippedWorkerImplementation=freecad-linked``freecadNamingBuild.status=production-linked``EX-TSN-02=completed`。本节当时 exact 计划为 `4 completed / 36 in_progress / 12 pending / 0 blocked`;第 238 节已关闭非唯一同构来源任务。`systemExact=false` 保持不变。
## 238. 2026-08-14 EX-TSN-03 同构 provenance 与原生属性缺口量化
生产命名入口不再按 `resultKind/resultIndex` 选择首条历史。新的 provenance 分类器将每个结果按来源对象、来源持久名、来源 stage 和关系分组;只有唯一来源进入 FreeCAD 私有 C++ 命名,多个原生来源则生成 `ambiguous` MappedNameRef 并完整保留候选集。若上游结果已经歧义,下游特征会展开上游候选而不是把中间对象的 `FaceN` 重新稳定化;部分 ElementMap 未覆盖但当前 Shape 合法存在的子形状使用普通 indexed `FaceN/EdgeN/VertexN` 作为输入 fallback不制造私有 token。
Node 与真实 Chrome 的 19-operation 生产矩阵均重跑通过。17 类样本存在唯一来源并执行 C++ 回调Loft 和 Pipe 的当前样本全部为多来源,明确走 ambiguity-only 路径。完全重合的两个等尺寸 Box 做 Common 产生 `26` 个歧义结果,候选数均不少于 2`arrayOrderFallbackUsed=false`。独立 Facade harness 通过真实 `DirectNativeOcctHistoryProvider` 和生产 Worker 验证:对称 Common 为 `26 ambiguous / 156 candidates`,混合 Cut 为 `3 stable + 9 ambiguous`,下游 Rotate 为 `9 stable + 35 ambiguous / 135 candidates`,候选对象仍包含原始 `cut-object``cut-tool`。JSON 持久化保持候选集;已有 Facade 事务测试继续覆盖显式选择、undo/redo 和重算。通用 FCStd `PropertyLinkSub/PropertyLinkSubList` 写出同时改为 fail-closed结构化 TopoRef 必须为 stable、对象一致且是无损原生 `FaceN/EdgeN/VertexN`
原生属性语义新增独立 probe/check。锁定 FreeCAD 1.1.1 的 `Document.supportedTypes` 实跑为 `352` 个注册对象类型、`348` 个可实例化对象、`4` 个明确不可实例化对象,以及 `85` 种属性类型/`5510` 条属性记录。当前原生可编辑 codec 覆盖 `18` 种/`4135`Sketch/Shape/Path/Expression 等专用 codec 覆盖 `5` 种/`683` 条;剩余 `62` 种/`692` 条仍只能 opaque FCStd proxy。观察到 15 类属性状态,其中 Facade 目前只原生表达 Hidden/ReadOnly`2639` 条记录包含其余 13 类状态语义。报告将原始数字位 13、22-27 按锁定 FreeCAD `Property::Status` 映射为 Ordered 与 Prop* 名称,未知数字位为 0。
因此 `EX-TSN-03=completed``EX-TSN-04=in_progress`exact 计划为 `5 completed / 36 in_progress / 11 pending / 0 blocked`。准确边界为 `availability=available`、三项生产回调齐全、`shippedWorkerImplementation=freecad-linked``freecadNamingBuild.status=production-linked``systemExact=false`。剩余系统阻断现为两项:穷尽跨特征命名/原生 FCStd save-reopen-resave 语料以及完整原生文档、属性类型、状态、editor 与生命周期语义。
## 239. 2026-08-14 EX-TSN-04 五阶段生产命名链
同构 provenance harness 已从 Cut→Rotate 扩展为生产 OCCT Worker 实际执行的 Cut→Rotate→Fillet→Mirrored→LinearPattern 五阶段链。每一阶段的 `NativeStageNamingEvidence` 都先经过 JSON 保存/恢复,再作为下一阶段输入;五阶段分别产生 `42/135/261/326/1090` 条原生历史关系stable/ambiguous 映射为 `3+9``9+35``9+35``11+40``55+89`。后四阶段均有 ElementMap2StringHasher 条目单调增长 `0→3→6→9→17`
候选传播门禁不再错误要求每个下游结果永久携带所有祖先对象,而是按当前原生历史实际引用的上游 `kind/sourceIndex` 计算必需候选集合。Rotate、Fillet、Mirrored 和 LinearPattern 所引用的候选对象均完整出现在下游 ambiguity evidence 中;若某阶段没有引用某个祖先歧义子形,则不凭空复制该候选。这一规则同时防止候选丢失和历史污染。
同一 harness 还补充 Web FCStd LinkSub 持久化边界stable、对象一致的 `Face1` 经写入、inspect、rewrite、再 inspect 后仍为 `Face1`ambiguous TopoRef 继续 fail-closed。该证据未执行 FreeCAD 桌面端 save-reopen-resave报告明确记录 `nativeDesktopResaveCovered=false`。因此 `EX-TSN-04=in_progress`、任务计数和 `systemExact=false` 均不变;剩余命名阻断仍是穷尽类型兼容的跨特征转换矩阵,以及完整原生 FreeCAD FCStd 往返语料。
## 240. 2026-08-14 原生复合模型二次 FCStd resave 门
扩展 `freecad-composite-history-elementmap-oracle` 的每个 30-case 流程:初次保存后关闭并重开,采集 ElementMap/`getElementMappedName()`;随后再次 `saveAs()`,再次关闭并重开,再采集同一组阶段和子形名称。报告新增 `nativeDesktopResaveCovered``resaveNameDrift` 及汇总计数,检查器要求 30/30 案例完成二次原生保存且 `roundtripNameDrift=0``resaveNameDrift=0`
本轮实际结果为 30 个案例、219 个阶段、91 个 ElementMap2 资源和 11 个 StringHasher 资源全部通过;二次保存后的命名漂移为 `0``wrongBindings/unexplainedRelations/roundtripNameDrift` 等 exact gate 指标继续为零。EX-TSN-04 已把该门禁登记为证据,但仍保持 `in_progress`:生产 Worker 五阶段链与桌面 30-case 语料尚未完成逐阶段相关性对账,跨特征 mutation 矩阵也未穷尽。完整 `./npmw run verify`、构建、签名发布和 `git diff --check` 均通过。

View File

@@ -5,10 +5,10 @@
## 当前状态 ## 当前状态
- exact claim`false` - exact claim`false`
- exact 任务:`1/52 completed``37 in_progress``14 pending``0 blocked` - exact 任务:`5/52 completed``36 in_progress``11 pending``0 blocked`
- exact 模块:`0/34` - exact 模块:`0/34`
- promotion task`EX-REL-01=pending` - promotion task`EX-REL-01=pending`
- 三项硬边界:原生 MappedNameRef/StringHasher、逐阶段 builder、无唯一同构来源时 fail-closed - 三项硬边界:私有 token 只来自原生 MappedNameRef/StringHasher、逐阶段 builder 关系必须有原生证据、无唯一同构来源时 fail-closed
## 实施顺序 ## 实施顺序

View File

@@ -389,17 +389,17 @@ PartDesign Mirrored 已形成 experimental whole-shape 垂直切片Facade 验
运行时命名证据必须来自 native provider 的 `MappedNameRef`/`StringHasher` 或逐阶段 builder capture。`NativeStageNamingEvidence` 对缺失证据写出 `final-shape-only`,对只有原始表而没有可重建回调的 FCStd 资源写出 `opaque-preserved`;两者都不能仅凭最终 Shape 生成新 FreeCAD token。跨阶段 ElementMap2 映射保存 `sourceStageId`、来源集合和 `ambiguous` 候选,校验失败直接阻断写回。 运行时命名证据必须来自 native provider 的 `MappedNameRef`/`StringHasher` 或逐阶段 builder capture。`NativeStageNamingEvidence` 对缺失证据写出 `final-shape-only`,对只有原始表而没有可重建回调的 FCStd 资源写出 `opaque-preserved`;两者都不能仅凭最终 Shape 生成新 FreeCAD token。跨阶段 ElementMap2 映射保存 `sourceStageId`、来源集合和 `ambiguous` 候选,校验失败直接阻断写回。
`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。 `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` 已链接锁定的 FreeCAD 1.1.1 私有命名实现,但该完成只关闭生产 ABI/builder 覆盖,不把系统整体提升为 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` 保持不变 ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAGobject tag 会从 `Document.xml``Object.id` 导入,按稳定值写回 FCStd并通过 SQLite schema v7、OPFS 重开和 checkpoint 往返。命名 ABI 只接收能解析到传输输入 `inputId/role` 的历史来源Hole、Pattern、Mirrored 等 builder 的内部 tool/copy 记录会被明确排除C++ bridge 继续拒绝未知来源,禁止把内部形体冒充为外部 Base。`build:occt-history` 现在是生产 fail-closed 构建入口只接受锁定提交、Emscripten 3.1.69、wasm Qt/Python、FreeCADBase/FreeCADApp/Part 静态库及三回调 bridge 的哈希清单,并发布 JS/WASM/DATAOCCT-only 构建只允许写入不发布的候选目录`check:occt-history-artifact` 会实际实例化 WASM 并探测三回调,Node 与 Chrome 生产矩阵还会逐项执行 19 类 builder。因此 `EX-TSN-02=completed`,而 `systemExact=false` 继续成立
Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256离线模式缺包时直接失败不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp``MappedName.cpp``StringHasher.cpp``MappedElement.cpp``ElementNamingUtils.cpp``ElementMap.cpp``Base/Handle.cpp` 七个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。运行时覆盖名称解析、StringHasher 去重/SHA-1 阈值/索引引用、ElementMap 编码/双向查找/历史/保存恢复以及 MappedElement 稳定排序;真实 ElementMap 保存体和 StringHasher 表还会经过 Web 侧 ElementMap2 parser、引用闭包及严格 `FreeCADPrivateNamingABI` 校验。探针只导出隔离的 `freecadNamingCandidate*` 名称持久化流、Python 包装、类型系统、日志和 Application/Document 生命周期仍由适配头提供FreeCADApp/Part/Python 静态库、真实文档集成、OCCT builder 上下文和生产 bridge 均未链接。因此它证明的是锁定私有源码子集与 ABI 数据可交叉编译/运行,不构成生产命名实现。 Qt 前置条件已按独立、可离线复现的链路落地:`build:qt6-wasm-core` 使用 Emscripten 3.1.69 构建 QtBase 6.8.2 静态 wasm `Qt6Core`,下载使用 USTC 国内镜像,但必须先通过 Qt 发布包官方 SHA-256离线模式缺包时直接失败不允许回退联网。`build:freecad-naming-source-probe` 进一步锁定 FreeCAD 提交及 `IndexedName.cpp``MappedName.cpp``StringHasher.cpp``MappedElement.cpp``ElementNamingUtils.cpp``ElementMap.cpp``Base/Handle.cpp` 七个原始编译单元的内容哈希,并验证它们能与 wasm Qt6Core 一起生成和执行 wasm32 探针。运行时覆盖名称解析、StringHasher 去重/SHA-1 阈值/索引引用、ElementMap 编码/双向查找/历史/保存恢复以及 MappedElement 稳定排序;真实 ElementMap 保存体和 StringHasher 表还会经过 Web 侧 ElementMap2 parser、引用闭包及严格 `FreeCADPrivateNamingABI` 校验。探针只导出隔离的 `freecadNamingCandidate*` 名称持久化流、Python 包装、类型系统、日志和 Application/Document 生命周期仍由适配头提供FreeCADApp/Part/Python 静态库、真实文档集成、OCCT builder 上下文和生产 bridge 均未链接。因此它证明的是锁定私有源码子集与 ABI 数据可交叉编译/运行,不构成生产命名实现。
`check:freecad-private-naming-boundary` 同时读取 ABI 合同、兼容性矩阵exact 计划,固定校验 `shippedWorkerImplementation=not-linked``freecadNamingBuild.status=contract-only``EX-TSN-02=in_progress``systemExact=false`;该检查已进入默认 `verify`、真实 wasm lane 和离线 smoke。只有生产 OCCT Worker 实际链接锁定的 FreeCAD 私有静态库/bridge且制品探针取得三项回调和完整命名证据后才能在后续批次修改这些状态。 `check:freecad-private-naming-boundary` 同时读取 ABI 合同、兼容性矩阵exact 计划及 Node/Chrome 生产报告,固定校验 `shippedWorkerImplementation=freecad-linked``freecadNamingBuild.status=production-linked``EX-TSN-02=completed``systemExact=false`。检查还要求三项回调、19 类 builder、链式 StringHasher、缺历史 fail-closed 及 JS/WASM/DATA/harness 哈希同时成立;该检查已进入默认 `verify`、真实 wasm lane和离线 smoke。
生产 SDK 前置已有完整候选闭包。七个锁定命名编译单元仍保留 `libFreeCADPrivateNamingProbe.a` 作为源码先决条件;实际 bridge 则链接真实 FreeCADBase/FreeCADApp/Part、QtCore、CPython 及 21 个传递依赖,并初始化真实 `App::Application``check:freecad-naming-sdk-readiness` 解析所有静态 `ar` 成员并拒绝宿主 ELF`generate:freecad-naming-sdk-manifest` 还锁定 C++20、pthread、兼容头、host adapter 和预加载的 Python wasm stdlib。完整 `check:freecad-naming-sdk` 对库、源码和运行时文件逐项复算 SHA-256。 生产 SDK 前置已有完整候选闭包。七个锁定命名编译单元仍保留 `libFreeCADPrivateNamingProbe.a` 作为源码先决条件;实际 bridge 则链接真实 FreeCADBase/FreeCADApp/Part、QtCore、CPython 及 21 个传递依赖,并初始化真实 `App::Application``check:freecad-naming-sdk-readiness` 解析所有静态 `ar` 成员并拒绝宿主 ELF`generate:freecad-naming-sdk-manifest` 还锁定 C++20、pthread、兼容头、host adapter 和预加载的 Python wasm stdlib。完整 `check:freecad-naming-sdk` 对库、源码和运行时文件逐项复算 SHA-256。
隔离 bridge 的 Node 烟测已用真实 `ElementMap/MappedName/StringHasher` 生成首阶段和链式证据,并验证 `$#id:index` 与 StringHasher 闭包。独立 pthread OCCT 候选模块进一步在 Node 和真实 Chrome 中执行 OCCT cut history,再从同一 wasm 调用三项 FreeCAD 私有命名回调Chrome 执行与报告检查分开,检查器会拒绝与当前 JS/WASM/DATA 哈希不一致的旧报告。该候选强制写入 `.cache/candidates``OCCT_HISTORY_PUBLISH=0`,没有替换仓库随附生产 Worker。因此候选 SDK/Worker 验证完成不等于生产链接完成`shippedWorkerImplementation=not-linked``EX-TSN-02=in_progress``systemExact=false` 仍保持不变 隔离 bridge 的 Node 烟测已用真实 `ElementMap/MappedName/StringHasher` 生成首阶段和链式证据,并验证 `$#id:index` 与 StringHasher 闭包。独立 pthread OCCT 候选仍保留为不发布的前置门;生产 Worker 则在 Node 和真实 Chrome 中对同一共享矩阵执行 19 类 OCCT builder,再从同一 wasm 调用三项 FreeCAD 私有命名回调Chrome 执行与报告检查分开,检查器会拒绝与当前 JS/WASM/DATA 或共享 harness 哈希不一致的旧报告。生产链接完成;剩余 exact 阻断为同构来源唯一性、完整跨特征/FCStd 往返和完整文档/属性语义
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 已求解全部圆锥曲线约束。 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 已求解全部圆锥曲线约束。
@@ -409,7 +409,7 @@ Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同Sketcher
全功能对标不再沿用已经 `83/83 completed` 的 compatible 发布 WBS 作为完成判据。`config/freecad-web-exact-parity-plan.json` 建立 EX01..EX10、52 项 exact 任务,覆盖 34 模块、15 格式、6 入口,详细依赖和验收见 `docs/freecad-full-parity-execution-wbs.zh-CN.md` 第 20 节。优先顺序为原生 oracle -> kernel/history/naming -> document/property -> Sketcher/Part/PartDesign -> FCStd/formats -> GUI -> 生产/工程/平台工作台 -> 跨浏览器质量 -> exact 签名发布。 全功能对标不再沿用已经 `83/83 completed` 的 compatible 发布 WBS 作为完成判据。`config/freecad-web-exact-parity-plan.json` 建立 EX01..EX10、52 项 exact 任务,覆盖 34 模块、15 格式、6 入口,详细依赖和验收见 `docs/freecad-full-parity-execution-wbs.zh-CN.md` 第 20 节。优先顺序为原生 oracle -> kernel/history/naming -> document/property -> Sketcher/Part/PartDesign -> FCStd/formats -> GUI -> 生产/工程/平台工作台 -> 跨浏览器质量 -> exact 签名发布。
`check:freecad-web-exact-parity-plan` 与旧 execution-plan gate 一起进入默认 verify。开放任务必须保留 `exactBlockedBy`,完成任务必须具有已登记的可执行 evidence 且不能继续带阻断项34 模块和 21 个格式/入口必须逐项映射到任务。最终 `EX-REL-01` 只有在所有任务完成、34 模块均为 exact、八项零差异指标成立时才允许关闭。当前状态为 `1/52 completed`,系统仍为 `exact=false` `check:freecad-web-exact-parity-plan` 与旧 execution-plan gate 一起进入默认 verify。开放任务必须保留 `exactBlockedBy`,完成任务必须具有已登记的可执行 evidence 且不能继续带阻断项34 模块和 21 个格式/入口必须逐项映射到任务。最终 `EX-REL-01` 只有在所有任务完成、34 模块均为 exact、八项零差异指标成立时才允许关闭。计划建立时基线为 `1/52 completed`;截至第 23 节已推进为 `3/52 completed`,系统仍为 `exact=false`
`check:freecad-web-exact-promotion` 进一步阻止直接编辑 `claim.exact` 或能力矩阵来“提升”版本:在 `exact=false` 状态只输出 fail-closed只有 `EX-REL-01` 完成并提交结构化 promotion report报告同时满足 52/52、34/34、八项指标全零和八类证据才允许 promotion gate 通过。 `check:freecad-web-exact-promotion` 进一步阻止直接编辑 `claim.exact` 或能力矩阵来“提升”版本:在 `exact=false` 状态只输出 fail-closed只有 `EX-REL-01` 完成并提交结构化 promotion report报告同时满足 52/52、34/34、八项指标全零和八类证据才允许 promotion gate 通过。
@@ -417,6 +417,14 @@ Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同Sketcher
### 22. 参数 mutation 与阶段命名证据闭合规则2026-08-10 ### 22. 参数 mutation 与阶段命名证据闭合规则2026-08-10
EX-ORA-02 的 mutation corpus 由 `config/freecad-parameter-mutation-plan.json` 定义为五项独立证据nominal 参数、非法边界、两个以上显式参数快照、真实编辑-重算-恢复事务,以及无差异 FCStd 往返。`config/freecad-parameter-mutation-report.json` 必须由现有黄金证据生成,`check:freecad-parameter-mutations` 会逐字节重算并拒绝陈旧或手工提升的报告。当前只有 `1/35` family 完整,因此 EX-ORA-02 保持 `in_progress` EX-ORA-02 的 mutation corpus 由 `config/freecad-parameter-mutation-plan.json` 定义为五项独立证据nominal 参数、非法边界、两个以上显式参数快照、真实编辑-重算-恢复事务,以及无差异 FCStd 往返。`config/freecad-parameter-mutation-report.json` 必须由现有黄金证据生成,`check:freecad-parameter-mutations` 会逐字节重算并拒绝陈旧或手工提升的报告。该矩阵建立时只有 `1/35` family 完整,当前已为 `35/35`EX-ORA-02 仍因逐 family 原生 Shape/history/ElementMap2/StringHasher 和未知语义差异的更大验收范围保持 `in_progress`
30 个复合模型的 219 个文档阶段已通过 FreeCAD 运行时 `getElementMappedName/getElementIndexedName/getElementHistory` 获得直接命名证据,并由两个门禁逐阶段验证。此处的 `native-evidence=219/219` 不等于内部 builder exact219 个阶段都有 IndexedName但只有 74 个阶段的全部子形有 MappedName145 个阶段只有原生索引身份;当前识别出的 42 个操作阶段仍为 `internalBuilderEvidence=0`。没有内部 builder Shape/history 时,最终几何不能用于制造中间 Generated/Modified/Deleted没有实际 StringHasher/MappedNameRef 时也不能制造私有 token同构候选没有唯一原生来源时继续持久化为 ambiguous。 30 个复合模型的 219 个文档阶段已通过 FreeCAD 运行时 `getElementMappedName/getElementIndexedName/getElementHistory` 获得直接命名证据,并由两个门禁逐阶段验证。此处的 `native-evidence=219/219` 不等于内部 builder exact219 个阶段都有 IndexedName但只有 74 个阶段的全部子形有 MappedName145 个阶段只有原生索引身份;当前识别出的 42 个操作阶段仍为 `internalBuilderEvidence=0`。没有内部 builder Shape/history 时,最终几何不能用于制造中间 Generated/Modified/Deleted没有实际 StringHasher/MappedNameRef 时也不能制造私有 token同构候选没有唯一原生来源时继续持久化为 ambiguous。
### 23. EX-PD-02 Attachment 与跨文档引用闭合2026-08-13
`EX-PD-02` 已按独立机器证据关闭。FreeCAD 1.1.1 原生 Attachment 注册表仍为 55 项,四类引擎实现并集为 50 项;新增全组合 oracle 将 Plane/Sketch/Line/Point 的所有模式及所有引用组合展开为 236 个可执行槽位。每项都通过初始重算、统一源几何平移和 FCStd 关闭重开Placement 与支持引用无漂移。原有未实现模式反例仍要求 `Touched + Invalid`,不会因本批完整执行覆盖而被误判为实现。
跨文档 corpus 同时覆盖 `PropertyXLink``PropertyXLinkSub``PropertyXLinkList``PropertyXLinkSubList` 和 SubShapeBinder。源关闭与源文件缺失时链接值清空、Binder 保留最后有效 Shape恢复原文档 ID/路径后自动重连,后续参数编辑和再次 FCStd 往返继续传播。Web Chrome 以真实 OCCT Shape、OPFS、undo/redo 和最终 `0/0` ownership 回放同一生命周期,执行命令与静态报告检查分离。
该完成不解除更大范围任务:`EX-PD-01` 仍需完整注册 PartDesign TypeId 与参数交叉积,`EX-DOC-04` 仍需全应用多文档/merge/conflict/crash recovery生产 Worker 命名链接已由 `EX-TSN-02` 关闭,同构 provenance 已由 `EX-TSN-03` 关闭。系统维持 `exact=false`,当前 exact 计划计数为 `5 completed / 36 in_progress / 11 pending / 0 blocked`

View File

@@ -66,8 +66,8 @@
}], }],
"productionPublication": false, "productionPublication": false,
"boundary": { "boundary": {
"freecadNamingBuildStatus": "contract-only", "freecadNamingBuildStatus": "production-linked",
"exTsn02": "in_progress", "exTsn02": "completed",
"systemExact": false "systemExact": false
} }
} }

View File

@@ -1,7 +1,9 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { resolve } from 'node:path'
import createOcctHistory from './dist/bitbybit-occt-history.js' import createOcctHistory from './dist/bitbybit-occt-history.js'
const occt = await createOcctHistory() const dist = resolve(import.meta.dirname, 'dist')
const occt = await createOcctHistory({ locateFile: (path) => resolve(dist, path) })
assert.match(occt.occtVersion(), /^8\./) assert.match(occt.occtVersion(), /^8\./)
const object = occt.makeBox(10, 10, 10) const object = occt.makeBox(10, 10, 10)
const tool = occt.makeBox(5, 5, 5) const tool = occt.makeBox(5, 5, 5)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
<!doctype html>
<meta charset="utf-8">
<title>Chrome PartDesign external reference lifecycle harness</title>
<pre id="result">running</pre>
<script type="module">
import { createMockFacade } from '/src/facade/mockFacade.ts'
import { ExternalPartDesignLinkRegistry } from '/src/facade/externalPartDesignLinks.ts'
const output = document.querySelector('#result')
const report = { schemaVersion: 1, browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true, status: 'running' }
const facade = createMockFacade()
const sourcePath = '/projects/XLinkSource.FCStd'
const objectEvidence = async (objectId, shape, dimensions) => {
const [mass, quality] = await Promise.all([facade.geometry.massProperties(shape), facade.geometry.qualityReport(shape)])
if (quality.solids !== 1 || quality.isNull || Math.abs(mass.volume - dimensions.length * dimensions.width * dimensions.height) > 1e-7) throw new Error(`${objectId} OCCT evidence is invalid.`)
return { objectId, ...dimensions, volume: mass.volume, surfaceArea: mass.surfaceArea, shapeId: shape.id }
}
const sourceEvidence = async (revision, sourceShape, secondShape, sourceDimensions, secondDimensions) => ({
documentId: 'XLinkSource', path: sourcePath, revision,
objects: {
SourceBox: await objectEvidence('SourceBox', sourceShape, sourceDimensions),
SecondBox: await objectEvidence('SecondBox', secondShape, secondDimensions),
},
})
const writeArchive = async (name, registry) => {
const root = await navigator.storage.getDirectory()
const directory = await root.getDirectoryHandle('partdesign-external-reference-lifecycle', { create: true })
const file = await directory.getFileHandle(name, { create: true })
const writer = await file.createWritable()
await writer.write(registry.serialize())
await writer.close()
}
const readArchive = async (name) => {
const root = await navigator.storage.getDirectory()
const directory = await root.getDirectoryHandle('partdesign-external-reference-lifecycle')
const file = await directory.getFileHandle(name)
return ExternalPartDesignLinkRegistry.deserialize(await (await file.getFile()).text())
}
try {
if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome PartDesign reference lifecycle requires isolated OPFS.')
const initialized = await facade.geometry.initialize()
if (initialized.status !== 'ready') throw new Error(initialized.reason || 'Bitbybit geometry runtime is unavailable.')
const sourceShape = await facade.geometry.createBox({ documentId: 'XLinkSource', documentVersion: 1, width: 5, length: 4, height: 6 })
const secondShape = await facade.geometry.createBox({ documentId: 'XLinkSource', documentVersion: 1, width: 7, length: 3, height: 2 })
const initialEvidence = await sourceEvidence(1, sourceShape, secondShape, { length: 4, width: 5, height: 6 }, { length: 3, width: 7, height: 2 })
const registry = new ExternalPartDesignLinkRegistry(initialEvidence)
const initial = registry.snapshot()
registry.closeSource()
const closed = registry.snapshot()
registry.relinkSource(initialEvidence)
const relinkedAfterClose = registry.snapshot()
registry.closeSource()
registry.markSourceMissing()
const missing = registry.snapshot()
await writeArchive('missing.json', registry)
const reopenedMissingRegistry = await readArchive('missing.json')
const reopenedMissing = reopenedMissingRegistry.snapshot()
reopenedMissingRegistry.relinkSource(initialEvidence)
const relinkedAfterMissing = reopenedMissingRegistry.snapshot()
const editedSourceShape = await facade.geometry.createBox({ documentId: 'XLinkSource', documentVersion: 2, width: 9, length: 8, height: 6 })
const editedSecondShape = await facade.geometry.createBox({ documentId: 'XLinkSource', documentVersion: 2, width: 7, length: 3, height: 4 })
const editedEvidence = await sourceEvidence(2, editedSourceShape, editedSecondShape, { length: 8, width: 9, height: 6 }, { length: 3, width: 7, height: 4 })
reopenedMissingRegistry.editSource(editedEvidence)
const edited = reopenedMissingRegistry.snapshot()
reopenedMissingRegistry.undo()
const undone = reopenedMissingRegistry.snapshot()
reopenedMissingRegistry.redo()
const redone = reopenedMissingRegistry.snapshot()
await writeArchive('edited.json', reopenedMissingRegistry)
const finalRegistry = await readArchive('edited.json')
const reopened = finalRegistry.snapshot()
finalRegistry.undo()
const reopenedUndo = finalRegistry.snapshot()
finalRegistry.redo()
const reopenedRedo = finalRegistry.snapshot()
await Promise.all([sourceShape, secondShape, editedSourceShape, editedSecondShape].map((shape) => facade.geometry.release(shape)))
const root = await navigator.storage.getDirectory()
await root.removeEntry('partdesign-external-reference-lifecycle', { recursive: true })
facade.geometry.dispose()
await new Promise((resolve) => setTimeout(resolve, 0))
const released = { ...facade.geometry.capabilities(), opfsRemoved: true }
Object.assign(report, { status: 'pass', initial, closed, relinkedAfterClose, missing, reopenedMissing, relinkedAfterMissing, edited, undone, redone, reopened, reopenedUndo, reopenedRedo, released })
} catch (error) {
facade.geometry.dispose()
Object.assign(report, { status: 'failed', error: error instanceof Error ? error.stack || error.message : String(error) })
}
output.textContent = JSON.stringify(report, null, 2)
await fetch('/__partdesign-reference-lifecycle-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
</script>

View File

@@ -69,12 +69,12 @@ const report = {
productionPublication: false, productionPublication: false,
productionManifestGenerated: false, productionManifestGenerated: false,
boundary: { boundary: {
freecadNamingBuildStatus: 'contract-only', freecadNamingBuildStatus: 'production-linked',
exTsn02: 'in_progress', exTsn02: 'completed',
systemExact: false, systemExact: false,
workerLinked: false, workerLinked: true,
availability: 'unavailable', availability: 'available',
callbacks: [], callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'],
}, },
} }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`) await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)

View File

@@ -0,0 +1,36 @@
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/chrome-partdesign-reference-lifecycle-verification.json'), 'utf8'))
const evidencePaths = [
'public/chrome-partdesign-reference-lifecycle-harness.html',
'src/facade/externalPartDesignLinks.ts',
'src/facade/mockFacade.ts',
'src/facade/geometryRuntime.ts',
'src/facade/geometryWorker.ts',
'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.a4a6ec2a.wasm',
]
const fail = (message) => { throw new Error(`Chrome PartDesign reference lifecycle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const linked = (snapshot) => snapshot?.source?.status === 'open' && snapshot.properties?.XLink?.objectId === 'SourceBox' && snapshot.properties.XLinkSub?.subElements?.[0] === 'Face1' && snapshot.properties.XLinkList?.length === 2 && snapshot.properties.XLinkSubList?.length === 2 && snapshot.binder?.cacheState === 'linked' && snapshot.binder.support?.length === 1 && same(Object.values(snapshot.propertyStatus ?? {}), [['21'], ['21'], ['21'], ['21']])
const unresolved = (snapshot, status) => snapshot?.source?.status === status && snapshot.properties?.XLink === null && snapshot.properties.XLinkSub === null && snapshot.properties.XLinkList?.length === 0 && snapshot.properties.XLinkSubList?.length === 0 && snapshot.binder?.cacheState === 'cached' && snapshot.binder.support?.length === 0 && snapshot.binder.area === 30
const initialDimensions = (snapshot) => snapshot?.dimensions?.xLinkLength === 4 && snapshot.dimensions.xLinkSubWidth === 5 && same(snapshot.dimensions.xLinkListHeights, [6, 2]) && same(snapshot.dimensions.xLinkSubListAreas, [30, 14, 14]) && snapshot.binder?.area === 30
const editedDimensions = (snapshot) => snapshot?.dimensions?.xLinkLength === 8 && snapshot.dimensions.xLinkSubWidth === 9 && same(snapshot.dimensions.xLinkListHeights, [6, 4]) && same(snapshot.dimensions.xLinkSubListAreas, [54, 28, 28]) && snapshot.binder?.area === 54
if (report.schemaVersion !== 1 || report.browserId !== 'chrome' || report.status !== 'pass' || report.crossOriginIsolated !== true || !/HeadlessChrome\/150\./.test(report.userAgent ?? '')) fail('evidence is not passing, isolated, or from the locked Chrome baseline.')
if (!Array.isArray(report.evidenceInputs) || !same(report.evidenceInputs.map(({ path }) => path), evidencePaths)) fail('evidence input inventory is incomplete.')
for (const artifact of report.evidenceInputs) {
const content = await readFile(resolve(root, artifact.path))
const sha256 = createHash('sha256').update(content).digest('hex')
if (artifact.bytes !== content.byteLength || artifact.sha256 !== sha256) fail(`stale implementation or artifact evidence for ${artifact.path}.`)
}
if (!linked(report.initial) || !initialDimensions(report.initial) || report.initial.history?.undo !== 0) fail('initial external reference graph is invalid.')
if (!unresolved(report.closed, 'closed') || !linked(report.relinkedAfterClose) || !initialDimensions(report.relinkedAfterClose)) fail('source close/relink behavior is invalid.')
if (!unresolved(report.missing, 'missing') || !unresolved(report.reopenedMissing, 'missing') || !linked(report.relinkedAfterMissing) || !initialDimensions(report.relinkedAfterMissing)) fail('OPFS missing-source reopen/relink behavior is invalid.')
if (!linked(report.edited) || !editedDimensions(report.edited) || report.edited.history?.undo !== 1 || !linked(report.undone) || !initialDimensions(report.undone) || report.undone.history?.redo !== 1 || !linked(report.redone) || !editedDimensions(report.redone)) fail('external source edit Undo/Redo behavior is invalid.')
if (!linked(report.reopened) || !editedDimensions(report.reopened) || !linked(report.reopenedUndo) || !initialDimensions(report.reopenedUndo) || !linked(report.reopenedRedo) || !editedDimensions(report.reopenedRedo)) fail('OPFS round-trip did not preserve external link history.')
if (report.released?.shapeCount !== 0 || report.released?.kernelReferenceCount !== 0 || report.released?.opfsRemoved !== true) fail('Chrome lifecycle leaked geometry or OPFS state.')
console.log(JSON.stringify({ status: 'chrome-partdesign-reference-lifecycle-pass', browserId: report.browserId, propertyKinds: ['XLink', 'XLinkSub', 'XLinkList', 'XLinkSubList'], sourceCloseRelink: true, missingSourceRoundtrip: true, editUndoRedo: true, opfsHistoryRoundtrip: true, released: { shapeCount: report.released.shapeCount, kernelReferenceCount: report.released.kernelReferenceCount } }, null, 2))

View File

@@ -0,0 +1,36 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-attachment-combination-oracle.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD attachment combination oracle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
const valid = (state, mode) => state?.mapMode === mode && state.finitePlacement === true && same(state.state, ['Up-to-date']) && state.status === 'Valid' && state.positionBySupport === true && state.suggestionMessage === 'OK' && state.suggestedModes?.includes(mode)
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-attachment-combination-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7) fail('baseline is invalid.')
const expectedEngineCounts = { plane: 85, line: 44, point: 22, sketch: 85 }
if (oracle.caseCount !== 236 || Object.entries(expectedEngineCounts).some(([engine, count]) => oracle.engineCaseCounts?.[engine] !== count) || Object.keys(oracle.engineCaseCounts ?? {}).length !== 4 || oracle.cases?.length !== oracle.caseCount) fail('the implemented engine/reference combination census is incomplete.')
if (!same(oracle.translation, [1.25, -0.75, 2])) fail('source mutation vector changed.')
const identities = new Set()
for (const entry of oracle.cases) {
const identity = `${entry.engine}\0${entry.mode}\0${entry.combinationIndex}`
if (identities.has(identity)) fail(`duplicate case ${entry.id}.`)
identities.add(identity)
if (!Array.isArray(entry.referenceCombination) || entry.referenceCombination.length === 0 || entry.referenceCombination.some((value) => typeof value !== 'string' || !value)) fail(`${entry.id} has no executable reference combination.`)
if (!valid(entry.initial, entry.mode)) fail(`${entry.id} initial execution is invalid: ${entry.initial?.status}`)
if (!valid(entry.mutated, entry.mode)) fail(`${entry.id} source mutation is invalid: ${entry.mutated?.status}`)
if (!valid(entry.roundtrip, entry.mode)) fail(`${entry.id} FCStd round-trip is invalid: ${entry.roundtrip?.status}`)
if (!same(entry.initial.support, entry.mutated.support) || !same(entry.mutated.support, entry.roundtrip.support)) fail(`${entry.id} support references drifted.`)
for (let index = 0; index < 3; index += 1) {
const initial = entry.initial.placement.position[index]
const mutated = entry.mutated.placement.position[index]
if (!near(mutated - initial, oracle.translation[index])) fail(`${entry.id} did not follow the source translation on axis ${index}.`)
if (!near(entry.roundtrip.placement.position[index], mutated)) fail(`${entry.id} placement drifted after FCStd reopen on axis ${index}.`)
}
for (let index = 0; index < 3; index += 1) if (!near(entry.roundtrip.placement.axis[index], entry.mutated.placement.axis[index])) fail(`${entry.id} rotation axis drifted after FCStd reopen.`)
if (!near(entry.roundtrip.placement.angleDegrees, entry.mutated.placement.angleDegrees)) fail(`${entry.id} rotation angle drifted after FCStd reopen.`)
}
console.log(JSON.stringify({ status: 'freecad-attachment-combination-oracle-pass', baselineId: oracle.baselineId, cases: oracle.caseCount, engines: oracle.engineCaseCounts, allImplementedModes: true, allReferenceCombinations: true, sourceMutation: oracle.translation, fcstdRoundtrip: true }, null, 2))

View File

@@ -6,8 +6,11 @@ import { migrateStringHasherSchema, parseStringHasherTable, validateStringHasher
const root = resolve(new URL('..', import.meta.url).pathname) const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-elementmap-oracle.json'), 'utf8')) const report = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-elementmap-oracle.json'), 'utf8'))
const resaveReport = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-resave-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1-composite-history-elementmap2' || report.freecadVersion !== '1.1.1' || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('FreeCAD composite history ElementMap2 oracle baseline is invalid.') if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1-composite-history-elementmap2' || report.freecadVersion !== '1.1.1' || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('FreeCAD composite history ElementMap2 oracle baseline is invalid.')
if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases.') if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases.')
if (resaveReport.schemaVersion !== 1 || resaveReport.baselineId !== report.baselineId || resaveReport.freecadVersion !== report.freecadVersion || resaveReport.gitCommit !== report.gitCommit || resaveReport.status !== 'pass' || resaveReport.summary?.cases !== 30 || resaveReport.summary?.passed !== 30 || resaveReport.summary?.failed !== 0 || resaveReport.summary?.roundtripNameDrift !== 0 || resaveReport.summary?.resaveNameDrift !== 0 || resaveReport.summary?.nativeDesktopResaveCases !== 30 || !Array.isArray(resaveReport.cases) || resaveReport.cases.length !== 30) throw new Error('FreeCAD composite resave oracle must contain exactly 30 passing save/reopen/resave cases.')
for (const fixture of resaveReport.cases) if (fixture.roundtripNameDrift !== 0 || fixture.resaveNameDrift !== 0 || fixture.nativeDesktopResaveCovered !== true) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after native FreeCAD resave.`)
let parsedResources = 0 let parsedResources = 0
let parsedStringHasherResources = 0 let parsedStringHasherResources = 0
let namingEvidenceStages = 0 let namingEvidenceStages = 0
@@ -74,4 +77,4 @@ for (const fixture of report.cases) {
if (parsedResources === 0) throw new Error('FreeCAD composite oracle did not capture any ElementMap2 resources.') if (parsedResources === 0) throw new Error('FreeCAD composite oracle did not capture any ElementMap2 resources.')
if (nativeIndexedNameStages !== namingEvidenceStages) throw new Error(`FreeCAD composite oracle must retain direct indexed-name evidence for every stage, found ${nativeIndexedNameStages}/${namingEvidenceStages}.`) if (nativeIndexedNameStages !== namingEvidenceStages) throw new Error(`FreeCAD composite oracle must retain direct indexed-name evidence for every stage, found ${nativeIndexedNameStages}/${namingEvidenceStages}.`)
if (namingEvidenceMissing !== 0 || namingEvidenceBoundaryViolations !== 0) throw new Error(`FreeCAD naming evidence boundary is incomplete: missing=${namingEvidenceMissing}, violations=${namingEvidenceBoundaryViolations}.`) if (namingEvidenceMissing !== 0 || namingEvidenceBoundaryViolations !== 0) throw new Error(`FreeCAD naming evidence boundary is incomplete: missing=${namingEvidenceMissing}, violations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages }, null, 2)) console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, nativeDesktopResaveCases: resaveReport.summary.nativeDesktopResaveCases, resaveNameDrift: resaveReport.summary.resaveNameDrift }, null, 2))

View File

@@ -235,7 +235,7 @@ if (nativeEvidenceValidatedStages !== nativeEvidenceStages) globalBlockers.push(
if (nativeIndexedNameStages !== nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - nativeIndexedNameStages} native-evidence stages have no direct FreeCAD indexed-name identity`) if (nativeIndexedNameStages !== nativeEvidenceStages) globalBlockers.push(`${nativeEvidenceStages - nativeIndexedNameStages} native-evidence stages have no direct FreeCAD indexed-name identity`)
if (privateTokenEvidenceCompleteStages < privateTokenEvidenceRequiredStages) globalBlockers.push(`${privateTokenEvidenceRequiredStages - privateTokenEvidenceCompleteStages} native builder stages do not have complete private MappedName token evidence`) if (privateTokenEvidenceCompleteStages < privateTokenEvidenceRequiredStages) globalBlockers.push(`${privateTokenEvidenceRequiredStages - privateTokenEvidenceCompleteStages} native builder stages do not have complete private MappedName token evidence`)
if (internalBuilderEvidenceMissingStages > 0) globalBlockers.push(`${internalBuilderEvidenceMissingStages} composite builder stages have no native intermediate builder evidence; final geometry cannot reconstruct private intermediate history`) if (internalBuilderEvidenceMissingStages > 0) globalBlockers.push(`${internalBuilderEvidenceMissingStages} composite builder stages have no native intermediate builder evidence; final geometry cannot reconstruct private intermediate history`)
globalBlockers.push('Browser OCCT provider has no FreeCAD private MappedNameRef/StringHasher callback ABI; native oracle tokens remain fixture-scoped') globalBlockers.push('Production private naming ABI is available and non-unique isomorphic provenance is fail-closed; exhaustive cross-feature mutation, FCStd round-trip closure and complete native document/property semantics remain open')
if (missingEvidenceStages > 0) globalBlockers.push(`${missingEvidenceStages} composite stages have missing naming evidence status`) if (missingEvidenceStages > 0) globalBlockers.push(`${missingEvidenceStages} composite stages have missing naming evidence status`)
if (elementMapResources === 0) globalBlockers.push('composite oracle has no ElementMap2 resources') if (elementMapResources === 0) globalBlockers.push('composite oracle has no ElementMap2 resources')
if (stringHasherResources === 0) globalBlockers.push('composite oracle has no StringHasher resources') if (stringHasherResources === 0) globalBlockers.push('composite oracle has no StringHasher resources')

View File

@@ -0,0 +1,33 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-isomorphic-provenance-verification.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD isomorphic provenance check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.runtime !== 'node' || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail('report boundary is invalid')
const symmetric = report.cases?.symmetricCommon
if (symmetric?.status !== 'ambiguous' || symmetric.stable !== 0 || symmetric.ambiguous < 1 || symmetric.candidates < symmetric.ambiguous * 2 || symmetric.persistedCandidateSets !== true) fail('symmetric Common evidence is incomplete')
const mixed = report.cases?.mixedCut
if (mixed?.status !== 'ambiguous' || mixed.stable < 1 || mixed.ambiguous < 1 || mixed.elementMaps < 1) fail('mixed Cut evidence is incomplete')
const downstream = report.cases?.downstreamRotate
if (downstream?.status !== 'ambiguous' || downstream.ambiguous < 1 || !downstream.candidateObjects?.includes('cut-object') || !downstream.candidateObjects?.includes('cut-tool')) fail('cross-feature candidate propagation is incomplete')
const longChain = report.cases?.longChain
if (JSON.stringify(longChain?.operations) !== '["cut","rotate","fillet","mirrored","linear-pattern"]' || longChain.stages?.length !== 5 || longChain.persistedFinalEvidence !== true || longChain.referencedCandidateObjectsRetained !== true || longChain.stringHasherMonotonic !== true) fail('long-chain naming evidence is incomplete')
for (const [index, stage] of longChain.stages.entries()) {
if (stage.status !== 'ambiguous' || stage.stable < 1 || stage.ambiguous < 1 || stage.candidates < stage.ambiguous * 2 || (index > 0 && stage.persistedInputEvidence !== true)) fail(`long-chain stage ${String(stage.operation)} did not preserve stable and ambiguous provenance`)
if (index > 0 && (stage.referencedCandidateObjectsRetained !== true || stage.elementMaps < 1 || stage.referencedInputCandidateObjects?.some((objectId) => !stage.candidateObjects?.includes(objectId)))) fail(`long-chain stage ${String(stage.operation)} lost referenced provenance or ElementMap2 evidence`)
if (index > 1 && stage.stringHasherEntries < longChain.stages[index - 1].stringHasherEntries) fail(`long-chain stage ${String(stage.operation)} regressed StringHasher state`)
}
if (report.fcstdAmbiguityRejected !== true) fail('FCStd fail-closed evidence is absent')
if (JSON.stringify(report.fcstdStableLinkRoundtrip?.subElements) !== '["Face1"]' || report.fcstdStableLinkRoundtrip.inspectRewritePreserved !== true || report.fcstdStableLinkRoundtrip.nativeDesktopResaveCovered !== false) fail('FCStd stable LinkSub persistence boundary is incomplete')
for (const artifact of report.artifacts ?? []) {
const path = resolve(root, 'native/occt-history/dist', artifact.name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== artifact.bytes || createHash('sha256').update(content).digest('hex') !== artifact.sha256) fail(`report is stale for ${artifact.name}`)
}
if (report.artifacts?.length !== 3) fail('JS/WASM/DATA artifact lock is incomplete')
const harnessPath = resolve(root, report.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (report.harness?.path !== 'scripts/run-freecad-isomorphic-provenance.ts' || harnessBytes !== report.harness.bytes || createHash('sha256').update(harnessContent).digest('hex') !== report.harness.sha256) fail('report is stale for its executable harness')
console.log(JSON.stringify({ status: 'freecad-isomorphic-provenance-pass', symmetricAmbiguities: symmetric.ambiguous, downstreamAmbiguities: downstream.ambiguous, longChainStages: longChain.stages.length, fcstdAmbiguityRejected: true, fcstdStableLinkRoundtrip: true, systemExact: false }, null, 2))

View File

@@ -15,8 +15,8 @@ for (const task of plan.orderedTasks) {
for (const dependency of task.dependencies ?? []) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task ${dependency}.`) for (const dependency of task.dependencies ?? []) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task ${dependency}.`)
for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing npm script ${evidence}.`) for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing npm script ${evidence}.`)
} }
if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'in_progress') fail('current SDK/parameter task status is stale.') if (taskById.get('SDK-01').status !== 'completed' || taskById.get('SDK-02A').status !== 'completed' || taskById.get('SDK-02B').status !== 'completed' || taskById.get('SDK-04').status !== 'completed' || taskById.get('SDK-03').status !== 'completed' || taskById.get('SDK-05').status !== 'completed' || taskById.get('PAR-01').status !== 'completed') fail('current SDK/parameter task status is stale.')
if (JSON.stringify(taskById.get('SDK-03').dependencies) !== JSON.stringify(['SDK-02B', 'SDK-04'])) fail('SDK-03 dependencies must retain both the archive set and isolated bridge.') if (JSON.stringify(taskById.get('SDK-03').dependencies) !== JSON.stringify(['SDK-02B', 'SDK-04'])) fail('SDK-03 dependencies must retain both the archive set and isolated bridge.')
if (taskById.get('SDK-04').dependencies?.length !== 1 || taskById.get('SDK-04').dependencies[0] !== 'SDK-02B') fail('SDK-04 must start directly from the completed archive set.') if (taskById.get('SDK-04').dependencies?.length !== 1 || taskById.get('SDK-04').dependencies[0] !== 'SDK-02B') fail('SDK-04 must start directly from the completed archive set.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'unavailable' || plan.boundary?.productionWorker?.callbacks?.length !== 0) fail('production boundary changed before the candidate Worker gate.') if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false || plan.boundary?.productionWorker?.availability !== 'available' || plan.boundary?.productionWorker?.callbacks?.length !== 3) fail('production boundary does not identify the callback-complete linked Worker.')
console.log(JSON.stringify({ status: 'freecad-naming-next-tasks-pass', tasks: plan.orderedTasks.length, completed: plan.orderedTasks.filter(({ status }) => status === 'completed').length, inProgress: plan.orderedTasks.filter(({ status }) => status === 'in_progress').map(({ id }) => id), boundary: plan.boundary }, null, 2)) console.log(JSON.stringify({ status: 'freecad-naming-next-tasks-pass', tasks: plan.orderedTasks.length, completed: plan.orderedTasks.filter(({ status }) => status === 'completed').length, inProgress: plan.orderedTasks.filter(({ status }) => status === 'in_progress').map(({ id }) => id), boundary: plan.boundary }, null, 2))

View File

@@ -0,0 +1,54 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { FREECAD_NAMING_CALLBACKS, FREECAD_NAMING_OPERATIONS } from './freecad-naming-production-matrix.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const fail = (message) => { throw new Error(`FreeCAD production naming verification failed: ${message}`) }
const checkReport = async (reportName, runtime, artifactRoot) => {
const report = JSON.parse(await readFile(resolve(root, 'config', reportName), 'utf8'))
if (report.status !== 'pass' || report.runtime !== runtime || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail(`${reportName} has an invalid production boundary.`)
if (JSON.stringify(report.callbacks) !== JSON.stringify(FREECAD_NAMING_CALLBACKS)) fail(`${reportName} callback list is incomplete.`)
if (JSON.stringify(report.operations) !== JSON.stringify(FREECAD_NAMING_OPERATIONS) || report.cases?.length !== FREECAD_NAMING_OPERATIONS.length) fail(`${reportName} operation matrix is incomplete.`)
if (report.cases.some((entry, index) => {
const native = entry.selectedRecords > 0
return entry.operation !== FREECAD_NAMING_OPERATIONS[index]
|| entry.status !== 'pass'
|| entry.provenanceResults < 1
|| entry.provenanceResults !== entry.selectedRecords + entry.ambiguousResults
|| entry.externallySourcedRecords < 1
|| entry.internalRecordsExcluded < 0
|| entry.externallySourcedRecords + entry.internalRecordsExcluded !== entry.historyRecords
|| entry.duplicateRecordsCollapsed < 0
|| entry.modifiedRelationPrecedence < 0
|| entry.callbackExecuted !== native
|| entry.mappedNames !== entry.selectedRecords
|| (native && (entry.stringHasherEntries < 0 || entry.elementMaps < 1 || !['native-evidence', 'mixed-native-and-ambiguous'].includes(entry.namingStatus)))
|| (!native && (entry.ambiguousResults < 1 || entry.stringHasherEntries !== 0 || entry.elementMaps !== 0 || entry.namingStatus !== 'ambiguous'))
})) fail(`${reportName} contains an invalid operation result.`)
const callbackCases = report.cases.filter(({ callbackExecuted }) => callbackExecuted).length
const ambiguityOnlyCases = report.cases.filter(({ namingStatus }) => namingStatus === 'ambiguous').length
if (report.callbackExecutedCases !== callbackCases || report.ambiguityOnlyCases !== ambiguityOnlyCases || callbackCases < 1 || callbackCases + ambiguityOnlyCases !== FREECAD_NAMING_OPERATIONS.length) fail(`${reportName} has invalid callback/ambiguity partitioning.`)
if (report.isomorphicCorpus?.status !== 'pass' || report.isomorphicCorpus.fixture !== 'coincident-equal-boxes' || report.isomorphicCorpus.uniqueResults !== 0 || report.isomorphicCorpus.ambiguousResults < 1 || report.isomorphicCorpus.arrayOrderFallbackUsed !== false || report.isomorphicCorpus.candidateSets?.some(({ candidates }) => candidates.length < 2)) fail(`${reportName} is missing its symmetric native-provenance corpus.`)
if (report.invalidHistoryRejected !== true || report.chainedStage?.mappedNames < 1 || report.chainedStage?.stringHasherEntries < 2) fail(`${reportName} is missing chained or fail-closed evidence.`)
for (const artifact of report.artifacts ?? []) {
const path = resolve(root, artifactRoot, artifact.name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== artifact.bytes || createHash('sha256').update(content).digest('hex') !== artifact.sha256) fail(`${reportName} is stale for ${artifact.name}.`)
}
if (report.artifacts?.length !== 3) fail(`${reportName} must lock JS, WASM and DATA.`)
const harnessPath = resolve(root, report.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (report.harness?.path !== 'scripts/freecad-naming-production-matrix.mjs' || harnessBytes !== report.harness.bytes || createHash('sha256').update(harnessContent).digest('hex') !== report.harness.sha256) fail(`${reportName} is stale for the shared production harness.`)
return { runtime, cases: report.cases.length, generatedAt: report.generatedAt }
}
const requestedRuntime = process.argv.find((argument) => argument.startsWith('--runtime='))?.slice('--runtime='.length) ?? 'all'
if (!['all', 'node', 'chrome'].includes(requestedRuntime)) fail(`unsupported runtime ${requestedRuntime}.`)
const specs = [
['freecad-naming-production-verification.json', 'node', 'native/occt-history/dist'],
['chrome-freecad-naming-production-verification.json', 'chrome', 'public/native/occt-history'],
].filter(([, runtime]) => requestedRuntime === 'all' || runtime === requestedRuntime)
const reports = []
for (const [reportName, runtime, artifactRoot] of specs) reports.push(await checkReport(reportName, runtime, artifactRoot))
console.log(JSON.stringify({ status: 'freecad-production-naming-pass', implementation: 'freecad-linked', callbacks: FREECAD_NAMING_CALLBACKS, operations: FREECAD_NAMING_OPERATIONS.length, reports, systemExact: false }, null, 2))

View File

@@ -22,7 +22,7 @@ const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK readiness:
if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.') if (plan.schemaVersion !== 1 || plan.scope !== 'pre-production-sdk-readiness') fail('unsupported plan schema or scope.')
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.') if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.')
if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.') if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.') if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false) fail('plan boundary must identify the completed production linkage while retaining non-exact system status.')
if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.') if (JSON.stringify(plan.libraries?.map(({ name }) => name)) !== JSON.stringify(REQUIRED_FREECAD_NAMING_LIBRARIES)) fail('library list or order does not match the production SDK contract.')
if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.') if (!REQUIRED_FREECAD_NAMING_CALLBACKS.every((callback) => plan.namingBridge?.requiredExports?.includes(callback))) fail('naming bridge plan does not require all production callbacks.')
if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.') if (plan.compileOptions?.cxxStandard !== 'c++20' || plan.compileOptions?.pthread !== true || JSON.stringify(plan.compileOptions?.definitions) !== JSON.stringify(REQUIRED_FREECAD_NAMING_DEFINITIONS)) fail('compile options must lock C++20, pthread and the FreeCAD wasm compatibility definitions.')

View File

@@ -19,7 +19,7 @@ const required = (value, name) => {
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) } const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) }
if (!sdkRoot) { 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)) console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the already-published production Worker must be checked through its artifact and production reports.' }, null, 2))
process.exit(0) process.exit(0)
} }
@@ -32,7 +32,7 @@ if (required(manifest.freecadVersion, 'freecadVersion') !== '1.1.1') fail('freec
if (required(manifest.sourceCommit, 'sourceCommit') !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('sourceCommit is not the locked FreeCAD commit.') if (required(manifest.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.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.')
if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.') if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.')
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'contract-only' || manifest.boundary?.exTsn02 !== 'in_progress' || manifest.boundary?.systemExact !== false) fail('manifest must preserve the candidate-only contract-only/in_progress/non-exact boundary.') if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'production-linked' || manifest.boundary?.exTsn02 !== 'completed' || manifest.boundary?.systemExact !== false) fail('manifest must remain a non-publishing SDK manifest while reflecting the completed production linkage and non-exact system boundary.')
const includeDirs = Array.isArray(manifest.includeDirs) ? manifest.includeDirs : fail('includeDirs must be an array.') 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 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 libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.')

View File

@@ -9,7 +9,7 @@ const matrix = await load('config/compatibility-matrix.json')
const oracle = await load('config/freecad-composite-history-elementmap-oracle.json') const oracle = await load('config/freecad-composite-history-elementmap-oracle.json')
const fail = (message) => { throw new Error(`FreeCAD native naming evidence gate: ${message}`) } const fail = (message) => { throw new Error(`FreeCAD native naming evidence gate: ${message}`) }
const blockers = matrix.nativeOcctHistory?.historyProtocol?.namingEvidence?.exactBlockers const blockers = matrix.nativeOcctHistory?.historyProtocol?.namingEvidence?.exactBlockers
if (!Array.isArray(blockers) || blockers.length !== 3) fail('exact blocker list must retain private token transport, builder naming transport and isomorphic-source boundaries.') if (!Array.isArray(blockers) || blockers.length !== 2 || blockers.some((blocker) => blocker.includes('lacks the FreeCAD-linked') || blocker.includes('browser builder naming evidence transport') || blocker.includes('non-unique isomorphic'))) fail('exact blocker list must remove the closed production linkage and isomorphic provenance boundaries and retain two broader exactness boundaries.')
const featureLevels = matrix.facadeCapabilities?.geometry?.featureLevels const featureLevels = matrix.facadeCapabilities?.geometry?.featureLevels
for (const feature of ['pad', 'pocket', 'revolution', 'groove', 'boolean']) { for (const feature of ['pad', 'pocket', 'revolution', 'groove', 'boolean']) {
if (!featureLevels?.[feature] || featureLevels[feature].level !== 'compatible' || !Array.isArray(featureLevels[feature].exactBlockedBy) || featureLevels[feature].exactBlockedBy.length === 0) fail(`${feature} must have a non-exact capability level with explicit blockers.`) if (!featureLevels?.[feature] || featureLevels[feature].level !== 'compatible' || !Array.isArray(featureLevels[feature].exactBlockedBy) || featureLevels[feature].exactBlockedBy.length === 0) fail(`${feature} must have a non-exact capability level with explicit blockers.`)

View File

@@ -0,0 +1,19 @@
import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-native-property-semantics.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD native property semantics check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baseline?.freecadVersion !== '1.1.1' || report.baseline?.commit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.exactPromotionReady !== false || report.exactBlocker !== 'complete native document and property semantics') fail('report boundary is invalid')
if (report.runtime?.registeredObjectTypes !== 352 || report.runtime.instantiableObjectTypes !== 348 || report.runtime.unavailableObjectTypes !== 4 || report.runtime.propertyRecords !== 5510 || report.runtime.propertyTypes !== 85 || report.runtime.unavailableObjects?.length !== 4) fail('runtime inventory counts changed')
const support = report.supportSummary
if (support?.['native-editable-codec']?.typeCount !== 18 || support['native-editable-codec'].recordCount !== 4135 || support?.['native-specialized-codec']?.typeCount !== 5 || support['native-specialized-codec'].recordCount !== 683 || support?.['opaque-fcstd-proxy']?.typeCount !== 62 || support['opaque-fcstd-proxy'].recordCount !== 692) fail('property support partition is stale')
if (report.types?.length !== 85 || new Set(report.types.map(({ typeId }) => typeId)).size !== 85 || report.types.reduce((total, entry) => total + entry.recordCount, 0) !== 5510) fail('per-TypeId inventory is invalid')
if (report.propertyStatus?.unknownNumericBits?.length !== 0 || report.propertyStatus.proxyOnlyRecordCount !== 2639 || report.propertyStatus.facadeNative?.join(',') !== 'Hidden,ReadOnly' || report.propertyStatus.proxyOnly?.length !== 13) fail('property status coverage is stale')
for (const locked of [report.source, report.harness]) {
const path = resolve(root, locked?.path ?? '')
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
if (bytes !== locked.bytes || createHash('sha256').update(content).digest('hex') !== locked.sha256) fail(`report is stale for ${locked.path}`)
}
console.log(JSON.stringify({ status: 'freecad-native-property-semantics-pass', propertyTypes: 85, propertyRecords: 5510, nativeCodecRecords: 4818, opaqueProxyRecords: 692, proxyOnlyStatusRecords: 2639, exactPromotionReady: false }, null, 2))

View File

@@ -3,81 +3,71 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname) const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8')) const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
const [abi, matrix, plan, readiness, sdkPlan, nextTasks, buildScript, smokeTest] = await Promise.all([ const [abi, matrix, plan, readiness, sdkPlan, nextTasks, nodeReport, chromeReport, packageJson, buildScript, smokeTest] = await Promise.all([
load('config/freecad-sketcher-partdesign-abi-contract.json'), load('config/freecad-sketcher-partdesign-abi-contract.json'),
load('config/compatibility-matrix.json'), load('config/compatibility-matrix.json'),
load('config/freecad-web-exact-parity-plan.json'), load('config/freecad-web-exact-parity-plan.json'),
load('config/freecad-private-naming-source-readiness.json'), load('config/freecad-private-naming-source-readiness.json'),
load('config/freecad-naming-sdk-plan.json'), load('config/freecad-naming-sdk-plan.json'),
load('config/freecad-naming-next-tasks.json'), load('config/freecad-naming-next-tasks.json'),
readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8'), load('config/freecad-naming-production-verification.json'),
load('config/chrome-freecad-naming-production-verification.json'),
load('package.json'),
readFile(resolve(root, 'native/occt-history/build.sh'), 'utf8'),
readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8'), readFile(resolve(root, 'native/freecad-naming-probe/smoke-test.mjs'), 'utf8'),
]) ])
const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) } const fail = (message) => { throw new Error(`FreeCAD private naming boundary: ${message}`) }
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
const operations = ['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']
const tasks = plan.programs.flatMap((program) => program.tasks) const tasks = plan.programs.flatMap((program) => program.tasks)
const task = tasks.find((entry) => entry.id === 'EX-TSN-02') const task = tasks.find((entry) => entry.id === 'EX-TSN-02')
if (!task) fail('EX-TSN-02 is missing from the exact parity plan.') if (!task) fail('EX-TSN-02 is missing from the exact parity plan.')
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'not-linked') fail('the shipped ABI contract must remain not-linked until the production artifact exports verified callbacks.') if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'freecad-linked') fail('the shipped ABI contract must identify the verified FreeCAD-linked Worker.')
if (abi.claim?.exactFreeCadParity !== false) fail('the supported facade contract cannot claim exact FreeCAD parity.') if (abi.claim?.exactFreeCadParity !== false || matrix.systemExactEvaluation?.exact !== false) fail('production linkage cannot promote system exactness.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'contract-only') fail('the FreeCAD naming build must remain contract-only before a locked SDK and callback probe exist.') if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'production-linked') fail('the FreeCAD naming build status must identify production linkage.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.prerequisiteStatus !== 'real-isolated-freecad-private-naming-bridge-pass') fail('the prerequisite status must identify the real isolated FreeCAD naming bridge.') if (matrix.nativeOcctHistory?.freecadNamingBuild?.prerequisiteStatus !== 'real-isolated-freecad-private-naming-bridge-pass') fail('the prerequisite status must identify the real isolated FreeCAD naming bridge.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must identify the complete candidate SDK without implying production linkage.') if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must retain the complete SDK prerequisite.')
if (task.status !== 'in_progress') fail(`EX-TSN-02 must remain in_progress, received ${String(task.status)}.`) if (task.status !== 'completed' || task.exactBlockedBy?.length !== 0) fail(`EX-TSN-02 must be completed and blocker-free, received ${String(task.status)}.`)
if (matrix.systemExactEvaluation?.exact !== false) fail('systemExact must remain false.')
const blockers = matrix.systemExactEvaluation?.blockers ?? [] const blockers = matrix.systemExactEvaluation?.blockers ?? []
if (!blockers.includes('shipped Worker lacks the FreeCAD-linked private naming ABI implementation')) fail('systemExact blockers must retain the missing FreeCAD-linked Worker implementation.') if (blockers.length !== 2 || blockers.some((blocker) => blocker.includes('lacks the FreeCAD-linked') || blocker.includes('browser builder naming evidence transport') || blocker.includes('non-unique isomorphic'))) fail('system exact blockers must remove the closed Worker linkage, browser transport and isomorphic provenance gaps.')
if (!task.exactBlockedBy?.includes('The shipped Worker does not link the FreeCAD private naming implementation for every builder')) fail('EX-TSN-02 must retain its production Worker linkage blocker.')
for (const [label, report, runtime] of [['Node', nodeReport, 'node'], ['Chrome', chromeReport, 'chrome']]) {
if (report.status !== 'pass' || report.runtime !== runtime || report.implementation !== 'freecad-linked' || report.productionWorkerLinked !== true || report.systemExact !== false) fail(`${label} production report has an invalid boundary.`)
if (JSON.stringify(report.callbacks) !== JSON.stringify(callbacks) || JSON.stringify(report.operations) !== JSON.stringify(operations) || report.cases?.length !== operations.length) fail(`${label} production report does not cover all callbacks and builders.`)
if (report.cases.some((entry, index) => entry.operation !== operations[index] || entry.status !== 'pass' || entry.provenanceResults !== entry.selectedRecords + entry.ambiguousResults || entry.provenanceResults < 1 || entry.mappedNames !== entry.selectedRecords || entry.callbackExecuted !== (entry.selectedRecords > 0) || (entry.selectedRecords > 0 ? entry.elementMaps < 1 : entry.namingStatus !== 'ambiguous'))) fail(`${label} production report contains incomplete provenance evidence.`)
if (report.isomorphicCorpus?.status !== 'pass' || report.isomorphicCorpus.uniqueResults !== 0 || report.isomorphicCorpus.ambiguousResults < 1 || report.isomorphicCorpus.arrayOrderFallbackUsed !== false) fail(`${label} production report lacks symmetric ambiguity evidence.`)
if (report.invalidHistoryRejected !== true || report.chainedStage?.stringHasherEntries < 2 || report.artifacts?.length !== 3) fail(`${label} production report lacks chained, fail-closed or artifact evidence.`)
}
if (readiness.scope !== 'standalone-wasm-source-prerequisite') fail('source readiness must remain a standalone prerequisite, not a production implementation.') if (readiness.scope !== 'standalone-wasm-source-prerequisite') fail('source readiness must remain a standalone prerequisite, not a production implementation.')
if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain pre-production and must not publish a Worker.') if (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain non-publishing even after the Worker is published by its own build.')
if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'contract-only' || sdkPlan.boundary?.exTsn02 !== 'in_progress' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.') if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'production-linked' || sdkPlan.boundary?.exTsn02 !== 'completed' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
if (nextTasks.boundary?.freecadNamingBuildStatus !== 'contract-only' || nextTasks.boundary?.exTsn02 !== 'in_progress' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'unavailable' || nextTasks.boundary?.productionWorker?.callbacks?.length !== 0) fail('next-task plan disagrees with the production boundary.') if (nextTasks.boundary?.freecadNamingBuildStatus !== 'production-linked' || nextTasks.boundary?.exTsn02 !== 'completed' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'available' || JSON.stringify(nextTasks.boundary.productionWorker.callbacks) !== JSON.stringify(callbacks)) fail('next-task plan disagrees with the production boundary.')
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessCommand !== './npmw run check:freecad-naming-sdk-readiness') fail('compatibility matrix must expose the SDK readiness command.') if (readiness.production?.workerLinked !== true || JSON.stringify(readiness.production?.exportedCallbacks) !== JSON.stringify(callbacks)) fail('the readiness manifest must record the linked production callbacks.')
if (readiness.production?.workerLinked !== false) fail('the readiness manifest cannot claim that the production Worker is linked.') if (readiness.boundary?.freecadNamingBuildStatus !== 'production-linked' || readiness.boundary?.exTsn02 !== 'completed' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
if (readiness.production?.exportedCallbacks?.length !== 0) fail('the prerequisite probe cannot declare production callback exports.') if (readiness.candidateAbi?.scope !== 'isolated-non-production' || readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI must remain isolated and non-publishing.')
if (readiness.boundary?.freecadNamingBuildStatus !== 'contract-only' || readiness.boundary?.exTsn02 !== 'in_progress' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
if (readiness.candidateAbi?.scope !== 'isolated-non-production') fail('the candidate ABI must remain isolated and non-production.')
if (readiness.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.') if (readiness.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.')
if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.') if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.')
if (readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI cannot claim OCCT builder context or Worker publication.') for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) { for (const callback of readiness.candidateAbi?.exports ?? []) if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`) for (const callback of callbacks) if (!smokeTest.includes(callback)) fail(`the isolated source smoke test must detect production callback ${callback}.`)
} for (const component of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'FreeCAD private naming Worker bridge']) if (!readiness.productionLinkedComponents?.includes(component)) fail(`production linked components omit ${component}.`)
for (const callback of readiness.candidateAbi?.exports ?? []) { for (const component of ['complete Application and Document parity', 'exhaustive FCStd edit and round-trip corpus']) if (!readiness.remainingExactComponents?.includes(component)) fail(`remaining exact components omit ${component}.`)
if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`) if (readiness.remainingExactComponents?.includes('unique isomorphic topology provenance')) fail('closed isomorphic provenance remains listed as an exact component.')
} if (!buildScript.includes('BITBYBIT_FREECAD_NAMING_LINKED=1') || !buildScript.includes('OCCT_HISTORY_PUBLISH')) fail('the Worker build does not enforce linked ABI compilation and explicit publication.')
for (const callback of readiness.production?.requiredCallbacks ?? []) { if (!packageJson.scripts?.['build:occt-history']?.includes('FREECAD_WASM_NAMING_REQUIRED=1') || !packageJson.scripts['build:occt-history'].includes('OCCT_HISTORY_PUBLISH=1')) fail('the production build command does not fail closed on FreeCAD linkage and publication.')
if (!smokeTest.includes(callback)) fail(`the source smoke test must reject production callback ${callback}.`) if (!packageJson.scripts?.['build:occt-history-only-candidate']?.includes('OCCT_HISTORY_PUBLISH=0')) fail('the OCCT-only build must remain an explicitly non-publishing candidate.')
}
for (const source of readiness.linkedOriginalSources ?? []) {
if (!buildScript.includes(source.path.split('/').at(-1)) || !buildScript.includes(source.sha256)) fail(`the source probe build does not pin ${source.path}.`)
}
for (const expected of ['src/App/StringHasher.cpp', 'src/App/MappedElement.cpp', 'src/App/ElementNamingUtils.cpp', 'src/App/ElementMap.cpp', 'src/Base/Handle.cpp']) {
if (!readiness.linkedOriginalSources?.some((source) => source.path === expected)) fail(`the source readiness manifest must include ${expected}.`)
}
for (const expected of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'real Application and Document integration', 'FreeCAD private naming Worker bridge']) {
if (!readiness.notLinkedProductionComponents?.includes(expected)) fail(`the source readiness manifest must retain ${expected} as not linked.`)
}
console.log(JSON.stringify({ console.log(JSON.stringify({
status: 'boundary-pass', status: 'boundary-pass',
shippedWorkerImplementation: 'not-linked', shippedWorkerImplementation: 'freecad-linked',
task: { id: task.id, status: task.status }, task: { id: task.id, status: task.status },
systemExact: matrix.systemExactEvaluation.exact, systemExact: false,
sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status, sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status,
prerequisiteStatus: matrix.nativeOcctHistory.freecadNamingBuild.prerequisiteStatus, callbacks,
sdkReadinessStatus: matrix.nativeOcctHistory.freecadNamingBuild.sdkReadinessStatus, operations: operations.length,
linkedOriginalSources: readiness.linkedOriginalSources.map((source) => source.path), productionReports: { node: nodeReport.generatedAt, chrome: chromeReport.generatedAt },
candidateAbi: { candidateAbi: { scope: readiness.candidateAbi.scope, strictWebValidator: readiness.candidateAbi.strictWebValidator, publishToWorker: readiness.candidateAbi.publishToWorker },
scope: readiness.candidateAbi.scope, sdkReadiness: { scope: sdkPlan.scope, productionPublication: sdkPlan.productionPublication, requiredLibraries: sdkPlan.libraries.map(({ name }) => name) },
strictWebValidator: readiness.candidateAbi.strictWebValidator,
publishToWorker: readiness.candidateAbi.publishToWorker,
},
sdkReadiness: {
scope: sdkPlan.scope,
productionPublication: sdkPlan.productionPublication,
requiredLibraries: sdkPlan.libraries.map(({ name }) => name),
},
workerLinked: readiness.production.workerLinked,
}, null, 2)) }, null, 2))

View File

@@ -38,7 +38,7 @@ for (const family of PARTDESIGN_PARAMETER_SPACE) {
check(FREECAD_PRIVATE_NAMING_ABI_VERSION === report.privateNamingAbi?.abiVersion, 'FreeCAD private naming ABI version report is stale') check(FREECAD_PRIVATE_NAMING_ABI_VERSION === report.privateNamingAbi?.abiVersion, 'FreeCAD private naming ABI version report is stale')
check(createFacadeRuntimeProfile('production').naming.nativeAbi === report.privateNamingAbi?.runtimeProfile, 'production runtime profile does not advertise the optional private ABI') check(createFacadeRuntimeProfile('production').naming.nativeAbi === report.privateNamingAbi?.runtimeProfile, 'production runtime profile does not advertise the optional private ABI')
check(createFacadeRuntimeProfile('mock').naming.nativeAbi === 'not-exposed', 'mock runtime must not advertise the private naming ABI') check(createFacadeRuntimeProfile('mock').naming.nativeAbi === 'not-exposed', 'mock runtime must not advertise the private naming ABI')
check(report.privateNamingAbi?.shippedWorkerImplementation === 'not-linked', 'the contract must not claim a FreeCAD-linked Worker before the artifact exports verified callbacks') check(report.privateNamingAbi?.shippedWorkerImplementation === 'freecad-linked', 'the production contract must identify the verified FreeCAD-linked Worker')
check(report.privateNamingAbi?.syntheticTokenGeneration === 'forbidden', 'synthetic FreeCAD token generation must remain forbidden') check(report.privateNamingAbi?.syntheticTokenGeneration === 'forbidden', 'synthetic FreeCAD token generation must remain forbidden')
if (failures.length) { if (failures.length) {

View File

@@ -89,5 +89,5 @@ console.log(JSON.stringify({
missingBuilderOperations: missingOperations, missingBuilderOperations: missingOperations,
reports: Object.fromEntries(Object.entries(operationEvidence).sort(([a], [b]) => a.localeCompare(b))), reports: Object.fromEntries(Object.entries(operationEvidence).sort(([a], [b]) => a.localeCompare(b))),
nativeStages: stageEvidence, nativeStages: stageEvidence,
blockers: ['FreeCAD private MappedNameRef/StringHasher token evidence remains unavailable for the browser provider'], blockers: ['Production private naming is linked and non-unique isomorphic provenance is fail-closed; exhaustive cross-feature mutation, FCStd round-trip closure and complete native document/property semantics remain open'],
}, null, 2)) }, null, 2))

View File

@@ -10,7 +10,7 @@ const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (plan.schemaVersion !== 1 || plan.scope !== 'candidate-only-freecad-wasm-sdk-build') fail('unsupported schema or scope.') if (plan.schemaVersion !== 1 || plan.scope !== 'candidate-only-freecad-wasm-sdk-build') fail('unsupported schema or scope.')
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.') if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.')
if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.') if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.')
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== false || plan.boundary?.callbacks?.length !== 0) fail('production boundary must remain contract-only, unlinked, and callback-free.') if (plan.boundary?.freecadNamingBuildStatus !== 'production-linked' || plan.boundary?.exTsn02 !== 'completed' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== true || plan.boundary?.callbacks?.length !== 3) fail('production boundary must identify the linked callback-complete Worker while retaining non-exact system status.')
if (JSON.stringify(plan.requiredArchives) !== JSON.stringify(['FreeCADBase', 'FreeCADApp', 'Part', 'Python'])) fail('required archive list is incomplete or reordered.') if (JSON.stringify(plan.requiredArchives) !== JSON.stringify(['FreeCADBase', 'FreeCADApp', 'Part', 'Python'])) fail('required archive list is incomplete or reordered.')
const expectedDependencies = [ const expectedDependencies = [
['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'], ['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'],

View File

@@ -93,7 +93,7 @@ if (new Set(plan.promotionGate.requiredZeroMetrics).size !== plan.promotionGate.
if (new Set(plan.promotionGate.requiredEvidenceClasses).size !== plan.promotionGate.requiredEvidenceClasses.length) fail('promotion evidence classes must be unique.') if (new Set(plan.promotionGate.requiredEvidenceClasses).size !== plan.promotionGate.requiredEvidenceClasses.length) fail('promotion evidence classes must be unique.')
const systemEvaluation = compatibility.systemExactEvaluation const systemEvaluation = compatibility.systemExactEvaluation
if (systemEvaluation?.exact !== false || systemEvaluation?.featureExactCount !== 0 || !Array.isArray(systemEvaluation.blockers) || systemEvaluation.blockers.length !== 3) fail('compatibility matrix must retain the current non-exact evaluation and three native blockers.') if (systemEvaluation?.exact !== false || systemEvaluation?.featureExactCount !== 0 || !Array.isArray(systemEvaluation.blockers) || systemEvaluation.blockers.length !== 2) fail('compatibility matrix must retain the current non-exact evaluation and two remaining exact blockers.')
if (platformCoverage.modules.some((module) => module.level === 'exact')) fail('platform coverage cannot contain exact modules before their exact tasks complete.') if (platformCoverage.modules.some((module) => module.level === 'exact')) fail('platform coverage cannot contain exact modules before their exact tasks complete.')
const tasks = [...taskById.values()] const tasks = [...taskById.values()]

View File

@@ -0,0 +1,31 @@
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const oracle = JSON.parse(await readFile(resolve(root, 'config/freecad-xlink-relink-oracle.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD XLink/relink oracle: ${message}`) }
const same = (actual, expected) => JSON.stringify(actual) === JSON.stringify(expected)
const near = (actual, expected) => typeof actual === 'number' && Math.abs(actual - expected) <= oracle.tolerance
const expectedTypes = ['App::PropertyXLink', 'App::PropertyXLinkSub', 'App::PropertyXLinkList', 'App::PropertyXLinkSubList']
const expectedTargets = {
xLink: { document: 'XLinkSource', object: 'SourceBox' },
xLinkSub: { document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] },
xLinkList: [{ document: 'XLinkSource', object: 'SourceBox' }, { document: 'XLinkSource', object: 'SecondBox' }],
xLinkSubList: [{ document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] }, { document: 'XLinkSource', object: 'SecondBox', subElements: ['Face1', 'Face2'] }],
}
const linked = (holder) => Object.entries(expectedTargets).every(([name, expected]) => same(holder?.[name], expected))
const validHolder = (holder) => linked(holder) && same(holder.state, ['Up-to-date']) && holder.status === 'Valid' && Object.values(holder.propertyStatus).every((status) => same(status, ['21']))
const validBinder = (binder, area) => same(binder?.support, [{ document: 'XLinkSource', object: 'SourceBox', subElements: ['Face1'] }]) && near(binder.area, area) && binder.shapeNull === false && same(binder.state, ['Up-to-date']) && binder.status === 'Valid'
const sameDimensions = (actual, expected) => Object.entries(expected).every(([name, value]) => Array.isArray(value) ? same(actual?.[name], value) : near(actual?.[name], value))
if (oracle.schemaVersion !== 1 || oracle.baselineId !== 'freecad-1.1.1-xlink-relink-oracle' || oracle.freecadVersion !== '1.1.1' || oracle.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || oracle.status !== 'pass' || oracle.tolerance !== 1e-7 || !same(oracle.propertyTypes, expectedTypes)) fail('baseline is invalid.')
if (!validHolder(oracle.initial?.holder) || !sameDimensions(oracle.initial.dimensions, { xLinkLength: 4, xLinkSubWidth: 5, xLinkListHeights: [6, 2], xLinkSubListAreas: [30, 14, 14] }) || !validBinder(oracle.initial.binder, 30)) fail('initial four-property XLink graph is invalid.')
if (oracle.sourceClosed?.sourceOpen !== false || linked(oracle.sourceClosed.holder) || oracle.sourceClosed.binder?.area !== 30 || oracle.sourceClosed.binder.shapeNull !== false) fail('closing the source document did not expose the native unresolved/cached state.')
if (oracle.relinkAfterClose?.sourceOpen !== true || !validHolder(oracle.relinkAfterClose.holder) || !validBinder(oracle.relinkAfterClose.binder, 30)) fail('same-session source reopen did not relink all references.')
if (oracle.missingSource?.sourceOpen !== false || linked(oracle.missingSource.holder) || oracle.missingSource.binder?.area !== 30 || oracle.missingSource.binder.shapeNull !== false) fail('missing source file did not preserve the native unresolved/cached state.')
if (oracle.relinkAfterMissing?.sourceOpen !== true || !validHolder(oracle.relinkAfterMissing.holder) || !validBinder(oracle.relinkAfterMissing.binder, 30)) fail('restoring the missing source path did not relink all references.')
const editedDimensions = { xLinkLength: 8, xLinkSubWidth: 9, xLinkListHeights: [6, 4], xLinkSubListAreas: [54, 28, 28] }
if (!validHolder(oracle.edited?.holder) || !sameDimensions(oracle.edited.dimensions, editedDimensions) || !validBinder(oracle.edited.binder, oracle.edited.sourceFaceArea) || !near(oracle.edited.sourceFaceArea, 54)) fail('post-relink source edit did not propagate through all link kinds and the binder.')
if (!validHolder(oracle.finalRoundtrip?.holder) || !sameDimensions(oracle.finalRoundtrip.dimensions, editedDimensions) || !validBinder(oracle.finalRoundtrip.binder, oracle.finalRoundtrip.sourceFaceArea) || !near(oracle.finalRoundtrip.sourceFaceArea, 54)) fail('final source/consumer FCStd round-trip drifted.')
console.log(JSON.stringify({ status: 'freecad-xlink-relink-oracle-pass', baselineId: oracle.baselineId, propertyTypes: oracle.propertyTypes, sourceCloseRelink: true, missingFileRelink: true, editPropagation: true, fcstdRoundtrip: true }, null, 2))

View File

@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
const root = resolve(new URL('..', import.meta.url).pathname) const root = resolve(new URL('..', import.meta.url).pathname)
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm'] const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const distRoot = resolve(root, 'native/occt-history/dist') const distRoot = resolve(root, 'native/occt-history/dist')
const publicRoot = resolve(root, 'public/native/occt-history') const publicRoot = resolve(root, 'public/native/occt-history')
const exists = async (path) => access(path).then(() => true).catch(() => false) const exists = async (path) => access(path).then(() => true).catch(() => false)
@@ -15,7 +15,7 @@ if (!distPresent.some(Boolean) && !publicPresent.some(Boolean)) {
console.log(JSON.stringify({ status: 'artifact-not-built', reason: 'Run ./npmw run build:occt-history when the pinned Emscripten/OCCT inputs are available.' }, null, 2)) console.log(JSON.stringify({ status: 'artifact-not-built', reason: 'Run ./npmw run build:occt-history when the pinned Emscripten/OCCT inputs are available.' }, null, 2))
process.exit(0) process.exit(0)
} }
if (distPresent.some((present) => !present) || publicPresent.some((present) => !present)) throw new Error('OCCT history browser artifact is incomplete; JS and WASM must be published together.') if (distPresent.some((present) => !present) || publicPresent.some((present) => !present)) throw new Error('OCCT history browser artifact is incomplete; JS, WASM and DATA must be published together.')
for (const name of artifactNames) { for (const name of artifactNames) {
const distBytes = await readFile(resolve(distRoot, name)) const distBytes = await readFile(resolve(distRoot, name))
const publicBytes = await readFile(resolve(publicRoot, name)) const publicBytes = await readFile(resolve(publicRoot, name))
@@ -48,6 +48,18 @@ if (callbacks.length === 3) {
const objectStep = nativeModule.shapeToStep(object) const objectStep = nativeModule.shapeToStep(object)
const toolStep = nativeModule.shapeToStep(tool) const toolStep = nativeModule.shapeToStep(tool)
const history = nativeModule.booleanHistoryFromStep(objectStep, toolStep, 'cut') const history = nativeModule.booleanHistoryFromStep(objectStep, toolStep, 'cut')
const seenResults = new Set()
const records = []
for (const record of history.records) {
if (record.relation === 'deleted') continue
for (const resultIndex of record.resultIndexes ?? [record.resultIndex]) {
const key = `${record.resultKind ?? record.kind}:${resultIndex}`
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0 || seenResults.has(key)) continue
seenResults.add(key)
records.push({ ...record, resultIndex, resultIndexes: undefined })
}
}
if (records.length === 0) throw new Error('Artifact Cut probe returned no unique native result relations.')
const request = { const request = {
schemaVersion: 1, schemaVersion: 1,
requestId: 'artifact-private-naming-probe', requestId: 'artifact-private-naming-probe',
@@ -65,7 +77,7 @@ if (callbacks.length === 3) {
stages: [{ stageId: 'artifact-probe:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }], stages: [{ stageId: 'artifact-probe:stage:0', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
resultStep: history.resultStep, resultStep: history.resultStep,
resultBrep: history.resultBrep, resultBrep: history.resultBrep,
history, history: { ...history, records },
} }
evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request))) evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request)))
} finally { } finally {
@@ -80,4 +92,4 @@ const expectedImplementation = abiContract.privateNamingAbi?.shippedWorkerImplem
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 === '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 (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.') 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)) console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'production-linked; EX-TSN-02 completed; broader exact plan remains open' : 'invalid production boundary' }, null, 2))

View File

@@ -34,9 +34,11 @@ assertEqual(sha256(canonicalSurface), baseline.bitbybit.surfaceSha256, 'Bitbybit
assertEqual(matrix.nativeOcctHistory.occtVersion, baseline.nativeOcct.version, 'Native OCCT version') assertEqual(matrix.nativeOcctHistory.occtVersion, baseline.nativeOcct.version, 'Native OCCT version')
assertEqual(matrix.nativeOcctHistory.sourceCommit, baseline.nativeOcct.sourceCommit, 'Native OCCT source commit') assertEqual(matrix.nativeOcctHistory.sourceCommit, baseline.nativeOcct.sourceCommit, 'Native OCCT source commit')
assertEqual(matrix.nativeOcctHistory.freecadNamingBuild.status, baseline.nativeOcct.buildStatus, 'Native OCCT naming build status')
for (const [kind, relativePath, expectedHash] of [ for (const [kind, relativePath, expectedHash] of [
['JavaScript', matrix.nativeOcctHistory.artifact.javascript, baseline.nativeOcct.javascriptSha256], ['JavaScript', matrix.nativeOcctHistory.artifact.javascript, baseline.nativeOcct.javascriptSha256],
['WASM', matrix.nativeOcctHistory.artifact.wasm, baseline.nativeOcct.wasmSha256] ['WASM', matrix.nativeOcctHistory.artifact.wasm, baseline.nativeOcct.wasmSha256],
['DATA', matrix.nativeOcctHistory.artifact.data, baseline.nativeOcct.dataSha256],
]) { ]) {
assertEqual(sha256(await readBytes(relativePath)), expectedHash, `Native OCCT ${kind} artifact`) assertEqual(sha256(await readBytes(relativePath)), expectedHash, `Native OCCT ${kind} artifact`)
} }
@@ -54,6 +56,6 @@ for (const entry of threeWay.reports) {
console.log(JSON.stringify({ console.log(JSON.stringify({
status: 'occt-upstream-drift-pass', status: 'occt-upstream-drift-pass',
bitbybit: { version: packageJson.version, sourceCommit: baseline.bitbybit.sourceCommit, surfaceSha256: baseline.bitbybit.surfaceSha256 }, bitbybit: { version: packageJson.version, sourceCommit: baseline.bitbybit.sourceCommit, surfaceSha256: baseline.bitbybit.surfaceSha256 },
nativeOcct: { version: baseline.nativeOcct.version, sourceCommit: baseline.nativeOcct.sourceCommit }, nativeOcct: { version: baseline.nativeOcct.version, sourceCommit: baseline.nativeOcct.sourceCommit, buildStatus: baseline.nativeOcct.buildStatus },
golden: { manifestScenarios: goldenManifest.scenarios.length, threeWayScenarios: threeWay.scenarioCount, operationCounts } golden: { manifestScenarios: goldenManifest.scenarios.length, threeWayScenarios: threeWay.scenarioCount, operationCounts }
}, null, 2)) }, null, 2))

View File

@@ -4,14 +4,16 @@ import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname) const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8')) const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
const lifecycle = await load('config/chrome-partdesign-lifecycle-verification.json') const lifecycle = await load('config/chrome-partdesign-lifecycle-verification.json')
const referenceLifecycle = await load('config/chrome-partdesign-reference-lifecycle-verification.json')
const loft = await load('config/chrome-partdesign-loft-verification.json') const loft = await load('config/chrome-partdesign-loft-verification.json')
const transform = await load('config/chrome-partdesign-transform-verification.json') const transform = await load('config/chrome-partdesign-transform-verification.json')
const base = await load('config/chrome-geometry-features-verification.json') const base = await load('config/chrome-geometry-features-verification.json')
if ([lifecycle, loft, transform, base].some((report) => report.status !== 'pass')) throw new Error('PartDesign closure requires passing lifecycle, base, loft and transform browser reports.') if ([lifecycle, referenceLifecycle, loft, transform, base].some((report) => report.status !== 'pass')) throw new Error('PartDesign closure requires passing lifecycle, external reference, base, loft and transform browser reports.')
const lifecycleStages = [lifecycle.initial, lifecycle.edited, lifecycle.undone, lifecycle.reopened] const lifecycleStages = [lifecycle.initial, lifecycle.edited, lifecycle.undone, lifecycle.reopened]
if (new Set(lifecycleStages.map((stage) => stage?.shapeId)).size !== lifecycleStages.length || lifecycleStages.some((stage) => !stage?.shapeId || !(stage.volume > 0) || stage.solids !== 1 || stage.tip !== 'pad' || stage.support !== 'XY_Plane') || lifecycle.edited.length !== 31 || lifecycle.undone.length !== 42 || lifecycle.reopened.length !== 42 || Math.abs(lifecycle.initial.volume - 504) > 1e-7 || Math.abs(lifecycle.edited.volume - 372) > 1e-7 || Math.abs(lifecycle.undone.volume - 504) > 1e-7 || Math.abs(lifecycle.reopened.volume - 504) > 1e-7) throw new Error('PartDesign edit/Undo/load lifecycle contains stale Shape, Tip or Support evidence.') if (new Set(lifecycleStages.map((stage) => stage?.shapeId)).size !== lifecycleStages.length || lifecycleStages.some((stage) => !stage?.shapeId || !(stage.volume > 0) || stage.solids !== 1 || stage.tip !== 'pad' || stage.support !== 'XY_Plane') || lifecycle.edited.length !== 31 || lifecycle.undone.length !== 42 || lifecycle.reopened.length !== 42 || Math.abs(lifecycle.initial.volume - 504) > 1e-7 || Math.abs(lifecycle.edited.volume - 372) > 1e-7 || Math.abs(lifecycle.undone.volume - 504) > 1e-7 || Math.abs(lifecycle.reopened.volume - 504) > 1e-7) throw new Error('PartDesign edit/Undo/load lifecycle contains stale Shape, Tip or Support evidence.')
if (lifecycle.structural?.reorderRejected !== true || lifecycle.structural?.dependencyRemovalRejected !== true || lifecycle.structural?.deleted?.tip !== 'pocket' || lifecycle.structural?.deletionUndone?.tip !== 'fillet' || lifecycle.structural?.deletionRedone?.tip !== 'pocket' || lifecycle.structural?.restoredTip !== 'fillet') throw new Error('PartDesign structural Undo/Redo and Tip evidence is invalid.') if (lifecycle.structural?.reorderRejected !== true || lifecycle.structural?.dependencyRemovalRejected !== true || lifecycle.structural?.deleted?.tip !== 'pocket' || lifecycle.structural?.deletionUndone?.tip !== 'fillet' || lifecycle.structural?.deletionRedone?.tip !== 'pocket' || lifecycle.structural?.restoredTip !== 'fillet') throw new Error('PartDesign structural Undo/Redo and Tip evidence is invalid.')
if (referenceLifecycle.initial?.properties?.XLinkList?.length !== 2 || referenceLifecycle.initial?.properties?.XLinkSubList?.length !== 2 || referenceLifecycle.closed?.binder?.cacheState !== 'cached' || referenceLifecycle.missing?.source?.status !== 'missing' || referenceLifecycle.relinkedAfterMissing?.binder?.cacheState !== 'linked' || referenceLifecycle.edited?.binder?.area !== 54 || referenceLifecycle.undone?.binder?.area !== 30 || referenceLifecycle.redone?.binder?.area !== 54 || referenceLifecycle.reopenedUndo?.binder?.area !== 30 || referenceLifecycle.reopenedRedo?.binder?.area !== 54) throw new Error('PartDesign external reference close, relink, edit, Undo/Redo or OPFS evidence is invalid.')
const expectedLoft = ['additive-loft', 'additive-pipe', 'subtractive-loft', 'subtractive-pipe'] const expectedLoft = ['additive-loft', 'additive-pipe', 'subtractive-loft', 'subtractive-pipe']
if (JSON.stringify(loft.operations?.map((entry) => entry.command)) !== JSON.stringify(expectedLoft) || loft.operations.some((entry) => entry.tip !== entry.objectId || !entry.base || !entry.shapeId || !(entry.volume > 0)) || new Set(loft.operations.map((entry) => entry.shapeId)).size !== loft.operations.length) throw new Error('PartDesign Loft/Pipe create and Tip/Base evidence is invalid.') if (JSON.stringify(loft.operations?.map((entry) => entry.command)) !== JSON.stringify(expectedLoft) || loft.operations.some((entry) => entry.tip !== entry.objectId || !entry.base || !entry.shapeId || !(entry.volume > 0)) || new Set(loft.operations.map((entry) => entry.shapeId)).size !== loft.operations.length) throw new Error('PartDesign Loft/Pipe create and Tip/Base evidence is invalid.')
@@ -27,7 +29,7 @@ if (ambiguity?.status !== 'failed' || ambiguity.retainedShapeId !== ambiguity.pr
const baseModes = ['pad-tapered', 'pocket-tapered', 'pocket-through-all', 'pocket-up-to-face', 'pad-midplane'] const baseModes = ['pad-tapered', 'pocket-tapered', 'pocket-through-all', 'pocket-up-to-face', 'pad-midplane']
if (baseModes.some((name) => !base.operations?.some((entry) => entry.name === name && entry.shapeId && entry.volume > 0))) throw new Error('PartDesign base mode creation evidence is incomplete.') if (baseModes.some((name) => !base.operations?.some((entry) => entry.name === name && entry.shapeId && entry.volume > 0))) throw new Error('PartDesign base mode creation evidence is incomplete.')
if (loft.persistence?.mode !== 'sqlite-opfs' || transform.persistence?.mode !== 'sqlite-opfs' || lifecycle.reopened?.persistenceMode !== 'sqlite-opfs' || loft.persistence.reopenedTip !== 'subtractive-pipe' || transform.persistence.reopenedTip !== transform.operations.at(-1).objectId || lifecycle.reopened.tip !== 'pad') throw new Error('PartDesign save/load Tip evidence is invalid.') if (loft.persistence?.mode !== 'sqlite-opfs' || transform.persistence?.mode !== 'sqlite-opfs' || lifecycle.reopened?.persistenceMode !== 'sqlite-opfs' || loft.persistence.reopenedTip !== 'subtractive-pipe' || transform.persistence.reopenedTip !== transform.operations.at(-1).objectId || lifecycle.reopened.tip !== 'pad') throw new Error('PartDesign save/load Tip evidence is invalid.')
if (loft.afterRelease?.shapeCount !== 0 || loft.afterRelease?.kernelReferenceCount !== 0 || transform.afterRelease?.shapeCount !== 0 || transform.afterRelease?.kernelReferenceCount !== 0 || lifecycle.released?.shapeCount !== 0 || lifecycle.released?.kernelReferenceCount !== 0 || base.afterRelease?.shapeCount !== 0 || base.afterRelease?.kernelReferenceCount !== 0) throw new Error('PartDesign closure reports leaked Shape ownership.') if (loft.afterRelease?.shapeCount !== 0 || loft.afterRelease?.kernelReferenceCount !== 0 || transform.afterRelease?.shapeCount !== 0 || transform.afterRelease?.kernelReferenceCount !== 0 || lifecycle.released?.shapeCount !== 0 || lifecycle.released?.kernelReferenceCount !== 0 || referenceLifecycle.released?.shapeCount !== 0 || referenceLifecycle.released?.kernelReferenceCount !== 0 || base.afterRelease?.shapeCount !== 0 || base.afterRelease?.kernelReferenceCount !== 0) throw new Error('PartDesign closure reports leaked Shape ownership.')
const historyPaths = new Map([ const historyPaths = new Map([
['pad', 'config/chrome-native-pad-history-verification.json'], ['pad', 'config/chrome-native-pad-history-verification.json'],
@@ -55,4 +57,4 @@ for (const [operation, path] of historyPaths) {
nativeHistory[operation] = count nativeHistory[operation] = count
} }
console.log(JSON.stringify({ status: 'partdesign-closure-pass', lifecycle: lifecycleStages.map((stage) => ({ length: stage.length, shapeId: stage.shapeId, volume: stage.volume, tip: stage.tip, support: stage.support })), createdFeatures: { loft: expectedLoft, transform: expectedTransform, baseModes }, failureRecovery: { loft: loft.failureRecovery, transform: transform.ambiguityRecovery }, persistence: { lifecycle: lifecycle.reopened, loft: loft.persistence, transform: transform.persistence }, nativeHistory }, null, 2)) console.log(JSON.stringify({ status: 'partdesign-closure-pass', lifecycle: lifecycleStages.map((stage) => ({ length: stage.length, shapeId: stage.shapeId, volume: stage.volume, tip: stage.tip, support: stage.support })), externalReferences: { propertyKinds: Object.keys(referenceLifecycle.initial.properties), sourceCloseRelink: true, missingSourceRelink: true, opfsUndoRedo: true }, createdFeatures: { loft: expectedLoft, transform: expectedTransform, baseModes }, failureRecovery: { loft: loft.failureRecovery, transform: transform.ambiguityRecovery }, persistence: { lifecycle: lifecycle.reopened, references: referenceLifecycle.reopened, loft: loft.persistence, transform: transform.persistence }, nativeHistory }, null, 2))

View File

@@ -5,8 +5,8 @@ const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse) const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [ const [
fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures, fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures,
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, partdesignFailures, partdesignRevolutionGroove, partBuilders, sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, attachmentCombinations, xlinkRelink, partdesignFailures, partdesignRevolutionGroove, partBuilders,
fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script, fcstdRoundTrip, app, partdesignReferenceLifecycle, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
] = await Promise.all([ ] = await Promise.all([
load('config/qa04-fcstd-fuzz-verification.json'), load('config/qa04-fcstd-fuzz-verification.json'),
load('config/qa04-geometry-fuzz-verification.json'), load('config/qa04-geometry-fuzz-verification.json'),
@@ -22,11 +22,14 @@ const [
load('config/freecad-partdesign-transform-oracle.json'), load('config/freecad-partdesign-transform-oracle.json'),
load('config/freecad-partdesign-structure-oracle.json'), load('config/freecad-partdesign-structure-oracle.json'),
load('config/freecad-attachment-mode-oracle.json'), load('config/freecad-attachment-mode-oracle.json'),
load('config/freecad-attachment-combination-oracle.json'),
load('config/freecad-xlink-relink-oracle.json'),
load('config/freecad-partdesign-failure-oracle.json'), load('config/freecad-partdesign-failure-oracle.json'),
load('config/freecad-partdesign-revolution-groove-oracle.json'), load('config/freecad-partdesign-revolution-groove-oracle.json'),
load('config/freecad-part-builders-oracle.json'), load('config/freecad-part-builders-oracle.json'),
load('config/freecad-fcstd-roundtrip-verification.json'), load('config/freecad-fcstd-roundtrip-verification.json'),
load('config/chrome-app-e2e-verification.json'), load('config/chrome-app-e2e-verification.json'),
load('config/chrome-partdesign-reference-lifecycle-verification.json'),
load('config/browser-matrix-verification.json'), load('config/browser-matrix-verification.json'),
load('config/chrome-performance-verification.json'), load('config/chrome-performance-verification.json'),
load('config/chrome-fault-injection-verification.json'), load('config/chrome-fault-injection-verification.json'),
@@ -48,6 +51,8 @@ const partdesignCases = [partdesignBase, partdesignLoft, partdesignDressup, part
if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].some((report) => report.status !== 'pass' || report.freecadVersion !== '1.1.1')) throw new Error('QA-02 PartDesign oracle evidence is incomplete.') if (partdesignCases !== 23 || [partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform].some((report) => report.status !== 'pass' || report.freecadVersion !== '1.1.1')) throw new Error('QA-02 PartDesign oracle evidence is incomplete.')
if (partdesignStructure.status !== 'pass' || partdesignStructure.freecadVersion !== '1.1.1' || Object.keys(partdesignStructure.initial?.bodyGroups ?? {}).length !== 4 || partdesignStructure.crossDocument?.initial?.shapeBinderExternalLink?.accepted !== false || partdesignStructure.crossDocument?.initial?.subShapeBinderSupportType !== 'App::PropertyXLinkSubList' || partdesignStructure.crossDocument?.edited?.subShapeBinderArea !== 48 || partdesignStructure.crossDocument?.deleted?.subShapeBinderCachedArea !== 48) throw new Error('QA-02 PartDesign structure and cross-document evidence is incomplete.') if (partdesignStructure.status !== 'pass' || partdesignStructure.freecadVersion !== '1.1.1' || Object.keys(partdesignStructure.initial?.bodyGroups ?? {}).length !== 4 || partdesignStructure.crossDocument?.initial?.shapeBinderExternalLink?.accepted !== false || partdesignStructure.crossDocument?.initial?.subShapeBinderSupportType !== 'App::PropertyXLinkSubList' || partdesignStructure.crossDocument?.edited?.subShapeBinderArea !== 48 || partdesignStructure.crossDocument?.deleted?.subShapeBinderCachedArea !== 48) throw new Error('QA-02 PartDesign structure and cross-document evidence is incomplete.')
if (attachmentModes.status !== 'pass' || attachmentModes.freecadVersion !== '1.1.1' || attachmentModes.registry?.modeCount !== 55 || attachmentModes.registry?.implementedUnionCount !== 50 || attachmentModes.engines?.plane?.implementedModes?.length !== 23 || attachmentModes.engines?.line?.implementedModes?.length !== 18 || attachmentModes.engines?.point?.implementedModes?.length !== 9 || attachmentModes.success?.roundtrip?.point?.placement?.position?.[2] !== 9 || Object.keys(attachmentModes.failures?.roundtrip ?? {}).length !== 3) throw new Error('QA-02 Attachment mode registry, execution or round-trip evidence is incomplete.') if (attachmentModes.status !== 'pass' || attachmentModes.freecadVersion !== '1.1.1' || attachmentModes.registry?.modeCount !== 55 || attachmentModes.registry?.implementedUnionCount !== 50 || attachmentModes.engines?.plane?.implementedModes?.length !== 23 || attachmentModes.engines?.line?.implementedModes?.length !== 18 || attachmentModes.engines?.point?.implementedModes?.length !== 9 || attachmentModes.success?.roundtrip?.point?.placement?.position?.[2] !== 9 || Object.keys(attachmentModes.failures?.roundtrip ?? {}).length !== 3) throw new Error('QA-02 Attachment mode registry, execution or round-trip evidence is incomplete.')
if (attachmentCombinations.status !== 'pass' || attachmentCombinations.freecadVersion !== '1.1.1' || attachmentCombinations.caseCount !== 236 || attachmentCombinations.cases?.length !== 236 || attachmentCombinations.cases.some((entry) => entry.initial?.status !== 'Valid' || entry.mutated?.status !== 'Valid' || entry.roundtrip?.status !== 'Valid')) throw new Error('QA-02 exhaustive Attachment mode/reference combination evidence is incomplete.')
if (xlinkRelink.status !== 'pass' || xlinkRelink.freecadVersion !== '1.1.1' || xlinkRelink.propertyTypes?.length !== 4 || xlinkRelink.relinkAfterMissing?.holder?.xLinkList?.length !== 2 || xlinkRelink.edited?.binder?.area !== 54 || xlinkRelink.finalRoundtrip?.binder?.area !== 54) throw new Error('QA-02 native XLink close, missing-file relink, edit or FCStd evidence is incomplete.')
if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.') if (partdesignFailures.status !== 'pass' || partdesignFailures.freecadVersion !== '1.1.1' || partdesignFailures.summary?.cases !== 17 || partdesignFailures.summary.passed !== 17 || partdesignFailures.summary.rejected !== 13 || partdesignFailures.summary.acceptedEmpty !== 4 || partdesignFailures.summary.accepted !== 0) throw new Error('QA-02 PartDesign failure oracle evidence is incomplete.')
if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.') if (partdesignRevolutionGroove.status !== 'pass' || partdesignRevolutionGroove.freecadVersion !== '1.1.1' || partdesignRevolutionGroove.summary?.cases !== 2 || partdesignRevolutionGroove.summary.passed !== 2 || partdesignRevolutionGroove.cases?.some((fixture) => fixture.shapeNull !== false || fixture.shapeValid !== true || fixture.solids !== 1)) throw new Error('QA-02 PartDesign Revolution/Groove oracle evidence is incomplete.')
if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.') if (partBuilders.status !== 'pass' || partBuilders.freecadVersion !== '1.1.1' || partBuilders.summary?.successCases !== 6 || partBuilders.summary.successPassed !== 6 || partBuilders.summary.failureCases !== 6 || partBuilders.summary.failurePassed !== 6 || partBuilders.summary.rejected !== 6 || partBuilders.summary.acceptedEmpty !== 0) throw new Error('QA-02 Part builders oracle evidence is incomplete.')
@@ -55,6 +60,7 @@ if (fcstdRoundTrip.status !== 'verified' || !Number.isSafeInteger(fcstdRoundTrip
const requiredAppWorkflows = ['start-to-workspace', 'freecad-menus', 'command-search-dialog', 'preferences-about-dialogs', 'model-tree-context-menu', 'workbench-change', 'create-sketch-command', 'loft-section-task', 'drawer-collapse', 'cam-workbench-job-flow', 'opfs-save-reopen', 'import-format-selection', 'export-format-selection'] const requiredAppWorkflows = ['start-to-workspace', 'freecad-menus', 'command-search-dialog', 'preferences-about-dialogs', 'model-tree-context-menu', 'workbench-change', 'create-sketch-command', 'loft-section-task', 'drawer-collapse', 'cam-workbench-job-flow', 'opfs-save-reopen', 'import-format-selection', 'export-format-selection']
if (app.status !== 'pass' || requiredAppWorkflows.some((workflow) => !app.workflows?.includes(workflow)) || app.pageErrors?.length !== 0 || app.desktop?.kernelPreviewSource !== 'bitbybit-occt' || app.desktop.canvas?.uniqueColors < 4 || app.desktop?.commandGroups < 4 || app.desktop?.selectionView !== true || app.desktop?.statusBar !== true || app.uiParity?.commandDialog?.modal !== true || app.uiParity?.preferencesDialog?.modal !== true || app.uiParity?.aboutDialog?.modal !== true || app.uiParity?.contextMenu?.items?.length !== 4 || app.uiParity?.camMenu?.items !== 60 || app.cam?.ui?.generated !== true || app.cam.ui.toolbarCommands !== 60 || app.cam.ui.toolpathPoints < 5 || app.mobile?.viewport?.width !== 390 || Object.keys(app.screenshots ?? {}).length !== 3) throw new Error('QA-03 production Chrome E2E evidence is incomplete.') if (app.status !== 'pass' || requiredAppWorkflows.some((workflow) => !app.workflows?.includes(workflow)) || app.pageErrors?.length !== 0 || app.desktop?.kernelPreviewSource !== 'bitbybit-occt' || app.desktop.canvas?.uniqueColors < 4 || app.desktop?.commandGroups < 4 || app.desktop?.selectionView !== true || app.desktop?.statusBar !== true || app.uiParity?.commandDialog?.modal !== true || app.uiParity?.preferencesDialog?.modal !== true || app.uiParity?.aboutDialog?.modal !== true || app.uiParity?.contextMenu?.items?.length !== 4 || app.uiParity?.camMenu?.items !== 60 || app.cam?.ui?.generated !== true || app.cam.ui.toolbarCommands !== 60 || app.cam.ui.toolpathPoints < 5 || app.mobile?.viewport?.width !== 390 || Object.keys(app.screenshots ?? {}).length !== 3) throw new Error('QA-03 production Chrome E2E evidence is incomplete.')
if (partdesignReferenceLifecycle.status !== 'pass' || partdesignReferenceLifecycle.crossOriginIsolated !== true || partdesignReferenceLifecycle.initial?.properties?.XLinkSubList?.length !== 2 || partdesignReferenceLifecycle.missing?.binder?.cacheState !== 'cached' || partdesignReferenceLifecycle.relinkedAfterMissing?.binder?.cacheState !== 'linked' || partdesignReferenceLifecycle.reopenedUndo?.binder?.area !== 30 || partdesignReferenceLifecycle.reopenedRedo?.binder?.area !== 54 || partdesignReferenceLifecycle.released?.shapeCount !== 0 || partdesignReferenceLifecycle.released?.kernelReferenceCount !== 0) throw new Error('QA-03 Chrome PartDesign external reference lifecycle evidence is incomplete.')
if (browserMatrix.status !== 'pass' || browserMatrix.browsers?.length !== 2 || !['firefox', 'webkit'].every((engine) => browserMatrix.browsers.some((entry) => entry.engine === engine && entry.status === 'pass' && entry.pageErrors?.length === 0 && entry.canvas?.geometrySource === 'bitbybit-occt'))) throw new Error('QA-03 non-Chrome browser matrix evidence is incomplete.') if (browserMatrix.status !== 'pass' || browserMatrix.browsers?.length !== 2 || !['firefox', 'webkit'].every((engine) => browserMatrix.browsers.some((entry) => entry.engine === engine && entry.status === 'pass' && entry.pageErrors?.length === 0 && entry.canvas?.geometrySource === 'bitbybit-occt'))) throw new Error('QA-03 non-Chrome browser matrix evidence is incomplete.')
if (fcstdFuzz.status !== 'fcstd-parser-fuzz-pass' || fcstdFuzz.cases !== 1000 || fcstdFuzz.counts?.accepted + fcstdFuzz.counts?.rejected !== 1000 || Object.keys(fcstdFuzz.categories ?? {}).length !== 13 || fcstdFuzz.timing?.p95Ms >= 50) throw new Error('QA-04 FCStd fuzz evidence is incomplete.') if (fcstdFuzz.status !== 'fcstd-parser-fuzz-pass' || fcstdFuzz.cases !== 1000 || fcstdFuzz.counts?.accepted + fcstdFuzz.counts?.rejected !== 1000 || Object.keys(fcstdFuzz.categories ?? {}).length !== 13 || fcstdFuzz.timing?.p95Ms >= 50) throw new Error('QA-04 FCStd fuzz evidence is incomplete.')
@@ -72,8 +78,8 @@ console.log(JSON.stringify({
tasks: 8, tasks: 8,
unitSuites: testFiles.length, unitSuites: testFiles.length,
unitTests: testCases, unitTests: testCases,
freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount }, freecad: { successFixtures: 105, failureFixtures: 56, primarySuccessFixtures: 100, primaryFailureFixtures: 51, supplementalSuccessFixtures: 5, supplementalFailureFixtures: 5, sketchConstraints: 19, partdesignCases, partdesignStructureBodies: Object.keys(partdesignStructure.initial.bodyGroups).length, attachmentModes: attachmentModes.registry.modeCount, attachmentImplementedUnion: attachmentModes.registry.implementedUnionCount, attachmentCombinationCases: attachmentCombinations.caseCount, xlinkPropertyKinds: xlinkRelink.propertyTypes.length, partdesignFailureCases: partdesignFailures.summary.cases, partdesignRejectedFailures: partdesignFailures.summary.rejected, partdesignAcceptedEmpty: partdesignFailures.summary.acceptedEmpty, partdesignRevolutionGrooveCases: partdesignRevolutionGroove.summary.cases, partBuilderSuccessCases: partBuilders.summary.successCases, partBuilderFailureCases: partBuilders.summary.failureCases, fcstdRoundTrips: fcstdRoundTrip.scenarioCount },
fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models }, fuzz: { fcstd: fcstdFuzz.cases, geometry: geometryFuzz.cases, solverModels: sketchFuzz.models },
chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes }, chrome: { workflows: app.workflows.length, screenshots: Object.keys(app.screenshots).length, accessibilityNodes: app.screenReader.nodes, partdesignExternalReferenceLifecycle: true },
browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])), browsers: Object.fromEntries(browserMatrix.browsers.map((entry) => [entry.engine, { status: entry.status, persistence: entry.persistence, geometrySource: entry.canvas.geometrySource }])),
}, null, 2)) }, null, 2))

View File

@@ -100,6 +100,6 @@ if (status === 'blocked') {
if (missing.length === 0) missing.push('unclassified CMake configure failure') if (missing.length === 0) missing.push('unclassified CMake configure failure')
} }
await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true }) await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true })
await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'contract-only', exTsn02: 'in_progress', systemExact: false, workerLinked: false, callbacks: [] } }, null, 2)}\n`) await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'production-linked', exTsn02: 'completed', systemExact: false, workerLinked: true, callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'] } }, null, 2)}\n`)
console.log(JSON.stringify({ status, exitCode, report: reportPath, missing, productionPublication: false }, null, 2)) console.log(JSON.stringify({ status, exitCode, report: reportPath, missing, productionPublication: false }, null, 2))
if (status !== 'configured') process.exit(exitCode) if (status !== 'configured') process.exit(exitCode)

View File

@@ -0,0 +1,214 @@
import json
import math
import os
import tempfile
import FreeCAD as App
import Part
import Sketcher # noqa: F401 - registers Sketcher::SketchObject
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
TRANSLATION = App.Vector(1.25, -0.75, 2.0)
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def placement_json(placement):
axis = placement.Rotation.Axis
base = placement.Base
return {
"position": [float(base.x), float(base.y), float(base.z)],
"axis": [float(axis.x), float(axis.y), float(axis.z)],
"angleDegrees": float(placement.Rotation.Angle * 180.0 / math.pi),
}
def support_json(value):
result = []
for obj, sub_elements in value:
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
result.append({
"object": obj.Name,
"subElements": [str(sub) for sub in subs if str(sub)],
})
return result
def finite_placement(value):
return all(math.isfinite(number) for number in value["position"] + value["axis"] + [value["angleDegrees"]])
def add_shape(document, name, shape):
obj = document.addObject("PartDesign::Feature", name)
obj.Shape = shape
return obj
def make_sources(document):
vector = App.Vector
sources = {
"BoxA": add_shape(document, "BoxA", Part.makeBox(4, 5, 6, vector(0, 0, 0))),
"BoxB": add_shape(document, "BoxB", Part.makeBox(3, 4, 7, vector(12, 3, 2))),
"BoxC": add_shape(document, "BoxC", Part.makeBox(2, 6, 4, vector(-8, 4, 1))),
"BoxD": add_shape(document, "BoxD", Part.makeBox(5, 3, 2, vector(3, -9, 5))),
"PlaneXY": add_shape(document, "PlaneXY", Part.makePlane(20, 20, vector(-10, -10, 0), vector(0, 0, 1))),
"PlaneYZ": add_shape(document, "PlaneYZ", Part.makePlane(20, 20, vector(0, -10, -10), vector(1, 0, 0))),
"Circle": add_shape(document, "Circle", Part.Circle(vector(0, 0, 0), vector(0, 0, 1), 5).toShape()),
"Ellipse": add_shape(document, "Ellipse", Part.Ellipse(vector(0, 0, 0), 6, 3).toShape()),
"Hyperbola": add_shape(document, "Hyperbola", Part.Hyperbola(vector(0, 0, 0), 5, 3).toShape(-1.0, 1.0)),
"Parabola": add_shape(document, "Parabola", Part.Parabola().toShape(-2.0, 2.0)),
"Curve": add_shape(document, "Curve", Part.Arc(vector(-4, 0, 0), vector(0, 4, 2), vector(4, 0, 0)).toShape()),
"V0": add_shape(document, "V0", Part.Vertex(vector(0, 0, 0))),
"V1": add_shape(document, "V1", Part.Vertex(vector(5, 0, 0))),
"V2": add_shape(document, "V2", Part.Vertex(vector(0, 4, 2))),
"V3": add_shape(document, "V3", Part.Vertex(vector(1, 2, 6))),
"LX": add_shape(document, "LX", Part.makeLine(vector(0, 0, 0), vector(6, 0, 0))),
"LY": add_shape(document, "LY", Part.makeLine(vector(0, 0, 0), vector(0, 5, 0))),
"LZ": add_shape(document, "LZ", Part.makeLine(vector(0, 0, 0), vector(0, 0, 4))),
"LD": add_shape(document, "LD", Part.makeLine(vector(0, 0, 0), vector(3, 4, 5))),
"LOff": add_shape(document, "LOff", Part.makeLine(vector(0, 4, 2), vector(3, 5, 6))),
}
document.recompute()
return sources
def source_ref(sources, reference_type, index, combination):
if reference_type == "Vertex":
if combination == ["Face", "Vertex"] or combination == ["Vertex", "Face"]:
name = "V1"
elif any(value in ("Curve", "Circle", "Edge") for value in combination):
name = "V1"
elif combination in (["Line", "Vertex"], ["Vertex", "Line"]):
name = "V2"
else:
name = ["V0", "V1", "V2", "V3"][index % 4]
return sources[name], "Vertex1"
if reference_type == "Line":
if combination == ["Line", "Line"] and index == 1:
return sources["LOff"], "Edge1"
return sources[["LX", "LY", "LZ", "LD"][index % 4]], "Edge1"
if reference_type == "Plane|Placement":
return sources["PlaneXY" if index == 0 else "PlaneYZ"], ""
if reference_type in ("Plane", "Face"):
return sources["PlaneXY" if index == 0 else "PlaneYZ"], "Face1"
if reference_type == "Edge":
return sources["Circle"], "Edge1"
if reference_type == "Curve":
return sources["Curve"], "Edge1"
if reference_type == "Circle":
return sources["Circle"], "Edge1"
if reference_type == "Conic":
return sources["Ellipse"], "Edge1"
if reference_type == "Ellipse":
return sources["Ellipse"], "Edge1"
if reference_type == "Hyperbola":
return sources["Hyperbola"], "Edge1"
if reference_type in ("Any", "Any|Placement"):
return sources[["BoxA", "BoxB", "BoxC", "BoxD"][index % 4]], ""
raise ValueError("Unsupported attachment reference category: " + reference_type)
def supports_for(sources, combination):
return [source_ref(sources, reference_type, index, combination) for index, reference_type in enumerate(combination)]
def case_state(obj):
suggestion = obj.Attacher.suggestModes()
placement = placement_json(obj.Placement)
return {
"mapMode": str(obj.MapMode),
"support": support_json(obj.AttachmentSupport),
"placement": placement,
"finitePlacement": finite_placement(placement),
"state": [str(value) for value in obj.State],
"status": str(obj.getStatusString()),
"positionBySupport": bool(obj.positionBySupport()),
"suggestedModes": list(suggestion["allApplicableModes"]),
"suggestionMessage": suggestion["message"],
}
document = App.newDocument("AttachmentCombinationOracle")
sources = make_sources(document)
engine_specs = {
"plane": ("PartDesign::Plane", document.addObject("PartDesign::Body", "PlaneCases")),
"line": ("PartDesign::Line", document.addObject("PartDesign::Body", "LineCases")),
"point": ("PartDesign::Point", document.addObject("PartDesign::Body", "PointCases")),
"sketch": ("Sketcher::SketchObject", document.addObject("PartDesign::Body", "SketchCases")),
}
seed_objects = {
name: body.newObject(type_id, "Seed" + name.title())
for name, (type_id, body) in engine_specs.items()
}
document.recompute()
cases = []
case_objects = {}
for engine_name, seed in seed_objects.items():
type_id, body = engine_specs[engine_name]
for mode in list(seed.Attacher.ImplementedModes):
combinations = seed.Attacher.getModeInfo(mode)["ReferenceCombinations"]
for combination_index, combination_value in enumerate(combinations):
combination = [str(value) for value in combination_value]
case_id = "%s-%s-%02d" % (engine_name, mode, combination_index)
object_name = "Case%04d" % (len(cases) + 1)
obj = body.newObject(type_id, object_name)
obj.Label = case_id
obj.AttachmentSupport = supports_for(sources, combination)
obj.MapMode = mode
cases.append({
"id": case_id,
"objectName": object_name,
"engine": engine_name,
"typeId": type_id,
"mode": str(mode),
"modeIndex": int(seed.Attacher.getModeInfo(mode)["ModeIndex"]),
"combinationIndex": combination_index,
"referenceCombination": combination,
})
case_objects[object_name] = obj
for seed in seed_objects.values():
seed.Document.removeObject(seed.Name)
document.recompute()
initial = {name: case_state(obj) for name, obj in case_objects.items()}
for source in sources.values():
source.Placement.Base = source.Placement.Base + TRANSLATION
document.recompute()
mutated = {name: case_state(obj) for name, obj in case_objects.items()}
with tempfile.TemporaryDirectory(prefix="freecad-attachment-combinations-") as temp_dir:
path = os.path.join(temp_dir, "AttachmentCombinationOracle.FCStd")
document.saveAs(path)
App.closeDocument(document.Name)
reopened = App.openDocument(path)
reopened.recompute()
roundtrip = {case["objectName"]: case_state(reopened.getObject(case["objectName"])) for case in cases}
App.closeDocument(reopened.Name)
for case in cases:
name = case["objectName"]
case["initial"] = initial[name]
case["mutated"] = mutated[name]
case["roundtrip"] = roundtrip[name]
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-attachment-combination-oracle",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"tolerance": 1e-7,
"translation": [float(TRANSLATION.x), float(TRANSLATION.y), float(TRANSLATION.z)],
"caseCount": len(cases),
"engineCaseCounts": {
engine: len([case for case in cases if case["engine"] == engine])
for engine in engine_specs
},
"cases": cases,
}
print("FREECAD_ATTACHMENT_COMBINATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -277,8 +277,16 @@ def collect_case(case_id, factory, index, directory):
after_stages = [entry for obj in reopened.Objects if (entry := stage_report(obj)) is not None] after_stages = [entry for obj in reopened.Objects if (entry := stage_report(obj)) is not None]
after_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in after_stages for entry in stage["names"]} after_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in after_stages for entry in stage["names"]}
roundtrip_drift = sum(1 for key, value in before_names.items() if after_names.get(key) != value) roundtrip_drift = sum(1 for key, value in before_names.items() if after_names.get(key) != value)
resaved_path = os.path.join(directory, "%s-resaved.FCStd" % case_id)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name) App.closeDocument(reopened.Name)
return {"id": case_id, "finalObject": final_name, "stages": stages, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "status": "pass"} resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_stages = [entry for obj in resaved.Objects if (entry := stage_report(obj)) is not None]
resaved_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in resaved_stages for entry in stage["names"]}
resave_name_drift = sum(1 for key, value in before_names.items() if resaved_names.get(key) != value)
App.closeDocument(resaved.Name)
return {"id": case_id, "finalObject": final_name, "stages": stages, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "resaveNameDrift": resave_name_drift, "nativeDesktopResaveCovered": True, "status": "pass"}
except Exception as error: except Exception as error:
return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)} return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)}
finally: finally:
@@ -309,6 +317,6 @@ report = {
"gitCommit": FREECAD_COMMIT, "gitCommit": FREECAD_COMMIT,
"status": "pass" if len(cases) == 30 and all(case["status"] == "pass" for case in cases) else "failed", "status": "pass" if len(cases) == 30 and all(case["status"] == "pass" for case in cases) else "failed",
"cases": cases, "cases": cases,
"summary": {"cases": len(cases), "passed": sum(1 for case in cases if case["status"] == "pass"), "failed": sum(1 for case in cases if case["status"] != "pass")}, "summary": {"cases": len(cases), "passed": sum(1 for case in cases if case["status"] == "pass"), "failed": sum(1 for case in cases if case["status"] != "pass"), "roundtripNameDrift": sum(case.get("roundtripNameDrift", 0) for case in cases), "resaveNameDrift": sum(case.get("resaveNameDrift", 0) for case in cases), "nativeDesktopResaveCases": sum(1 for case in cases if case.get("nativeDesktopResaveCovered") is True)},
} }
print("FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":"))) print("FREECAD_COMPOSITE_HISTORY_ELEMENTMAP_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -0,0 +1,347 @@
export const FREECAD_NAMING_CALLBACKS = [
'freecadNamingAbiVersion',
'freecadNamingCapabilitiesJson',
'freecadNamingEvidenceJson',
]
export const FREECAD_NAMING_OPERATIONS = [
'fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe',
'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness',
'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform',
]
const shapeStep = (module, method, ...args) => {
const shape = module[method](...args)
try {
return module.shapeToStep(shape)
} finally {
shape.delete?.()
}
}
const box = (module, ...args) => shapeStep(module, 'makeBox', ...args)
const placedBox = (module, ...args) => shapeStep(module, 'makeBoxPlaced', ...args)
const rectangle = (module, ...args) => shapeStep(module, 'makeRectangleFace', ...args)
const placedRectangle = (module, ...args) => shapeStep(module, 'makeRectangleFacePlaced', ...args)
const line = (module, ...args) => shapeStep(module, 'makeLineWire', ...args)
const input = (inputId, role, objectTag, step) => ({
inputId,
objectId: `production-${inputId}`,
role,
objectTag,
step,
})
const fixtures = {
fuse(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = placedBox(module, 5, 5, 5, 8, 0, 0)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'fuse'), inputs: [input('object', 'object', 101, objectStep), input('tool', 'tool', 102, toolStep)] }
},
cut(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = box(module, 5, 5, 5)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'cut'), inputs: [input('object', 'object', 111, objectStep), input('tool', 'tool', 112, toolStep)] }
},
common(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = placedBox(module, 5, 5, 5, 8, 2, 2)
return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'common'), inputs: [input('object', 'object', 121, objectStep), input('tool', 'tool', 122, toolStep)] }
},
rotate(module) {
const objectStep = placedBox(module, 2, 1, 1, 1, 0, 0)
return { response: module.rotateHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 30), inputs: [input('object', 'object', 131, objectStep)] }
},
pad(module) {
const objectStep = rectangle(module, 2, 3)
return { response: module.prismHistoryFromStep(objectStep, 0, 0, 5), inputs: [input('object', 'object', 141, objectStep)] }
},
pocket(module) {
const objectStep = box(module, 10, 10, 10)
const toolStep = rectangle(module, 2, 3)
return { response: module.pocketHistoryFromStep(objectStep, toolStep, 0, 0, 5), inputs: [input('object', 'object', 151, objectStep), input('tool', 'tool', 152, toolStep)] }
},
loft(module) {
const objectStep = placedRectangle(module, 2, 2, 0, 0, 0)
const toolStep = placedRectangle(module, 2, 2, 0, 0, 5)
return { response: module.loftHistoryFromStep(objectStep, toolStep, false), inputs: [input('object', 'object', 161, objectStep), input('tool', 'tool', 162, toolStep)] }
},
pipe(module) {
const objectStep = rectangle(module, 2, 2)
const toolStep = line(module, 0, 0, 0, 0, 0, 5)
return { response: module.pipeHistoryFromStep(objectStep, toolStep), inputs: [input('object', 'object', 171, objectStep), input('tool', 'tool', 172, toolStep)] }
},
revolution(module) {
const objectStep = rectangle(module, 2, 3)
return { response: module.revolutionHistoryFromStep(objectStep, -1, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 181, objectStep)] }
},
groove(module) {
const objectStep = placedBox(module, 10, 10, 8, -5, 0, -4)
const toolStep = placedRectangle(module, 1, 3, 0.5, 2, 0)
return { response: module.grooveHistoryFromStep(objectStep, toolStep, 0, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 191, objectStep), input('tool', 'tool', 192, toolStep)] }
},
fillet(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.filletHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 201, objectStep)] }
},
chamfer(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.chamferHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 211, objectStep)] }
},
hole(module) {
const objectStep = box(module, 10, 10, 10)
return { response: module.holeHistoryFromStep(objectStep, 1, 10, 0, 0, -5, 0, 0, 1), inputs: [input('object', 'object', 221, objectStep)] }
},
draft(module) {
const objectStep = box(module, 10, 10, 10)
return { response: module.draftHistoryFromStep(objectStep, 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 1, false), inputs: [input('object', 'object', 231, objectStep)] }
},
thickness(module) {
const objectStep = box(module, 6, 6, 6)
return { response: module.thicknessHistoryFromStep(objectStep, 1, -0.4, false), inputs: [input('object', 'object', 241, objectStep)] }
},
'linear-pattern'(module) {
const objectStep = box(module, 2, 2, 2)
return { response: module.linearPatternHistoryFromStep(objectStep, 2, 0, 0), inputs: [input('object', 'object', 251, objectStep)] }
},
'polar-pattern'(module) {
const objectStep = placedBox(module, 2, 1, 1, -1, -0.5, -0.5)
return { response: module.polarPatternHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 90), inputs: [input('object', 'object', 261, objectStep)] }
},
mirrored(module) {
const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
return { response: module.mirroredHistoryFromStep(objectStep, 0, 0, 0, 1, 0, 0), inputs: [input('object', 'object', 271, objectStep)] }
},
'multi-transform'(module) {
const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
return {
response: module.multiTransformHistoryFromStep(objectStep, [
{ type: 'linear', direction: [1, 0, 0] },
{ type: 'mirrored', axisOrigin: [0, 0, 0], direction: [1, 0, 0] },
]),
inputs: [input('object', 'object', 281, objectStep)],
}
},
}
const serializableHistory = (response) => ({
provider: response.provider,
occtVersion: response.occtVersion,
records: response.records,
hasModified: response.hasModified,
hasGenerated: response.hasGenerated,
hasDeleted: response.hasDeleted,
resultStep: response.resultStep,
resultBrep: response.resultBrep,
})
const externallySourcedRecords = (records, inputs) => {
const sources = new Set(inputs.flatMap(({ inputId, role }) => [inputId, role]).filter(Boolean))
if (inputs.length > 0) sources.add('object')
if (inputs.length > 1) sources.add('tool')
return records.filter((record) => sources.has(record.sourceId ?? record.source))
}
const resolveInput = (record, inputs) => {
const source = record.sourceId ?? record.source
return inputs.find((entry) => entry.inputId === source)
?? inputs.find((entry) => entry.role === source)
?? (source === 'object' ? inputs[0] : undefined)
?? (source === 'tool' ? inputs[1] : undefined)
}
const persistentSourceName = (record, sourceInput) => {
const prior = sourceInput.namingEvidence?.mappedNames?.find((mapped) => mapped.kind === record.kind && mapped.resultIndex === record.sourceIndex)
const title = `${record.kind[0].toUpperCase()}${record.kind.slice(1)}`
return prior?.resultPersistentId ?? `${title}${record.sourceIndex + 1}`
}
export const classifyFreeCadNamingProvenance = (records, inputs) => {
const externalRecords = externallySourcedRecords(records, inputs)
const grouped = new Map()
for (const record of externalRecords) {
if (record.relation === 'deleted') continue
const sourceInput = resolveInput(record, inputs)
if (!sourceInput) throw new Error(`Native source ${record.sourceId ?? record.source} has no transport input.`)
const resultKind = record.resultKind || record.kind
const indexes = Array.isArray(record.resultIndexes) ? record.resultIndexes : [record.resultIndex]
for (const resultIndex of indexes) {
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0) throw new Error(`Native result ${resultKind}:${String(resultIndex)} is invalid.`)
const key = `${resultKind}:${resultIndex}`
const entries = grouped.get(key) ?? []
entries.push({
record: { ...record, resultKind, resultIndex, resultIndexes: undefined },
resultKind,
resultIndex,
candidate: {
inputId: sourceInput.inputId,
objectId: sourceInput.objectId || sourceInput.inputId,
persistentId: persistentSourceName(record, sourceInput),
...(sourceInput.stageId ? { stageId: sourceInput.stageId } : {}),
relation: record.relation,
sourceKind: record.kind,
sourceIndex: record.sourceIndex,
},
})
grouped.set(key, entries)
}
}
const selected = externalRecords.filter((record) => record.relation === 'deleted').map((record) => ({ ...record }))
const ambiguities = []
let duplicateRecordCount = 0
let relationConflictCount = 0
for (const entries of grouped.values()) {
const unique = new Map()
for (const entry of entries) {
const candidate = entry.candidate
const key = `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}\u0000${candidate.relation}`
if (!unique.has(key)) unique.set(key, entry)
}
duplicateRecordCount += entries.length - unique.size
const distinctSources = new Set([...unique.values()].map(({ candidate }) => `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}`))
if (distinctSources.size > 1) {
ambiguities.push({ resultKind: entries[0].resultKind, resultIndex: entries[0].resultIndex, candidates: [...unique.values()].map(({ candidate }) => candidate) })
continue
}
const relations = new Set([...unique.values()].map(({ candidate }) => candidate.relation))
if (relations.size > 1) relationConflictCount += 1
const stable = [...unique.values()].find(({ candidate }) => candidate.relation === 'modified') ?? [...unique.values()][0]
selected.push(stable.record)
}
return {
records: selected,
ambiguities,
externalRecordCount: externalRecords.length,
internalRecordCount: records.length - externalRecords.length,
duplicateRecordCount,
relationConflictCount,
}
}
const validateEvidence = (operation, evidence, recordCount) => {
if (evidence.status !== 'native-evidence') throw new Error(`${operation}: callback did not return native evidence.`)
if (evidence.mappedNames?.length !== recordCount || recordCount < 1) throw new Error(`${operation}: callback did not map every selected native history result.`)
// A first-generation mapping from plain Face1/Edge1 names legitimately has
// no interned strings. Chained mappings below must restore and grow the table.
if (!Array.isArray(evidence.stringHasher?.entries)) throw new Error(`${operation}: StringHasher evidence is absent.`)
if (!Array.isArray(evidence.elementMap2?.maps) || evidence.elementMap2.maps.length < 1) throw new Error(`${operation}: ElementMap2 evidence is empty.`)
const hasherIds = new Set(evidence.stringHasher.entries.map((entry) => entry.id))
const tokens = evidence.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
for (const mapped of evidence.mappedNames) {
const reference = mapped.reference
if (typeof reference?.name !== 'string' || !reference.name) throw new Error(`${operation}: MappedNameRef is malformed.`)
if (reference.name.startsWith('#')) {
if (!Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !reference.stringIds.every((id) => hasherIds.has(id))) throw new Error(`${operation}: MappedNameRef is not closed over StringHasher.`)
if (!tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error(`${operation}: MappedNameRef is absent from ElementMap2.`)
}
}
}
export const runFreeCadNamingProductionMatrix = async (module) => {
if (!FREECAD_NAMING_CALLBACKS.every((name) => typeof module[name] === 'function')) throw new Error('Production Worker omits a required FreeCAD naming callback.')
if (module.freecadNamingAbiVersion() !== 1) throw new Error('Production Worker FreeCAD naming ABI version is not 1.')
const descriptor = JSON.parse(module.freecadNamingCapabilitiesJson())
if (descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Production Worker naming descriptor is not source locked.')
if (descriptor.operations?.length !== FREECAD_NAMING_OPERATIONS.length || FREECAD_NAMING_OPERATIONS.some((operation) => !descriptor.operations.includes(operation))) throw new Error('Production Worker naming descriptor does not cover all native builders.')
const cases = []
let chainedSource
for (let index = 0; index < FREECAD_NAMING_OPERATIONS.length; index += 1) {
const operation = FREECAD_NAMING_OPERATIONS[index]
const { response, inputs } = fixtures[operation](module)
try {
if (response.provider !== 'occt-native' || !/^8\./.test(response.occtVersion) || !response.resultStep?.startsWith('ISO-10303-21;')) throw new Error(`${operation}: native builder response is invalid.`)
const provenance = classifyFreeCadNamingProvenance(response.records, inputs)
const records = provenance.records
const stableResultCount = records.filter((record) => record.relation !== 'deleted').length
if (stableResultCount < 1 && provenance.ambiguities.length < 1) throw new Error(`${operation}: native builder returned no usable result history.`)
const request = {
schemaVersion: 1,
requestId: `production-${operation}-request`,
documentId: 'production-naming-matrix',
documentVersion: index + 1,
operationId: `production-${operation}`,
operation,
stageId: `production:${operation}:stage`,
resultObjectId: `production:${operation}:result`,
resultObjectTag: 1000 + index,
inputs,
stages: [{ stageId: `production:${operation}:stage`, operation, inputIds: inputs.map(({ inputId }) => inputId), ordinal: 0 }],
resultStep: response.resultStep,
resultBrep: response.resultBrep,
...(provenance.ambiguities.length > 0 ? { historyAmbiguities: provenance.ambiguities } : {}),
history: { ...serializableHistory(response), records },
}
const evidence = stableResultCount > 0 ? JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(request))) : undefined
if (evidence) validateEvidence(operation, evidence, stableResultCount)
cases.push({ operation, status: 'pass', namingStatus: evidence ? (provenance.ambiguities.length > 0 ? 'mixed-native-and-ambiguous' : 'native-evidence') : 'ambiguous', callbackExecuted: Boolean(evidence), historyRecords: response.records.length, externallySourcedRecords: provenance.externalRecordCount, internalRecordsExcluded: provenance.internalRecordCount, duplicateRecordsCollapsed: provenance.duplicateRecordCount, modifiedRelationPrecedence: provenance.relationConflictCount, selectedRecords: stableResultCount, ambiguousResults: provenance.ambiguities.length, provenanceResults: stableResultCount + provenance.ambiguities.length, mappedNames: evidence?.mappedNames.length ?? 0, stringHasherEntries: evidence?.stringHasher.entries.length ?? 0, elementMaps: evidence?.elementMap2.maps.length ?? 0 })
if (operation === 'cut' && evidence) chainedSource = { request, evidence, record: records.find((record) => record.relation !== 'deleted') }
} finally {
response.result?.delete?.()
}
}
const symmetricObjectStep = box(module, 10, 10, 10)
const symmetricToolStep = box(module, 10, 10, 10)
const symmetricResponse = module.booleanHistoryFromStep(symmetricObjectStep, symmetricToolStep, 'common')
let isomorphicCorpus
try {
const symmetricInputs = [input('symmetric-object', 'object', 301, symmetricObjectStep), input('symmetric-tool', 'tool', 302, symmetricToolStep)]
const provenance = classifyFreeCadNamingProvenance(symmetricResponse.records, symmetricInputs)
if (provenance.ambiguities.length < 1 || provenance.ambiguities.some(({ candidates }) => candidates.length < 2)) throw new Error('Symmetric Common did not retain its non-unique native source candidates.')
const candidateSets = provenance.ambiguities.map(({ resultKind, resultIndex, candidates }) => ({ result: `${resultKind}:${resultIndex}`, candidates: candidates.map(({ objectId, persistentId, stageId }) => `${objectId}:${persistentId}:${stageId || ''}`).sort() }))
isomorphicCorpus = {
operation: 'common',
fixture: 'coincident-equal-boxes',
status: 'pass',
historyRecords: symmetricResponse.records.length,
uniqueResults: provenance.records.filter((record) => record.relation !== 'deleted').length,
ambiguousResults: provenance.ambiguities.length,
candidateSets,
arrayOrderFallbackUsed: false,
}
} finally {
symmetricResponse.result?.delete?.()
}
if (!chainedSource) throw new Error('Production naming matrix lacks its chained cut source.')
const nextIndex = chainedSource.record.resultIndex + 1
const chainedRequest = {
...chainedSource.request,
requestId: 'production-cut-chained-request',
documentVersion: 20,
operationId: 'production-cut-chained',
stageId: 'production:cut:chained-stage',
resultObjectId: 'production:cut:chained-result',
resultObjectTag: 1100,
inputs: [{ inputId: 'object', objectId: chainedSource.request.resultObjectId, role: 'object', stageId: chainedSource.request.stageId, objectTag: chainedSource.request.resultObjectTag, step: chainedSource.request.resultStep, namingEvidence: chainedSource.evidence }],
stages: [{ stageId: 'production:cut:chained-stage', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
history: { ...chainedSource.request.history, records: [{ relation: 'modified', source: 'object', kind: chainedSource.record.resultKind || chainedSource.record.kind, sourceIndex: chainedSource.record.resultIndex, resultIndex: nextIndex }] },
}
const chainedEvidence = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(chainedRequest)))
validateEvidence('cut-chained', chainedEvidence, 1)
if (chainedEvidence.stringHasher.entries.length <= chainedSource.evidence.stringHasher.entries.length) throw new Error('Production Worker did not restore and extend the prior StringHasher table.')
const invalid = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify({ ...chainedSource.request, requestId: 'production-invalid-history', history: { ...chainedSource.request.history, records: [] } })))
if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Production Worker did not fail closed for missing native history.')
return {
schemaVersion: 1,
status: 'pass',
implementation: 'freecad-linked',
productionWorkerLinked: true,
occtVersion: module.occtVersion(),
freecadVersion: descriptor.freecadVersion,
sourceCommit: descriptor.sourceCommit,
callbacks: [...FREECAD_NAMING_CALLBACKS],
operations: [...FREECAD_NAMING_OPERATIONS],
cases,
callbackExecutedCases: cases.filter(({ callbackExecuted }) => callbackExecuted).length,
ambiguityOnlyCases: cases.filter(({ namingStatus }) => namingStatus === 'ambiguous').length,
isomorphicCorpus,
chainedStage: { mappedNames: chainedEvidence.mappedNames.length, stringHasherEntries: chainedEvidence.stringHasher.entries.length },
invalidHistoryRejected: true,
systemExact: false,
}
}

View File

@@ -0,0 +1,231 @@
import json
import os
import tempfile
import FreeCAD as App
import Part # noqa: F401 - registers Part and PartDesign shapes
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def document_open(name):
return name in App.listDocuments()
def object_ref(obj):
if obj is None:
return None
return {"document": obj.Document.Name, "object": obj.Name}
def xlink_ref(value):
return object_ref(value)
def xlink_sub_ref(value):
if not value or value[0] is None:
return None
obj, sub_elements = value
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
return {**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]}
def xlink_list_ref(value):
return [object_ref(obj) for obj in value if obj is not None]
def xlink_sub_list_ref(value):
output = []
for obj, sub_elements in value:
if obj is None:
continue
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
output.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
return output
def property_status(holder, name):
return [str(value) for value in holder.getPropertyStatus(name)]
def holder_state(holder):
return {
"xLink": xlink_ref(holder.XLink),
"xLinkSub": xlink_sub_ref(holder.XLinkSub),
"xLinkList": xlink_list_ref(holder.XLinkList),
"xLinkSubList": xlink_sub_list_ref(holder.XLinkSubList),
"propertyStatus": {
name: property_status(holder, name)
for name in ["XLink", "XLinkSub", "XLinkList", "XLinkSubList"]
},
"state": [str(value) for value in holder.State],
"status": str(holder.getStatusString()),
}
def binder_state(binder):
support = []
for obj, sub_elements in binder.Support:
subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
support.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
return {
"support": support,
"area": float(binder.Shape.Area),
"shapeNull": bool(binder.Shape.isNull()),
"state": [str(value) for value in binder.State],
"status": str(binder.getStatusString()),
}
def linked_dimensions(holder):
return {
"xLinkLength": float(holder.XLink.Length) if holder.XLink else None,
"xLinkSubWidth": float(holder.XLinkSub[0].Width) if holder.XLinkSub and holder.XLinkSub[0] else None,
"xLinkListHeights": [float(obj.Height) for obj in holder.XLinkList if obj is not None],
"xLinkSubListAreas": [float(obj.getSubObject(sub).Area) for obj, subs in holder.XLinkSubList for sub in subs],
}
with tempfile.TemporaryDirectory(prefix="freecad-xlink-relink-") as temp_dir:
source_path = os.path.join(temp_dir, "XLinkSource.FCStd")
consumer_path = os.path.join(temp_dir, "XLinkConsumer.FCStd")
missing_path = os.path.join(temp_dir, "XLinkSource.missing.FCStd")
source = App.newDocument("XLinkSource")
source_body = source.addObject("PartDesign::Body", "SourceBody")
source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
source_box.Length = 4
source_box.Width = 5
source_box.Height = 6
second_body = source.addObject("PartDesign::Body", "SecondBody")
second_box = second_body.newObject("PartDesign::AdditiveBox", "SecondBox")
second_box.Length = 3
second_box.Width = 7
second_box.Height = 2
source.recompute()
source.saveAs(source_path)
consumer = App.newDocument("XLinkConsumer")
consumer.saveAs(consumer_path)
holder = consumer.addObject("App::FeaturePython", "XLinkHolder")
holder.addProperty("App::PropertyXLink", "XLink", "External")
holder.addProperty("App::PropertyXLinkSub", "XLinkSub", "External")
holder.addProperty("App::PropertyXLinkList", "XLinkList", "External")
holder.addProperty("App::PropertyXLinkSubList", "XLinkSubList", "External")
holder.XLink = source_box
holder.XLinkSub = (source_box, ["Face1"])
holder.XLinkList = [source_box, second_box]
holder.XLinkSubList = [(source_box, ["Face1"]), (second_box, ["Face1", "Face2"])]
binder_body = consumer.addObject("PartDesign::Body", "BinderBody")
binder = binder_body.newObject("PartDesign::SubShapeBinder", "ExternalBinder")
binder.Support = [(source_box, ("Face1",))]
consumer.recompute()
consumer.saveAs(consumer_path)
source.save()
consumer.save()
initial = {
"holder": holder_state(holder),
"dimensions": linked_dimensions(holder),
"binder": binder_state(binder),
"sourceDocuments": sorted(document.Name for document in App.listDocuments().values()),
}
App.closeDocument(source.Name)
consumer.recompute()
source_closed = {
"holder": holder_state(holder),
"binder": binder_state(binder),
"sourceOpen": document_open("XLinkSource"),
}
reopened_source = App.openDocument(source_path)
reopened_source.recompute()
consumer.recompute()
relink_after_close = {
"holder": holder_state(holder),
"dimensions": linked_dimensions(holder),
"binder": binder_state(binder),
"sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is reopened_source,
}
App.closeDocument(consumer.Name)
App.closeDocument(reopened_source.Name)
os.rename(source_path, missing_path)
missing_consumer = App.openDocument(consumer_path)
missing_consumer.recompute()
missing_holder = missing_consumer.getObject("XLinkHolder")
missing_binder = missing_consumer.getObject("ExternalBinder")
missing_source = {
"holder": holder_state(missing_holder),
"binder": binder_state(missing_binder),
"sourceOpen": document_open("XLinkSource"),
}
os.rename(missing_path, source_path)
restored_source = App.openDocument(source_path)
restored_source.recompute()
missing_consumer.recompute()
restored_holder = missing_consumer.getObject("XLinkHolder")
restored_binder = missing_consumer.getObject("ExternalBinder")
relink_after_missing = {
"holder": holder_state(restored_holder),
"dimensions": linked_dimensions(restored_holder),
"binder": binder_state(restored_binder),
"sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is restored_source,
}
restored_box = restored_source.getObject("SourceBox")
restored_second = restored_source.getObject("SecondBox")
restored_box.Length = 8
restored_box.Width = 9
restored_second.Height = 4
restored_source.recompute()
missing_consumer.recompute()
edited = {
"holder": holder_state(restored_holder),
"dimensions": linked_dimensions(restored_holder),
"binder": binder_state(restored_binder),
"sourceFaceArea": float(restored_box.Shape.Face1.Area),
}
restored_source.save()
missing_consumer.save()
App.closeDocument(missing_consumer.Name)
App.closeDocument(restored_source.Name)
final_source = App.openDocument(source_path)
final_consumer = App.openDocument(consumer_path)
final_source.recompute()
final_consumer.recompute()
final_holder = final_consumer.getObject("XLinkHolder")
final_binder = final_consumer.getObject("ExternalBinder")
final_roundtrip = {
"holder": holder_state(final_holder),
"dimensions": linked_dimensions(final_holder),
"binder": binder_state(final_binder),
"sourceFaceArea": float(final_source.getObject("SourceBox").Shape.Face1.Area),
}
App.closeDocument(final_consumer.Name)
App.closeDocument(final_source.Name)
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-xlink-relink-oracle",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "pass",
"tolerance": 1e-7,
"propertyTypes": ["App::PropertyXLink", "App::PropertyXLinkSub", "App::PropertyXLinkList", "App::PropertyXLinkSubList"],
"initial": initial,
"sourceClosed": source_closed,
"relinkAfterClose": relink_after_close,
"missingSource": missing_source,
"relinkAfterMissing": relink_after_missing,
"edited": edited,
"finalRoundtrip": final_roundtrip,
}
print("FREECAD_XLINK_RELINK_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))

View File

@@ -607,7 +607,7 @@ async function smoke() {
FREECAD_SOURCE_OFFLINE: '1', FREECAD_SOURCE_OFFLINE: '1',
OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'), OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'),
} }
for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) { for (const script of ['check:runtime', 'check:freecad-source', 'build:freecad-naming-source-probe', 'test:freecad-naming-source-probe', 'check:freecad-naming-sdk-readiness', 'check:freecad-wasm-sdk-build-plan', 'check:freecad-naming-next-tasks', 'publish:occt-history', 'check:freecad-naming-production', 'check:freecad-isomorphic-provenance', 'check:freecad-native-property-semantics', 'check:freecad-private-naming-boundary', 'check:occt-history-artifact', 'test:occt-history', 'test:planegcs', 'build']) {
print(`[offline:smoke] npm run ${script}`) print(`[offline:smoke] npm run ${script}`)
await run(resolve(root, 'npmw'), ['run', script], { env: environment }) await run(resolve(root, 'npmw'), ['run', script], { env: environment })
} }

View File

@@ -0,0 +1,18 @@
import { copyFile, mkdir, 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 dist = resolve(root, 'native/occt-history/dist')
const publicRoot = resolve(root, 'public/native/occt-history')
const artifacts = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const factory = (await import(pathToFileURL(resolve(dist, artifacts[0])).href)).default
const module = await factory({ locateFile: (path) => resolve(dist, path) })
const callbacks = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
if (!callbacks.every((name) => typeof module[name] === 'function') || module.freecadNamingAbiVersion() !== 1) throw new Error('Refusing to publish an OCCT history artifact without the locked FreeCAD naming ABI.')
const descriptor = JSON.parse(module.freecadNamingCapabilitiesJson())
if (descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Refusing to publish an unlocked FreeCAD naming ABI.')
await Promise.all(artifacts.map((name) => readFile(resolve(dist, name))))
await mkdir(publicRoot, { recursive: true })
await Promise.all(artifacts.map((name) => copyFile(resolve(dist, name), resolve(publicRoot, name))))
console.log(JSON.stringify({ status: 'occt-history-published', implementation: 'freecad-linked', artifacts, callbacks }, null, 2))

View File

@@ -0,0 +1,80 @@
import { createHash } from 'node:crypto'
import { createReadStream, existsSync } from 'node:fs'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { extname, normalize, resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const productionRoot = resolve(root, 'public/native/occt-history')
const matrixPath = resolve(root, 'scripts/freecad-naming-production-matrix.mjs')
const reportPath = resolve(root, 'config/chrome-freecad-naming-production-verification.json')
const chrome = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
for (const name of artifactNames) if (!existsSync(resolve(productionRoot, name))) throw new Error(`Missing production Worker artifact: ${name}`)
if (!existsSync(chrome)) throw new Error(`Chrome executable is unavailable: ${chrome}`)
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(productionRoot, name)
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const matrixContent = await readFile(matrixPath)
const harness = { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixContent.length, sha256: createHash('sha256').update(matrixContent).digest('hex') }
let resolveReport
let rejectReport
const reportPromise = new Promise((resolveValue, rejectValue) => { resolveReport = resolveValue; rejectReport = rejectValue })
const html = `<!doctype html><meta charset="utf-8"><title>Production FreeCAD naming Worker</title><script type="module">
const send = async (report) => fetch('/__report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
try {
if (!crossOriginIsolated || typeof SharedArrayBuffer !== 'function') throw new Error('Production naming harness requires cross-origin isolation and SharedArrayBuffer.')
const [{ default: createModule }, { runFreeCadNamingProductionMatrix }] = await Promise.all([import('/production/bitbybit-occt-history.js'), import('/matrix.mjs')])
const module = await createModule({ locateFile: (path) => new URL('/production/' + path, location.href).href })
const result = await runFreeCadNamingProductionMatrix(module)
await send({ ...result, runtime: 'chrome', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated })
} catch (error) {
await send({ schemaVersion: 1, status: 'failed', runtime: 'chrome', browserId: 'chrome', error: error instanceof Error ? error.stack || error.message : String(error), productionWorkerLinked: false, systemExact: false })
}
</script>`
const contentTypes = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream' }
const server = createServer((request, response) => {
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
if (request.method === 'POST' && request.url === '/__report') {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk) => { body += chunk })
request.on('end', () => {
try { resolveReport(JSON.parse(body)); response.writeHead(204); response.end() }
catch (error) { rejectReport(error); response.writeHead(400); response.end(String(error)) }
})
return
}
if (request.url === '/' || request.url === '/index.html') { response.setHeader('content-type', 'text/html'); response.end(html); return }
const file = request.url === '/matrix.mjs'
? matrixPath
: request.url?.startsWith('/production/') ? normalize(resolve(productionRoot, decodeURIComponent(request.url.slice('/production/'.length)))) : ''
if (!file || (file !== matrixPath && !file.startsWith(productionRoot)) || !existsSync(file)) { response.writeHead(404); response.end('not found'); return }
response.setHeader('content-type', contentTypes[extname(file)] || 'application/octet-stream')
createReadStream(file).pipe(response)
})
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
const port = server.address().port
const profile = await createChromeProfile('freecad-naming-production')
const child = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/`], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] })
let stderr = ''
child.stderr.on('data', (chunk) => { stderr += String(chunk) })
const timeout = setTimeout(() => { rejectReport(new Error(`Chrome production naming harness timed out. ${stderr.slice(-2000)}`)); child.kill('SIGTERM') }, 300_000)
try {
const report = { ...(await reportPromise), artifacts, harness, generatedAt: new Date().toISOString() }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
if (report.status !== 'pass') process.exitCode = 1
} finally {
clearTimeout(timeout)
child.kill('SIGTERM')
await new Promise((resolveServer) => server.close(resolveServer))
await removeChromeProfile(profile)
}

View File

@@ -0,0 +1,78 @@
import { createServer } from 'node:http'
import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { spawn } from 'node:child_process'
import { createChromeProfile, removeChromeProfile } from './chrome-profile.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const reportPath = resolve(root, 'config/chrome-partdesign-reference-lifecycle-verification.json')
const evidencePaths = [
'public/chrome-partdesign-reference-lifecycle-harness.html',
'src/facade/externalPartDesignLinks.ts',
'src/facade/mockFacade.ts',
'src/facade/geometryRuntime.ts',
'src/facade/geometryWorker.ts',
'node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.a4a6ec2a.wasm',
]
const collectEvidenceInputs = async () => Promise.all(evidencePaths.map(async (path) => {
const bytes = await readFile(resolve(root, path))
return { path, bytes: bytes.byteLength, sha256: createHash('sha256').update(bytes).digest('hex') }
}))
const waitForPort = (child) => new Promise((resolvePort, reject) => {
let output = ''
const timer = setTimeout(() => reject(new Error(`Vite did not announce a port: ${output.slice(-2000)}`)), 30_000)
const onData = (chunk) => { output += String(chunk); const match = output.match(/http:\/\/127\.0\.0\.1:(\d+)/); if (match) { clearTimeout(timer); resolvePort(Number(match[1])) } }
child.stdout.on('data', onData)
child.stderr.on('data', onData)
})
const vite = spawn('./nodew', ['node_modules/vite/bin/vite.js', '--host', '127.0.0.1'], { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] })
let vitePort
try { vitePort = await waitForPort(vite) } catch (error) { vite.kill('SIGTERM'); throw error }
let resolveReport
const reportPromise = new Promise((resolveValue) => { resolveReport = resolveValue })
const server = createServer(async (request, response) => {
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
response.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
response.setHeader('Cross-Origin-Resource-Policy', 'same-origin')
if (request.method === 'POST' && request.url === '/__partdesign-reference-lifecycle-report') {
let body = ''
request.setEncoding('utf8')
request.on('data', (chunk) => { body += chunk })
request.on('end', async () => {
const report = { ...JSON.parse(body), evidenceInputs: await collectEvidenceInputs() }
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
resolveReport(report)
response.writeHead(204)
response.end()
})
return
}
if (request.url?.startsWith('/chrome-partdesign-reference-lifecycle-harness.html')) {
response.setHeader('content-type', 'text/html; charset=utf-8')
response.end(await readFile(resolve(root, 'public/chrome-partdesign-reference-lifecycle-harness.html')))
return
}
try {
const upstream = await fetch(`http://127.0.0.1:${vitePort}${request.url}`, { headers: { ...request.headers } })
response.writeHead(upstream.status, Object.fromEntries(upstream.headers.entries()))
response.end(Buffer.from(await upstream.arrayBuffer()))
} catch (error) { response.writeHead(502); response.end(String(error)) }
})
await new Promise((resolveServer) => server.listen(0, '127.0.0.1', resolveServer))
const port = server.address().port
const executable = process.env.CHROME_BIN || '/home/mes123456/.local/bin/google-chrome'
if (!existsSync(executable)) throw new Error(`Chrome executable is missing: ${executable}`)
const profile = await createChromeProfile('partdesign-reference-lifecycle')
const chrome = spawn(executable, ['--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--noerrdialogs', '--no-first-run', `--user-data-dir=${profile}`, `http://127.0.0.1:${port}/chrome-partdesign-reference-lifecycle-harness.html`], { cwd: root, stdio: ['ignore', 'ignore', 'ignore'] })
const report = await Promise.race([reportPromise, new Promise((resolveTimeout) => setTimeout(() => resolveTimeout({ status: 'timeout', browserId: 'chrome' }), 180_000))])
chrome.kill('SIGTERM')
vite.kill('SIGTERM')
server.closeAllConnections?.()
server.close()
await removeChromeProfile(profile)
console.log(JSON.stringify(report, null, 2))
if (report.status !== 'pass') process.exit(1)
process.exit(0)

View File

@@ -0,0 +1,27 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
if (!existsSync(executable)) throw new Error(`FreeCAD attachment combination oracle executable is missing: ${executable}`)
const sysroot = resolve(root, '.cache/freecad/sysroot')
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-attachment-combination-oracle.py')], {
cwd: root,
encoding: 'utf8',
timeout: 300_000,
maxBuffer: 128 * 1024 * 1024,
env: {
...process.env,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_ATTACHMENT_COMBINATION_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD attachment combination oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-attachment-combination-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, cases: report.caseCount, engines: report.engineCaseCounts, mutation: report.translation, fcstdRoundtrip: true }, null, 2))

View File

@@ -8,6 +8,7 @@ const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/inst
const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages') const pythonPath = resolve(root, '.cache/freecad/sysroot/usr/lib/python3/dist-packages')
const script = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py') const script = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py')
const outputPath = resolve(root, 'config/freecad-composite-history-elementmap-oracle.json') const outputPath = resolve(root, 'config/freecad-composite-history-elementmap-oracle.json')
const resaveOutputPath = resolve(root, 'config/freecad-composite-history-resave-verification.json')
if (!existsSync(executable)) throw new Error(`FreeCAD composite oracle executable is missing: ${executable}`) if (!existsSync(executable)) throw new Error(`FreeCAD composite oracle executable is missing: ${executable}`)
const execution = spawnSync(executable, ['--python-path', pythonPath, script], { const execution = spawnSync(executable, ['--python-path', pythonPath, script], {
cwd: root, cwd: root,
@@ -28,5 +29,29 @@ const markerIndex = output.indexOf(marker)
const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined const payload = markerIndex >= 0 ? output.slice(markerIndex + marker.length).match(/\{.*\}/s)?.[0] : undefined
if (execution.error || execution.status !== 0 || !payload) throw new Error(`FreeCAD composite history oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`) if (execution.error || execution.status !== 0 || !payload) throw new Error(`FreeCAD composite history oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(payload) const report = JSON.parse(payload)
const resaveReport = {
schemaVersion: 1,
baselineId: report.baselineId,
freecadVersion: report.freecadVersion,
gitCommit: report.gitCommit,
status: report.status,
cases: report.cases.map(({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered }) => ({ id, roundtripNameDrift, resaveNameDrift, nativeDesktopResaveCovered })),
summary: {
cases: report.summary.cases,
passed: report.summary.passed,
failed: report.summary.failed,
roundtripNameDrift: report.summary.roundtripNameDrift,
resaveNameDrift: report.summary.resaveNameDrift,
nativeDesktopResaveCases: report.summary.nativeDesktopResaveCases,
},
}
await writeFile(resaveOutputPath, `${JSON.stringify(resaveReport, null, 2)}\n`)
for (const caseReport of report.cases) {
delete caseReport.resaveNameDrift
delete caseReport.nativeDesktopResaveCovered
}
delete report.summary.roundtripNameDrift
delete report.summary.resaveNameDrift
delete report.summary.nativeDesktopResaveCases
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2)) console.log(JSON.stringify(report, null, 2))

View File

@@ -0,0 +1,223 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { inspectFcstdArchive, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive } from '../src/facade/fcstd'
import { DirectNativeOcctHistoryProvider, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from '../src/facade/nativeHistoryProtocol'
import type { NativeStageNamingEvidence } from '../src/facade/nativeNamingEvidence'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from '../src/facade/nativeHistoryProvider'
import type { DocumentSnapshot, TopoRefValue } from '../src/facade/types'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const modulePath = resolve(dist, 'bitbybit-occt-history.js')
const reportPath = resolve(root, 'config/freecad-isomorphic-provenance-verification.json')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const fail = (message: string): never => { throw new Error(`FreeCAD isomorphic provenance verification failed: ${message}`) }
const createModule = (await import(pathToFileURL(modulePath).href)).default
const module = await createModule({ locateFile: (path: string) => resolve(dist, path) })
const provider = new DirectNativeOcctHistoryProvider(module)
const shapeStep = (method: string, ...args: number[]) => {
const shape = module[method](...args)
try { return module.shapeToStep(shape) as string }
finally { shape.delete?.() }
}
const namingCounts = (evidence: NativeStageNamingEvidence) => ({
status: evidence.status,
stable: evidence.mappedNames?.filter(({ relation }) => relation !== 'ambiguous').length ?? 0,
ambiguous: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').length ?? 0,
candidates: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').reduce((count, mapped) => count + (mapped.candidates?.length ?? 0), 0) ?? 0,
stringHasherEntries: evidence.stringHasher ? ('entries' in evidence.stringHasher ? evidence.stringHasher.entries.length : 0) : 0,
elementMaps: evidence.elementMap2 ? ('maps' in evidence.elementMap2 ? evidence.elementMap2.maps.length : 0) : 0,
})
const referencedUpstreamCandidateObjects = (records: NativeOcctHistoryResponse['records'], evidence: NativeStageNamingEvidence) => {
const objects = new Set<string>()
for (const record of records) {
if (record.relation === 'deleted' || !['object', 'tool'].includes(record.sourceId ?? record.source)) continue
const mapped = evidence.mappedNames?.find((entry) => entry.kind === record.kind && entry.resultIndex === record.sourceIndex)
for (const candidate of mapped?.candidates ?? []) objects.add(candidate.objectId)
}
return objects
}
const commonObjectStep = shapeStep('makeBox', 10, 10, 10)
const commonToolStep = shapeStep('makeBox', 10, 10, 10)
const common = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'isomorphic-common', documentId: 'isomorphic-doc', documentVersion: 1,
operationId: 'common', resultObjectId: 'common-result', resultObjectTag: 401, operation: 'common',
objectStep: commonObjectStep, toolStep: commonToolStep,
inputs: [
{ inputId: 'object', objectId: 'symmetric-object', role: 'object', stageId: 'source-object', objectTag: 301, step: commonObjectStep },
{ inputId: 'tool', objectId: 'symmetric-tool', role: 'tool', stageId: 'source-tool', objectTag: 302, step: commonToolStep },
],
stages: [{ stageId: 'common-stage', operation: 'common', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const commonEvidence = common.history.namingEvidence ?? fail('symmetric Common omitted naming evidence')
const commonCounts = namingCounts(commonEvidence)
if (commonCounts.status !== 'ambiguous' || commonCounts.stable !== 0 || commonCounts.ambiguous < 1 || commonCounts.candidates < commonCounts.ambiguous * 2) fail('symmetric Common did not retain all native candidates')
const persistedCommon = JSON.parse(JSON.stringify(commonEvidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedCommon.mappedNames) !== JSON.stringify(commonEvidence.mappedNames)) fail('candidate sets changed across JSON persistence')
const cutObjectStep = shapeStep('makeBox', 10, 10, 10)
const cutToolStep = shapeStep('makeBox', 5, 5, 5)
const cut = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'mixed-cut', documentId: 'isomorphic-doc', documentVersion: 2,
operationId: 'cut', resultObjectId: 'cut-result', resultObjectTag: 501, operation: 'cut',
objectStep: cutObjectStep, toolStep: cutToolStep,
inputs: [
{ inputId: 'object', objectId: 'cut-object', role: 'object', stageId: 'cut-object-source', objectTag: 311, step: cutObjectStep },
{ inputId: 'tool', objectId: 'cut-tool', role: 'tool', stageId: 'cut-tool-source', objectTag: 312, step: cutToolStep },
],
stages: [{ stageId: 'cut-stage', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const cutEvidence = cut.history.namingEvidence ?? fail('mixed Cut omitted naming evidence')
const cutCounts = namingCounts(cutEvidence)
if (cutCounts.status !== 'ambiguous' || cutCounts.stable < 1 || cutCounts.ambiguous < 1 || cutCounts.elementMaps < 1) fail('mixed Cut did not combine private names with explicit ambiguity')
const cutResultStep = cut.history.resultStep ?? fail('mixed Cut omitted result STEP')
const downstream = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: 'downstream-rotate', documentId: 'isomorphic-doc', documentVersion: 3,
operationId: 'rotate', resultObjectId: 'rotate-result', resultObjectTag: 601, operation: 'rotate',
objectStep: cutResultStep, axisOrigin: [0, 0, 0], direction: [0, 0, 1], angle: 15,
inputs: [{ inputId: 'object', objectId: 'cut-result', role: 'object', stageId: 'cut-stage', objectTag: 501, step: cutResultStep, namingEvidence: JSON.parse(JSON.stringify(cutEvidence)) as NativeStageNamingEvidence }],
stages: [{ stageId: 'rotate-stage', operation: 'rotate', inputIds: ['object'], ordinal: 0 }],
}, new AbortController().signal)
const downstreamEvidence = downstream.history.namingEvidence ?? fail('downstream Rotate omitted naming evidence')
const downstreamCounts = namingCounts(downstreamEvidence)
const downstreamCandidateObjects = new Set(downstreamEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
const downstreamReferencedCandidateObjects = referencedUpstreamCandidateObjects(downstream.history.records, cutEvidence)
if (downstreamCounts.status !== 'ambiguous' || downstreamCounts.ambiguous < 1 || !downstreamCandidateObjects.has('cut-object') || !downstreamCandidateObjects.has('cut-tool')) fail('downstream feature collapsed upstream ambiguity')
if ([...downstreamReferencedCandidateObjects].some((objectId) => !downstreamCandidateObjects.has(objectId))) fail('downstream feature lost a referenced upstream candidate')
type ChainSource = { objectId: string; objectTag: number; stageId: string; step: string; evidence: NativeStageNamingEvidence }
type ChainDefinition = { operation: NativeOcctHistoryOperation; parameters: Record<string, unknown> }
const chainStages = [{
operation: 'cut',
historyRecords: cut.history.records.length,
...cutCounts,
candidateObjects: [...new Set(cutEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])].sort(),
persistedInputEvidence: false,
}, {
operation: 'rotate',
historyRecords: downstream.history.records.length,
...downstreamCounts,
candidateObjects: [...downstreamCandidateObjects].sort(),
referencedInputCandidateObjects: [...downstreamReferencedCandidateObjects].sort(),
referencedCandidateObjectsRetained: true,
persistedInputEvidence: true,
}]
const chainResponses = [cut, downstream]
let chainSource: ChainSource = {
objectId: 'rotate-result', objectTag: 601, stageId: 'rotate-stage',
step: downstream.history.resultStep ?? fail('downstream Rotate omitted result STEP'), evidence: downstreamEvidence,
}
let previousHasherEntries = downstreamCounts.stringHasherEntries
const chainDefinitions: ChainDefinition[] = [
{ operation: 'fillet', parameters: { radius: 0.2 } },
{ operation: 'mirrored', parameters: { axisOrigin: [0, 0, 0], direction: [1, 0, 0] } },
{ operation: 'linear-pattern', parameters: { direction: [3, 0, 0] } },
]
for (const [index, definition] of chainDefinitions.entries()) {
const documentVersion = index + 4
const stageId = `${definition.operation}-stage`
const resultObjectId = `${definition.operation}-result`
const resultObjectTag = 701 + index
const persistedInputEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
const response = await provider.capture({
protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
requestId: `long-chain-${definition.operation}`, documentId: 'isomorphic-doc', documentVersion,
operationId: definition.operation, resultObjectId, resultObjectTag, operation: definition.operation,
objectStep: chainSource.step,
inputs: [{ inputId: 'object', objectId: chainSource.objectId, role: 'object', stageId: chainSource.stageId, objectTag: chainSource.objectTag, step: chainSource.step, namingEvidence: persistedInputEvidence }],
stages: [{ stageId, operation: definition.operation, inputIds: ['object'], ordinal: 0 }],
...definition.parameters,
} as Parameters<typeof provider.capture>[0], new AbortController().signal)
const evidence = response.history.namingEvidence ?? fail(`${definition.operation} omitted naming evidence`)
const counts = namingCounts(evidence)
const candidateObjects = new Set(evidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
const referencedInputCandidateObjects = referencedUpstreamCandidateObjects(response.history.records, persistedInputEvidence)
if (counts.status !== 'ambiguous' || counts.stable < 1 || counts.ambiguous < 1 || counts.elementMaps < 1) fail(`${definition.operation} did not preserve mixed stable/ambiguous naming evidence`)
if ([...referencedInputCandidateObjects].some((objectId) => !candidateObjects.has(objectId))) fail(`${definition.operation} lost a referenced upstream candidate`)
if (counts.stringHasherEntries < previousHasherEntries) fail(`${definition.operation} regressed the restored StringHasher table`)
previousHasherEntries = counts.stringHasherEntries
chainStages.push({ operation: definition.operation, historyRecords: response.history.records.length, ...counts, candidateObjects: [...candidateObjects].sort(), referencedInputCandidateObjects: [...referencedInputCandidateObjects].sort(), referencedCandidateObjectsRetained: true, persistedInputEvidence: true })
chainResponses.push(response)
chainSource = { objectId: resultObjectId, objectTag: resultObjectTag, stageId, step: response.history.resultStep ?? fail(`${definition.operation} omitted result STEP`), evidence }
}
const persistedFinalEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedFinalEvidence.mappedNames) !== JSON.stringify(chainSource.evidence.mappedNames)) fail('long-chain naming evidence changed across final JSON persistence')
const ambiguousRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
const fcstdDocument: DocumentSnapshot = {
id: 'fcstd-ambiguous', label: 'FCStd ambiguity boundary', version: 1, dirty: false, readOnly: false, units: 'mm',
tree: [{ id: 'Source', label: 'Source', type: 'feature' }, { id: 'Consumer', label: 'Consumer', type: 'feature' }],
objects: [
{ id: 'Source', typeId: 'Part::Feature', properties: [] },
{ id: 'Consumer', typeId: 'Part::Feature', properties: [{ name: 'Support', label: 'Support', group: 'Links', scope: 'data', type: 'App::PropertyLinkSub', value: { schemaVersion: 1, objectId: 'Source', subElements: [ambiguousRef] } }] },
],
dependencies: [{ sourceId: 'Consumer', targetId: 'Source', relation: 'topo-ref', propertyName: 'Support', reference: 'Face1' }],
recompute: { generation: 0, status: 'idle', objectStates: { Source: 'clean', Consumer: 'clean' }, dirtyObjects: [], order: [], errors: [] },
}
let fcstdAmbiguityRejected = false
try { serializeFcstdMetadataArchive(fcstdDocument) }
catch (error) { fcstdAmbiguityRejected = /stable topology status/.test(error instanceof Error ? error.message : String(error)) }
if (!fcstdAmbiguityRejected) fail('FCStd writer did not reject an ambiguous LinkSub')
const stableRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 5, generation: 5, status: 'stable' }
const stableFcstdDocument: DocumentSnapshot = {
...fcstdDocument,
id: 'fcstd-stable', label: 'FCStd stable LinkSub boundary', version: 5,
objects: fcstdDocument.objects.map((object) => object.id !== 'Consumer' ? object : {
...object,
properties: object.properties.map((property) => property.name !== 'Support' ? property : { ...property, value: { schemaVersion: 1, objectId: 'Source', subElements: [stableRef] } }),
}),
}
const stableArchive = serializeFcstdMetadataArchive(stableFcstdDocument)
const stableInspection = inspectFcstdArchive(stableArchive)
const nativeLinkSub = stableInspection.objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(nativeLinkSub?.subElements) !== '["Face1"]') fail('FCStd writer did not emit the stable native LinkSub name')
const rewrittenStableArchive = rewriteFcstdMetadataArchive(stableArchive, stableInspection.proxyDocument)
const rewrittenStableLinkSub = inspectFcstdArchive(rewrittenStableArchive).objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(rewrittenStableLinkSub?.subElements) !== '["Face1"]') fail('FCStd inspect/rewrite changed the stable native LinkSub name')
for (const response of [common, ...chainResponses]) (response.history as typeof response.history & { result?: { delete?(): void } }).result?.delete?.()
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(dist, name)
const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const harnessPath = resolve(root, 'scripts/run-freecad-isomorphic-provenance.ts')
const harnessContent = await readFile(harnessPath)
const report = {
schemaVersion: 1, status: 'pass', runtime: 'node', implementation: 'freecad-linked', productionWorkerLinked: true,
cases: {
symmetricCommon: { ...commonCounts, persistedCandidateSets: true },
mixedCut: cutCounts,
downstreamRotate: { ...downstreamCounts, candidateObjects: [...downstreamCandidateObjects].sort() },
longChain: {
operations: chainStages.map(({ operation }) => operation),
stages: chainStages,
persistedFinalEvidence: true,
referencedCandidateObjectsRetained: true,
stringHasherMonotonic: true,
},
},
fcstdAmbiguityRejected,
fcstdStableLinkRoundtrip: { subElements: rewrittenStableLinkSub?.subElements, inspectRewritePreserved: true, nativeDesktopResaveCovered: false },
artifacts,
harness: { path: 'scripts/run-freecad-isomorphic-provenance.ts', bytes: harnessContent.length, sha256: createHash('sha256').update(harnessContent).digest('hex') },
exactBlocker: 'exhaustive cross-feature naming and native FreeCAD FCStd save-reopen-resave corpus',
systemExact: false,
generatedAt: new Date().toISOString(),
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))

View File

@@ -0,0 +1,23 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { runFreeCadNamingProductionMatrix } from './freecad-naming-production-matrix.mjs'
const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const matrixPath = resolve(root, 'scripts/freecad-naming-production-matrix.mjs')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const createModule = (await import(pathToFileURL(resolve(dist, artifactNames[0])).href)).default
const module = await createModule({ locateFile: (path) => resolve(dist, path) })
const report = await runFreeCadNamingProductionMatrix(module)
const artifacts = await Promise.all(artifactNames.map(async (name) => {
const path = resolve(dist, name)
const [bytes, content] = await Promise.all([stat(path).then(({ size }) => size), readFile(path)])
return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const matrixContent = await readFile(matrixPath)
const harness = { path: 'scripts/freecad-naming-production-matrix.mjs', bytes: matrixContent.length, sha256: createHash('sha256').update(matrixContent).digest('hex') }
const output = { ...report, runtime: 'node', artifacts, harness, generatedAt: new Date().toISOString() }
await writeFile(resolve(root, 'config/freecad-naming-production-verification.json'), `${JSON.stringify(output, null, 2)}\n`)
console.log(JSON.stringify(output, null, 2))

View File

@@ -0,0 +1,82 @@
import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const sourcePath = resolve(root, '.cache/freecad/reference-desktop.json')
const outputPath = resolve(root, 'config/freecad-native-property-semantics.json')
const harnessPath = resolve(root, 'scripts/run-freecad-native-property-semantics.mjs')
const sourceContent = await readFile(sourcePath)
const source = JSON.parse(sourceContent)
const fail = (message) => { throw new Error(`FreeCAD native property semantics probe failed: ${message}`) }
if (source.schemaVersion !== 1 || source.freecadVersion !== '1.1.1' || source.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('desktop oracle is not the locked FreeCAD baseline')
const runtime = source.runtimeObjects
if (!runtime?.available || runtime.candidateCount !== 352 || runtime.availableCount !== 348 || runtime.unavailableCount !== 4 || !Array.isArray(runtime.types)) fail('runtime Document.supportedTypes inventory is incomplete')
const editableTypes = new Set([
'App::PropertyAngle', 'App::PropertyBool', 'App::PropertyDistance', 'App::PropertyEnumeration',
'App::PropertyFloat', 'App::PropertyFloatList', 'App::PropertyInteger', 'App::PropertyIntegerConstraint',
'App::PropertyIntegerList', 'App::PropertyLength', 'App::PropertyLink', 'App::PropertyLinkList',
'App::PropertyLinkSub', 'App::PropertyLinkSubList', 'App::PropertyPlacement', 'App::PropertyString',
'App::PropertyStringList', 'App::PropertyVector',
])
const specializedTypes = new Set([
'App::PropertyExpressionEngine', 'Part::PropertyGeometryList', 'Part::PropertyPartShape',
'Path::PropertyPath', 'Sketcher::PropertyConstraintList',
])
const numericStatusNames = new Map([
[13, 'Ordered'], [22, 'PropNoPersist'], [23, 'PropNoRecompute'], [24, 'PropReadOnly'],
[25, 'PropTransient'], [26, 'PropHidden'], [27, 'PropOutput'],
])
const facadeStatusNames = new Set(['Hidden', 'ReadOnly'])
const normalizeStatus = (status) => status.map((entry) => typeof entry === 'number' ? (numericStatusNames.get(entry) ?? `UnknownBit${entry}`) : entry)
const properties = runtime.types.flatMap((object) => (object.properties ?? []).map((property) => ({ ...property, objectTypeId: object.typeId, status: normalizeStatus(property.status) })))
if (properties.length !== 5510 || properties.some((property) => !property.name || !property.typeId || !Array.isArray(property.status))) fail('runtime property records are incomplete')
const byType = new Map()
for (const property of properties) {
const entry = byType.get(property.typeId) ?? { typeId: property.typeId, recordCount: 0, objectTypeIds: new Set(), statusNames: new Set() }
entry.recordCount += 1
entry.objectTypeIds.add(property.objectTypeId)
property.status.forEach((status) => entry.statusNames.add(status))
byType.set(property.typeId, entry)
}
const types = [...byType.values()].sort((left, right) => left.typeId.localeCompare(right.typeId)).map((entry) => ({
typeId: entry.typeId,
support: editableTypes.has(entry.typeId) ? 'native-editable-codec' : specializedTypes.has(entry.typeId) ? 'native-specialized-codec' : 'opaque-fcstd-proxy',
recordCount: entry.recordCount,
objectTypeCount: entry.objectTypeIds.size,
statusNames: [...entry.statusNames].sort(),
}))
const supportSummary = Object.fromEntries(['native-editable-codec', 'native-specialized-codec', 'opaque-fcstd-proxy'].map((support) => {
const selected = types.filter((entry) => entry.support === support)
return [support, { typeCount: selected.length, recordCount: selected.reduce((total, entry) => total + entry.recordCount, 0) }]
}))
const observedStatuses = [...new Set(properties.flatMap((property) => property.status))].sort()
const unsupportedStatusNames = observedStatuses.filter((status) => !facadeStatusNames.has(status))
const unsupportedStatusRecordCount = properties.filter((property) => property.status.some((status) => unsupportedStatusNames.includes(status))).length
const unavailableObjects = runtime.types.filter(({ available }) => !available).map(({ typeId, error }) => ({ typeId, error }))
const harnessContent = await readFile(harnessPath)
const report = {
schemaVersion: 1,
status: 'pass',
baseline: { freecadVersion: source.freecadVersion, commit: source.gitCommit },
source: { path: '.cache/freecad/reference-desktop.json', bytes: sourceContent.length, sha256: createHash('sha256').update(sourceContent).digest('hex') },
runtime: { registeredObjectTypes: runtime.candidateCount, instantiableObjectTypes: runtime.availableCount, unavailableObjectTypes: runtime.unavailableCount, unavailableObjects, propertyRecords: properties.length, propertyTypes: types.length },
supportSummary,
types,
propertyStatus: {
observed: observedStatuses,
facadeNative: [...facadeStatusNames].sort(),
proxyOnly: unsupportedStatusNames,
proxyOnlyRecordCount: unsupportedStatusRecordCount,
unknownNumericBits: observedStatuses.filter((status) => status.startsWith('UnknownBit')),
},
harness: { path: 'scripts/run-freecad-native-property-semantics.mjs', bytes: (await stat(harnessPath)).size, sha256: createHash('sha256').update(harnessContent).digest('hex') },
exactPromotionReady: false,
exactBlocker: 'complete native document and property semantics',
generatedAt: new Date().toISOString(),
}
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, runtime: report.runtime, supportSummary, propertyStatus: report.propertyStatus, exactPromotionReady: false }, null, 2))

View File

@@ -0,0 +1,27 @@
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
if (!existsSync(executable)) throw new Error(`FreeCAD XLink/relink oracle executable is missing: ${executable}`)
const sysroot = resolve(root, '.cache/freecad/sysroot')
const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), resolve(root, 'scripts/freecad-xlink-relink-oracle.py')], {
cwd: root,
encoding: 'utf8',
timeout: 180_000,
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
},
})
const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
const marker = 'FREECAD_XLINK_RELINK_RESULT='
const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD XLink/relink oracle failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
const report = JSON.parse(line.slice(line.indexOf(marker) + marker.length))
await writeFile(resolve(root, 'config/freecad-xlink-relink-oracle.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, baselineId: report.baselineId, propertyTypes: report.propertyTypes, sourceCloseRelink: true, missingFileRelink: true, editPropagation: true, fcstdRoundtrip: true }, null, 2))

View File

@@ -39,6 +39,8 @@ const wasmBuilds = process.env.CI_REAL_REBUILD_WASM === '1'
'build:boost-wasm', 'build:boost-wasm',
'build:freecad-naming-source-probe', 'build:freecad-naming-source-probe',
'test:freecad-naming-source-probe', 'test:freecad-naming-source-probe',
'build:freecad-naming-sdk-candidate',
'generate:freecad-naming-sdk-manifest',
'check:freecad-naming-sdk-readiness', 'check:freecad-naming-sdk-readiness',
'check:freecad-wasm-sdk-build-plan', 'check:freecad-wasm-sdk-build-plan',
'build:occt-history', 'build:occt-history',
@@ -80,6 +82,8 @@ const lanes = {
'probe:freecad-partdesign-transform', 'probe:freecad-partdesign-transform',
'probe:freecad-partdesign-structure', 'probe:freecad-partdesign-structure',
'probe:freecad-attachment-modes', 'probe:freecad-attachment-modes',
'probe:freecad-attachment-combinations',
'probe:freecad-xlink-relink',
'probe:freecad-partdesign-failures', 'probe:freecad-partdesign-failures',
'probe:freecad-partdesign-revolution-groove', 'probe:freecad-partdesign-revolution-groove',
'probe:freecad-part-builders', 'probe:freecad-part-builders',
@@ -87,6 +91,8 @@ const lanes = {
'test:freecad-fcstd-native', 'test:freecad-fcstd-native',
'generate:freecad-parameter-mutations', 'generate:freecad-parameter-mutations',
'check:freecad-oracle-coverage', 'check:freecad-oracle-coverage',
'probe:freecad-native-property-semantics',
'check:freecad-native-property-semantics',
'check:freecad-golden-coverage', 'check:freecad-golden-coverage',
'check:freecad-parameter-mutations', 'check:freecad-parameter-mutations',
'check:freecad-sketcher-constraints', 'check:freecad-sketcher-constraints',
@@ -97,6 +103,10 @@ const lanes = {
], ],
wasm: [ wasm: [
...wasmBuilds, ...wasmBuilds,
'test:freecad-naming-production',
'check:freecad-naming-production',
'test:freecad-isomorphic-provenance',
'check:freecad-isomorphic-provenance',
'check:occt-history-artifact', 'check:occt-history-artifact',
'check:freecad-private-naming-boundary', 'check:freecad-private-naming-boundary',
'test:browser-occt', 'test:browser-occt',

View File

@@ -0,0 +1,181 @@
export type ExternalDocumentStatus = 'open' | 'closed' | 'missing'
export type ExternalSourceObjectEvidence = {
objectId: string
length: number
width: number
height: number
volume: number
surfaceArea: number
shapeId: string
}
export type ExternalSourceEvidence = {
documentId: string
path: string
revision: number
objects: {
SourceBox: ExternalSourceObjectEvidence
SecondBox: ExternalSourceObjectEvidence
}
}
export type ExternalLinkTarget = {
documentId: string
objectId: string
subElements?: string[]
}
type ExternalRegistryState = {
status: ExternalDocumentStatus
source: ExternalSourceEvidence
}
type ExternalRegistryArchive = {
schemaVersion: 1
current: ExternalRegistryState
undo: ExternalRegistryState[]
redo: ExternalRegistryState[]
}
export type ExternalPartDesignLinkSnapshot = {
schemaVersion: 1
source: { documentId: string; path: string; revision: number; status: ExternalDocumentStatus }
properties: {
XLink: ExternalLinkTarget | null
XLinkSub: ExternalLinkTarget | null
XLinkList: ExternalLinkTarget[]
XLinkSubList: ExternalLinkTarget[]
}
propertyStatus: Record<'XLink' | 'XLinkSub' | 'XLinkList' | 'XLinkSubList', ['21']>
dimensions: {
xLinkLength: number | null
xLinkSubWidth: number | null
xLinkListHeights: number[]
xLinkSubListAreas: number[]
}
binder: {
support: ExternalLinkTarget[]
area: number
shapeId: string
state: ['Up-to-date']
status: 'Valid'
cacheState: 'linked' | 'cached'
}
history: { undo: number; redo: number }
}
const finitePositive = (value: unknown) => typeof value === 'number' && Number.isFinite(value) && value > 0
const clone = <T>(value: T): T => structuredClone(value)
const validateObject = (value: ExternalSourceObjectEvidence, name: string) => {
if (!value || value.objectId !== name || !finitePositive(value.length) || !finitePositive(value.width) || !finitePositive(value.height) || !finitePositive(value.volume) || !finitePositive(value.surfaceArea) || typeof value.shapeId !== 'string' || !value.shapeId) throw new TypeError(`External source object ${name} is invalid.`)
const expectedVolume = value.length * value.width * value.height
const expectedArea = 2 * (value.length * value.width + value.length * value.height + value.width * value.height)
if (Math.abs(value.volume - expectedVolume) > 1e-7 || Math.abs(value.surfaceArea - expectedArea) > 1e-7) throw new RangeError(`External source object ${name} geometry evidence does not match its dimensions.`)
}
const validateSource = (value: ExternalSourceEvidence) => {
if (!value || value.documentId !== 'XLinkSource' || value.path !== '/projects/XLinkSource.FCStd' || !Number.isSafeInteger(value.revision) || value.revision < 0) throw new TypeError('External source document identity is invalid.')
validateObject(value.objects?.SourceBox, 'SourceBox')
validateObject(value.objects?.SecondBox, 'SecondBox')
}
const validateState = (value: ExternalRegistryState) => {
if (!value || !['open', 'closed', 'missing'].includes(value.status)) throw new TypeError('External source status is invalid.')
validateSource(value.source)
}
const objectTarget = (objectId: string): ExternalLinkTarget => ({ documentId: 'XLinkSource', objectId })
const subTarget = (objectId: string, subElements: string[]): ExternalLinkTarget => ({ documentId: 'XLinkSource', objectId, subElements: [...subElements] })
export class ExternalPartDesignLinkRegistry {
private current: ExternalRegistryState
private readonly undoStack: ExternalRegistryState[]
private readonly redoStack: ExternalRegistryState[]
constructor(source: ExternalSourceEvidence, archive?: Pick<ExternalRegistryArchive, 'current' | 'undo' | 'redo'>) {
validateSource(source)
this.current = archive ? clone(archive.current) : { status: 'open', source: clone(source) }
this.undoStack = archive ? clone(archive.undo) : []
this.redoStack = archive ? clone(archive.redo) : []
validateState(this.current)
this.undoStack.forEach(validateState)
this.redoStack.forEach(validateState)
}
closeSource() {
if (this.current.status !== 'open') throw new Error('Only an open external source can be closed.')
this.current = { ...this.current, status: 'closed' }
}
markSourceMissing() {
if (this.current.status === 'open') throw new Error('An open external source cannot become missing before it is closed.')
this.current = { ...this.current, status: 'missing' }
}
relinkSource(source: ExternalSourceEvidence) {
validateSource(source)
if (source.documentId !== this.current.source.documentId || source.path !== this.current.source.path) throw new Error('External source relink requires the original document id and path.')
this.current = { status: 'open', source: clone(source) }
}
editSource(source: ExternalSourceEvidence) {
validateSource(source)
if (this.current.status !== 'open') throw new Error('External source edits require an open and resolved document.')
if (source.documentId !== this.current.source.documentId || source.path !== this.current.source.path || source.revision <= this.current.source.revision) throw new Error('External source edits require the same document and a newer revision.')
this.undoStack.push(clone(this.current))
this.redoStack.length = 0
this.current = { status: 'open', source: clone(source) }
}
undo() {
const previous = this.undoStack.pop()
if (!previous) throw new Error('External link history has nothing to undo.')
this.redoStack.push(clone(this.current))
this.current = previous
}
redo() {
const next = this.redoStack.pop()
if (!next) throw new Error('External link history has nothing to redo.')
this.undoStack.push(clone(this.current))
this.current = next
}
snapshot(): ExternalPartDesignLinkSnapshot {
const resolved = this.current.status === 'open'
const source = this.current.source.objects.SourceBox
const second = this.current.source.objects.SecondBox
const XLink = objectTarget('SourceBox')
const XLinkSub = subTarget('SourceBox', ['Face1'])
const XLinkList = [objectTarget('SourceBox'), objectTarget('SecondBox')]
const XLinkSubList = [subTarget('SourceBox', ['Face1']), subTarget('SecondBox', ['Face1', 'Face2'])]
return {
schemaVersion: 1,
source: { documentId: this.current.source.documentId, path: this.current.source.path, revision: this.current.source.revision, status: this.current.status },
properties: { XLink: resolved ? XLink : null, XLinkSub: resolved ? XLinkSub : null, XLinkList: resolved ? XLinkList : [], XLinkSubList: resolved ? XLinkSubList : [] },
propertyStatus: { XLink: ['21'], XLinkSub: ['21'], XLinkList: ['21'], XLinkSubList: ['21'] },
dimensions: {
xLinkLength: resolved ? source.length : null,
xLinkSubWidth: resolved ? source.width : null,
xLinkListHeights: resolved ? [source.height, second.height] : [],
xLinkSubListAreas: resolved ? [source.width * source.height, second.width * second.height, second.width * second.height] : [],
},
binder: { support: resolved ? [XLinkSub] : [], area: source.width * source.height, shapeId: source.shapeId, state: ['Up-to-date'], status: 'Valid', cacheState: resolved ? 'linked' : 'cached' },
history: { undo: this.undoStack.length, redo: this.redoStack.length },
}
}
serialize() {
const archive: ExternalRegistryArchive = { schemaVersion: 1, current: clone(this.current), undo: clone(this.undoStack), redo: clone(this.redoStack) }
return JSON.stringify(archive)
}
static deserialize(text: string) {
const archive = JSON.parse(text) as ExternalRegistryArchive
if (archive?.schemaVersion !== 1 || !Array.isArray(archive.undo) || !Array.isArray(archive.redo)) throw new TypeError('External link registry archive is invalid.')
validateState(archive.current)
return new ExternalPartDesignLinkRegistry(archive.current.source, archive)
}
}

View File

@@ -926,6 +926,15 @@ const pathPropertyValue = (value: unknown, context: string): PathPropertyValue =
return { schemaVersion: 1, commands, ...(resourcePath === undefined ? {} : { resourcePath }), version, ...(center === undefined ? {} : { center: center as PathPropertyValue['center'] }) } return { schemaVersion: 1, commands, ...(resourcePath === undefined ? {} : { resourcePath }), version, ...(center === undefined ? {} : { center: center as PathPropertyValue['center'] }) }
} }
const nativeLinkTopoRefName = (value: TopoRefValue, context: string, expectedObjectId: string) => {
if (value.schemaVersion !== 1 || value.objectId !== expectedObjectId || !value.persistentId.trim()) throw new TypeError(`${context} TopoRef is invalid or targets a different object.`)
if (!Number.isSafeInteger(value.topologyVersion) || value.topologyVersion < 0 || !Number.isSafeInteger(value.generation) || value.generation < 0) throw new TypeError(`${context} TopoRef requires non-negative integer topology versions.`)
if (value.status !== 'stable') throw new Error(`${context} '${value.persistentId}' must have stable topology status.`)
const prefix = value.kind === 'face' ? 'Face' : value.kind === 'edge' ? 'Edge' : 'Vertex'
if (!new RegExp(`^${prefix}[1-9]\\d*$`).test(value.persistentId)) throw new Error(`${context} '${value.persistentId}' is not a lossless native ${prefix}N reference.`)
return value.persistentId
}
const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId?: string, topologyElementMap?: ElementMapSnapshot) => { const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId?: string, topologyElementMap?: ElementMapSnapshot) => {
const attributes = native ? property.type === 'App::PropertyPlacement' ? ' status="8388608"' : '' : ` group="${xmlEscape(property.group ?? '')}" doc="${xmlEscape(property.label ?? property.name)}" attr="0" ro="${property.readOnly ? 1 : 0}" hide="${property.hidden ? 1 : 0}"` const attributes = native ? property.type === 'App::PropertyPlacement' ? ' status="8388608"' : '' : ` group="${xmlEscape(property.group ?? '')}" doc="${xmlEscape(property.label ?? property.name)}" attr="0" ro="${property.readOnly ? 1 : 0}" hide="${property.hidden ? 1 : 0}"`
if (property.type === 'Path::PropertyPath') { if (property.type === 'Path::PropertyPath') {
@@ -979,8 +988,7 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId
if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('entries' in property.value) || property.value.schemaVersion !== 1 || !Array.isArray(property.value.entries)) throw new TypeError(`FCStd LinkSubList property ${property.name} requires a structured value.`) if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('entries' in property.value) || property.value.schemaVersion !== 1 || !Array.isArray(property.value.entries)) throw new TypeError(`FCStd LinkSubList property ${property.name} requires a structured value.`)
const links = property.value.entries.map((entry) => { const links = property.value.entries.map((entry) => {
if (!entry || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} requires non-empty object ids.`) if (!entry || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} requires non-empty object ids.`)
const subElement = entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : entry.subElement.persistentId const subElement = entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : nativeLinkTopoRefName(entry.subElement, `FCStd LinkSubList property ${property.name}`, entry.objectId)
if (entry.subElement && typeof entry.subElement === 'object' && entry.subElement.objectId !== entry.objectId) throw new RangeError(`FCStd LinkSubList property ${property.name} TopoRef targets a different object.`)
if (entry.subElement !== null && !subElement.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} contains an empty sub-element.`) if (entry.subElement !== null && !subElement.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} contains an empty sub-element.`)
return `<Link obj="${xmlEscape(entry.objectId)}" sub="${xmlEscape(subElement)}"/>` return `<Link obj="${xmlEscape(entry.objectId)}" sub="${xmlEscape(subElement)}"/>`
}).join('') }).join('')
@@ -1013,9 +1021,9 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId
const value = property.value const value = property.value
if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'objectId' in value && typeof value.objectId === 'string') { if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'objectId' in value && typeof value.objectId === 'string') {
objectId = value.objectId objectId = value.objectId
if ('subElements' in value && Array.isArray(value.subElements)) subElements = value.subElements.map((entry) => typeof entry === 'string' ? entry : entry.persistentId) if ('subElements' in value && Array.isArray(value.subElements)) subElements = value.subElements.map((entry) => typeof entry === 'string' ? entry : nativeLinkTopoRefName(entry, `FCStd LinkSub property ${property.name}`, objectId))
else if ('persistentId' in value && typeof value.persistentId === 'string') subElements = [value.persistentId] else if ('persistentId' in value && typeof value.persistentId === 'string') subElements = [nativeLinkTopoRefName(value as TopoRefValue, `FCStd LinkSub property ${property.name}`, objectId)]
else if ('subElement' in value && value.subElement) subElements = [typeof value.subElement === 'string' ? value.subElement : value.subElement.persistentId] else if ('subElement' in value && value.subElement) subElements = [typeof value.subElement === 'string' ? value.subElement : nativeLinkTopoRefName(value.subElement, `FCStd LinkSub property ${property.name}`, objectId)]
} else if (value !== null) throw new TypeError(`FCStd LinkSub property ${property.name} requires a structured value.`) } else if (value !== null) throw new TypeError(`FCStd LinkSub property ${property.name} requires a structured value.`)
if (subElements.some((entry) => !entry.trim())) throw new TypeError(`FCStd LinkSub property ${property.name} contains an empty sub-element.`) if (subElements.some((entry) => !entry.trim())) throw new TypeError(`FCStd LinkSub property ${property.name} contains an empty sub-element.`)
const subs = subElements.map((entry) => `<Sub value="${xmlEscape(entry)}"/>`).join('') const subs = subElements.map((entry) => `<Sub value="${xmlEscape(entry)}"/>`).join('')

View File

@@ -79,8 +79,8 @@ export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOc
export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol' export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from './nativeHistoryProtocol'
export { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, nativeNamingAbiCapabilities, nativeNamingCapabilitiesForModule } from './nativeHistoryProtocol' export { NATIVE_OCCT_NAMING_ABI_UNAVAILABLE, nativeNamingAbiCapabilities, nativeNamingCapabilitiesForModule } from './nativeHistoryProtocol'
export type { NativeNamingAbiCapabilities, NativeOcctHistoryCapabilities, NativeOcctHistoryExecution, NativeOcctHistoryInputTransport, NativeOcctHistoryProvider, NativeOcctHistoryProtocolResponse, NativeOcctHistoryRequest, NativeOcctHistoryStageTransport } from './nativeHistoryProtocol' 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 { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, 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 type { FreeCadPrivateNamingAbiDescriptor, FreeCadPrivateNamingAbiProbe, FreeCadPrivateNamingAbiRequest, FreeCadPrivateNamingHistoryAmbiguity, FreeCadPrivateNamingHistoryCandidate, FreeCadPrivateNamingHistoryClassification, NativeFreeCadNamingAbiModule } from './nativeNamingAbi'
export { assertNativeNamingEvidence, cloneNativeNamingEvidence, 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 type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence'
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient' export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'

View File

@@ -1,5 +1,5 @@
import { migrateElementMap2Schema, validateElementMap2 } from './elementMap2' import { migrateElementMap2Schema, validateElementMap2 } from './elementMap2'
import { assertNativeNamingEvidence, cloneNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence' import { assertNativeNamingEvidence, cloneNativeNamingEvidence, type NativeMappedNameRef, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { migrateStringHasherSchema, validateElementMap2StringHasherEvidence, validateStringHasherTable } from './stringHasher' import { migrateStringHasherSchema, validateElementMap2StringHasherEvidence, validateStringHasherTable } from './stringHasher'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider' import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider'
@@ -54,15 +54,197 @@ export type FreeCadPrivateNamingAbiRequest = {
resultStep?: string resultStep?: string
resultBrep?: string resultBrep?: string
resultStepByStage?: Record<string, string> resultStepByStage?: Record<string, string>
/** Results with more than one native source stay explicit and never enter the private-name builder. */
historyAmbiguities?: FreeCadPrivateNamingHistoryAmbiguity[]
history: Pick<NativeOcctHistoryResponse, 'provider' | 'occtVersion' | 'records' | 'hasModified' | 'hasGenerated' | 'hasDeleted' | 'resultStep' | 'resultBrep'> history: Pick<NativeOcctHistoryResponse, 'provider' | 'occtVersion' | 'records' | 'hasModified' | 'hasGenerated' | 'hasDeleted' | 'resultStep' | 'resultBrep'>
} }
export type FreeCadPrivateNamingHistoryCandidate = {
inputId: string
objectId: string
persistentId: string
stageId?: string
relation: 'modified' | 'generated'
sourceKind: 'face' | 'edge' | 'vertex'
sourceIndex: number
}
export type FreeCadPrivateNamingHistoryAmbiguity = {
resultKind: 'face' | 'edge' | 'vertex'
resultIndex: number
candidates: FreeCadPrivateNamingHistoryCandidate[]
}
export type FreeCadPrivateNamingHistoryClassification = {
records: NativeOcctHistoryResponse['records']
ambiguities: FreeCadPrivateNamingHistoryAmbiguity[]
externalRecordCount: number
internalRecordCount: number
duplicateRecordCount: number
relationConflictCount: number
}
const encoder = new TextEncoder() const encoder = new TextEncoder()
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
const supportedOperations = new Set<NativeOcctHistoryOperation>(['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform']) const supportedOperations = new Set<NativeOcctHistoryOperation>(['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'])
const boundedLimit = (value: unknown, maximum: number) => Number.isSafeInteger(value) && (value as number) > 0 ? Math.min(value as number, maximum) : maximum const boundedLimit = (value: unknown, maximum: number) => Number.isSafeInteger(value) && (value as number) > 0 ? Math.min(value as number, maximum) : maximum
type FreeCadPrivateNamingInputIdentity = Pick<FreeCadPrivateNamingAbiRequest['inputs'][number], 'inputId' | 'role'>
type FreeCadPrivateNamingInput = FreeCadPrivateNamingAbiRequest['inputs'][number]
const namingKinds = new Set(['face', 'edge', 'vertex'])
const titleKind = (kind: FreeCadPrivateNamingHistoryAmbiguity['resultKind']) => `${kind[0].toUpperCase()}${kind.slice(1)}`
const effectiveSource = (record: NativeOcctHistoryResponse['records'][number]) => record.sourceId ?? record.source
const resolveNamingInput = (record: NativeOcctHistoryResponse['records'][number], inputs: FreeCadPrivateNamingInput[]): FreeCadPrivateNamingInput | undefined => {
const source = effectiveSource(record)
return inputs.find((input) => input.inputId === source)
?? inputs.find((input) => input.role === source)
?? (source === 'object' ? inputs[0] : undefined)
?? (source === 'tool' ? inputs[1] : undefined)
}
const sourceCandidates = (record: NativeOcctHistoryResponse['records'][number], input: FreeCadPrivateNamingInput, kind: FreeCadPrivateNamingHistoryCandidate['sourceKind']): FreeCadPrivateNamingHistoryCandidate[] => {
const prior = input.namingEvidence?.mappedNames?.find((mapped) => mapped.kind === kind && mapped.resultIndex === record.sourceIndex)
if (prior?.candidates && prior.candidates.length > 1) return prior.candidates.map((candidate) => ({
inputId: input.inputId,
objectId: candidate.objectId,
persistentId: candidate.persistentId,
...(candidate.stageId ? { stageId: candidate.stageId } : {}),
relation: record.relation as 'modified' | 'generated',
sourceKind: kind,
sourceIndex: record.sourceIndex,
}))
return [{
inputId: input.inputId,
objectId: input.objectId ?? input.inputId,
persistentId: prior?.resultPersistentId ?? `${titleKind(kind)}${record.sourceIndex + 1}`,
...(input.stageId ? { stageId: input.stageId } : {}),
relation: record.relation as 'modified' | 'generated',
sourceKind: kind,
sourceIndex: record.sourceIndex,
}]
}
export const selectFreeCadPrivateNamingHistoryRecords = (
records: NativeOcctHistoryResponse['records'],
inputs: FreeCadPrivateNamingInputIdentity[],
): NativeOcctHistoryResponse['records'] => {
const sources = new Set<string>()
for (const input of inputs) {
if (input.inputId.trim()) sources.add(input.inputId)
if (input.role?.trim()) sources.add(input.role)
}
if (inputs.length > 0) sources.add('object')
if (inputs.length > 1) sources.add('tool')
return records.filter((record) => sources.has(record.sourceId ?? record.source))
}
/**
* Classifies the native graph before the FreeCAD private-name ABI is called.
* A result is stable only when every contributing native relation resolves to
* one source object/subshape/stage. Isomorphic multi-source results retain all
* candidates and are deliberately excluded from private token generation.
*/
export const classifyFreeCadPrivateNamingHistory = (
records: NativeOcctHistoryResponse['records'],
inputs: FreeCadPrivateNamingInput[],
): FreeCadPrivateNamingHistoryClassification => {
const externalRecords = selectFreeCadPrivateNamingHistoryRecords(records, inputs)
const deletedRecords = externalRecords.filter((record) => record.relation === 'deleted').map((record) => ({ ...record, resultIndexes: record.resultIndexes ? [...record.resultIndexes] : undefined }))
const grouped = new Map<string, Array<{ record: NativeOcctHistoryResponse['records'][number]; input: FreeCadPrivateNamingInput; resultKind: FreeCadPrivateNamingHistoryAmbiguity['resultKind']; resultIndex: number; candidate: FreeCadPrivateNamingHistoryCandidate }>>()
for (const record of externalRecords) {
if (record.relation === 'deleted') continue
const resultKind = record.resultKind ?? record.kind
if (!namingKinds.has(record.kind) || !namingKinds.has(resultKind)) throw new TypeError(`FreeCAD naming history contains unsupported kind '${record.kind}' -> '${resultKind}'.`)
if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0) throw new RangeError('FreeCAD naming history sourceIndex must be a non-negative safe integer.')
const input = resolveNamingInput(record, inputs)
if (!input) throw new Error(`FreeCAD naming history source '${effectiveSource(record)}' has no transport input.`)
const indexes = record.resultIndexes ?? (record.resultIndex === undefined ? [] : [record.resultIndex])
for (const resultIndex of indexes) {
if (!Number.isSafeInteger(resultIndex) || resultIndex < 0) throw new RangeError('FreeCAD naming history resultIndex must be a non-negative safe integer.')
const sourceKind = record.kind as FreeCadPrivateNamingHistoryCandidate['sourceKind']
const kind = resultKind as FreeCadPrivateNamingHistoryAmbiguity['resultKind']
const key = `${kind}:${resultIndex}`
const entries = grouped.get(key) ?? []
for (const candidate of sourceCandidates(record, input, sourceKind)) entries.push({ record, input, resultKind: kind, resultIndex, candidate })
grouped.set(key, entries)
}
}
const selected: NativeOcctHistoryResponse['records'] = [...deletedRecords]
const ambiguities: FreeCadPrivateNamingHistoryAmbiguity[] = []
let duplicateRecordCount = 0
let relationConflictCount = 0
for (const entries of grouped.values()) {
const unique = new Map<string, typeof entries[number]>()
for (const entry of entries) {
const candidate = entry.candidate
const key = `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId ?? ''}\u0000${candidate.relation}`
if (!unique.has(key)) unique.set(key, entry)
}
const distinctSources = new Set([...unique.values()].map(({ candidate }) => `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId ?? ''}`))
if (distinctSources.size > 1) {
const first = entries[0]
ambiguities.push({ resultKind: first.resultKind, resultIndex: first.resultIndex, candidates: [...unique.values()].map(({ candidate }) => ({ ...candidate })) })
continue
}
const relations = new Set([...unique.values()].map(({ candidate }) => candidate.relation))
if (relations.size > 1) relationConflictCount += 1
// OCCT may expose one unchanged result through both Modified() and Generated().
// Modified preserves identity and therefore has deterministic precedence.
const stable = [...unique.values()].find(({ candidate }) => candidate.relation === 'modified') ?? unique.values().next().value as typeof entries[number]
selected.push({ ...stable.record, resultKind: stable.resultKind, resultIndex: stable.resultIndex, resultIndexes: undefined })
duplicateRecordCount += entries.length - unique.size
}
return {
records: selected,
ambiguities,
externalRecordCount: externalRecords.length,
internalRecordCount: records.length - externalRecords.length,
duplicateRecordCount,
relationConflictCount,
}
}
const ambiguousMappedNames = (ambiguities: FreeCadPrivateNamingHistoryAmbiguity[]): NativeMappedNameRef[] => ambiguities.map((ambiguity) => {
const name = `${titleKind(ambiguity.resultKind)}${ambiguity.resultIndex + 1}`
return {
kind: ambiguity.resultKind,
resultIndex: ambiguity.resultIndex,
resultPersistentId: name,
reference: { name },
relation: 'ambiguous',
candidates: ambiguity.candidates.map(({ objectId, persistentId, stageId }) => ({ objectId, persistentId, ...(stageId ? { stageId } : {}) })),
}
})
const withIndexedSourceFallbacks = (inputs: FreeCadPrivateNamingInput[], records: NativeOcctHistoryResponse['records']): FreeCadPrivateNamingInput[] => {
const cloned = inputs.map((input) => ({ ...input, ...(input.namingEvidence ? { namingEvidence: cloneNativeNamingEvidence(input.namingEvidence) } : {}) }))
for (const record of records) {
if (record.relation === 'deleted' || !namingKinds.has(record.kind)) continue
const input = resolveNamingInput(record, cloned)
const evidence = input?.namingEvidence
if (!input || !evidence || evidence.status === 'final-shape-only' || evidence.status === 'missing' || evidence.mappedNames?.some((mapped) => mapped.kind === record.kind && mapped.resultIndex === record.sourceIndex)) continue
const kind = record.kind as NativeMappedNameRef['kind']
const name = `${titleKind(kind)}${record.sourceIndex + 1}`
evidence.mappedNames = [...(evidence.mappedNames ?? []), {
kind,
resultIndex: record.sourceIndex,
resultPersistentId: name,
reference: { name },
relation: 'preserved',
sourceRefs: [{ objectId: input.objectId ?? input.inputId, persistentId: name, ...(input.stageId ? { stageId: input.stageId } : {}) }],
}]
assertNativeNamingEvidence(evidence)
}
return cloned
}
export const probeFreeCadPrivateNamingAbi = (module: NativeFreeCadNamingAbiModule): FreeCadPrivateNamingAbiProbe => { export const probeFreeCadPrivateNamingAbi = (module: NativeFreeCadNamingAbiModule): FreeCadPrivateNamingAbiProbe => {
if (typeof module.freecadNamingAbiVersion !== 'function' || typeof module.freecadNamingCapabilitiesJson !== 'function' || typeof module.freecadNamingEvidenceJson !== 'function') return { if (typeof module.freecadNamingAbiVersion !== 'function' || typeof module.freecadNamingCapabilitiesJson !== 'function' || typeof module.freecadNamingEvidenceJson !== 'function') return {
availability: 'unavailable', availability: 'unavailable',
@@ -99,18 +281,22 @@ export const createFreeCadPrivateNamingAbiRequest = (input: Omit<FreeCadPrivateN
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 (!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 (!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.') if (input.resultObjectTag !== undefined && (!Number.isSafeInteger(input.resultObjectTag) || input.resultObjectTag <= 0)) throw new RangeError('FreeCAD naming ABI resultObjectTag must be a positive safe integer.')
const 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.')
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)) } : {}) }
})
const classification = classifyFreeCadPrivateNamingHistory(input.history.records, inputs)
const abiInputs = withIndexedSourceFallbacks(inputs, classification.records)
return { return {
schemaVersion: 1, schemaVersion: 1,
...input, ...input,
inputs: input.inputs.map((entry) => { inputs: abiInputs,
if (!entry.inputId.trim() || !entry.step.trim()) throw new TypeError('FreeCAD naming ABI inputs require non-empty inputId and STEP text.')
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] })), stages: input.stages.map((entry) => ({ ...entry, inputIds: [...entry.inputIds] })),
...(input.resultStepByStage ? { resultStepByStage: { ...input.resultStepByStage } } : {}), ...(input.resultStepByStage ? { resultStepByStage: { ...input.resultStepByStage } } : {}),
history: { ...input.history, records: input.history.records.map((record) => ({ ...record, resultIndexes: record.resultIndexes ? [...record.resultIndexes] : undefined })) }, ...(classification.ambiguities.length > 0 ? { historyAmbiguities: classification.ambiguities.map((ambiguity) => ({ ...ambiguity, candidates: ambiguity.candidates.map((candidate) => ({ ...candidate })) })) } : {}),
history: { ...input.history, records: classification.records.map((record) => ({ ...record, resultIndexes: record.resultIndexes ? [...record.resultIndexes] : undefined })) },
} }
} }
@@ -121,6 +307,16 @@ export const captureFreeCadPrivateNamingEvidence = (
): NativeStageNamingEvidence | undefined => { ): NativeStageNamingEvidence | undefined => {
if (probe.availability !== 'available' || !probe.descriptor || typeof module.freecadNamingEvidenceJson !== 'function') return undefined if (probe.availability !== 'available' || !probe.descriptor || typeof module.freecadNamingEvidenceJson !== 'function') return undefined
if (!probe.descriptor.operations.includes(request.operation)) return undefined if (!probe.descriptor.operations.includes(request.operation)) return undefined
const ambiguities = request.historyAmbiguities ?? []
const stableResultCount = request.history.records.filter((record) => record.relation !== 'deleted').reduce((count, record) => count + (record.resultIndexes?.length ?? (record.resultIndex === undefined ? 0 : 1)), 0)
if (stableResultCount === 0 && ambiguities.length > 0) return assertNativeNamingEvidence({
schemaVersion: 1,
stageId: request.stageId,
resultObjectId: request.resultObjectId,
status: 'ambiguous',
mappedNames: ambiguousMappedNames(ambiguities),
reason: 'Native topology has more than one source provenance; private FreeCAD names were not minted.',
})
const requestJson = JSON.stringify(request) const requestJson = JSON.stringify(request)
const requestBytes = encoder.encode(requestJson).byteLength const requestBytes = encoder.encode(requestJson).byteLength
const requestLimit = boundedLimit(probe.descriptor.maxRequestBytes, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES) const requestLimit = boundedLimit(probe.descriptor.maxRequestBytes, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES)
@@ -147,5 +343,12 @@ export const captureFreeCadPrivateNamingEvidence = (
if (!elementMap2Report.valid) throw new Error(`FreeCAD naming ABI ElementMap2 is invalid: ${elementMap2Report.issues[0].path}: ${elementMap2Report.issues[0].message}`) if (!elementMap2Report.valid) throw new Error(`FreeCAD naming ABI ElementMap2 is invalid: ${elementMap2Report.issues[0].path}: ${elementMap2Report.issues[0].message}`)
const closureIssues = validateElementMap2StringHasherEvidence(elementMap2, stringHasher) const closureIssues = validateElementMap2StringHasherEvidence(elementMap2, stringHasher)
if (closureIssues.length > 0) throw new Error(`FreeCAD naming ABI StringHasher closure is invalid: ${closureIssues[0].path}: ${closureIssues[0].message}`) if (closureIssues.length > 0) throw new Error(`FreeCAD naming ABI StringHasher closure is invalid: ${closureIssues[0].path}: ${closureIssues[0].message}`)
return { ...evidence, stringHasher, elementMap2 } const normalized = { ...evidence, stringHasher, elementMap2 }
if (ambiguities.length === 0) return normalized
return assertNativeNamingEvidence({
...normalized,
status: 'ambiguous',
mappedNames: [...(normalized.mappedNames ?? []), ...ambiguousMappedNames(ambiguities)],
reason: 'Unique native sources use FreeCAD private names; non-unique isomorphic sources retain explicit candidates.',
})
} }

View File

@@ -0,0 +1,39 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { ExternalPartDesignLinkRegistry, type ExternalSourceEvidence } from '../src/facade/externalPartDesignLinks'
const source = (revision = 1): ExternalSourceEvidence => ({
documentId: 'XLinkSource',
path: '/projects/XLinkSource.FCStd',
revision,
objects: {
SourceBox: { objectId: 'SourceBox', length: revision === 1 ? 4 : 8, width: revision === 1 ? 5 : 9, height: 6, volume: revision === 1 ? 120 : 432, surfaceArea: revision === 1 ? 148 : 348, shapeId: `source-${revision}` },
SecondBox: { objectId: 'SecondBox', length: 3, width: 7, height: revision === 1 ? 2 : 4, volume: revision === 1 ? 42 : 84, surfaceArea: revision === 1 ? 82 : 122, shapeId: `second-${revision}` },
},
})
test('external PartDesign links close, cache, relink and preserve history', () => {
const registry = new ExternalPartDesignLinkRegistry(source())
assert.equal(registry.snapshot().properties.XLinkSubList.length, 2)
registry.closeSource()
assert.deepEqual(registry.snapshot().properties.XLinkList, [])
assert.equal(registry.snapshot().binder.cacheState, 'cached')
registry.markSourceMissing()
const restored = ExternalPartDesignLinkRegistry.deserialize(registry.serialize())
assert.equal(restored.snapshot().source.status, 'missing')
restored.relinkSource(source())
restored.editSource(source(2))
assert.equal(restored.snapshot().binder.area, 54)
restored.undo()
assert.equal(restored.snapshot().binder.area, 30)
restored.redo()
assert.equal(restored.snapshot().binder.area, 54)
})
test('external PartDesign links reject divergent relink and unresolved edits', () => {
const registry = new ExternalPartDesignLinkRegistry(source())
registry.closeSource()
assert.throws(() => registry.editSource(source(2)), /open and resolved/)
assert.throws(() => registry.relinkSource({ ...source(), path: '/projects/Other.FCStd' }), /identity/)
assert.throws(() => new ExternalPartDesignLinkRegistry({ ...source(), objects: { ...source().objects, SourceBox: { ...source().objects.SourceBox, volume: 1 } } }), /geometry evidence/)
})

View File

@@ -17,7 +17,7 @@ import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory' import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider' import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol' import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
import { probeFreeCadPrivateNamingAbi } from '../src/facade/nativeNamingAbi' import { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi, selectFreeCadPrivateNamingHistoryRecords } from '../src/facade/nativeNamingAbi'
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient' import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy' import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, FREECAD_SKETCHER_CONSTRAINT_TYPES, FREECAD_SKETCHER_GEOMETRY_TYPES, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher' import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, FREECAD_SKETCHER_CONSTRAINT_TYPES, FREECAD_SKETCHER_GEOMETRY_TYPES, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
@@ -705,6 +705,79 @@ test('native naming ABI capabilities forbid synthetic FreeCAD tokens unless a pr
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' }) assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' })
}) })
test('FreeCAD naming ABI excludes native history sources without an external transport input', () => {
const records = [
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'generated' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 1 },
{ relation: 'modified' as const, source: 'base-input', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
{ relation: 'generated' as const, source: 'object', sourceId: 'native-cylinder', kind: 'face', sourceIndex: 0, resultIndex: 3 },
]
const baseInput = { inputId: 'base-input', objectId: 'base', role: 'object', step: 'ISO-10303-21; base', objectTag: 17 }
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput]), [records[0], records[2]])
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput, { inputId: 'tool-input', role: 'tool' }]), [records[0], records[1], records[2]])
const request = createFreeCadPrivateNamingAbiRequest({
requestId: 'hole-naming', documentId: 'doc', documentVersion: 1, operationId: 'hole', operation: 'hole', stageId: 'hole:stage', resultObjectId: 'hole:result', resultObjectTag: 23,
inputs: [baseInput], stages: [{ stageId: 'hole:stage', operation: 'hole', inputIds: ['base-input'], ordinal: 0 }],
history: { provider: 'occt-native', occtVersion: '8.0.0', records, hasModified: true, hasGenerated: true, hasDeleted: false },
})
assert.deepEqual(request.history.records.map(({ source, resultIndex }) => ({ source, resultIndex })), [{ source: 'object', resultIndex: 0 }, { source: 'base-input', resultIndex: 2 }])
})
test('FreeCAD naming ABI preserves non-unique native provenance instead of selecting the first source', () => {
const inputs = [
{ inputId: 'base', objectId: 'base-object', role: 'object', stageId: 'base-stage', step: 'ISO-10303-21; base', objectTag: 17 },
{ inputId: 'tool', objectId: 'tool-object', role: 'tool', stageId: 'tool-stage', step: 'ISO-10303-21; tool', objectTag: 19 },
]
const records = [
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'modified' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 0 },
{ relation: 'generated' as const, source: 'object', kind: 'edge', sourceIndex: 1, resultIndexes: [1, 1] },
{ relation: 'generated' as const, source: 'native-profile', sourceId: 'native-profile', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
]
const classification = classifyFreeCadPrivateNamingHistory(records, inputs)
assert.deepEqual(classification.records.map(({ kind, resultIndex }) => ({ kind, resultIndex })), [{ kind: 'edge', resultIndex: 1 }])
assert.equal(classification.externalRecordCount, 3)
assert.equal(classification.internalRecordCount, 1)
assert.equal(classification.duplicateRecordCount, 1)
assert.equal(classification.relationConflictCount, 0)
assert.deepEqual(classification.ambiguities, [{
resultKind: 'face', resultIndex: 0, candidates: [
{ inputId: 'base', objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
{ inputId: 'tool', objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
],
}])
const request = createFreeCadPrivateNamingAbiRequest({
requestId: 'ambiguous', documentId: 'doc', documentVersion: 1, operationId: 'common', operation: 'common', stageId: 'common:stage', resultObjectId: 'common:result', resultObjectTag: 23,
inputs, stages: [{ stageId: 'common:stage', operation: 'common', inputIds: ['base', 'tool'], ordinal: 0 }],
history: { provider: 'occt-native', occtVersion: '8.0.0', records: records.slice(0, 2), hasModified: true, hasGenerated: false, hasDeleted: false },
})
let called = false
const module = {
freecadNamingAbiVersion: () => 1,
freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['common'] }),
freecadNamingEvidenceJson: () => { called = true; return '{}' },
}
const evidence = captureFreeCadPrivateNamingEvidence(module, request)
assert.equal(called, false)
assert.equal(evidence?.status, 'ambiguous')
assert.deepEqual(evidence?.mappedNames?.[0].candidates, [
{ objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage' },
{ objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage' },
])
const downstream = classifyFreeCadPrivateNamingHistory(
[{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
[{ inputId: 'object', objectId: 'common:result', role: 'object', stageId: 'common:stage', step: 'ISO-10303-21; common', namingEvidence: evidence }],
)
assert.equal(downstream.records.length, 0)
assert.deepEqual(downstream.ambiguities[0].candidates.map(({ objectId, persistentId }) => ({ objectId, persistentId })), [
{ objectId: 'base-object', persistentId: 'Face1' },
{ objectId: 'tool-object', persistentId: 'Face1' },
])
})
test('versioned FreeCAD private naming ABI is capability-gated and attaches validated native evidence', async () => { test('versioned FreeCAD private naming ABI is capability-gated and attaches validated native evidence', async () => {
const elementMap2 = parseElementMap2([ const elementMap2 = parseElementMap2([
'BeginElementMap v1', '1 PostfixCount 1', 'Edge', 'MapCount 1', 'ElementMap 1 1 1', 'Edge', 'BeginElementMap v1', '1 PostfixCount 1', 'Edge', 'MapCount 1', 'ElementMap 1 1 1', 'Edge',
@@ -737,7 +810,13 @@ test('versioned FreeCAD private naming ABI is capability-gated and attaches vali
assert.equal(capturedRequest?.resultObjectTag, 23) 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<{ 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<{ objectId?: string }>)[0].objectId, 'base')
assert.equal((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string } }>)[0].namingEvidence?.stageId, 'source:stage:0') assert.deepEqual((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string; status: string; mappedNames?: unknown[] } }>)[0].namingEvidence, {
schemaVersion: 1,
stageId: 'source:stage:0',
resultObjectId: 'source',
status: 'final-shape-only',
reason: 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.',
})
assert.notEqual((capturedRequest?.inputs as Array<{ namingEvidence?: unknown }>)[0].namingEvidence, inputEvidence) 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/) 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/)
@@ -4974,6 +5053,11 @@ test('FCStd LinkSub uses native multi-subelement XML and round-trips without JSO
assert.match(writtenXml, /<LinkSub value="pad" count="2"><Sub value="Edge1"\/><Sub value="Edge7"\/><\/LinkSub>/) assert.match(writtenXml, /<LinkSub value="pad" count="2"><Sub value="Edge1"\/><Sub value="Edge7"\/><\/LinkSub>/)
const roundTripped = inspectFcstdArchive(zipSync({ 'Document.xml': written })).objects.find((object) => object.name === 'fillet')?.properties[0] const roundTripped = inspectFcstdArchive(zipSync({ 'Document.xml': written })).objects.find((object) => object.name === 'fillet')?.properties[0]
assert.deepEqual(roundTripped?.subElements, ['Edge1', 'Edge7']) assert.deepEqual(roundTripped?.subElements, ['Edge1', 'Edge7'])
const edgeRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'edge', persistentId: 'Edge1', topologyVersion: 2, generation: 1, status: 'stable' }
const withRef = (ref: TopoRefValue) => serializeFcstdMetadataArchive({ ...document, objects: [document.objects[0], { ...document.objects[1], properties: [{ ...document.objects[1].properties[0], value: { schemaVersion: 1, objectId: 'pad', subElements: [ref] } }] }] })
assert.match(new TextDecoder().decode(unzipSync(withRef(edgeRef))['Document.xml']), /<Sub value="Edge1"\/>/)
assert.throws(() => withRef({ ...edgeRef, status: 'ambiguous', candidates: ['Edge1', 'Edge2'] }), /stable topology status/)
assert.throws(() => withRef({ ...edgeRef, persistentId: 'web-edge-id' }), /lossless native EdgeN/)
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(xml.replace('count="2"', 'count="3"')) })), /LinkSub count/) assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(xml.replace('count="2"', 'count="3"')) })), /LinkSub count/)
}) })
@@ -4990,6 +5074,9 @@ test('FCStd LinkSubList uses native obj/sub children and preserves structured en
const summary = inspectFcstdArchive(serializeFcstdMetadataArchive(document)).objects.find((object) => object.name === 'holder')?.properties[0] const summary = inspectFcstdArchive(serializeFcstdMetadataArchive(document)).objects.find((object) => object.name === 'holder')?.properties[0]
assert.deepEqual(summary?.linkSubs, [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }]) assert.deepEqual(summary?.linkSubs, [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }])
assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }] }, decoded: true }) assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }] }, decoded: true })
const faceRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
const ambiguousDocument = { ...document, objects: [{ ...document.objects[0], properties: [{ ...document.objects[0].properties[0], value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: faceRef }] } }] }, ...document.objects.slice(1)] }
assert.throws(() => serializeFcstdMetadataArchive(ambiguousDocument), /stable topology status/)
const malformed = '<Document><Objects><Object name="holder" type="Part::Feature"/></Objects><ObjectData><Object name="holder"><Properties><Property name="References" type="App::PropertyLinkSubList"><LinkSubList count="2"><Link obj="pad" sub="Face1"/></LinkSubList></Property></Properties></Object></ObjectData></Document>' const malformed = '<Document><Objects><Object name="holder" type="Part::Feature"/></Objects><ObjectData><Object name="holder"><Properties><Property name="References" type="App::PropertyLinkSubList"><LinkSubList count="2"><Link obj="pad" sub="Face1"/></LinkSubList></Property></Properties></Object></ObjectData></Document>'
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(malformed) })), /LinkSubList count/) assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(malformed) })), /LinkSubList count/)
}) })

View File

@@ -73,13 +73,13 @@ test('private naming boundary checker cross-validates all authoritative status f
'config/freecad-private-naming-source-readiness.json', 'config/freecad-private-naming-source-readiness.json',
'config/freecad-naming-sdk-plan.json', 'config/freecad-naming-sdk-plan.json',
]) assert.match(source, new RegExp(path.replaceAll('.', '\\.'))) ]) assert.match(source, new RegExp(path.replaceAll('.', '\\.')))
assert.match(source, /shippedWorkerImplementation !== 'not-linked'/) assert.match(source, /shippedWorkerImplementation !== 'freecad-linked'/)
assert.match(source, /task\.status !== 'in_progress'/) assert.match(source, /task\.status !== 'completed'/)
assert.match(source, /systemExactEvaluation\?\.exact !== false/) assert.match(source, /systemExactEvaluation\?\.exact !== false/)
assert.match(source, /production\?\.workerLinked !== false/) assert.match(source, /production\?\.workerLinked !== true/)
assert.match(source, /candidateAbi\?\.scope !== 'isolated-non-production'/) assert.match(source, /candidateAbi\?\.scope !== 'isolated-non-production'/)
assert.match(source, /callback\.startsWith\('freecadNamingCandidate'\)/) assert.match(source, /callback\.startsWith\('freecadNamingCandidate'\)/)
assert.match(source, /FreeCAD private naming Worker bridge/) assert.match(source, /productionLinkedComponents/)
assert.match(source, /sdkPlan\.productionPublication !== false/) assert.match(source, /sdkPlan\.productionPublication !== false/)
}) })
@@ -157,12 +157,12 @@ test('SDK readiness verifies the complete candidate wasm closure and keeps it pr
assert.match(sdkOverlay, /worktree add --detach/) assert.match(sdkOverlay, /worktree add --detach/)
assert.match(sdkOverlay, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/) assert.match(sdkOverlay, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/)
assert.match(sdkConfigure, /FreeCADBase|BUILD_PART/) assert.match(sdkConfigure, /FreeCADBase|BUILD_PART/)
assert.match(sdkConfigure, /callbacks: \[\]/) assert.match(sdkConfigure, /freecadNamingEvidenceJson/)
assert.match(sdkBuild, /cmake --build "\$\{BUILD_DIR\}" --target Part/) assert.match(sdkBuild, /cmake --build "\$\{BUILD_DIR\}" --target Part/)
assert.match(sdkBuild, /libFreeCADBase\.a/) assert.match(sdkBuild, /libFreeCADBase\.a/)
assert.match(sdkBuild, /ElementMap\.cpp\.o/) assert.match(sdkBuild, /ElementMap\.cpp\.o/)
assert.match(sdkBuild, /productionManifestGenerated: false/) assert.match(sdkBuild, /productionManifestGenerated: false/)
assert.match(sdkBuild, /availability: 'unavailable'/) assert.match(sdkBuild, /availability: 'available'/)
assert.match(bridgeBuild, /libHacl_Hash_SHA2\.a/) assert.match(bridgeBuild, /libHacl_Hash_SHA2\.a/)
assert.match(bridgeBuild, /--preload-file/) assert.match(bridgeBuild, /--preload-file/)
assert.match(bridgeBuild, /FREECAD_NAMING_BRIDGE_REPORT/) assert.match(bridgeBuild, /FREECAD_NAMING_BRIDGE_REPORT/)
@@ -214,4 +214,11 @@ test('real wasm and offline smoke lanes rebuild and execute the source prerequis
assert.match(real, /probe:freecad-attachment-modes/) assert.match(real, /probe:freecad-attachment-modes/)
assert.match(real, /name\.startsWith\('test:chrome-'\)/) assert.match(real, /name\.startsWith\('test:chrome-'\)/)
assert.match(real, /requestedPhase === 'execute' \? chromeExecution : chromeChecks/) assert.match(real, /requestedPhase === 'execute' \? chromeExecution : chromeChecks/)
assert.match(real, /test:freecad-naming-production/)
assert.match(real, /test:freecad-isomorphic-provenance/)
assert.match(real, /probe:freecad-native-property-semantics/)
assert.match(offline, /publish:occt-history/)
assert.match(offline, /check:freecad-naming-production/)
assert.match(offline, /check:freecad-isomorphic-provenance/)
assert.match(offline, /check:freecad-native-property-semantics/)
}) })

View File

@@ -38,6 +38,7 @@ test('offline profile covers runtime, native sources, browsers and host recovery
'camotics-worktree', 'camotics-worktree',
'linuxcnc-worktree', 'linuxcnc-worktree',
'occt-history-dist', 'occt-history-dist',
'freecad-naming-wasm-sdk',
'planegcs-dist', 'planegcs-dist',
'qt6-wasm-core-downloads', 'qt6-wasm-core-downloads',
'qt6-wasm-core-source', 'qt6-wasm-core-source',
@@ -46,3 +47,11 @@ test('offline profile covers runtime, native sources, browsers and host recovery
'debian-package-cache', 'debian-package-cache',
]) assert.ok(ids.has(id), id) ]) assert.ok(ids.has(id), id)
}) })
test('offline profile restores the linked naming SDK and production Worker', () => {
const byId = new Map(config.archives.map((resource) => [resource.id, resource]))
assert.equal(byId.get('freecad-naming-wasm-sdk')?.source, '.cache/toolchains/freecad-naming-sdk')
assert.equal(byId.get('freecad-naming-wasm-sdk')?.required, true)
assert.equal(byId.get('occt-history-dist')?.source, 'native/occt-history/dist')
assert.equal(byId.get('occt-history-dist')?.required, true)
})