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:
Ben Stull
2026-05-25 05:42:15 -07:00
parent 36635049c7
commit a255429e57
11 changed files with 755 additions and 57 deletions
+86
View File
@@ -44,6 +44,92 @@ rare and surgical and live in the appropriate numbered section per
## State of the codebase
### Post-v1: §6.6 per-RFC model availability (UX half)
The first §19.2 candidate settled after v1 shipped. The heavier
per-RFC-model topic explicitly subdivided into "model availability"
(UX-shaped) and "credential delegation + funder role" (security,
billing, rotation); this session folded in the lighter half and
left the heavier one as its own §19.2 entry.
The settlement: a new optional `models:` frontmatter field on the
meta-repo RFC entry, in the same shape as `owners:` / `arbiters:`.
Absent inherits the operator's universe (the §18 `ENABLED_MODELS`
env contract, now reframed as the operator's provisioned set).
Populated lists narrow the picker to the intersection with that
universe, preserving the entry's stated order. An empty list
(`models: []`) opts the RFC out of AI participation entirely —
every AI surface honors the refusal honestly. The first entry of
the resolved list is the RFC's default model; the §10.2 PR-draft
uses it when present and falls back to its existing deterministic
stub when the resolved list is empty.
§6.6 carries the structural rule. §2.1's frontmatter example shows
the field. §8.12 names the picker's option-list source. §10.2
names the model used for drafting. §16 narrows its "model picker"
deferral to the chrome only. §18 reframes `ENABLED_MODELS` as the
operator universe. §19.2's per-RFC-model entry is split — the UX
half is marked *settled* with a pointer to §6.6; the credential
half lives on as its own entry.
Code changes:
- [`backend/migrations/009_per_rfc_models.sql`](../backend/migrations/009_per_rfc_models.sql)
adds `cached_rfcs.models_json` as a nullable column — NULL means
the frontmatter field is absent (inherit the universe), `'[]'`
means the explicit opt-out, `'[...]'` is the populated list. The
absent-vs-empty distinction is load-bearing per §6.6, so this
column cannot collapse to a `NOT NULL DEFAULT '[]'` the way the
other `*_json` columns do.
- [`backend/app/entry.py`](../backend/app/entry.py) grows an
optional `models: list[str] | None` field on the `Entry`
dataclass. The parser uses a sentinel to distinguish "key not in
YAML" from "key set to null" so both round-trip cleanly. The
serializer emits `models:` only when set, preserving the absent
case in canonical entry text.
- [`backend/app/cache.py`](../backend/app/cache.py)'s
`_upsert_cached_rfc` writes `NULL` or `json.dumps(entry.models)`
on the round-trip from frontmatter through the cache.
- [`backend/app/models_resolver.py`](../backend/app/models_resolver.py)
is the new small module that holds the rule. Two functions:
`resolve_models_for_rfc(slug, providers)` returns the resolved
list, and `default_model_for_rfc(slug, providers)` returns the
first entry or empty string. Every AI surface calls into one of
these; the rule lives in one place.
- [`backend/app/api_branches.py`](../backend/app/api_branches.py)
replaces the slug-less `GET /api/models` with the per-RFC
`GET /api/rfcs/{slug}/models` per §6.6. The chat-stream and
reask-change paths now resolve through the per-RFC list and
refuse with 503 when it is empty. Both `default_model` references
inside the module are removed; the resolver replaces them.
- [`backend/app/api_prs.py`](../backend/app/api_prs.py)'s §10.2
`pr-draft` endpoint resolves the RFC's default via the new
helper. The internal `_draft_with_provider` falls back to the
deterministic stub when `default_model` is empty (the §6.6
opt-out case), extending the prior "no providers configured"
fallback symmetrically.
- [`frontend/src/api.js`](../frontend/src/api.js) and
[`frontend/src/components/RFCView.jsx`](../frontend/src/components/RFCView.jsx)
pass the slug to `listModels(slug)`, hitting the new per-RFC
endpoint.
Settlement ships covered by
[`test_per_rfc_models.py`](../backend/tests/test_per_rfc_models.py)
— ten integration tests: absent-frontmatter inheriting the
operator universe, populated-frontmatter narrowing, the
empty-list opt-out, intersection with the operator's provisioned
set, intersection-empty matching the opt-out shape, the §10.2
draft using the RFC default, the §10.2 stub fallback on opt-out,
the chat surface's 503 refusal on opt-out, the cache round-trip
from meta-repo frontmatter through `refresh_meta_repo`, and the
entry parser/serializer round-trip preserving the absent /
empty / populated distinction.
The full test suite is 106/106 green (96 prior + 10 new). No
behavioral change for RFCs without `models:` in frontmatter — the
operator universe is preserved as the default, so existing
deployments see no surface change until a frontmatter edit lands.
### Slice 1 — shipped
The repository scaffolding (`backend/`, `frontend/`, `scripts/`,