§22 S6: release v0.45.0 — two-project/multi-collection test + §20.4 changelog
The release wrap for the S6 scope shipped this slice (SPEC merge, per-collection enabled_models, type-driven entry noun). - test_s6_two_project_multicollection.py: an integrative pass proving the new per-collection knobs (§22.12 enabled_models, §22.4a noun) resolve independently per collection across two projects with no cross-project/-collection bleed. - CHANGELOG.md 0.45.0: the §20.4 entry + upgrade-steps (migration 031 is additive + automatic; per-collection enabled_models and typed collections are optional). Notes the two OPEN S6 items carried to a follow-up slice (per-type surfaces; request-to-join + cross-collection inbox). - VERSION + frontend/package.json → 0.45.0. Gate: backend 530 passed, frontend 30 passed, build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,77 @@ skip versions are the composition of each intervening adjacent
|
||||
release's steps in order — no A-to-B path is pre-computed beyond
|
||||
that.
|
||||
|
||||
## 0.45.0 — 2026-06-06
|
||||
|
||||
**Minor (non-breaking) — §22 three-tier refactor, slice S6: *the SPEC merge +
|
||||
per-collection model universe + the type-driven entry noun.* The three-tier
|
||||
model (deployment → project → RFC collection) is now written into the binding
|
||||
`SPEC.md` as a canonical §22, so the spec finally reflects the shipped S1–S5
|
||||
behavior; a collection may narrow its project's model universe; and the entry
|
||||
noun ("RFC" / "Spec" / "Feature") follows the collection's type. Purely additive
|
||||
over S5 — one additive column (no rebuild), docs, and two small features; no
|
||||
change to existing read or write semantics.**
|
||||
|
||||
See [`docs/design/2026-06-05-three-tier-projects-collections.md`](./docs/design/2026-06-05-three-tier-projects-collections.md)
|
||||
(Part E slice S6; Parts A/B/D, merged into `SPEC.md` §22). With S6 the §22
|
||||
three-tier model is realized in the binding contract. **Two S6 items remain
|
||||
open and are carried to a follow-up slice** (they want a discovery/spec pass
|
||||
first, lacking BDD scenarios in Part C): the per-type *surfaces* — per-type
|
||||
frontmatter schemas, the `specification` release-planning data model, the `bdd`
|
||||
scenario/coverage views (§22.4a items 1 & 3, flagged "first proposals" in the
|
||||
design doc) — and **request-to-join + the cross-collection inbox** (§22.8; S4
|
||||
shipped the invite half).
|
||||
|
||||
Added:
|
||||
|
||||
- **§22 merged into `SPEC.md` (the keystone)** — a canonical §22.1–§22.14 writes
|
||||
the three-tier model into the binding spec: the tiers + collection-grain
|
||||
isolation, the registry (`projects.yaml`) + `.collection.yaml` manifests,
|
||||
one-content-repo-per-project, per-collection slug identity, collection
|
||||
`type`/`initial_state`/`unreviewed`, two-tier (narrow-only) visibility, the
|
||||
unified `{owner, contributor}` role vocabulary at `{global, project,
|
||||
collection}`, the four-layer most-permissive union, discovery/joining,
|
||||
runtime branding, `/p/<project>/c/<collection>/` routing, the one inbox, the
|
||||
per-collection model universe, and the default-project+collection migration.
|
||||
The **S3 keystone reinterpretation of §B.1/§B.3** lands here: a plain granted
|
||||
account is a granted *account*, not a global write role; "global RFC
|
||||
Contributor" is an explicit `scope_type='global'` grant; the implicit-public
|
||||
write baseline is grandfathered onto the migration-seeded default collection
|
||||
only. Forward-pointer amendment notes added at §1/§2/§5/§6. The registry +
|
||||
manifest formats are documented for operators in
|
||||
[`docs/DEPLOYMENTS.md`](./docs/DEPLOYMENTS.md).
|
||||
- **Per-collection model universe (§22.12)** — a collection's `.collection.yaml`
|
||||
may carry an `enabled_models` list that narrows its project's universe, which
|
||||
in turn narrows the deployment `ENABLED_MODELS`. Resolution (extending
|
||||
§6.6/§6.7) is `funder ∩ per-entry models ∩ collection ∩ project`, with the
|
||||
operator providers as the ceiling — a collection can only narrow, never widen.
|
||||
An absent list inherits the parent; an empty list opts the collection out of
|
||||
AI. Surfaced as `enabled_models` on `GET /api/projects/:id/collections/:cid`.
|
||||
- **Type-driven entry noun (§22.4a)** — the displayed noun for an entry follows
|
||||
the collection type (`document` → "RFC", `specification` → "Spec", `bdd` →
|
||||
"Feature"), defined once in the framework and read from the API
|
||||
(`entry_noun` on the collection, directory, and project surfaces) rather than
|
||||
hardcoded. The catalog's propose control and the propose modal name entries
|
||||
accordingly.
|
||||
|
||||
Schema:
|
||||
|
||||
- **Migration `031_collection_enabled_models.sql`** — additive
|
||||
`collections.config_json` (paralleling `projects.config_json`), holding the
|
||||
per-collection `enabled_models`. No table rebuild; `NULL` means "inherit the
|
||||
project's universe."
|
||||
|
||||
Upgrade steps (from 0.44.0):
|
||||
|
||||
1. Rebuild and redeploy as usual; the framework **SHALL** apply migration
|
||||
`031_collection_enabled_models.sql` automatically on start (additive column,
|
||||
no data movement, no operator action).
|
||||
2. A deployment **MAY** now add an `enabled_models` list to any
|
||||
`.collection.yaml` to narrow that collection's model universe, and **MAY**
|
||||
create `specification`- or `bdd`-typed collections to get the corresponding
|
||||
entry noun. Both are optional; absent them every collection behaves exactly
|
||||
as before.
|
||||
|
||||
## 0.44.0 — 2026-06-06
|
||||
|
||||
**Minor (non-breaking) — §22 three-tier refactor, slice S5: *in-app
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""§22 S6 — two-project / multi-collection integrative pass.
|
||||
|
||||
Proves the S6 surface holds across a deployment with two projects, each owning
|
||||
distinct collections, with the new per-collection knobs (§22.12 enabled_models,
|
||||
§22.4a type noun) resolving independently per collection — no cross-project or
|
||||
cross-collection bleed.
|
||||
|
||||
Slug note: the resolver is slug-keyed, so this test uses distinct slugs across
|
||||
collections (the documented limitation — same-slug-across-collections model
|
||||
resolution is part of the broader collection-id-threading follow-up).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import collections, db, models_resolver
|
||||
from test_propose_vertical import app_with_fake_gitea, tmp_env # noqa: F401
|
||||
from test_rfc_view_vertical import FakeProvider # noqa: F401
|
||||
|
||||
|
||||
def _project(pid: str, visibility: str = "public") -> None:
|
||||
db.conn().execute(
|
||||
"INSERT OR REPLACE INTO projects (id, name, content_repo, visibility, config_json, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, datetime('now'))",
|
||||
(pid, pid.capitalize(), f"{pid}-content", visibility, None),
|
||||
)
|
||||
|
||||
|
||||
def _collection(cid: str, project_id: str, *, ctype: str, enabled_models=None) -> None:
|
||||
cfg = None if enabled_models is None else json.dumps({"enabled_models": enabled_models})
|
||||
db.conn().execute(
|
||||
"INSERT OR REPLACE INTO collections "
|
||||
"(id, project_id, type, subfolder, initial_state, visibility, name, config_json, "
|
||||
" created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, 'super-draft', 'public', ?, ?, datetime('now'), datetime('now'))",
|
||||
(cid, project_id, ctype, cid, cid.capitalize(), cfg),
|
||||
)
|
||||
|
||||
|
||||
def _entry(slug: str, collection_id: str) -> None:
|
||||
db.conn().execute(
|
||||
"INSERT OR REPLACE INTO cached_rfcs (slug, title, state, collection_id) "
|
||||
"VALUES (?, ?, 'active', ?)",
|
||||
(slug, slug.upper(), collection_id),
|
||||
)
|
||||
|
||||
|
||||
def _three_providers(app) -> None:
|
||||
app.state.providers.clear()
|
||||
for k in ("claude", "gemini", "gpt"):
|
||||
app.state.providers[k] = FakeProvider("TITLE: A\nDESCRIPTION: B")
|
||||
|
||||
|
||||
def test_two_projects_multicollection_model_universe_isolated(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app):
|
||||
_three_providers(app)
|
||||
# alpha: a document collection narrowed to claude.
|
||||
_project("alpha")
|
||||
_collection("alpha-docs", "alpha", ctype="document", enabled_models=["claude"])
|
||||
_entry("alpha-intro", "alpha-docs")
|
||||
# beta: two collections — a spec narrowed to gemini, a bdd left open.
|
||||
_project("beta")
|
||||
_collection("beta-specs", "beta", ctype="specification", enabled_models=["gemini"])
|
||||
_collection("beta-features", "beta", ctype="bdd") # no narrowing → all three
|
||||
_entry("beta-runtime", "beta-specs")
|
||||
_entry("beta-login", "beta-features")
|
||||
|
||||
# Each entry resolves against ITS collection's universe — no bleed.
|
||||
assert models_resolver.resolve_models_for_rfc("alpha-intro", app.state.providers) == ["claude"]
|
||||
assert models_resolver.resolve_models_for_rfc("beta-runtime", app.state.providers) == ["gemini"]
|
||||
assert models_resolver.resolve_models_for_rfc("beta-login", app.state.providers) == [
|
||||
"claude", "gemini", "gpt"
|
||||
]
|
||||
|
||||
|
||||
def test_two_projects_multicollection_type_noun_isolated(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app):
|
||||
_project("alpha")
|
||||
_collection("alpha-docs", "alpha", ctype="document")
|
||||
_project("beta")
|
||||
_collection("beta-specs", "beta", ctype="specification")
|
||||
_collection("beta-features", "beta", ctype="bdd")
|
||||
|
||||
assert collections.get_collection("alpha-docs")["entry_noun"] == "RFC"
|
||||
assert collections.get_collection("beta-specs")["entry_noun"] == "Spec"
|
||||
assert collections.get_collection("beta-features")["entry_noun"] == "Feature"
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rfc-app-frontend",
|
||||
"private": true,
|
||||
"version": "0.44.0",
|
||||
"version": "0.45.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Reference in New Issue
Block a user