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.
- api/__init__.py +0 -0
- api/class_fit.py +334 -0
- api/classes.py +557 -0
- api/ranking/__init__.py +12 -0
- api/ranking/engine.py +1943 -0
- cli/__init__.py +0 -0
- cli/modelspec/__init__.py +0 -0
- cli/modelspec/cli.py +1819 -0
- cli/modelspec/commands/__init__.py +0 -0
- cli/modelspec/decide_cmd.py +333 -0
- cli/modelspec/offline.py +623 -0
- cli/modelspec/snapshot.py +698 -0
- cli/modelspec/snapshot_build_cmd.py +49 -0
- cli/modelspec/verify_cmd.py +125 -0
- cli/modelspec/vocab_cmd.py +204 -0
- cli/modelspec/vocabulary_cache.py +54 -0
- decision/__init__.py +13 -0
- decision/capability.py +872 -0
- decision/computed.py +125 -0
- decision/contract.py +1575 -0
- decision/engine.py +238 -0
- decision/excluded.py +34 -0
- decision/explain.py +908 -0
- decision/filter.py +796 -0
- decision/model.py +438 -0
- decision/normalise.py +604 -0
- decision/optimise.py +320 -0
- decision/registry.py +717 -0
- decision/relax.py +132 -0
- decision/resolve.py +111 -0
- decision/schema.py +21 -0
- decision/snapshot.py +1483 -0
- decision/sources.py +544 -0
- decision/templates.py +134 -0
- decision/verify.py +1745 -0
- decision/vocabulary.py +433 -0
- modelspec_dev-0.1.0.dist-info/METADATA +101 -0
- modelspec_dev-0.1.0.dist-info/RECORD +63 -0
- modelspec_dev-0.1.0.dist-info/WHEEL +4 -0
- modelspec_dev-0.1.0.dist-info/entry_points.txt +2 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE +43 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE-DATA +428 -0
- pipeline/__init__.py +0 -0
- pipeline/class_export.py +172 -0
- pipeline/hardware.py +434 -0
- pipeline/hosts.py +247 -0
- pipeline/load.py +224 -0
- pipeline/ranking.py +551 -0
- registry/domains.yaml +130 -0
- registry/facets.yaml +888 -0
- registry/harnesses.yaml +79 -0
- registry/providers.yaml +354 -0
- registry/sources.yaml +3059 -0
- registry/templates.yaml +166 -0
- schema/__init__.py +0 -0
- schema/applicability.py +147 -0
- schema/benchmark.py +175 -0
- schema/benchmark_eligibility.py +304 -0
- schema/card.py +1463 -0
- schema/enrichment.py +162 -0
- schema/enums.py +327 -0
- schema/graph.py +406 -0
- 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_)
|