§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:
Ben Stull
2026-06-06 01:35:57 -07:00
parent 79a27a946b
commit 839404da0c
5 changed files with 104 additions and 10 deletions
+8 -2
View File
@@ -31,6 +31,9 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
// collection's caps load; we fall back to "any authenticated viewer" so the
// common case doesn't flicker, then refine.
const [canContribute, setCanContribute] = useState(null)
// §22.4a: the type-driven entry noun ("RFC" | "Spec" | "Feature") for this
// collection, read from the API. Defaults to the generic "RFC" until loaded.
const [entryNoun, setEntryNoun] = useState('RFC')
const [search, setSearch] = useState('')
const [sort, setSort] = useState('recent')
const [activeChips, setActiveChips] = useState(new Set())
@@ -46,7 +49,10 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
listProposals(pid).then(d => setProposals(d.items)).catch(() => setProposals([]))
setCanContribute(null)
getCollection(pid, cid)
.then(c => setCanContribute(!!c?.viewer?.can_contribute))
.then(c => {
setCanContribute(!!c?.viewer?.can_contribute)
setEntryNoun(c?.entry_noun || 'RFC')
})
.catch(() => setCanContribute(false))
}, [version, pid, cid])
@@ -177,7 +183,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
// contribute to *this* collection (the scope-role gate); a granted
// viewer with no contribute right in this collection sees nothing.
mayPropose && (
<button className="btn-propose" onClick={onProposeRFC}>+ Propose New RFC</button>
<button className="btn-propose" onClick={onProposeRFC}>+ Propose New {entryNoun}</button>
)
) : (
<a className="btn-propose" href="/auth/login" title="Private beta — only invited emails can propose">