Decisions

ADR-059: Platform UI adopts project-first information architecture

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 /…

#059

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:

  1. 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.
  2. 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 sunset while 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:

  1. "What's on fire" band — active alerts / failing deploys / incidents, rendered only when red
  2. "Today vs. yesterday" KPI strip — fleet uptime, total requests, error rate, active deploys, each with a delta
  3. Products grid — 5 cards (today; grows), each with status dot + headline metric + sparkline
  4. Restricted projects grid — cards with a "Restricted" badge, showing signup-gate state
  5. Platform grid — library/tooling cards showing current version + consumers-on-latest count + health
  6. Dependency graph — small DAG showing upstream/downstream edges across all projects
  7. 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 consumes edge paints the wrong dependency graph

What we sign up to maintain.

  • projects/*.yaml as the single source of truth for project membership. A repo that is not listed in any project's repos: 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_by pair. 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 if TODO.md proves 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-061gyrum-projects is 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.