Post-v1: per-RFC model availability (UX half) folded into §6.6
First §19.2 candidate settled after v1. The heavier per-RFC-model
topic subdivided into UX (this) and credential delegation + funder
role (still §19.2). New §6.6 carries the rule: an optional `models:`
frontmatter field on the meta-repo RFC entry; absent inherits the
operator universe, populated narrows the picker to the intersection
with provisioned providers, `[]` opts the RFC out of AI entirely.
The first resolved entry is the RFC default. §18's ENABLED_MODELS is
reframed as the operator universe.
Code: migration 009 adds nullable cached_rfcs.models_json (NULL ≠ []
is load-bearing); entry.py grows the optional field with absent-vs-
empty round-tripping in parse/serialize; new models_resolver module
holds the rule; api_branches replaces /api/models with the slug-aware
/api/rfcs/{slug}/models and threads the chat + reask paths through
the resolver; api_prs §10.2 uses the resolver and extends the stub
fallback to the opt-out case; frontend passes slug to listModels.
Tests 106/106 green (96 prior + 10 in test_per_rfc_models.py). No
behavioral change for entries without `models:` — operator universe
preserved as default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.DOTALL)
|
||||
|
||||
SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
|
||||
|
||||
_ABSENT = object()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
@@ -38,6 +40,11 @@ class Entry:
|
||||
owners: list[str] = field(default_factory=list)
|
||||
arbiters: list[str] = field(default_factory=list)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
# §6.6: per-RFC model availability. None means the key is absent
|
||||
# from frontmatter (inherit the operator universe). An empty list
|
||||
# means an explicit opt-out from AI on this RFC. A populated list
|
||||
# narrows the picker to its intersection with the operator universe.
|
||||
models: list[str] | None = None
|
||||
body: str = ""
|
||||
|
||||
|
||||
@@ -47,6 +54,11 @@ def parse(text: str) -> Entry:
|
||||
raise ValueError("Entry file missing frontmatter")
|
||||
fm = yaml.safe_load(match.group(1)) or {}
|
||||
body = match.group(2).lstrip("\n")
|
||||
raw_models = fm.get("models", _ABSENT)
|
||||
if raw_models is _ABSENT or raw_models is None:
|
||||
models: list[str] | None = None
|
||||
else:
|
||||
models = [str(m) for m in raw_models]
|
||||
return Entry(
|
||||
slug=str(fm.get("slug") or ""),
|
||||
title=str(fm.get("title") or ""),
|
||||
@@ -60,6 +72,7 @@ def parse(text: str) -> Entry:
|
||||
owners=list(fm.get("owners") or []),
|
||||
arbiters=list(fm.get("arbiters") or []),
|
||||
tags=list(fm.get("tags") or []),
|
||||
models=models,
|
||||
body=body,
|
||||
)
|
||||
|
||||
@@ -80,6 +93,10 @@ def serialize(entry: Entry) -> str:
|
||||
"arbiters": entry.arbiters,
|
||||
"tags": entry.tags,
|
||||
}
|
||||
# §6.6: emit `models:` only when set. Absent in the frontmatter
|
||||
# is meaningfully different from `models: []` per §6.6.
|
||||
if entry.models is not None:
|
||||
fm["models"] = entry.models
|
||||
yaml_text = yaml.safe_dump(fm, sort_keys=False, default_flow_style=False).rstrip()
|
||||
body = entry.body.lstrip("\n")
|
||||
if body:
|
||||
|
||||
Reference in New Issue
Block a user