What’s better than 5 semantic outputs? 5 MOAR semantic outputs! The compiler semantic toolchain enables engineers to have reachability, diagnostics, provenance, policy diff, and equivalence. That answered “what did the compiler decide?” and “did my refactor break anything?”.
Now the toolchain extends to ten semantic outputs total, answering “prove my invariants hold,” “how big is this change operationally,” “what does my network look like from one VPC’s perspective,” “is my policy carrying dead weight,” and “show me the topology.”
Same principle as before: every output operates on the compiled
reachability matrix, not route resources. Every output is
scope-invariant across Regional IR (Centralized Router), Global IR
(Full Mesh Trio), and Domain IR (Super Router). Enable them through the
inspect field on any IR module.
centralized_router = {
name = "mystique"
routing_policy = local.routing_policy
vpcs = module.vpcs
inspect = {
segment_report = true
policy_normalization = true
connectivity_graph = true
assertions = {
must_deny = [{ from = vpcs["infra"], to = vpcs["dev"] }]
must_permit = [{ from = vpcs["app"], to = vpcs["db"] }]
}
}
}
Five New Semantic Outputs
Assertions: Static Analysis for Network Policy
The routing policy defines what connectivity should be. Assertions define what connectivity must be, verified against the compiled reachability matrix at plan time.
Two assertion types:
must_deny: the pair must be deniedmust_permit: the pair must be permitted
{
"passed": true,
"violations": {
"must_deny": [],
"must_permit": []
}
}
When an invariant is violated:
{
"passed": false,
"violations": {
"must_deny": [],
"must_permit": [
{
"pair": "app:db",
"verdict": "denied:default"
}
]
}
}
This separates policy authorship from policy verification. The network team writes the routing policy. A security team defines assertions independently: “cardholder data VPCs must never reach general workloads,” “production must never reach development,” “monitoring must always reach application VPCs.”
Both live in Terraform. Both evaluate at plan time. A policy change that violates a standing assertion shows the exact pair and verdict in the output, before any infrastructure is applied.
Blast Radius: Impact Analysis
Policy diff tells you what changed semantically. Blast radius tells you how big the change is operationally.
{
"affected_vpcs": ["app", "db"],
"routes_added": 12,
"routes_removed": 4,
"pairs_changed": 2,
"route_tables_affected": 8
}
The difference between “app:db connectivity changed” and “this change touches 2 VPCs, 16 routes across 8 route tables.” Engineers and change advisory boards care about scope, not just content.
Blast radius is automatically computed whenever
policy_diff.previous_reachability is provided. No separate toggle.
Route counts account for secondary CIDRs and both directions. A VPC
with 3 route tables and 2 CIDRs (primary + secondary) contributes 6
routes per permitted pair direction, not 3. The math is exact because
the compiler has full route table and CIDR metadata for every VPC.
When nothing changed:
{
"affected_vpcs": [],
"routes_added": 0,
"routes_removed": 0,
"pairs_changed": 0,
"route_tables_affected": 0
}
Segment Report: The VPC-Oriented View
The reachability matrix is pair-oriented. That’s the right shape for proving properties, but engineers troubleshoot from one VPC: “what can app talk to?” The segment report pivots the matrix to a per-VPC view.
{
"app": {
"segment": "workers",
"reaches": ["cicd", "general"],
"denied": []
},
"cicd": {
"segment": "workers",
"reaches": ["app", "general"],
"denied": []
},
"db": {
"segment": "unsegmented",
"reaches": [],
"denied": ["app", "cicd", "general"]
},
"general": {
"segment": "unsegmented",
"reaches": ["app", "cicd"],
"denied": ["db"]
}
}
Three fields per VPC:
- segment: which segment the VPC belongs to, or
"unsegmented"if not in any segment - reaches: VPC names this VPC has permitted connectivity to
- denied: VPC names this VPC is denied connectivity to
Same information as the reachability matrix, different axis. When you need to answer “why can’t this VPC reach anything?” or verify a VPC’s isolation posture, the segment report is the direct lookup. The compiler analogy is a symbol table dump: per-symbol metadata extracted from the compiled output.
Policy Normalization: The Decompiler
Given any policy, the normalizer reconstructs the minimal equivalent policy that produces the same reachability. This is the inverse of compilation: instead of policy-to-routes, it’s reachability-to-minimal-policy.
{
"current_rule_count": 3,
"normalized_rule_count": 1,
"normalized_policy": {
"default": "deny",
"segments": {
"group_0": ["app", "cicd", "general"]
},
"allow": [],
"deny": []
}
}
The normalizer:
- Walks the compiled reachability matrix
- Fingerprints each VPC by its connectivity profile (which VPCs it can reach)
- Groups VPCs with identical fingerprints as segment candidates
- Tries both
default="deny"anddefault="allow" - Picks whichever form uses fewer total primitives
The reachability fingerprinting step is the key insight. VPCs with
identical connectivity profiles are natural segment candidates. Three
VPCs that all reach each other and nothing else are a segment, whether
the original policy expressed them as a segment, three allow rules, or
default="allow" with denies to everything else.
Compare current_rule_count to normalized_rule_count. If they’re
equal, your policy is already minimal. If the normalized count is lower,
the normalized_policy shows the shorter form. Examples of what it
detects:
- 3 explicit allow rules forming a full mesh becomes
default="allow"with 0 rules - 2 allow rules under deny becomes 1 segment
- 2 deny rules isolating a VPC becomes
default="allow"with 1 deny
A lower count doesn’t mean you should switch. The current policy may encode structural intent (meaningful segment names, explicit groupings) that the normalizer doesn’t see.
The output tells you the reachability cost of that intent: “you wrote 3 rules but 1 would produce the same connectivity.” Whether the extra structure is worth keeping is a judgment call. The normalizer gives you the data for that decision.
Connectivity Graph: See the Topology
DOT format rendering of the reachability matrix. Nodes are VPCs, edges are permitted pairs with colored edges encoding the verdict reason, and segment memberships render as dashed subgraph clusters.
graph connectivity {
graph [rankdir=LR]
node [shape=box, style=filled, fillcolor="#f0f0f0"]
edge [fontsize=10]
subgraph cluster_workers {
label="workers"
style=dashed
color="#95a5a6"
"app"
"cicd"
}
"general"
"app" -- "cicd" [color="#2ecc71", label="segment"]
"app" -- "general" [color="#3498db", label="allow"]
"cicd" -- "general" [color="#3498db", label="allow"]
}
The output is a .dot file written to
inspect/<router-name>-connectivity-graph.dot. Render it with
Graphviz:
brew install graphviz
dot -Tpng inspect/myrouter-connectivity-graph.dot -o connectivity.png
dot -Tsvg inspect/myrouter-connectivity-graph.dot -o connectivity.svg
Edge colors:
- Blue (#3498db):
allowrule - Green (#2ecc71):
segmentmembership - Gray (#95a5a6):
defaultfallthrough
Denied pairs produce no edges. A fully denied topology renders all nodes with no connections. Segment clusters appear as dashed boxes. Unsegmented VPCs appear as standalone nodes outside any cluster.
Engineers scan a graph faster than they read a JSON matrix, especially as VPC count grows. Segment clusters make isolation boundaries visible at a glance, and edge colors distinguish why connectivity exists without reading verdict strings.
Why These Five
The first five outputs answered questions about the compilation itself: what was decided, why, what changed, and can I prove equivalence. The new five shift toward operational use:
- Assertions let you codify invariants that survive policy changes. A standing assertion catches regressions at plan time, not during an incident.
- Blast radius gives change advisory boards the scope metric they need: not just “connectivity changed” but “how many VPCs, routes, and route tables are affected.”
- Segment report gives the VPC-oriented view that individual engineers need when troubleshooting or verifying isolation.
- Policy normalization tells you whether your policy is carrying unnecessary complexity and shows the minimal equivalent form.
- Connectivity graph makes the topology visual. The graph is the artifact you put in a design doc, a PR review, or an architecture diagram.
Together with the original five, the toolchain covers the full lifecycle: compile, inspect, verify, diff, prove, analyze, normalize, and visualize. Ten outputs, one compilation unit, same algebra at every scope.
Compiler Analogy: Where Each Output Fits
| Output | Compiler Analogy |
|---|---|
| Reachability | IR dump |
| Diagnostics | -Wall |
| Provenance | Debug symbols |
| Policy Diff | Incremental compilation |
| Equivalence | Translation validation |
| Assertions | Static analysis |
| Blast Radius | Impact analysis |
| Segment Report | Symbol table dump |
| Normalization | Optimizer / decompiler |
| Connectivity Graph | Graph/IR visualization |
Test Coverage
The compiler now has 143 passing terraform test cases. The five new
outputs added 40 tests: 9 assertions, 8 blast radius, 8 segment
report, 8 policy normalization, 7 connectivity graph. Each test
asserts on the exact output structure, not just that the output exists.
As before, declare intent, compile routes, inspect semantics, refactor with proof but now with even MOAR confidence!