← Commit history
addin/AdomBridge/commands/mechanical/compute.py+167−11
@@ -13,10 +13,17 @@ timeline's health after Fusion's own compute, and the full recompute runs at a c - before an export leaves Fusion; - whenever a caller asks: health with recompute:true. -The counters and the last health a write verified are per document, for this add-in session."""+The counters and the last health a write verified are per document, for this add-in session.++A write is held to the faults the model carried before it (its baseline), not to a perfect model: a+production design can carry old timeline faults that nobody is about to fix. The write may not add a+fault, change one (its health state or message), or leave a fault on what it edits in place; faults it+clears are reported. With a healthy baseline this is the old rule: any fault refuses."""+from collections import Counter import time -from .core import health, require+from . import core+from .core import MechanicalError, health, same  CHECKPOINT = 6.0 # a move to another part checkpoints edits worth one: an edit of existing history, a joint, a delete, two appends@@ -90,30 +97,179 @@ def status(document):             "writesSinceFullCompute": led["writes"], "focus": led["focus"], "lastFullCompute": led["last"]}  +FIRST = 5  # faults a reply lists; the count covers the rest+++def fault_rows(state):+    """A health read's faults, then the items whose health could not be read."""+    return list(state.get("faults") or ()) + list(state.get("unknown") or ())+++def fault_key(row):+    """A fault as writes compare it: the item's name, health state and message. Not its timeline index,+    which moves when an earlier item is deleted."""+    if "error" in row:+        return ("unreadable", row.get("name"), row.get("error"))+    return ("fault", row.get("name"), row.get("state"), row.get("message"))+++def unmatched(rows, others):+    """The rows the other read does not account for, counting repeats: two faults with the same name and+    message are two. Matching runs in timeline order, so the later of two lookalikes is the one left over."""+    budget = Counter(fault_key(row) for row in others)+    left = []+    for row in rows:+        key = fault_key(row)+        if budget[key] > 0:+            budget[key] -= 1+        else:+            left.append(row)+    return left+++def summary(baseline, state=None):+    """The faults the model carried before the write, for its reply: how many, the first few, and the ones+    the write cleared (deleted, suppressed or repaired)."""+    rows = fault_rows(baseline)+    result = {"count": len(rows), "faults": rows[:FIRST]}+    if len(rows) > FIRST:+        result["more"] = len(rows) - FIRST+    if state is not None:+        cleared = unmatched(rows, fault_rows(state))+        if cleared:+            result["cleared"] = {"count": len(cleared), "faults": cleared[:FIRST]}+    return result+++def _native(entity):+    try:+        return getattr(entity, "nativeObject", None) or entity+    except Exception:+        return entity+++# What a write edits in place, by argument. A fault the model already carried there that the write did not+# clear is the write's own result. Inputs it only reads (a profile, an axis, joint geometry) are not, and a+# new item it creates is held to "no new fault".+SUBJECTS = {"feature_edit": "feature", "occurrence_update": "occurrence", "joint_edit": "joint",+            "regular_joint_edit": "joint", "rigid_group_edit": "group", "joint_origin_edit": "origin",+            "motion_link_edit": "link", **{op: "sketch" for op in (+                "sketch_geometry", "sketch_constraint", "sketch_dimension", "sketch_project", "sketch_edit",+                "sketch_curve_edit", "sketch_offset")}}+++def owner_of(design, name):+    """The feature (or sketch dimension, or joint) that owns a model parameter, as a list; a user parameter has none."""+    try:+        parameter = design.allParameters.itemByName(name)+        owner = getattr(parameter, "createdBy", None) if parameter is not None else None+    except (AttributeError, RuntimeError):+        return []+    return [owner] if owner is not None else []+++def subjects(design, value, args):+    """The entities a write edits in place (SUBJECTS; a parameter batch edits the owners of the parameters it+    sets), read before the edit. A reference that does not resolve is left for the operation to refuse."""+    found = []+    key = SUBJECTS.get(args.get("op"))+    if key and isinstance(args.get(key), dict):+        try:+            found.append(core.resolve(design, value, args[key]))+        except (MechanicalError, AttributeError, RuntimeError):+            pass+    if args.get("op") == "parameter_batch":+        for change in args.get("changes") or ():+            if isinstance(change, dict) and change.get("action") == "set":+                found.extend(owner_of(design, change.get("name")))+    return found+++def edited(natives, edits):+    """The faults among natives ((row, timeline item) pairs) that sit on an entity the write edits in place:+    a fault left there means the write's own result is not healthy."""+    if not edits:+        return []+    mine = [_native(entity) for entity in edits if entity is not None]+    found = []+    for row, item in natives:+        try:+            entity = item.entity+        except Exception:+            continue+        if entity is None:+            continue+        entity = _native(entity)+        try:+            if any(same(entity, other) for other in mine):+                found.append(row)+        except Exception:+            continue+    return found+++def judge(state, baseline, natives=(), edits=(), **details):+    """Refuse (unhealthy_model) a health read a write may not leave behind: a fault the baseline lacks (a+    new one, or an existing one whose state or message changed), a fault left on an entity the write edits+    in place, or a full recompute that failed. (Design.computeAll returns true on a model that carries+    faults: measured with 15 lost-reference warnings and with a failed extrude.)"""+    added = unmatched(fault_rows(state), fault_rows(baseline))+    own = [] if added else edited(natives, edits)+    failed = state.get("computeSucceeded") is False+    if not (added or own or failed):+        return+    if added:+        message = "The write added a fault, or changed one the model already had."+    elif own:+        message = "The write left a fault on an entity it edited."+    else:+        message = "Recompute failed."+    raise MechanicalError("unhealthy_model", message, health=state, newFaults=added, editedFaults=own,+                          preExistingFaults=summary(baseline), **details)++ def health_before(design, document, revision):-    """The model's health before a write: what the last write verified when nothing changed since-    (same revision), otherwise a walk of the timeline. Refuses an unhealthy model."""+    """The model's health before a write, the baseline the write is held to: what the last write verified+    when nothing changed since (same revision), otherwise a walk of the timeline. Faults the model already+    carries do not refuse the write; settle refuses one that adds a fault or changes one."""     cached = ledger(document)["verified"]     if cached is not None and cached["revision"] == revision:         return dict(cached["health"])-    state = health(design, False)-    require(state["healthy"], "unhealthy_model", "Recompute or feature health check failed.", health=state)-    return state+    return health(design, False)  -def settle(design, document, args):+def settle(design, document, args, baseline, edits=()):     """After a write's edit and before it commits. Fusion's own compute has run; the full recompute runs-    first when a checkpoint is due. Refuses an unhealthy result, as require_healthy does."""+    first when a checkpoint is due. Refuses a result that judge refuses against the baseline; edits are+    the entities the write edits in place (subjects)."""     w = weight(args.get("op"), args)     focus = focus_of(args)     reason = due(document, w, focus)     started = time.perf_counter()-    state = health(design, reason is not None)+    natives = []+    state = health(design, reason is not None, natives)     info = {"full": reason is not None, "reason": reason, "seconds": round(time.perf_counter() - started, 2), "weight": w, "focus": focus}-    require(state["healthy"], "unhealthy_model", "Recompute or feature health check failed.", health=state, compute=info)+    judge(state, baseline, natives, edits, compute=info)     return state, info  +def midway(design):+    """Inside a write, before it settles (Fusion solves a joint as it is added): the model's health now,+    held to the write's baseline."""+    baseline = core.write_baseline()+    state = health(design, False)+    judge(state, baseline if baseline is not None else {})+    return state+++def restored(design, baseline):+    """After a failed write is undone, or the transaction probe: the full recompute, and no fault the+    model did not carry before."""+    state = health(design, True)+    judge(state, baseline)+    return state++ def committed(document, revision, state, info):     """Count a committed write, remember the health it verified, and report the ledger."""     led = ledger(document)
addin/AdomBridge/commands/mechanical/core.py+39−5
@@ -3,6 +3,7 @@ Session IDs are capabilities for this running add-in, not persistent CAD IDs. Tokens are only resolver inputs: they are never compared to decide entity identity. """+from contextlib import contextmanager import hashlib import json import math@@ -30,6 +31,25 @@ def require(condition, code, message, **details):         raise MechanicalError(code, message, **details)  +# The write in progress and the model's health before it, the baseline it is held to (compute.judge), for+# a check inside the operation. Fusion runs one write at a time on its main thread.+_write = None+++@contextmanager+def write_context(baseline):+    global _write+    previous, _write = _write, {"baseline": baseline}+    try:+        yield _write+    finally:+        _write = previous+++def write_baseline():+    return _write["baseline"] if _write is not None else None++ def items(collection):     require(collection.count <= MAX_ENTITIES, "inspection_budget", "Collection exceeds inspection budget.")     return [collection.item(i) for i in range(collection.count)]@@ -509,7 +529,9 @@ def resolve(design, target_value, value, allowed=()):     return entity  -def health(design, recompute=False):+def health(design, recompute=False, natives=None):+    """Every timeline item's health. natives, when given, collects (row, timeline item) for each fault and+    unreadable item, so a write can tell which of them it edited."""     computed = None     if recompute:         computed = bool(design.computeAll())@@ -529,8 +551,17 @@ def health(design, recompute=False):             if state != adsk.fusion.FeatureHealthStates.HealthyFeatureHealthState:                 faults.append({"index": index, "name": item.name, "state": int(state),                                "message": item.errorOrWarningMessage})+                if natives is not None:+                    natives.append((faults[-1], item))         except Exception as exc:-            unknown.append({"index": index, "error": str(exc)})+            row = {"index": index, "error": str(exc)}+            try:+                row["name"] = item.name+            except Exception:+                pass+            unknown.append(row)+            if natives is not None:+                natives.append((row, item))     return {"healthy": computed is not False and not faults and not unknown,             "computeSucceeded": computed, "complete": not unknown, "faults": faults,             "unknown": unknown, "suppressed": suppressed, "timelineCount": design.timeline.count}@@ -588,10 +619,13 @@ def parameter_data(design, parameter):     return row  -def recovery_snapshot(design, health_state=None):+def recovery_snapshot(design, health_state=None, strict_sketches=()):     """Semantic recovery witness; native revision IDs legitimately change on undo. health_state is the     health just read on this same unchanged model, when the caller has it (each walk reads every timeline-    item, about 23 ms apiece on a big assembly)."""+    item, about 23 ms apiece on a big assembly). strict_sketches are the sketches the write edits or reads:+    their constraint graph must read completely, or the write refuses before it changes anything. Every+    other sketch enters with what can be read, and a relationship the reader does not support enters as+    its type, so an unsupported relationship elsewhere in the design does not block the write."""     from .readback import construction_state, feature_data, sketch_data, topology_witness     from .assembly_state import component_state, occurrence_state     from .configurations import snapshot as configuration_snapshot@@ -600,7 +634,7 @@ def recovery_snapshot(design, health_state=None):     return {"configuration": configuration,             "parameters": [[p.name, p.expression, p.unit] for p in items(design.allParameters)],             "components": [[identity(c), c.name, [topology_witness(b) for b in items(c.bRepBodies)],-                            [sketch_data(s) for s in items(c.sketches)],+                            [sketch_data(s, strict=any(same(s, t) for t in strict_sketches)) for s in items(c.sketches)],                             [feature_data(f, order=i) for i, f in enumerate(items(c.features))], component_state(c), construction_state(c)]                            for c in components_by_occurrence(design, all_occurrences, design.allComponents.count)],             "occurrences": [[identity(o), list(o.transform2.asArray()), occurrence_state(o)] for o in all_occurrences],
addin/AdomBridge/commands/mechanical/operations.py+23−11
@@ -93,7 +93,8 @@ def inspect_entity(app, args):         result["edgeRefs"] = [ref(e, value) for e in items(entity.edges)]     elif entity.objectType.endswith("::Sketch"):         from .readback import sketch_data-        result.update(sketch_data(entity, value))+        # a read: a relationship the graph reader does not support is listed in constraintGraph+        result.update(sketch_data(entity, value, strict=False))     elif entity.objectType.endswith("::Occurrence"):         result["bodyRefs"] = [ref(b, value, entity) for b in items(entity.component.bRepBodies)]         result["childRefs"] = [ref(o, value) for o in items(entity.childOccurrences)]@@ -198,21 +199,27 @@ def parameter_set(app, args):     require(parameter is not None, "parameter_missing", "No parameter named " + name)     require(design.unitsManager.isValidExpression(expression, parameter.unit), "invalid_expression", "Expression is invalid for the parameter's unit.")     before, old = recovery_snapshot(design, healthy), parameter.expression+    # a model parameter is its feature's: a fault left on that feature is this write's own result+    owner = compute.owner_of(design, name)     try:         parameter.expression = expression-        state, settled = compute.settle(design, before_target["documentId"], args)+        state, settled = compute.settle(design, before_target["documentId"], args, healthy, owner)     except Exception as exc:         recovery = {"strategy": "restore_parameter_expression", "verified": False,                     "scope": "parameter expressions, body metrics, occurrence transforms, counts and health"}         try:             parameter.expression = old-            require_healthy(design)+            compute.restored(design, healthy)             recovery["verified"] = recovery_snapshot(design) == before         except Exception as restore_error:             recovery["error"] = str(restore_error)-        raise MechanicalError("parameter_edit_failed", str(exc), recovery=recovery, target=current_target(app))+        details = {"recovery": recovery, "target": current_target(app)}+        if isinstance(exc, MechanicalError) and exc.code == "unhealthy_model":+            details.update(causeCode=exc.code, causeDetails=exc.details)+        raise MechanicalError("parameter_edit_failed", str(exc), **details)     fresh = current_target(app)     return {"target": fresh, "parameter": parameter_data(design, parameter), "health": state,+            "preExistingFaults": compute.summary(healthy, state),             "compute": compute.committed(before_target["documentId"], fresh["revision"], state, settled),             "recoveryStrategy": "restore_parameter_expression", "transactional": False} @@ -230,20 +237,24 @@ def parameter_create(app, args):     try:         parameter = design.userParameters.add(name, adsk.core.ValueInput.createByString(expression), unit, args.get("comment", ""))         require(parameter is not None, "parameter_create_failed", "Fusion returned no parameter.")-        state, settled = compute.settle(design, value["documentId"], args)+        state, settled = compute.settle(design, value["documentId"], args, healthy)     except Exception as exc:         recovery = {"strategy": "delete_created_parameter", "verified": False}         try:             created = parameter or design.userParameters.itemByName(name)             if created:                 created.deleteMe()-            require_healthy(design)+            compute.restored(design, healthy)             recovery["verified"] = recovery_snapshot(design) == before         except Exception as restore_error:             recovery["error"] = str(restore_error)-        raise MechanicalError("parameter_create_failed", str(exc), recovery=recovery)+        details = {"recovery": recovery}+        if isinstance(exc, MechanicalError) and exc.code == "unhealthy_model":+            details.update(causeCode=exc.code, causeDetails=exc.details)+        raise MechanicalError("parameter_create_failed", str(exc), **details)     fresh = current_target(app)     return {"target": fresh, "parameter": parameter_data(design, parameter), "health": state,+            "preExistingFaults": compute.summary(healthy, state),             "compute": compute.committed(value["documentId"], fresh["revision"], state, settled), "transactional": False}  @@ -258,15 +269,16 @@ def check_health(app, args):  def export(app, args):     _, design, value = target(app, args, True, model_write=False)+    # faults the model already carries leave with it and are reported; the recompute may not add one+    baseline = compute.health_before(design, value["documentId"], value["revision"])     if compute.pending(value["documentId"]):         # the design leaves Fusion: the edits since the last full recompute get one now-        require(compute.checkpoint(design, value["documentId"], "export")["healthy"], "unhealthy_model",-                "Recompute or feature health check failed.", compute=compute.status(value["documentId"]))-    else:-        require_healthy(design, False)+        compute.judge(compute.checkpoint(design, value["documentId"], "export"), baseline,+                      compute=compute.status(value["documentId"]))     result = export_design(design, args)     result["target"] = current_target(app)     result["compute"] = compute.status(value["documentId"])+    result["preExistingFaults"] = compute.summary(baseline)     return result  
addin/AdomBridge/commands/mechanical/readback.py+41−6
@@ -2,7 +2,7 @@ import json import adsk.core import adsk.fusion-from .core import body_data, digest, identity, items, point_mm, ref, require, same+from .core import MechanicalError, body_data, digest, identity, items, point_mm, ref, require, same   def concrete_geometry(geometry):@@ -100,18 +100,41 @@ def plane_data(plane, value=None):     return result  -def sketch_data(sketch, value=None):+def sketch_data(sketch, value=None, strict=True):+    """A sketch's curves, points and constraint graph. strict (the default) refuses a relationship the+    graph reader cannot read completely (unsupported_constraint_graph); otherwise that relationship, or a+    curve whose geometry cannot be read, is reported as its type with the reason, and constraintGraph says+    the graph is incomplete. A fully readable sketch reads the same either way."""     curves, points = items(sketch.sketchCurves), items(sketch.sketchPoints)     constraints, dimensions = items(sketch.geometricConstraints), items(sketch.sketchDimensions)     graph = SketchGraph(curves, points, constraints)+    unreadable = []++    def read(reader, fallback):+        if strict:+            return reader()+        try:+            return reader()+        except (MechanicalError, AttributeError, RuntimeError) as error:+            if isinstance(error, MechanicalError) and error.code not in ("unsupported_constraint_graph", "unavailable_geometry"):+                raise+            row = fallback()+            row["unreadable"] = str(error)[:300]+            unreadable.append({"type": row.get("type"), "reason": row["unreadable"]})+            return row+     result = {"name": sketch.name, "space": "sketch", "lengthUnit": "mm",               "fullyConstrained": sketch.isFullyConstrained,-              "curves": [curve_data(curve, value) for curve in curves],+              "curves": [read(lambda: curve_data(curve, value), lambda: {"type": curve.objectType}) for curve in curves],               "points": [{"position": point_mm(p.geometry), "fixed": p.isFixed, **({"ref": ref(p, value)} if value else {})}                          for p in points],-              "curvePointLinks": [graph.curve_points(curve) for curve in curves],-              "constraints": [constraint_data(constraint, graph) for constraint in constraints],-              "dimensions": [dimension_data(dimension, graph) for dimension in dimensions]}+              "curvePointLinks": [read(lambda: graph.curve_points(curve), lambda: {"type": curve.objectType}) for curve in curves],+              "constraints": [read(lambda: constraint_data(constraint, graph), lambda: {"type": constraint.objectType})+                              for constraint in constraints],+              "dimensions": [read(lambda: dimension_data(dimension, graph), lambda: dimension_summary(dimension))+                             for dimension in dimensions]}+    if unreadable:+        result["constraintGraph"] = {"complete": False, "unreadable": unreadable}     if value:         # where the sketch sits: its x, y and normal in component space, so a sketch-space point         # (every curve, point and centroid above) maps to the model without guessing the plane@@ -222,6 +245,18 @@ def constraint_data(constraint, graph):     return result  +def dimension_summary(dimension):+    """What any sketch dimension says about itself, for one whose links cannot be read."""+    row = {"type": dimension.objectType}+    try:+        parameter = dimension.parameter+        row.update(name=parameter.name if parameter else None, expression=parameter.expression if parameter else None,+                   driving=dimension.isDriving)+    except (AttributeError, RuntimeError):+        pass+    return row++ def dimension_data(dimension, graph):     dimension, kind = graph_item(dimension, DIMENSION_LINKS)     parameter = dimension.parameter
addin/AdomBridge/commands/mechanical/regular_joints.py+5−3
@@ -5,7 +5,8 @@ import math import adsk.core import adsk.fusion -from .core import concrete, identity, items, ref, require, require_healthy, resolve, same+from . import compute+from .core import concrete, identity, items, ref, require, resolve, same from .geometry import native, owner, root_entity from .joint_state import (geometry_state, matrix_data, origin_data,                           regular_state)@@ -366,8 +367,9 @@ def regular_joint_create(design, value, args):     inp.isFlipped = args.get("flipped", False)     set_motion(inp, args)     joint = named(design.rootComponent.joints.add(inp), args)-    # Fusion solves the joint as it is added; the write's own settle decides on a full recompute-    require_healthy(design, False)+    # Fusion solves the joint as it is added; the write's own settle decides on a full recompute. Faults+    # the model already carried are allowed, as in settle: the joint may not add or change one.+    compute.midway(design)     alignment = initial_alignment(joint, expected["offset"][1], expected["angle"][1], args["type"])     apply_lock(joint, args)     configure_joint(design, joint, args)
addin/AdomBridge/commands/mechanical/transactions.py+56−12
@@ -11,8 +11,8 @@ import uuid import adsk.core  from . import compute-from .core import (MechanicalError, active, current_target, recovery_snapshot,-                   require, require_healthy, same, target)+from .core import (MechanicalError, active, current_target, health, recovery_snapshot,+                   require, resolve, same, target, write_context)  _verified = False _probe_evidence = None@@ -55,7 +55,9 @@ def probe(app, args): def _probe(app, args):     global _verified, _probe_evidence     doc, design, _ = target(app, args, True)-    before = recovery_snapshot(design, require_healthy(design, False))+    # faults the model already carries are its baseline: the probe must leave exactly those+    baseline = health(design, False)+    before = recovery_snapshot(design, baseline)     name = "AdomProbe" + uuid.uuid4().hex     started = False     try:@@ -74,12 +76,12 @@ def _probe(app, args):         parameter = design.userParameters.itemByName(name)         require(parameter is not None and abs(parameter.value - 0.2) < 1e-9, "transaction_probe_failed", "Commit did not preserve the probe parameter.")         require(bool(parameter.deleteMe()), "transaction_probe_failed", "Could not remove committed probe parameter.")-        require_healthy(design)+        compute.restored(design, baseline)         require(equivalent(before, recovery_snapshot(design)), "transaction_probe_failed", "Probe did not restore the semantic witness.")         _verified = True         _probe_evidence = {"abortRemovedParameter": True, "commitPreservedParameter": True,                            "semanticRestoration": True, "applicationVersion": str(getattr(app, "version", "unknown"))}-        return {"target": current_target(app), "transaction": capability()}+        return {"target": current_target(app), "transaction": capability(), "preExistingFaults": compute.summary(baseline)}     except Exception as exc:         _verified = False         recovery = {"verified": False, "strategy": "abort_and_remove_probe_parameter"}@@ -90,7 +92,7 @@ def _probe(app, args):             parameter = design.userParameters.itemByName(name)             if parameter:                 require(parameter.deleteMe(), "recovery_failed", "Probe parameter deletion failed.")-            require_healthy(design)+            compute.restored(design, baseline)             recovery["verified"] = equivalent(before, recovery_snapshot(design))         except Exception as restore_error:             recovery["error"] = str(restore_error)@@ -108,21 +110,63 @@ def _retarget(value, fresh):             _retarget(nested, fresh)  +def _native(entity):+    try:+        return getattr(entity, "nativeObject", None) or entity+    except Exception:+        return entity+++def sketches_named(design, value, args):+    """The sketches a write's references name: a sketch, or the sketch that owns a referenced sketch curve,+    point, constraint or dimension. The write edits or reads these, so their constraint graph must read+    completely before it starts. A profile's sketch is read only for the profile. A reference that does+    not resolve is left for the operation to refuse."""+    found = []+    pending = [item for key, item in args.items() if key != "target"]+    while pending:+        item = pending.pop()+        if isinstance(item, list):+            pending.extend(item)+            continue+        if not isinstance(item, dict):+            continue+        kind = item.get("objectType")+        if not (isinstance(kind, str) and "documentId" in item):+            pending.extend(item.values())+            continue+        name = kind.split("::")[-1]+        if name == "Profile" or not (name.startswith("Sketch") or name.endswith("Constraint")):+            continue+        try:+            entity = resolve(design, value, item)+        except (MechanicalError, RuntimeError, AttributeError):+            continue+        sketch = _native(entity if name == "Sketch" else getattr(entity, "parentSketch", None))+        if sketch is not None and not any(same(sketch, other) for other in found):+            found.append(sketch)+    return found++ def execute(app, args, operation):     require(_verified, "transaction_not_verified", "Run transaction_probe in this add-in session before typed feature mutations.")     doc, design, value = target(app, args, True)     require(design.timeline.markerPosition == design.timeline.count, "timeline_not_at_end", "Finish editing history before a typed model mutation.")-    before = recovery_snapshot(design, compute.health_before(design, value["documentId"], value["revision"]))+    # the faults the model already carries: the write may not add one or change one (compute.judge)+    baseline = compute.health_before(design, value["documentId"], value["revision"])+    before = recovery_snapshot(design, baseline, sketches_named(design, value, args))+    edits = compute.subjects(design, value, args)     started, committed = False, False     try:         command(app, 'PTransaction.Start "Adom mechanical operation"')         started = True-        serialize = operation(design, value, args)-        require(same(app.activeDocument, doc), "wrong_document", "Active document changed during the operation.")-        state, settled = compute.settle(design, value["documentId"], args)+        with write_context(baseline):+            serialize = operation(design, value, args)+            require(same(app.activeDocument, doc), "wrong_document", "Active document changed during the operation.")+            state, settled = compute.settle(design, value["documentId"], args, baseline, edits)         provisional = current_target(app)         result = serialize(provisional) if callable(serialize) else serialize-        result.update(target=provisional, health=state, transactional=True)+        result.update(target=provisional, health=state, transactional=True, preExistingFaults=compute.summary(baseline, state))         json.dumps(result, allow_nan=False)  # Serialization is validated before commit.         command(app, "PTransaction.Commit")         committed = True@@ -142,7 +186,7 @@ def execute(app, args, operation):             require(same(app.activeDocument, doc), "wrong_document", "Refusing to abort another document's transaction.")             if started:                 command(app, "PTransaction.Abort")-            require_healthy(design)+            compute.restored(design, baseline)             recovery["verified"] = equivalent(before, recovery_snapshot(design))         except Exception as restore_error:             recovery["error"] = str(restore_error)
docs/MECHANICAL.md+26−4
@@ -33,14 +33,20 @@ The mechanical layer is measured on a 4,579-occurrence, 1,249-component producti | `inspect` (full) | 13 s | | resolving one occurrence reference | 6 ms | | `bom` (part numbers cached) | 15 s |+| timeline health walk (689 items) | 14 s |+| recovery witness, bodies already read | 7 s |+| `parameter_create` (a model write without a full recompute) | 46 s |+| `Design.computeAll` | 66 s |  - On a design with more than 200 components, `inspect` leaves out each body's physical properties and bounds (`bodyMetrics:true` reads them; `bodyMetricsRead` says which you got). With more than 200 timeline items it leaves out the timeline health walk, which reads every item at about 23 ms apiece there (`health:true` reads it, or call `health`). - The first guarded call after the add-in loads registers every entity once: on that assembly, the first revision digest takes about 45 s instead of 5 s. - Writes keep their guards: a health check before, Fusion's own compute and a health check after, and the recovery witness. The full recompute runs at checkpoints ([Recompute](#recompute)). The witness reads again only the bodies whose revision changed since the last one.+- The witness reads every body the first time, 2,642 bodies in about 300 s on that assembly. A full recompute changes the revision of every body the design computes itself (1,591 there; bodies of referenced designs keep theirs), so the next witness reads those again (about 220 s). The write after a checkpoint, and `transaction_probe`, which recomputes in full and then takes the witness, pay that: the probe there would take about 5 minutes.+- That assembly carries 15 old timeline faults (warnings) and three sketches holding relationships the graph reader cannot read (rectangular pattern constraints, and midpoint constraints linked outside their sketch). Writes are held to the faults already there, and only the sketches a write names must read completely ([Designs that already carry faults](#designs-that-already-carry-faults)).  ## Recompute -Fusion recomputes what an edit touches as the edit is made. Autodesk keeps Compute All for checkpoints: major milestones, significant changes, edits upstream in the timeline that later features reference, and a check before the design leaves Fusion. Writes follow that lead. Each write reads the timeline's health after Fusion's own compute and refuses an unhealthy result (and rolls it back), as before. `Design.computeAll` runs at a checkpoint:+Fusion recomputes what an edit touches as the edit is made. Autodesk keeps Compute All for checkpoints: major milestones, significant changes, edits upstream in the timeline that later features reference, and a check before the design leaves Fusion. Writes follow that lead. Each write reads the timeline's health after Fusion's own compute and refuses (and rolls back) a result that adds a fault or changes one the model already had ([Designs that already carry faults](#designs-that-already-carry-faults)). `Design.computeAll` runs at a checkpoint:  - **Significance.** Writes since the last full recompute add up their weights (`compute.py`):   - 0: metadata, materials and new parameters;@@ -57,6 +63,20 @@ Fusion recomputes what an edit touches as the edit is made. Autodesk keeps Compu  Fusion computes on its main thread either way, so none of this runs in the background; the policy only chooses when. Failure recovery still recomputes in full. While the revision is unchanged, a write's pre-check reuses the health the previous write verified, so consecutive writes walk the timeline once each. Every write reply and `health` carry `compute`: `full`, `reason`, `seconds`, `significance`, `checkpointAt`, `writesSinceFullCompute`, `focus` and `lastFullCompute`. The counters are per document, for the add-in session. +## Designs that already carry faults++A production design can carry timeline faults that nobody is about to fix, such as a plane or sketch whose face was deleted long ago. A write is held to the faults the model carried before it (its baseline), not to a perfect model:++- The baseline is the health the previous write verified at the same revision, otherwise a walk of the timeline: the same walk as before, so it costs no extra walk. Comparing 15 faults takes under 15 µs.+- A fault is the same fault when its timeline item's name, health state and message are the same. Its index can move when an earlier item is deleted.+- The write succeeds when its result adds no fault, changes none, and leaves no fault on what it edits in place: the feature of `feature_edit`, the feature that owns a parameter it sets, the sketch of a sketch edit, or the occurrence, joint, rigid group, joint origin or motion link it edits. Inputs it only reads, such as a profile, an axis or joint geometry, do not count.+- Otherwise it refuses with `unhealthy_model` and rolls back as before. `newFaults` lists the faults it added or changed, and `editedFaults` the faults left on what it edited. Recovery is verified against the same baseline.+- The replies of model writes (the transactional operations, `parameter_set` and `parameter_create`), `transaction_probe` and `export` carry `preExistingFaults`: `count`, the first five `faults`, `more` when there are more, and `cleared` for the faults the write removed (deleted, suppressed or repaired). While old faults remain, `health.healthy` stays false on a write that succeeded.+- A full recompute at a checkpoint is held to the same baseline (`Design.computeAll` succeeds on a model that carries faults; a failed one still refuses). On the 4,579-occurrence assembly the first full recompute after opening showed the same 15 faults. Should one ever show a fault Fusion had not computed since the design opened, `health {recompute:true}` computes the design and the next write takes its baseline from that.+- `transaction_probe` and `export` follow the same rule. With a healthy baseline the rule is the old one: any fault refuses.++Sketches: the recovery witness records every sketch's constraint graph. The sketches a write names, or whose curve, point, constraint or dimension it names, must read completely, or the write refuses with `unsupported_constraint_graph` before it changes anything. A profile's sketch does not count: a feature reads only the profile. Every other sketch enters the witness with what can be read, and a relationship type the reader does not support (a polygon constraint, for example) enters as its type, so it no longer blocks writes elsewhere in the design. `inspect_entity` on such a sketch lists those relationships in `constraintGraph.unreadable` (`complete: false`).+ ## Plan exploded assembly steps  `exploded_view_plan` returns an immutable source-bound instance/home-pose plan,@@ -286,8 +306,10 @@ fully-constrained/fixed/linked/reference observations; it does not invent solver a conflicting-constraint explanation, and it does not recompute or alter the sketch.  Model transaction recovery records sketch constraint and dimension links into their native curve/point-inventory, including offset relationships and curve endpoint links. Unsupported relationship classes,-foreign links or unreadable graph properties fail closed before mutation. A reordered inventory can+inventory, including offset relationships and curve endpoint links. In a sketch the write names (or whose+curve, point, constraint or dimension it names), unsupported relationship classes, foreign links or+unreadable graph properties fail closed before mutation; other sketches enter the witness with what can be+read ([Designs that already carry faults](#designs-that-already-carry-faults)). A reordered inventory can conservatively leave recovery unverified; equal current coordinates alone never establish equal design intent. Readback does not compare entity-token strings to establish these relationships. @@ -402,7 +424,7 @@ Metadata always wins: `makeBuy`, `asPurchased`, `category`, `supplier`, `supplie  Native origin `transform` is component-local; an occurrence-context origin returns its root-space proxy transform. The nested `geometry` frame is separately labeled `native_joint_geometry_api`: Fusion can expose an origin's defining geometry through a placed-instance proxy even for a native origin. Definition edits verify the exact native source and owner; regular-joint attachments additionally require the exact selected occurrence. -A moving-to-rigid redefinition can make an existing Capture Position snapshot empty and unhealthy in Fusion. Such an edit refuses with `causeCode: unhealthy_model`; the response reports whether `PTransaction.Abort` verified restoration. Existing snapshot history is retained. Successful family changes and geometry reattachments require healthy resulting history; the bridge does not retarget or discard snapshots automatically.+A moving-to-rigid redefinition can make an existing Capture Position snapshot empty and unhealthy in Fusion. Such an edit refuses with `causeCode: unhealthy_model`; the response reports whether `PTransaction.Abort` verified restoration. Existing snapshot history is retained. Successful family changes and geometry reattachments require resulting history with no new or changed fault; the bridge does not retarget or discard snapshots automatically.  `motion_link_create/edit/delete` use the native November 2025+ API, with explicit joint degrees of freedom and positive mm/deg ratio magnitudes. `reversed` controls opposite motion. Missing native support fails explicitly. Motion link membership changes require recreating the link. 
tests/test_mechanical_baseline.pyadded+337
@@ -0,0 +1,337 @@+"""Writes on a design that already carries faults, offline: the faults present before a write are its+baseline; a write may not add one, change one or leave one on what it edited. A sketch relationship the+graph reader cannot read refuses only a write that edits or reads that sketch."""+import importlib+from types import SimpleNamespace as NS+import unittest+from unittest.mock import patch++from test_mechanical import Collection, Entity, adsk, c, ops++compute = importlib.import_module(c.__package__ + '.compute')+tx = importlib.import_module(c.__package__ + '.transactions')+rb = importlib.import_module(c.__package__ + '.readback')+++def fault(name, message='lost reference', state=2, index='4'):+    return {'index': index, 'name': name, 'state': state, 'message': message}+++def health(*faults, unknown=(), computed=None):+    return {'healthy': computed is not False and not faults and not unknown, 'computeSucceeded': computed,+            'complete': not unknown, 'faults': list(faults), 'unknown': list(unknown), 'suppressed': [], 'timelineCount': 9}+++OLD = [fault('Fillet7', 'edge lost', index='2'), fault('Plane3', 'face lost', index='5')]+++class Judge(unittest.TestCase):+    def test_a_write_that_adds_nothing_is_allowed_on_a_faulted_model(self):+        compute.judge(health(*OLD), health(*OLD))++    def test_a_new_fault_refuses_and_is_named(self):+        with self.assertRaises(c.MechanicalError) as error:+            compute.judge(health(*OLD, fault('Extrude9', 'no profile', index='8')), health(*OLD))+        self.assertEqual(error.exception.code, 'unhealthy_model')+        self.assertEqual([f['name'] for f in error.exception.details['newFaults']], ['Extrude9'])+        self.assertEqual(error.exception.details['preExistingFaults']['count'], 2)++    def test_a_changed_fault_refuses(self):+        worse = [OLD[0], fault('Plane3', 'face lost', state=1, index='5')]+        with self.assertRaises(c.MechanicalError) as error:+            compute.judge(health(*worse), health(*OLD))+        self.assertEqual(error.exception.details['newFaults'], [worse[1]])+        other = [OLD[0], fault('Plane3', 'another message', index='5')]+        with self.assertRaises(c.MechanicalError):+            compute.judge(health(*other), health(*OLD))++    def test_an_earlier_delete_moves_indexes_without_reading_as_a_change(self):+        moved = [dict(row, index=str(int(row['index']) - 1)) for row in OLD]+        compute.judge(health(*moved), health(*OLD))++    def test_lookalike_faults_count(self):+        two = [fault('Boss1', index='3'), fault('Boss1', index='6')]+        compute.judge(health(*two), health(*two))+        with self.assertRaises(c.MechanicalError) as error:+            compute.judge(health(*two, fault('Boss1', index='9')), health(*two))+        self.assertEqual([f['index'] for f in error.exception.details['newFaults']], ['9'])++    def test_a_cleared_fault_is_allowed_and_reported(self):+        compute.judge(health(OLD[1]), health(*OLD))+        self.assertEqual(compute.summary(health(*OLD), health(OLD[1]))['cleared'], {'count': 1, 'faults': [OLD[0]]})++    def test_unreadable_items_are_faults_too(self):+        lost = {'index': '7', 'name': 'Pattern2', 'error': 'InternalValidationError'}+        compute.judge(health(unknown=[lost]), health(unknown=[lost]))+        with self.assertRaises(c.MechanicalError):+            compute.judge(health(unknown=[lost]), health())++    def test_a_healthy_baseline_keeps_the_old_rule(self):+        with self.assertRaises(c.MechanicalError):+            compute.judge(health(fault('Fillet1')), health())+        with self.assertRaises(c.MechanicalError) as error:+            compute.judge(health(computed=False), health())+        self.assertEqual(str(error.exception), 'Recompute failed.')+        # a failed full recompute refuses on a faulted model too: Fusion's computeAll succeeds there+        with self.assertRaises(c.MechanicalError):+            compute.judge(health(*OLD, computed=False), health(*OLD))++    def test_a_fault_left_on_what_the_write_edits_is_its_own(self):+        feature = Entity(objectType='adsk::fusion::FilletFeature', name='Fillet7')+        natives = [(OLD[0], NS(entity=feature)), (OLD[1], NS(entity=Entity(objectType='adsk::fusion::ConstructionPlane')))]+        compute.judge(health(*OLD), health(*OLD), natives, [Entity(objectType='adsk::fusion::BRepFace')])+        with self.assertRaises(c.MechanicalError) as error:+            compute.judge(health(*OLD), health(*OLD), natives, [feature])+        self.assertEqual(error.exception.details['editedFaults'], [OLD[0]])+        self.assertEqual(str(error.exception), 'The write left a fault on an entity it edited.')++    def test_the_subjects_are_what_a_write_edits_in_place_not_its_inputs(self):+        feature = Entity(objectType='adsk::fusion::FilletFeature', name='Fillet7', entityToken='tok-fillet')+        axis = Entity(objectType='adsk::fusion::ConstructionAxis', name='Axis1', entityToken='tok-axis')+        tokens = {'tok-fillet': feature, 'tok-axis': axis}+        owned = Entity(name='d5', createdBy=feature)+        design = NS(findEntityByToken=lambda token: [tokens[token]] if token in tokens else [],+                    allParameters=Collection([owned, Entity(name='width')]))+        value = {'documentId': 'd', 'revision': 'r'}+        ref = lambda token, kind: dict(value, token=token, objectType='adsk::fusion::' + kind)+        self.assertEqual(compute.subjects(design, value, {'op': 'feature_edit', 'feature': ref('tok-fillet', 'FilletFeature')}), [feature])+        self.assertEqual(compute.subjects(design, value, {'op': 'feature_create', 'axis': ref('tok-axis', 'ConstructionAxis')}), [])+        self.assertEqual(compute.subjects(design, value, {'op': 'feature_edit', 'feature': ref('tok-gone', 'FilletFeature')}), [])+        batch = {'op': 'parameter_batch', 'changes': [{'action': 'set', 'name': 'd5'}, {'action': 'set', 'name': 'width'},+                                                     {'action': 'create', 'name': 'x'}]}+        self.assertEqual(compute.subjects(design, value, batch), [feature])++    def test_a_reply_lists_the_first_few_and_counts_the_rest(self):+        many = [fault('F%d' % i, index=str(i)) for i in range(15)]+        out = compute.summary(health(*many), health(*many))+        self.assertEqual((out['count'], len(out['faults']), out['more']), (15, 5, 10))+        self.assertNotIn('cleared', out)+        self.assertEqual(compute.summary(health()), {'count': 0, 'faults': []})+++class Walk(unittest.TestCase):+    def test_the_walk_hands_back_the_items_behind_its_faults(self):+        class Broken:+            name, isGroup, isSuppressed = 'Pattern2', False, False++            @property+            def healthState(self):+                raise RuntimeError('InternalValidationError')+        bad = NS(name='Fillet7', isGroup=False, isSuppressed=False, healthState=2, errorOrWarningMessage='edge lost')+        good = NS(name='Boss1', isGroup=False, isSuppressed=False, healthState=0)+        design = NS(timeline=Collection([good, bad, Broken()]))+        natives = []+        state = c.health(design, False, natives)+        self.assertEqual(state['faults'], [{'index': '1', 'name': 'Fillet7', 'state': 2, 'message': 'edge lost'}])+        self.assertEqual(state['unknown'], [{'index': '2', 'error': 'InternalValidationError', 'name': 'Pattern2'}])+        self.assertEqual([item for _, item in natives][:1], [bad])+        self.assertEqual(len(natives), 2)++    def test_a_check_inside_a_write_is_held_to_its_baseline(self):+        with patch.object(compute, 'health', lambda design, recompute=False, natives=None: health(*OLD)):+            with c.write_context(health(*OLD)):+                compute.midway(None)+            with c.write_context(health(OLD[0])), self.assertRaises(c.MechanicalError):+                compute.midway(None)+            with self.assertRaises(c.MechanicalError):+                compute.midway(None)  # outside a write the baseline is a healthy model+++class Transactions(unittest.TestCase):+    """The adapter on a faulted model: the faults are the baseline, a breaking write is undone and the+    undo is verified against the same baseline."""+    def setUp(self):+        compute.reset()+        self.state = [health(*OLD)]+        self.reads = []+        self.commands = []+        self.patches = [patch.object(compute, 'health', self.read),+                        patch.object(tx, 'health', self.read),+                        patch.object(tx, '_verified', True),+                        patch.object(tx, 'target', lambda app, args, write=False: ('doc', self.design, {'documentId': 'd', 'revision': 'r0'})),+                        patch.object(tx, 'current_target', lambda app: {'documentId': 'd', 'revision': 'r1'}),+                        patch.object(tx, 'recovery_snapshot', lambda design, health_state=None, strict_sketches=(): {'faults': len(self.state[0]['faults'])}),+                        patch.object(tx, 'same', lambda a, b: True),+                        patch.object(tx, 'command', self.command)]+        self.design = NS(timeline=NS(markerPosition=3, count=3), findEntityByToken=lambda token: [])+        for p in self.patches:+            p.start()++    def tearDown(self):+        for p in self.patches:+            p.stop()++    def read(self, design, recompute=False, natives=None):+        self.reads.append(recompute)+        return dict(self.state[0], computeSucceeded=True if recompute else None)++    def command(self, app, text):+        self.commands.append(text.split()[0])+        if text.startswith('PTransaction.Abort'):+            self.state[0] = health(*OLD)  # the undo puts the model back++    def test_a_write_elsewhere_commits_and_reports_the_faults_already_there(self):+        out = tx.execute(NS(activeDocument='doc'), {'op': 'feature_create', 'target': {}}, lambda design, value, args: {'made': 1})+        self.assertEqual(self.commands, ['PTransaction.Start', 'PTransaction.Commit'])+        self.assertEqual((out['preExistingFaults']['count'], out['health']['healthy']), (2, False))+        # the next write's baseline is what this one verified, faults included, without another walk+        self.assertEqual(compute.health_before(None, 'd', 'r1')['faults'], OLD)++    def test_a_write_that_breaks_something_is_undone_and_the_undo_verified(self):+        def breaks(design, value, args):+            self.state[0] = health(*OLD, fault('Extrude9', 'no profile', index='8'))+            return {'made': 1}+        with self.assertRaises(c.MechanicalError) as error:+            tx.execute(NS(activeDocument='doc'), {'op': 'feature_create', 'target': {}}, breaks)+        details = error.exception.details+        self.assertEqual((error.exception.code, details['causeCode']), ('mutation_failed', 'unhealthy_model'))+        self.assertEqual([f['name'] for f in details['causeDetails']['newFaults']], ['Extrude9'])+        self.assertEqual(self.commands, ['PTransaction.Start', 'PTransaction.Abort'])+        self.assertTrue(details['recovery']['verified'], details['recovery'])++    def test_the_probe_passes_on_a_faulted_model(self):+        params = {}+        design = NS(userParameters=NS(add=lambda name, value, unit, comment: params.setdefault(name, NS(value=0.2, deleteMe=lambda: params.pop(name) is not None)),+                                      itemByName=lambda name: params.get(name)))++        def command(app, text):+            self.commands.append(text.split()[0])+            if text.startswith('PTransaction.Abort'):+                params.clear()+        with patch.object(tx, 'target', lambda app, args, write=False: ('doc', design, {'documentId': 'd', 'revision': 'r0'})), \+                patch.object(tx, 'command', command), patch.object(tx, '_verified', False), \+                patch.object(adsk.core, 'ValueInput', NS(createByString=lambda text: text), create=True):+            out = tx._probe(NS(version='test'), {'target': {}})+            self.assertTrue(out['transaction']['verified'])+            self.assertEqual(out['preExistingFaults']['count'], 2)+++class Sketch(unittest.TestCase):+    """A sketch whose graph holds a relationship the reader does not support."""+    def sketch(self):+        point = lambda x, y: Entity(objectType='adsk::fusion::SketchPoint', geometry=NS(x=x, y=y, z=0), isFixed=False)+        a, b = point(0, 0), point(1, 0)+        line = Entity(objectType='adsk::fusion::SketchLine', isConstruction=False, isFixed=False,+                      geometry=NS(objectType='adsk::core::Line3D', startPoint=a.geometry, endPoint=b.geometry),+                      startSketchPoint=a, endSketchPoint=b)+        horizontal = Entity(objectType='adsk::fusion::HorizontalConstraint', line=line)+        polygon = Entity(objectType='adsk::fusion::PolygonConstraint')+        dimension = Entity(objectType='adsk::fusion::SketchLinearDimension', entityOne=a, entityTwo=b, isDriving=True,+                           value=1.0, orientation=0, parameter=NS(name='d1', expression='10 mm'))+        sketch = Entity(objectType='adsk::fusion::Sketch', name='Hex', isFullyConstrained=False,+                        sketchCurves=Collection([line]), sketchPoints=Collection([a, b]),+                        geometricConstraints=Collection([horizontal]), sketchDimensions=Collection([dimension]))+        return sketch, polygon++    def test_a_readable_sketch_reads_the_same_either_way(self):+        sketch, _ = self.sketch()+        self.assertEqual(rb.sketch_data(sketch), rb.sketch_data(sketch, strict=False))+        self.assertNotIn('constraintGraph', rb.sketch_data(sketch, strict=False))++    def test_strict_refuses_and_lenient_lists_the_relationship(self):+        sketch, polygon = self.sketch()+        sketch.geometricConstraints.values.append(polygon)+        with self.assertRaises(c.MechanicalError) as error:+            rb.sketch_data(sketch)+        self.assertEqual(error.exception.code, 'unsupported_constraint_graph')+        data = rb.sketch_data(sketch, strict=False)+        self.assertEqual(data['constraints'][1]['type'], 'adsk::fusion::PolygonConstraint')+        self.assertEqual(data['constraintGraph']['complete'], False)+        self.assertEqual([row['type'] for row in data['constraintGraph']['unreadable']], ['adsk::fusion::PolygonConstraint'])+        # the readable part of the graph is still there+        self.assertEqual(data['constraints'][0]['links'], {'line': {'collection': 'curves', 'index': 0}})+        self.assertEqual(data['dimensions'][0]['name'], 'd1')++    def test_only_a_sketch_the_write_names_must_read_completely(self):+        sketch, polygon = self.sketch()+        sketch.geometricConstraints.values.append(polygon)+        other, _ = self.sketch()+        component = Entity(objectType='adsk::fusion::Component', name='root', sketches=Collection([sketch, other]),+                           bRepBodies=Collection(), features=Collection())+        design = NS(rootComponent=NS(allOccurrences=Collection()), allComponents=Collection([component]),+                    allParameters=Collection(), timeline=NS(count=0, markerPosition=0))+        with patch.object(c, 'components_by_occurrence', lambda design, occurrences, size: [component]), \+                patch('mechanical_test_commands.mechanical.configurations.snapshot', lambda design, strict=False: {}), \+                patch('mechanical_test_commands.mechanical.assembly_state.component_state', lambda comp: {}):+            snapshot = c.recovery_snapshot(design, health())+            self.assertEqual(snapshot['components'][0][3][0]['constraintGraph']['complete'], False)+            c.recovery_snapshot(design, health(), [other])+            with self.assertRaises(c.MechanicalError) as error:+                c.recovery_snapshot(design, health(), [sketch])+            self.assertEqual(error.exception.code, 'unsupported_constraint_graph')++    def test_reading_a_sketch_lists_what_it_cannot_read_instead_of_refusing(self):+        sketch = Entity(objectType='adsk::fusion::Sketch', name='Hex')+        with patch.object(ops, 'target', return_value=(None, None, {'documentId': 'd', 'revision': 'r'})), \+                patch.object(ops, 'resolve', return_value=sketch), \+                patch.object(ops, 'ref', return_value={'token': 'tok-hex'}), \+                patch.object(rb, 'sketch_data', lambda entity, value=None, strict=True: {'strict': strict}):+            out = ops.inspect_entity(None, {'entity': {}})+        self.assertEqual(out['entity']['strict'], False)++    def test_the_sketches_a_write_names(self):+        sketch, _ = self.sketch()+        line = sketch.sketchCurves.item(0)+        line.parentSketch = sketch+        found = {'tok-sketch': sketch, 'tok-line': line}+        design = NS(findEntityByToken=lambda token: [found[token]] if token in found else [])+        value = {'documentId': 'd', 'revision': 'r'}+        ref = lambda token, kind: dict(value, token=token, objectType='adsk::fusion::' + kind)+        self.assertEqual(tx.sketches_named(design, value, {'sketch': ref('tok-sketch', 'Sketch')}), [sketch])+        self.assertEqual(tx.sketches_named(design, value, {'entities': [ref('tok-line', 'SketchLine')]}), [sketch])+        # a profile's sketch is read only for the profile; other references and the target are skipped+        profile = dict(value, objectType='adsk::fusion::Profile', sketchToken='tok-sketch', profileIndex=0)+        face = ref('tok-face', 'BRepFace')+        self.assertEqual(tx.sketches_named(design, value, {'target': ref('tok-sketch', 'Sketch'), 'profiles': [profile], 'faces': [face]}), [])+        # a reference that does not resolve is left for the operation+        self.assertEqual(tx.sketches_named(design, value, {'sketch': ref('tok-gone', 'Sketch')}), [])+++class Parameters(unittest.TestCase):+    def setUp(self):+        compute.reset()+        self.fillet = Entity(objectType='adsk::fusion::FilletFeature', name='Fillet7')++    def run_set(self, owner, faults_after):+        p = Entity(name='d3', expression='10 mm', unit='mm', createdBy=owner)+        design = NS(allParameters=Collection([p]), unitsManager=NS(isValidExpression=lambda *a: True))+        reads = []++        def read(design, recompute=False, natives=None):+            reads.append(recompute)+            rows = faults_after if p.expression == '12 mm' else OLD+            if natives is not None:+                natives.extend((row, NS(entity=self.fillet if row['name'] == 'Fillet7' else None)) for row in rows)+            return health(*rows, computed=True if recompute else None)+        with patch.object(ops, 'target', return_value=(None, design, {'documentId': 'd', 'revision': 'r'})), \+                patch.object(compute, 'health', read), \+                patch.object(ops, 'recovery_snapshot', side_effect=lambda d, health_state=None: {'expression': p.expression}), \+                patch.object(ops, 'current_target', return_value={'documentId': 'd', 'revision': 'r2'}), \+                patch.object(ops, 'parameter_data', lambda design, parameter: {'name': parameter.name}):+            return ops.parameter_set(None, {'op': 'parameter_set', 'name': 'd3', 'expression': '12 mm'}), p++    def test_a_parameter_change_that_adds_nothing_succeeds(self):+        out, p = self.run_set(Entity(objectType='adsk::fusion::ExtrudeFeature', name='Extrude2'), OLD)+        self.assertEqual((p.expression, out['preExistingFaults']['count']), ('12 mm', 2))++    def test_a_parameter_of_a_faulted_feature_that_stays_faulted_is_refused(self):+        with self.assertRaises(c.MechanicalError) as error:+            self.run_set(self.fillet, OLD)+        self.assertEqual(error.exception.code, 'parameter_edit_failed')+        self.assertEqual(error.exception.details['causeDetails']['editedFaults'], [OLD[0]])+        self.assertTrue(error.exception.details['recovery']['verified'])+++class Export(unittest.TestCase):+    def test_a_faulted_design_exports_and_reports_its_faults(self):+        compute.reset()+        with patch.object(ops, 'target', lambda app, args, write=False, model_write=True: ('doc', None, {'documentId': 'd', 'revision': 'r'})), \+                patch.object(ops, 'current_target', lambda app: {'documentId': 'd', 'revision': 'r'}), \+                patch.object(compute, 'health', lambda design, recompute=False, natives=None: health(*OLD)), \+                patch.object(ops, 'export_design', lambda design, args: {'path': 'out.step'}):+            out = ops.export(None, {'target': {}})+        self.assertEqual((out['path'], out['preExistingFaults']['count']), ('out.step', 2))+++if __name__ == '__main__':+    unittest.main()
tests/test_mechanical_compute.py+21−12
@@ -23,9 +23,13 @@ class Health:     def __init__(self, healthy=True):         self.calls, self.healthy = [], healthy -    def __call__(self, design, recompute=False):+    def __call__(self, design, recompute=False, natives=None):         self.calls.append(recompute)-        return {"healthy": self.healthy, "computeSucceeded": True if recompute else None, "faults": [], "unknown": []}+        faults = [] if self.healthy else [{"index": "3", "name": "Fillet1", "state": 2, "message": "lost edge"}]+        return {"healthy": self.healthy, "computeSucceeded": True if recompute else None, "faults": faults, "unknown": []}+++HEALTHY = {"healthy": True, "faults": [], "unknown": []}   class Weights(unittest.TestCase):@@ -65,7 +69,7 @@ class Policy(unittest.TestCase):         self.patch.stop()      def write(self, op, revision, **args):-        state, info = compute.settle(None, "d", {"op": op, **args})+        state, info = compute.settle(None, "d", {"op": op, **args}, HEALTHY)         return compute.committed("d", revision, state, info)      def test_appends_stay_incremental_until_they_add_up_to_a_checkpoint(self):@@ -93,9 +97,10 @@ class Policy(unittest.TestCase):     def test_an_unhealthy_result_is_refused_with_its_health_and_counts_nothing(self):         self.health.healthy = False         with self.assertRaises(c.MechanicalError) as error:-            compute.settle(None, "d", {"op": "feature_edit", "feature": ref("Lid:1")})+            compute.settle(None, "d", {"op": "feature_edit", "feature": ref("Lid:1")}, HEALTHY)         self.assertEqual(error.exception.code, "unhealthy_model")         self.assertEqual(error.exception.details["compute"]["full"], False)+        self.assertEqual([f["name"] for f in error.exception.details["newFaults"]], ["Fillet1"])         self.assertEqual(compute.status("d")["writesSinceFullCompute"], 0)      def test_health_a_write_verified_is_reused_while_nothing_changed(self):@@ -105,11 +110,13 @@ class Policy(unittest.TestCase):         compute.health_before(None, "d", "someone-else-edited")         self.assertEqual(self.health.calls, [False, False]) -    def test_an_unhealthy_model_is_refused_before_a_write(self):+    def test_faults_a_model_already_carries_are_the_baseline_not_a_refusal(self):         self.health.healthy = False-        with self.assertRaises(c.MechanicalError) as error:-            compute.health_before(None, "d", "r9")-        self.assertEqual(error.exception.code, "unhealthy_model")+        state = compute.health_before(None, "d", "r9")+        self.assertEqual([f["name"] for f in state["faults"]], ["Fillet1"])+        # the same faults after the write: nothing new, so the write settles+        settled, _ = compute.settle(None, "d", {"op": "feature_create"}, state)+        self.assertFalse(settled["healthy"])      def test_an_explicit_checkpoint_starts_the_counters_again(self):         self.write("entity_delete", "r1")@@ -130,7 +137,7 @@ class Writes(unittest.TestCase):                         patch.object(tx, "_verified", True),                         patch.object(tx, "target", lambda app, args, write=False: ("doc", self.design, {"documentId": "d", "revision": self.revision[0]})),                         patch.object(tx, "current_target", lambda app: {"documentId": "d", "revision": self.revision[0]}),-                        patch.object(tx, "recovery_snapshot", lambda design, health_state=None: {}),+                        patch.object(tx, "recovery_snapshot", lambda design, health_state=None, strict_sketches=(): {}),                         patch.object(tx, "same", lambda a, b: True),                         patch.object(tx, "command", lambda app, text: self.commands.append(text.split()[0]))]         self.design = NS(timeline=NS(markerPosition=3, count=3))@@ -163,10 +170,12 @@ class Writes(unittest.TestCase):                 patch.object(ops, "current_target", lambda app: {"documentId": "d", "revision": "r1"}), \                 patch.object(ops, "export_design", lambda design, args: {"path": "out.step"}):             out = ops.export(None, {"target": {}})+            # the edit's verified health is the baseline; the export's full recompute is held to it             self.assertEqual((self.health.calls, out["compute"]["lastFullCompute"]["reason"]), ([True], "export"))-            with patch.object(ops, "require_healthy", lambda design, recompute=True: {"healthy": True}):-                ops.export(None, {"target": {}})-            self.assertEqual(self.health.calls, [True])+            self.assertEqual(out["preExistingFaults"], {"count": 0, "faults": []})+            ops.export(None, {"target": {}})+            # nothing changed since: no recompute, one walk for the baseline+            self.assertEqual(self.health.calls, [True, False])   if __name__ == "__main__":