ADR-059: Platform UI adopts project-first information architecture
Status: Accepted Date: 2026-04-22 Extends: ADR-058 (five-mode-platform-ui-extends-adr-054)
Context
ADR-054 and ADR-058 landed the five-mode shell — Home / Ship / Operate / Infra / Experiment — as the Platform UI's primary navigation. Modes are verbs: "what are we building / is it running / what's underneath / what are we learning". They answer "which lens do I want on the fleet right now?"
On 2026-04-22 afternoon, during the IA review for the new homepage, the operator surfaced a different primary question: "how is this product doing?" The example used was Focus — a product that spans multiple repos (focus-frontend, focus-backend, focus-mobile), has docs, has a todo list, has customers, and has a lifecycle. The mode-first IA answers fleet questions cleanly but is awkward for the product-centric question; every "how is Focus doing?" answer requires filtering four separate mode views.
Two further shapes surfaced during the discussion that expand the requirement:
- Internal projects used by the public products. The fleet is not just
the five external-facing products. There are ~9 internal projects
(libraries and tooling:
gyrum-go,gyrum-js,dark-factory,ai-frontend,ai-research-run,devtools, and others) that exist specifically to power the external five. They are first-class projects, not hidden infrastructure. - Restricted projects. Some projects are standalone SaaS products that the operator is trialing before wider release — for example, a social-proof component and a shared API — which are consumed as services by the external products. These have their own runtimes, their own metrics, and their own (closed, restricted-signup) public surfaces. They are neither public products nor internal libraries.
The mode-first IA does not have a clean place to put any of this without tagging-and-filtering gymnastics. Promoting Project to a first-class noun, orthogonal to modes, gives each class of work its own surface while preserving modes as cross-cutting lenses.
Decision
The Platform UI adopts a project-first information architecture, with
modes retained as orthogonal fleet-wide lenses. Project is a first-class
noun in the URL, in the navigation, and in the data model.
1. Project classes
Three classes, distinguished by who consumes the project and how they access it:
| Class | Consumers | Access | Examples |
|---|---|---|---|
| Product | End users (paying) | Public — open signup | Focus, Hiphip, Research, Guardian |
| Restricted | Other projects, and a closed trial audience | Gated — signups controlled | social-proof, api-gateway |
| Platform | Developer teams (internal) | Internal only | gyrum-go, gyrum-js, dark-factory, devtools |
Restricted replaces an earlier working name ("Alpha") that implied a maturity stage; the real distinction is access control, not product maturity. A Restricted project can be production-grade and still be Restricted — the operator has not yet opened signup to the world.
Class determines:
- Which grid a project appears in on the homepage
- Which KPIs are relevant (user-facing uptime vs. internal-consumer health)
- Which UI affordances apply (e.g. Products and Restricted projects have a Run tab; Platform libraries usually do not)
2. URL structure — path-scoped, not query-scoped
Project scope lives in the path, not a ?project= query string. Path scoping
gives shareable URLs ("send me the Focus operate view") and clean routing.
/ → Fleet home (all projects, cross-cutting dashboard)
/ship, /operate, ... → Fleet-wide mode lenses (today's behaviour, unchanged)
/projects → Project index (grouped by class)
/focus → Focus product overview
/focus/build → Focus — Build tab (Ship-equivalent view, scoped)
/focus/run → Focus — Run tab (Operate-equivalent view, scoped)
/focus/docs → Focus — Docs
/focus/todos → Focus — Todos
/platform/gyrum-go → Platform project (namespaced to avoid slug collisions)
/restricted/social-proof → Restricted project (namespaced)
The /platform/* and /restricted/* namespaces prevent product slugs from
colliding with platform or tooling slugs. Products sit at the root (/focus)
because the operator's primary navigation target is the product.
3. Project tabs
Every project has a consistent tab shape, so learning one project teaches you all of them:
| Tab | What it shows | Hidden when |
|---|---|---|
| Overview | Status, today's KPIs, recent activity | — (always shown) |
| Build | Repos, open PRs, CI status, library versions | — |
| Run | Uptime, alerts, requests, errors, SLOs | Class = Platform with no runtime |
| Docs | Project docs tree (filtered into the shared /docs viewer) |
— |
| Todos | TODO.md from the project's main repo, rendered as checklist |
— |
Run tab is hidden for Platform libraries (gyrum-go has no runtime). Every
other class gets all five tabs.
4. Project YAML as source of truth
Each project is defined by a YAML file under projects/ in the dark-factory
repo. Schema lives in docs/specs/project-spec.md. Example:
slug: buzzy
name: Buzzy
class: product
visibility: public
lifecycle: building
owner: jon
repos:
- buzzy-frontend
- buzzy-backend
consumes:
- gyrum-go
- social-proof
consumed_by: []
docs_root: docs/projects/buzzy
The backend (ai-research-run) reads projects/*.yaml and exposes a
/api/v1/projects endpoint. Every other fleet endpoint that takes a scope
filter accepts an optional project slug and uses the YAML to resolve which
repos / services / metrics belong to that project.
5. Lifecycle states
Every project carries a lifecycle state — planning | building | live | maintenance | sunset. This drives visual treatment on the homepage grid
(a planning project's card does not sit next to a live one with the same
weight) and sort order. Default list views hide planning and sunset.
6. Dependency graph
Projects declare upstream dependencies with consumes (list of project
slugs) and downstream with consumed_by. The two edges mirror each other —
enforced by a lint check in CI. This graph powers:
- Impact analysis on each project's page — "Focus depends on social-proof (Restricted, healthy) and gyrum-go v2.3 (latest)"
- A DAG visualisation on the homepage — red edges when an upstream is unhealthy, so a product owner sees platform-health ripple into product- health at a glance
- Sunset safety — cannot set a project to
sunsetwhile any downstream project still consumes it (CI check)
7. Modes coexist — they become fleet-wide lenses
The five modes do not go away. After ADR-059 they answer a different
question: "give me this lens across the whole fleet". /ship shows every
open PR across every project; /operate shows every active alert; /infra
shows the physical layer underneath everything.
Default entry flips from mode-first to project-first. The homepage (/)
is the fleet dashboard. A user who wants to drill into a product clicks its
card. A user who wants to sweep across projects clicks a mode button in the
mode pill. Both patterns remain one click away.
8. Homepage shape
The homepage becomes the "business owner" dashboard the operator asked for:
- "What's on fire" band — active alerts / failing deploys / incidents, rendered only when red
- "Today vs. yesterday" KPI strip — fleet uptime, total requests, error rate, active deploys, each with a delta
- Products grid — 5 cards (today; grows), each with status dot + headline metric + sparkline
- Restricted projects grid — cards with a "Restricted" badge, showing signup-gate state
- Platform grid — library/tooling cards showing current version + consumers-on-latest count + health
- Dependency graph — small DAG showing upstream/downstream edges across all projects
- Recent activity timeline — cross-fleet deploys, merges, incidents, newest first
9. Sidebar behaviour
Mode-dependent sidebar (ADR-058) extends to project scope:
- Inside a project (
/focus/*) — sidebar shows the project's tabs and any project-specific nav - At fleet mode (
/operate/*) — sidebar shows mode nav (today) - At homepage (
/) — sidebar shows a project chooser + fleet actions
10. "Today vs. yesterday" needs a daily snapshot
The delta-vs-yesterday KPIs require persisted daily snapshots. A cron writes
current KPIs to a kpi_snapshots table at midnight UTC; the KPI endpoint
returns {current, yesterday, delta}. This is a small backend addition
(table + cron + endpoint) tracked as a follow-up task, not in this ADR's
scope.
Consequences
Easier.
- The primary question "how is Focus doing?" is a single click (
/focus), not four mode views and a mental merge - Restricted projects have a clean home — they are neither products nor platform; they have their own grid and their own namespace
- Platform projects (libraries) are visible rather than hidden — consumers can see version currency and dependency health at a glance
- Sunsetting a project is a safe operation — the CI lint catches downstream consumers
- Docs and todos get a natural home per project rather than floating free
Harder.
- Two primary navigation patterns now coexist (project-first and mode-first). Teaching a new user "when do I use which?" needs a clear answer
- Every backend endpoint that returns fleet data now needs an optional project-slug filter — one-time retrofit
- The project YAML becomes load-bearing — a misconfigured
consumesedge paints the wrong dependency graph
What we sign up to maintain.
projects/*.yamlas the single source of truth for project membership. A repo that is not listed in any project'srepos:is invisible in the project-first IA — this is intentional (forces explicit ownership)- The three-class taxonomy. A fourth class (e.g. "Deprecated but still running") is a future ADR, not a silent addition
- Lifecycle states. If new states are needed (e.g.
trial-closed), that is an explicit extension with explicit UI treatment, not a silent YAML field - The mirrored
consumes/consumed_bypair. CI enforces; manual edits that break the mirror are a hard failure, not a warning
Alternatives considered
- Six modes (add Project as Cmd+6). Rejected — Cmd+1..5 is a clean muscle-memory contract, and Projects are nouns not verbs. Using a mode slot for a project selector collapses two axes into one and muddles the model.
- Projects as a query-string filter (
/ship?project=focus). Rejected — path scoping is more shareable, generates cleaner breadcrumbs, and lets the sidebar cleanly distinguish "I'm in a project" from "I'm in a mode". - Single flat project list, no classes. Rejected — the KPIs that matter for a Product (user-visible uptime, signups, MRR) are not the KPIs that matter for a Platform library (version adoption, breaking-change risk). One grid cannot serve both.
- Todos as GitHub issues aggregated by label. Considered. Rejected for
v1 in favour of per-project
TODO.md— simpler, no label discipline required, works offline, renders anywhere. GitHub issues remain the home for bug tracking and PR-linked work; Todos is for the PM-level backlog. A later ADR may reconsider ifTODO.mdproves too thin. - Keep "Alpha" as the class name. Rejected in discussion — "Alpha" implies a maturity stage (pre-v1), but the real distinction is access control. A production-grade project with gated signup is still Restricted; relabelling to "Alpha" would wrongly suggest it is unfinished.
Related ADRs
- Extends: ADR-058 (five-mode Platform UI) — modes coexist with projects; ADR-058's URL/palette/sidebar contract still holds at the fleet level.
- Supersedes by extension: none — this is a pure addition. The five mode routes continue to exist and remain addressable.
- Followed by: ADR-06X (backend KPI snapshot for today-vs-yesterday deltas — to be written when that work lands).
Extended by
Later ADRs that build on this one's project-first IA and YAML schema:
- ADR-060 — the
catalog-driven infrastructure work validates project YAML and
joins catalog targets to projects via the
repos:and (future)servers:lists.scripts/validate-projects.sh(PR #294) is the CI enforcement point. - ADR-061 —
gyrum-projectsis the standalone library that implements this ADR's YAML schema and validator, shared across dark-factory, the ai-research backend, and client-side import flows. - ADR-063 — projects gain
a
tenant:field and carry tenant scope through every endpoint that filters by project. - ADR-064 — the gyrum-catalog library is the concrete implementation that satisfies ADR-060's capture layer for projects defined by this ADR.
Reference projects: buzzy (Product) + social-proof (Restricted,
consumed by Buzzy). Together they exercise both primary classes, the
consumes / consumed_by edge, the lifecycle states, and the access-control
distinction. The first end-to-end project-first UI build uses these two as
the reference implementation.
Ship PRs: to be filed against ai-frontend and ai-research-run after this ADR is accepted.