feat: complete production naming and reference lifecycle gates
This commit is contained in:
692
config/chrome-freecad-naming-production-verification.json
Normal file
692
config/chrome-freecad-naming-production-verification.json
Normal 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"
|
||||
}
|
||||
1081
config/chrome-partdesign-reference-lifecycle-verification.json
Normal file
1081
config/chrome-partdesign-reference-lifecycle-verification.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -81,8 +81,10 @@
|
||||
"artifact": {
|
||||
"javascript": "native/occt-history/dist/bitbybit-occt-history.js",
|
||||
"wasm": "native/occt-history/dist/bitbybit-occt-history.wasm",
|
||||
"javascriptSha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557",
|
||||
"wasmSha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6"
|
||||
"data": "native/occt-history/dist/bitbybit-occt-history.data",
|
||||
"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",
|
||||
"historyProtocol": {
|
||||
@@ -97,11 +99,11 @@
|
||||
"status": "explicit-runtime-contract",
|
||||
"mappedNameRef": "optional provider evidence; final-shape-only stages are marked and cannot mint FreeCAD tokens",
|
||||
"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": {
|
||||
"status": "contract-only",
|
||||
"status": "production-linked",
|
||||
"prerequisiteStatus": "real-isolated-freecad-private-naming-bridge-pass",
|
||||
"sdkReadinessStatus": "candidate-complete",
|
||||
"sdkEnvironment": "FREECAD_WASM_SDK_DIR",
|
||||
@@ -110,7 +112,8 @@
|
||||
"requiredFreecadVersion": "1.1.1",
|
||||
"requiredSourceCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
|
||||
"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",
|
||||
"qtFreecadSdkBuildCommand": "./npmw run build:qt6-freecad-wasm",
|
||||
"yamlCppBuildCommand": "./npmw run build:yaml-cpp-wasm",
|
||||
@@ -127,16 +130,19 @@
|
||||
"candidateWorkerTestCommand": "./npmw run test:freecad-naming-worker-candidate",
|
||||
"candidateChromeRunCommand": "./npmw run test:chrome-freecad-naming-worker-candidate",
|
||||
"candidateChromeCheckCommand": "./npmw run check:chrome-freecad-naming-worker-candidate",
|
||||
"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",
|
||||
"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": {
|
||||
"entry": "src/facade/nativeHistoryWorkerEntry.ts",
|
||||
"client": "src/facade/nativeHistoryWorkerClient.ts",
|
||||
"moduleUrl": "/native/occt-history/bitbybit-occt-history.js",
|
||||
"runtimeConfiguration": "BitbybitGeometryRuntime.configureNativeHistory(provider, timeoutMs)",
|
||||
"artifactPublishCommand": "./npmw run build:occt-history",
|
||||
"artifactPublishCommand": "./npmw run build:occt-history",
|
||||
"status": "default-application-worker-with-fallback",
|
||||
"browserVerification": "config/occt-browser-verification.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"] }
|
||||
},
|
||||
"systemExactEvaluation": {
|
||||
"evaluatedAt": "2026-08-11",
|
||||
"evaluatedAt": "2026-08-14",
|
||||
"exact": false,
|
||||
"featureExactCount": 0,
|
||||
"plan": "config/freecad-web-exact-parity-plan.json",
|
||||
"promotionTask": "EX-REL-01",
|
||||
"taskStatus": { "completed": 2, "inProgress": 37, "pending": 13, "blocked": 0 },
|
||||
"blockers": ["shipped Worker lacks the FreeCAD-linked private naming ABI implementation", "browser builder naming evidence transport", "non-unique isomorphic source"]
|
||||
"taskStatus": { "completed": 5, "inProgress": 36, "pending": 11, "blocked": 0 },
|
||||
"blockers": ["exhaustive cross-feature naming and FCStd round-trip corpus", "complete native document and property semantics"]
|
||||
},
|
||||
"rules": [
|
||||
"UI status does not imply geometry or file compatibility.",
|
||||
|
||||
34974
config/freecad-attachment-combination-oracle.json
Normal file
34974
config/freecad-attachment-combination-oracle.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
197
config/freecad-composite-history-resave-verification.json
Normal file
197
config/freecad-composite-history-resave-verification.json
Normal 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
|
||||
}
|
||||
}
|
||||
195
config/freecad-isomorphic-provenance-verification.json
Normal file
195
config/freecad-isomorphic-provenance-verification.json
Normal 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"
|
||||
}
|
||||
@@ -59,11 +59,11 @@
|
||||
{
|
||||
"id": "PAR-01",
|
||||
"title": "Expand Datum/Attachment/ShapeBinder, multi-Body and parameter combinations",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dependencies": ["SDK-05"],
|
||||
"deliverables": ["Native oracle corpus", "Edit/recompute/recovery/FCStd round-trip matrix"],
|
||||
"acceptance": ["Every family has success and failure evidence", "Support/Tip/references survive mutation without silent rebinding"],
|
||||
"evidence": ["check:freecad-sketcher-partdesign-abi", "check:freecad-partdesign-structure", "check:freecad-attachment-modes", "check:chrome-partdesign-lifecycle"]
|
||||
"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",
|
||||
@@ -76,12 +76,12 @@
|
||||
}
|
||||
],
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"freecadNamingBuildStatus": "production-linked",
|
||||
"exTsn02": "completed",
|
||||
"systemExact": false,
|
||||
"productionWorker": {
|
||||
"availability": "unavailable",
|
||||
"callbacks": []
|
||||
"availability": "available",
|
||||
"callbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
689
config/freecad-naming-production-verification.json
Normal file
689
config/freecad-naming-production-verification.json
Normal 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"
|
||||
}
|
||||
@@ -270,8 +270,8 @@
|
||||
],
|
||||
"productionPublication": false,
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"freecadNamingBuildStatus": "production-linked",
|
||||
"exTsn02": "completed",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
|
||||
893
config/freecad-native-property-semantics.json
Normal file
893
config/freecad-native-property-semantics.json
Normal 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"
|
||||
}
|
||||
@@ -64,14 +64,17 @@
|
||||
"hostAdapterBound": true,
|
||||
"productionEligible": false
|
||||
},
|
||||
"notLinkedProductionComponents": [
|
||||
"productionLinkedComponents": [
|
||||
"FreeCADBase static library",
|
||||
"FreeCADApp static library",
|
||||
"Part static library",
|
||||
"Python static library",
|
||||
"real Application and Document integration",
|
||||
"FreeCAD private naming Worker bridge"
|
||||
],
|
||||
"remainingExactComponents": [
|
||||
"complete Application and Document parity",
|
||||
"exhaustive FCStd edit and round-trip corpus"
|
||||
],
|
||||
"candidateAbi": {
|
||||
"scope": "isolated-non-production",
|
||||
"exports": [
|
||||
@@ -90,8 +93,12 @@
|
||||
},
|
||||
"production": {
|
||||
"workerArtifact": "native/occt-history/dist/bitbybit-occt-history.wasm",
|
||||
"workerLinked": false,
|
||||
"exportedCallbacks": [],
|
||||
"workerLinked": true,
|
||||
"exportedCallbacks": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
"freecadNamingEvidenceJson"
|
||||
],
|
||||
"requiredCallbacks": [
|
||||
"freecadNamingAbiVersion",
|
||||
"freecadNamingCapabilitiesJson",
|
||||
@@ -99,8 +106,8 @@
|
||||
]
|
||||
},
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"freecadNamingBuildStatus": "production-linked",
|
||||
"exTsn02": "completed",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"claim": {
|
||||
"scope": "supported-web-facade",
|
||||
"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": {
|
||||
"geometryTypes": [
|
||||
@@ -92,7 +92,7 @@
|
||||
"ElementMap2"
|
||||
],
|
||||
"runtimeProfile": "freecad-private-v1-optional-worker",
|
||||
"shippedWorkerImplementation": "not-linked",
|
||||
"shippedWorkerImplementation": "freecad-linked",
|
||||
"fallback": "final-shape-only",
|
||||
"syntheticTokenGeneration": "forbidden"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
],
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"freecadNamingBuildStatus": "production-linked",
|
||||
"exTsn02": "completed",
|
||||
"systemExact": false,
|
||||
"workerLinked": false,
|
||||
"callbacks": []
|
||||
"workerLinked": true,
|
||||
"callbacks": ["freecadNamingAbiVersion", "freecadNamingCapabilitiesJson", "freecadNamingEvidenceJson"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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-TSN-00", "title": "Fail closed when native naming or builder evidence is absent", "priority": "P0", "status": "completed", "dependencies": [], "deliverables": ["Runtime naming evidence contract", "final-shape-only and ambiguous persistence", "ElementMap2 writer evidence guard"], "acceptance": ["Final geometry cannot mint a private FreeCAD token", "Unproven stage relations and isomorphic sources cannot become stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": [] },
|
||||
{ "id": "EX-TSN-01", "title": "Capture native stage Shape and Generated/Modified/Deleted for every builder", "priority": "P0", "status": "completed", "dependencies": ["EX-KER-01", "EX-TSN-00"], "deliverables": ["All builder-stage captures", "Multi-input and one-to-many lineage", "Local feature recompute history"], "acceptance": ["Every feature result relation has a producing native stage", "No final-result index is applied to an intermediate Shape"], "evidence": ["check:freecad-composite-history-elementmap", "check:freecad-tsn-stage-evidence"], "exactBlockedBy": [] },
|
||||
{ "id": "EX-TSN-02", "title": "Expose native MappedNameRef and StringHasher decisions for every feature", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-01"], "deliverables": ["Native naming callback ABI", "Per-stage StringHasher evidence", "Lossless ElementMap2 token generation"], "acceptance": ["All new tokens originate in native evidence", "FreeCAD round-trip names remain byte and semantic stable"], "evidence": ["check:freecad-native-naming-evidence", "check:freecad-sketcher-partdesign-abi", "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-03", "title": "Resolve isomorphic topology only from unique native provenance", "priority": "P0", "status": "in_progress", "dependencies": ["EX-TSN-01", "EX-TSN-02"], "deliverables": ["Symmetric Boolean corpus", "Candidate provenance graph", "Ambiguity repair lifecycle"], "acceptance": ["Unique native sources resolve deterministically", "Non-unique sources remain ambiguous across save, undo and recompute"], "evidence": ["test:topology-replay", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["Symmetric cases without unique native provenance remain intentionally ambiguous"] },
|
||||
{ "id": "EX-TSN-04", "title": "Close topology naming across all feature edits and FCStd round-trips", "priority": "P0", "status": "pending", "dependencies": ["EX-TSN-02", "EX-TSN-03"], "deliverables": ["Cross-feature mutation matrix", "ElementMap2 history migration", "Long-chain LinkSub stability report"], "acceptance": ["Wrong bindings, unexplained relations and name drift are zero for the exhaustive corpus"], "evidence": ["check:freecad-fcstd-roundtrip", "check:freecad-exact-history-elementmap-gate"], "exactBlockedBy": ["Exhaustive cross-feature naming corpus is incomplete"] }
|
||||
{ "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": "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": "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",
|
||||
"title": "Application document, property and recompute semantics",
|
||||
"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-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"] },
|
||||
@@ -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-PART-01", "title": "Complete all Part primitives, builders, booleans, healing and inspection", "priority": "P0", "status": "in_progress", "dependencies": ["EX-KER-01", "EX-TSN-04"], "deliverables": ["All Part commands and parameters", "Healing and tolerance tools", "Native history for local operations"], "acceptance": ["Success and failure outputs match native Shape, history and diagnostics"], "evidence": ["check:chrome-part-primitives", "check:freecad-golden-fixtures"], "exactBlockedBy": ["Current golden corpus does not enumerate every Part command and option"] },
|
||||
{ "id": "EX-PD-01", "title": "Complete all PartDesign features, parameters and additive/subtractive combinations", "priority": "P0", "status": "in_progress", "dependencies": ["EX-PART-01", "EX-TSN-04"], "deliverables": ["Complete PartDesign feature matrix", "Body Tip and feature-list semantics", "Per-feature native history and naming"], "acceptance": ["Every feature edit, failure recovery and save-reopen matches FreeCAD"], "evidence": ["check:partdesign-closure", "check:freecad-sketcher-partdesign-abi", "check:chrome-partdesign-lifecycle"], "exactBlockedBy": ["The parameter contract covers the supported Facade subset, not every registered PartDesign TypeId or cross-product"] },
|
||||
{ "id": "EX-PD-02", "title": "Complete attachment, datum, ShapeBinder, SubShapeBinder and body workflows", "priority": "P0", "status": "pending", "dependencies": ["EX-PD-01", "EX-DOC-04"], "deliverables": ["All map modes and support rules", "Datum and binder lifecycle", "Multi-body and cross-document corpus"], "acceptance": ["Support migration, visibility, Tip and references remain exact through mutation"], "evidence": ["check:chrome-partdesign-lifecycle", "check:freecad-fcstd-roundtrip", "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",
|
||||
"title": "FCStd and all import/export entrypoints",
|
||||
"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-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"] },
|
||||
|
||||
544
config/freecad-xlink-relink-oracle.json
Normal file
544
config/freecad-xlink-relink-oracle.json
Normal 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
|
||||
}
|
||||
@@ -17,8 +17,10 @@
|
||||
"nativeOcct": {
|
||||
"version": "8.0.0",
|
||||
"sourceCommit": "bb75017e915f6509a4bfa8b551f853352754c2da",
|
||||
"javascriptSha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557",
|
||||
"wasmSha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6"
|
||||
"buildStatus": "production-linked",
|
||||
"javascriptSha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0",
|
||||
"wasmSha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515",
|
||||
"dataSha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
|
||||
},
|
||||
"golden": {
|
||||
"manifestScenarioCount": 100,
|
||||
|
||||
@@ -111,6 +111,13 @@
|
||||
"libraryPath": "artifacts/occt-history-dist.tar.zst",
|
||||
"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",
|
||||
"source": "native/planegcs/dist",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"wrong-document-root": 77
|
||||
},
|
||||
"timing": {
|
||||
"p95Ms": 0.137,
|
||||
"maxMs": 2.691
|
||||
"p95Ms": 0.209,
|
||||
"maxMs": 3.268
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"zero-revolution-axis": 67
|
||||
},
|
||||
"timing": {
|
||||
"p95Ms": 0.02,
|
||||
"maxMs": 1.133
|
||||
"p95Ms": 0.022,
|
||||
"maxMs": 1.146
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"invalid": 800
|
||||
},
|
||||
"timing": {
|
||||
"p95Ms": 0.032,
|
||||
"maxMs": 0.733
|
||||
"p95Ms": 0.061,
|
||||
"maxMs": 1.509
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,20 @@
|
||||
"bytes": 556597,
|
||||
"sha256": "1679307fb0f01e9d7b9f37a081458bf44583e4d1899edf9224b49a1435ce7105"
|
||||
},
|
||||
{
|
||||
"path": "assets/index-B7ENiszF.js",
|
||||
"bytes": 1134628,
|
||||
"sha256": "53bd3b2876d56238158be315cfdf8d7143767c46c0a429271d9985c36f003ca4"
|
||||
},
|
||||
{
|
||||
"path": "assets/index-BnRqhr3m.css",
|
||||
"bytes": 65976,
|
||||
"sha256": "b8eacf86224d2705e3c27367b3a0b166f4d662a5b8142d34a8abcdeef58753ec"
|
||||
},
|
||||
{
|
||||
"path": "assets/nativeHistoryWorkerEntry-C7hZKjun.js",
|
||||
"bytes": 21764,
|
||||
"sha256": "a4fbc5e765e1d919c8f066b6b07e2064f62e3c291496972a2b4006069a17902b"
|
||||
"path": "assets/index-BZhOOUrh.js",
|
||||
"bytes": 1135258,
|
||||
"sha256": "f8d20cf0ceeb131295d21f8d9809a542aa8871a50e74a712fdc065be307fd021"
|
||||
},
|
||||
{
|
||||
"path": "assets/nativeHistoryWorkerEntry-BEDB1h7o.js",
|
||||
"bytes": 26720,
|
||||
"sha256": "461d02c24e96338bf3028da9d0c2b2d2021665954ecf46b21357cf51805a747d"
|
||||
},
|
||||
{
|
||||
"path": "assets/persistenceWorker-DQjay47f.js",
|
||||
@@ -232,6 +232,11 @@
|
||||
"bytes": 190,
|
||||
"sha256": "3cb76a9187f3f0cda082bfcf28c78c5bbf6abfc78a3cdb57d8560c2a7632fa51"
|
||||
},
|
||||
{
|
||||
"path": "chrome-partdesign-reference-lifecycle-harness.html",
|
||||
"bytes": 5646,
|
||||
"sha256": "eb682b4a3681e189e80c90ec9ac9c246bb6922f9da2f0fe9ea0e3b3b25f05903"
|
||||
},
|
||||
{
|
||||
"path": "chrome-partdesign-transform-harness.html",
|
||||
"bytes": 200,
|
||||
@@ -330,22 +335,27 @@
|
||||
{
|
||||
"path": "index.html",
|
||||
"bytes": 678,
|
||||
"sha256": "019c7f7b5255a12f94a19ae72edc50a45343e81779bed1b68842a934b31da01a"
|
||||
"sha256": "b78bfb7e023a1f03371b5f2c322cb367a9a05c622e45a06e37670c87cc68cd3a"
|
||||
},
|
||||
{
|
||||
"path": "manifest.webmanifest",
|
||||
"bytes": 344,
|
||||
"sha256": "cc7f6ffa211d1a79486885182e2e129640b0bf4367a18a2d2d08bd2c6cff3758"
|
||||
},
|
||||
{
|
||||
"path": "native/occt-history/bitbybit-occt-history.data",
|
||||
"bytes": 3322679,
|
||||
"sha256": "ada204b276208027a5cb1cc1288d4aab95b8ee1f28e3a43a00cd062720b87e99"
|
||||
},
|
||||
{
|
||||
"path": "native/occt-history/bitbybit-occt-history.js",
|
||||
"bytes": 237925,
|
||||
"sha256": "78f1cdd17a6a7b61ff8435c0ff8c912a43c4517e6ef49f3f741521662a903557"
|
||||
"bytes": 359739,
|
||||
"sha256": "6a90575b81ceab5c00a2440e45e010bd62d3455b012541b4449f7b0e88a644c0"
|
||||
},
|
||||
{
|
||||
"path": "native/occt-history/bitbybit-occt-history.wasm",
|
||||
"bytes": 23091986,
|
||||
"sha256": "b52125bd2c4999f62d435f0a34664556b7f1feb23590ddc0503fc6d3c7f1c8d6"
|
||||
"bytes": 37482285,
|
||||
"sha256": "c03cf553e71bf4e74667c5aada921769bb241d6021f28a5b166b49c738965515"
|
||||
},
|
||||
{
|
||||
"path": "native/occt-history/browser-harness-worker.js",
|
||||
@@ -380,7 +390,7 @@
|
||||
{
|
||||
"path": "sw.js",
|
||||
"bytes": 1390,
|
||||
"sha256": "5389aba344f93a8948bfed30aa20c0a1c38cdba7d9ebf120964d0897bba7e8cc"
|
||||
"sha256": "020250b7a88f8106b0666ad9d7070aefab28892371e4c670ab3d164b9e0922b6"
|
||||
},
|
||||
{
|
||||
"path": "vendor/camotics/camotics-sweep.wasm",
|
||||
@@ -402,7 +412,7 @@
|
||||
"status": "signed",
|
||||
"algorithm": "Ed25519",
|
||||
"keyId": "bitbybit-local-release-2026",
|
||||
"payloadSha256": "c8734e374954b614941eb295623252af2094e9de7e415a46a399936d1cc391a9",
|
||||
"signature": "vtnNIPTf3sCXN2F0c5lRVO7E76DXuolr+B5dZ8YQ34rWeWbJvgLqFVIqQ3IHke9nQVUhwYOR7SR2EyG3gucBAQ=="
|
||||
"payloadSha256": "44d3d76c20d3a5d4e691b2d98cc5dc6fa1df910e492bf811fa64133fb2f6177a",
|
||||
"signature": "m2GxJ+PLZ2RcDSI9w0hUMF+ArOLKydE6ilbgRXKgJlj8qiIRrgvaFghAZQL23NGws++j/9Xv78EsaXRvZC8oDA=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 门禁。
|
||||
|
||||
同一 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
|
||||
|
||||
新增独立 `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`。后四阶段均有 ElementMap2,StringHasher 条目单调增长 `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` 均通过。
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
## 当前状态
|
||||
|
||||
- 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`
|
||||
- promotion task:`EX-REL-01=pending`
|
||||
- 三项硬边界:无原生 MappedNameRef/StringHasher、无逐阶段 builder、无唯一同构来源时 fail-closed
|
||||
- 三项硬边界:私有 token 只来自原生 MappedNameRef/StringHasher、逐阶段 builder 关系必须有原生证据、无唯一同构来源时 fail-closed
|
||||
|
||||
## 实施顺序
|
||||
|
||||
|
||||
@@ -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` 候选,校验失败直接阻断写回。
|
||||
|
||||
`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 和阶段 DAG;object tag 会从 `Document.xml` 的 `Object.id` 导入,按稳定值写回 FCStd,并通过 SQLite schema v7、OPFS 重开和 checkpoint 往返。`build:freecad-naming-worker` 是独立的 fail-closed 构建入口,只接受锁定提交、Emscripten 3.1.69、wasm Qt/Python、FreeCADBase/FreeCADApp/Part 静态库及三回调 bridge 的哈希清单;普通 `build:occt-history` 继续是 OCCT-only。`check:occt-history-artifact` 会实际实例化 WASM 并探测三回调,当前结果为 unavailable,因此 `EX-TSN-02=in_progress` 与 `systemExact=false` 保持不变。
|
||||
ABI 输入现同时携带文档 `objectId`、FCStd 原生正整数 object tag、上一阶段命名证据、明确的 result object/tag 和阶段 DAG;object 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/DATA;OCCT-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 数据可交叉编译/运行,不构成生产命名实现。
|
||||
|
||||
`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。
|
||||
|
||||
隔离 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 已求解全部圆锥曲线约束。
|
||||
|
||||
@@ -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 签名发布。
|
||||
|
||||
`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 通过。
|
||||
|
||||
@@ -417,6 +417,14 @@ Sketcher/PartDesign 的 Web 支持范围新增可执行参数合同:Sketcher
|
||||
|
||||
### 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 exact:219 个阶段都有 IndexedName,但只有 74 个阶段的全部子形有 MappedName,145 个阶段只有原生索引身份;当前识别出的 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`。
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
}],
|
||||
"productionPublication": false,
|
||||
"boundary": {
|
||||
"freecadNamingBuildStatus": "contract-only",
|
||||
"exTsn02": "in_progress",
|
||||
"freecadNamingBuildStatus": "production-linked",
|
||||
"exTsn02": "completed",
|
||||
"systemExact": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { resolve } from 'node:path'
|
||||
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\./)
|
||||
const object = occt.makeBox(10, 10, 10)
|
||||
const tool = occt.makeBox(5, 5, 5)
|
||||
|
||||
24
package.json
24
package.json
File diff suppressed because one or more lines are too long
95
public/chrome-partdesign-reference-lifecycle-harness.html
Normal file
95
public/chrome-partdesign-reference-lifecycle-harness.html
Normal 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>
|
||||
@@ -69,12 +69,12 @@ const report = {
|
||||
productionPublication: false,
|
||||
productionManifestGenerated: false,
|
||||
boundary: {
|
||||
freecadNamingBuildStatus: 'contract-only',
|
||||
exTsn02: 'in_progress',
|
||||
freecadNamingBuildStatus: 'production-linked',
|
||||
exTsn02: 'completed',
|
||||
systemExact: false,
|
||||
workerLinked: false,
|
||||
availability: 'unavailable',
|
||||
callbacks: [],
|
||||
workerLinked: true,
|
||||
availability: 'available',
|
||||
callbacks: ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson'],
|
||||
},
|
||||
}
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
|
||||
36
scripts/check-chrome-partdesign-reference-lifecycle.mjs
Normal file
36
scripts/check-chrome-partdesign-reference-lifecycle.mjs
Normal 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))
|
||||
36
scripts/check-freecad-attachment-combination-oracle.mjs
Normal file
36
scripts/check-freecad-attachment-combination-oracle.mjs
Normal 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))
|
||||
@@ -6,8 +6,11 @@ import { migrateStringHasherSchema, parseStringHasherTable, validateStringHasher
|
||||
|
||||
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 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.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 parsedStringHasherResources = 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 (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}.`)
|
||||
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))
|
||||
|
||||
@@ -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 (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`)
|
||||
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 (elementMapResources === 0) globalBlockers.push('composite oracle has no ElementMap2 resources')
|
||||
if (stringHasherResources === 0) globalBlockers.push('composite oracle has no StringHasher resources')
|
||||
|
||||
33
scripts/check-freecad-isomorphic-provenance.mjs
Normal file
33
scripts/check-freecad-isomorphic-provenance.mjs
Normal 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))
|
||||
@@ -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 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 (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))
|
||||
|
||||
54
scripts/check-freecad-naming-production.mjs
Normal file
54
scripts/check-freecad-naming-production.mjs
Normal 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))
|
||||
@@ -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.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to the required FreeCAD/Emscripten wasm target.')
|
||||
if (plan.productionPublication !== false) fail('candidate SDK readiness cannot publish a production Worker.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false) fail('plan boundary must remain contract-only/in_progress/non-exact.')
|
||||
if (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 (!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.')
|
||||
|
||||
@@ -19,7 +19,7 @@ const required = (value, name) => {
|
||||
const fail = (message) => { throw new Error(`FreeCAD WASM naming SDK: ${message}`) }
|
||||
|
||||
if (!sdkRoot) {
|
||||
console.log(JSON.stringify({ status: 'sdk-not-configured', availability: 'unavailable', systemExact: false, reason: 'FREECAD_WASM_SDK_DIR is not configured; the shipped Worker remains OCCT-only.' }, null, 2))
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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.emscriptenVersion, 'emscriptenVersion') !== '3.1.69') fail('emscriptenVersion must be 3.1.69.')
|
||||
if (required(manifest.qtTarget, 'qtTarget') !== 'wasm32-emscripten' || required(manifest.pythonTarget, 'pythonTarget') !== 'wasm32-emscripten') fail('Qt and Python must both be wasm32-emscripten targets.')
|
||||
if (manifest.productionPublication !== false || manifest.boundary?.freecadNamingBuildStatus !== 'contract-only' || manifest.boundary?.exTsn02 !== 'in_progress' || manifest.boundary?.systemExact !== false) fail('manifest must preserve the candidate-only contract-only/in_progress/non-exact boundary.')
|
||||
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 resolvedIncludeDirs = includeDirs.map((includeDir) => isAbsolute(includeDir) ? includeDir : resolve(sdkRoot, includeDir))
|
||||
const libraries = Array.isArray(manifest.libraries) ? manifest.libraries : fail('libraries must be an array.')
|
||||
|
||||
@@ -9,7 +9,7 @@ const matrix = await load('config/compatibility-matrix.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 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
|
||||
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.`)
|
||||
|
||||
19
scripts/check-freecad-native-property-semantics.mjs
Normal file
19
scripts/check-freecad-native-property-semantics.mjs
Normal 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))
|
||||
@@ -3,81 +3,71 @@ import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
|
||||
const [abi, matrix, plan, 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/compatibility-matrix.json'),
|
||||
load('config/freecad-web-exact-parity-plan.json'),
|
||||
load('config/freecad-private-naming-source-readiness.json'),
|
||||
load('config/freecad-naming-sdk-plan.json'),
|
||||
load('config/freecad-naming-next-tasks.json'),
|
||||
readFile(resolve(root, 'scripts/build-freecad-naming-source-probe.sh'), 'utf8'),
|
||||
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'),
|
||||
])
|
||||
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 task = tasks.find((entry) => entry.id === 'EX-TSN-02')
|
||||
if (!task) fail('EX-TSN-02 is missing from the exact parity plan.')
|
||||
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'not-linked') fail('the shipped ABI contract must remain not-linked until the production artifact exports verified callbacks.')
|
||||
if (abi.claim?.exactFreeCadParity !== false) fail('the supported facade contract cannot claim exact FreeCAD parity.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.status !== 'contract-only') fail('the FreeCAD naming build must remain contract-only before a locked SDK and callback probe exist.')
|
||||
if (abi.privateNamingAbi?.shippedWorkerImplementation !== 'freecad-linked') fail('the shipped ABI contract must identify the verified FreeCAD-linked Worker.')
|
||||
if (abi.claim?.exactFreeCadParity !== false || matrix.systemExactEvaluation?.exact !== false) fail('production linkage cannot promote system exactness.')
|
||||
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?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must identify the complete candidate SDK without implying production linkage.')
|
||||
if (task.status !== 'in_progress') fail(`EX-TSN-02 must remain in_progress, received ${String(task.status)}.`)
|
||||
if (matrix.systemExactEvaluation?.exact !== false) fail('systemExact must remain false.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessStatus !== 'candidate-complete') fail('the SDK readiness status must retain the complete SDK prerequisite.')
|
||||
if (task.status !== 'completed' || task.exactBlockedBy?.length !== 0) fail(`EX-TSN-02 must be completed and blocker-free, received ${String(task.status)}.`)
|
||||
const blockers = matrix.systemExactEvaluation?.blockers ?? []
|
||||
if (!blockers.includes('shipped Worker lacks the FreeCAD-linked private naming ABI implementation')) fail('systemExact blockers must retain the missing FreeCAD-linked Worker implementation.')
|
||||
if (!task.exactBlockedBy?.includes('The shipped Worker does not link the FreeCAD private naming implementation for every builder')) fail('EX-TSN-02 must retain its production Worker linkage blocker.')
|
||||
if (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.')
|
||||
|
||||
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 (sdkPlan.scope !== 'pre-production-sdk-readiness' || sdkPlan.productionPublication !== false) fail('SDK readiness must remain pre-production and must not publish a Worker.')
|
||||
if (sdkPlan.boundary?.freecadNamingBuildStatus !== 'contract-only' || sdkPlan.boundary?.exTsn02 !== 'in_progress' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
|
||||
if (nextTasks.boundary?.freecadNamingBuildStatus !== 'contract-only' || nextTasks.boundary?.exTsn02 !== 'in_progress' || nextTasks.boundary?.systemExact !== false || nextTasks.boundary?.productionWorker?.availability !== 'unavailable' || nextTasks.boundary?.productionWorker?.callbacks?.length !== 0) fail('next-task plan disagrees with the production boundary.')
|
||||
if (matrix.nativeOcctHistory?.freecadNamingBuild?.sdkReadinessCommand !== './npmw run check:freecad-naming-sdk-readiness') fail('compatibility matrix must expose the SDK readiness command.')
|
||||
if (readiness.production?.workerLinked !== false) fail('the readiness manifest cannot claim that the production Worker is linked.')
|
||||
if (readiness.production?.exportedCallbacks?.length !== 0) fail('the prerequisite probe cannot declare production callback exports.')
|
||||
if (readiness.boundary?.freecadNamingBuildStatus !== 'contract-only' || readiness.boundary?.exTsn02 !== 'in_progress' || readiness.boundary?.systemExact !== false) fail('the readiness manifest boundary disagrees with the authoritative status files.')
|
||||
if (readiness.candidateAbi?.scope !== 'isolated-non-production') fail('the candidate ABI must remain isolated and non-production.')
|
||||
if (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 !== 'production-linked' || sdkPlan.boundary?.exTsn02 !== 'completed' || sdkPlan.boundary?.systemExact !== false) fail('SDK readiness boundary disagrees with the authoritative status files.')
|
||||
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 (readiness.production?.workerLinked !== true || JSON.stringify(readiness.production?.exportedCallbacks) !== JSON.stringify(callbacks)) fail('the readiness manifest must record the linked production callbacks.')
|
||||
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.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.isolatedSourceArchive?.target !== 'wasm32-emscripten' || readiness.isolatedSourceArchive?.expectedObjectMembers !== 7 || readiness.isolatedSourceArchive?.hostAdapterBound !== true || readiness.isolatedSourceArchive?.productionEligible !== false) fail('the source archive must remain a seven-object, host-adapter-bound wasm prerequisite.')
|
||||
if (readiness.candidateAbi?.strictWebValidator !== 'pass') fail('the candidate ABI must pass the strict Web validator.')
|
||||
if (readiness.candidateAbi?.occtBuilderContext !== false || readiness.candidateAbi?.publishToWorker !== false) fail('the candidate ABI cannot claim OCCT builder context or Worker publication.')
|
||||
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) {
|
||||
if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
|
||||
}
|
||||
for (const callback of readiness.candidateAbi?.exports ?? []) {
|
||||
if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
|
||||
}
|
||||
for (const callback of readiness.production?.requiredCallbacks ?? []) {
|
||||
if (!smokeTest.includes(callback)) fail(`the source smoke test must reject production callback ${callback}.`)
|
||||
}
|
||||
for (const source of readiness.linkedOriginalSources ?? []) {
|
||||
if (!buildScript.includes(source.path.split('/').at(-1)) || !buildScript.includes(source.sha256)) fail(`the source probe build does not pin ${source.path}.`)
|
||||
}
|
||||
for (const expected of ['src/App/StringHasher.cpp', 'src/App/MappedElement.cpp', 'src/App/ElementNamingUtils.cpp', 'src/App/ElementMap.cpp', 'src/Base/Handle.cpp']) {
|
||||
if (!readiness.linkedOriginalSources?.some((source) => source.path === expected)) fail(`the source readiness manifest must include ${expected}.`)
|
||||
}
|
||||
for (const expected of ['FreeCADBase static library', 'FreeCADApp static library', 'Part static library', 'Python static library', 'real Application and Document integration', 'FreeCAD private naming Worker bridge']) {
|
||||
if (!readiness.notLinkedProductionComponents?.includes(expected)) fail(`the source readiness manifest must retain ${expected} as not linked.`)
|
||||
}
|
||||
for (const resource of ['MappedNameRef', 'StringHasher', 'ElementMap2']) if (!readiness.candidateAbi?.nativeResources?.includes(resource)) fail(`the candidate ABI must validate native ${resource} evidence.`)
|
||||
for (const callback of readiness.candidateAbi?.exports ?? []) if (!callback.startsWith('freecadNamingCandidate')) fail(`candidate export ${callback} could be mistaken for a production callback.`)
|
||||
for (const callback of 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 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 (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.')
|
||||
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 (!packageJson.scripts?.['build:occt-history-only-candidate']?.includes('OCCT_HISTORY_PUBLISH=0')) fail('the OCCT-only build must remain an explicitly non-publishing candidate.')
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'boundary-pass',
|
||||
shippedWorkerImplementation: 'not-linked',
|
||||
shippedWorkerImplementation: 'freecad-linked',
|
||||
task: { id: task.id, status: task.status },
|
||||
systemExact: matrix.systemExactEvaluation.exact,
|
||||
systemExact: false,
|
||||
sdkStatus: matrix.nativeOcctHistory.freecadNamingBuild.status,
|
||||
prerequisiteStatus: matrix.nativeOcctHistory.freecadNamingBuild.prerequisiteStatus,
|
||||
sdkReadinessStatus: matrix.nativeOcctHistory.freecadNamingBuild.sdkReadinessStatus,
|
||||
linkedOriginalSources: readiness.linkedOriginalSources.map((source) => source.path),
|
||||
candidateAbi: {
|
||||
scope: readiness.candidateAbi.scope,
|
||||
strictWebValidator: readiness.candidateAbi.strictWebValidator,
|
||||
publishToWorker: readiness.candidateAbi.publishToWorker,
|
||||
},
|
||||
sdkReadiness: {
|
||||
scope: sdkPlan.scope,
|
||||
productionPublication: sdkPlan.productionPublication,
|
||||
requiredLibraries: sdkPlan.libraries.map(({ name }) => name),
|
||||
},
|
||||
workerLinked: readiness.production.workerLinked,
|
||||
callbacks,
|
||||
operations: operations.length,
|
||||
productionReports: { node: nodeReport.generatedAt, chrome: chromeReport.generatedAt },
|
||||
candidateAbi: { scope: readiness.candidateAbi.scope, strictWebValidator: readiness.candidateAbi.strictWebValidator, publishToWorker: readiness.candidateAbi.publishToWorker },
|
||||
sdkReadiness: { scope: sdkPlan.scope, productionPublication: sdkPlan.productionPublication, requiredLibraries: sdkPlan.libraries.map(({ name }) => name) },
|
||||
}, null, 2))
|
||||
|
||||
@@ -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(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(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')
|
||||
|
||||
if (failures.length) {
|
||||
|
||||
@@ -89,5 +89,5 @@ console.log(JSON.stringify({
|
||||
missingBuilderOperations: missingOperations,
|
||||
reports: Object.fromEntries(Object.entries(operationEvidence).sort(([a], [b]) => a.localeCompare(b))),
|
||||
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))
|
||||
|
||||
@@ -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.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.sourceCommit !== lockedCommit || plan.baseline?.emscriptenVersion !== '3.1.69' || plan.baseline?.target !== 'wasm32-emscripten') fail('baseline is not locked to FreeCAD 1.1.1 and Emscripten 3.1.69 wasm32.')
|
||||
if (plan.configuration?.productionPublication !== false) fail('candidate SDK build cannot publish production artifacts.')
|
||||
if (plan.boundary?.freecadNamingBuildStatus !== 'contract-only' || plan.boundary?.exTsn02 !== 'in_progress' || plan.boundary?.systemExact !== false || plan.boundary?.workerLinked !== false || plan.boundary?.callbacks?.length !== 0) fail('production boundary must remain contract-only, unlinked, and callback-free.')
|
||||
if (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.')
|
||||
const expectedDependencies = [
|
||||
['Qt6', '6.8.2', 'npm run build:qt6-freecad-wasm'],
|
||||
|
||||
@@ -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.')
|
||||
|
||||
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.')
|
||||
|
||||
const tasks = [...taskById.values()]
|
||||
|
||||
31
scripts/check-freecad-xlink-relink-oracle.mjs
Normal file
31
scripts/check-freecad-xlink-relink-oracle.mjs
Normal 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))
|
||||
@@ -4,7 +4,7 @@ import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm']
|
||||
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
|
||||
const distRoot = resolve(root, 'native/occt-history/dist')
|
||||
const publicRoot = resolve(root, 'public/native/occt-history')
|
||||
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))
|
||||
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) {
|
||||
const distBytes = await readFile(resolve(distRoot, name))
|
||||
const publicBytes = await readFile(resolve(publicRoot, name))
|
||||
@@ -48,6 +48,18 @@ if (callbacks.length === 3) {
|
||||
const objectStep = nativeModule.shapeToStep(object)
|
||||
const toolStep = nativeModule.shapeToStep(tool)
|
||||
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 = {
|
||||
schemaVersion: 1,
|
||||
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 }],
|
||||
resultStep: history.resultStep,
|
||||
resultBrep: history.resultBrep,
|
||||
history,
|
||||
history: { ...history, records },
|
||||
}
|
||||
evidence = JSON.parse(nativeModule.freecadNamingEvidenceJson(JSON.stringify(request)))
|
||||
} 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 === 'freecad-linked' && namingAbi.availability !== 'available') throw new Error('Shipped-worker contract claims FreeCAD linkage but the artifact ABI probe is unavailable.')
|
||||
if (!['not-linked', 'freecad-linked'].includes(expectedImplementation)) throw new Error('Shipped-worker FreeCAD naming implementation status is invalid.')
|
||||
console.log(JSON.stringify({ status: 'artifact-pass', files, namingAbi, exactBoundary: namingAbi.availability === 'available' ? 'probe-only; FreeCAD evidence still requires locked descriptor and response validation' : 'occt-only; EX-TSN-02 remains in_progress and systemExact=false' }, null, 2))
|
||||
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))
|
||||
|
||||
@@ -34,9 +34,11 @@ assertEqual(sha256(canonicalSurface), baseline.bitbybit.surfaceSha256, 'Bitbybit
|
||||
|
||||
assertEqual(matrix.nativeOcctHistory.occtVersion, baseline.nativeOcct.version, 'Native OCCT version')
|
||||
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 [
|
||||
['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`)
|
||||
}
|
||||
@@ -54,6 +56,6 @@ for (const entry of threeWay.reports) {
|
||||
console.log(JSON.stringify({
|
||||
status: 'occt-upstream-drift-pass',
|
||||
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 }
|
||||
}, null, 2))
|
||||
|
||||
@@ -4,14 +4,16 @@ import { resolve } from 'node:path'
|
||||
const root = resolve(new URL('..', import.meta.url).pathname)
|
||||
const load = async (path) => JSON.parse(await readFile(resolve(root, path), 'utf8'))
|
||||
const 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 transform = await load('config/chrome-partdesign-transform-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]
|
||||
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 (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']
|
||||
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']
|
||||
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.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([
|
||||
['pad', 'config/chrome-native-pad-history-verification.json'],
|
||||
@@ -55,4 +57,4 @@ for (const [operation, path] of historyPaths) {
|
||||
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))
|
||||
|
||||
@@ -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 [
|
||||
fcstdFuzz, geometryFuzz, sketchFuzz, successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures,
|
||||
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, partdesignFailures, partdesignRevolutionGroove, partBuilders,
|
||||
fcstdRoundTrip, app, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
|
||||
sketchOracle, partdesignBase, partdesignLoft, partdesignDressup, partdesignTransform, partdesignStructure, attachmentModes, attachmentCombinations, xlinkRelink, partdesignFailures, partdesignRevolutionGroove, partBuilders,
|
||||
fcstdRoundTrip, app, partdesignReferenceLifecycle, browserMatrix, performance, fault, opfsMigration, security, qa08, addon, script,
|
||||
] = await Promise.all([
|
||||
load('config/qa04-fcstd-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-structure-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-revolution-groove-oracle.json'),
|
||||
load('config/freecad-part-builders-oracle.json'),
|
||||
load('config/freecad-fcstd-roundtrip-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/chrome-performance-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 (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 (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 (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.')
|
||||
@@ -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']
|
||||
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 (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,
|
||||
unitSuites: testFiles.length,
|
||||
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 },
|
||||
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 }])),
|
||||
}, null, 2))
|
||||
|
||||
@@ -100,6 +100,6 @@ if (status === 'blocked') {
|
||||
if (missing.length === 0) missing.push('unclassified CMake configure failure')
|
||||
}
|
||||
await mkdir(resolve(root, '.cache/toolchains/freecad-naming-sdk'), { recursive: true })
|
||||
await writeFile(reportPath, `${JSON.stringify({ schemaVersion: 1, status, exitCode, startedAt, command, missing, buildDir, outputTail: output.slice(-12000), productionPublication: false, boundary: { freecadNamingBuildStatus: 'contract-only', exTsn02: 'in_progress', systemExact: false, workerLinked: false, callbacks: [] } }, null, 2)}\n`)
|
||||
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))
|
||||
if (status !== 'configured') process.exit(exitCode)
|
||||
|
||||
214
scripts/freecad-attachment-combination-oracle.py
Normal file
214
scripts/freecad-attachment-combination-oracle.py
Normal 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=(",", ":")))
|
||||
@@ -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_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)
|
||||
resaved_path = os.path.join(directory, "%s-resaved.FCStd" % case_id)
|
||||
reopened.saveAs(resaved_path)
|
||||
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:
|
||||
return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)}
|
||||
finally:
|
||||
@@ -309,6 +317,6 @@ report = {
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"status": "pass" if len(cases) == 30 and all(case["status"] == "pass" for case in cases) else "failed",
|
||||
"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=(",", ":")))
|
||||
|
||||
347
scripts/freecad-naming-production-matrix.mjs
Normal file
347
scripts/freecad-naming-production-matrix.mjs
Normal 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,
|
||||
}
|
||||
}
|
||||
231
scripts/freecad-xlink-relink-oracle.py
Normal file
231
scripts/freecad-xlink-relink-oracle.py
Normal 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=(",", ":")))
|
||||
@@ -607,7 +607,7 @@ async function smoke() {
|
||||
FREECAD_SOURCE_OFFLINE: '1',
|
||||
OCCT_SOURCE_DIR: resolve(root, '.cache/occt/occt'),
|
||||
}
|
||||
for (const script of ['check:runtime', 'check:freecad-source', '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}`)
|
||||
await run(resolve(root, 'npmw'), ['run', script], { env: environment })
|
||||
}
|
||||
|
||||
18
scripts/publish-occt-history.mjs
Normal file
18
scripts/publish-occt-history.mjs
Normal 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))
|
||||
80
scripts/run-chrome-freecad-naming-production.mjs
Normal file
80
scripts/run-chrome-freecad-naming-production.mjs
Normal 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)
|
||||
}
|
||||
78
scripts/run-chrome-partdesign-reference-lifecycle.mjs
Normal file
78
scripts/run-chrome-partdesign-reference-lifecycle.mjs
Normal 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)
|
||||
27
scripts/run-freecad-attachment-combination-oracle.mjs
Normal file
27
scripts/run-freecad-attachment-combination-oracle.mjs
Normal 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))
|
||||
@@ -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 script = resolve(root, 'scripts/freecad-composite-history-elementmap-oracle.py')
|
||||
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}`)
|
||||
const execution = spawnSync(executable, ['--python-path', pythonPath, script], {
|
||||
cwd: root,
|
||||
@@ -28,5 +29,29 @@ const markerIndex = output.indexOf(marker)
|
||||
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()}`)
|
||||
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`)
|
||||
console.log(JSON.stringify(report, null, 2))
|
||||
|
||||
223
scripts/run-freecad-isomorphic-provenance.ts
Normal file
223
scripts/run-freecad-isomorphic-provenance.ts
Normal 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))
|
||||
23
scripts/run-freecad-naming-production.mjs
Normal file
23
scripts/run-freecad-naming-production.mjs
Normal 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))
|
||||
82
scripts/run-freecad-native-property-semantics.mjs
Normal file
82
scripts/run-freecad-native-property-semantics.mjs
Normal 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))
|
||||
27
scripts/run-freecad-xlink-relink-oracle.mjs
Normal file
27
scripts/run-freecad-xlink-relink-oracle.mjs
Normal 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))
|
||||
@@ -39,6 +39,8 @@ const wasmBuilds = process.env.CI_REAL_REBUILD_WASM === '1'
|
||||
'build:boost-wasm',
|
||||
'build: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-wasm-sdk-build-plan',
|
||||
'build:occt-history',
|
||||
@@ -80,6 +82,8 @@ const lanes = {
|
||||
'probe:freecad-partdesign-transform',
|
||||
'probe:freecad-partdesign-structure',
|
||||
'probe:freecad-attachment-modes',
|
||||
'probe:freecad-attachment-combinations',
|
||||
'probe:freecad-xlink-relink',
|
||||
'probe:freecad-partdesign-failures',
|
||||
'probe:freecad-partdesign-revolution-groove',
|
||||
'probe:freecad-part-builders',
|
||||
@@ -87,6 +91,8 @@ const lanes = {
|
||||
'test:freecad-fcstd-native',
|
||||
'generate:freecad-parameter-mutations',
|
||||
'check:freecad-oracle-coverage',
|
||||
'probe:freecad-native-property-semantics',
|
||||
'check:freecad-native-property-semantics',
|
||||
'check:freecad-golden-coverage',
|
||||
'check:freecad-parameter-mutations',
|
||||
'check:freecad-sketcher-constraints',
|
||||
@@ -97,6 +103,10 @@ const lanes = {
|
||||
],
|
||||
wasm: [
|
||||
...wasmBuilds,
|
||||
'test:freecad-naming-production',
|
||||
'check:freecad-naming-production',
|
||||
'test:freecad-isomorphic-provenance',
|
||||
'check:freecad-isomorphic-provenance',
|
||||
'check:occt-history-artifact',
|
||||
'check:freecad-private-naming-boundary',
|
||||
'test:browser-occt',
|
||||
|
||||
181
src/facade/externalPartDesignLinks.ts
Normal file
181
src/facade/externalPartDesignLinks.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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'] }) }
|
||||
}
|
||||
|
||||
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 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') {
|
||||
@@ -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.`)
|
||||
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.`)
|
||||
const subElement = entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : entry.subElement.persistentId
|
||||
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.`)
|
||||
const subElement = entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : nativeLinkTopoRefName(entry.subElement, `FCStd LinkSubList property ${property.name}`, entry.objectId)
|
||||
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)}"/>`
|
||||
}).join('')
|
||||
@@ -1013,9 +1021,9 @@ const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId
|
||||
const value = property.value
|
||||
if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'objectId' in value && typeof value.objectId === 'string') {
|
||||
objectId = value.objectId
|
||||
if ('subElements' in value && Array.isArray(value.subElements)) subElements = value.subElements.map((entry) => typeof entry === 'string' ? entry : entry.persistentId)
|
||||
else if ('persistentId' in value && typeof value.persistentId === 'string') subElements = [value.persistentId]
|
||||
else if ('subElement' in value && value.subElement) subElements = [typeof value.subElement === 'string' ? value.subElement : value.subElement.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 = [nativeLinkTopoRefName(value as TopoRefValue, `FCStd LinkSub property ${property.name}`, objectId)]
|
||||
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.`)
|
||||
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('')
|
||||
|
||||
@@ -79,8 +79,8 @@ export { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NativeOc
|
||||
export { NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } 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 { captureFreeCadPrivateNamingEvidence, createFreeCadPrivateNamingAbiRequest, FREECAD_PRIVATE_NAMING_ABI_VERSION, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES, probeFreeCadPrivateNamingAbi } from './nativeNamingAbi'
|
||||
export type { FreeCadPrivateNamingAbiDescriptor, FreeCadPrivateNamingAbiProbe, FreeCadPrivateNamingAbiRequest, NativeFreeCadNamingAbiModule } from './nativeNamingAbi'
|
||||
export { 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, FreeCadPrivateNamingHistoryAmbiguity, FreeCadPrivateNamingHistoryCandidate, FreeCadPrivateNamingHistoryClassification, NativeFreeCadNamingAbiModule } from './nativeNamingAbi'
|
||||
export { assertNativeNamingEvidence, cloneNativeNamingEvidence, createFinalShapeOnlyNamingEvidence, createNativeStageNamingEvidence, hasNativeMappedNameEvidence, validateNativeNamingEvidence } from './nativeNamingEvidence'
|
||||
export type { NativeMappedNameRef, NativeMappedNameRelation, NativeNamingEvidenceIssue, NativeNamingEvidenceReport, NativeNamingEvidenceStatus, NativeStageNamingEvidence } from './nativeNamingEvidence'
|
||||
export { NativeOcctHistoryWorkerProvider } from './nativeHistoryWorkerClient'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider'
|
||||
|
||||
@@ -54,15 +54,197 @@ export type FreeCadPrivateNamingAbiRequest = {
|
||||
resultStep?: string
|
||||
resultBrep?: 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'>
|
||||
}
|
||||
|
||||
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 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 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 => {
|
||||
if (typeof module.freecadNamingAbiVersion !== 'function' || typeof module.freecadNamingCapabilitiesJson !== 'function' || typeof module.freecadNamingEvidenceJson !== 'function') return {
|
||||
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 (!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.')
|
||||
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 {
|
||||
schemaVersion: 1,
|
||||
...input,
|
||||
inputs: input.inputs.map((entry) => {
|
||||
if (!entry.inputId.trim() || !entry.step.trim()) throw new TypeError('FreeCAD naming ABI inputs require non-empty inputId and STEP text.')
|
||||
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)) } : {}) }
|
||||
}),
|
||||
inputs: abiInputs,
|
||||
stages: input.stages.map((entry) => ({ ...entry, inputIds: [...entry.inputIds] })),
|
||||
...(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 => {
|
||||
if (probe.availability !== 'available' || !probe.descriptor || typeof module.freecadNamingEvidenceJson !== 'function') 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 requestBytes = encoder.encode(requestJson).byteLength
|
||||
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}`)
|
||||
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}`)
|
||||
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.',
|
||||
})
|
||||
}
|
||||
|
||||
39
tests/externalPartDesignLinks.test.ts
Normal file
39
tests/externalPartDesignLinks.test.ts
Normal 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/)
|
||||
})
|
||||
@@ -17,7 +17,7 @@ import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
|
||||
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
|
||||
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
|
||||
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 { 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'
|
||||
@@ -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' })
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const elementMap2 = parseElementMap2([
|
||||
'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?.inputs as Array<{ objectTag?: number; namingEvidence?: { stageId: string } }>)[0].objectTag, 17)
|
||||
assert.equal((capturedRequest?.inputs as Array<{ objectId?: string }>)[0].objectId, 'base')
|
||||
assert.equal((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string } }>)[0].namingEvidence?.stageId, 'source:stage:0')
|
||||
assert.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)
|
||||
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>/)
|
||||
const roundTripped = inspectFcstdArchive(zipSync({ 'Document.xml': written })).objects.find((object) => object.name === 'fillet')?.properties[0]
|
||||
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/)
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
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 })
|
||||
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>'
|
||||
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(malformed) })), /LinkSubList count/)
|
||||
})
|
||||
|
||||
@@ -73,13 +73,13 @@ test('private naming boundary checker cross-validates all authoritative status f
|
||||
'config/freecad-private-naming-source-readiness.json',
|
||||
'config/freecad-naming-sdk-plan.json',
|
||||
]) assert.match(source, new RegExp(path.replaceAll('.', '\\.')))
|
||||
assert.match(source, /shippedWorkerImplementation !== 'not-linked'/)
|
||||
assert.match(source, /task\.status !== 'in_progress'/)
|
||||
assert.match(source, /shippedWorkerImplementation !== 'freecad-linked'/)
|
||||
assert.match(source, /task\.status !== 'completed'/)
|
||||
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, /callback\.startsWith\('freecadNamingCandidate'\)/)
|
||||
assert.match(source, /FreeCAD private naming Worker bridge/)
|
||||
assert.match(source, /productionLinkedComponents/)
|
||||
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, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/)
|
||||
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, /libFreeCADBase\.a/)
|
||||
assert.match(sdkBuild, /ElementMap\.cpp\.o/)
|
||||
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, /--preload-file/)
|
||||
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, /name\.startsWith\('test:chrome-'\)/)
|
||||
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/)
|
||||
})
|
||||
|
||||
@@ -38,6 +38,7 @@ test('offline profile covers runtime, native sources, browsers and host recovery
|
||||
'camotics-worktree',
|
||||
'linuxcnc-worktree',
|
||||
'occt-history-dist',
|
||||
'freecad-naming-wasm-sdk',
|
||||
'planegcs-dist',
|
||||
'qt6-wasm-core-downloads',
|
||||
'qt6-wasm-core-source',
|
||||
@@ -46,3 +47,11 @@ test('offline profile covers runtime, native sources, browsers and host recovery
|
||||
'debian-package-cache',
|
||||
]) 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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user