Add 'specs' collection: rfc-app solution designs for in-app review
regenerate-readme-index / noop (push) Has been cancelled

Seeds the framework's docs/design/*.md as a document collection on the ohm
project (subfolder specs/, unlisted) so the solution designs can be reviewed
and collaborated on in the RFC app until graduated. Dogfood from session
OHM-0090.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 21:09:41 -07:00
parent f65a2234f7
commit 85edd17ac4
8 changed files with 3218 additions and 0 deletions
+549
View File
@@ -0,0 +1,549 @@
---
slug: 2026-06-08-scoped-admin-ia
title: "Scoped Admin Information Architecture"
state: super-draft
id: null
repo: null
proposed_by: ben.stull@wiggleverse.org
proposed_at: '2026-06-08'
graduated_at: null
graduated_by: null
owners:
- ben.stull@wiggleverse.org
arbiters: []
tags:
- solution-design
---
# Solution Design: Scoped Admin Information Architecture
| | |
| --- | --- |
| **Author(s)** | Ben Stull |
| **Reviewers / approvers** | Ben Stull |
| **Status** | `draft` |
| **Version** | v0.1.1 |
| **Source artifacts** | Product user stories (session OHM-0090.0) · Binds onto [`2026-06-05-three-tier-projects-collections.md`](./2026-06-05-three-tier-projects-collections.md) (§22) now merged into `SPEC.md` §22 · Resolves decision **D8** of [`2026-06-06-configurable-collection-metadata.md`](./2026-06-06-configurable-collection-metadata.md) |
**Change log**
| Date | Version | Change | By |
| --- | --- | --- | --- |
| 2026-06-08 | v0.1.0 | Initial draft from brainstorming session OHM-0090.0 | Ben Stull |
| 2026-06-08 | v0.1.1 | G-3 resolved (warn+allow); admin surfaces kept type-agnostic (G-11); §9 LLM authoring-context **config** surfaces added (consumption split to a separate spec, G-12) | Ben Stull |
---
## 1. Context
*The business lens is intentionally light: this design's solution is known up front — it is admin functionality inside the RFC app. The inputs are **product** user stories, not business outcomes. §1 frames the problem; the weight is in §3–§5.*
### 1.1 Executive summary
§22 delivered the *authority* model for the three-tier world — unified roles
`{owner, contributor}` attached at any of three scopes `{global, project,
collection}`, composing downward by a most-permissive union (`SPEC.md` §22.6,
§22.7). What it did **not** deliver is the *administration surface* for that
world. The admin **page** is still single-tier: `/admin` is mounted only at
deployment scope, and it mixes grains that belong to different tiers. Worse,
the management actions that *do* exist (create project, create collection,
assign owners) are reachable only through directory pages that the single-item
(`N=1`) landing redirects skip past — so on a typical one-project,
one-collection deployment an Owner has **no UI path** to manage or grow their
own deployment. The value at stake: an Owner at every tier can configure and
grow exactly the scope they own, from a surface that matches the tier they're
standing in — with no API-only dead-ends.
### 1.2 Background — the four defects
| # | Defect | Evidence |
| --- | --- | --- |
| DEF-1 | `/admin` is mounted only at deployment scope | `App.jsx:349` (`/admin/*``AdminWithSidebar`); no `/p/<id>/admin` or `/p/<id>/c/<cid>/admin` route exists |
| DEF-2 | Retired RFCs — a **collection-grained** concept — live in the global, site-owner-only admin, unfiltered across collections | `Admin.jsx:54` (`RetiredTab`, owner-gated); `GET /api/admin/retired-rfcs` lists `cached_rfcs WHERE state='retired'` with **no collection filter** (`api_admin.py:834`); `cached_rfcs` PK is `(collection_id, slug)` (mig 029) |
| DEF-3 | Graduation queue is collection-keyed data shown at site level; the frontend already pulls `useProjectId()` but only for links, never to scope the query | `Admin.jsx:723` (`useProjectId`); `GET /api/admin/graduation-queue` lists `cached_rfcs WHERE state='super-draft'` unfiltered (`api_admin.py:789`) |
| DEF-4 | Create-project / create-collection work but are stranded behind directories the `N=1` landing skips | Buttons live in `Directory.jsx:33/43` and `CollectionDirectory.jsx:66/75`; `DeploymentLanding` redirects past `Directory` when `projects.length===1` (`App.jsx:448`); `CollectionDirectory` redirects past itself when `cols.length===1` (`CollectionDirectory.jsx:45`) |
A fifth, related gap: **decision D8** of the configurable-collection-metadata
design deferred in-app editing of a collection's `fields:` schema (edit
`.collection.yaml` in git for v1). That is logically a Collection-Admin
capability and is folded in here.
### 1.3 Actors / roles
The §22 unified roles, named as the personas this design serves. Authority
**inherits downward** and composes as a most-permissive union (§22.7), so each
row below *is also* every row beneath it within its subtree.
| Role | Scope | Inherits |
| --- | --- | --- |
| **Global Owner** (incl. deployment `owner`/`admin`) | deployment | is a Project Owner of every project, and a Collection Owner of every collection |
| **Project Owner** | one project | is a Collection Owner of every collection in that project (incl. future ones) |
| **Collection Owner** | one collection | — |
| **Contributor** | global / project / collection | propose entries; at project/global scope may also create collections |
| **Requester** | (no role yet) | someone holding a private URL who can request access |
This inheritance is **not new** — it is exactly what `effective_role_at_scope`,
`is_project_superuser`, and `is_collection_superuser` already compute
(`auth.py:407`, `:598`, `:658`). This design only *surfaces* it; it changes no
resolution logic.
### 1.4 Non-goals (this design)
- No change to the §22.7 role-resolution algorithm.
- No per-scope **audit log** or **permission events** (left deployment-wide; see §6).
- No **post-create rename** of a project or collection slug (create-time only; §3.10, decision D-SLUG).
- No collection living in a **different repo** than its project's content repo (path within the project repo only; §3.11, decision D-STORE).
---
## 2. Product — user stories & traceability
The session's product user stories, each given an ID and traced to the surface
that satisfies it (§3) and the decision that shaped it (§5).
| # | User story | Satisfied by | Decision |
| --- | --- | --- | --- |
| US-1 | As a global owner, I want to create a project | Site admin → **Projects** panel (§3.4.1); scope switcher "+ New project" | — |
| US-2 | As a project owner, I want to create an RFC collection and assign owners | Project admin → **Collections** panel (§3.4.2) | — |
| US-3 | As a project owner, I want to configure the project slug (URL) while creating it | Create-project form (already collects `project_id`); §3.10 | D-SLUG |
| US-4 | Global owners are project owners for all projects | §1.3 inheritance — no change (§22.7) | — |
| US-5 | Project owners are collection owners for all collections in their project | §1.3 inheritance — no change (§22.7) | — |
| US-6 | As a collection owner, I want to configure the metadata for my collection | Collection admin → **Fields** panel (D8 resolution, §3.12) | D8 |
| US-7 | As a collection owner, I want to configure the collection slug (URL) | Create-collection form (already collects `collection_id`); §3.10 | D-SLUG |
| US-8 | As a collection owner, I want to configure the default starting state for new documents | Collection admin → **Settings** (`initial_state`, §3.4.3) | — |
| US-9 | As a collection owner, I want to configure the repo & path where my collection is stored | Collection admin → **Settings** (path within project repo, §3.11) | D-STORE |
| US-10 | As a collection owner, I want the path auto-created if it doesn't exist | Bot provisioning on create / settings save (§3.11) | D-STORE |
| US-11 | As a collection owner, I want to retire my entire collection so it's hidden from the project list | Collection admin → **Danger zone** (new `retired` state, §3.13) | D-RETIRE |
| US-12 | As a project owner, I want to mark my project Private so it's hidden from the everyone-list | Project admin → **Settings** (visibility `gated`, §3.8) | D-PRIVATE |
| US-13 | As a project owner, I want collections in a private project to be private by default | Create-collection default + runtime clamp (§3.9) | D-PRIVATE |
| US-14 | As a URL-holder of a private collection, I want to share it and let people request access | Request-access page on gated resources (§3.8) | D-PRIVATE |
| US-15 | As a member of a private project/collection, I want it to show in the project nav dropdown | Scope switcher includes member-visible private scopes (§3.3) | — |
---
## 3. Solution — the scoped admin IA
### 3.1 The model: one admin scope per tier
Three administration surfaces, one per tier, each owning **exactly** its grain
and nothing above or below it:
```
deployment ──▶ Site admin /admin
project ──▶ Project admin /p/<pid>/admin
collection ▶ Collection admin /p/<pid>/c/<cid>/admin
```
The guiding rule: **a surface lives at the tier whose data it mutates.** A
panel that edits collection-keyed data (retired entries, the graduation queue,
metadata fields) lives in Collection admin even if a Project or Global Owner —
who *inherit* collection authority — is the one using it.
### 3.2 Routing
| Route | Shell | Gate |
| --- | --- | --- |
| `/admin/*` | Site admin (existing `Admin.jsx`, slimmed) | `viewer.role ∈ {owner, admin}` *or* global Owner |
| `/p/<pid>/admin/*` | **new** `ProjectAdmin` (inside `ProjectLayout`) | `is_project_superuser(viewer, pid)` |
| `/p/<pid>/c/<cid>/admin/*` | **new** `CollectionAdmin` | `is_collection_superuser(viewer, cid)` |
Conventions, learned from the recent sidebar-nav fix (commit `8e207a6`): tab
rails use **absolute** `NavLink` paths with `end`, never relative links that
accumulate. Add path builders to `entryPaths.js`: `siteAdminPath()`,
`projectAdminPath(pid)`, `collectionAdminPath(pid, cid)`.
`ProjectAdmin` reads its scope from `useProject()` (the `ProjectContext`
provider, `ProjectLayout.jsx:20`). `CollectionAdmin` reads `useCollectionId()`
plus a collection fetch for the `viewer.role` flag (the pattern
`CollectionDirectory.jsx` already uses).
### 3.3 Reachability — the two always-present entry points (fixes DEF-4)
We **do not** special-case the `N=1` redirects. Instead we add two entry points
that are present regardless of how many projects/collections exist:
1. **Scope switcher** (top nav dropdown — satisfies US-15). Lists every project
the viewer can access and, under the current project, its collections —
*including private (gated) ones the viewer is a member of*, because
`/api/deployment` `projects[]` and `/api/projects/:id/collections` already
filter to caller-visible scopes and a member can read a gated scope (§22.5).
Owners additionally see a per-row **Manage** (gear → that scope's admin) and,
at the bottom, **+ New project** (`can_create_project`) / **+ New
collection** (`can_create_collection`).
2. **Gear / "Manage" affordance** on each project landing (`<Welcome/>` / the
project directory) and collection landing, shown to owners of that scope,
linking to the scoped admin.
Because the gear sits on the very page the `N=1` redirect *lands the Owner on*,
the dead-end disappears with no change to `DeploymentLanding` /
`CollectionDirectory` redirect logic. The existing directory create-buttons stay
(reachable when `N>1`); they become a second path, not the only one.
### 3.4 Per-scope surface inventory
#### 3.4.1 Site admin — `/admin` (deployment grain only)
| Panel | Status | Notes |
| --- | --- | --- |
| Users | unchanged | role / write-mute / permission_state |
| Allowlist | unchanged | private-beta OAuth gate |
| Audit log | unchanged | deployment-wide (future scoping — §6) |
| Permission events | unchanged | deployment-wide |
| **Projects** | **new** | list all projects; **create project** (US-1); mark a project **Private** (US-12) |
| ~~Graduation queue~~ | **removed** | re-homed to Collection admin (§3.6) |
| ~~Retired~~ | **removed** | re-homed to Collection admin (§3.6) |
#### 3.4.2 Project admin — `/p/<pid>/admin`
| Panel | Notes |
| --- | --- |
| **Settings** | project name; **visibility incl. Private** (US-12); private cascade default (§3.9) |
| **Collections** | list; **create collection & assign owners** (US-2); per-collection quick links to its admin |
| **Members** | project-scope grants + grant collection-scope (existing scope-aware `POST /api/projects/:pid/members`, `api_memberships.py`) |
| **Graduation (rollup)** | read-only union across the project's collections; convenience view (canonical surface is per-collection) |
#### 3.4.3 Collection admin — `/p/<pid>/c/<cid>/admin`
| Panel | Notes |
| --- | --- |
| **Settings** | name; **default starting state** `initial_state` (US-8); **storage path** within project repo (US-9/10, §3.11); visibility |
| **Fields** | in-app `fields:` schema editor — **resolves D8** (US-6, §3.12) |
| **Members** | collection-scope grants |
| **Graduation queue** | re-homed here, filtered to this collection (§3.6) |
| **Retired entries** | re-homed here, filtered to this collection; un-retire (§3.6) |
| **Danger zone** | **Retire collection** (US-11, new `retired` state, §3.13) |
### 3.5 Permission gating — every surface on the four-layer composition
Each surface gates on an existing `auth.py` primitive; **no new authorization
logic** is introduced. Backend gates are authoritative; the frontend mirrors
them from the `viewer` sub-object already embedded in API responses
(`/api/auth/me`, the collection/collections GETs).
| Surface / action | Backend gate | Frontend flag source |
| --- | --- | --- |
| Site admin | `viewer.role ∈ {owner,admin}` | `/api/auth/me``viewer.role` |
| Create project (US-1) | `can_create_project` (`auth.py:721`, global Owner only) | `/api/deployment``viewer.can_create_project` |
| Project admin | `is_project_superuser` (`auth.py:407`) | `/api/projects/:id/collections``viewer.role==='owner'` |
| Create collection (US-2) | `can_create_collection` (`auth.py:701`) | `viewer.can_create_collection` |
| Project/collection invite | `can_invite_at_project` / `can_invite_at_collection` (`auth.py:740/751`) | `viewer.can_invite` |
| Collection admin | `is_collection_superuser` (`auth.py:658`) | `/api/projects/:id/collections/:cid``viewer.role==='owner'` |
| Edit collection fields (US-6) | `is_collection_superuser` | `viewer.role==='owner'` |
| Retire collection (US-11) | `is_collection_superuser` | `viewer.role==='owner'` |
| Request access (US-14) | `can_request_join` (§22.8) | `viewer.can_request_join` |
### 3.6 Re-homing retired & graduation (fixes DEF-2, DEF-3)
Both are collection-keyed (`cached_rfcs` PK `(collection_id, slug)`). Their
canonical home becomes **Collection admin**, scoped to one collection:
- **New** `GET /api/projects/:pid/collections/:cid/graduation` → the existing
ready/blocked split (owners-set + zero-blocking-PRs), `WHERE collection_id = :cid`,
gated `is_collection_superuser`. The old `/api/admin/graduation-queue` is
retained only to back the **project rollup** (re-gated to project superuser,
filtered to the project's collections) and the panel removed from Site admin.
- **New** `GET /api/projects/:pid/collections/:cid/retired` → `WHERE state='retired'
AND collection_id=:cid`, gated `is_collection_superuser`.
- **Un-retire becomes collection-scoped**: `POST
/api/projects/:pid/collections/:cid/rfcs/:slug/unretire`, replacing the
slug-only owner-gated `POST /api/rfcs/:slug/unretire` (`api_graduation.py:548`).
This closes a real bug: the slug-only route is **collection-ambiguous** when
the same slug exists in two collections (the same class of bug noted for
`resolve_models_for_rfc`). See §4.4.
### 3.7 (reserved)
### 3.8 Private + request-access (US-12, US-14 — decision D-PRIVATE)
Today `gated` returns a bare **404** to non-members (`SPEC.md` §22.5; read gate
in `auth.py:635`). We refine this so a private resource is shareable by URL:
- **Signed-in non-member** hitting a direct URL to a `gated` project/collection
→ a **request-access page** (the resource's display *name* is revealed,
content is not), wired to the existing §22.8 request-to-join flow
(`join_requests`, mig 032) and the owner's existing cross-collection inbox.
- **Anonymous** visitor → redirect to login, *then* the request-access page.
Existence is never revealed to an anonymous caller.
- **Directories** still omit gated scopes (unchanged); existence is revealed
only to someone who already holds the URL **and** has signed in.
Backend: the gated read gate returns a distinguishable **403 + `{name,
can_request_join}`** for a signed-in non-member, vs **404** for genuinely-absent
ids and for anonymous callers. This is a **normative amendment to §22.5** (§4.1)
— the only binding change in this design.
### 3.9 Private-by-default cascade (US-13 — decision D-PRIVATE)
Marking a project Private (`gated`) affects collections two ways:
1. **New** collections default their create-form visibility to `gated`.
2. **Existing** collections need *no* data change: §22.5's keystone already
makes effective visibility the **stricter** of project and collection
("stricter wins"). A child stored `public` under a `gated` parent therefore
*reads* as gated at runtime. We document this and surface it in the
Collections panel ("effective: Private (inherited)") so it isn't surprising.
### 3.10 Slug configuration (US-3, US-7 — decision D-SLUG: create-time only)
Project (`project_id`) and collection (`collection_id`) slugs are the URL id and
are **set at creation** — already collected by `CreateProjectModal` /
`CreateCollectionModal`. This design adds **no post-create rename**: a rename
would break live URLs and re-key `cached_rfcs`, demanding redirect machinery
out of scope here. The Settings panels show the slug **read-only** with a "set
at creation" note. (Post-create rename-with-redirects is a candidate future
slice — §6.)
### 3.11 Collection storage config + auto-provision (US-9, US-10 — decision D-STORE)
Scope (D-STORE): a collection's content lives in **its project's content_repo**;
the collection owner configures the **path (subfolder)** within that repo, and
the path is **auto-created** if absent. No different-repo override.
- Today a collection's `.collection.yaml` lives at a conventional subfolder
(`<collection_id>/`); the registry maps the collection by that convention.
This design records an **explicit `path`** for the collection in the manifest
/ registry so path and slug are decoupled, defaulting to `<collection_id>/`.
- "Auto-create if it doesn't exist": creating a collection already writes
`.collection.yaml` at its path (so the path is provisioned by the create
commit). For a settings-time path that points somewhere new, the bot writes
the manifest (and a `.gitkeep` if needed, since git has no empty dirs) at the
new path. **Moving** an existing collection's files between paths is out of
scope (same rationale as slug rename) — path is settable but the *move* of
existing entries is deferred (§6); see gap G-4.
### 3.12 Metadata field-schema editor (US-6 — resolves D8)
The configurable-collection-metadata work shipped entry-metadata *value* editing
(contributor-gated) but deferred the *schema* editor (D8). This adds it:
- **New** owner-gated write path that commits the `fields:` block of
`.collection.yaml` via the bot, then triggers a registry refresh — the same
git-is-truth → mirror loop collection-create uses. **No** direct write to
`collections.config_json` (the mirror stays derived).
- Reuses `metadata_schema.parse_fields` / `validate` (already lenient,
advisory) for in-form validation; field types `enum` / `tags` / `text` (v1),
matching the value editor.
- **Destructive-edit handling** (decision G-3: *warn + allow*): removing a field
or narrowing an enum can orphan existing entry values. The editor **shows the
usage count and warns, but allows** the edit; orphaned sidecar values are
*kept but ignored* (consistent with the existing `metadata_malformed` advisory
flag), and the Owner-gated `metadata.migrate_collection` endpoint (shipped in
metadata SLICE-4) is offered to normalize. No hard block on removing an in-use
field — re-adding the field restores its values.
### 3.13 Collection retire (US-11 — decision D-RETIRE: distinct state)
A new **`retired` collection state**, mirroring per-entry retire (§13.7):
- **Hidden** from the project's collection list and the scope switcher.
- **Read-only**: write endpoints (propose, metadata edit, etc.) refuse for a
retired collection.
- **Reversible**: an Owner can un-retire from Collection admin.
- Members can still reach it by direct URL (read-only) — we hide, not block
reads (the chosen middle option; not a hard archive).
- **Storage**: a manifest field (`retired: true` in `.collection.yaml` or the
project registry), committed via bot → mirrored to a `collections.retired`
column, keeping git the source of truth (consistent with §22's
registry-is-truth stance). See gap G-2 for the manifest-vs-DB decision detail.
---
## 4. Data model & API deltas
### 4.1 SPEC (binding) amendments — docs-first (§4.1 handbook)
| Section | Change |
| --- | --- |
| §22.5 | Gated read gate: signed-in non-member on a direct URL → request-access (403 + name) instead of 404; anonymous → 404/login. (§3.8) |
| §22 (new sub-section) | The scoped admin IA: three admin scopes, routing, and the per-tier surface ownership rule. (§3.13.4) |
| §22 collection model | New `retired` collection state (§3.13); explicit collection `path` (§3.11). |
### 4.2 New / changed endpoints
| Method + path | Gate | Purpose |
| --- | --- | --- |
| `GET /api/projects/:pid/collections/:cid/graduation` | collection superuser | scoped graduation queue (§3.6) |
| `GET /api/projects/:pid/collections/:cid/retired` | collection superuser | scoped retired list (§3.6) |
| `POST /api/projects/:pid/collections/:cid/rfcs/:slug/unretire` | collection superuser | collection-scoped un-retire (replaces slug-only, §3.6/§4.4) |
| `PUT /api/projects/:pid` (project settings) | project superuser | name, visibility — git-writing (projects.yaml) |
| `PUT /api/projects/:pid/collections/:cid` (collection settings) | collection superuser | name, visibility, initial_state, path — git-writing (.collection.yaml) |
| `PUT /api/projects/:pid/collections/:cid/fields` | collection superuser | D8 schema editor (.collection.yaml `fields:`) |
| `POST /api/projects/:pid/collections/:cid/retire` / `.../unretire-collection` | collection superuser | retire / un-retire collection (§3.13) |
Note: **no project/collection *settings* write path exists today** — only
create + read (confirmed: no `PUT`/`PATCH` for project/collection metadata). The
settings backend (a git-writing `PUT` per scope) is net-new and is the largest
backend lift here (gap G-1).
### 4.3 New columns / state
- `collections.path TEXT` (default `<collection_id>`) — explicit storage path.
- `collections.retired` (state/flag) — mirrored from manifest.
- Both populated by the registry mirror; **git remains source of truth**.
### 4.4 Closing the slug-ambiguity bug
The current `POST /api/rfcs/:slug/unretire` keys by slug alone — ambiguous when
a slug exists in two collections (the same defect class as the known slug-keyed
`resolve_models_for_rfc`). Re-homing un-retire under
`/projects/:pid/collections/:cid/` resolves it. Audit other slug-only entry
routes for the same issue while in this area (gap G-5).
---
## 5. Decisions
| # | Decision | Choice | Date |
| --- | --- | --- | --- |
| D-PRIVATE | How "Private" maps to §22.5 | `gated` + request-access page (refine gated from 404 → request-access for signed-in non-members) | 2026-06-08 |
| D-SLUG | Slug editing scope | Create-time config only; no post-create rename | 2026-06-08 |
| D-STORE | Collection storage config | Path within the project's content_repo + auto-create; no different-repo override | 2026-06-08 |
| D-RETIRE | Collection retire model | Distinct `retired` state — hidden, read-only, reversible (mirrors §13.7) | 2026-06-08 |
| D-IA | Admin IA shape | Per-scope routes + scope-switcher + gear (Approach C) | 2026-06-08 |
| D-GRAD | Graduation/retired home | Canonical at collection scope; read-only rollup at project | 2026-06-08 |
| D-AUDIT | Audit/permission-events scope | Left deployment-wide for now (future scoping) | 2026-06-08 |
---
## 6. Out of scope / deferred
- Post-create **slug rename** (projects & collections) with redirects + cache re-key.
- **Moving** an existing collection's files to a new path (path is settable; the move is deferred).
- Collection in a **different repo** than its project content_repo.
- **Per-scope audit log / permission events** (stay deployment-wide).
- `ref` / `multi-enum` field types in the schema editor (still deferred per the metadata design).
- **Eliminating the collection `type` field.** The metadata work already removed
type-*driven schemas* (D9/D11); fully retiring `type` (today immutable and
terminology-only per §22.4a — it selects the entry noun) is a separate §22.4a
change, not an admin-IA concern. **This spec keeps every admin surface
type-agnostic** — `type` is never presented as a managed setting (see G-11).
---
## 7. Delivery plan (proposed slices)
Each slice is its own writing-plans → executing-plans coding session; plans
authored just-in-time. **S0 is docs-first** (handbook §4.1): amend the binding
SPEC before code.
| Slice | Scope | Delivers |
| --- | --- | --- |
| **S0** | SPEC amendments (§4.1): §22.5 gated→request-access; admin-IA section; `retired` collection state; collection `path` | Binding contract updated |
| **S1** | Scoped admin **routing shell + reachability** (routes, gear, scope switcher), re-home graduation/retired to collection-scoped endpoints + project rollup; slim Site admin | DEF-1/2/3/4 navigational fix; US-15 |
| **S2** | **Settings write paths** (git-writing `PUT` for project & collection: name, visibility, initial_state) + Settings panels | US-8, US-12 |
| **S3** | **Private + request-access** (gated behavior change, request-access page) + private cascade default | US-13, US-14 |
| **S4** | **Collection storage path** config + auto-provision | US-9, US-10 |
| **S5** | **Metadata field-schema editor** (D8) | US-6 |
| **S6** | **Collection retire** state | US-11 |
| **S8** | **LLM authoring-context config surfaces** (project & collection Context panels; git-persisted block + source list; read-time composition + preview) — §9 | new (LLM-context request) |
(US-1/2/3/4/5/7 are largely satisfied by S1's surfacing of existing create
endpoints + inheritance; create-time slug is already collected.)
---
## 8. Risks, gaps & areas to dig into
*The analysis requested at spec time — what this design leans on that isn't yet
proven, and what a planning session must resolve before coding.*
| # | Gap / risk | Why it matters | Proposed resolution / where to dig |
| --- | --- | --- | --- |
| G-1 | **No settings write path exists** — only create + read. A coherent git-writing `PUT` per scope (project & collection) is net-new and is the bulk of the backend lift | S2 underpins S3S6; if the git-write+mirror loop for settings is wrong, everything downstream wobbles | Dig: design one `PUT` that reads the manifest, merges changed keys, commits via bot, refreshes registry — mirror the create flow (`api_collections.py:119`, `bot.create_collection`). Confirm partial-update semantics & concurrent-edit safety |
| G-2 | **Retire storage: manifest field vs DB flag** | Git-is-truth says manifest; but a DB-only flag is simpler and avoids a commit per retire | Recommend manifest field for consistency with §22; confirm the registry can carry a per-collection boolean and that retired collections survive a mirror rebuild |
| G-3 | **Destructive schema edits orphan values** | Removing a field / narrowing an enum can strand sidecar values | Editor warns; keep-but-ignore orphans (reuse `metadata_malformed`); offer `metadata.migrate_collection`. Decide whether to *block* removal of an in-use field or just warn |
| G-4 | **Path decoupled from slug** — registry today maps collection by conventional `<collection_id>/` subfolder | An explicit `path` must thread through manifest → registry → all path-resolving reads/writes; getting it half-done risks entries written to the wrong place | Audit every place that derives a collection's repo path; add `path` to the registry schema; default to `<collection_id>` for back-compat. Verify against `registry.py` parse/upsert |
| G-5 | **Slug-only entry routes are collection-ambiguous** | `unretire` (and possibly retire / others) key by slug alone (same class as `resolve_models_for_rfc`) | Re-home under `/projects/:pid/collections/:cid/`; grep for other `…/rfcs/:slug…` routes lacking collection scope and fix in the same slice |
| G-6 | **Inherited owners aren't membership rows** | A global/project Owner won't appear in a collection's Members list, which could read as "no owners" | Members panels manage *explicit* grants at that scope; add a read-only "inherited owners (via project/global)" note computed from `effective_role_at_scope`. Decide display, don't change resolution |
| G-7 | **Existence leak on gated request-access** | Revealing a private resource's name to any signed-in URL-holder is a deliberate tradeoff | Accept for the URL-holder (they have the link); never reveal to anonymous. Confirm no deployment needs a strict-404 mode; if so, make it a per-resource toggle (future) |
| G-8 | **Scope-switcher data shape** | Listing accessible-incl-private projects + per-project collections could be N calls | Lazy-load collections when a project row expands, reusing existing endpoints; only build a "my scopes" aggregate endpoint if the N calls prove heavy |
| G-9 | **"Assign owners" at collection create (US-2)** | Create returns the collection; owner assignment is a second `POST …/members` call | Two-step (create → manage members) is acceptable for v1; inline owner-assign at create is a nicety. Decide in S1/S2 |
| G-10 | **Graduation rollup reuses the old global endpoint** | Re-gating `/api/admin/graduation-queue` to project superuser + project filter must not regress the deployment-wide view if any caller still depends on it | Verify no remaining caller expects the unfiltered global behavior before re-gating |
| G-11 | **`type` retained but slated for elimination** | §22.4a keeps `type` immutable + terminology-only; the operator's stated direction is "all collections are documents + metadata." Admin surfaces must not present `type` as a managed setting or they'll contradict that direction | Admin IA stays type-agnostic; track full `type` elimination as its own §22.4a design (out of scope here) |
| G-12 | **LLM authoring-context provisioning** (new request) | A per-scope context block + context-source repos/refs for the in-app LLM raises composition, permission-leak, retrieval-architecture, and vendor-coupling questions (see §9) | Split: admin IA owns the *config surfaces*; a separate spec owns the *consumption/retrieval architecture* (§9) |
### Self-review against the solution-design requirements
- **Placeholders:** none — §3.7 is an intentional reserved spacer, all decisions resolved.
- **Internal consistency:** the per-tier "data it mutates" rule (§3.1) is applied consistently in the re-homing (§3.6) and the surface inventory (§3.4); gates (§3.5) all map to existing `auth.py` primitives.
- **Scope:** broad but coherently sliceable (§7); S2 (settings write path) is the load-bearing prerequisite and is called out as such (G-1).
- **Ambiguity:** the four decisions that had no clear default are pinned in §5; the remaining unknowns are *implementation* unknowns, captured as G-1..G-12 for the planning sessions rather than left implicit.
---
## 9. LLM authoring-context configuration (new)
*Added this session. Decision: the **configuration surfaces** are specced here
(they are genuinely admin IA); how the in-app LLM **consumes** the sources —
retrieval vs RAG, the permission model, token/freshness — is a **separate
design** (§9.5, gap G-12).*
### 9.1 Two distinct concepts (do not conflate)
| Concept | What it is | Scope | Storage |
| --- | --- | --- | --- |
| **Instruction block** | free-form markdown guidance — what this corpus is, how to collaborate on it | project & collection | git, **once per scope** |
| **Context-source list** | ordered list of sources the LLM may draw on: `repo`, `repo+path`, or a **collection-ref** | project & collection | git (scope config) |
Instructions (KB of guidance) and retrieval data (MB of code) have different
sizes, lifecycles, and trust levels — they are separate config, not one field.
### 9.2 Composition — store once, compose at read time (decision)
Project and collection blocks are stored **separately and once each**; the app
**composes** them when building the LLM prompt for an entry:
```
effective authoring context = project.instruction_block ∘ collection.instruction_block
effective sources = project.sources collection.sources
```
No project text is ever copied into a collection file. Updating the project
block changes every collection's effective context with **no file rewrites** —
the opposite of the rejected concatenate-into-files model, which drifts and
clobbers hand-edits. This mirrors the §22.7 downward role composition exactly.
**Storage:** each block is a scope-config value — a referenced `CONTEXT.md` at
the project root / collection path (neutral filename, not vendor-locked),
recorded in `.collection.yaml` / the project registry and mirrored to the DB. A
physical `CLAUDE.md` for real coding sessions, if wanted, is **generated from**
the composition or uses `@import` to reference the project file — never an
embedded copy.
### 9.3 Admin surfaces
- **Project admin → Context** (new panel): edit the project instruction block;
manage project-level sources. Gated `is_project_superuser`.
- **Collection admin → Context** (new panel): edit the collection block; manage
collection sources; **preview the composed result** (project ∘ collection) so
the owner sees exactly what the LLM will receive. Gated `is_collection_superuser`.
### 9.4 Context-source model — unify with `ref`
A source is `repo` (whole repo), `repo+path` (subtree), or a **collection-ref**
(another collection in the deployment). The collection-ref case is the **same
primitive as the deferred `ref` field type** (metadata design D10) — build one
cross-collection reference mechanism, not two.
### 9.5 Deferred to the consumption design (NOT here)
Explicitly out of scope for admin IA; belongs to the separate consumption spec:
- **Retrieval architecture** — tool/file-fetch vs embeddings/RAG (a whole repo
cannot be prompt-stuffed; this drives cost, freshness, infra).
- **Permission model / exfiltration guard** — context-repo reads must be scoped
to what the asking user may already see, or each source flagged explicitly
shareable; otherwise the LLM is a confused-deputy exfiltration channel. **Hard
prerequisite before consumption is enabled.**
- **Token budget, freshness, pinning** (latest vs a pinned ref).
- Tracing how the **current in-app chat** injects context today (it has a system
prompt per `CLAUDE.md`; its retrieval model is untraced).
### 9.6 Delivery
New slice **S8 — context config surfaces**: project & collection Context panels,
git-persisted blocks + source lists, read-time composition + preview. Independent
of the consumption design and ships value alone — owners can author context, and
the composed block can be surfaced/copied manually until consumption is built.