Gate Graph
Turn a sprawling gate/validator/middleware layer into a deterministic map: how
many modules, which ones overlap, and which are wired to nothing.
Overview
Long-lived agents grow gates the way houses grow junk drawers. Each new check
looks cheap, but every module in the pipeline is code the agent pays for on
every request — in tokens, latency, and maintenance surface. Nobody removes one
because nobody can prove it's redundant or dead. gate_graph.py is that proof:
it parses every module with the ast stdlib (no imports run, nothing executes),
builds a structural fingerprint per module (functions, classes, attributes,
regex/substring literals, raised exceptions), and reports three things —
how many gates, which pairs overlap (Jaccard on fingerprints), and
which gates are orphans (imported nowhere in the tree). It exits non-zero
when the gate count blows a budget or two gates overlap past a threshold, so it
drops straight into CI.
Real run against a production agent's gate layer: 56 modules, 26 orphans
imported nowhere, and self_mod_canary ↔ self_mod_canary_io at 0.43 overlap
— exactly the "declared but never wired" and "split then forgotten" patterns
that hand-maintained inventory whitelists miss.
When to use
- A pipeline has grown to dozens of gate/validator/middleware modules and you
suspect duplication or dead code.
- You want CI to fail when the gate count grows past a budget, or when two gates
structurally converge (a sign one should absorb the other).
- You're deciding what to consolidate and want a matrix, not vibes.
Not for: semantic correctness (two gates can look similar and do different
things — read the overlap as a candidate, then confirm), or replacing tests.
The method
- Point it at the gate directory.
python scripts/gate_graph.py path/to/gates --max-gates 40 --max-overlap 0.5
It recurses, skips __init__.py and vendor/cache dirs, and fingerprints
each remaining module offline.
- Read the three signals.
- Gate count vs
--max-gates: your growth budget.
- High-overlap pairs (>
--max-overlap): consolidation candidates. Only
these carry the shared/left-only/right-only fingerprint diff so you can see
what they duplicate.
- Orphan gates: modules imported nowhere in the tree — dead until proven
otherwise. Confirm none are dynamically loaded before deleting.
- Wire it into CI. The tool exits non-zero over either budget:
# .github/workflows/gate-graph.yml
- run: python scripts/gate_graph.py src/agent/gates --max-gates 40 --max-overlap 0.5
Now "the gate layer got fatter" fails the build instead of silently taxing
every request.
- Consolidate the top, re-measure. Merge an overlapping pair, delete a
confirmed orphan, then re-run. The count and matrix are the scoreboard.
Output modes
- Default (text): human-readable report — fingerprint counts, the full
matrix, high-overlap pairs, orphans, and a
FAIL: line per breach.
--json: machine-readable and deliberately lean — gate names,
orphans, the top ranked pairs (name + score only), and the heavy
fingerprint diff only for pairs that breach the threshold. On a 56-gate
layer this is ~12 KB.
--json --full-matrix: adds the full NxN matrix and every module's
fingerprint set (~210 KB on the same layer). Opt in only when you need it.
Anti-patterns
- Dumping every pair's fingerprint diff. With N gates there are N² pairs;
attaching the full shared/diff sets to all of them is the exact token bloat
this tool exists to catch. The JSON stays lean by default and enriches only
breaching pairs — a gate-maintenance tool must not itself be the bloat.
- Deleting an orphan on sight. "Imported nowhere" is a strong signal, not a
proof — check for dynamic/string-based loading (
importlib, registries, hook
tables) first.
- Treating overlap as duplication. High Jaccard means structurally
similar, which is a review prompt, not a verdict. Confirm behavior before merging.
- Hardcoding the inventory. The point is that the map is recomputed from the
AST every run — a static "these are the real gates" whitelist rots the moment
someone adds a module.
Example
$ python scripts/gate_graph.py src/agent/gates --max-gates 40 --max-overlap 0.5
Gate-graph overlap report
==============================
gates: 56
max-gates: 40
max-overlap: 0.5
...
High-overlap candidate pairs:
self_mod_canary <-> self_mod_canary_io: 0.43 (below 0.5 threshold, watch)
Orphan gates:
- actionable_resource_model
- temporal_live_claims
... (26 total)
FAIL: gate_count 56 > max_gates 40
$ echo $?
1
1---2name: gate-graph3description: Find overlap and dead weight in a Python gate layer by extracting AST fingerprints, generating an overlap matrix, and failing CI when redundant gates or too many gates are detected.4license: MIT5---67# Gate Graph89Turn a sprawling gate/validator/middleware layer into a deterministic map: how10many modules, which ones overlap, and which are wired to nothing.1112## Overview1314Long-lived agents grow gates the way houses grow junk drawers. Each new check15looks cheap, but every module in the pipeline is code the agent pays for on16every request — in tokens, latency, and maintenance surface. Nobody removes one17because nobody can prove it's redundant or dead. `gate_graph.py` is that proof:18it parses every module with the `ast` stdlib (no imports run, nothing executes),19builds a structural fingerprint per module (functions, classes, attributes,20regex/substring literals, raised exceptions), and reports three things —21**how many gates**, **which pairs overlap** (Jaccard on fingerprints), and22**which gates are orphans** (imported nowhere in the tree). It exits non-zero23when the gate count blows a budget or two gates overlap past a threshold, so it24drops straight into CI.2526Real run against a production agent's gate layer: **56 modules, 26 orphans27imported nowhere, and `self_mod_canary` ↔ `self_mod_canary_io` at 0.43 overlap**28— exactly the "declared but never wired" and "split then forgotten" patterns29that hand-maintained inventory whitelists miss.3031## When to use3233- A pipeline has grown to dozens of gate/validator/middleware modules and you34 suspect duplication or dead code.35- You want CI to fail when the gate count grows past a budget, or when two gates36 structurally converge (a sign one should absorb the other).37- You're deciding what to consolidate and want a matrix, not vibes.3839Not for: semantic correctness (two gates can look similar and do different40things — read the overlap as a *candidate*, then confirm), or replacing tests.4142## The method43441. **Point it at the gate directory.**45 ```bash46 python scripts/gate_graph.py path/to/gates --max-gates 40 --max-overlap 0.547 ```48 It recurses, skips `__init__.py` and vendor/cache dirs, and fingerprints49 each remaining module offline.502. **Read the three signals.**51 - *Gate count* vs `--max-gates`: your growth budget.52 - *High-overlap pairs* (> `--max-overlap`): consolidation candidates. Only53 these carry the shared/left-only/right-only fingerprint diff so you can see54 *what* they duplicate.55 - *Orphan gates*: modules imported nowhere in the tree — dead until proven56 otherwise. Confirm none are dynamically loaded before deleting.573. **Wire it into CI.** The tool exits non-zero over either budget:58 ```yaml59 # .github/workflows/gate-graph.yml60 - run: python scripts/gate_graph.py src/agent/gates --max-gates 40 --max-overlap 0.561 ```62 Now "the gate layer got fatter" fails the build instead of silently taxing63 every request.644. **Consolidate the top, re-measure.** Merge an overlapping pair, delete a65 confirmed orphan, then re-run. The count and matrix are the scoreboard.6667## Output modes6869- **Default (text):** human-readable report — fingerprint counts, the full70 matrix, high-overlap pairs, orphans, and a `FAIL:` line per breach.71- **`--json`:** machine-readable and **deliberately lean** — gate names,72 orphans, the top ranked pairs (name + score only), and the heavy73 fingerprint diff *only* for pairs that breach the threshold. On a 56-gate74 layer this is ~12 KB.75- **`--json --full-matrix`:** adds the full NxN matrix and every module's76 fingerprint set (~210 KB on the same layer). Opt in only when you need it.7778## Anti-patterns7980- **Dumping every pair's fingerprint diff.** With N gates there are N² pairs;81 attaching the full shared/diff sets to all of them is the exact token bloat82 this tool exists to catch. The JSON stays lean by default and enriches only83 breaching pairs — a gate-maintenance tool must not itself be the bloat.84- **Deleting an orphan on sight.** "Imported nowhere" is a strong signal, not a85 proof — check for dynamic/string-based loading (`importlib`, registries, hook86 tables) first.87- **Treating overlap as duplication.** High Jaccard means *structurally88 similar*, which is a review prompt, not a verdict. Confirm behavior before merging.89- **Hardcoding the inventory.** The point is that the map is recomputed from the90 AST every run — a static "these are the real gates" whitelist rots the moment91 someone adds a module.9293## Example9495```96$ python scripts/gate_graph.py src/agent/gates --max-gates 40 --max-overlap 0.597Gate-graph overlap report98==============================99gates: 56100max-gates: 40101max-overlap: 0.5102...103High-overlap candidate pairs:104 self_mod_canary <-> self_mod_canary_io: 0.43 (below 0.5 threshold, watch)105106Orphan gates:107 - actionable_resource_model108 - temporal_live_claims109 ... (26 total)110FAIL: gate_count 56 > max_gates 40111$ echo $?1121113```