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/filter.py ADDED
@@ -0,0 +1,796 @@
1
+ """Three-valued filter (MODEL-141, design §6.2).
2
+
3
+ A condition is pass, fail or unknown for each candidate. ``all`` is Kleene AND:
4
+ a fail wins over an unknown. ``any`` is Kleene OR: a pass wins over an unknown.
5
+ ``not`` swaps pass and fail and leaves unknown unknown.
6
+
7
+ The unknown policy comes from the condition, otherwise from the facets that
8
+ are unknown for that candidate. Capability moves the candidate to
9
+ ``may_qualify``. A governance unknown does not pass; the elimination is
10
+ surfaced as ``unverified: may qualify``. A governance facet the candidate
11
+ already passes does not change this. A per-condition ``unknown`` override wins.
12
+ ``soft`` does not change who remains. No condition adds a score.
13
+
14
+ ``ids_where`` is called with ``= != < <= > >=`` and ``known``. A window is
15
+ ``>=`` intersected with ``<=``. A set is an ``any`` of ``=``, or on a
16
+ set-valued facet one ``contains_any``. An evidence
17
+ condition calls ``evidence`` and then applies its qualifiers; ``@direct`` asks
18
+ whether the benchmark is direct for a capability the spec requests. Retired
19
+ candidates are excluded unless the resolved spec asks for lifecycle ``retired``.
20
+
21
+ A model with offerings is represented by them (MODEL-159). Its bare model row
22
+ would tie with them on the evidence they inherit, so it never enters the
23
+ lineup and is not reported as eliminated. A model with no offering (open
24
+ weights run on one's own hardware) is its own row.
25
+
26
+ The shared snapshot protocol lives in ``decision/snapshot.py`` (MODEL-138).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Callable, Sequence
32
+ from dataclasses import dataclass
33
+ from typing import Any, Literal
34
+
35
+ from decision.contract import (
36
+ AllOf,
37
+ AnyOf,
38
+ Compare,
39
+ EvidenceQualifiers,
40
+ FunnelStep,
41
+ InSet,
42
+ Issue,
43
+ Known,
44
+ ModelRef,
45
+ NotOf,
46
+ SpecError,
47
+ Window,
48
+ render_condition,
49
+ )
50
+ from decision.resolve import Resolved
51
+
52
+ Leg = Literal["pass", "fail", "unknown"]
53
+ UNVERIFIED_MAY_QUALIFY = "unverified: may qualify"
54
+
55
+ # Measurers that are not the model's own lab or provider.
56
+ _INDEPENDENT = frozenset({
57
+ "benchmark_author", "independent", "independent_evaluator", "modelspec", "outcome_protocol",
58
+ })
59
+ _PROVIDER = frozenset({"provider_self_report"})
60
+
61
+ _RETIRED_CONDITION = "model.lifecycle not in {retired}"
62
+
63
+
64
+ class _Missing:
65
+ """The reference model has no known value, so the comparison is unknown."""
66
+
67
+
68
+ _MISSING = _Missing()
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class Bits:
73
+ """Three disjoint bitsets over ``candidates()``. A hole is unknown."""
74
+
75
+ passing: int = 0
76
+ failing: int = 0
77
+ unknown: int = 0
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class FunnelCount:
82
+ _condition: Any
83
+ before: int
84
+ after: int
85
+ may_qualify: int
86
+ models_before: int
87
+ models_after: int
88
+ offerings_before: int
89
+ offerings_after: int
90
+ models_may_qualify: int
91
+ offerings_may_qualify: int
92
+
93
+ @property
94
+ def condition(self) -> str:
95
+ if isinstance(self._condition, str):
96
+ return self._condition
97
+ return render_condition(self._condition)
98
+
99
+ def as_contract(self) -> FunnelStep:
100
+ return FunnelStep(
101
+ condition=self.condition, before=self.before, after=self.after,
102
+ may_qualify=self.may_qualify,
103
+ models_before=self.models_before, models_after=self.models_after,
104
+ offerings_before=self.offerings_before, offerings_after=self.offerings_after,
105
+ models_may_qualify=self.models_may_qualify,
106
+ offerings_may_qualify=self.offerings_may_qualify,
107
+ )
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class Elimination:
112
+ """Why one candidate left the lineup. ``value`` is theirs, ``threshold`` is the bar."""
113
+
114
+ candidate: str
115
+ _condition: Any
116
+ value: Any
117
+ threshold: Any
118
+ facet: str | None
119
+ unverified: bool = False
120
+
121
+ @property
122
+ def condition(self) -> str:
123
+ if isinstance(self._condition, str):
124
+ return self._condition
125
+ return render_condition(self._condition)
126
+
127
+ @property
128
+ def surface(self) -> str | None:
129
+ if self.unverified:
130
+ return UNVERIFIED_MAY_QUALIFY
131
+ return None
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class MayQualify:
136
+ """A candidate removed from the ranking because a capability value is unknown."""
137
+
138
+ candidate: str
139
+ unknown: tuple[str, ...]
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class SoftPenalty:
144
+ """A soft condition, for the optimiser. It did not remove anyone."""
145
+
146
+ condition: str
147
+ penalty: float
148
+ failing: tuple[str, ...]
149
+ unknown: tuple[str, ...]
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class FilterResult:
154
+ """Who remains after the hard conditions. There is no score here."""
155
+
156
+ snapshot_id: str
157
+ feasible: tuple[str, ...]
158
+ may_qualify: tuple[MayQualify, ...]
159
+ funnel: tuple[FunnelCount, ...]
160
+ eliminated: tuple[Elimination, ...]
161
+ penalties: tuple[SoftPenalty, ...]
162
+ deprecated: tuple[str, ...]
163
+
164
+
165
+ def tri_not(leg: Leg) -> Leg:
166
+ if leg == "pass":
167
+ return "fail"
168
+ if leg == "fail":
169
+ return "pass"
170
+ return "unknown"
171
+
172
+
173
+ def tri_and(left: Leg, right: Leg) -> Leg:
174
+ if left == "fail" or right == "fail":
175
+ return "fail"
176
+ if left == "unknown" or right == "unknown":
177
+ return "unknown"
178
+ return "pass"
179
+
180
+
181
+ def tri_or(left: Leg, right: Leg) -> Leg:
182
+ if left == "pass" or right == "pass":
183
+ return "pass"
184
+ if left == "unknown" or right == "unknown":
185
+ return "unknown"
186
+ return "fail"
187
+
188
+
189
+ def bit_not(bits: Bits, universe: int) -> Bits:
190
+ return Bits(bits.failing & universe, bits.passing & universe, bits.unknown & universe)
191
+
192
+
193
+ def bit_and(left: Bits, right: Bits, universe: int) -> Bits:
194
+ passing = left.passing & right.passing & universe
195
+ failing = (left.failing | right.failing) & universe
196
+ return Bits(passing, failing, universe & ~passing & ~failing)
197
+
198
+
199
+ def bit_or(left: Bits, right: Bits, universe: int) -> Bits:
200
+ passing = (left.passing | right.passing) & universe
201
+ failing = left.failing & right.failing & universe
202
+ return Bits(passing, failing, universe & ~passing & ~failing)
203
+
204
+
205
+ def _as_bits(raw: Any, universe: int) -> Bits:
206
+ passing = int(raw.passing) & universe
207
+ failing = int(raw.failing) & universe
208
+ unknown = int(raw.unknown) & universe
209
+ if (passing & failing) or (passing & unknown) or (failing & unknown):
210
+ raise ValueError("ids_where returned overlapping bitsets")
211
+ unknown |= universe & ~passing & ~failing
212
+ return Bits(passing, failing, unknown)
213
+
214
+
215
+ def _measurers(qualifier: str | None) -> frozenset[str] | None:
216
+ if qualifier is None or qualifier == "any":
217
+ return None
218
+ if qualifier == "independent":
219
+ return _INDEPENDENT
220
+ if qualifier == "provider_self_report":
221
+ return _PROVIDER
222
+ raise ValueError(f"unknown measured_by qualifier {qualifier!r}")
223
+
224
+
225
+ def _op(op: str, left: Any, right: Any) -> bool:
226
+ if op == "=":
227
+ return left == right
228
+ if op == "!=":
229
+ return left != right
230
+ if op == "<":
231
+ return left < right
232
+ if op == "<=":
233
+ return left <= right
234
+ if op == ">":
235
+ return left > right
236
+ if op == ">=":
237
+ return left >= right
238
+ raise ValueError(f"unsupported op {op!r}")
239
+
240
+
241
+ def _judge(values: Sequence[Any], pred: Callable[[Any], bool]) -> Leg:
242
+ if not values:
243
+ return "unknown"
244
+ saw_fail = False
245
+ saw_unknown = False
246
+ for value in values:
247
+ try:
248
+ ok = pred(value)
249
+ except TypeError:
250
+ saw_unknown = True
251
+ continue
252
+ if ok:
253
+ return "pass"
254
+ saw_fail = True
255
+ if saw_unknown:
256
+ return "unknown"
257
+ return "fail" if saw_fail else "unknown"
258
+
259
+
260
+ def _shown(values: Sequence[Any], op: str | None) -> Any:
261
+ ordered = [value for value in values if not isinstance(value, bool)]
262
+ if not ordered:
263
+ return None
264
+ if op in ("<", "<="):
265
+ return min(ordered)
266
+ if op in (">", ">="):
267
+ return max(ordered)
268
+ if len(ordered) == 1:
269
+ return ordered[0]
270
+ return tuple(ordered)
271
+
272
+
273
+ def _collapse(bits: Bits, policy: str, universe: int) -> Bits:
274
+ if policy == "pass":
275
+ return Bits((bits.passing | bits.unknown) & universe, bits.failing & universe, 0)
276
+ if policy == "fail":
277
+ return Bits(bits.passing & universe, (bits.failing | bits.unknown) & universe, 0)
278
+ return bits
279
+
280
+
281
+ def _leg_of(bits: Bits, bit: int) -> Leg:
282
+ if bits.passing & bit:
283
+ return "pass"
284
+ if bits.failing & bit:
285
+ return "fail"
286
+ return "unknown"
287
+
288
+
289
+ def _children(cond: Any) -> tuple[Any, ...]:
290
+ if isinstance(cond, AnyOf):
291
+ return tuple(cond.any)
292
+ if isinstance(cond, AllOf):
293
+ return tuple(cond.all)
294
+ if isinstance(cond, NotOf):
295
+ return (cond.not_,)
296
+ return ()
297
+
298
+
299
+ def _facet_of(cond: Any) -> str | None:
300
+ if isinstance(cond, Known):
301
+ return cond.known
302
+ facet = getattr(cond, "facet", None)
303
+ return facet if isinstance(facet, str) else None
304
+
305
+
306
+ def _is_leaf(cond: Any) -> bool:
307
+ return isinstance(cond, Compare | Window | InSet | Known)
308
+
309
+
310
+ def _keep_row(row: Any, qualifiers: EvidenceQualifiers, measured: frozenset[str] | None) -> bool:
311
+ if not getattr(row, "verified", False):
312
+ return False
313
+ if measured is not None and row.measured_by not in measured:
314
+ return False
315
+ if qualifiers.effort is not None and row.effort != qualifiers.effort:
316
+ return False
317
+ if qualifiers.harness is not None and row.harness != qualifiers.harness:
318
+ return False
319
+ after = qualifiers.measured_after
320
+ return after is None or (row.date is not None and row.date > after)
321
+
322
+
323
+ class _Run:
324
+ def __init__(self, resolved: Resolved, index: Any) -> None:
325
+ self.resolved = resolved
326
+ self.index = index
327
+ self.facets = resolved.facets
328
+ self.facet_cache: dict[str, Any] = {}
329
+ self.ids = list(index.candidates())
330
+ if len(set(self.ids)) != len(self.ids):
331
+ raise ValueError("snapshot candidates are not unique")
332
+ self.n = len(self.ids)
333
+ self.universe = (1 << self.n) - 1
334
+ self.pos = {cid: i for i, cid in enumerate(self.ids)}
335
+ self.cache: dict[int, Bits] = {}
336
+ self.evidence_conditions: dict[int, bool] = {}
337
+ self.resolved_threshold: dict[int, Any] = {}
338
+ self.penalties: list[SoftPenalty] = []
339
+ self.path = ""
340
+ self.lineup = 0
341
+ #: The capabilities asked about, which ``@direct`` is relative to.
342
+ self.domains = frozenset(resolved.spec.capabilities or {})
343
+
344
+ def _ids_of(self, bits: int) -> tuple[str, ...]:
345
+ ids = []
346
+ while bits:
347
+ bit = bits & -bits
348
+ ids.append(self.ids[bit.bit_length() - 1])
349
+ bits ^= bit
350
+ return tuple(ids)
351
+
352
+ def _life(self, cid: str) -> str:
353
+ life = self.index.lifecycle(cid)
354
+ if life not in ("active", "deprecated", "retired"):
355
+ raise ValueError(f"lifecycle of {cid} is {life!r}, not active, deprecated or retired")
356
+ return life
357
+
358
+ def _facet(self, facet_id: str) -> Any:
359
+ if facet_id in self.facet_cache:
360
+ return self.facet_cache[facet_id]
361
+ try:
362
+ facet = self.facets(facet_id)
363
+ self.facet_cache[facet_id] = facet
364
+ return facet
365
+ except KeyError:
366
+ raise SpecError([Issue(
367
+ None, facet_id, f"unknown facet {facet_id!r}: not in the facet registry", self.path,
368
+ )]) from None
369
+
370
+ def _is_evidence(self, cond: Any) -> bool:
371
+ key = id(cond)
372
+ if key not in self.evidence_conditions:
373
+ facet_id = _facet_of(cond)
374
+ self.evidence_conditions[key] = (
375
+ getattr(cond, "qualifiers", None) is not None
376
+ or (facet_id is not None and not isinstance(cond, Known) and (
377
+ self._facet(facet_id).subject == "evidence" or facet_id.startswith("evidence.")
378
+ ))
379
+ )
380
+ return self.evidence_conditions[key]
381
+
382
+ def _unknown_disposition(self, cond: Any, unk_bits: int) -> tuple[int, int, int]:
383
+ """Split unknown bits into (treat as pass, may_qualify, unverified fail)."""
384
+ if not unk_bits:
385
+ return 0, 0, 0
386
+ explicit = getattr(cond, "unknown", None)
387
+ if explicit == "pass":
388
+ return unk_bits, 0, 0
389
+ if explicit == "fail" or isinstance(cond, Known):
390
+ return 0, 0, unk_bits
391
+ if explicit == "list":
392
+ return 0, unk_bits, 0
393
+ listed = failed = 0
394
+ for cid in self._ids_of(unk_bits):
395
+ bit = 1 << self.pos[cid]
396
+ facets = self._unknown_facets(cond, bit)
397
+ risks = [self._facet(facet_id).risk for facet_id in facets]
398
+ if not risks or any(risk == "governance" for risk in risks):
399
+ failed |= bit
400
+ else:
401
+ listed |= bit
402
+ return 0, listed, failed
403
+
404
+ def _admitted(self, cid: str, cond: Any) -> list[Any]:
405
+ qualifiers = cond.qualifiers or EvidenceQualifiers()
406
+ if qualifiers.direct and not self.index.direct_for(cond.facet, self.domains):
407
+ return []
408
+ measured = _measurers(qualifiers.measured_by)
409
+ rows = self.index.evidence(
410
+ cid, cond.facet,
411
+ measured_by=None if measured is None else set(measured),
412
+ effort=qualifiers.effort,
413
+ harness=qualifiers.harness,
414
+ after=qualifiers.measured_after,
415
+ )
416
+ return [row.value for row in rows if _keep_row(row, qualifiers, measured)]
417
+
418
+ def _reference(self, cond: Compare) -> Any:
419
+ assert isinstance(cond.value, ModelRef)
420
+ ref = cond.value.model
421
+ key = id(cond)
422
+ if key in self.resolved_threshold:
423
+ return self.resolved_threshold[key]
424
+ try:
425
+ if self._is_evidence(cond):
426
+ values = self._admitted(ref, cond)
427
+ got: Any = _MISSING if not values else _shown(values, cond.op)
428
+ else:
429
+ fact = self.index.fact(ref, cond.facet)
430
+ got = fact.value if fact.state == "known" and fact.value is not None else _MISSING
431
+ except KeyError:
432
+ raise SpecError([Issue(
433
+ render_condition(cond), ref, f"model {ref} is not in the snapshot", self.path,
434
+ )]) from None
435
+ self.resolved_threshold[key] = got
436
+ return got
437
+
438
+ def _evidence_bits(self, cond: Any, op: str, arg: Any) -> Bits:
439
+ qualifiers = cond.qualifiers or EvidenceQualifiers()
440
+ measured = _measurers(qualifiers.measured_by)
441
+ indexed = getattr(self.index, "evidence_where", None)
442
+ if indexed is not None:
443
+ return _as_bits(indexed(
444
+ cond.facet,
445
+ op,
446
+ arg,
447
+ measured_by=None if measured is None else set(measured),
448
+ effort=qualifiers.effort,
449
+ harness=qualifiers.harness,
450
+ after=qualifiers.measured_after,
451
+ direct=qualifiers.direct,
452
+ domains=self.domains,
453
+ ), self.universe)
454
+ passing = failing = 0
455
+ for i, cid in enumerate(self.ids):
456
+ try:
457
+ values = self._admitted(cid, cond)
458
+ except KeyError:
459
+ raise SpecError([Issue(
460
+ render_condition(cond), cid, f"{cid} is not in the snapshot", self.path,
461
+ )]) from None
462
+ if op == "between":
463
+ low, high = arg
464
+ leg = _judge(values, lambda value: _op(">=", value, low)
465
+ and _op("<=", value, high))
466
+ else:
467
+ leg = _judge(values, lambda value: _op(op, value, arg))
468
+ bit = 1 << i
469
+ if leg == "pass":
470
+ passing |= bit
471
+ elif leg == "fail":
472
+ failing |= bit
473
+ return Bits(passing, failing, self.universe & ~passing & ~failing)
474
+
475
+ def _compare(self, cond: Compare) -> Bits:
476
+ if isinstance(cond.value, ModelRef):
477
+ threshold = self._reference(cond)
478
+ if threshold is _MISSING:
479
+ return Bits(0, 0, self.universe)
480
+ else:
481
+ threshold = cond.value
482
+ if self._is_evidence(cond):
483
+ return self._evidence_bits(cond, cond.op, threshold)
484
+ return _as_bits(self.index.ids_where(cond.facet, cond.op, threshold), self.universe)
485
+
486
+ def _window(self, cond: Window) -> Bits:
487
+ low, high = cond.between
488
+ if self._is_evidence(cond):
489
+ return self._evidence_bits(cond, "between", (low, high))
490
+ lo = _as_bits(self.index.ids_where(cond.facet, ">=", low), self.universe)
491
+ hi = _as_bits(self.index.ids_where(cond.facet, "<=", high), self.universe)
492
+ return bit_and(lo, hi, self.universe)
493
+
494
+ def _set(self, cond: InSet) -> Bits:
495
+ values = cond.in_ if cond.in_ is not None else cond.not_in
496
+ if not values:
497
+ raise ValueError("a set condition has no values")
498
+ if self._is_evidence(cond):
499
+ acc: Bits | None = None
500
+ for value in values:
501
+ bit = self._evidence_bits(cond, "=", value)
502
+ acc = bit if acc is None else bit_or(acc, bit, self.universe)
503
+ assert acc is not None
504
+ bits = acc
505
+ elif getattr(getattr(self._facet(cond.facet), "value_type", None), "kind", None) == "set":
506
+ # A set-valued facet is in {a, b} when it holds a or b, and not in
507
+ # {a, b} when it holds neither. Equality never matches a set.
508
+ bits = _as_bits(
509
+ self.index.ids_where(cond.facet, "contains_any", list(values)), self.universe
510
+ )
511
+ else:
512
+ acc: Bits | None = None
513
+ for value in values:
514
+ bit = _as_bits(self.index.ids_where(cond.facet, "=", value), self.universe)
515
+ acc = bit if acc is None else bit_or(acc, bit, self.universe)
516
+ assert acc is not None
517
+ bits = acc
518
+ if cond.not_in is not None:
519
+ bits = bit_not(bits, self.universe)
520
+ return bits
521
+
522
+ def _known(self, cond: Known) -> Bits:
523
+ bits = _as_bits(self.index.ids_where(cond.known, "known", None), self.universe)
524
+ return Bits(bits.passing, self.universe & ~bits.passing, 0)
525
+
526
+ def _presented(self, cond: Any, raw: Bits) -> Bits:
527
+ explicit = getattr(cond, "unknown", None)
528
+ if explicit is None:
529
+ return raw
530
+ return _collapse(raw, explicit, self.universe)
531
+
532
+ def _combine(self, children: Sequence[Any], op: Callable[[Bits, Bits, int], Bits]) -> Bits:
533
+ parts: list[Bits] = []
534
+ for child in children:
535
+ if child.soft is not None:
536
+ self._eval(child)
537
+ continue
538
+ parts.append(self._presented(child, self._eval(child)))
539
+ if not parts:
540
+ return Bits(self.universe, 0, 0)
541
+ acc = parts[0]
542
+ for part in parts[1:]:
543
+ acc = op(acc, part, self.universe)
544
+ return acc
545
+
546
+ def _eval(self, cond: Any) -> Bits:
547
+ key = id(cond)
548
+ cached = self.cache.get(key)
549
+ if cached is not None:
550
+ return cached
551
+ if isinstance(cond, Compare):
552
+ bits = self._compare(cond)
553
+ elif isinstance(cond, Window):
554
+ bits = self._window(cond)
555
+ elif isinstance(cond, InSet):
556
+ bits = self._set(cond)
557
+ elif isinstance(cond, Known):
558
+ bits = self._known(cond)
559
+ elif isinstance(cond, AnyOf):
560
+ bits = self._combine(cond.any, bit_or)
561
+ elif isinstance(cond, AllOf):
562
+ bits = self._combine(cond.all, bit_and)
563
+ elif isinstance(cond, NotOf):
564
+ child = cond.not_
565
+ if child.soft is not None:
566
+ self._eval(child)
567
+ bits = Bits(self.universe, 0, 0)
568
+ else:
569
+ bits = bit_not(self._presented(child, self._eval(child)), self.universe)
570
+ else:
571
+ raise TypeError(f"not a condition: {type(cond).__name__}")
572
+ self.cache[key] = bits
573
+ if getattr(cond, "soft", None) is not None:
574
+ self._record_penalty(cond, bits)
575
+ return bits
576
+
577
+ def _record_penalty(self, cond: Any, raw: Bits) -> None:
578
+ lineup = self.lineup
579
+ self.penalties.append(SoftPenalty(
580
+ condition=render_condition(cond),
581
+ penalty=cond.soft.penalty,
582
+ failing=self._ids_of(raw.failing & lineup),
583
+ unknown=self._ids_of(raw.unknown & lineup),
584
+ ))
585
+
586
+ def _decisive(self, cond: Any, bit: int, leg: Leg) -> Any:
587
+ if isinstance(cond, NotOf):
588
+ child = cond.not_
589
+ if child.soft is not None:
590
+ return cond
591
+ flipped: Leg = "unknown" if leg == "unknown" else ("fail" if leg == "pass" else "pass")
592
+ return self._decisive(child, bit, flipped)
593
+ children = _children(cond)
594
+ if not children:
595
+ return cond
596
+ for child in children:
597
+ if child.soft is not None:
598
+ continue
599
+ shown = self._presented(child, self._eval(child))
600
+ if _leg_of(shown, bit) == leg:
601
+ return self._decisive(child, bit, leg)
602
+ for child in children:
603
+ if child.soft is not None:
604
+ continue
605
+ if self._eval(child).unknown & bit:
606
+ return self._decisive(child, bit, "unknown")
607
+ return cond
608
+
609
+ def _unknown_facets(self, cond: Any, bit: int) -> list[str]:
610
+ if isinstance(cond, NotOf):
611
+ return self._unknown_facets(cond.not_, bit)
612
+ children = _children(cond)
613
+ if children:
614
+ found: list[str] = []
615
+ for child in children:
616
+ if child.soft is not None:
617
+ continue
618
+ if self._eval(child).unknown & bit:
619
+ for facet_id in self._unknown_facets(child, bit):
620
+ if facet_id not in found:
621
+ found.append(facet_id)
622
+ return found
623
+ facet_id = _facet_of(cond)
624
+ return [facet_id] if facet_id else []
625
+
626
+ def _value(self, cond: Any, cid: str) -> Any:
627
+ if isinstance(cond, Known):
628
+ try:
629
+ return self.index.fact(cid, cond.known).state
630
+ except KeyError:
631
+ return None
632
+ facet_id = _facet_of(cond)
633
+ if facet_id is None:
634
+ return None
635
+ if self._is_evidence(cond):
636
+ values = self._admitted(cid, cond)
637
+ op = cond.op if isinstance(cond, Compare) else None
638
+ return _shown(values, op) if values else None
639
+ try:
640
+ fact = self.index.fact(cid, facet_id)
641
+ except KeyError:
642
+ return None
643
+ if fact.state != "known" or fact.value is None:
644
+ return None
645
+ return fact.value
646
+
647
+ def _threshold(self, cond: Any) -> Any:
648
+ if isinstance(cond, Compare):
649
+ if isinstance(cond.value, ModelRef):
650
+ got = self.resolved_threshold.get(id(cond), _MISSING)
651
+ if got is _MISSING:
652
+ got = self._reference(cond)
653
+ return None if got is _MISSING else got
654
+ return cond.value
655
+ if isinstance(cond, Window):
656
+ return cond.between
657
+ if isinstance(cond, InSet):
658
+ return tuple(cond.in_ if cond.in_ is not None else cond.not_in or ())
659
+ if isinstance(cond, Known):
660
+ return "known"
661
+ return None
662
+
663
+ def _leaf_was_unknown(self, cond: Any, bit: int) -> bool:
664
+ leaf = self._decisive(cond, bit, "fail")
665
+ if not _is_leaf(leaf):
666
+ leaf = self._decisive(cond, bit, "unknown")
667
+ if not _is_leaf(leaf):
668
+ return False
669
+ return bool(self._eval(leaf).unknown & bit)
670
+
671
+ def _cover(self, feasible: int, maybe: int, eliminated: int) -> None:
672
+ if (feasible & maybe) or (feasible & eliminated) or (maybe & eliminated):
673
+ raise RuntimeError("filter partition overlaps")
674
+ if (feasible | maybe | eliminated) != self.universe:
675
+ raise RuntimeError("filter partition does not cover the lineup")
676
+
677
+ def _grain_counts(self, bits: int) -> tuple[int, int]:
678
+ ids = self._ids_of(bits)
679
+ return (
680
+ len({self.index.model_of(cid) for cid in ids}),
681
+ sum(self.index.kind(cid) == "offering" for cid in ids),
682
+ )
683
+
684
+ def run(self) -> FilterResult:
685
+ wanted = self.resolved.spec.snapshot
686
+ if wanted != "latest" and wanted != self.index.snapshot_id:
687
+ raise SpecError([Issue(
688
+ None, "snapshot",
689
+ f"spec asks for {wanted} but the snapshot is {self.index.snapshot_id}",
690
+ "snapshot",
691
+ )])
692
+ sold = {self.index.model_of(cid) for cid in self.ids
693
+ if self.index.kind(cid) == "offering"}
694
+ represented = retired = 0
695
+ for i, cid in enumerate(self.ids):
696
+ if cid in sold:
697
+ represented |= 1 << i
698
+ elif self._life(cid) == "retired":
699
+ retired |= 1 << i
700
+ eliminations: list[Elimination] = []
701
+ if self.resolved.include_retired:
702
+ self.lineup = self.universe & ~represented
703
+ else:
704
+ self.lineup = self.universe & ~retired & ~represented
705
+ for cid in self._ids_of(retired):
706
+ eliminations.append(Elimination(
707
+ candidate=cid, _condition=_RETIRED_CONDITION, value="retired",
708
+ threshold=("active", "deprecated"), facet="model.lifecycle",
709
+ ))
710
+ feasible = self.lineup
711
+ maybe = 0
712
+ eliminated = self.universe & ~self.lineup
713
+ unknown_facets: dict[str, list[str]] = {}
714
+ funnel: list[FunnelCount] = []
715
+ rules = 0 if self.resolved.profile is None else len(self.resolved.profile.rules)
716
+ self._cover(feasible, maybe, eliminated)
717
+
718
+ for index, cond in enumerate(self.resolved.conditions):
719
+ self.path = (f"profile.rules[{index}]" if index < rules
720
+ else f"where[{index - rules}]")
721
+ before = feasible.bit_count()
722
+ models_before, offerings_before = self._grain_counts(feasible)
723
+ if cond.soft is not None:
724
+ self._eval(cond)
725
+ funnel.append(FunnelCount(
726
+ cond, before, before, 0,
727
+ models_before, models_before, offerings_before, offerings_before,
728
+ 0, 0,
729
+ ))
730
+ continue
731
+ raw = self._eval(cond)
732
+ fe_pass, fe_fail, fe_unk = _split(raw, feasible)
733
+ _mb_pass, mb_fail, mb_unk = _split(raw, maybe)
734
+ fe_as_pass, fe_as_list, fe_as_fail = self._unknown_disposition(cond, fe_unk)
735
+ _mb_as_pass, mb_as_list, mb_as_fail = self._unknown_disposition(cond, mb_unk)
736
+ feasible = fe_pass | fe_as_pass
737
+ new_maybe = fe_as_list
738
+ drop = mb_fail | mb_as_fail
739
+ maybe = (maybe & ~drop) | new_maybe
740
+ new_elim = fe_fail | fe_as_fail | drop
741
+ unverified_bits = fe_as_fail | mb_as_fail
742
+ for cid in self._ids_of(new_maybe | mb_as_list):
743
+ found = unknown_facets.setdefault(cid, [])
744
+ for facet_id in self._unknown_facets(cond, 1 << self.pos[cid]):
745
+ if facet_id not in found:
746
+ found.append(facet_id)
747
+ for cid in self._ids_of(drop):
748
+ unknown_facets.pop(cid, None)
749
+ for cid in self._ids_of(new_elim):
750
+ bit = 1 << self.pos[cid]
751
+ unverified = bool(unverified_bits & bit) or self._leaf_was_unknown(cond, bit)
752
+ leg: Leg = "unknown" if unverified else "fail"
753
+ leaf = self._decisive(cond, bit, leg)
754
+ eliminations.append(Elimination(
755
+ candidate=cid, _condition=cond,
756
+ value=None if unverified else self._value(leaf, cid),
757
+ threshold=self._threshold(leaf),
758
+ facet=_facet_of(leaf),
759
+ unverified=unverified,
760
+ ))
761
+ eliminated |= new_elim
762
+ models_after, offerings_after = self._grain_counts(feasible)
763
+ models_maybe, offerings_maybe = self._grain_counts(new_maybe)
764
+ funnel.append(FunnelCount(
765
+ cond, before, feasible.bit_count(), new_maybe.bit_count(),
766
+ models_before, models_after, offerings_before, offerings_after,
767
+ models_maybe, offerings_maybe,
768
+ ))
769
+ self._cover(feasible, maybe, eliminated)
770
+
771
+ may_qualify = tuple(
772
+ MayQualify(cid, tuple(unknown_facets.get(cid, ())))
773
+ for cid in self._ids_of(maybe)
774
+ )
775
+ deprecated = tuple(cid for cid in self.ids if self._life(cid) == "deprecated")
776
+ return FilterResult(
777
+ snapshot_id=self.index.snapshot_id,
778
+ feasible=self._ids_of(feasible),
779
+ may_qualify=may_qualify,
780
+ funnel=tuple(funnel),
781
+ eliminated=tuple(eliminations),
782
+ penalties=tuple(self.penalties),
783
+ deprecated=deprecated,
784
+ )
785
+
786
+
787
+ def _split(raw: Bits, mask: int) -> tuple[int, int, int]:
788
+ passing = raw.passing & mask
789
+ failing = raw.failing & mask
790
+ unknown = mask & ~passing & ~failing
791
+ return passing, failing, unknown
792
+
793
+
794
+ def apply(resolved: Resolved, index: Any) -> FilterResult:
795
+ """Filter ``index`` by the resolved conditions. The same inputs give the same result."""
796
+ return _Run(resolved, index).run()