Repository navigation
256 lines (237 loc) · 13 KB
/
Copy pathvalidate.yml
File metadata and controls
256 lines (237 loc) · 13 KB
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
name: validate
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate all JSON and JSONL (stdlib, fails loudly)
run: |
python3 - <<'PY'
import json, os, sys, urllib.request, urllib.error
root = os.getcwd()
failures = []
def fail(msg):
failures.append(msg)
print(f"FAIL: {msg}")
# 1. every .json / .jsonl file must parse
for dirpath, _, files in os.walk(root):
if "/.git/" in dirpath or "/.github/" in dirpath:
continue
for name in files:
if name.endswith((".json", ".jsonl")):
path = os.path.join(dirpath, name)
try:
text = open(path, encoding="utf-8").read()
except Exception as e:
fail(f"{path}: cannot read: {e}")
continue
try:
if name.endswith(".jsonl"):
for i, line in enumerate(text.splitlines(), 1):
if line.strip():
json.loads(line)
else:
json.loads(text)
except Exception as e:
fail(f"{path}: invalid JSON: {e}")
# 2. schema_version contract + strict field-level schema checks
EXPRESSIONS = {"celebrating", "working", "idle", "alert"}
# 2a. per-domain schema-version registry (SCHEMA-VERSIONS.json):
# every .json document must declare schema_version and match the
# expectation for its registry pattern. A document with no registry
# coverage fails loudly. The registry file declares its own
# schema_version (meta-schema 1.0.0), so it is checked like any other.
REGISTRY_PATH = os.path.join(root, "SCHEMA-VERSIONS.json")
REGISTRY_DOC_KEYS = {"$description", "how_to_add_a_new_domain_version"}
try:
REGISTRY = json.load(open(REGISTRY_PATH, encoding="utf-8"))
if not isinstance(REGISTRY, dict):
raise ValueError("registry root must be a JSON object")
for _rk, _rv in REGISTRY.items():
if _rk in REGISTRY_DOC_KEYS:
continue
if not isinstance(_rv, str):
raise ValueError(f"registry entry {_rk!r} must map to a version string")
except Exception as e:
fail(f"SCHEMA-VERSIONS.json missing or invalid: {e}")
REGISTRY = {}
def expected_version(rel):
import fnmatch
for _pat, _ver in REGISTRY.items():
if _pat in REGISTRY_DOC_KEYS or _pat == "default":
continue
if fnmatch.fnmatch(rel, _pat):
return _ver
return REGISTRY.get("default")
def need(doc, rel, fields):
for f in fields:
if f not in doc:
fail(f"{rel}: missing required field \"{f}\"")
# --- world.json ---
try:
world = json.load(open(os.path.join(root, "world.json"), encoding="utf-8"))
need(world, "world.json", ["schema_version", "generated_at", "generator",
"world", "agents_index", "guests_index",
"activity_ledger", "rules", "counts"])
need(world["world"], "world.json.world", ["time_of_day", "atmosphere", "cycle_hours"])
for k in ("agents", "guests", "activity_events_24h"):
if not isinstance(world["counts"].get(k), int) or world["counts"][k] < 0:
fail(f"world.json: counts.{k} must be a non-negative integer")
if world.get("generator") != "tools/build-world.py":
fail("world.json: generator must be \"tools/build-world.py\"")
except Exception:
pass # JSON parse failures already reported above
# --- rules.json ---
try:
rules = json.load(open(os.path.join(root, "rules.json"), encoding="utf-8"))
need(rules, "rules.json", ["schema_version", "preamble", "conduct",
"joining", "what_counts_as_activity",
"truth_law", "enforcement"])
if rules.get("truth_law") != "only real, verifiable actions render in the world; nothing is faked":
fail("rules.json: truth_law wording is immutable in v1")
except Exception:
pass
# --- status.json ---
try:
status = json.load(open(os.path.join(root, "status.json"), encoding="utf-8"))
need(status, "status.json", ["schema_version", "world_url", "counts",
"departments", "guests", "generated_at"])
if not status.get("world_url", "").startswith("https://"):
fail("status.json: world_url must be https://")
except Exception:
pass
# --- agents/<id>.json (the 8 residents) ---
for aid in ["needle", "marquee", "seal", "dial", "dateline",
"fader", "ledger", "charter"]:
rel = f"agents/{aid}.json"
path = os.path.join(root, rel)
if not os.path.exists(path):
fail(f"{rel}: required file missing")
continue
try:
a = json.load(open(path, encoding="utf-8"))
except Exception:
continue
need(a, rel, ["schema_version", "id", "name", "department", "role",
"x", "y", "expression", "current_action", "last_win",
"last_win_url", "avatar", "poster", "persona_line"])
if a.get("id") != aid:
fail(f"{rel}: id {a.get('id')!r} must match filename stem {aid!r}")
if a.get("expression") not in EXPRESSIONS:
fail(f"{rel}: expression {a.get('expression')!r} not in {sorted(EXPRESSIONS)}")
for axis in ("x", "y"):
v = a.get(axis)
if not isinstance(v, (int, float)) or not (0 <= v <= 100):
fail(f"{rel}: {axis} must be a number in 0..100")
if not a.get("last_win_url", "").startswith("https://"):
fail(f"{rel}: last_win_url must be https://")
# --- agents/index.json must match the resident files ---
try:
aindex = json.load(open(os.path.join(root, "agents", "index.json"), encoding="utf-8"))
expected = sorted(f[:-5] for f in os.listdir(os.path.join(root, "agents")) if f.endswith(".json") and f != "index.json")
if sorted(aindex.get("agents", [])) != expected:
fail(f"agents/index.json lists {aindex.get('agents')}, directory has {expected}")
except Exception:
pass
# --- schema_version consistency: every dict document must declare
# schema_version and match its SCHEMA-VERSIONS.json expectation ---
for dirpath, _, files in os.walk(root):
if "/.git/" in dirpath or "/.github/" in dirpath:
continue
for name in files:
if not name.endswith(".json"):
continue
path = os.path.join(dirpath, name)
rel = os.path.relpath(path, root)
try:
doc = json.load(open(path, encoding="utf-8"))
except Exception:
continue # parse failures already reported
if not isinstance(doc, dict):
continue # only documents carry schema_version
exp = expected_version(rel)
if exp is None:
fail(f"{rel}: not covered by any SCHEMA-VERSIONS.json pattern and no default is set")
elif "schema_version" not in doc:
fail(f"{rel}: missing schema_version (SCHEMA-VERSIONS.json expects {exp!r})")
elif doc["schema_version"] != exp:
fail(f"{rel}: schema_version {doc['schema_version']!r} != registry expects {exp!r}")
# 3. guests: file-per-guest + name==filename + index matches dir
guests_dir = os.path.join(root, "guests")
seen = []
for name in sorted(os.listdir(guests_dir)):
if not name.endswith(".json") or name == "index.json":
continue
stem = name[:-5]
if stem != stem.lower().replace("_", "-") or not stem.replace("-", "").replace("_", "").isalnum():
fail(f"guests/{name}: name must be lowercase letters/numbers/hyphens")
try:
g = json.load(open(os.path.join(guests_dir, name), encoding="utf-8"))
except Exception:
continue
for field in ("name", "agent_card_url", "joined_at", "status", "last_action"):
if not g.get(field):
fail(f"guests/{name}: missing or empty field \"{field}\"")
if g.get("name") != stem:
fail(f"guests/{name}: name {g.get('name')!r} must match filename stem {stem!r}")
url = g.get("agent_card_url", "")
if not url.startswith("https://"):
fail(f"guests/{name}: agent_card_url must be https://")
else:
try:
req = urllib.request.Request(url, headers={"User-Agent": "ultimate-ci/1.0"}, method="HEAD")
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status != 200:
fail(f"guests/{name}: agent_card_url returned HTTP {resp.status}")
except urllib.error.HTTPError as e:
if e.code == 405: # HEAD not allowed -> fall back to GET
try:
req = urllib.request.Request(url, headers={"User-Agent": "ultimate-ci/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status != 200:
fail(f"guests/{name}: agent_card_url returned HTTP {resp.status}")
except Exception as e2:
fail(f"guests/{name}: agent_card_url unreachable: {e2}")
else:
fail(f"guests/{name}: agent_card_url returned HTTP {e.code}")
except Exception as e:
fail(f"guests/{name}: agent_card_url unreachable: {e}")
seen.append(stem)
try:
index = json.load(open(os.path.join(guests_dir, "index.json"), encoding="utf-8"))
if index.get("guests") != seen:
fail(f"guests/index.json lists {index.get('guests')}, directory has {seen} — run tools/build-world.py --regen-guests")
except Exception:
pass # reported above
# 4. agents/index.json must match the 8 agent files
try:
aindex = json.load(open(os.path.join(root, "agents", "index.json"), encoding="utf-8"))
expected = sorted(f[:-5] for f in os.listdir(os.path.join(root, "agents")) if f.endswith(".json") and f != "index.json")
if sorted(aindex.get("agents", [])) != expected:
fail(f"agents/index.json lists {aindex.get('agents')}, directory has {expected}")
except Exception:
pass
# 5. activity ledger events carry the required fields + rollup policy intact
try:
ledger = json.load(open(os.path.join(root, "activity", "ledger.json"), encoding="utf-8"))
if "rollup_policy" not in ledger or "30 days" not in str(ledger["rollup_policy"]):
fail("activity/ledger.json: rollup_policy missing or retention windows altered")
for i, ev in enumerate(ledger.get("events", [])):
for field in ("ts", "actor", "action", "detail"):
if not ev.get(field):
fail(f"activity/ledger.json event {i}: missing or empty \"{field}\"")
for day, s in ledger.get("daily_summaries", {}).items():
if not isinstance(s.get("events"), int):
fail(f"activity/ledger.json daily_summaries[{day}]: events must be an integer")
except Exception:
pass
if failures:
print(f"\n{len(failures)} validation failure(s). The world keeps its truth law.")
sys.exit(1)
print("All JSON/JSONL valid; schema contract, guests, and ledger checks passed.")
PY