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
|
File without changes
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Run an offline decision and optionally write a self-contained HTML explanation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from decision import contract
|
|
13
|
+
from decision.engine import decide as run_decision
|
|
14
|
+
from decision.engine import validate as validate_decision
|
|
15
|
+
from decision.registry import facet
|
|
16
|
+
|
|
17
|
+
from .vocabulary_cache import (
|
|
18
|
+
VocabularyInvalidError,
|
|
19
|
+
VocabularyMissingError,
|
|
20
|
+
load_cached_vocabulary,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
EXIT_ERROR = 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _facet_lookup() -> contract.FacetLookup:
|
|
27
|
+
return facet
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _leaf_conditions(condition: Any, path: str):
|
|
31
|
+
if isinstance(condition, contract.AnyOf | contract.AllOf):
|
|
32
|
+
name = "any" if isinstance(condition, contract.AnyOf) else "all"
|
|
33
|
+
for index, child in enumerate(getattr(condition, name)):
|
|
34
|
+
yield from _leaf_conditions(child, f"{path}.{name}[{index}]")
|
|
35
|
+
elif isinstance(condition, contract.NotOf):
|
|
36
|
+
yield from _leaf_conditions(condition.not_, f"{path}.not")
|
|
37
|
+
else:
|
|
38
|
+
yield condition, path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _vocabulary_warnings(spec: contract.Spec, vocabulary: dict[str, Any]) -> list[str]:
|
|
42
|
+
warnings: list[str] = []
|
|
43
|
+
published = {
|
|
44
|
+
row["id"]
|
|
45
|
+
for section in ("facets", "benchmarks", "domains")
|
|
46
|
+
for row in vocabulary.get(section, [])
|
|
47
|
+
}
|
|
48
|
+
providers = set(vocabulary.get("providers", {}))
|
|
49
|
+
domains = {row["id"] for row in vocabulary.get("domains", [])}
|
|
50
|
+
|
|
51
|
+
def add(path: str, value: str) -> None:
|
|
52
|
+
warning = (
|
|
53
|
+
f"{path}: {value!r} has no verified evidence in this snapshot; "
|
|
54
|
+
"results will be empty or may_qualify"
|
|
55
|
+
)
|
|
56
|
+
if warning not in warnings:
|
|
57
|
+
warnings.append(warning)
|
|
58
|
+
|
|
59
|
+
for domain in spec.capabilities or {}:
|
|
60
|
+
if domain not in domains:
|
|
61
|
+
add(f"capabilities.{domain}", domain)
|
|
62
|
+
if spec.task_type is not None and spec.task_type not in set(vocabulary.get("task_types", [])):
|
|
63
|
+
add("task_type", spec.task_type)
|
|
64
|
+
if isinstance(spec.profile, contract.InventoryProfile):
|
|
65
|
+
for index, offering in enumerate(spec.profile.offerings):
|
|
66
|
+
if offering.provider not in providers:
|
|
67
|
+
add(f"profile.offerings[{index}].provider", offering.provider)
|
|
68
|
+
|
|
69
|
+
conditions = list(enumerate(spec.where))
|
|
70
|
+
if isinstance(spec.profile, contract.InventoryProfile):
|
|
71
|
+
for index, condition in enumerate(spec.profile.rules):
|
|
72
|
+
for leaf, path in _leaf_conditions(condition, f"profile.rules[{index}]"):
|
|
73
|
+
conditions.append((path, leaf))
|
|
74
|
+
for index, condition in conditions:
|
|
75
|
+
path = f"where[{index}]" if isinstance(index, int) else index
|
|
76
|
+
leaves = (
|
|
77
|
+
_leaf_conditions(condition, path)
|
|
78
|
+
if isinstance(index, int)
|
|
79
|
+
else [(condition, path)]
|
|
80
|
+
)
|
|
81
|
+
for leaf, leaf_path in leaves:
|
|
82
|
+
facet_id = leaf.known if isinstance(leaf, contract.Known) else leaf.facet
|
|
83
|
+
if facet_id == "offering.provider" and not isinstance(leaf, contract.Known):
|
|
84
|
+
values = (
|
|
85
|
+
(leaf.in_ or leaf.not_in)
|
|
86
|
+
if isinstance(leaf, contract.InSet)
|
|
87
|
+
else [getattr(leaf, "value", None)]
|
|
88
|
+
)
|
|
89
|
+
for value in values or []:
|
|
90
|
+
if isinstance(value, str) and value not in providers:
|
|
91
|
+
add(leaf_path, value)
|
|
92
|
+
if facet_id not in published:
|
|
93
|
+
add(leaf_path, facet_id)
|
|
94
|
+
objective = spec.optimize
|
|
95
|
+
objective_facets = [objective.max, objective.min]
|
|
96
|
+
objective_facets += list(objective.weights or {}) + list(objective.pareto or [])
|
|
97
|
+
objective_facets += [step.facet for step in objective.lexicographic or []]
|
|
98
|
+
for facet_id in objective_facets:
|
|
99
|
+
if facet_id and facet_id.removeprefix("-") not in published:
|
|
100
|
+
add("optimize", facet_id.removeprefix("-"))
|
|
101
|
+
return warnings
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _issues(exc: contract.SpecError, vocabulary: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
105
|
+
known = set()
|
|
106
|
+
if vocabulary is not None:
|
|
107
|
+
known = {
|
|
108
|
+
row["id"]
|
|
109
|
+
for section in ("facets", "benchmarks", "domains")
|
|
110
|
+
for row in vocabulary.get(section, [])
|
|
111
|
+
}
|
|
112
|
+
rendered = []
|
|
113
|
+
for issue in exc.issues:
|
|
114
|
+
reason = issue.reason
|
|
115
|
+
if issue.field and "unknown facet" in reason:
|
|
116
|
+
close = difflib.get_close_matches(issue.field, sorted(known), n=3, cutoff=0.6)
|
|
117
|
+
extra = [item for item in close if repr(item) not in reason]
|
|
118
|
+
if extra:
|
|
119
|
+
reason += f"; cached vocabulary suggests {', '.join(repr(item) for item in extra)}"
|
|
120
|
+
rendered.append({
|
|
121
|
+
"path": issue.path, "condition": issue.condition,
|
|
122
|
+
"field": issue.field, "reason": reason,
|
|
123
|
+
})
|
|
124
|
+
return rendered
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _summary(spec: contract.Spec) -> str:
|
|
128
|
+
objective = spec.optimize
|
|
129
|
+
prefers = (len(objective.weights or {}) + len(objective.pareto or [])
|
|
130
|
+
+ len(objective.lexicographic or [])
|
|
131
|
+
+ int(objective.max is not None) + int(objective.min is not None))
|
|
132
|
+
return f"musts: {len(spec.where)}, prefers: {prefers}, snapshot: {spec.snapshot}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _fail(payload: dict[str, Any], lines: list[str], as_json: bool) -> None:
|
|
136
|
+
if as_json:
|
|
137
|
+
typer.echo(json.dumps(payload, indent=2), err=True)
|
|
138
|
+
else:
|
|
139
|
+
for line in lines:
|
|
140
|
+
typer.echo(line, err=True)
|
|
141
|
+
raise typer.Exit(EXIT_ERROR)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _template(templates: list[dict[str, Any]], template_id: str, as_json: bool) -> dict[str, Any]:
|
|
145
|
+
for row in templates:
|
|
146
|
+
if row.get("id") == template_id:
|
|
147
|
+
spec = row.get("spec")
|
|
148
|
+
if isinstance(spec, dict):
|
|
149
|
+
return row
|
|
150
|
+
valid = sorted(str(row.get("id")) for row in templates if isinstance(row.get("id"), str))
|
|
151
|
+
message = f"unknown template {template_id!r}; valid ids: {', '.join(valid)}"
|
|
152
|
+
_fail(
|
|
153
|
+
{"contract_version": contract.CONTRACT_VERSION, "command": "decide",
|
|
154
|
+
"error": {"code": "unknown_template", "message": message, "valid_ids": valid}},
|
|
155
|
+
[f"error: {message}"],
|
|
156
|
+
as_json,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _merge_template(template: dict[str, Any], raw: Any) -> dict[str, Any]:
|
|
161
|
+
fragment = dict(template["spec"])
|
|
162
|
+
if raw is None:
|
|
163
|
+
return fragment
|
|
164
|
+
if not isinstance(raw, dict):
|
|
165
|
+
return raw
|
|
166
|
+
template_where = fragment.get("where") or []
|
|
167
|
+
file_where = raw.get("where") or []
|
|
168
|
+
merged = fragment | raw
|
|
169
|
+
merged["where"] = [*template_where, *file_where]
|
|
170
|
+
return merged
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def decide(
|
|
174
|
+
spec_path: Optional[Path] = typer.Argument( # noqa: UP045 - Typer reads the annotation
|
|
175
|
+
None, help="The optional spec, as YAML. Required without --template."
|
|
176
|
+
),
|
|
177
|
+
template: Optional[str] = typer.Option( # noqa: UP045 - Typer reads the annotation
|
|
178
|
+
None, "--template", help="Start from a template in the cached vocabulary."
|
|
179
|
+
),
|
|
180
|
+
explain: Optional[str] = typer.Option( # noqa: UP045 - Typer reads the annotation
|
|
181
|
+
None, "--explain", help="Override the spec's explain: none, summary or full."
|
|
182
|
+
),
|
|
183
|
+
as_json: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
184
|
+
snapshot_file: Optional[Path] = typer.Option( # noqa: UP045 - Typer annotation
|
|
185
|
+
None,
|
|
186
|
+
"--snapshot-file",
|
|
187
|
+
envvar="MODELSPEC_DECISION_SNAPSHOT",
|
|
188
|
+
help="Local decision snapshot (.gz).",
|
|
189
|
+
),
|
|
190
|
+
html: Optional[Path] = typer.Option( # noqa: UP045 - Typer annotation
|
|
191
|
+
None, "--html", help="Write a self-contained HTML report."
|
|
192
|
+
),
|
|
193
|
+
check: bool = typer.Option(
|
|
194
|
+
False, "--check", help="Validate against the cached vocabulary without deciding."
|
|
195
|
+
),
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Decide which model or offering fits a spec (the decision contract, v1)."""
|
|
198
|
+
base = {"contract_version": contract.CONTRACT_VERSION, "command": "decide"}
|
|
199
|
+
if spec_path is None and template is None:
|
|
200
|
+
_fail(
|
|
201
|
+
base | {"error": {
|
|
202
|
+
"code": "spec_required", "message": "pass SPEC.yaml or --template ID"
|
|
203
|
+
}},
|
|
204
|
+
["error: pass SPEC.yaml or --template ID"],
|
|
205
|
+
as_json,
|
|
206
|
+
)
|
|
207
|
+
text = ""
|
|
208
|
+
if spec_path is not None:
|
|
209
|
+
try:
|
|
210
|
+
text = spec_path.read_text()
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
_fail(
|
|
213
|
+
base | {"error": {"code": "unreadable", "message": str(exc)}},
|
|
214
|
+
[f"error: cannot read {spec_path}: {exc.strerror or exc}"],
|
|
215
|
+
as_json,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
vocabulary: dict[str, Any] | None = None
|
|
219
|
+
if check or template is not None:
|
|
220
|
+
try:
|
|
221
|
+
vocabulary = load_cached_vocabulary()
|
|
222
|
+
except VocabularyMissingError as exc:
|
|
223
|
+
_fail(base | {"error": {"code": "snapshot_required", "message": str(exc)}},
|
|
224
|
+
[f"error: {exc}"], as_json)
|
|
225
|
+
except VocabularyInvalidError as exc:
|
|
226
|
+
_fail(base | {"error": {"code": "decision_failed", "message": str(exc)}},
|
|
227
|
+
[f"error: {exc}"], as_json)
|
|
228
|
+
templates = vocabulary.get("templates", []) if vocabulary is not None else []
|
|
229
|
+
if template is not None and not isinstance(templates, list):
|
|
230
|
+
message = "cached decision vocabulary has an invalid templates field"
|
|
231
|
+
_fail(base | {"error": {"code": "decision_failed", "message": message}},
|
|
232
|
+
[f"error: {message}"], as_json)
|
|
233
|
+
facets = _facet_lookup()
|
|
234
|
+
selected_template: dict[str, Any] | None = None
|
|
235
|
+
try:
|
|
236
|
+
raw = contract.load_yaml(text) if spec_path is not None else None
|
|
237
|
+
if template is not None:
|
|
238
|
+
selected_template = _template(templates, template, as_json)
|
|
239
|
+
raw = _merge_template(selected_template, raw)
|
|
240
|
+
if explain is not None and isinstance(raw, dict):
|
|
241
|
+
raw = raw | {"explain": explain}
|
|
242
|
+
spec = contract.parse_spec(raw, facets=facets)
|
|
243
|
+
except contract.SpecError as exc:
|
|
244
|
+
issues = _issues(exc, vocabulary)
|
|
245
|
+
_fail(
|
|
246
|
+
base | {"error": {"code": "invalid_spec", "issues": issues}},
|
|
247
|
+
["error: invalid spec", *(f" {issue}" for issue in exc.issues)],
|
|
248
|
+
as_json,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
base |= {"spec_hash": contract.spec_hash(spec), "explain": spec.explain}
|
|
252
|
+
template_warning = None
|
|
253
|
+
if selected_template is not None and not selected_template.get("available", True):
|
|
254
|
+
template_warning = selected_template.get("unavailable_reason") or (
|
|
255
|
+
"this template is unavailable against the cached snapshot"
|
|
256
|
+
)
|
|
257
|
+
typer.echo(f"warning: {template_warning}", err=True)
|
|
258
|
+
using_cached_snapshot = snapshot_file is None
|
|
259
|
+
if using_cached_snapshot:
|
|
260
|
+
from .snapshot import decision_snapshot_path
|
|
261
|
+
|
|
262
|
+
snapshot_file = decision_snapshot_path()
|
|
263
|
+
if not snapshot_file.is_file():
|
|
264
|
+
message = (
|
|
265
|
+
"no cached decision snapshot. Run `modelspec snapshot fetch`, "
|
|
266
|
+
"pass --snapshot-file, or set MODELSPEC_DECISION_SNAPSHOT"
|
|
267
|
+
)
|
|
268
|
+
_fail(
|
|
269
|
+
base | {"error": {"code": "snapshot_required", "message": message}},
|
|
270
|
+
[f"error: {message}"],
|
|
271
|
+
as_json,
|
|
272
|
+
)
|
|
273
|
+
if html is not None and spec.explain != "full":
|
|
274
|
+
_fail(
|
|
275
|
+
base
|
|
276
|
+
| {"error": {"code": "full_required", "message": "--html requires --explain full"}},
|
|
277
|
+
["error: --html requires --explain full"],
|
|
278
|
+
as_json,
|
|
279
|
+
)
|
|
280
|
+
from decision.snapshot import load_snapshot
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
index = (load_snapshot(snapshot_file, key=None, include_archive=True)
|
|
284
|
+
if using_cached_snapshot
|
|
285
|
+
else load_snapshot(snapshot_file, include_archive=True))
|
|
286
|
+
if check:
|
|
287
|
+
validate_decision(spec, index, facets=facets)
|
|
288
|
+
assert vocabulary is not None
|
|
289
|
+
warnings = _vocabulary_warnings(spec, vocabulary)
|
|
290
|
+
if template_warning is not None and template_warning not in warnings:
|
|
291
|
+
warnings.insert(0, template_warning)
|
|
292
|
+
cached_snapshot = vocabulary.get("snapshot")
|
|
293
|
+
if spec.snapshot != "latest" and spec.snapshot != cached_snapshot:
|
|
294
|
+
warnings.append(
|
|
295
|
+
f"spec pins {spec.snapshot}, but the cached vocabulary describes "
|
|
296
|
+
f"{cached_snapshot}"
|
|
297
|
+
)
|
|
298
|
+
for warning in warnings:
|
|
299
|
+
typer.echo(f"warning: {warning}", err=True)
|
|
300
|
+
summary = _summary(spec)
|
|
301
|
+
if as_json:
|
|
302
|
+
payload = base | {
|
|
303
|
+
"ok": True, "summary": summary, "snapshot": cached_snapshot,
|
|
304
|
+
"warnings": warnings,
|
|
305
|
+
}
|
|
306
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
307
|
+
else:
|
|
308
|
+
typer.echo(f"ok: {summary}")
|
|
309
|
+
return
|
|
310
|
+
result = run_decision(spec, index, facets=facets)
|
|
311
|
+
if html is not None:
|
|
312
|
+
from decision.explain import render_html
|
|
313
|
+
|
|
314
|
+
html.write_text(render_html(result, index), encoding="utf-8")
|
|
315
|
+
except contract.SpecError as exc:
|
|
316
|
+
issues = _issues(exc, vocabulary)
|
|
317
|
+
_fail(
|
|
318
|
+
base | {"error": {"code": "decision_failed", "issues": issues}},
|
|
319
|
+
["error: decision failed", *(f" {i['path']}: {i['reason']}" for i in issues)],
|
|
320
|
+
as_json,
|
|
321
|
+
)
|
|
322
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
323
|
+
_fail(
|
|
324
|
+
base | {"error": {"code": "decision_failed", "message": str(exc)}},
|
|
325
|
+
[f"error: {exc}"],
|
|
326
|
+
as_json,
|
|
327
|
+
)
|
|
328
|
+
if as_json:
|
|
329
|
+
# The Worker's body byte for byte (api/worker/src/decide_service.serialise).
|
|
330
|
+
typer.echo(json.dumps(result.model_dump(mode="json"), ensure_ascii=False,
|
|
331
|
+
separators=(",", ":")))
|
|
332
|
+
else:
|
|
333
|
+
typer.echo(result.model_dump_json(indent=2))
|