generated from coulomb/repo-seed
Some checks failed
validate-registry / validate (push) Has been cancelled
T05: tests/test_effective_config.py (6 tests) — order-independence, most-specific winner, no value/secret leak; wired into make validate + CI. T04: tools/blast_radius.py + make blast-radius — consumers, transitive dependents (cycle-safe), secret refs, fan-out risk band. T03: tools/config_graph.py + make graph/graph-query — emit config-typed edges to registry/indexes/graph.yaml (queryable by surface id); staleness check in the gate. WP-0004 finished (5/5). Read-first control-plane MVP complete: explain, graph, and blast-radius over the seeded surfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Config knowledge-graph edges (ATLAS-WP-0004-T03).
|
|
|
|
Express the config-typed relationships declared on surface entries as a
|
|
normalized, queryable edge list at `registry/indexes/graph.yaml`. config-atlas
|
|
owns the *config semantics* of each edge (consumed_by, overrides,
|
|
depends_on_secret, related_to); the State Hub owns topology/identity storage
|
|
(ecosystem-boundaries §2.5, repo-boundary). This tool therefore *emits* a
|
|
graph artifact the hub can ingest -- it does not build a separate graph store.
|
|
|
|
Modes:
|
|
python3 tools/config_graph.py # regenerate graph.yaml
|
|
python3 tools/config_graph.py --check # fail if graph.yaml stale
|
|
python3 tools/config_graph.py --surface <id> # edges touching a surface id
|
|
|
|
The emitted graph is queryable by surface id (both edge directions).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError as exc: # pragma: no cover
|
|
raise SystemExit(f"setup error: missing PyYAML ({exc}). pip install pyyaml")
|
|
|
|
from effective_config import ROOT, SURFACES_DIR, load_entry
|
|
|
|
GRAPH_PATH = ROOT / "registry" / "indexes" / "graph.yaml"
|
|
EDGE_TYPES = ["consumed_by", "overrides", "depends_on_secret", "related_to"]
|
|
|
|
|
|
def _secret_node(ref: str) -> str:
|
|
return f"secret:{ref}"
|
|
|
|
|
|
def build_graph() -> dict:
|
|
entries = {p.stem: load_entry(p) for p in sorted(SURFACES_DIR.glob("*.md"))}
|
|
nodes: dict[str, dict] = {}
|
|
edges: list[dict] = []
|
|
|
|
def node(nid: str, ntype: str, **attrs):
|
|
if nid not in nodes:
|
|
nodes[nid] = {"id": nid, "type": ntype, **{k: v for k, v in attrs.items() if v}}
|
|
|
|
for sid, e in entries.items():
|
|
node(sid, "surface", kind=e.get("kind"), owner=e.get("owner"))
|
|
rel = e.get("relations", {}) or {}
|
|
for svc in rel.get("consumed_by", []) or []:
|
|
node(svc, "service")
|
|
edges.append({"src": sid, "type": "consumed_by", "dst": svc})
|
|
for tgt in rel.get("overrides", []) or []:
|
|
node(tgt, "surface")
|
|
edges.append({"src": sid, "type": "overrides", "dst": tgt})
|
|
for sec in rel.get("depends_on_secret", []) or []:
|
|
node(_secret_node(sec), "secret")
|
|
edges.append({"src": sid, "type": "depends_on_secret", "dst": _secret_node(sec)})
|
|
for tgt in rel.get("related_to", []) or []:
|
|
node(tgt, "surface")
|
|
edges.append({"src": sid, "type": "related_to", "dst": tgt})
|
|
|
|
edges.sort(key=lambda x: (x["src"], x["type"], x["dst"]))
|
|
return {
|
|
"version": 1,
|
|
"generated_by": "tools/config_graph.py",
|
|
"note": "config-typed edges emitted from surface relations; ingest into the State Hub graph (it owns storage).",
|
|
"summary": {"nodes": len(nodes), "edges": len(edges)},
|
|
"nodes": [nodes[k] for k in sorted(nodes)],
|
|
"edges": edges,
|
|
}
|
|
|
|
|
|
def write_graph() -> dict:
|
|
graph = build_graph()
|
|
GRAPH_PATH.write_text(yaml.safe_dump(graph, sort_keys=False))
|
|
return graph
|
|
|
|
|
|
def edges_for(surface_id: str, graph: dict) -> list[dict]:
|
|
return [e for e in graph["edges"] if e["src"] == surface_id or e["dst"] == surface_id]
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if argv and argv[0] == "--surface":
|
|
if len(argv) != 2:
|
|
print(__doc__)
|
|
return 2
|
|
sid = argv[1]
|
|
graph = yaml.safe_load(GRAPH_PATH.read_text()) if GRAPH_PATH.exists() else build_graph()
|
|
hits = edges_for(sid, graph)
|
|
if not hits:
|
|
print(f"no edges for {sid}")
|
|
return 0
|
|
print(f"edges touching {sid}:")
|
|
for e in hits:
|
|
arrow = "->" if e["src"] == sid else "<-"
|
|
other = e["dst"] if e["src"] == sid else e["src"]
|
|
print(f" {arrow} {e['type']:18} {other}")
|
|
return 0
|
|
|
|
if argv and argv[0] == "--check":
|
|
current = GRAPH_PATH.read_text() if GRAPH_PATH.exists() else ""
|
|
expected = yaml.safe_dump(build_graph(), sort_keys=False)
|
|
if current != expected:
|
|
print("FAIL: registry/indexes/graph.yaml is stale — run `make graph`", file=sys.stderr)
|
|
return 1
|
|
print("OK: graph.yaml up to date")
|
|
return 0
|
|
|
|
g = write_graph()
|
|
print(f"wrote {GRAPH_PATH.relative_to(ROOT)}: {g['summary']['nodes']} nodes, {g['summary']['edges']} edges")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|