← Commit history
addin/AdomBridge/commands/mechanical/compute.pyadded+144
@@ -0,0 +1,144 @@+"""When a write runs Fusion's full recompute (Design.computeAll), following Fusion's own lead.++Fusion recomputes what an edit touches as the edit is made ("Typically Fusion computes everything+automatically as you edit a parametric design", Autodesk's Compute All help) and keeps Compute All for+checkpoints: major milestones, significant changes, edits upstream in the timeline that later features+reference (deletes above all), and a check before the design leaves. Fusion computes on its main thread+in both cases, so nothing runs in the background; the only choice is when. So every write reads the+timeline's health after Fusion's own compute, and the full recompute runs at a checkpoint:++- the writes since the last full recompute add up to CHECKPOINT (WEIGHTS: deletes and assembly+  relationships count most, edits of existing history next, appended features least, metadata nothing);+- the writes move on to another top-level occurrence, so the previous part's edits are done;+- 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."""+import time++from .core import health, require++CHECKPOINT = 6.0+# a move to another part checkpoints edits worth one: an edit of existing history, a joint, a delete, two appends+FOCUS_MIN = 2.0+WEIGHTS = {+    # appended at the end of the timeline: nothing later depends on it yet+    "sketch_create": 0.5, "sketch_geometry": 0.5, "sketch_constraint": 0.5, "sketch_dimension": 0.5,+    "sketch_project": 0.5, "sketch_offset": 0.5, "plane_create": 0.5, "construction_axis": 0.5,+    "joint_origin_create": 0.5, "feature_create": 1.0, "component_create": 1.0, "occurrence_insert": 1.0,+    "cad_import": 1.0, "occurrence_update": 1.0,+    # edits of existing history, which later features may reference+    "parameter_set": 1.5, "feature_edit": 2.0, "sketch_edit": 2.0, "sketch_curve_edit": 2.0, "joint_origin_edit": 2.0,+    # assembly relationships+    "joint_create": 2.0, "joint_edit": 2.0, "regular_joint_create": 2.0, "regular_joint_edit": 2.0,+    "rigid_group_create": 2.0, "rigid_group_edit": 2.0, "motion_link_create": 2.0, "motion_link_edit": 2.0,+    # deletes of something later features or joints may reference+    "entity_delete": 3.0, "occurrence_delete": 3.0, "joint_delete": 3.0, "joint_origin_delete": 3.0,+    "rigid_group_delete": 3.0, "motion_link_delete": 3.0,+    # no geometry+    "parameter_create": 0.0, "component_metadata": 0.0, "material_assign": 0.0,+}+BATCH = {"create": 0.0, "set": WEIGHTS["parameter_set"], "delete": 3.0}++_ledgers = {}+++def ledger(document):+    return _ledgers.setdefault(document, {"significance": 0.0, "writes": 0, "focus": None, "last": None, "verified": None})+++def reset():+    _ledgers.clear()+++def weight(op, args):+    if op == "parameter_batch":+        return min(CHECKPOINT, sum(BATCH.get(change.get("action"), 1.0) for change in args.get("changes") or []))+    return WEIGHTS.get(op, 1.0)+++def focus_of(args):+    """The top-level occurrence a write works in: the first segment of the first occurrence path among+    its references, in argument order. None for root-level work, which never counts as a move to+    another part."""+    pending = [value for key, value in reversed(list(args.items())) if key != "target"]+    while pending:+        item = pending.pop()+        if isinstance(item, dict):+            path = item.get("occurrencePath")+            if isinstance(path, str) and path:+                return path.split("+")[0]+            pending.extend(reversed(list(item.values())))+        elif isinstance(item, list):+            pending.extend(reversed(item))+    return None+++def due(document, w, focus):+    """Why this write runs the full recompute, or None."""+    led = ledger(document)+    if led["significance"] + w >= CHECKPOINT:+        return "significance"+    if focus is not None and led["focus"] is not None and focus != led["focus"] and led["significance"] >= FOCUS_MIN:+        return "focus_change"+    return None+++def status(document):+    led = ledger(document)+    return {"significance": round(led["significance"], 2), "checkpointAt": CHECKPOINT,+            "writesSinceFullCompute": led["writes"], "focus": led["focus"], "lastFullCompute": led["last"]}+++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."""+    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+++def settle(design, document, args):+    """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."""+    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)+    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)+    return state, info+++def committed(document, revision, state, info):+    """Count a committed write, remember the health it verified, and report the ledger."""+    led = ledger(document)+    if info["full"]:+        led.update(significance=0.0, writes=0, last={"reason": info["reason"], "seconds": info["seconds"], "at": round(time.time())})+    else:+        led["significance"] += info["weight"]+        led["writes"] += 1+    if info["focus"] is not None:+        led["focus"] = info["focus"]+    # stored as a plain walk reports it, so a later witness compares equal+    led["verified"] = {"revision": revision, "health": dict(state, computeSucceeded=None)}+    return dict(status(document), full=info["full"], reason=info["reason"], seconds=info["seconds"])+++def checkpoint(design, document, reason):+    """Run the full recompute now (an export, or a caller asking) and start the counters again."""+    started = time.perf_counter()+    state = health(design, True)+    led = ledger(document)+    led.update(significance=0.0, writes=0, verified=None,+               last={"reason": reason, "seconds": round(time.perf_counter() - started, 2), "at": round(time.time())})+    return state+++def pending(document):+    led = ledger(document)+    return led["writes"] > 0 or led["significance"] > 0
addin/AdomBridge/commands/mechanical/operations.py+27−13
@@ -7,6 +7,7 @@ import adsk.fusion from .core import (MechanicalError, components_by_occurrence, active, body_data, current_target, health,                    identity, items, number, parameter_data, recovery_snapshot,                    ref, require, require_healthy, resolve, target, UNITS)+from . import compute from .exporting import export_design  @@ -190,7 +191,7 @@ def create_box(design, dimensions):  def parameter_set(app, args):     _, design, before_target = target(app, args, True)-    healthy = require_healthy(design, False)+    healthy = compute.health_before(design, before_target["documentId"], before_target["revision"])     name, expression = args.get("name"), args.get("expression")     require(isinstance(name, str) and isinstance(expression, str) and expression.strip(), "invalid_input", "name and expression are required strings.")     parameter = design.allParameters.itemByName(name)@@ -199,7 +200,7 @@ def parameter_set(app, args):     before, old = recovery_snapshot(design, healthy), parameter.expression     try:         parameter.expression = expression-        state = require_healthy(design)+        state, settled = compute.settle(design, before_target["documentId"], args)     except Exception as exc:         recovery = {"strategy": "restore_parameter_expression", "verified": False,                     "scope": "parameter expressions, body metrics, occurrence transforms, counts and health"}@@ -210,13 +211,15 @@ def parameter_set(app, args):         except Exception as restore_error:             recovery["error"] = str(restore_error)         raise MechanicalError("parameter_edit_failed", str(exc), recovery=recovery, target=current_target(app))-    return {"target": current_target(app), "parameter": parameter_data(design, parameter),-            "health": state, "recoveryStrategy": "restore_parameter_expression", "transactional": False}+    fresh = current_target(app)+    return {"target": fresh, "parameter": parameter_data(design, parameter), "health": state,+            "compute": compute.committed(before_target["documentId"], fresh["revision"], state, settled),+            "recoveryStrategy": "restore_parameter_expression", "transactional": False}   def parameter_create(app, args):-    _, design, _ = target(app, args, True)-    healthy = require_healthy(design, False)+    _, design, value = target(app, args, True)+    healthy = compute.health_before(design, value["documentId"], value["revision"])     name, expression, unit = args.get("name"), args.get("expression"), args.get("unit")     require(isinstance(name, str) and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", name), "invalid_input", "Use a short parameter identifier.")     require(unit in ("mm", "deg", "kg", ""), "invalid_input", "unit must be mm, deg, kg or empty for unitless.")@@ -227,7 +230,7 @@ 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 = require_healthy(design)+        state, settled = compute.settle(design, value["documentId"], args)     except Exception as exc:         recovery = {"strategy": "delete_created_parameter", "verified": False}         try:@@ -239,20 +242,31 @@ def parameter_create(app, args):         except Exception as restore_error:             recovery["error"] = str(restore_error)         raise MechanicalError("parameter_create_failed", str(exc), recovery=recovery)-    return {"target": current_target(app), "parameter": parameter_data(design, parameter), "health": state, "transactional": False}+    fresh = current_target(app)+    return {"target": fresh, "parameter": parameter_data(design, parameter), "health": state,+            "compute": compute.committed(value["documentId"], fresh["revision"], state, settled), "transactional": False}   def check_health(app, args):-    _, design, _ = target(app, args, args.get("recompute", False))-    state = health(design, args.get("recompute", False))-    return {"target": current_target(app), "health": state}+    _, design, value = target(app, args, args.get("recompute", False))+    if args.get("recompute", False):+        state = compute.checkpoint(design, value["documentId"], "requested")+    else:+        state = health(design, False)+    return {"target": current_target(app), "health": state, "compute": compute.status(value["documentId"])}   def export(app, args):     _, design, value = target(app, args, True, model_write=False)-    require_healthy(design, False)+    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)     result = export_design(design, args)-    result["target"] = value+    result["target"] = current_target(app)+    result["compute"] = compute.status(value["documentId"])     return result  
addin/AdomBridge/commands/mechanical/regular_joints.py+2−1
@@ -366,7 +366,8 @@ 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)-    require_healthy(design)+    # Fusion solves the joint as it is added; the write's own settle decides on a full recompute+    require_healthy(design, False)     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+4−2
@@ -10,6 +10,7 @@ import uuid  import adsk.core +from . import compute from .core import (MechanicalError, active, current_target, recovery_snapshot,                    require, require_healthy, same, target) @@ -111,14 +112,14 @@ 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, require_healthy(design, False))+    before = recovery_snapshot(design, compute.health_before(design, value["documentId"], value["revision"]))     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 = require_healthy(design)+        state, settled = compute.settle(design, value["documentId"], args)         provisional = current_target(app)         result = serialize(provisional) if callable(serialize) else serialize         result.update(target=provisional, health=state, transactional=True)@@ -128,6 +129,7 @@ def execute(app, args, operation):         fresh = current_target(app)         _retarget(result, fresh)         result["transaction"] = {"committed": True, "backend": "PTransaction"}+        result["compute"] = compute.committed(value["documentId"], fresh["revision"], state, settled)         return result     except Exception as exc:         if committed:
docs/MECHANICAL.md+20−1
@@ -36,7 +36,26 @@ The mechanical layer is measured on a 4,579-occurrence, 1,249-component producti  - 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 full guards: a health check before, a full recompute and health check after, and the recovery witness. The witness reads again only the bodies whose revision changed since the last one.+- 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.++## 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:++- **Significance.** Writes since the last full recompute add up their weights (`compute.py`):+  - 0: metadata, materials and new parameters;+  - 0.5: appended sketches, planes and axes;+  - 1: features, components, inserts and occurrence moves;+  - 1.5: parameter changes;+  - 2: edits of existing features, sketches and joint origins, and joints, rigid groups and motion links;+  - 3: deletes.++  The write that brings the total to 6 runs the full recompute before it commits.+- **A move to another part.** A write whose references sit under a different top-level occurrence than the last write's checkpoints the previous part's edits first, once they add up to at least 2 (an edit of existing history, a joint, a delete, or two appended features).+- **Export.** `export` runs the full recompute first when anything changed since the last one.+- **On request.** `health {recompute:true}` runs it and starts the counters again: at a milestone, or before saving a long run of edits.++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.  ## Plan exploded assembly steps 
skills/fusion-design-efficiency/SKILL.md+4−3
@@ -105,9 +105,10 @@ Run it before and after a change, and before calling a build done.     of repeating sketch entities. 14. **Zero timeline errors and warnings.** Every one is recomputed each time. Reference origin     geometry rather than other components.-15. **Batch work into fewer operations.** Each guarded operation pays a revision check and a-    recompute (O(model)), so N small operations cost O(N²) on a big design. Prefer one pattern, one-    multi-entity sketch geometry call or one insert at the final pose.+15. **Batch work into fewer operations.** Each guarded operation pays a revision check and a read of+    the timeline's health (O(model)), and every few writes a full recompute, so N small operations+    cost O(N²) on a big design. Prefer one pattern, one multi-entity sketch geometry call or one insert+    at the final pose.  ## Heavy detail 
tests/test_mechanical.py+2−1
@@ -144,7 +144,8 @@ class Safety(unittest.TestCase):         def check(*args):             if p.expression=='-1 mm':raise c.MechanicalError('unhealthy_model','bad feature')             return {'healthy':True}-        with patch.object(ops,'target',return_value=(None,design,{})),patch.object(ops,'require_healthy',side_effect=check),patch.object(ops,'recovery_snapshot',side_effect=lambda d,health_state=None:{'expression':p.expression}),patch.object(ops,'current_target',return_value={}):+        compute=importlib.import_module('mechanical_test_commands.mechanical.compute');compute.reset()+        with patch.object(ops,'target',return_value=(None,design,{'documentId':'d','revision':'r'})),patch.object(compute,'health',side_effect=lambda d,recompute=False:{'healthy':p.expression!='-1 mm'}),patch.object(ops,'require_healthy',side_effect=check),patch.object(ops,'recovery_snapshot',side_effect=lambda d,health_state=None:{'expression':p.expression}),patch.object(ops,'current_target',return_value={}):             with self.assertRaises(c.MechanicalError) as error:ops.parameter_set(None,{'name':'depth','expression':'-1 mm'})         self.assertEqual(p.expression,'10 mm');self.assertTrue(error.exception.details['recovery']['verified']) 
tests/test_mechanical_compute.pyadded+173
@@ -0,0 +1,173 @@+"""When writes run Fusion's full recompute, offline: Fusion's own compute plus a health read per write,+the full recompute at a checkpoint (enough significant change, a move to another part, an export, a+caller asking), and the health a write verified reused while nothing changed."""+import importlib+from types import SimpleNamespace as NS+import unittest+from unittest.mock import patch++import test_mechanical  # noqa: F401  (installs the fake adsk and the test package)++c = importlib.import_module('mechanical_test_commands.mechanical.core')+compute = importlib.import_module('mechanical_test_commands.mechanical.compute')+tx = importlib.import_module('mechanical_test_commands.mechanical.transactions')+ops = importlib.import_module('mechanical_test_commands.mechanical.operations')+++def ref(path):+    return {"documentId": "d", "revision": "r", "token": "t", "objectType": "adsk::fusion::Occurrence", "occurrencePath": path}+++class Health:+    """compute.health stand-in: records whether each read asked for the full recompute."""+    def __init__(self, healthy=True):+        self.calls, self.healthy = [], healthy++    def __call__(self, design, recompute=False):+        self.calls.append(recompute)+        return {"healthy": self.healthy, "computeSucceeded": True if recompute else None, "faults": [], "unknown": []}+++class Weights(unittest.TestCase):+    def test_deletes_outweigh_relationships_edits_and_appends(self):+        w = lambda op: compute.weight(op, {})+        self.assertLess(w("component_metadata"), w("sketch_create"))+        self.assertLess(w("sketch_create"), w("feature_create"))+        self.assertLess(w("feature_create"), w("feature_edit"))+        self.assertLessEqual(w("feature_edit"), w("joint_create"))+        self.assertLess(w("joint_create"), w("entity_delete"))+        self.assertEqual(w("some_future_write"), 1.0)++    def test_a_parameter_batch_weighs_its_changes(self):+        args = {"changes": [{"action": "create"}, {"action": "set"}, {"action": "set"}, {"action": "delete"}]}+        self.assertEqual(compute.weight("parameter_batch", args), 6.0)+        self.assertEqual(compute.weight("parameter_batch", {"changes": [{"action": "create"}]}), 0.0)+++class Focus(unittest.TestCase):+    def test_the_first_occurrence_path_names_the_part(self):+        self.assertEqual(compute.focus_of({"feature": ref("Drawer:1+Handle:2")}), "Drawer:1")+        self.assertEqual(compute.focus_of({"geometryOne": {"entity": ref("Lid:1")}, "geometryTwo": ref("Box:1")}), "Lid:1")++    def test_root_level_work_and_the_target_name_no_part(self):+        self.assertIsNone(compute.focus_of({"target": {"occurrencePath": "Lid:1"}, "name": "w", "expression": "2 mm"}))+        self.assertIsNone(compute.focus_of({"feature": dict(ref(None), occurrencePath=None)}))+++class Policy(unittest.TestCase):+    def setUp(self):+        compute.reset()+        self.health = Health()+        self.patch = patch.object(compute, "health", self.health)+        self.patch.start()++    def tearDown(self):+        self.patch.stop()++    def write(self, op, revision, **args):+        state, info = compute.settle(None, "d", {"op": op, **args})+        return compute.committed("d", revision, state, info)++    def test_appends_stay_incremental_until_they_add_up_to_a_checkpoint(self):+        out = [self.write("feature_create", "r%d" % i, profiles=[ref("Lid:1")]) for i in range(6)]+        self.assertEqual(self.health.calls, [False, False, False, False, False, True])+        self.assertEqual([o["full"] for o in out], [False] * 5 + [True])+        self.assertEqual((out[4]["significance"], out[5]["significance"], out[5]["reason"]), (5.0, 0.0, "significance"))+        self.assertEqual(out[5]["lastFullCompute"]["reason"], "significance")++    def test_moving_on_to_another_part_checkpoints_the_last_one(self):+        self.write("feature_edit", "r1", feature=ref("Lid:1"))+        out = self.write("feature_create", "r2", profiles=[ref("Box:1")])+        self.assertEqual((self.health.calls, out["reason"], out["focus"]), ([False, True], "focus_change", "Box:1"))++    def test_metadata_only_on_one_part_does_not_checkpoint_the_move(self):+        self.write("component_metadata", "r1", component=ref("Lid:1"))+        self.write("feature_create", "r2", profiles=[ref("Box:1")])+        self.assertEqual(self.health.calls, [False, False])++    def test_one_occurrence_move_is_not_a_cluster_worth_a_checkpoint(self):+        self.write("occurrence_update", "r1", occurrence=ref("Lid:1"))+        self.write("occurrence_update", "r2", occurrence=ref("Box:1"))+        self.assertEqual(self.health.calls, [False, False])++    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")})+        self.assertEqual(error.exception.code, "unhealthy_model")+        self.assertEqual(error.exception.details["compute"]["full"], False)+        self.assertEqual(compute.status("d")["writesSinceFullCompute"], 0)++    def test_health_a_write_verified_is_reused_while_nothing_changed(self):+        self.write("feature_create", "r1")+        state = compute.health_before(None, "d", "r1")+        self.assertEqual((self.health.calls, state["computeSucceeded"]), ([False], None))+        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):+        self.health.healthy = False+        with self.assertRaises(c.MechanicalError) as error:+            compute.health_before(None, "d", "r9")+        self.assertEqual(error.exception.code, "unhealthy_model")++    def test_an_explicit_checkpoint_starts_the_counters_again(self):+        self.write("entity_delete", "r1")+        self.assertTrue(compute.pending("d"))+        compute.checkpoint(None, "d", "requested")+        self.assertEqual((self.health.calls[-1], compute.pending("d"), compute.status("d")["lastFullCompute"]["reason"]),+                         (True, False, "requested"))+++class Writes(unittest.TestCase):+    """The transaction adapter and the parameter writes report the ledger; export checkpoints first."""+    def setUp(self):+        compute.reset()+        self.health = Health()+        self.revision = ["r0"]+        self.commands = []+        self.patches = [patch.object(compute, "health", self.health),+                        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, "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))+        for p in self.patches:+            p.start()++    def tearDown(self):+        for p in self.patches:+            p.stop()++    def edit(self, op, n):+        def operation(design, value, args):+            self.revision[0] = "r%d" % n+            return {"done": n}+        return tx.execute(NS(activeDocument="doc"), {"op": op, "target": {}, "feature": ref("Lid:1")}, operation)++    def test_feature_edits_read_health_without_recomputing_until_the_third_reaches_a_checkpoint(self):+        results = [self.edit("feature_edit", n) for n in range(1, 5)]+        # one walk before the first write; after that each write's verified health is reused+        self.assertEqual(self.health.calls, [False, False, False, True, False])+        self.assertEqual([r["compute"]["full"] for r in results], [False, False, True, False])+        self.assertEqual([r["compute"]["significance"] for r in results], [2.0, 4.0, 0.0, 2.0])+        self.assertEqual(results[2]["compute"]["reason"], "significance")+        self.assertEqual(self.commands.count("PTransaction.Commit"), 4)++    def test_an_export_after_edits_runs_the_full_recompute_first(self):+        self.edit("feature_create", 1)+        self.health.calls.clear()+        with patch.object(ops, "target", lambda app, args, write=False, model_write=True: ("doc", self.design, {"documentId": "d", "revision": "r1"})), \+                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": {}})+            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])+++if __name__ == "__main__":+    unittest.main()