Files
settled-reach/tooling/tune-star-map-topology.py
T
jpmschweitzerandClaude Fable 5 346d87df7a chore(meta): docs/build sweep + tooling test gate (T-1069, T-1066)
- make test-tooling: planet-gen determinism guard + import_economics
  --dry-run, wired into pre-push on TOOLING_CHANGED; ruff widened to
  E4/E7/E9/F/W (90 safe auto-fixes applied; E402/E702/F841 ignored with
  documented counts)
- one-generator reality fixed in DEVOPS.md, asset-pipeline rule, CLAUDE.md
  (import_economics sole generator since #951/D-223); dead check-protocol
  target deleted; DEVOPS hook/config sections rewritten from the actual
  hook sources; team-patterns gate description updated (client+tooling)
- project.yaml: 0.2.0 → 0.4.0 per the 0.{phase}.{n} scheme, description
  refreshed from the v0.1 Sova narration to cascade reality
- stale comment sweep: voxel.rs stub claims (all 8 families implemented),
  cascade.rs TODO recited to T-1044, main.rs D-192 handshake claim,
  relationships.rs/chunk_streaming.rs version targets → phase language
- gitignore: client/settings.db* e2e-run artifacts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:22:55 +02:00

902 lines
34 KiB
Python

#!/usr/bin/env python3
"""
Star Map Topology Tuner — The Settled Reach
Starting from the current map (300 nodes, 347 edges):
Current vs Target:
dead_end 12.7% (38) → 20% (60) +22
spur_end 13.0% (39) → 15% (45) +6
through_route 29.3% (88) → 25% (75) -13
loop_member 17.7% (53) → 15% (45) -8
junction 23.3% (70) → 18% (54) -16
hub 4.0% (12) → 7% (21) +9
Strategy:
Pass A — Promote junctions→hubs: add edges to degree-4 junctions in core/inner
to push them to degree 5+. Target: +9 hubs.
Pass B — Increase dead_ends: remove non-bridge edges from degree-2+ nodes
in deep_frontier/outer to reduce their degree to 1. Target: +22 dead_ends.
Pass C — Reduce loop_members and through_routes: remove non-bridge edges
from loop_member and through_route nodes in frontier sectors.
Pass D — Final cleanup: reduce remaining junctions by stripping edges.
Constraints:
- Graph must remain fully connected (checked after each pass)
- S-001 (Gateway): degree=4 locked, gate_topology="hub", aperture_count=5
- Max 8 apertures per node (D-095)
- Targets are guidelines (±2-3%), not hard constraints
Standard library only. No external dependencies.
"""
from __future__ import annotations
import json
from collections import defaultdict, deque
from pathlib import Path
from typing import Optional
REPO_ROOT = Path(__file__).parent.parent
INPUT_JSON = REPO_ROOT / "docs" / "design" / "star-map.json"
OUTPUT_JSON = INPUT_JSON
TARGETS = {
"dead_end": 0.20,
"spur_end": 0.15,
"through_route": 0.25,
"loop_member": 0.15,
"junction": 0.18,
"hub": 0.07,
}
GATEWAY_ID = "S-001"
# ── Graph helpers ──────────────────────────────────────────────────────────────
def build_adjacency(edges: list) -> dict[str, set[str]]:
adj: dict[str, set[str]] = defaultdict(set)
for e in edges:
a, b = e[0], e[1]
adj[a].add(b)
adj[b].add(a)
return adj
def is_connected(all_nodes: list[str], adj: dict[str, set[str]]) -> bool:
if not all_nodes:
return True
start = all_nodes[0]
visited = {start}
queue = deque([start])
while queue:
cur = queue.popleft()
for nb in adj.get(cur, set()):
if nb not in visited:
visited.add(nb)
queue.append(nb)
return len(visited) == len(all_nodes)
def find_bridges(all_nodes: list[str], adj: dict[str, set[str]]) -> set[frozenset]:
"""Iterative Tarjan bridge-finding. Returns set of frozenset({u, v})."""
n = len(all_nodes)
idx_map = {v: i for i, v in enumerate(all_nodes)}
disc = [-1] * n
low = [-1] * n
timer = [0]
bridges: set[frozenset] = set()
for start_node in all_nodes:
si = idx_map[start_node]
if disc[si] != -1:
continue
nbrs_start = sorted(adj.get(start_node, set()))
stack: list[tuple[str, Optional[str], list[str], int]] = [
(start_node, None, nbrs_start, 0)
]
disc[si] = low[si] = timer[0]
timer[0] += 1
while stack:
node, par, nbr_list, ni = stack[-1]
node_i = idx_map[node]
if ni < len(nbr_list):
nb = nbr_list[ni]
stack[-1] = (node, par, nbr_list, ni + 1)
nb_i = idx_map[nb]
if disc[nb_i] == -1:
disc[nb_i] = low[nb_i] = timer[0]
timer[0] += 1
nb_nbrs = sorted(adj.get(nb, set()))
stack.append((nb, node, nb_nbrs, 0))
elif nb != par:
low[node_i] = min(low[node_i], disc[nb_i])
else:
stack.pop()
if par is not None:
par_i = idx_map[par]
low[par_i] = min(low[par_i], low[node_i])
if low[node_i] > disc[par_i]:
bridges.add(frozenset([par, node]))
return bridges
def find_cycle_members(all_nodes: list[str], adj: dict[str, set[str]]) -> set[str]:
bridge_set = find_bridges(all_nodes, adj)
on_cycle: set[str] = set()
for node in all_nodes:
neighbors = adj.get(node, set())
if len(neighbors) < 2:
continue
non_bridge_count = sum(
1 for nb in neighbors
if frozenset([node, nb]) not in bridge_set
)
if non_bridge_count > 0:
on_cycle.add(node)
return on_cycle
def classify_node(
node_id: str,
adj: dict[str, set[str]],
cycle_members: set[str],
is_gateway: bool,
) -> str:
if is_gateway:
return "hub"
deg = len(adj.get(node_id, set()))
if deg <= 1:
return "dead_end"
if deg == 2:
if node_id in cycle_members:
return "loop_member"
neighbors = list(adj[node_id])
nb_degs = [len(adj.get(nb, set())) for nb in neighbors]
if any(d >= 3 for d in nb_degs):
return "spur_end"
return "through_route"
if deg <= 4:
return "junction"
return "hub"
def reclassify_all(nodes: list[dict], adj: dict[str, set[str]]) -> None:
all_ids = [n["system_id"] for n in nodes]
cycle_members = find_cycle_members(all_ids, adj)
for n in nodes:
nid = n["system_id"]
is_gw = n.get("is_gateway", False)
topo = classify_node(nid, adj, cycle_members, is_gw)
n["gate_topology"] = topo
deg = len(adj.get(nid, set()))
n["gate_connections"] = deg
if is_gw:
n["aperture_count"] = max(5, deg) # +1 for dormant Sol aperture
else:
n["aperture_count"] = deg
def count_topology(nodes: list[dict], adj: dict[str, set[str]]) -> dict[str, int]:
all_ids = [n["system_id"] for n in nodes]
cycle_members = find_cycle_members(all_ids, adj)
counts: dict[str, int] = defaultdict(int)
for n in nodes:
t = classify_node(n["system_id"], adj, cycle_members, n.get("is_gateway", False))
counts[t] += 1
return dict(counts)
def remove_edge(a: str, b: str, adj: dict[str, set[str]], edges: list) -> None:
adj[a].discard(b)
adj[b].discard(a)
edges[:] = [
e for e in edges
if not ((e[0] == a and e[1] == b) or (e[0] == b and e[1] == a))
]
def add_edge(a: str, b: str, adj: dict[str, set[str]], edges: list) -> None:
adj[a].add(b)
adj[b].add(a)
edges.append([a, b])
def print_distribution(counts: dict[str, int], total: int, label: str) -> None:
print(f"\n {label}:")
order = ["dead_end", "spur_end", "through_route", "loop_member", "junction", "hub"]
for t in order:
c = counts.get(t, 0)
pct = c / total * 100
tgt = TARGETS.get(t, 0) * 100
delta = pct - tgt
flag = " <<<" if abs(delta) > 4 else ""
print(f" {t:<14} {c:>4} ({pct:5.1f}%) target={tgt:.0f}% delta={delta:+.1f}%{flag}")
# ── Pass A: Promote junctions to hubs by adding edges ─────────────────────────
def promote_junctions_to_hubs(
nodes: list[dict],
edges: list,
adj: dict[str, set[str]],
target_hub_count: int,
) -> int:
"""
Find degree-4 junctions in core/north_reach/east_reach/south_reach/west_reach
inner bands, and add edges to push them to degree 5 (hub).
Strategy: for each candidate hub-target, find another degree-3 or degree-4
node in the same sector that is NOT already connected to it, with BFS
distance 2-6. Prefer nodes whose degree would become 4 (stay junction)
rather than going to 5 themselves.
Returns number of hub promotions achieved.
"""
node_map = {n["system_id"]: n for n in nodes}
all_ids = [n["system_id"] for n in nodes]
edges_set = {frozenset(e[:2]) for e in edges}
# Count current hubs
cycle_members = find_cycle_members(all_ids, adj)
current_hubs = sum(
1 for n in nodes
if classify_node(n["system_id"], adj, cycle_members, n.get("is_gateway", False)) == "hub"
)
hubs_needed = target_hub_count - current_hubs
if hubs_needed <= 0:
print(f" Pass A: already at {current_hubs} hubs, target={target_hub_count}. Skipping.")
return 0
print(f" Pass A: promoting junctions to hubs. Need {hubs_needed} more hubs.")
promoted = 0
# Priority sectors for hub promotion
priority_sectors = {"core", "north_reach", "east_reach", "south_reach", "west_reach"}
# Priority bands: inner > mid > outer
band_rank = {"inner": 3, "core": 3, "mid": 2, "outer": 1}
# Find all degree-4 junctions in priority sectors (potential hub candidates)
candidates = []
for n in nodes:
nid = n["system_id"]
if nid == GATEWAY_ID:
continue
deg = len(adj.get(nid, set()))
if deg != 4:
continue
sector = n.get("geographic_sector", "")
if sector not in priority_sectors:
continue
band = n.get("geographic_band", "")
br = band_rank.get(band, 0)
candidates.append((nid, sector, br, deg))
# Sort by band rank desc (prefer inner/core band nodes)
candidates.sort(key=lambda x: -x[2])
for (cand_id, cand_sector, cand_br, _) in candidates:
if promoted >= hubs_needed:
break
# Find a suitable neighbor to connect to
# Must be: same sector, not already connected, degree <= 4 (won't become hub itself),
# not gateway, BFS distance 2-6
current_deg = len(adj.get(cand_id, set()))
if current_deg >= 5:
# Already became a hub from a previous promotion
continue
best_partner = None
best_partner_score = -999
for n2 in nodes:
n2id = n2["system_id"]
if n2id == GATEWAY_ID or n2id == cand_id:
continue
if frozenset([cand_id, n2id]) in edges_set:
continue
deg2 = len(adj.get(n2id, set()))
# Don't connect if it would push n2 over 8 apertures
if deg2 >= 8:
continue
# Prefer same sector, allow adjacent sectors
sector2 = n2.get("geographic_sector", "")
if sector2 != cand_sector:
continue
band2 = n2.get("geographic_band", "")
# Prefer connecting to a junction (deg 3-4) so it stays junction
# rather than becoming a hub itself
if deg2 >= 5:
continue # skip — would just inflate an existing hub
# BFS distance check — avoid trivially short connections
# (we don't want to make a multi-edge or a triangle that kills topology)
# We do a lightweight BFS for this check
bfs_dist = _bfs_dist(adj, cand_id, n2id)
if bfs_dist < 2 or bfs_dist > 8:
continue
# Score: prefer closer BFS distance (but not adjacent), prefer same band
br2 = band_rank.get(band2, 0)
score = br2 * 10 - abs(bfs_dist - 3)
if score > best_partner_score:
best_partner_score = score
best_partner = n2id
if best_partner is None:
print(f" {cand_id}: no suitable partner found, skipping")
continue
# Add the edge
add_edge(cand_id, best_partner, adj, edges)
edges_set.add(frozenset([cand_id, best_partner]))
promoted += 1
new_deg = len(adj.get(cand_id, set()))
new_deg2 = len(adj.get(best_partner, set()))
print(f" +edge {cand_id}(deg {new_deg}) <-> {best_partner}(deg {new_deg2})")
assert is_connected(all_ids, adj), "ERROR: Disconnected after Pass A!"
return promoted
def _bfs_dist(adj: dict[str, set[str]], start: str, end: str) -> int:
if start == end:
return 0
visited = {start}
queue = deque([(start, 0)])
while queue:
node, dist = queue.popleft()
if dist >= 9:
return 9999
for nb in adj.get(node, set()):
if nb == end:
return dist + 1
if nb not in visited:
visited.add(nb)
queue.append((nb, dist + 1))
return 9999
# ── Pass B: Create dead_ends by removing edges ────────────────────────────────
def create_dead_ends(
nodes: list[dict],
edges: list,
adj: dict[str, set[str]],
target_dead_end_count: int,
all_ids: list[str],
) -> int:
"""
Remove edges to increase dead_end count toward target.
For each edge removal, we look for a degree-2 node in deep_frontier/outer
where one of its incident edges is NOT a bridge AND removing it leaves
that node at degree 1 (dead_end). We do this carefully:
- The node being demoted: must have degree 2 now (after removal: degree 1 = dead_end)
- The removed edge must NOT be a bridge (so graph stays connected)
OR: it IS a bridge but the other endpoint has degree >= 3 so removing it
just splits off a dead_end leaf, which is fine (the leaf stays connected
to the rest via its remaining edge... wait, no — a bridge removal always
disconnects). So we must only remove non-bridges OR handle the special
case where the node being made dead_end has degree 2 and removing ONE
edge keeps the remaining edge still connecting it to the graph.
Actually: for a degree-2 node, BOTH its edges are bridges (removing either
disconnects the portion of the graph accessible only through that node's
chain). So we need a different approach:
For a degree-3 node: it has 3 edges. If at least one is NOT a bridge,
we can remove it — the node drops to degree 2 (becomes spur_end/through_route).
That's not a dead_end directly.
Better approach: find degree-2 nodes (spur_ends or through_routes) where
one edge IS technically "safe" to remove — meaning the other end of that
edge has degree >= 3, so after removal the graph remains connected
(the degree-2 node becomes a dead_end hanging off its one remaining neighbor,
which still has degree >= 2 connecting it to the rest of the graph).
Wait — if (A-B-C) and B has degree 2, edge A-B and B-C both connect B.
Removing A-B: B becomes dead_end (degree 1, connected via B-C). A drops by 1.
The graph remains connected AS LONG AS A is still connected to the rest.
A is connected to the rest via its other edges (since A had degree >= 2 and
we only removed one edge — A must have degree >= 2 so after removal A has
degree >= 1). If A had degree exactly 2, it now has degree 1 — and A is now
a dead_end too! That might be acceptable, but let's prefer A has degree >= 3
so A stays as junction/hub.
So the rule: pick a node B with degree 2. Find which of B's two neighbors
(call it A) has degree >= 3. Remove edge A-B. B becomes dead_end, A stays
junction/hub. Graph remains connected.
This always works and never disconnects the graph.
Returns: number of dead_ends created.
"""
node_map = {n["system_id"]: n for n in nodes}
# Priority sectors: deep_frontier, then outer band
def node_priority(nid: str) -> int:
n = node_map.get(nid, {})
sector = n.get("geographic_sector", "")
band = n.get("geographic_band", "")
if sector == "deep_frontier":
return 3
if band == "outer":
return 2
if band == "mid":
return 1
return 0
created = 0
# Count current dead_ends
cycle_members = find_cycle_members(all_ids, adj)
current_de = sum(
1 for n in nodes
if classify_node(n["system_id"], adj, cycle_members, n.get("is_gateway", False)) == "dead_end"
)
needed = target_dead_end_count - current_de
if needed <= 0:
print(f" Pass B: already at {current_de} dead_ends, target={target_dead_end_count}. Skipping.")
return 0
print(f" Pass B: creating dead_ends. Need {needed} more.")
max_iterations = needed * 8 # safety limit
iteration = 0
skipped = set() # edges that would disconnect the graph
while created < needed and iteration < max_iterations:
iteration += 1
# Find degree-2 nodes (not gateway) with at least one neighbor of degree >= 3
best_node = None
best_removable_neighbor = None
best_score = -1
for n in nodes:
nid = n["system_id"]
if nid == GATEWAY_ID:
continue
deg = len(adj.get(nid, set()))
if deg != 2:
continue
# Check neighbors — find one with deg >= 3 to remove edge toward
neighbors = list(adj[nid])
# The node being demoted (nid) will keep its OTHER neighbor as its
# sole connection. That other neighbor must have degree >= 2 (i.e.,
# it has at least one other connection besides nid) so the remaining
# B-C subgraph stays reachable from the rest of the graph.
for nb in neighbors:
nb_deg = len(adj.get(nb, set()))
if nb_deg < 3 or nb == GATEWAY_ID:
continue
if (nid, nb) in skipped or (nb, nid) in skipped:
continue
# The remaining neighbor (the one we do NOT remove) must have deg >= 2
other_neighbors = [x for x in neighbors if x != nb]
if not other_neighbors:
continue
other_nb = other_neighbors[0]
other_deg = len(adj.get(other_nb, set()))
if other_deg < 2:
# other_nb is already a dead_end; removing nb-nid would leave
# nid and other_nb disconnected from the rest of the graph.
continue
# Safe: remove edge nid-nb. nid becomes dead_end, graph stays connected.
score = node_priority(nid) * 10 + node_priority(nb) + nb_deg
if score > best_score:
best_score = score
best_node = nid
best_removable_neighbor = nb
if best_node is None:
print(f" No more suitable degree-2 nodes found after {created} dead_ends created.")
break
remove_edge(best_node, best_removable_neighbor, adj, edges)
# Check connectivity after every removal — rollback if disconnected
if not is_connected(all_ids, adj):
# Rollback
adj[best_node].add(best_removable_neighbor)
adj[best_removable_neighbor].add(best_node)
edges.append([best_node, best_removable_neighbor])
skipped.add((best_node, best_removable_neighbor))
continue
created += 1
new_deg = len(adj.get(best_node, set()))
print(f" -edge {best_node}(now deg {new_deg}) <- {best_removable_neighbor}")
return created
# ── Pass C: Reduce loop_members and through_routes ────────────────────────────
def reduce_degree2_excess(
nodes: list[dict],
edges: list,
adj: dict[str, set[str]],
all_ids: list[str],
target_lm: int,
target_tr: int,
) -> int:
"""
Reduce loop_member and through_route counts toward targets.
Method: find loop_member or through_route (degree-2) nodes in frontier/outer
sectors where one neighbor has degree >= 3. Remove the edge to that
high-degree neighbor, converting the degree-2 node to a dead_end.
This simultaneously reduces loop_member/through_route AND increases dead_end.
We stop when both LM and TR are within tolerance of targets, or we've used
our budget.
Note: we may already be creating dead_ends in Pass B, so this pass focuses
on reducing excess degree-2 nodes that are loop_members or through_routes.
"""
node_map = {n["system_id"]: n for n in nodes}
total = len(nodes)
tolerance = int(total * 0.03)
def node_priority(nid: str) -> int:
n = node_map.get(nid, {})
sector = n.get("geographic_sector", "")
band = n.get("geographic_band", "")
if sector == "deep_frontier":
return 3
if band == "outer":
return 2
if band == "mid":
return 1
return 0
removed = 0
max_budget = 50 # enough budget for up to 50 reductions
for _ in range(max_budget):
cycle_members = find_cycle_members(all_ids, adj)
counts = {
t: sum(1 for n in nodes if classify_node(
n["system_id"], adj, cycle_members, n.get("is_gateway", False)
) == t)
for t in ["loop_member", "through_route", "dead_end"]
}
lm_now = counts["loop_member"]
tr_now = counts["through_route"]
lm_ok = lm_now <= target_lm + tolerance
tr_ok = tr_now <= target_tr + tolerance
if lm_ok and tr_ok:
print(f" Pass C done: LM={lm_now} TR={tr_now} within tolerance.")
break
# Find a good candidate to demote
best_node = None
best_nb = None
best_score = -1
for n in nodes:
nid = n["system_id"]
if nid == GATEWAY_ID:
continue
deg = len(adj.get(nid, set()))
if deg != 2:
continue
# Is this node a loop_member or through_route?
topo = classify_node(nid, adj, cycle_members, n.get("is_gateway", False))
if topo not in ("loop_member", "through_route"):
continue
# Check if we still need to reduce this type
if topo == "loop_member" and lm_ok:
continue
if topo == "through_route" and tr_ok:
continue
# Find a neighbor with degree >= 3, AND the OTHER neighbor still
# has degree >= 2 after removal (so nid-other stays connected to graph)
neighbors_list = list(adj[nid])
for nb in neighbors_list:
nb_deg = len(adj.get(nb, set()))
if nb_deg < 3 or nb == GATEWAY_ID:
continue
others = [x for x in neighbors_list if x != nb]
if not others:
continue
other_deg = len(adj.get(others[0], set()))
if other_deg < 2:
continue # would strand nid with only a dead_end neighbor
score = node_priority(nid) * 10 + nb_deg
if score > best_score:
best_score = score
best_node = nid
best_nb = nb
if best_node is None:
print(f" Pass C: no more candidates (LM={lm_now}, TR={tr_now}).")
break
remove_edge(best_node, best_nb, adj, edges)
if not is_connected(all_ids, adj):
# Rollback
adj[best_node].add(best_nb)
adj[best_nb].add(best_node)
edges.append([best_node, best_nb])
continue
removed += 1
print(f" -edge {best_node} <- {best_nb}")
return removed
# ── Pass D: Reduce junction count ─────────────────────────────────────────────
def reduce_junctions(
nodes: list[dict],
edges: list,
adj: dict[str, set[str]],
all_ids: list[str],
target_junction_count: int,
) -> int:
"""
Reduce junction count by removing edges from degree-3 junctions in
frontier/outer sectors, demoting them to degree-2 (spur_end/through_route).
Only removes non-bridge edges (to preserve connectivity).
"""
node_map = {n["system_id"]: n for n in nodes}
total = len(nodes)
tolerance = int(total * 0.03)
def node_priority(nid: str) -> int:
n = node_map.get(nid, {})
sector = n.get("geographic_sector", "")
band = n.get("geographic_band", "")
if sector == "deep_frontier":
return 3
if band == "outer":
return 2
if band == "mid":
return 1
return 0
removed = 0
max_budget = 50
for _ in range(max_budget):
cycle_members = find_cycle_members(all_ids, adj)
counts = {
t: sum(1 for n in nodes if classify_node(
n["system_id"], adj, cycle_members, n.get("is_gateway", False)
) == t)
for t in ["junction"]
}
j_now = counts["junction"]
if j_now <= target_junction_count + tolerance:
print(f" Pass D done: junction={j_now} within tolerance (target={target_junction_count}).")
break
# Find non-bridge edges adjacent to degree-3 junctions in frontier/outer
bridge_set = find_bridges(all_ids, adj)
best_a = None
best_b = None
best_score = -1
for e in edges:
a, b = e[0], e[1]
if frozenset([a, b]) in bridge_set:
continue
if a == GATEWAY_ID or b == GATEWAY_ID:
continue
da = len(adj.get(a, set()))
db = len(adj.get(b, set()))
# At least one endpoint should be a degree-3 junction in frontier/outer
pa = node_priority(a)
pb = node_priority(b)
# Only remove if at least one is degree 3 (demotes to 2) in a priority area
# and the other won't drop below 2 (we don't want to accidentally make more dead_ends)
if da == 3 and pa >= 1 and db >= 3:
score = pa * 10 + pb + db # prefer higher priority, higher degree on the other end
if score > best_score:
best_score = score
best_a, best_b = a, b
elif db == 3 and pb >= 1 and da >= 3:
score = pb * 10 + pa + da
if score > best_score:
best_score = score
best_a, best_b = a, b
if best_a is None:
print(f" Pass D: no suitable non-bridge degree-3 edges found (junction={j_now}).")
break
remove_edge(best_a, best_b, adj, edges)
if not is_connected(all_ids, adj):
adj[best_a].add(best_b)
adj[best_b].add(best_a)
edges.append([best_a, best_b])
continue
removed += 1
da_new = len(adj.get(best_a, set()))
db_new = len(adj.get(best_b, set()))
print(f" -edge {best_a}(now {da_new}) <-> {best_b}(now {db_new})")
return removed
# ── Main ───────────────────────────────────────────────────────────────────────
def main() -> None:
print(f"Reading {INPUT_JSON} ...")
with open(INPUT_JSON) as f:
data = json.load(f)
nodes: list[dict] = data["nodes"]
edges: list = data["edges"]
total = len(nodes)
print(f" {total} nodes, {len(edges)} edges")
sample = edges[0]
assert isinstance(sample, list) and len(sample) == 2, \
f"Unexpected edge format: {sample!r}"
adj = build_adjacency(edges)
all_ids = [n["system_id"] for n in nodes]
# Verify gateway constraints
gw_deg = len(adj.get(GATEWAY_ID, set()))
print(f" Gateway {GATEWAY_ID}: degree={gw_deg}")
assert gw_deg == 4, f"Gateway degree should be 4, got {gw_deg}"
assert is_connected(all_ids, adj), "ERROR: Input graph is not connected!"
print(" Connectivity: OK")
before_counts = count_topology(nodes, adj)
print_distribution(before_counts, total, "BEFORE")
# Targets (rounded)
target_hubs = int(TARGETS["hub"] * total) # 21
target_dead = int(TARGETS["dead_end"] * total) # 60
target_lm = int(TARGETS["loop_member"] * total) # 45
target_tr = int(TARGETS["through_route"] * total) # 75
target_junction = int(TARGETS["junction"] * total) # 54
print(f"\n Targets: hubs={target_hubs} dead_ends={target_dead} "
f"loop_member={target_lm} through_route={target_tr} junction={target_junction}")
# ── Pass A: Promote junctions to hubs ────────────────────────────────────
print("\n--- Pass A: Promote junctions to hubs ---")
a_promoted = promote_junctions_to_hubs(nodes, edges, adj, target_hubs)
reclassify_all(nodes, adj)
after_a = count_topology(nodes, adj)
print_distribution(after_a, total, "After Pass A")
print(f" Edges: {len(edges)}")
# ── Pass B: Create dead_ends ──────────────────────────────────────────────
print("\n--- Pass B: Create dead_ends ---")
b_created = create_dead_ends(nodes, edges, adj, target_dead, all_ids)
reclassify_all(nodes, adj)
after_b = count_topology(nodes, adj)
print_distribution(after_b, total, "After Pass B")
print(f" Edges: {len(edges)}")
# ── Pass C: Reduce loop_member and through_route excess ───────────────────
print("\n--- Pass C: Reduce loop_member / through_route excess ---")
c_removed = reduce_degree2_excess(nodes, edges, adj, all_ids, target_lm, target_tr)
reclassify_all(nodes, adj)
after_c = count_topology(nodes, adj)
print_distribution(after_c, total, "After Pass C")
print(f" Edges: {len(edges)} (Pass C removed {c_removed})")
# ── Pass D: Reduce junction count ─────────────────────────────────────────
print("\n--- Pass D: Reduce junctions ---")
d_removed = reduce_junctions(nodes, edges, adj, all_ids, target_junction)
reclassify_all(nodes, adj)
after_d = count_topology(nodes, adj)
print_distribution(after_d, total, "After Pass D")
print(f" Edges: {len(edges)} (Pass D removed {d_removed})")
# ── Pass E: Second dead_end pass if still short ────────────────────────────
de_now = count_topology(nodes, adj).get("dead_end", 0)
tolerance = int(total * 0.03) # 3% = 9 systems
if de_now < target_dead - tolerance:
print(f"\n--- Pass E: Second dead_end pass (have {de_now}, need {target_dead}) ---")
e_created = create_dead_ends(nodes, edges, adj, target_dead, all_ids)
reclassify_all(nodes, adj)
after_e = count_topology(nodes, adj)
print_distribution(after_e, total, "After Pass E")
print(f" Edges: {len(edges)} (Pass E created {e_created} more dead_ends)")
# ── Pass F: Second junction reduction pass if still high ───────────────────
j_now = count_topology(nodes, adj).get("junction", 0)
if j_now > target_junction + tolerance:
print(f"\n--- Pass F: Second junction reduction (have {j_now}, target {target_junction}) ---")
f_removed = reduce_junctions(nodes, edges, adj, all_ids, target_junction)
reclassify_all(nodes, adj)
after_f = count_topology(nodes, adj)
print_distribution(after_f, total, "After Pass F")
print(f" Edges: {len(edges)} (Pass F removed {f_removed})")
# ── Final reclassify and aperture update ──────────────────────────────────
reclassify_all(nodes, adj)
# ── Verify gateway constraints ────────────────────────────────────────────
gw_node = next(n for n in nodes if n["system_id"] == GATEWAY_ID)
assert gw_node["gate_topology"] == "hub", "Gateway topology changed!"
assert gw_node["aperture_count"] == 5, f"Gateway aperture_count={gw_node['aperture_count']} (should be 5)"
assert len(adj[GATEWAY_ID]) == 4, f"Gateway degree={len(adj[GATEWAY_ID])} (should be 4)"
print("\n Gateway constraint check: OK (degree=4, aperture=5, topology=hub)")
# ── Verify no aperture violations ────────────────────────────────────────
violations = [
n for n in nodes
if n.get("aperture_count", 0) > 8
]
if violations:
print(f"\n WARNING: {len(violations)} nodes exceed 8 apertures!")
for v in violations:
print(f" {v['system_id']}: aperture={v['aperture_count']}")
else:
print(" Aperture max-8 constraint: OK")
# ── Update metadata ───────────────────────────────────────────────────────
data["_meta"]["edge_count"] = len(edges)
data["_meta"]["tuned"] = "2026-03-13"
data["_meta"]["tune_note"] = (
"Topology tuning: promoted junctions to hubs, created dead_ends, "
"reduced loop_member/through_route/junction excess. Reclassified all nodes."
)
# ── Write output ──────────────────────────────────────────────────────────
print(f"\n Writing {OUTPUT_JSON} ...")
with open(OUTPUT_JSON, "w") as f:
json.dump(data, f, indent=2)
print(" Written.")
# ── Final summary ─────────────────────────────────────────────────────────
print("\n" + "=" * 60)
print("FINAL TOPOLOGY SUMMARY")
print("=" * 60)
final = count_topology(nodes, adj)
print_distribution(final, total, "Final distribution")
print(f"\n Nodes: {total} Edges: {len(edges)}")
print("\n Breakdown by sector:")
st: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for n in nodes:
st[n.get("geographic_sector", "?")][n["gate_topology"]] += 1
for sec in sorted(st.keys()):
sd = st[sec]
n_in = sum(sd.values())
parts = " ".join(f"{t}={c}" for t, c in sorted(sd.items()))
print(f" {sec:<15} n={n_in:>3} {parts}")
print("\n Core sector final degrees:")
core_final = [
(n["system_id"], len(adj.get(n["system_id"], set())), n["gate_topology"])
for n in nodes if n.get("geographic_sector") == "core"
]
core_final.sort(key=lambda x: -x[1])
for nid, deg, topo in core_final:
print(f" {nid}: deg={deg} {topo}")
if __name__ == "__main__":
main()