Files
ohm-content/specs/rfcs/2026-06-08-scoped-admin-ia.md
T
ben.stull 5d0cadda3b
regenerate-readme-index / noop (push) Has been cancelled
Sync scoped-admin-ia entry to v0.1.2 (§4.5 collection discovery)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:19:36 -07:00

37 KiB
Raw Blame History

slug, title, state, id, repo, proposed_by, proposed_at, graduated_at, graduated_by, owners, arbiters, tags
slug title state id repo proposed_by proposed_at graduated_at graduated_by owners arbiters tags
2026-06-08-scoped-admin-ia Scoped Admin Information Architecture super-draft null null ben.stull@wiggleverse.org 2026-06-08 null null
ben.stull@wiggleverse.org
solution-design

Solution Design: Scoped Admin Information Architecture

Author(s) Ben Stull
Reviewers / approvers Ben Stull
Status draft
Version v0.1.2
Source artifacts Product user stories (session OHM-0090.0) · Binds onto 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

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
2026-06-08 v0.1.2 §4.5 prompt collection discovery (content-repo webhook → refresh_registry, parity with create-collection API; D-DISCOVER), surfaced by the dogfood; added to S1 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/meviewer.role
Create project (US-1) can_create_project (auth.py:721, global Owner only) /api/deploymentviewer.can_create_project
Project admin is_project_superuser (auth.py:407) /api/projects/:id/collectionsviewer.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/:cidviewer.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/retiredWHERE 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).

4.5 Prompt collection discovery on content-repo push (decision D-DISCOVER)

A collection is discovered — registered in the collections table — only by refresh_registry (→ _mirror_named_collections, which scans each content repo for .collection.yaml subdirs, registry.py:305). The create-collection API already calls refresh_registry (api_collections.py:176), so collections created in-app appear immediately. But the content-repo Gitea webhook does not: on a content-repo push it calls only refresh_meta_repo / refresh_meta_branches / refresh_meta_pulls (webhooks.py:97100), never refresh_registry. So a .collection.yaml change made directly in git — a new collection, or (in this design) a manifest edit that flips visibility, retired, or path — is invisible until the next 300 s reconciler sweep (cache.Reconciler, cache.py:765/789).

This was observed first-hand seeding this very spec's review collection (a git-native collection took a full sweep to appear, and the visibility flip took another). The fix: the content-repo webhook also triggers refresh_registry when the push touches a .collection.yaml (or unconditionally — the refresh is idempotent and cheap), bringing the git-native path to parity with the create-collection API. This makes every manifest-backed admin write in this spec (visibility, retire, path, fields) take effect promptly rather than on the sweep. The 300 s reconciler stays as the backstop; this is the fast path, not a replacement.


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
D-DISCOVER Prompt collection discovery Content-repo webhook also triggers refresh_registry on a .collection.yaml change (parity with the create-collection API); 300 s reconciler stays as backstop 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-agnostictype 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; content-repo webhook → refresh_registry (§4.5) 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.