§22 S6: type-driven entry noun (§22.4a) — backend source of truth + chrome
The displayed noun for an entry is a framework concept keyed on the collection's immutable type — document→"RFC", specification→"Spec", bdd→"Feature" — not deployment content. The chrome reads it from the API instead of hardcoding "RFC". - collections.ENTRY_NOUN + entry_noun(type) (unknown type → generic "RFC"). - Surfaced as entry_noun on get_collection, list_collections items, the /api/deployment directory items, and GET /api/projects/:id. - Frontend: Catalog reads entry_noun for the "+ Propose New <noun>" control; ProposeModal fetches the active collection's noun for its title + field copy. - test_s6_entry_noun_vertical.py: map + API surfacing (collection + directory). Scope note: this is §22.4a item (2) — terminology. The deeper type-module work (item 1 per-type frontmatter schemas; item 3 specification release-planning + bdd scenario/coverage surfaces) is flagged OPEN in the design doc and is handed off to a follow-up spec+slice. Backend 528 passed; frontend 30 passed + build green. Part of v0.45.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,11 @@ def make_router(config: Config, gitea: Gitea, bot: Bot) -> APIRouter:
|
||||
"type": collections_mod.collection_type(
|
||||
collections_mod.default_collection_id(r["id"])
|
||||
),
|
||||
"entry_noun": collections_mod.entry_noun(
|
||||
collections_mod.collection_type(
|
||||
collections_mod.default_collection_id(r["id"])
|
||||
)
|
||||
),
|
||||
"visibility": r["visibility"],
|
||||
}
|
||||
for r in rows
|
||||
@@ -221,6 +226,7 @@ def make_router(config: Config, gitea: Gitea, bot: Bot) -> APIRouter:
|
||||
"name": row["name"],
|
||||
"tagline": (dep["tagline"] if dep else "") or "",
|
||||
"type": collections_mod.collection_type(cid),
|
||||
"entry_noun": collections_mod.entry_noun(collections_mod.collection_type(cid)),
|
||||
"visibility": row["visibility"],
|
||||
"initial_state": collections_mod.collection_initial_state(cid),
|
||||
"theme": cfg.get("theme") or {},
|
||||
|
||||
@@ -14,6 +14,23 @@ from . import db
|
||||
|
||||
DEFAULT_COLLECTION_ID = "default"
|
||||
|
||||
# §22.4a item (2): the displayed noun for an entry is a type-driven label, a
|
||||
# framework concept (like role names), not deployment content. The chrome reads
|
||||
# this from the API rather than hardcoding "RFC", so a `specification` collection
|
||||
# says "Spec" and a `bdd` collection says "Feature" with no per-deployment config.
|
||||
ENTRY_NOUN = {
|
||||
"document": "RFC",
|
||||
"specification": "Spec",
|
||||
"bdd": "Feature",
|
||||
}
|
||||
_DEFAULT_ENTRY_NOUN = "RFC"
|
||||
|
||||
|
||||
def entry_noun(collection_type: str) -> str:
|
||||
"""The §22.4a entry noun for a collection type. Unknown types fall back to
|
||||
the generic 'RFC' so a future type is never label-less."""
|
||||
return ENTRY_NOUN.get(collection_type, _DEFAULT_ENTRY_NOUN)
|
||||
|
||||
|
||||
def _enabled_models_from_config(config_json: str | None) -> list[str] | None:
|
||||
"""§22.12 per-collection enabled_models from a `config_json` blob, or None
|
||||
@@ -86,6 +103,7 @@ def get_collection(collection_id: str) -> dict | None:
|
||||
return None
|
||||
out = dict(row)
|
||||
out["enabled_models"] = _enabled_models_from_config(out.pop("config_json", None))
|
||||
out["entry_noun"] = entry_noun(out["type"])
|
||||
return out
|
||||
|
||||
|
||||
@@ -102,5 +120,7 @@ def list_collections(project_id: str, include_unlisted: bool = False) -> list[di
|
||||
for r in rows:
|
||||
if not include_unlisted and r["visibility"] == "unlisted":
|
||||
continue
|
||||
out.append(dict(r))
|
||||
item = dict(r)
|
||||
item["entry_noun"] = entry_noun(item["type"])
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""§22.4a S6 — the type-driven entry noun.
|
||||
|
||||
The displayed noun for an entry is a framework concept keyed on the
|
||||
collection's immutable type: document→"RFC", specification→"Spec", bdd→"Feature".
|
||||
The chrome reads it from the API rather than hardcoding "RFC", so the propose
|
||||
CTA + entry chrome name entries correctly per collection type with no
|
||||
per-deployment config.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import collections, db
|
||||
from test_propose_vertical import ( # noqa: F401
|
||||
app_with_fake_gitea, provision_user_row, sign_in_as, tmp_env,
|
||||
)
|
||||
|
||||
|
||||
def test_entry_noun_map():
|
||||
assert collections.entry_noun("document") == "RFC"
|
||||
assert collections.entry_noun("specification") == "Spec"
|
||||
assert collections.entry_noun("bdd") == "Feature"
|
||||
# An unknown/future type falls back to the generic noun, never label-less.
|
||||
assert collections.entry_noun("mystery") == "RFC"
|
||||
|
||||
|
||||
def test_collection_api_surfaces_entry_noun(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
provision_user_row(user_id=1, login="ben", role="owner")
|
||||
sign_in_as(client, user_id=1, gitea_login="ben", display_name="Ben",
|
||||
role="owner", email="ben@test")
|
||||
# Flip the default collection to a bdd type and confirm the noun follows.
|
||||
db.conn().execute("UPDATE collections SET type='bdd' WHERE id='default'")
|
||||
r = client.get("/api/projects/default/collections/default")
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["entry_noun"] == "Feature"
|
||||
|
||||
|
||||
def test_directory_items_carry_entry_noun(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
db.conn().execute("UPDATE projects SET visibility='public' WHERE id='default'")
|
||||
db.conn().execute("UPDATE collections SET type='specification' WHERE id='default'")
|
||||
r = client.get("/api/deployment")
|
||||
assert r.status_code == 200, r.text
|
||||
item = next(p for p in r.json()["projects"] if p["id"] == "default")
|
||||
assert item["entry_noun"] == "Spec"
|
||||
Reference in New Issue
Block a user