setspec 0.2.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.
- setspec/__about__.py +3 -0
- setspec/__init__.py +114 -0
- setspec/artifacts.py +4 -0
- setspec/base.py +256 -0
- setspec/benchmark/v1.py +594 -0
- setspec/capability/v1.py +394 -0
- setspec/envelope.py +468 -0
- setspec/error/v1.py +4 -0
- setspec/errors.py +68 -0
- setspec/event/v1.py +4 -0
- setspec/goal/v1.py +390 -0
- setspec/goldens/.gitkeep +0 -0
- setspec/machine/v1.py +131 -0
- setspec/metrics.py +184 -0
- setspec/model/v1.py +169 -0
- setspec/provenance.py +59 -0
- setspec/py.typed +0 -0
- setspec/schemas/.gitkeep +0 -0
- setspec/serialization.py +330 -0
- setspec/vocabulary.py +205 -0
- setspec-0.2.0.dist-info/METADATA +160 -0
- setspec-0.2.0.dist-info/RECORD +24 -0
- setspec-0.2.0.dist-info/WHEEL +4 -0
- setspec-0.2.0.dist-info/licenses/LICENSE +201 -0
setspec/serialization.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Contract module — canonical JSON, the wire codecs, and the guards on untrusted input.
|
|
2
|
+
|
|
3
|
+
Imports pydantic and :mod:`baseaicore`; performs no I/O and reads no clock. This module owns the
|
|
4
|
+
three places where a Python value and its JSON form disagree, and it resolves each one exactly
|
|
5
|
+
once so that no payload model has to:
|
|
6
|
+
|
|
7
|
+
* a measurement that does not exist here is the string ``"unsupported"``, never ``null``, never
|
|
8
|
+
``0`` (ADR-0016 §4);
|
|
9
|
+
* a timestamp is RFC 3339 UTC at millisecond precision, never naive
|
|
10
|
+
([spec §11.6](../../docs/packages/setspec/spec.md));
|
|
11
|
+
* byte-identical output for equal input, which is what makes a hash of a payload comparable across
|
|
12
|
+
machines (spec §11.4, §16).
|
|
13
|
+
|
|
14
|
+
Canonical output is delegated to :func:`baseaicore.canonical_json` rather than reimplemented. That
|
|
15
|
+
is deliberate: a machine fingerprint, a runtime profile hash and an exported payload must agree
|
|
16
|
+
byte-for-byte, and two independent canonicalizers eventually disagree about a float or a
|
|
17
|
+
non-ASCII string.
|
|
18
|
+
|
|
19
|
+
Untrusted input is bounded before it is parsed, not after. :data:`MAX_PAYLOAD_BYTES` and
|
|
20
|
+
:data:`MAX_PAYLOAD_DEPTH` are module constants rather than configuration because the supported
|
|
21
|
+
limits of a build are part of what that build promises (spec §12), and a caller that needs a
|
|
22
|
+
tighter bound passes one per call.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from datetime import UTC, datetime
|
|
29
|
+
from typing import TYPE_CHECKING, Annotated, Any, Final
|
|
30
|
+
|
|
31
|
+
from baseaicore import (
|
|
32
|
+
UNSUPPORTED,
|
|
33
|
+
Measurement,
|
|
34
|
+
Unsupported,
|
|
35
|
+
ValidationError,
|
|
36
|
+
canonical_json,
|
|
37
|
+
from_rfc3339,
|
|
38
|
+
is_supported,
|
|
39
|
+
to_rfc3339,
|
|
40
|
+
)
|
|
41
|
+
from pydantic import (
|
|
42
|
+
BaseModel,
|
|
43
|
+
BeforeValidator,
|
|
44
|
+
GetCoreSchemaHandler,
|
|
45
|
+
GetJsonSchemaHandler,
|
|
46
|
+
PlainSerializer,
|
|
47
|
+
WithJsonSchema,
|
|
48
|
+
)
|
|
49
|
+
from pydantic.json_schema import JsonSchemaValue
|
|
50
|
+
from pydantic_core import core_schema
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from collections.abc import Mapping
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"MAX_PAYLOAD_BYTES",
|
|
57
|
+
"MAX_PAYLOAD_DEPTH",
|
|
58
|
+
"UNSUPPORTED_JSON",
|
|
59
|
+
"MeasurementField",
|
|
60
|
+
"TimestampField",
|
|
61
|
+
"canonical_dumps",
|
|
62
|
+
"parse_json",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
UNSUPPORTED_JSON: Final[str] = "unsupported"
|
|
66
|
+
"""The JSON form of an unavailable measurement, fixed by ADR-0016 §4."""
|
|
67
|
+
|
|
68
|
+
MAX_PAYLOAD_DEPTH: Final[int] = 64
|
|
69
|
+
"""Deepest container nesting accepted from untrusted JSON.
|
|
70
|
+
|
|
71
|
+
Chosen to sit far above any payload this suite defines — an evidence bundle nests about six deep,
|
|
72
|
+
and the most elaborate provenance block roughly ten — and far below CPython's default recursion
|
|
73
|
+
limit of 1000. The gap is the point: a hostile document is refused by a named limit with a useful
|
|
74
|
+
error, rather than by a ``RecursionError`` raised somewhere inside the JSON parser.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
MAX_PAYLOAD_BYTES: Final[int] = 16 * 1024 * 1024
|
|
78
|
+
"""Largest single document accepted from untrusted JSON, in bytes of UTF-8.
|
|
79
|
+
|
|
80
|
+
Matches the prompt-bearing request body limit in
|
|
81
|
+
API Standards §10, so a payload that a suite
|
|
82
|
+
API would accept is one this parser will also accept. Large exports are not meant to arrive as one
|
|
83
|
+
document: they are JSONL with one envelope per line (spec §15), which keeps both the parser and
|
|
84
|
+
the consumer's memory bounded regardless of how many results the file holds.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_MICROSECONDS_PER_MILLISECOND = 1_000
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_measurement(value: Any) -> Measurement: # noqa: ANN401 — accepts raw JSON scalars
|
|
91
|
+
"""Accept a number or ``"unsupported"``; refuse anything that would fabricate a measurement."""
|
|
92
|
+
# `isinstance` rather than `is UNSUPPORTED`: the sentinel is a singleton so the two are
|
|
93
|
+
# equivalent at runtime, but comparing an `Any` against a constant makes mypy widen the
|
|
94
|
+
# constant to `Any` too, and the narrowed form keeps the return type honest.
|
|
95
|
+
if isinstance(value, Unsupported):
|
|
96
|
+
return value
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
if value == UNSUPPORTED_JSON:
|
|
99
|
+
return UNSUPPORTED
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"a measurement is a number or the string {UNSUPPORTED_JSON!r}; got the string "
|
|
102
|
+
f"{value!r}. A measurement is never parsed from text, because a string that looks "
|
|
103
|
+
"like a number is usually a bug upstream rather than a reading."
|
|
104
|
+
)
|
|
105
|
+
# bool is an int in Python, and `True` reaching a metric means a flag was wired to a value.
|
|
106
|
+
if isinstance(value, bool):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"a bool is not a measurement; use a number, or UNSUPPORTED if this environment "
|
|
109
|
+
"cannot provide the value (ADR-0016)"
|
|
110
|
+
)
|
|
111
|
+
if isinstance(value, int | float):
|
|
112
|
+
# Narrowed from `Any` by the isinstance above; the annotation restates it for mypy.
|
|
113
|
+
numeric: int | float = value
|
|
114
|
+
return numeric
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"a measurement is a number or the string {UNSUPPORTED_JSON!r}; got "
|
|
117
|
+
f"{type(value).__name__}. Never null and never 0 — an absent measurement is "
|
|
118
|
+
f"{UNSUPPORTED_JSON!r} (ADR-0016 §4)."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _serialize_measurement(value: Measurement) -> int | float | str:
|
|
123
|
+
"""Render a measurement as JSON: a number, or the string ``"unsupported"``."""
|
|
124
|
+
# `is_supported` is a TypeGuard, so this narrows for the type checker; a bare
|
|
125
|
+
# `value is UNSUPPORTED` test would not.
|
|
126
|
+
return value if is_supported(value) else UNSUPPORTED_JSON
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _MeasurementCodec:
|
|
130
|
+
"""Pydantic hooks for :data:`MeasurementField`.
|
|
131
|
+
|
|
132
|
+
A full core schema rather than a stack of ``Annotated`` validators: the underlying type is
|
|
133
|
+
``int | float | Unsupported``, and pydantic cannot build a schema for the sentinel class on its
|
|
134
|
+
own — it is a plain Python object with no fields. Replacing the schema outright also keeps the
|
|
135
|
+
generated JSON Schema honest, which matters because Phase 4 publishes it as package data.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def __get_pydantic_core_schema__(
|
|
140
|
+
cls, source: Any, handler: GetCoreSchemaHandler
|
|
141
|
+
) -> core_schema.CoreSchema:
|
|
142
|
+
"""Return the validate/serialize schema for a measurement."""
|
|
143
|
+
return core_schema.no_info_plain_validator_function(
|
|
144
|
+
_validate_measurement,
|
|
145
|
+
serialization=core_schema.plain_serializer_function_ser_schema(_serialize_measurement),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def __get_pydantic_json_schema__(
|
|
150
|
+
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
|
|
151
|
+
) -> JsonSchemaValue:
|
|
152
|
+
"""Return the JSON Schema form: a number, or the string ``"unsupported"``."""
|
|
153
|
+
return {"anyOf": [{"type": "number"}, {"const": UNSUPPORTED_JSON}]}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
MeasurementField = Annotated[Measurement, _MeasurementCodec]
|
|
157
|
+
"""A :data:`baseaicore.Measurement` field with its wire codec attached.
|
|
158
|
+
|
|
159
|
+
Use this for any quantity a sensor, provider or benchmark might fail to report. It accepts a
|
|
160
|
+
number or the string ``"unsupported"`` on the way in, emits the same two forms on the way out, and
|
|
161
|
+
refuses ``null``, ``0``-as-absent, bools and numeric strings — the four ways a measurement that was
|
|
162
|
+
never taken gets into a chart.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _validate_timestamp(value: Any) -> datetime: # noqa: ANN401 — accepts str or datetime
|
|
167
|
+
"""Parse and normalize a timestamp to UTC at millisecond precision, refusing naive input."""
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
try:
|
|
170
|
+
value = from_rfc3339(value)
|
|
171
|
+
except ValidationError as exc:
|
|
172
|
+
# `from_rfc3339` raises a SuiteError, which pydantic does not wrap. Letting it escape
|
|
173
|
+
# would mean a bad timestamp aborts validation immediately while every other bad field
|
|
174
|
+
# is reported together — so the caller would learn about one problem per attempt.
|
|
175
|
+
# Re-raised as ValueError, it joins the other field errors in one pydantic report.
|
|
176
|
+
raise ValueError(str(exc)) from exc
|
|
177
|
+
if not isinstance(value, datetime):
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"a timestamp is an RFC 3339 string or a timezone-aware datetime; got "
|
|
180
|
+
f"{type(value).__name__}"
|
|
181
|
+
)
|
|
182
|
+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
"a naive datetime has no defensible UTC reading, and guessing one would shift every "
|
|
185
|
+
"downstream timestamp by the local offset; attach a timezone (ADR-0016 sibling rule, "
|
|
186
|
+
"coding standards §5)"
|
|
187
|
+
)
|
|
188
|
+
in_utc = value.astimezone(UTC)
|
|
189
|
+
# Truncate to the precision `to_rfc3339` emits, so `load(dump(x)) == x` holds for a datetime
|
|
190
|
+
# built from `utc_now()`, which carries microseconds. Without this the model would round-trip
|
|
191
|
+
# to a *different* instant and the round-trip contract (spec §11.3) would be false.
|
|
192
|
+
truncated = (
|
|
193
|
+
in_utc.microsecond // _MICROSECONDS_PER_MILLISECOND
|
|
194
|
+
) * _MICROSECONDS_PER_MILLISECOND
|
|
195
|
+
return in_utc.replace(microsecond=truncated)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
TimestampField = Annotated[
|
|
199
|
+
datetime,
|
|
200
|
+
BeforeValidator(_validate_timestamp),
|
|
201
|
+
PlainSerializer(to_rfc3339, return_type=str),
|
|
202
|
+
WithJsonSchema({"type": "string", "format": "date-time"}),
|
|
203
|
+
]
|
|
204
|
+
"""A timezone-aware UTC timestamp field, serialized as RFC 3339 with millisecond precision.
|
|
205
|
+
|
|
206
|
+
Accepts an RFC 3339 string or an aware :class:`~datetime.datetime` in any timezone, normalizes to
|
|
207
|
+
UTC, and truncates to milliseconds — the precision :func:`baseaicore.to_rfc3339` emits, so a value
|
|
208
|
+
survives a load/dump round trip unchanged. Naive datetimes are refused rather than assumed to be
|
|
209
|
+
UTC.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def canonical_dumps(value: BaseModel | Mapping[str, Any]) -> str:
|
|
214
|
+
"""Serialize a model or mapping to canonical JSON.
|
|
215
|
+
|
|
216
|
+
Canonical means byte-identical for equal input: keys sorted by code point, minimal separators,
|
|
217
|
+
non-ASCII emitted as itself, floats in their shortest round-tripping form. That property is
|
|
218
|
+
what lets a payload be hashed, compared across machines and diffed between releases
|
|
219
|
+
(spec §11.4, §16).
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
value: A pydantic model — dumped through its own serializers first, so measurements and
|
|
223
|
+
timestamps take their wire forms — or an already-plain mapping.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Canonical JSON text. Encode with UTF-8 to get the bytes that were promised.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValidationError: If the structure holds something canonical JSON cannot represent
|
|
230
|
+
unambiguously: a non-string mapping key, a non-finite float, a naive datetime, raw
|
|
231
|
+
bytes, a :class:`~decimal.Decimal`, or a reference cycle.
|
|
232
|
+
"""
|
|
233
|
+
plain = value.model_dump() if isinstance(value, BaseModel) else value
|
|
234
|
+
return canonical_json(plain)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def parse_json(
|
|
238
|
+
data: bytes | str,
|
|
239
|
+
*,
|
|
240
|
+
max_bytes: int = MAX_PAYLOAD_BYTES,
|
|
241
|
+
max_depth: int = MAX_PAYLOAD_DEPTH,
|
|
242
|
+
) -> Any: # noqa: ANN401 — returns whatever JSON structure the document held
|
|
243
|
+
"""Parse untrusted JSON text with both guards applied before the parser runs.
|
|
244
|
+
|
|
245
|
+
Order matters. Size is checked first because it is O(1) and rejects the cheapest attack; depth
|
|
246
|
+
is measured by scanning the text, before :func:`json.loads` is given a chance to exhaust the
|
|
247
|
+
C stack on a document that is nothing but ten thousand open brackets.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
data: JSON text, or its UTF-8 bytes.
|
|
251
|
+
max_bytes: Largest document accepted, in bytes of UTF-8. Defaults to
|
|
252
|
+
:data:`MAX_PAYLOAD_BYTES`; pass a smaller value when the caller knows its own bound
|
|
253
|
+
(spec §14 makes size limiting the caller's prerogative).
|
|
254
|
+
max_depth: Deepest container nesting accepted. Defaults to :data:`MAX_PAYLOAD_DEPTH`.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
The parsed structure: a mapping, list or scalar exactly as JSON defines it.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ValidationError: If the document exceeds ``max_bytes``, nests deeper than ``max_depth``,
|
|
261
|
+
is not valid UTF-8, or is not parsable JSON. Every message names the limit or the
|
|
262
|
+
parse position, because "too big" without a number tells the caller nothing.
|
|
263
|
+
"""
|
|
264
|
+
text = _decode(data)
|
|
265
|
+
size = len(text.encode("utf-8"))
|
|
266
|
+
if size > max_bytes:
|
|
267
|
+
raise ValidationError(
|
|
268
|
+
f"Payload is {size} bytes, over the {max_bytes}-byte limit. Large exports are "
|
|
269
|
+
"JSONL — one envelope per line — so that producer and consumer both stream "
|
|
270
|
+
"instead of holding the whole document in memory (spec §15).",
|
|
271
|
+
details={"size_bytes": size, "limit_bytes": max_bytes},
|
|
272
|
+
)
|
|
273
|
+
depth = _scan_depth(text)
|
|
274
|
+
if depth > max_depth:
|
|
275
|
+
raise ValidationError(
|
|
276
|
+
f"Payload nests {depth} containers deep, over the {max_depth}-level limit. No payload "
|
|
277
|
+
"this suite defines approaches that depth, so a document that does is either "
|
|
278
|
+
"corrupt or hostile.",
|
|
279
|
+
details={"depth": depth, "limit_depth": max_depth},
|
|
280
|
+
)
|
|
281
|
+
try:
|
|
282
|
+
return json.loads(text)
|
|
283
|
+
except json.JSONDecodeError as exc:
|
|
284
|
+
raise ValidationError(
|
|
285
|
+
f"Not parsable JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}.",
|
|
286
|
+
details={"line": exc.lineno, "column": exc.colno, "position": exc.pos},
|
|
287
|
+
) from exc
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _decode(data: bytes | str) -> str:
|
|
291
|
+
"""Return ``data`` as text, naming the byte offset when it is not valid UTF-8."""
|
|
292
|
+
if isinstance(data, str):
|
|
293
|
+
return data
|
|
294
|
+
try:
|
|
295
|
+
return data.decode("utf-8")
|
|
296
|
+
except UnicodeDecodeError as exc:
|
|
297
|
+
raise ValidationError(
|
|
298
|
+
f"Payload is not valid UTF-8 at byte {exc.start}: {exc.reason}. JSON on the wire is "
|
|
299
|
+
"UTF-8 (RFC 8259 §8.1).",
|
|
300
|
+
details={"position": exc.start, "reason": exc.reason},
|
|
301
|
+
) from exc
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _scan_depth(text: str) -> int:
|
|
305
|
+
"""Return the deepest container nesting in JSON text, counting brackets outside strings.
|
|
306
|
+
|
|
307
|
+
Scans rather than recurses so that measuring a hostile document cannot itself overflow the
|
|
308
|
+
stack — which is the whole point of checking depth before parsing.
|
|
309
|
+
"""
|
|
310
|
+
depth = 0
|
|
311
|
+
deepest = 0
|
|
312
|
+
in_string = False
|
|
313
|
+
escaped = False
|
|
314
|
+
for character in text:
|
|
315
|
+
if in_string:
|
|
316
|
+
if escaped:
|
|
317
|
+
escaped = False
|
|
318
|
+
elif character == "\\":
|
|
319
|
+
escaped = True
|
|
320
|
+
elif character == '"':
|
|
321
|
+
in_string = False
|
|
322
|
+
continue
|
|
323
|
+
if character == '"':
|
|
324
|
+
in_string = True
|
|
325
|
+
elif character in "{[":
|
|
326
|
+
depth += 1
|
|
327
|
+
deepest = max(deepest, depth)
|
|
328
|
+
elif character in "}]":
|
|
329
|
+
depth -= 1
|
|
330
|
+
return deepest
|
setspec/vocabulary.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Contract module — the suite's capability vocabulary: which terms exist, and their version.
|
|
2
|
+
|
|
3
|
+
Imports :mod:`baseaicore` for :class:`~baseaicore.CapabilityId`'s syntax rules; performs no I/O.
|
|
4
|
+
|
|
5
|
+
BaseAiCore owns only the *shape* of a capability ID (dotted, lowercase, `[a-z][a-z0-9_]*` segments)
|
|
6
|
+
so that adding a term never forces a BaseAiCore release. This module owns the *contents*: which
|
|
7
|
+
terms are real, what a specialization must specialize, and how strictly an unrecognized term is
|
|
8
|
+
treated ([spec §4](../../docs/packages/setspec/spec.md), traceability matrix "who owns the
|
|
9
|
+
capability vocabulary").
|
|
10
|
+
|
|
11
|
+
**Versioning** ([spec §11.8](../../docs/packages/setspec/spec.md)): additions are minor, a removal
|
|
12
|
+
or a redefinition is major. ``CAPABILITY_VOCABULARY_VERSION`` reuses the same ``MAJOR.MINOR`` shape
|
|
13
|
+
as every schema version in this package, via :class:`~setspec.envelope.SchemaVersion`, so "is this
|
|
14
|
+
payload's vocabulary newer than mine" is the same comparison as everywhere else, not a second
|
|
15
|
+
version format to reason about.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Final
|
|
21
|
+
|
|
22
|
+
from baseaicore import CapabilityId
|
|
23
|
+
from baseaicore import ValidationError as SuiteValidationError
|
|
24
|
+
|
|
25
|
+
from setspec.envelope import SchemaVersion
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"CAPABILITIES",
|
|
29
|
+
"CAPABILITY_VOCABULARY_VERSION",
|
|
30
|
+
"RESERVED_ROOTS",
|
|
31
|
+
"is_known_capability",
|
|
32
|
+
"validate_capability",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
CAPABILITY_VOCABULARY_VERSION: Final[str] = "1.1"
|
|
36
|
+
"""The vocabulary's own version, independent of every schema and package version in this build."""
|
|
37
|
+
|
|
38
|
+
CAPABILITIES: Final[frozenset[str]] = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"reasoning",
|
|
41
|
+
"coding",
|
|
42
|
+
"code_review",
|
|
43
|
+
"auditing",
|
|
44
|
+
"debugging",
|
|
45
|
+
"instruction_following",
|
|
46
|
+
"structured_output",
|
|
47
|
+
"tool_use",
|
|
48
|
+
"agentic",
|
|
49
|
+
"summarization",
|
|
50
|
+
"creative_writing",
|
|
51
|
+
"judging",
|
|
52
|
+
"critiquing",
|
|
53
|
+
"long_context",
|
|
54
|
+
"speed",
|
|
55
|
+
"latency",
|
|
56
|
+
"memory_efficiency",
|
|
57
|
+
"token_efficiency",
|
|
58
|
+
"energy_efficiency",
|
|
59
|
+
"reliability",
|
|
60
|
+
# 1.1 — reserved; valid only as a specialization. See RESERVED_ROOTS.
|
|
61
|
+
"user",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
"""Every known **root** as of :data:`CAPABILITY_VOCABULARY_VERSION`, drawn from the capabilities
|
|
65
|
+
FreeWeight's benchmark catalog already maps results onto.
|
|
66
|
+
|
|
67
|
+
Roots only — never a specialization. A specialization is valid whenever its own root is a member
|
|
68
|
+
of this set: ``coding.python`` and ``coding.rust`` are both accepted the moment ``coding`` is
|
|
69
|
+
known, because :class:`~baseaicore.CapabilityId` already defines ``coding.rust`` as inheriting
|
|
70
|
+
from ``coding`` (its root), and asking a maintainer to pre-enumerate every language, framework or
|
|
71
|
+
task variant a root might be specialized into would make the vocabulary permanently incomplete by
|
|
72
|
+
construction. :func:`validate_capability` checks a candidate's *root* against this set, never the
|
|
73
|
+
full dotted string, so a root's specializations are open-ended while an unknown root is still
|
|
74
|
+
rejected outright.
|
|
75
|
+
|
|
76
|
+
This is a starting vocabulary, not a closed one — Phase 2's own risk note is "guessing the result
|
|
77
|
+
shape," and a root FreeWeight's real benchmarks need that is missing here is exactly the kind of
|
|
78
|
+
gap Phase 4's freeze against real output is meant to find and correct with a minor version bump.
|
|
79
|
+
|
|
80
|
+
``user`` was added at ``1.1`` and is a member of this set only so that the root rule above accepts
|
|
81
|
+
its specializations; it is refused in its bare form. See :data:`RESERVED_ROOTS`.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
RESERVED_ROOTS: Final[frozenset[str]] = frozenset({"user"})
|
|
85
|
+
"""Roots that are valid **only** as a specialization; the bare root is refused.
|
|
86
|
+
|
|
87
|
+
``user`` is the sole member. It carries FreeWeight's user-authored goal evidence as
|
|
88
|
+
``user.<slug>`` (ADR-0032 §1), and one root added once closes the question permanently: the
|
|
89
|
+
open-ended specialization rule already accepts ``user.noir_tech_voice`` and every other goal any
|
|
90
|
+
user will ever write, so no future rubric is a vocabulary change.
|
|
91
|
+
|
|
92
|
+
The bare form is refused because it would mean nothing. Every other root in
|
|
93
|
+
:data:`CAPABILITIES` names a real, measurable capability that a benchmark maps onto; ``user``
|
|
94
|
+
names only the fact that a user defined something. A payload claiming ``capability_id: "user"``
|
|
95
|
+
has lost the identity that is the entire point of the namespace, and accepting it would let that
|
|
96
|
+
loss pass silently into a routing decision.
|
|
97
|
+
|
|
98
|
+
Membership in this set is checked *in addition to* :data:`CAPABILITIES`, not instead of it, so a
|
|
99
|
+
reserved root's specializations follow exactly the same rule as ``coding.rust``.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_known_capability(capability_id: str) -> bool:
|
|
104
|
+
"""Report whether ``capability_id`` is both syntactically valid and in the current vocabulary.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
capability_id: The candidate term.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
``True`` iff ``capability_id`` parses as a :class:`~baseaicore.CapabilityId` and its root
|
|
111
|
+
is a member of :data:`CAPABILITIES` — so a specialization of a known root, even one never
|
|
112
|
+
explicitly enumerated, reports ``True``. A bare :data:`RESERVED_ROOTS` member is
|
|
113
|
+
``False``: ``user`` is not itself a capability, only a namespace for them. A
|
|
114
|
+
syntactically invalid string is ``False``, not an exception — this function answers a
|
|
115
|
+
yes/no question; :func:`validate_capability` is the one that raises.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
parsed = CapabilityId(capability_id)
|
|
119
|
+
except SuiteValidationError:
|
|
120
|
+
return False
|
|
121
|
+
if parsed.root not in CAPABILITIES:
|
|
122
|
+
return False
|
|
123
|
+
return parsed.is_specialization or parsed.root not in RESERVED_ROOTS
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_capability(
|
|
127
|
+
capability_id: str,
|
|
128
|
+
*,
|
|
129
|
+
vocabulary_version: str | None = None,
|
|
130
|
+
) -> CapabilityId:
|
|
131
|
+
"""Validate a capability ID's syntax and, by default, its membership in the vocabulary.
|
|
132
|
+
|
|
133
|
+
Membership is enforced strictly unless ``vocabulary_version`` proves the payload comes from a
|
|
134
|
+
*newer minor* of the same major than this build knows — the forward-compatibility rule in
|
|
135
|
+
[spec §13](../../docs/packages/setspec/spec.md): "unknown capability ID: ``ValidationError``
|
|
136
|
+
when strict; a preserved warning when the payload's vocabulary version is newer." SetSpec has
|
|
137
|
+
no logging ([spec §17](../../docs/packages/setspec/spec.md)), so "preserved" is literal: the
|
|
138
|
+
term is accepted and returned rather than rejected, and a caller who wants to know whether
|
|
139
|
+
leniency was actually applied checks :func:`is_known_capability` itself.
|
|
140
|
+
|
|
141
|
+
A newer *major* is not given this leniency: rule 2 says a major vocabulary change may remove
|
|
142
|
+
or redefine a term, so an unrecognized ID under a newer major could just as easily be a
|
|
143
|
+
genuine removal as an addition, and treating it as forward-compatible would let the wrong case
|
|
144
|
+
through silently.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
capability_id: The candidate term.
|
|
148
|
+
vocabulary_version: The vocabulary version the payload declares it was written against,
|
|
149
|
+
if known. Compared against :data:`CAPABILITY_VOCABULARY_VERSION` using the same
|
|
150
|
+
``MAJOR.MINOR`` rules every schema version uses.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The parsed, syntactically valid :class:`~baseaicore.CapabilityId`.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValidationError: If ``capability_id`` is not syntactically valid at all; if it is a bare
|
|
157
|
+
:data:`RESERVED_ROOTS` member such as ``"user"``, which is a namespace rather than a
|
|
158
|
+
capability; or if its root is unrecognized and no forward-compatibility exception
|
|
159
|
+
applies. A specialization of a known root — ``coding.rust`` when ``coding`` is known,
|
|
160
|
+
``user.house_voice`` when ``user`` is — is never rejected on that basis alone, even
|
|
161
|
+
when the specialization itself was never explicitly enumerated.
|
|
162
|
+
|
|
163
|
+
A bare reserved root is refused **regardless of** ``vocabulary_version``: forward
|
|
164
|
+
compatibility exists so an older build accepts a term a newer one added, and no
|
|
165
|
+
future minor can turn ``user`` into a capability, because what it lacks is a
|
|
166
|
+
specialization rather than a vocabulary entry.
|
|
167
|
+
"""
|
|
168
|
+
parsed = CapabilityId(capability_id) # raises baseaicore.ValidationError on bad syntax
|
|
169
|
+
if parsed.root in RESERVED_ROOTS and not parsed.is_specialization:
|
|
170
|
+
raise SuiteValidationError(
|
|
171
|
+
f"{capability_id!r} is a reserved namespace, not a capability. Use a specialization "
|
|
172
|
+
f"such as {capability_id}.my_goal — a bare {capability_id!r} carries no identity, "
|
|
173
|
+
"which is the whole reason the namespace exists.",
|
|
174
|
+
details={
|
|
175
|
+
"field": "capability_id",
|
|
176
|
+
"value": capability_id,
|
|
177
|
+
"reserved_roots": sorted(RESERVED_ROOTS),
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
if parsed.root in CAPABILITIES:
|
|
181
|
+
return parsed
|
|
182
|
+
if _is_forward_compatible(vocabulary_version):
|
|
183
|
+
return parsed
|
|
184
|
+
raise SuiteValidationError(
|
|
185
|
+
f"{capability_id!r} is not a known capability in vocabulary "
|
|
186
|
+
f"{CAPABILITY_VOCABULARY_VERSION}. Known roots and specializations are listed in "
|
|
187
|
+
"setspec.vocabulary.CAPABILITIES.",
|
|
188
|
+
details={
|
|
189
|
+
"field": "capability_id",
|
|
190
|
+
"value": capability_id,
|
|
191
|
+
"vocabulary_version": CAPABILITY_VOCABULARY_VERSION,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _is_forward_compatible(vocabulary_version: str | None) -> bool:
|
|
197
|
+
"""Report whether ``vocabulary_version`` is a newer minor of this build's known major."""
|
|
198
|
+
if vocabulary_version is None:
|
|
199
|
+
return False
|
|
200
|
+
try:
|
|
201
|
+
declared = SchemaVersion.parse(vocabulary_version)
|
|
202
|
+
known = SchemaVersion.parse(CAPABILITY_VOCABULARY_VERSION)
|
|
203
|
+
except SuiteValidationError:
|
|
204
|
+
return False
|
|
205
|
+
return declared.major == known.major and declared > known
|