From 1bd8f0f8b9de952dc21d33c5a608b8075bb29efa Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 10:10:51 -0400 Subject: [PATCH] feat(infra-net): reconcile project .infra.yaml against mesh-hosts.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New bin/infra-net walks every project .infra.yaml (convention:infra_manifest), validates schema + host∈mesh-hosts (alias-aware) + port collisions, prints the live infra-net and writes data/infra-net.json (gitignored, non-destructive — does not touch the services map). Caught prospector's stale host name on first run. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + bin/infra-net | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100755 bin/infra-net diff --git a/.gitignore b/.gitignore index 85809e0..4532920 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ __pycache__/ # Volatile discovered state (current LAN IPs) — written by the daemon, not source. data/lan-state.json data/agent-status.json +data/.tray-disabled tray/.venv/ +data/infra-net.json diff --git a/bin/infra-net b/bin/infra-net new file mode 100755 index 0000000..586195e --- /dev/null +++ b/bin/infra-net @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""infra-net — reconcile every project's .infra.yaml against mesh-hosts.json. + +Walks ~/Code for .infra.yaml manifests (convention:infra_manifest), validates each +against the convention's JSON Schema, checks that `service.host` is a known +mesh-hosts.json host, flags per-host port collisions, prints the live infra-net +table, and writes the reconciled inventory to data/infra-net.json (non-destructive +— it does NOT overwrite mesh-hosts.json's services map, so existing consumers are +untouched). + + net-tools/bin/infra-net # print + write inventory + net-tools/bin/infra-net --check # validate only, non-zero exit on problems +""" +import glob +import json +import os +import sys + +HOME = os.path.expanduser("~") +CODE = os.path.join(HOME, "Code") +NET_TOOLS = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +MESH = os.path.join(NET_TOOLS, "data", "mesh-hosts.json") +SCHEMA_CONV = os.path.join(CODE, "@conventions", "programming_general", "infra_manifest.yaml") +OUT = os.path.join(NET_TOOLS, "data", "infra-net.json") + +# Where deployable projects live (each may carry a root .infra.yaml). +ROOTS = [ + os.path.join(CODE, "@applications", "*", ".infra.yaml"), + os.path.join(CODE, "@projects", "@cocottetech", "@platform", "codebase", "@features", "*", ".infra.yaml"), + os.path.join(CODE, "@projects", "@cocottetech", ".infra.yaml"), + os.path.join(CODE, "@projects", "@magic-civilization", ".infra.yaml"), +] + + +def main() -> int: + try: + import yaml + import jsonschema + except ImportError as e: + print(f"infra-net needs pyyaml + jsonschema ({e})", file=sys.stderr) + return 2 + + check_only = "--check" in sys.argv + + mesh = json.load(open(MESH)) + # Accept canonical host names AND their aliases (e.g. lime == lilith-store-backend). + hosts = {h["name"] for h in mesh.get("hosts", [])} + hosts |= {a for h in mesh.get("hosts", []) for a in h.get("aliases", [])} + schema = yaml.safe_load(open(SCHEMA_CONV))["providesFile"]["schema"] + + manifests = [] + for pat in ROOTS: + manifests.extend(sorted(glob.glob(pat))) + + problems = [] + seen_ports: dict[tuple[str, int], str] = {} + rows = [] + for path in manifests: + rel = path.replace(CODE + "/", "") + try: + m = yaml.safe_load(open(path)) + jsonschema.validate(m, schema) + except Exception as e: # noqa: BLE001 — surface any parse/validation error + problems.append(f"{rel}: {getattr(e, 'message', e)}") + continue + svc = m.get("service", {}) or {} + host, port = svc.get("host"), svc.get("port") + if host and host not in hosts: + problems.append(f"{rel}: service.host '{host}' not in mesh-hosts.json {sorted(hosts)}") + if host and port is not None: + key = (host, port) + if key in seen_ports: + problems.append(f"port collision on {host}:{port} — {m['project']} vs {seen_ports[key]}") + else: + seen_ports[key] = m["project"] + db = m.get("database", {}) or {} + rows.append({ + "project": m["project"], + "provider": m.get("provider"), + "host": host, + "port": port, + "db": (f"{db.get('name')}@{db.get('cluster')}" if db else None), + "depends_on": m.get("depends_on", []), + "source": rel, + }) + + rows.sort(key=lambda r: (r["host"] or "~", r["port"] or 0)) + w = max([len(r["project"]) for r in rows] + [7]) + print(f"\n infra-net — {len(rows)} services across {len(hosts)} hosts\n") + print(f" {'PROJECT'.ljust(w)} {'HOST'.ljust(8)} {'PORT'.ljust(5)} {'PROVIDER'.ljust(12)} DB / DEPS") + for r in rows: + deps = (" deps:" + ",".join(r["depends_on"])) if r["depends_on"] else "" + dbp = (r["db"] or "") + deps + print(f" {r['project'].ljust(w)} {str(r['host']).ljust(8)} {str(r['port'] or '').ljust(5)} {str(r['provider']).ljust(12)} {dbp}") + + if problems: + print("\n PROBLEMS:") + for p in problems: + print(f" ✗ {p}") + else: + print("\n ✓ all manifests valid; hosts known; no port collisions") + + if not check_only: + json.dump({"_generated_by": "net-tools/bin/infra-net", "_source": "project .infra.yaml + mesh-hosts.json", + "services": rows, "problems": problems}, open(OUT, "w"), indent=2) + print(f"\n wrote {os.path.relpath(OUT, NET_TOOLS)}") + + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main())