diff --git a/docs/MOUNT_TAXONOMY.md b/docs/MOUNT_TAXONOMY.md index c685e9a..0a8b8f2 100644 --- a/docs/MOUNT_TAXONOMY.md +++ b/docs/MOUNT_TAXONOMY.md @@ -75,6 +75,15 @@ Mellumwork ternary / conflict-resolution-faithfulness machinery is paid *only* o Add `reference mount (no copy)` as a fourth column on the intermittent-link box with the availability precondition on the edge, and the two diagrams are one. +**The Needs firewall prunes the lattice first.** `backend_for` now takes `needs` + +`offline_tolerant` + `store_locality`, and two Needs forbid the cheap reference-mount *before* link +availability is even consulted: a **`no_egress`** Need on a **remote** store (a remote reference-mount +*is* egress → forced to a local copy), and **offline-tolerance** (a reference-mount has zero offline +capability → must cache locally even over a reliable link). A `no_egress` Need on a *local* store is +fine — a local mount is not egress. So the real signature is +`intent × link × durability × needs → backend`, and the Needs firewall and the mount lattice are one +plane, not two. + ## The honest counterexample The sandbox's root ext4 is simultaneously runtime image, scratch, cache, and working directory — the diff --git a/tools/data_sphere.py b/tools/data_sphere.py index 81ebe67..e3c01df 100644 --- a/tools/data_sphere.py +++ b/tools/data_sphere.py @@ -85,13 +85,29 @@ def check_egress_invariant(mounts: list) -> dict: else f"{len(egress)} egress mounts ({egress}); the invariant is at most one, named")} -def backend_for(*, intent: str, link_availability: str, durability: str) -> dict: - """intent x link_availability x durability -> backend. The reconciliation burden is a function of - this product, not a property of the data. Over a reliable link: REFERENCE-mount (no copy).""" +def backend_for(*, intent: str, link_availability: str, durability: str, needs: dict | None = None, + offline_tolerant: bool = False, store_locality: str = "remote") -> dict: + """intent x link_availability x durability x NEEDS -> backend. The reconciliation burden is a + function of this product, not a property of the data. + + The Needs firewall prunes the lattice BEFORE link availability, because two Needs forbid the + cheap reference-mount outright: + * a `no_egress` Need on a REMOTE store — a remote reference-mount *is* egress, so it's + forbidden; the data must be copied local. + * offline-tolerance — a reference-mount has zero offline capability (cut the link, it fails), + so a workload that must survive link loss has to cache locally even over a reliable link. + Only after those prunes does link availability pick reference-mount vs copy+reconcile. + """ + needs = needs or {} + if needs.get("no_egress") and store_locality == "remote": + return {"backend": "local-copy", "copy": True, "reconciliation": durability == "canonical", + "note": "no_egress Need forbids a remote reference-mount (which IS egress) -> local copy"} + if offline_tolerant: + return {"backend": "local-cache", "copy": True, "reconciliation": durability == "canonical", + "note": "offline-tolerant: a reference-mount fails on link loss -> local cache"} if link_availability == "reliable": return {"backend": "reference-mount", "copy": False, "reconciliation": False, "note": "mount it, don't sync it — no second copy, no divergence, no CRF machinery"} - # intermittent link -> forced into copy semantics if durability == "canonical": return {"backend": "copy+reconcile", "copy": True, "reconciliation": True, "note": "intermittent + canonical: copy forces divergence -> conflict-resolution (SP-EVAL-CRF-001)"} diff --git a/tools/test_data_sphere.py b/tools/test_data_sphere.py index ddb133f..f1fbdc0 100644 --- a/tools/test_data_sphere.py +++ b/tools/test_data_sphere.py @@ -65,6 +65,21 @@ def test_backend_lattice_reference_mounts_over_reliable_links(): assert derived["copy"] is True and derived["reconciliation"] is False +def test_needs_prune_the_lattice_before_link_availability(): + # a no_egress Need on a REMOTE store: a remote reference-mount IS egress -> forbidden -> local copy + remote = ds.backend_for(intent="canonical", link_availability="reliable", durability="canonical", + needs={"no_egress": True}, store_locality="remote") + assert remote["backend"] == "local-copy" and remote["copy"] is True + # but a LOCAL store may still be reference-mounted under no_egress (a local mount is not egress) + local = ds.backend_for(intent="canonical", link_availability="reliable", durability="canonical", + needs={"no_egress": True}, store_locality="local") + assert local["backend"] == "reference-mount" + # offline-tolerance forbids reference-mount even over a reliable link (it fails on link loss) + off = ds.backend_for(intent="canonical", link_availability="reliable", durability="canonical", + offline_tolerant=True) + assert off["backend"] == "local-cache" and off["copy"] is True + + if __name__ == "__main__": import sys fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]