OSS-first docs
These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.
Single navigation entry contract that binds a capability to a web route with persona and grouping semantics. Includes optional mobile-extension fields for Expo Router native navigation (Phase D.0).
Overview
`NavSurfaceItemSpec` mirrors the `PresentationSpec.capability` pattern and defines a hard binding between:
•
A registered **capability** that provides the navigation surface (validated against CapabilityRegistry)
•
A **route** (web path + optional API root)
•
**Persona visibility** (which personas can see this entry)
•
**Grouping** (sidebar/nav section)
•
**Icon** (logical key resolved at render time)
Key Semantics
**Capability Binding**: Every nav item MUST reference a `CapabilityRef` so the host can validate at build time that every navigation surface is backed by a real capability registered with `surface: 'navigation'`.
**Persona Visibility**: The `personaVisibility` array lists personas allowed to see the entry. An empty array means hidden from all personas.
**Optional Policy**: The `policy` field adds an additional gate on top of persona visibility (e.g., require a feature flag or explicit permission).
**Optional Density Hint**: The `densityHint` field suggests UI density ('compact' | 'comfortable') applied to the rendered entry.
Mobile Extension Fields (additive, Phase D.0)
Three optional fields enable native Expo Router navigation without app-level hard-coding:
**`tabBarIcon?`**: Logical icon key for native tab bar rendering, separate from the web sidebar `iconKey`. When absent the host may fall back to `iconKey` or a platform default. Only meaningful on tab-root items (`parentGroupKey` absent).
**`gestureHint?`** (`NativeGestureHint`): Declares the primary gesture affordance for this screen. Use `'none'` to explicitly disable back/dismiss gestures (e.g. auditor audit-log where accidental swipe-dismiss would lose evidence context).
**`parentGroupKey?`**: Expresses the tab-vs-stack hierarchy. Absent = item IS a tab-bar root. Present = item is a stack-child pushed onto the named tab's navigator and does NOT appear in the tab bar.
Tab/stack hierarchy example:
•
`cockpit` → `parentGroupKey: undefined` → tab bar root
•
`work-graph` → `parentGroupKey: 'cockpit'` → stack child under cockpit tab
•
`inbox` → `parentGroupKey: undefined` → tab bar root
•
`threads/[id]` → `parentGroupKey: 'inbox'` → stack child under inbox tab
Web shells ignore these fields (absent optional fields produce no effect).
Usage in NavRegistry
The `NavRegistry` consumes `NavSurfaceItemSpec` entries and provides:
•
`indexByPersona(persona)`: Get all entries visible to a persona
•
`indexByGroup(groupKey)`: Get all entries in a nav group
•
`resolveForRoleMorph(roleMorph, preferences)`: Resolve visible entries for a role-morph projection (filtered by persona + hidden groups)
Example
const inboxNav = defineNavSurfaceItem({
meta: {
key: 'inbox',
version: '1.0.0',
description: 'Inbox navigation entry',
goal: 'Direct users to their messages and notifications',
context: 'Core workflow entry point for all personas',
owners: ['team-platform'],
tags: ['core', 'workflow'],
},
capability: { key: 'inbox.read' },
route: { web: '/companyos/inbox', api: '/api/companyos/inbox' },
personaVisibility: ['founder_approver', 'operations_manager', 'frontline_staff'],
groupKey: 'workflow',
iconKey: 'icon.inbox',
densityHint: 'comfortable',
// Mobile extension fields (optional — web shell ignores these)
tabBarIcon: 'tab.inbox',
gestureHint: 'swipe-back',
});