""" Emit `design/atp_data.js` from `atp/seed.py` so the Agent-view React UI reads one canonical, referentially-consistent data blob. Output shape (see docs/ATP.md §3 + §7.1): window.ATP_DATA = { STANDARD, INFRA, LAYERS, AGENTS, CERTS, EVIDENCE, POLICIES, TIERS, RL, PLAYBOOKS, MARKETPLACE, VAULT, COMPOSE, EXPERT_REQUESTS, agentById, certById, evidenceById, certsForAgent, labelFor, matchExperts }; The JSON payload is serialised with `json.dumps`; the helper functions are appended as plain JS text inside the same IIFE. Run: python3 scripts/build_atp_data.py """ from __future__ import annotations import json import sys from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from atp.seed import build_seed # noqa: E402 OUT_PATH = PROJECT_ROOT / "design" / "atp_data.js" JS_TEMPLATE = """// Generated by scripts/build_atp_data.py — do not edit by hand. // // window.ATP_DATA — the ATP (Agent University) data contract. See docs/ATP.md // §3 + §7.1. Every certId / evidenceId / policyId / agentId referenced anywhere // in this blob resolves; awards <-> AGENTS.certIds are bidirectional. window.ATP_DATA = (() => {{ const DATA = {payload}; // ── indices ────────────────────────────────────────────────────────────── const _agents = {{}}; (DATA.AGENTS || []).forEach((a) => {{ _agents[a.id] = a; }}); const _certs = {{}}; (DATA.CERTS || []).forEach((c) => {{ _certs[c.id] = c; }}); const _ev = {{}}; (DATA.EVIDENCE || []).forEach((e) => {{ _ev[e.id] = e; }}); // ── helpers ────────────────────────────────────────────────────────────── function agentById(id) {{ return _agents[id] || null; }} function certById(id) {{ return _certs[id] || null; }} function evidenceById(id) {{ return _ev[id] || null; }} function certsForAgent(agentId) {{ const a = _agents[agentId]; return a ? (a.certIds || []).map((id) => _certs[id]).filter(Boolean) : []; }} function labelFor(certId) {{ const c = _certs[certId]; return c ? c.label : null; }} // matchExperts({{course, concentrationId, badgeIds, packId}}) -> // {{ exact:[agentId], partial:[{{agentId, covered:[str], missing:[str]}}] }} // exact = agent whose skills cover all track competencies AND holds the badge // certs AND (packId ? bound to the pack's policies : true). Partial ranked by // coverage. Revoked agents are excluded from supply. function matchExperts(sel) {{ sel = sel || {{}}; const badgeIds = sel.badgeIds || []; const packId = sel.packId || null; let comps = []; (DATA.COMPOSE && DATA.COMPOSE.majors ? DATA.COMPOSE.majors : []).forEach((m) => {{ if (sel.course != null && String(m.course) !== String(sel.course)) return; (m.concentrations || []).forEach((cc) => {{ if (sel.concentrationId && cc.id !== sel.concentrationId) return; comps = comps.concat(cc.competencies || []); }}); }}); comps = Array.from(new Set(comps)); let packPolicies = []; if (packId && DATA.COMPOSE && DATA.COMPOSE.packs) {{ const pk = DATA.COMPOSE.packs.find((p) => p.id === packId); if (pk) packPolicies = pk.policyIds || []; }} const exact = []; const partial = []; (DATA.AGENTS || []).forEach((a) => {{ if (a.status === "revoked") return; const skillNames = (a.skills || []).map((s) => s.name); const certIds = a.certIds || []; const policyIds = a.policyIds || []; const covered = comps.filter((c) => skillNames.includes(c)); const missing = comps.filter((c) => !skillNames.includes(c)); const holdsBadges = badgeIds.every((b) => certIds.includes(b)); const boundPack = !packId || packPolicies.every((p) => policyIds.includes(p)); if (comps.length && missing.length === 0 && holdsBadges && boundPack) {{ exact.push(a.id); }} else if (covered.length || badgeIds.some((b) => certIds.includes(b))) {{ partial.push({{ agentId: a.id, covered: covered, missing: missing }}); }} }}); partial.sort((x, y) => y.covered.length - x.covered.length); return {{ exact: exact, partial: partial }}; }} return Object.assign(DATA, {{ agentById, certById, evidenceById, certsForAgent, labelFor, matchExperts, }}); }})(); """ def main() -> None: data = build_seed() payload = json.dumps(data, indent=2, ensure_ascii=False) out = JS_TEMPLATE.format(payload=payload) OUT_PATH.write_text(out, encoding="utf-8") size_kb = len(out.encode("utf-8")) / 1024 print(f"[Done] wrote {OUT_PATH.relative_to(PROJECT_ROOT)} ({size_kb:.1f} KB)") if size_kb > 300: print(f" WARNING: exceeds 300KB budget ({size_kb:.1f} KB)") if __name__ == "__main__": main()