"""§6.6 per-RFC model availability — the resolver. The meta-repo entry's optional `models:` frontmatter and the operator's provisioned providers (the §18 `ENABLED_MODELS` universe) combine into a single resolved list per RFC. Every AI surface picks against that list: the §8.12 chat picker, the §10.2 PR-draft, the §8.13 flag-resolution invocation, the §9.1 tag suggestions when graduation is in scope. Rule, in one place: - Cache row's `models_json` is NULL → the field is absent on the entry. Resolved list = the operator's provisioned universe. - Cache row's `models_json` is a JSON array → the field is set on the entry. Resolved list = intersection of the array with the operator's provisioned universe, preserving the entry's stated order. The empty case folds in naturally: an entry with `models: []` yields an empty intersection, and an entry whose listed models are no longer provisioned by the operator also yields an empty intersection. Callers treat both the same — surfaces refuse cleanly per §6.6. The function is slug-aware and provider-aware; it does not depend on the FastAPI request lifecycle, which keeps it cheap to call inside any endpoint that knows the slug and has the providers dict in scope. """ from __future__ import annotations import json from . import db from .providers import BaseProvider def resolve_models_for_rfc( slug: str, providers: dict[str, BaseProvider] ) -> list[str]: """Return the per-RFC resolved model keys per §6.6. The first entry is the RFC's default model. An empty list means AI is unavailable on this RFC and callers refuse the AI surface. """ universe = list(providers.keys()) row = db.conn().execute( "SELECT models_json FROM cached_rfcs WHERE slug = ?", (slug,), ).fetchone() if row is None or row["models_json"] is None: return universe try: listed = [str(m) for m in json.loads(row["models_json"])] except (json.JSONDecodeError, TypeError): return universe if not listed: return [] return [m for m in listed if m in providers] def default_model_for_rfc( slug: str, providers: dict[str, BaseProvider] ) -> str: """The first entry in the resolved list, or empty string if none. Callers that need a deterministic single choice — §10.2's draft, §9.1's tag suggestions — use this. An empty return signals the refusal path per §6.6. """ resolved = resolve_models_for_rfc(slug, providers) return resolved[0] if resolved else ""