MVP-1 client-reachable surface inventory — 2026-08-12 (OGUR-70)¶
Governing rule: mvp-scope.md §2.3 / §5 — every surface a client
can reach must be real; anything not real must be gated (unreachable) or
deleted. This document records the per-surface decision QA reads.
Gate mechanism. frontend/src/lib/demoMode.ts → demoSurfacesEnabled(),
reading VITE_DEMO_SURFACES at build time. Default (unset) = gated — client
builds, CI, and the deployed container never register the demo routes and hide
every affordance pointing at them. Founders opt back in locally with
VITE_DEMO_SURFACES=true. The gate is deliberately not identity-based: User
has no role field, and an anonymous visitor at the deployed URL is a pre-login
client, so the flag must hold regardless of session. No mock was wired to a real
endpoint (§5 forbids it — conversational Ask is OUT).
Evidence pass. 2026-08-12, exercised as the client identity
genfit.provisional@ogur.local (seeded by scripts/seed/seed_users.py
--provisional, logged in via POST /api/auth/login) against the built frontend
served same-origin by the API (OGUR-67 shape). Before gating, that session could
reach the full Sanofi mock franchise, mock Ask, mock conversation history, the
Dupixent asset workspace with a simulated briefing, and blank pages at
/settings and unknown URLs. After gating, every row below was re-verified.
| Surface | Entry points | Observed as client (pre-gate) | Disposition | Mechanism | Evidence | Verified by |
|---|---|---|---|---|---|---|
/ ModeSelector |
root URL, LeftRail logo | Explore + Monitor cards | real (Monitor card gated) | conditional card render | ModeSelector.tsx |
clientGating: "ModeSelector offers Explore but not Monitor" |
/explore Workspaces tab |
rail, ModeSelector, ⌘K, catch-all target | real built-landscape roster plus mock workspaces + templates | real (mock cards gated) | WORKSPACES grid demo-only |
WorkspacesTab.tsx |
manual (gated browse) |
/explore Asset catalog tab |
tab bar, ?tab=Asset catalog |
mock ALL_PORTFOLIO_ASSETS table |
gated | tab removed; ?tab= validated against visible tabs |
Explore.tsx |
clientGating: "crafted ?tab= URL" |
/explore/research/new ResearchWizard |
Workspaces hero | real decompose/classes/match; mock adjacency panel | real (adjacency panel gated) | panel demo-only | ResearchWizard.tsx |
manual |
/explore/landscape/:id LandscapeReport |
built-landscapes cards | real frozen packs + feedback | real — untouched | — | LandscapeReport.tsx |
manual (GNS561 report renders) |
/signals (+ /deals redirect) |
rail, ⌘K | real GET /api/signals |
real — kept | — | GlobalSignals.tsx |
manual |
/ask GlobalAsk |
⌘K, direct URL | scripted mock answers (MOCK_FALLBACK_ENABLED) |
gated | route unregistered → catch-all | App.tsx |
clientGating route table |
| Ask pill + ⌘J + hint bubble + AskDrawer | every shell page | opens mock Ask drawer | gated | all four behind demo in AppShell |
App.tsx |
clientGating: "no Ask AI pill, ⌘J opens nothing" |
/franchise, /franchise/:slug |
rail, ModeSelector Monitor | full Sanofi mock franchise (hand-written LOE, phase mix, cliffs) | gated | routes unregistered | App.tsx |
clientGating route table |
/history |
rail | MOCK_CONVERSATIONS (12 fabricated threads) |
gated | route unregistered | App.tsx |
clientGating route table |
/asset/:drugName |
⌘K assets, asset tables, Inspector | mock briefings, SIMULATED docs, Dupixent-only timeline on every asset (CompetitiveTimeline default DUPIXENT_TIMELINE) |
gated | route unregistered — covers §5's timeline + LandscapeTab hardcode; every entry point also gated |
App.tsx |
clientGating route table |
/foundations |
URL only | design-system gallery | gated | route unregistered | App.tsx |
clientGating route table |
/settings rail link |
rail | blank page — route never existed | deleted (both modes) | nav item removed | LeftRail.tsx |
clientGating + demo file |
| Unknown URLs | — | blank page (no 404 route) | fixed | path="*" → /explore |
App.tsx |
clientGating: /no-such-route |
| LeftRail "My Franchise", "Conversation history", ⌘J shortcut row | rail | links into gated surfaces | gated | nav/shortcut arrays filtered | LeftRail.tsx |
clientGating: "LeftRail shows Explorer only" |
| ⌘K palette: Ask item, Assets, Targets, Companies groups | ⌘K | navigate to gated routes / open mock Inspector panels | gated | conditional groups | CommandPalette.tsx |
clientGating: "command palette has no…" |
| Inspector "Summarize with AI" | signal selection | mockSummarize() behind fake 700ms delay, rendered as "AI summary" |
gated | button demo-only; real signal.summary source text kept |
Inspector.tsx |
clientGating: Inspector signal panel |
| Inspector asset / target / company panels | ⌘K, entity chips | SANOFI mock hydration; static placeholders; "Open full view" → gated /asset/* |
gated | panel renders nothing for non-signal selections | Inspector.tsx |
clientGating: "mock panel renders nothing" |
Accepted debt (recorded, not gated)¶
GlobalSignals.tsxhardcodeslandscape_id: 'nsclc-001'— real data, wrong scope parameterization. Real per §5; fix is post-release.src/mocks/*files,MOCK_FALLBACK_ENABLED,DUPIXENT_TIMELINE, and theLandscapeTabdupilumab hardcode remain on disk — unreachable from a client build. §5 names their deletion as post-release debt, not an Aug 13 requirement./dealsredirects into/signals; if/signalsis ever gated,/dealsmust follow.
Replay¶
- Gated default:
npx vitest run src/__tests__/clientGating.test.tsx - Demo mode:
npx vitest run src/__tests__/clientGating.demo.test.tsx - Live: build without
VITE_DEMO_SURFACES, serve via the API (FRONTEND_DIST_PATH=frontend/dist), sign in as a provisional client, and walk the table.