Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 KiB
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 |
|
|
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:
- 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/deploymentprojects[]and/api/projects/:id/collectionsalready 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). - 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) |
| removed | re-homed to Collection admin (§3.6) | |
| 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, gatedis_collection_superuser. The old/api/admin/graduation-queueis 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, gatedis_collection_superuser. - Un-retire becomes collection-scoped:
POST /api/projects/:pid/collections/:cid/rfcs/:slug/unretire, replacing the slug-only owner-gatedPOST /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 forresolve_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
gatedproject/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:
- New collections default their create-form visibility to
gated. - 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
publicunder agatedparent 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.yamllives at a conventional subfolder (<collection_id>/); the registry maps the collection by that convention. This design records an explicitpathfor 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.yamlat 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.gitkeepif 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.yamlvia the bot, then triggers a registry refresh — the same git-is-truth → mirror loop collection-create uses. No direct write tocollections.config_json(the mirror stays derived). - Reuses
metadata_schema.parse_fields/validate(already lenient, advisory) for in-form validation; field typesenum/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_malformedadvisory flag), and the Owner-gatedmetadata.migrate_collectionendpoint (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: truein.collection.yamlor the project registry), committed via bot → mirrored to acollections.retiredcolumn, 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.1–3.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:97–100), 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-enumfield types in the schema editor (still deferred per the metadata design).- Eliminating the collection
typefield. The metadata work already removed type-driven schemas (D9/D11); fully retiringtype(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 —typeis 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 S3–S6; 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.pyprimitives. - 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.