feat: advance FreeCAD exact parity evidence
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
@@ -34,8 +35,32 @@ def square_sketch(body, name, half_size=2.0, z=0.0):
|
||||
|
||||
def shape_summary(shape):
|
||||
if shape is None or shape.isNull():
|
||||
return {"valid": False, "solids": 0, "faces": 0, "edges": 0, "volume": 0.0}
|
||||
return {"valid": bool(shape.isValid()), "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "volume": float(shape.Volume)}
|
||||
return {"valid": False, "solids": 0, "faces": 0, "edges": 0, "vertices": 0, "volume": 0.0, "area": 0.0, "brepSha256": None}
|
||||
brep = shape.exportBrepToString()
|
||||
bounds = shape.BoundBox
|
||||
geometry = {
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"volume": round(float(shape.Volume), 9),
|
||||
"area": round(float(shape.Area), 9),
|
||||
"bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
"vertexPoints": sorted([round(float(vertex.Point.x), 9), round(float(vertex.Point.y), 9), round(float(vertex.Point.z), 9)] for vertex in shape.Vertexes),
|
||||
"edgeLengths": sorted(round(float(edge.Length), 9) for edge in shape.Edges),
|
||||
"faceAreas": sorted(round(float(face.Area), 9) for face in shape.Faces),
|
||||
}
|
||||
return {
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
"edges": len(shape.Edges),
|
||||
"vertices": len(shape.Vertexes),
|
||||
"volume": float(shape.Volume),
|
||||
"area": float(shape.Area),
|
||||
"brepSha256": hashlib.sha256(brep.encode("utf-8")).hexdigest(),
|
||||
"geometryDigest": digest_json(geometry),
|
||||
}
|
||||
|
||||
|
||||
def history_entry(obj, name):
|
||||
@@ -59,6 +84,81 @@ def relation_from_mapped_name(mapped_name):
|
||||
return "preserved"
|
||||
|
||||
|
||||
def normalize_mapped_name(mapped_name):
|
||||
# StringHasher IDs are process-local. Preserve the token grammar while
|
||||
# removing only the volatile hash-table slot assigned in this process.
|
||||
return re.sub(r":H[0-9a-fA-F]+", ":H#", mapped_name or "")
|
||||
|
||||
|
||||
def source_element_name(mapped_name):
|
||||
match = re.search(r"(?:^|[;(])((?:Face|Edge|Vertex)\d+)", mapped_name or "")
|
||||
if match:
|
||||
return match.group(1)
|
||||
local_token = re.search(r"(?:^|[;(])(#[a-zA-Z0-9]+:[0-9a-fA-F]+)", mapped_name or "")
|
||||
if local_token:
|
||||
return local_token.group(1)
|
||||
sketch_token = re.search(r"(?:^|[;(])(g\d+(?:v\d+)?;SKT)", mapped_name or "")
|
||||
if sketch_token:
|
||||
return sketch_token.group(1)
|
||||
indexed_token = re.search(r"(?:^|[;(])(#[0-9a-fA-F]+)", mapped_name or "")
|
||||
return indexed_token.group(1) if indexed_token else ""
|
||||
|
||||
|
||||
def linked_objects(obj):
|
||||
result = set()
|
||||
|
||||
def collect(value):
|
||||
if hasattr(value, "Name") and getattr(value, "Document", None) is obj.Document:
|
||||
result.add(value.Name)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for item in value:
|
||||
collect(item)
|
||||
|
||||
for property_name in obj.PropertiesList:
|
||||
try:
|
||||
property_type = obj.getTypeIdOfProperty(property_name)
|
||||
except Exception:
|
||||
continue
|
||||
if "PropertyLink" not in property_type:
|
||||
continue
|
||||
try:
|
||||
collect(getattr(obj, property_name))
|
||||
except Exception:
|
||||
pass
|
||||
result.discard(obj.Name)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def relation_records(obj, names):
|
||||
records = []
|
||||
for entry in names:
|
||||
history = entry.get("history") or []
|
||||
sources = []
|
||||
for item in history:
|
||||
if item.get("object") == obj.Name or not item.get("object"):
|
||||
continue
|
||||
sources.append({
|
||||
"sourceObject": item.get("object"),
|
||||
"sourceTypeId": item.get("typeId"),
|
||||
"sourceElement": source_element_name(item.get("mappedName")),
|
||||
"mappedName": normalize_mapped_name(item.get("mappedName")),
|
||||
"children": sorted(normalize_mapped_name(child) for child in (item.get("children") or [])),
|
||||
})
|
||||
records.append({
|
||||
"resultName": entry["name"],
|
||||
"mappedName": normalize_mapped_name(entry.get("mappedName")),
|
||||
"indexedName": entry.get("indexedName") or "",
|
||||
"relation": relation_from_mapped_name(entry.get("mappedName")),
|
||||
"sources": sorted(sources, key=lambda source: (source["sourceObject"], source["sourceElement"], source["mappedName"])),
|
||||
})
|
||||
return records
|
||||
|
||||
|
||||
def digest_json(value):
|
||||
payload = json.dumps(value, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def native_mapping(obj, kind, index, name, mapped_name, mapped_ids, indexed_name, indexed_ids, history):
|
||||
reference_name = mapped_name or indexed_name or name
|
||||
source_refs = []
|
||||
@@ -132,6 +232,7 @@ def stage_report(obj):
|
||||
"history": history,
|
||||
})
|
||||
native_mapped_names.append(native_mapping(obj, kind, index, name, mapped, mapped_ids, indexed, indexed_ids, history))
|
||||
relations = relation_records(obj, names)
|
||||
native_evidence = {
|
||||
"schemaVersion": 1,
|
||||
"stageId": obj.Name,
|
||||
@@ -159,10 +260,167 @@ def stage_report(obj):
|
||||
"elementMapVersion": getattr(shape, "ElementMapVersion", None),
|
||||
"elementMapSize": int(getattr(shape, "ElementMapSize", 0) or 0),
|
||||
"names": names,
|
||||
"linkedObjects": linked_objects(obj),
|
||||
"relations": relations,
|
||||
"relationDigest": digest_json(relations),
|
||||
"semanticNameDigest": digest_json([{
|
||||
"name": entry["name"],
|
||||
"mappedName": normalize_mapped_name(entry.get("mappedName")),
|
||||
"indexedName": entry.get("indexedName") or "",
|
||||
} for entry in names]),
|
||||
"nativeEvidence": native_evidence,
|
||||
}
|
||||
|
||||
|
||||
def correlation_snapshot(case_id, stages):
|
||||
return [{
|
||||
"key": "%s:%03d:%s" % (case_id, ordinal, stage["name"]),
|
||||
"ordinal": ordinal,
|
||||
"name": stage["name"],
|
||||
"typeId": stage["typeId"],
|
||||
"linkedObjects": stage["linkedObjects"],
|
||||
"relationCount": len(stage["relations"]),
|
||||
"relationDigest": stage["relationDigest"],
|
||||
"semanticNameDigest": stage["semanticNameDigest"],
|
||||
} for ordinal, stage in enumerate(stages)]
|
||||
|
||||
|
||||
def parameter_value(value):
|
||||
if hasattr(value, "Value"):
|
||||
return float(value.Value)
|
||||
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
|
||||
return [float(value.x), float(value.y), float(value.z)]
|
||||
if isinstance(value, (bool, int, float, str)) or value is None:
|
||||
return value
|
||||
return str(value)
|
||||
|
||||
|
||||
def mutation_stage_snapshot(case_id, stages):
|
||||
return [{
|
||||
"key": "%s:%03d:%s" % (case_id, ordinal, stage["name"]),
|
||||
"ordinal": ordinal,
|
||||
"name": stage["name"],
|
||||
"typeId": stage["typeId"],
|
||||
"shape": stage["shape"],
|
||||
"relationDigest": stage["relationDigest"],
|
||||
"semanticNameDigest": stage["semanticNameDigest"],
|
||||
} for ordinal, stage in enumerate(stages)]
|
||||
|
||||
|
||||
def mutation_geometry_fingerprint(stage):
|
||||
return stage["shape"]["geometryDigest"]
|
||||
|
||||
|
||||
def mutation_naming_fingerprint(stage):
|
||||
return (stage["relationDigest"], stage["semanticNameDigest"])
|
||||
|
||||
|
||||
def collect_named_stages(document, stage_names):
|
||||
stages = []
|
||||
for stage_name in stage_names:
|
||||
obj = document.getObject(stage_name)
|
||||
stage = stage_report(obj) if obj is not None else None
|
||||
if stage is None:
|
||||
raise RuntimeError("mutation removed or invalidated stage %s" % stage_name)
|
||||
stages.append(stage)
|
||||
return stages
|
||||
|
||||
|
||||
def capture_mutation(case_id, document, construction, before_stages):
|
||||
final = construction["final"]
|
||||
target = construction["mutationTarget"]
|
||||
property_name = construction["mutationProperty"]
|
||||
edited_value = construction["mutationValue"]
|
||||
stage_names = [stage["name"] for stage in before_stages]
|
||||
stage_ordinals = {stage_name: ordinal for ordinal, stage_name in enumerate(stage_names)}
|
||||
if target.Name not in stage_ordinals or final.Name not in stage_ordinals:
|
||||
raise RuntimeError("mutation target or final object has no captured Shape stage")
|
||||
if property_name not in target.PropertiesList:
|
||||
raise RuntimeError("mutation property %s.%s is not registered" % (target.Name, property_name))
|
||||
editor_modes = list(target.getEditorMode(property_name))
|
||||
property_type = target.getTypeIdOfProperty(property_name)
|
||||
native_editable = "ReadOnly" not in editor_modes and "Hidden" not in editor_modes
|
||||
original_value = getattr(target, property_name)
|
||||
before_value = parameter_value(original_value)
|
||||
try:
|
||||
setattr(target, property_name, edited_value)
|
||||
document.recompute()
|
||||
edited_value_readback = parameter_value(getattr(target, property_name))
|
||||
edited_stages = collect_named_stages(document, stage_names)
|
||||
finally:
|
||||
setattr(target, property_name, original_value)
|
||||
document.recompute()
|
||||
restored_value = parameter_value(getattr(target, property_name))
|
||||
restored_stages = collect_named_stages(document, stage_names)
|
||||
before_geometry = [mutation_geometry_fingerprint(stage) for stage in before_stages]
|
||||
edited_geometry = [mutation_geometry_fingerprint(stage) for stage in edited_stages]
|
||||
restored_geometry = [mutation_geometry_fingerprint(stage) for stage in restored_stages]
|
||||
before_naming = [mutation_naming_fingerprint(stage) for stage in before_stages]
|
||||
edited_naming = [mutation_naming_fingerprint(stage) for stage in edited_stages]
|
||||
restored_naming = [mutation_naming_fingerprint(stage) for stage in restored_stages]
|
||||
changed_ordinals = [ordinal for ordinal, (before, edited) in enumerate(zip(before_geometry, edited_geometry)) if before != edited]
|
||||
restoration_drift_ordinals = [ordinal for ordinal, (before, restored) in enumerate(zip(before_geometry, restored_geometry)) if before != restored]
|
||||
naming_changed_ordinals = [ordinal for ordinal, (before, edited) in enumerate(zip(before_naming, edited_naming)) if before != edited]
|
||||
naming_restoration_drift_ordinals = [ordinal for ordinal, (before, restored) in enumerate(zip(before_naming, restored_naming)) if before != restored]
|
||||
final_ordinal = stage_ordinals[final.Name]
|
||||
target_ordinal = stage_ordinals[target.Name]
|
||||
property_changed = before_value != edited_value_readback
|
||||
final_shape_changed = before_geometry[final_ordinal] != edited_geometry[final_ordinal]
|
||||
final_brep_changed = before_stages[final_ordinal]["shape"]["brepSha256"] != edited_stages[final_ordinal]["shape"]["brepSha256"]
|
||||
property_restored = before_value == restored_value
|
||||
all_stages_restored = len(restoration_drift_ordinals) == 0
|
||||
edited_shapes_valid = all(stage["shape"]["valid"] for stage in edited_stages)
|
||||
status = "pass" if native_editable and property_changed and final_shape_changed and property_restored and all_stages_restored and edited_shapes_valid else "failed"
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"category": construction["category"],
|
||||
"contract": {
|
||||
"targetObject": target.Name,
|
||||
"targetTypeId": target.TypeId,
|
||||
"targetStageOrdinal": target_ordinal,
|
||||
"propertyPath": property_name,
|
||||
"propertyType": property_type,
|
||||
"editorModes": editor_modes,
|
||||
"nativeEditable": native_editable and property_type.startswith("App::Property"),
|
||||
"finalObject": final.Name,
|
||||
"finalStageOrdinal": final_ordinal,
|
||||
"requiresFinalPropagation": True,
|
||||
"requiresAllStageRestore": True,
|
||||
},
|
||||
"values": {
|
||||
"before": before_value,
|
||||
"edited": edited_value_readback,
|
||||
"restored": restored_value,
|
||||
},
|
||||
"phases": {
|
||||
"before": mutation_stage_snapshot(case_id, before_stages),
|
||||
"edited": mutation_stage_snapshot(case_id, edited_stages),
|
||||
"restored": mutation_stage_snapshot(case_id, restored_stages),
|
||||
},
|
||||
"metrics": {
|
||||
"propertyChanged": property_changed,
|
||||
"finalShapeChanged": final_shape_changed,
|
||||
"finalBrepChanged": final_brep_changed,
|
||||
"propertyRestored": property_restored,
|
||||
"allStagesRestored": all_stages_restored,
|
||||
"allStageGeometryRestored": all_stages_restored,
|
||||
"allStageNamingRestored": len(naming_restoration_drift_ordinals) == 0,
|
||||
"editedShapesValid": edited_shapes_valid,
|
||||
"changedStageOrdinals": changed_ordinals,
|
||||
"changedStages": len(changed_ordinals),
|
||||
"restorationDriftOrdinals": restoration_drift_ordinals,
|
||||
"restorationDriftStages": len(restoration_drift_ordinals),
|
||||
"namingChangedStageOrdinals": naming_changed_ordinals,
|
||||
"namingChangedStages": len(naming_changed_ordinals),
|
||||
"namingRestorationDriftOrdinals": naming_restoration_drift_ordinals,
|
||||
"namingRestorationDriftStages": len(naming_restoration_drift_ordinals),
|
||||
"stageRecords": len(before_stages),
|
||||
"phaseStageRecords": len(before_stages) * 3,
|
||||
},
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def make_boolean(doc, operation, index):
|
||||
base = doc.addObject("Part::Box", "Box%02d" % index)
|
||||
base.Length, base.Width, base.Height = 10.0, 10.0, 10.0
|
||||
@@ -176,7 +434,14 @@ def make_boolean(doc, operation, index):
|
||||
else:
|
||||
result = doc.addObject("Part::Cut", "Cut%02d" % index)
|
||||
result.Base, result.Tool = base, tool
|
||||
return result
|
||||
return {
|
||||
"category": "boolean",
|
||||
"final": result,
|
||||
"base": base,
|
||||
"mutationTarget": tool,
|
||||
"mutationProperty": "Radius",
|
||||
"mutationValue": float(tool.Radius.Value) + 0.5,
|
||||
}
|
||||
|
||||
|
||||
def make_partdesign(doc, mode, index):
|
||||
@@ -210,18 +475,37 @@ def make_partdesign(doc, mode, index):
|
||||
elif mode == "pocket-twoside":
|
||||
pocket.Length2 = 1.0
|
||||
doc.recompute()
|
||||
return pocket
|
||||
return pad
|
||||
return {
|
||||
"category": "partdesign",
|
||||
"final": pocket,
|
||||
"mutationTarget": pad,
|
||||
"mutationProperty": "Length",
|
||||
"mutationValue": float(pad.Length.Value) + 0.75,
|
||||
}
|
||||
return {
|
||||
"category": "partdesign",
|
||||
"final": pad,
|
||||
"mutationTarget": pad,
|
||||
"mutationProperty": "Length",
|
||||
"mutationValue": float(pad.Length.Value) + 0.75,
|
||||
}
|
||||
|
||||
|
||||
def make_composite(doc, index):
|
||||
first = make_boolean(doc, "fuse" if index % 2 else "cut", index)
|
||||
first_construction = make_boolean(doc, "fuse" if index % 2 else "cut", index)
|
||||
first = first_construction["final"]
|
||||
second = doc.addObject("Part::Box", "SecondBox%02d" % index)
|
||||
second.Length, second.Width, second.Height = 4.0, 4.0, 4.0
|
||||
second.Placement.Base = App.Vector(3.0, 3.0, 3.0)
|
||||
final = doc.addObject("Part::Cut", "CompositeCut%02d" % index)
|
||||
final.Base, final.Tool = first, second
|
||||
return final
|
||||
return {
|
||||
"category": "composite-boolean",
|
||||
"final": final,
|
||||
"mutationTarget": first_construction["base"],
|
||||
"mutationProperty": "Length",
|
||||
"mutationValue": float(first_construction["base"].Length.Value) + 0.5,
|
||||
}
|
||||
|
||||
|
||||
def make_dressup(doc, mode, index):
|
||||
@@ -236,17 +520,27 @@ def make_dressup(doc, mode, index):
|
||||
feature = body.newObject("PartDesign::Chamfer", "Chamfer%02d" % index)
|
||||
feature.Base, feature.Size = (box, ["Edge1"]), 1.0
|
||||
doc.recompute()
|
||||
return feature
|
||||
return {
|
||||
"category": "dress-up",
|
||||
"final": feature,
|
||||
"mutationTarget": feature,
|
||||
"mutationProperty": "Radius" if mode == "fillet" else "Size",
|
||||
"mutationValue": 1.25,
|
||||
}
|
||||
|
||||
|
||||
def collect_case(case_id, factory, index, directory):
|
||||
name = "ElementMapOracle%02d" % index
|
||||
doc = App.newDocument(name)
|
||||
try:
|
||||
final = factory(doc, index)
|
||||
construction = factory(doc, index)
|
||||
final = construction["final"]
|
||||
doc.recompute()
|
||||
stages = [entry for obj in doc.Objects if (entry := stage_report(obj)) is not None]
|
||||
before_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in stages for entry in stage["names"]}
|
||||
mutation = capture_mutation(case_id, doc, construction, stages)
|
||||
stages = collect_named_stages(doc, [stage["name"] for stage in stages])
|
||||
before_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in stages for entry in stage["names"]}
|
||||
initial_correlations = correlation_snapshot(case_id, stages)
|
||||
final_name = getattr(final, "Name", "")
|
||||
path = os.path.join(directory, "%s.FCStd" % case_id)
|
||||
doc.saveAs(path)
|
||||
@@ -275,7 +569,8 @@ def collect_case(case_id, factory, index, directory):
|
||||
reopened = App.openDocument(path)
|
||||
reopened.recompute()
|
||||
after_stages = [entry for obj in reopened.Objects if (entry := stage_report(obj)) is not None]
|
||||
after_names = {(stage["name"], entry["name"]): entry.get("mappedName") for stage in after_stages for entry in stage["names"]}
|
||||
after_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in after_stages for entry in stage["names"]}
|
||||
reopened_correlations = correlation_snapshot(case_id, after_stages)
|
||||
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)
|
||||
@@ -283,10 +578,11 @@ def collect_case(case_id, factory, index, directory):
|
||||
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"]}
|
||||
resaved_names = {(stage["name"], entry["name"]): normalize_mapped_name(entry.get("mappedName")) for stage in resaved_stages for entry in stage["names"]}
|
||||
resaved_correlations = correlation_snapshot(case_id, resaved_stages)
|
||||
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"}
|
||||
return {"id": case_id, "category": construction["category"], "finalObject": final_name, "stages": stages, "mutation": mutation, "stageCorrelations": {"initial": initial_correlations, "reopened": reopened_correlations, "resaved": resaved_correlations}, "elementMapResources": resources, "stringHasherResource": string_hasher_resource, "roundtripNameDrift": roundtrip_drift, "resaveNameDrift": resave_name_drift, "nativeDesktopResaveCovered": True, "status": "pass" if mutation["status"] == "pass" else "failed"}
|
||||
except Exception as error:
|
||||
return {"id": case_id, "finalObject": "", "stages": [], "elementMapResources": {}, "status": "failed", "error": str(error)}
|
||||
finally:
|
||||
@@ -312,11 +608,30 @@ with tempfile.TemporaryDirectory(prefix="freecad-elementmap-oracle-") as directo
|
||||
version = App.Version()
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"mutationContractVersion": 1,
|
||||
"baselineId": "freecad-1.1.1-composite-history-elementmap2",
|
||||
"freecadVersion": version_text(),
|
||||
"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"), "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)},
|
||||
"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"),
|
||||
"categoryCases": {category: sum(1 for case in cases if case.get("category") == category) for category in ("boolean", "partdesign", "composite-boolean", "dress-up")},
|
||||
"stageCorrelations": sum(len(case.get("stages", [])) for case in cases),
|
||||
"relationRecords": sum(sum(len(stage.get("relations", [])) for stage in case.get("stages", [])) for case in cases),
|
||||
"mutationCases": sum(1 for case in cases if case.get("mutation")),
|
||||
"mutationPassed": sum(1 for case in cases if case.get("mutation", {}).get("status") == "pass"),
|
||||
"mutationStageRecords": sum(case.get("mutation", {}).get("metrics", {}).get("stageRecords", 0) for case in cases),
|
||||
"mutationPhaseStageRecords": sum(case.get("mutation", {}).get("metrics", {}).get("phaseStageRecords", 0) for case in cases),
|
||||
"mutationFinalPropagationFailures": sum(1 for case in cases if case.get("mutation", {}).get("metrics", {}).get("finalShapeChanged") is not True),
|
||||
"mutationStageRestoreFailures": sum(case.get("mutation", {}).get("metrics", {}).get("restorationDriftStages", 0) for case in cases),
|
||||
"mutationNamingRestoreDriftCases": sum(1 for case in cases if case.get("mutation", {}).get("metrics", {}).get("namingRestorationDriftStages", 0) > 0),
|
||||
"mutationNamingRestoreDriftStages": sum(case.get("mutation", {}).get("metrics", {}).get("namingRestorationDriftStages", 0) for case in cases),
|
||||
"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=(",", ":")))
|
||||
|
||||
Reference in New Issue
Block a user