modelspec-dev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. api/__init__.py +0 -0
  2. api/class_fit.py +334 -0
  3. api/classes.py +557 -0
  4. api/ranking/__init__.py +12 -0
  5. api/ranking/engine.py +1943 -0
  6. cli/__init__.py +0 -0
  7. cli/modelspec/__init__.py +0 -0
  8. cli/modelspec/cli.py +1819 -0
  9. cli/modelspec/commands/__init__.py +0 -0
  10. cli/modelspec/decide_cmd.py +333 -0
  11. cli/modelspec/offline.py +623 -0
  12. cli/modelspec/snapshot.py +698 -0
  13. cli/modelspec/snapshot_build_cmd.py +49 -0
  14. cli/modelspec/verify_cmd.py +125 -0
  15. cli/modelspec/vocab_cmd.py +204 -0
  16. cli/modelspec/vocabulary_cache.py +54 -0
  17. decision/__init__.py +13 -0
  18. decision/capability.py +872 -0
  19. decision/computed.py +125 -0
  20. decision/contract.py +1575 -0
  21. decision/engine.py +238 -0
  22. decision/excluded.py +34 -0
  23. decision/explain.py +908 -0
  24. decision/filter.py +796 -0
  25. decision/model.py +438 -0
  26. decision/normalise.py +604 -0
  27. decision/optimise.py +320 -0
  28. decision/registry.py +717 -0
  29. decision/relax.py +132 -0
  30. decision/resolve.py +111 -0
  31. decision/schema.py +21 -0
  32. decision/snapshot.py +1483 -0
  33. decision/sources.py +544 -0
  34. decision/templates.py +134 -0
  35. decision/verify.py +1745 -0
  36. decision/vocabulary.py +433 -0
  37. modelspec_dev-0.1.0.dist-info/METADATA +101 -0
  38. modelspec_dev-0.1.0.dist-info/RECORD +63 -0
  39. modelspec_dev-0.1.0.dist-info/WHEEL +4 -0
  40. modelspec_dev-0.1.0.dist-info/entry_points.txt +2 -0
  41. modelspec_dev-0.1.0.dist-info/licenses/LICENSE +43 -0
  42. modelspec_dev-0.1.0.dist-info/licenses/LICENSE-DATA +428 -0
  43. pipeline/__init__.py +0 -0
  44. pipeline/class_export.py +172 -0
  45. pipeline/hardware.py +434 -0
  46. pipeline/hosts.py +247 -0
  47. pipeline/load.py +224 -0
  48. pipeline/ranking.py +551 -0
  49. registry/domains.yaml +130 -0
  50. registry/facets.yaml +888 -0
  51. registry/harnesses.yaml +79 -0
  52. registry/providers.yaml +354 -0
  53. registry/sources.yaml +3059 -0
  54. registry/templates.yaml +166 -0
  55. schema/__init__.py +0 -0
  56. schema/applicability.py +147 -0
  57. schema/benchmark.py +175 -0
  58. schema/benchmark_eligibility.py +304 -0
  59. schema/card.py +1463 -0
  60. schema/enrichment.py +162 -0
  61. schema/enums.py +327 -0
  62. schema/graph.py +406 -0
  63. schema/suppliers.py +72 -0
decision/registry.py ADDED
@@ -0,0 +1,717 @@
1
+ """Load and validate the registries in `registry/*.yaml` (MODEL-133).
2
+
3
+ Facets, units, source kinds, providers, harnesses and domains are open sets
4
+ (design §3.4, ADR 0002). This module holds no list of any of them: it reads the
5
+ YAML, checks every entry, and answers typed lookups. Adding a facet, provider,
6
+ harness or domain is a registry entry, never an edit here.
7
+
8
+ Two rules the engine depends on:
9
+
10
+ * **Unknown IDs fail loudly.** Every accessor raises `UnknownIdError` naming
11
+ the kind, the ID and the nearest registered IDs. Nothing is silently ignored
12
+ (design §6.1).
13
+ * **An unknown harness is `unregistered`.** `Registry.resolve_harness` maps any
14
+ string to a registered `name@major.minor` or to the literal `unregistered`,
15
+ never to free text (design §4.1).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import difflib
21
+ import re
22
+ from dataclasses import dataclass, replace
23
+ from datetime import date
24
+ from functools import cache
25
+ from pathlib import Path
26
+ from types import MappingProxyType
27
+ from typing import Any, Callable, Iterable, Literal, Mapping
28
+
29
+ import yaml
30
+
31
+ REPO_ROOT = Path(__file__).resolve().parents[1]
32
+ REGISTRY_DIR = REPO_ROOT / "registry"
33
+
34
+ #: What a harness that is not registered is reported as.
35
+ UNREGISTERED = "unregistered"
36
+
37
+ Subject = Literal["model", "offering", "evidence"]
38
+ Tier = Literal["guaranteed", "best_effort"]
39
+ Risk = Literal["capability", "governance"]
40
+ UnknownPolicy = Literal["may_qualify", "not_satisfied"]
41
+
42
+ SUBJECTS = ("model", "offering", "evidence")
43
+ TIERS = ("guaranteed", "best_effort")
44
+ RISKS = ("capability", "governance")
45
+ KINDS = ("number", "enum", "boolean", "date", "set", "range")
46
+ PROVIDER_KINDS = ("lab_api", "cloud", "inference", "aggregator")
47
+ SHOWN_BY = ("address", "incorporation", "governing_law")
48
+ BASES = ("service_terms", "website_terms")
49
+
50
+ #: A facet definition shorter than this is a label, not a definition.
51
+ MIN_DEFINITION_WORDS = 12
52
+
53
+ FACET_ID = re.compile(r"^[a-z][a-z0-9_]*(\.[a-z0-9_]+)+$")
54
+ SNAKE_ID = re.compile(r"^[a-z][a-z0-9_]*$")
55
+ KEBAB_ID = re.compile(r"^[a-z0-9][a-z0-9-]*$")
56
+ COUNTRY = re.compile(r"^[A-Z]{2}$")
57
+ HARNESS_REF = re.compile(r"^([a-z0-9][a-z0-9-]*)@(\d+)\.(\d+)(?:[.+-][0-9A-Za-z.+-]*)?$")
58
+
59
+
60
+ class RegistryError(ValueError):
61
+ """A registry file is missing, malformed, or has an invalid entry."""
62
+
63
+
64
+ class UnknownIdError(RegistryError, KeyError):
65
+ """A lookup named an ID no registry holds."""
66
+
67
+ def __init__(self, kind: str, id_: str, known: Iterable[str]):
68
+ self.kind, self.id = kind, id_
69
+ close = difflib.get_close_matches(id_, list(known), n=3, cutoff=0.6)
70
+ hint = f"; did you mean {', '.join(repr(c) for c in close)}?" if close else ""
71
+ super().__init__(f"unknown {kind} {id_!r}: not in registry/{_FILE_FOR.get(kind, kind)}{hint}")
72
+
73
+ def __str__(self) -> str: # KeyError would repr() the message
74
+ return str(self.args[0])
75
+
76
+
77
+ _FILE_FOR = {
78
+ "facet": "facets.yaml", "unit": "facets.yaml", "source_kind": "facets.yaml",
79
+ "provider": "providers.yaml", "harness": "harnesses.yaml", "domain": "domains.yaml",
80
+ }
81
+
82
+
83
+ # ── entries ────────────────────────────────────────────────────────────────
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class Unit:
88
+ id: str
89
+ definition: str
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class SourceKind:
94
+ id: str
95
+ definition: str
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class ValueType:
100
+ kind: str
101
+ values: tuple[str, ...] | None = None
102
+ values_from: str | None = None
103
+ #: A known value may be the literal `unbounded` (no limit).
104
+ unbounded: bool = False
105
+ #: A known value may be the literal `not_offered`.
106
+ not_offered: bool = False
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class Parameter:
111
+ """A facet that is a family: one value per member of a named list."""
112
+ name: str
113
+ values_from: str
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class Facet:
118
+ id: str
119
+ subject: Subject
120
+ value_type: ValueType
121
+ definition: str
122
+ tier: Tier
123
+ risk: Risk
124
+ permitted_source_kinds: tuple[str, ...]
125
+ unit: str | None = None
126
+ parameter: Parameter | None = None
127
+ required_qualifiers: tuple[str, ...] = ()
128
+ computed_by: str | None = None
129
+ #: A short name for people, such as "Input price". Optional.
130
+ label: str | None = None
131
+ #: Plain labels for enum or set values, as ``(value, label)`` pairs, so a
132
+ #: page never shows a token such as ``permitted_with_conditions``.
133
+ value_labels: tuple[tuple[str, str], ...] = ()
134
+
135
+ def value_label(self, value: str) -> str | None:
136
+ return dict(self.value_labels).get(value)
137
+
138
+ @property
139
+ def unknown_policy(self) -> UnknownPolicy:
140
+ """Capability unknowns may qualify; governance unknowns are not satisfied."""
141
+ return "may_qualify" if self.risk == "capability" else "not_satisfied"
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class Sourced:
146
+ """A value read from a URL on a date, or unknown (`value is None`)."""
147
+ value: str | None = None
148
+ source: str | None = None
149
+ read: str | None = None
150
+ note: str = ""
151
+
152
+ @property
153
+ def known(self) -> bool:
154
+ return self.value is not None
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class Jurisdiction(Sourced):
159
+ entity: str = ""
160
+ shown_by: str = ""
161
+ basis: str = ""
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class Provider:
166
+ id: str
167
+ name: str
168
+ url: str
169
+ kind: str
170
+ jurisdiction: Jurisdiction
171
+ attestations: Mapping[str, Sourced]
172
+ v1_availability_field: str | None = None
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class HarnessVersion:
177
+ id: str
178
+ source: str | None
179
+ read: str
180
+ note: str = ""
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class Harness:
185
+ id: str
186
+ name: str
187
+ url: str | None
188
+ version_records: tuple[HarnessVersion, ...]
189
+ url_note: str = ""
190
+
191
+ @property
192
+ def versions(self) -> tuple[str, ...]:
193
+ return tuple(v.id for v in self.version_records)
194
+
195
+
196
+ @dataclass(frozen=True)
197
+ class Domain:
198
+ id: str
199
+ name: str
200
+ definition: str
201
+ proxy_only: bool = False
202
+ #: The benchmark a task in this domain ranks on by default, when it has
203
+ #: verified evidence (Jamie's product call). Otherwise the direct benchmark
204
+ #: with the most verified lineup models. The page lets the user switch.
205
+ default_benchmark: str | None = None
206
+
207
+
208
+ # ── the registry ───────────────────────────────────────────────────────────
209
+
210
+
211
+ class Registry:
212
+ """Typed, validated lookups over every registry file."""
213
+
214
+ def __init__(self, *, units, source_kinds, facets, providers, harnesses, domains,
215
+ named_lists: Mapping[str, Callable[[], frozenset[str]] | None]):
216
+ self._units: Mapping[str, Unit] = MappingProxyType(units)
217
+ self._source_kinds: Mapping[str, SourceKind] = MappingProxyType(source_kinds)
218
+ self._facets: Mapping[str, Facet] = MappingProxyType(facets)
219
+ self._providers: Mapping[str, Provider] = MappingProxyType(providers)
220
+ self._harnesses: Mapping[str, Harness] = MappingProxyType(harnesses)
221
+ self._domains: Mapping[str, Domain] = MappingProxyType(domains)
222
+ self._named_lists = named_lists
223
+ self._harness_versions = frozenset(v for h in harnesses.values() for v in h.versions)
224
+
225
+ @staticmethod
226
+ def _get(kind: str, table: Mapping[str, Any], id_: str):
227
+ try:
228
+ return table[id_]
229
+ except (KeyError, TypeError):
230
+ raise UnknownIdError(kind, str(id_), table) from None
231
+
232
+ def facet(self, id_: str) -> Facet:
233
+ if id_ in self._facets:
234
+ return self._facets[id_]
235
+ for facet in self._facets.values():
236
+ if facet.parameter is None:
237
+ continue
238
+ producer = self._named_lists[facet.parameter.values_from]
239
+ if producer is not None and id_ in producer():
240
+ return replace(facet, id=id_)
241
+ raise UnknownIdError("facet", str(id_), self._known_facet_ids())
242
+
243
+ def _known_facet_ids(self) -> tuple[str, ...]:
244
+ ids = list(self._facets)
245
+ for facet in self._facets.values():
246
+ if facet.parameter is None:
247
+ continue
248
+ producer = self._named_lists[facet.parameter.values_from]
249
+ if producer is not None:
250
+ ids.extend(producer())
251
+ return tuple(ids)
252
+
253
+ def unit(self, id_: str) -> Unit:
254
+ return self._get("unit", self._units, id_)
255
+
256
+ def source_kind(self, id_: str) -> SourceKind:
257
+ return self._get("source_kind", self._source_kinds, id_)
258
+
259
+ def provider(self, id_: str) -> Provider:
260
+ return self._get("provider", self._providers, id_)
261
+
262
+ def harness(self, id_: str) -> Harness:
263
+ """A harness by name (`claude-code`), not by version."""
264
+ return self._get("harness", self._harnesses, id_)
265
+
266
+ def domain(self, id_: str) -> Domain:
267
+ return self._get("domain", self._domains, id_)
268
+
269
+ def facets(self) -> tuple[Facet, ...]:
270
+ return tuple(self._facets.values())
271
+
272
+ def units(self) -> tuple[Unit, ...]:
273
+ return tuple(self._units.values())
274
+
275
+ def source_kinds(self) -> tuple[SourceKind, ...]:
276
+ return tuple(self._source_kinds.values())
277
+
278
+ def providers(self) -> tuple[Provider, ...]:
279
+ return tuple(self._providers.values())
280
+
281
+ def harnesses(self) -> tuple[Harness, ...]:
282
+ return tuple(self._harnesses.values())
283
+
284
+ def domains(self) -> tuple[Domain, ...]:
285
+ return tuple(self._domains.values())
286
+
287
+ def resolve_harness(self, raw: str | None) -> str:
288
+ """The registered `name@major.minor` for `raw`, else `unregistered`."""
289
+ m = HARNESS_REF.match((raw or "").strip())
290
+ if not m:
291
+ return UNREGISTERED
292
+ canonical = f"{m.group(1)}@{int(m.group(2))}.{int(m.group(3))}"
293
+ return canonical if canonical in self._harness_versions else UNREGISTERED
294
+
295
+ def allowed_values(self, facet: Facet) -> frozenset[str] | None:
296
+ """The values an enum or set facet admits; `None` for an open list
297
+ (a code standard such as ISO 3166, or a list validated elsewhere)."""
298
+ vt = facet.value_type
299
+ if vt.values is not None:
300
+ return frozenset(vt.values)
301
+ if vt.values_from is None:
302
+ return None
303
+ producer = self._named_lists[vt.values_from]
304
+ return producer() if producer else None
305
+
306
+
307
+ # ── loading and validation ─────────────────────────────────────────────────
308
+
309
+
310
+ def _named_lists(repo_root: Path, raw: Mapping[str, list[dict]]) -> dict[str, Callable[[], frozenset[str]] | None]:
311
+ """Every name a `values_from` may use. `None` marks an open list whose
312
+ members are validated where the values are written, not here."""
313
+
314
+ def ids(name: str, key: str = "id") -> Callable[[], frozenset[str]]:
315
+ return lambda: frozenset(str(e.get(key)) for e in raw.get(name, []))
316
+
317
+ def harness_versions() -> frozenset[str]:
318
+ return frozenset(str(v.get("id")) for h in raw.get("harnesses", []) for v in h.get("versions") or [])
319
+
320
+ def model_classes() -> frozenset[str]:
321
+ from api.classes import CLASS_BY_ID
322
+ return frozenset(CLASS_BY_ID)
323
+
324
+ def architecture_types() -> frozenset[str]:
325
+ from schema.enums import ArchitectureType
326
+ return frozenset(a.value for a in ArchitectureType)
327
+
328
+ def stems(directory: str, pattern: str) -> Callable[[], frozenset[str]]:
329
+ return lambda: frozenset(
330
+ p.stem for p in (repo_root / directory).glob(pattern)
331
+ if not p.name.startswith("_") and p.name not in {"README.md", "LICENSE.md", "AUTHORING.md"})
332
+
333
+ return {
334
+ "registry:providers": ids("providers"),
335
+ "registry:harnesses": harness_versions,
336
+ "registry:domains": ids("domains"),
337
+ "model_classes": model_classes,
338
+ "architecture_types": architecture_types,
339
+ "benchmarks": stems("benchmarks", "*.md"),
340
+ "hardware": stems("hardware", "*.yaml"),
341
+ "iso_3166_1_alpha_2": None,
342
+ "bcp_47": None,
343
+ "model_ids": None,
344
+ "outcome_task_types": None,
345
+ }
346
+
347
+
348
+ class _Errors:
349
+ def __init__(self) -> None:
350
+ self.items: list[str] = []
351
+
352
+ def add(self, where: str, message: str) -> None:
353
+ self.items.append(f"{where}: {message}")
354
+
355
+ def raise_if_any(self) -> None:
356
+ if self.items:
357
+ raise RegistryError("invalid registry:\n " + "\n ".join(self.items))
358
+
359
+
360
+ def _read(root: Path, name: str, key: str) -> list[dict]:
361
+ path = root / f"{name}.yaml"
362
+ if not path.is_file():
363
+ raise RegistryError(f"missing registry file {path}")
364
+ try:
365
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
366
+ except yaml.YAMLError as exc:
367
+ raise RegistryError(f"{path}: not valid YAML: {exc}") from exc
368
+ if data.get("schema_version") != 1:
369
+ raise RegistryError(f"{path}: schema_version must be 1")
370
+ entries = data.get(key)
371
+ if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
372
+ raise RegistryError(f"{path}: `{key}` must be a list of mappings")
373
+ return entries
374
+
375
+
376
+ def _keys(err: _Errors, where: str, entry: dict, required: set[str], optional: set[str]) -> None:
377
+ for k in sorted(required - entry.keys()):
378
+ err.add(where, f"missing required field {k!r}")
379
+ for k in sorted(entry.keys() - required - optional):
380
+ err.add(where, f"unknown field {k!r}")
381
+
382
+
383
+ def _iso_date(value: Any) -> bool:
384
+ try:
385
+ date.fromisoformat(str(value))
386
+ return True
387
+ except ValueError:
388
+ return False
389
+
390
+
391
+ def _https(value: Any) -> bool:
392
+ return isinstance(value, str) and value.startswith("https://")
393
+
394
+
395
+ def _unique(err: _Errors, file: str, entries: list[dict], pattern: re.Pattern, label: str) -> None:
396
+ seen: set[str] = set()
397
+ for e in entries:
398
+ id_ = e.get("id")
399
+ if not isinstance(id_, str) or not pattern.match(id_):
400
+ err.add(f"{file} {id_!r}", f"id must be {label}")
401
+ elif id_ in seen:
402
+ err.add(f"{file} {id_!r}", "duplicate id")
403
+ seen.add(str(id_))
404
+
405
+
406
+ def _described(err: _Errors, where: str, entry: dict, min_words: int = 1) -> None:
407
+ text = entry.get("definition")
408
+ if not isinstance(text, str) or len(text.split()) < min_words:
409
+ err.add(where, f"definition must be at least {min_words} words")
410
+
411
+
412
+ def _load_units_and_kinds(err: _Errors, root: Path) -> tuple[dict, dict]:
413
+ tables = []
414
+ for key in ("units", "source_kinds"):
415
+ entries = _read(root, "facets", key)
416
+ _unique(err, f"facets.yaml {key}", entries, SNAKE_ID, "snake_case")
417
+ for e in entries:
418
+ where = f"facets.yaml {key} {e.get('id')!r}"
419
+ _keys(err, where, e, {"id", "definition"}, set())
420
+ _described(err, where, e)
421
+ tables.append(entries)
422
+ units = {e["id"]: Unit(e["id"], str(e.get("definition", ""))) for e in tables[0] if "id" in e}
423
+ kinds = {e["id"]: SourceKind(e["id"], str(e.get("definition", ""))) for e in tables[1] if "id" in e}
424
+ return units, kinds
425
+
426
+
427
+ def _value_type(err: _Errors, where: str, raw: Any, unit: Any, units: Mapping, lists: Mapping) -> ValueType | None:
428
+ if not isinstance(raw, dict):
429
+ err.add(where, "value_type must be a mapping with a `kind`")
430
+ return None
431
+ kind = raw.get("kind")
432
+ if kind not in KINDS:
433
+ err.add(where, f"value_type.kind {kind!r} must be one of {', '.join(KINDS)}")
434
+ return None
435
+ optional = {"unbounded", "not_offered"} if kind == "number" else set()
436
+ if kind in ("enum", "set"):
437
+ optional = {"values", "values_from"}
438
+ _keys(err, where, raw, {"kind"}, optional)
439
+ if kind in ("number", "range"):
440
+ if unit is None:
441
+ err.add(where, f"a {kind} facet needs a unit")
442
+ elif unit not in units:
443
+ err.add(where, f"unit {unit!r} is not registered in facets.yaml units")
444
+ elif unit is not None:
445
+ err.add(where, f"a {kind} facet takes no unit")
446
+ values, values_from = raw.get("values"), raw.get("values_from")
447
+ if kind in ("enum", "set"):
448
+ if (values is None) == (values_from is None):
449
+ err.add(where, f"a {kind} facet needs exactly one of values or values_from")
450
+ if values is not None and (not isinstance(values, list) or not values
451
+ or len(set(map(str, values))) != len(values)):
452
+ err.add(where, "values must be a non-empty list without duplicates")
453
+ if values_from is not None and values_from not in lists:
454
+ err.add(where, f"values_from {values_from!r} is not a known list ({', '.join(sorted(lists))})")
455
+ return ValueType(
456
+ kind=kind,
457
+ values=tuple(map(str, values)) if isinstance(values, list) else None,
458
+ values_from=values_from,
459
+ unbounded=bool(raw.get("unbounded", False)),
460
+ not_offered=bool(raw.get("not_offered", False)),
461
+ )
462
+
463
+
464
+ def _value_labels(err: _Errors, where: str, raw: Any, vt: ValueType | None,
465
+ lists: Mapping) -> tuple[tuple[str, str], ...]:
466
+ """``value_labels``: a label for each named value of an enum or set facet."""
467
+ if raw is None:
468
+ return ()
469
+ if vt is None or vt.kind not in ("enum", "set"):
470
+ err.add(where, "value_labels is only for enum and set facets")
471
+ return ()
472
+ if not isinstance(raw, dict) or not all(
473
+ isinstance(label, str) and label.strip() for label in raw.values()):
474
+ err.add(where, "value_labels must map each value to a non-empty label")
475
+ return ()
476
+ allowed = set(vt.values) if vt.values is not None else None
477
+ producer = lists.get(vt.values_from) if vt.values_from else None
478
+ if allowed is None and producer is not None:
479
+ allowed = set(producer())
480
+ for value in raw:
481
+ if allowed is not None and str(value) not in allowed:
482
+ err.add(where, f"value_labels names {value!r}, which is not one of its values")
483
+ return tuple(sorted((str(k), str(v)) for k, v in raw.items()))
484
+
485
+
486
+ def _load_facets(err: _Errors, root: Path, units: Mapping, kinds: Mapping, lists: Mapping) -> dict[str, Facet]:
487
+ entries = _read(root, "facets", "facets")
488
+ _unique(err, "facets.yaml", entries, FACET_ID, "dotted snake_case, such as model.context_window")
489
+ required = {"id", "subject", "value_type", "definition", "tier", "risk", "permitted_source_kinds"}
490
+ optional = {"unit", "parameter", "required_qualifiers", "computed_by", "label", "value_labels"}
491
+ out: dict[str, Facet] = {}
492
+ for e in entries:
493
+ where = f"facets.yaml {e.get('id')!r}"
494
+ _keys(err, where, e, required, optional)
495
+ _described(err, where, e, MIN_DEFINITION_WORDS)
496
+ for field_, allowed in (("subject", SUBJECTS), ("tier", TIERS), ("risk", RISKS)):
497
+ if e.get(field_) not in allowed:
498
+ err.add(where, f"{field_} {e.get(field_)!r} must be one of {', '.join(allowed)}")
499
+ vt = _value_type(err, where, e.get("value_type"), e.get("unit"), units, lists)
500
+ psk = e.get("permitted_source_kinds")
501
+ if not isinstance(psk, list) or not psk:
502
+ err.add(where, "permitted_source_kinds must be a non-empty list")
503
+ psk = []
504
+ for k in psk:
505
+ if k not in kinds:
506
+ err.add(where, f"permitted source kind {k!r} is not registered in facets.yaml source_kinds")
507
+ param = e.get("parameter")
508
+ parameter = None
509
+ if param is not None:
510
+ if not isinstance(param, dict) or set(param) != {"name", "values_from"}:
511
+ err.add(where, "parameter must be {name, values_from}")
512
+ elif param["values_from"] not in lists:
513
+ err.add(where, f"parameter values_from {param['values_from']!r} is not a known list")
514
+ else:
515
+ parameter = Parameter(str(param["name"]), str(param["values_from"]))
516
+ label = e.get("label")
517
+ if label is not None and (not isinstance(label, str) or not label.strip()):
518
+ err.add(where, "label must be a non-empty string")
519
+ label = None
520
+ value_labels = _value_labels(err, where, e.get("value_labels"), vt, lists)
521
+ rq = e.get("required_qualifiers", [])
522
+ if not isinstance(rq, list) or not all(isinstance(q, str) for q in rq):
523
+ err.add(where, "required_qualifiers must be a list of names")
524
+ rq = []
525
+ if vt is None or not isinstance(e.get("id"), str):
526
+ continue
527
+ out.setdefault(e["id"], Facet(
528
+ id=e["id"], subject=e.get("subject"), value_type=vt,
529
+ definition=" ".join(str(e.get("definition", "")).split()),
530
+ tier=e.get("tier"), risk=e.get("risk"), permitted_source_kinds=tuple(psk),
531
+ unit=e.get("unit"), parameter=parameter, required_qualifiers=tuple(rq),
532
+ computed_by=e.get("computed_by"), label=label, value_labels=value_labels,
533
+ ))
534
+ return out
535
+
536
+
537
+ def _sourced(err: _Errors, where: str, raw: Any, extra: set[str] = frozenset()) -> dict | None:
538
+ """`unknown`, or a mapping with a value, an https source and a read date."""
539
+ if raw == "unknown":
540
+ return None
541
+ if not isinstance(raw, dict):
542
+ err.add(where, "must be `unknown` or a mapping with value, source and read")
543
+ return None
544
+ _keys(err, where, raw, {"value", "source", "read"}, {"note"} | set(extra))
545
+ if "source" in raw and not _https(raw["source"]):
546
+ err.add(where, "source must be an https URL")
547
+ if "read" in raw and not _iso_date(raw["read"]):
548
+ err.add(where, "read must be a date YYYY-MM-DD")
549
+ return raw
550
+
551
+
552
+ def _load_providers(err: _Errors, root: Path) -> dict[str, Provider]:
553
+ entries = _read(root, "providers", "providers")
554
+ _unique(err, "providers.yaml", entries, KEBAB_ID, "kebab-case")
555
+ required = {"id", "name", "url", "kind", "jurisdiction", "attestations"}
556
+ availability: set[str] | None = None
557
+ out: dict[str, Provider] = {}
558
+ for e in entries:
559
+ where = f"providers.yaml {e.get('id')!r}"
560
+ _keys(err, where, e, required, {"v1_availability_field"})
561
+ if not _https(e.get("url")):
562
+ err.add(where, "url must be an https URL")
563
+ if e.get("kind") not in PROVIDER_KINDS:
564
+ err.add(where, f"kind {e.get('kind')!r} must be one of {', '.join(PROVIDER_KINDS)}")
565
+ field_ = e.get("v1_availability_field")
566
+ if field_ is not None:
567
+ if availability is None:
568
+ from schema.card import Availability, PlatformEntry
569
+ availability = {n for n, f in Availability.model_fields.items() if f.annotation is PlatformEntry}
570
+ if field_ not in availability:
571
+ err.add(where, f"v1_availability_field {field_!r} is not a platform field on Availability")
572
+ j = _sourced(err, f"{where} jurisdiction", e.get("jurisdiction"), {"entity", "shown_by", "basis"})
573
+ jurisdiction = Jurisdiction()
574
+ if j is not None:
575
+ if not COUNTRY.match(str(j.get("value", ""))):
576
+ err.add(f"{where} jurisdiction", "value must be an ISO 3166-1 alpha-2 code")
577
+ if j.get("shown_by") not in SHOWN_BY:
578
+ err.add(f"{where} jurisdiction", f"shown_by must be one of {', '.join(SHOWN_BY)}")
579
+ if j.get("basis") not in BASES:
580
+ err.add(f"{where} jurisdiction", f"basis must be one of {', '.join(BASES)}")
581
+ if not j.get("entity"):
582
+ err.add(f"{where} jurisdiction", "entity must name the contracting entity")
583
+ jurisdiction = Jurisdiction(
584
+ value=str(j.get("value")), source=j.get("source"), read=str(j.get("read")),
585
+ note=str(j.get("note", "")), entity=str(j.get("entity", "")),
586
+ shown_by=str(j.get("shown_by", "")), basis=str(j.get("basis", "")))
587
+ attestations: dict[str, Sourced] = {}
588
+ raw_att = e.get("attestations")
589
+ if not isinstance(raw_att, dict):
590
+ err.add(where, "attestations must be a mapping of name to `unknown` or a sourced value")
591
+ raw_att = {}
592
+ for name, raw in raw_att.items():
593
+ a = _sourced(err, f"{where} attestation {name!r}", raw)
594
+ attestations[str(name)] = Sourced() if a is None else Sourced(
595
+ value=str(a.get("value")), source=a.get("source"), read=str(a.get("read")),
596
+ note=str(a.get("note", "")))
597
+ if isinstance(e.get("id"), str):
598
+ out.setdefault(e["id"], Provider(
599
+ id=e["id"], name=str(e.get("name", "")), url=str(e.get("url", "")),
600
+ kind=str(e.get("kind", "")), jurisdiction=jurisdiction,
601
+ attestations=MappingProxyType(attestations), v1_availability_field=field_))
602
+ return out
603
+
604
+
605
+ def _load_harnesses(err: _Errors, root: Path) -> dict[str, Harness]:
606
+ entries = _read(root, "harnesses", "harnesses")
607
+ _unique(err, "harnesses.yaml", entries, KEBAB_ID, "kebab-case")
608
+ out: dict[str, Harness] = {}
609
+ for e in entries:
610
+ where = f"harnesses.yaml {e.get('id')!r}"
611
+ _keys(err, where, e, {"id", "name", "url", "versions"}, {"url_note"})
612
+ url = e.get("url")
613
+ if url is None and not e.get("url_note"):
614
+ err.add(where, "a harness with no public url needs a url_note saying why")
615
+ elif url is not None and not _https(url):
616
+ err.add(where, "url must be an https URL or null")
617
+ records: list[HarnessVersion] = []
618
+ versions = e.get("versions")
619
+ if not isinstance(versions, list) or not versions:
620
+ err.add(where, "versions must be a non-empty list")
621
+ versions = []
622
+ for v in versions:
623
+ if not isinstance(v, dict):
624
+ err.add(where, "each version must be a mapping")
625
+ continue
626
+ vid = v.get("id")
627
+ vwhere = f"{where} version {vid!r}"
628
+ _keys(err, vwhere, v, {"id", "source", "read"}, {"note"})
629
+ if not re.fullmatch(rf"{re.escape(str(e.get('id')))}@\d+\.\d+", str(vid)):
630
+ err.add(vwhere, "version id must be canonical name@major.minor")
631
+ if v.get("source") is not None and not _https(v.get("source")):
632
+ err.add(vwhere, "source must be an https URL or null")
633
+ if v.get("source") is None and url is not None:
634
+ err.add(vwhere, "a version of a public harness needs a source")
635
+ if not _iso_date(v.get("read")):
636
+ err.add(vwhere, "read must be a date YYYY-MM-DD")
637
+ records.append(HarnessVersion(str(vid), v.get("source"), str(v.get("read")), str(v.get("note", ""))))
638
+ if len({r.id for r in records}) != len(records):
639
+ err.add(where, "duplicate version id")
640
+ if isinstance(e.get("id"), str):
641
+ out.setdefault(e["id"], Harness(
642
+ id=e["id"], name=str(e.get("name", "")), url=url,
643
+ version_records=tuple(records), url_note=str(e.get("url_note", ""))))
644
+ return out
645
+
646
+
647
+ def _load_domains(err: _Errors, root: Path) -> dict[str, Domain]:
648
+ entries = _read(root, "domains", "domains")
649
+ _unique(err, "domains.yaml", entries, SNAKE_ID, "snake_case")
650
+ out: dict[str, Domain] = {}
651
+ for e in entries:
652
+ where = f"domains.yaml {e.get('id')!r}"
653
+ _keys(err, where, e, {"id", "name", "definition"}, {"proxy_only", "default_benchmark"})
654
+ _described(err, where, e, MIN_DEFINITION_WORDS)
655
+ if not isinstance(e.get("proxy_only", False), bool):
656
+ err.add(where, "proxy_only must be true or false")
657
+ default = e.get("default_benchmark")
658
+ if default is not None and not (isinstance(default, str) and SNAKE_ID.match(default)):
659
+ err.add(where, "default_benchmark must be a snake_case benchmark id")
660
+ if isinstance(e.get("id"), str):
661
+ out.setdefault(e["id"], Domain(
662
+ id=e["id"], name=str(e.get("name", "")),
663
+ definition=" ".join(str(e.get("definition", "")).split()),
664
+ proxy_only=e.get("proxy_only", False) is True,
665
+ default_benchmark=e.get("default_benchmark") if isinstance(
666
+ e.get("default_benchmark"), str) else None))
667
+ return out
668
+
669
+
670
+ def load(root: Path | None = None, *, repo_root: Path | None = None) -> Registry:
671
+ """Read and validate every registry file under `root` (default `registry/`).
672
+
673
+ Raises `RegistryError` listing every problem found, not just the first.
674
+ `repo_root` is where file-backed lists (benchmark pages, hardware SKUs) are
675
+ read from; it defaults to this repository.
676
+ """
677
+ root = Path(root) if root is not None else REGISTRY_DIR
678
+ repo_root = Path(repo_root) if repo_root is not None else REPO_ROOT
679
+ err = _Errors()
680
+ raw = {
681
+ "providers": _read(root, "providers", "providers"),
682
+ "harnesses": _read(root, "harnesses", "harnesses"),
683
+ "domains": _read(root, "domains", "domains"),
684
+ }
685
+ lists = _named_lists(repo_root, raw)
686
+ units, kinds = _load_units_and_kinds(err, root)
687
+ facets = _load_facets(err, root, units, kinds, lists)
688
+ providers = _load_providers(err, root)
689
+ harnesses = _load_harnesses(err, root)
690
+ domains = _load_domains(err, root)
691
+ err.raise_if_any()
692
+ return Registry(units=units, source_kinds=kinds, facets=facets, providers=providers,
693
+ harnesses=harnesses, domains=domains, named_lists=lists)
694
+
695
+
696
+ @cache
697
+ def default() -> Registry:
698
+ """The repository's own registries, loaded once."""
699
+ return load()
700
+
701
+
702
+ # Module-level shortcuts over the default registry. Other `decision` modules
703
+ # (the contract, the engine) call these rather than holding a Registry.
704
+ def facet(id_: str) -> Facet:
705
+ return default().facet(id_)
706
+
707
+
708
+ def provider(id_: str) -> Provider:
709
+ return default().provider(id_)
710
+
711
+
712
+ def harness(id_: str) -> Harness:
713
+ return default().harness(id_)
714
+
715
+
716
+ def domain(id_: str) -> Domain:
717
+ return default().domain(id_)