File size: 5,098 Bytes
4a8ceaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
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()