Spaces:
Sleeping
Sleeping
File size: 8,682 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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | #!/usr/bin/env python3
"""
Seed referential-integrity gate β docs/HARDENING.md Phase 5 CI check.
Exit-code wrapper around the internal-consistency invariants the ATP seed
promises (atp/seed.py docstring + docs/ATP.md Β§3):
* every certId / evidenceId / agentId / policyId / packId cross-reference
anywhere in the seed resolves to a defined record
* LAYERS.agentIds β AGENTS, bidirectional with AGENTS.level
* MARKETPLACE lists only licensable agents (and every licensable agent
is listed β ATP.md Β§3 seed content requirement)
* COMPOSE (majors / badges / packs) certIds + policyIds resolve
* EXPERT_REQUESTS statuses are valid + their cross-ids resolve
* blueprint section weights sum β 1 per cert
* item banks (atp/items/<certId>.json) cover every blueprint section of
their cert, and never name a section the blueprint doesn't have
* skillEdges reference skill ids defined on the same agent
Usage: python3 scripts/check_integrity.py
Exit 0 = all invariants hold; exit 1 = violations (each printed on stderr).
Stdlib-only on purpose (atp/seed.py is pure data) so CI needs no pip step.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from atp.seed import build_seed # noqa: E402
ITEMS_DIR = ROOT / "atp" / "items"
#: EXPERT_REQUESTS lifecycle states the UI knows how to render
#: (design/screens/atp-marketplace.jsx STATUS map).
VALID_REQUEST_STATUSES = {"matched", "gap", "training", "certifying", "listed"}
#: seed key name β registry it must resolve in (generic reference sweep)
REF_KEYS = {
"agentId": "agents",
"agentIds": "agents",
"matchedAgentId": "agents",
"resultAgentId": "agents",
"coveringAgentIds": "agents",
"boundAgentIds": "agents",
"bindsAgentIds": "agents",
"certId": "certs",
"certIds": "certs",
"badgeIds": "certs",
"evidenceIds": "evidence",
"policyIds": "policies",
"packId": "packs",
}
ERRORS: list[str] = []
def fail(msg: str) -> None:
ERRORS.append(msg)
def sweep(node, registries: dict[str, set], path: str = "$") -> None:
"""Recursively resolve every REF_KEYS reference in the seed.
Scalar reference keys may be None (e.g. EVIDENCE.certId for
training-interaction rows, EXPERT_REQUESTS.matchedAgentId while a
request is unfilled) β None means 'no reference', not a dangling one.
"""
if isinstance(node, dict):
for key, val in node.items():
here = f"{path}.{key}"
registry = REF_KEYS.get(key)
if registry is not None:
ids = registries[registry]
refs = val if isinstance(val, list) else [val]
for ref in refs:
if ref is None:
continue
if not isinstance(ref, str) or ref not in ids:
fail(f"{here}: dangling {registry} reference {ref!r}")
sweep(val, registries, here)
elif isinstance(node, list):
for i, item in enumerate(node):
sweep(item, registries, f"{path}[{i}]")
def check_layers(data: dict, agent_ids: set) -> None:
"""LAYERS.agentIds β AGENTS + level β layer membership is bidirectional."""
placed: dict[str, int] = {}
for layer in data["LAYERS"]:
for aid in layer["agentIds"]:
if aid not in agent_ids:
fail(f"LAYERS[{layer['id']}].agentIds: unknown agent {aid!r}")
elif aid in placed:
fail(f"agent {aid!r} appears in two layers "
f"(L{placed[aid]} and {layer['id']})")
else:
placed[aid] = layer["n"]
for agent in data["AGENTS"]:
if placed.get(agent["id"]) != agent["level"]:
fail(f"agent {agent['id']!r} has level {agent['level']} but sits "
f"in layer {placed.get(agent['id'])} (LAYERS.agentIds)")
def check_marketplace(data: dict) -> None:
"""Marketplace β licensable agents, both directions (ATP.md Β§3)."""
licensable = {a["id"] for a in data["AGENTS"]
if (a.get("licensing") or {}).get("available")}
listed = [entry["agentId"] for entry in data["MARKETPLACE"]["listings"]]
for aid in listed:
if aid not in licensable:
fail(f"MARKETPLACE lists {aid!r}, which is not licensable "
f"(licensing.available != true)")
if len(set(listed)) != len(listed):
fail("MARKETPLACE has duplicate listings for one agent")
for aid in sorted(licensable - set(listed)):
fail(f"agent {aid!r} is licensable but has no MARKETPLACE listing")
def check_expert_requests(data: dict) -> None:
pack_ids = {p["id"] for p in data["COMPOSE"]["packs"]}
for req in data["EXPERT_REQUESTS"]:
rid = req.get("id", "?")
if req.get("status") not in VALID_REQUEST_STATUSES:
fail(f"EXPERT_REQUESTS[{rid}]: invalid status {req.get('status')!r} "
f"(valid: {sorted(VALID_REQUEST_STATUSES)})")
pack = req.get("packId")
if pack is not None and pack not in pack_ids:
fail(f"EXPERT_REQUESTS[{rid}]: unknown packId {pack!r}")
def check_blueprint_weights(data: dict, tolerance: float = 1e-3) -> None:
for cert in data["CERTS"]:
sections = (cert.get("blueprint") or {}).get("sections") or []
if not sections:
fail(f"cert {cert['id']!r}: blueprint has no sections")
continue
total = sum(s.get("weight", 0) for s in sections)
if abs(total - 1.0) > tolerance:
fail(f"cert {cert['id']!r}: blueprint weights sum to {total:.4f}, "
f"expected β 1.0")
def check_item_banks(data: dict) -> None:
"""Every atp/items/<certId>.json bank must belong to a seed cert and
cover its blueprint sections exactly (atp/exams.py load contract:
blueprints-before-commissioning)."""
certs = {c["id"]: c for c in data["CERTS"]}
for path in sorted(ITEMS_DIR.glob("*.json")):
cert_id = path.stem
cert = certs.get(cert_id)
if cert is None:
fail(f"item bank {path.name}: no seed cert {cert_id!r}")
continue
try:
bank = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
fail(f"item bank {path.name}: unreadable ({exc})")
continue
items = bank.get("items") or []
if not items:
fail(f"item bank {path.name}: empty 'items' list")
continue
blueprint = {s["name"] for s in cert["blueprint"]["sections"]}
banked = {item.get("section") for item in items}
for section in sorted(banked - blueprint, key=str):
fail(f"item bank {path.name}: section {section!r} is not in "
f"cert {cert_id!r}'s blueprint")
for section in sorted(blueprint - banked):
fail(f"item bank {path.name}: blueprint section {section!r} "
f"has no items")
def check_skill_edges(data: dict) -> None:
for agent in data["AGENTS"]:
skill_ids = {s["id"] for s in agent.get("skills") or []}
for edge in agent.get("skillEdges") or []:
for end in edge:
if end not in skill_ids:
fail(f"agent {agent['id']!r}: skillEdge endpoint {end!r} "
f"is not one of its skills")
def main() -> int:
data = build_seed()
registries = {
"agents": {a["id"] for a in data["AGENTS"]},
"certs": {c["id"] for c in data["CERTS"]},
"evidence": {e["id"] for e in data["EVIDENCE"]},
"policies": {p["id"] for p in data["POLICIES"]},
"packs": {p["id"] for p in data["COMPOSE"]["packs"]},
}
sweep(data, registries)
check_layers(data, registries["agents"])
check_marketplace(data)
check_expert_requests(data)
check_blueprint_weights(data)
check_item_banks(data)
check_skill_edges(data)
counts = ", ".join(
f"{len(registries[k])} {k}" for k in ("agents", "certs", "evidence", "policies"))
if ERRORS:
print(f"check_integrity: {len(ERRORS)} violation(s) across seed "
f"({counts}):", file=sys.stderr)
for err in ERRORS:
print(f" β {err}", file=sys.stderr)
return 1
print(f"check_integrity: OK β {counts}, "
f"{len(data['MARKETPLACE']['listings'])} listings, "
f"{len(list(ITEMS_DIR.glob('*.json')))} item banks; "
f"all cross-references resolve.")
return 0
if __name__ == "__main__":
sys.exit(main())
|