Appendix B - 5. Control↔substrate dependency (computed blast-radius)
The Structure of Control↔substrate dependency (computed blast-radius) — its shape at a glance:
Each substrate-reading mechanism declares its stance in a typed field. A query joins those declarations against the substrate model and, given a migration target, prints exactly which mechanisms a change puts in scope — the blast radius, computed before you touch the substrate.
flowchart LR
C1[Control A<br/>stance: plane-X] --> Q{Blast-radius query}
C2[Control B<br/>stance: agnostic] --> Q
Sub[(Substrate model)] --> Q
Q -->|join on target| Table([in-scope mechanisms])
Accessible description: two mechanisms each declare a substrate stance. A query joins those declarations against the substrate model and, for a migration target, emits the table of mechanisms that change puts in scope. The table is derived, not hand-maintained.
Projected from the catalogue entry system-models / Control↔substrate dependency (computed blast-radius).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Make each control declare the substrate assumption it bakes in as typed metadata, so "which controls depend on which part of the substrate, and what will break when I change it" is a computed query, not a grep-and-read. Before a cross-cutting substrate change, the static-analysis blast radius is known up front (our instance: each lint that reads the deployment-topology model declares a typed plane-assumption (GKE-only / Cloud-Run-only / plane-aware / plane-agnostic) joined against the topology to print exactly which lints a migration puts in scope).
Motivation
A control (a lint, a gate, a validator) usually bakes an assumption about the substrate it checks against: "a service is a Kubernetes Deployment under this directory," "the scaler exposes this method," "the manifest carries this field." The assumption sits buried in the control's body. It is invisible until you change the substrate.
Then the change lands and the fleet fails in two silent ways at once:
- False FAIL. A migrated service keeps its old annotations, and controls validate it against manifests, methods, or fields that no longer exist. On a no-baseline deploy gate this is deploy-blocking: one buried assumption stops every release.
- False PASS. A migrated service drops its old annotation and vanishes from every totality, smoke, and disjointness check. It looks clean because nothing checks it anymore. An unmonitored service is the worse of the two.
The engineer planning the migration cannot see this coming. "Which of our checks assume the old substrate?" is answerable only by grepping for who imports the substrate model, then reading each body to judge whether it bakes the old shape. The entanglement that decides the whole refactor's blast radius lives in prose and grep: exactly the invariant-that-lives-only-in-code smell, and the failure is near-certain the moment someone migrates a service without knowing which controls will misfire.
Applicability
- The substrate is already a queryable model — a typed source-of-truth for the topology the controls check against, not scattered manifests. Without it there is nothing to join against.
- Controls already carry a metadata block the declaration can extend. Reuse the existing convention rather than standing up a parallel registry (adopt the schema you already lint).
- A closed enum of substrate stances small enough to be exhaustive and typed. An open string field reintroduces the drift and the typo class this exists to remove.
Structure
The Structure diagram appears at the top of this page.
Sample Code
Each mechanism carries a typed stance toward the substrate. A join reads every declaration and, given a migration target, returns the mechanisms that assume the old shape — the blast radius, computed from declarations rather than grepped and read body-by-body.
from dataclasses import dataclass
# A small closed enum keeps the stance exhaustive and typed.
STANCES = {"assumes_plane_a", "assumes_plane_b", "branches_per_plane", "agnostic"}
@dataclass(frozen=True)
class Control:
name: str
stance: str # one of STANCES; a topology-reading control MUST declare it
def blast_radius(controls: list[Control], migrate_to: str) -> list[str]:
"""Controls put in scope by a migration to `migrate_to` (they assume a different plane)."""
hits = []
for c in controls:
assumes_other = c.stance.startswith("assumes_") and c.stance != f"assumes_{migrate_to}"
if assumes_other:
hits.append(f"{c.name}: stance '{c.stance}' misfires on migration to {migrate_to}")
return hits
def undeclared(controls: list[Control]) -> list[str]:
return [c.name for c in controls if c.stance not in STANCES] # declaration lint
if __name__ == "__main__":
import sys
controls = load_controls() # reads each control's declared stance metadata
for line in blast_radius(controls, migrate_to="plane_b"):
print(f"IN-SCOPE: {line}")
sys.exit(1 if undeclared(controls) else 0)
Consequences
- Every substrate-reading control gains one declaration — the intended tax; it is what makes the edge queryable. A control that forgets fails the declaration lint.
- The enum must fit the substrate dimension. A stance the enum can't express (a third plane, a hybrid) forces an enum change, which is the honest signal that the substrate model itself grew a dimension.
- It pays off only at a substrate change. In a stable system the declarations sit inert. This is a design-time-on-the-predictive-smell control: worth it when a cross-cutting substrate migration is live or foreseeable, over-built if you add it speculatively to a substrate you will never change.
Example use within DocAble
- Each static analysis that reads the deployment-topology model declares a typed plane-assumption (GKE-only / Cloud-Run-only / plane-aware / plane-agnostic); a
plane-depsquery joins those declarations against the topology to print, per lint, whether a service's migration to a given plane puts it in scope and whether it bakes the old plane. - A declaration lint that fails any topology-reading control missing its plane-assumption (audit-only, then blocking); the deploy realization gate composes that guard, so no un-declared substrate consumer ships.
Related Patterns
- Bridge — agents and refactor plans query the computed blast radius to know what a substrate change touches (agent side) ◀──▶ the declaration lint governs the control fleet, keeping every substrate stance declared (product/controls side).
- Enabler — deployment-topology-model: the substrate model whose facts the controls assume and this mechanism makes them declare against.
- See also — meta-model-consumption: the read-don't-copy discipline this extends from values to dependency edges.
- See also — query-surface: the query path the computed blast-radius table rides on.
- See also — drift-parity-gates: the model↔reality parity family this sits beside, a different question (does the model match reality?) about a different join.