# I2 (root distance) is never a hard error anywhere — is that the right call? ## Background Companion to #225 (dead-code cleanup) and #227 (I3 naming collision), surfaced during the same identity-domain invariant investigation. `muse/plugins/identity/plugin.py`'s docstring lists three invariants: ``` I1 Acyclicity — merge() rejects any new relationship that would introduce a cycle. I2 Root distance — derived property; not enforced at merge time. I3 Quorum soundness — hub-level concern; not enforced at merge time. ``` I1 not being enforced at *merge* time is fine — it can't be, a local two-branch merge has no way to check org-authorization signatures or global graph position. But I2 turns out not to be enforced **anywhere**, not just at merge time: ```python # musehub/graph/push_validator.py idx = RootDistanceIndex.build(dag) for rec in identities: h = rec["handle"] if idx.distance(h) is None: warnings.append( f"I2 warning: {h!r} ({rec['type']}) has no path to any human root — " f"it is an orphaned node" ) ``` This is the *only* place I2 is checked in the entire push/merge pipeline, and it only ever appends a warning. The push is accepted regardless. An identity graph can contain an arbitrary number of nodes with no path to any human root, indefinitely, with nothing ever blocking it. ## The actual question Is "orphaned node, no path to a human root" always a real problem (should eventually be a hard error, at least in some cases), or is it a legitimate transient state that should stay a soft warning forever? Plausible legitimate cases for a temporarily-orphaned node: - An identity mid-construction, about to receive its first `spawns` or `member_of` edge in the same push/session. - An org or agent identity provisioned ahead of the relationship that will connect it (e.g., bulk-importing identities before wiring relationships in a second pass). Plausible cases where it's a real integrity problem worth blocking: - A node that has been orphaned for a long time (some age/grace-period threshold) with no relationship ever added. - A node deliberately detached by a malicious or buggy push, e.g. as part of an attempt to hide a node from ancestry-based authorization checks that key off `human_ancestors()`. Given `IdentityGraphService.human_ancestors()` exists and is presumably meant to answer "who is accountable for this node," a permanently orphaned node might silently break whatever *consumes* that answer elsewhere in the system (issue #225 covers that this method currently has no callers either — worth resolving together). ## Scope 1. Decide, deliberately, whether I2 should ever become a hard error, and under what condition (e.g., "no path to root after a grace period" or "no path to root at the end of a specific action type" vs. "never, soft-warning is correct forever because orphaning is always legitimate transient state"). 2. If a hard-error condition is chosen, implement it in `push_validator.py` alongside the existing warning path, with a test that constructs both the legitimate-transient case and the should-be-blocked case to confirm the distinction actually works. 3. If soft-warning-forever is the deliberate final answer, update `muse/plugins/identity/plugin.py`'s docstring and any other "I2 ... not enforced" language to say so explicitly and explain why, rather than reading like an unfinished TODO. ## Acceptance criteria - A documented, deliberate answer exists for "can an identity node stay permanently orphaned, and if not, what actually stops it" — not left as an open question implied by a docstring caveat.