Two independent design reviews found the same gap three days apart: something in Smeldr's own product design assumed a computation that didn't exist yet, "walk the relation graph outward from an anchor, N hops, and report what's found (or genuinely absent) at each ring." That's RelationStore.Reachability now.
What was actually there before
Two things looked close enough to mistake for it, and weren't:
ScopeDynamic(governance) resolves exactly one hop, a boolean "is this reachable
from the grant's anchor" check, not a traversal.
BuildContextPacket(Context Packets, A214) does bounded breadth-first traversal,
but it's capped at depth 2, hardcoded to five orchestration content types, and shaped for a one-shot JSON export, not a general, repeatable, arbitrary-type distance read.
What it is
result, err := relationStore.Reachability(ctx, "post", "p1", "related_to", "outgoing", 3)
// result.Rings[0] = everything one hop from p1
// result.Rings[1] = everything two hops away
// result.Rings[2] = everything three hops away, possibly empty, and that's data
Every requested depth returns a ring, even after the graph runs out of new nodes to find. A ring with zero items is a genuine, reportable absence, not an error, not a missing entry. That distinction matters: an operational-tension reading that says "nothing found two hops out" needs to be able to say that as a fact, not go silent.
kind filters by relation kind (empty = all kinds); direction is "incoming", "outgoing", or "both", the same vocabulary MCPGetRelations already uses. maxDepth is capped at MaxReachabilityDepth (10), a safety ceiling against pathological graphs, not a typical-use number.
Why not just extend ScopeDynamic
It has the right shape already, anchor, relation kind, direction, so "just add a depth parameter to relationExists" looked tempting. Two reasons it isn't the right move: the return shape doesn't match (a boolean access check and a ring-structured report are genuinely different things, and the boolean is a trivial derivative of the ring data, not the other way round), and governance.go's authorization path is fail-closed and security-critical, and bolting a general traversal capability onto it as a side effect of an unrelated reporting primitive is exactly the kind of scope creep worth avoiding. The new primitive lives in its own file, reachability.go, and governance.go is untouched.
The implementation pattern isn't new
BuildContextPacket already proved the shape: iterative frontier-expansion BFS, visited-set dedup, no recursive SQL. That's deliberate: an earlier design spike for the relation graph (T06) already established that SQLite has no off-the-shelf bounded-traversal pattern and recursive CTEs get expensive with no query-plan optimizer to lean on. Reachability generalizes the same pattern rather than inventing a new one: no hardcoded type table, any anchor type works, kind and direction are just query filters passed through to the same GetBySource/ GetByTarget calls every other relation read already uses.