Overview
arkaik is a product graph browser built on Next.js 16 App Router with React Flow (@xyflow/react). It renders an interactive graph centered on reusable flows and views, with parallel layers for data models and API endpoints.
App Router Structure
app/
layout.tsx # Root layout: fonts (Geist), ThemeProvider, global CSS
page.tsx # Landing page — signed-in visitors redirect straight to /projects
projects/
page.tsx # Project list: create, import, seed, restore
generate/
page.tsx # Prompt builder UI for LLM-assisted ProjectBundle generation
llms-full.txt/
route.ts # Full LLM-readable context bundle (docs + schema + example)
sitemap.ts # XML sitemap route
docs/
layout.tsx # Documentation shell with sidebar + page frame
page.tsx # Docs home: renders repository root README.md
[...slug]/
page.tsx # Markdown document route mapped from docs/**/*.md
project/
[id]/
layout.tsx # Shared project shell: persistent sidebar, switcher, panel-stack provider
page.tsx # Redirects to /project/[id]/overview — a project opens on the global picture
canvas/
page.tsx # Redirects to /project/[id]/maps/journey (old links keep working)
overview/
page.tsx # Overview dashboard — the strategist reading over lib/utils/coverage.ts projections
maps/
page.tsx # Maps index — built-ins + custom maps from project.metadata.maps
[mapId]/
page.tsx # Renderer shell: journey → JourneyMap, system → SystemMap
library/
page.tsx # Gallery/directory node browser (species via sidebar ?species= links)
delivery/
page.tsx # Delivery board — (node × platform) items grouped by status
acceptances/
page.tsx # Acceptance matrix — acceptances against per-platform status
decisions/
page.tsx # Decision log — ADR-style records with their own status
pyramid/
page.tsx # Value-elements aggregation over acceptances (Bain pyramid)
design/
page.tsx # Design funnel — backlog, commitments, decisions from the journal
changelog/
page.tsx # One section per milestone, its deliverables as a timeline
history/
page.tsx # The journal itself, as a raw event log
settings/
page.tsx # Project settings: products, linked repos, danger zone
settings/
tokens/
page.tsx # Account-level `ark_` API tokens (mint/revoke)
p/
[id]/
page.tsx # Publik snapshot preview (server-rendered)
api/
graph/ # Hosted projects: CRUD + the validated `mutations` write path
tokens/ # `ark_` token mint/revoke
github/webhook/ # GitHub App events → acceptance ref promotion
publik/ synk/ auth/ # Share, backup, Auth.js (spec/services.md)
The Journey map (components/maps/JourneyMap.tsx) is the core of the graph renderer; its graph construction is the pure buildJourneyGraph in lib/utils/journey-graph.ts (golden-tested against the Pebbles seed). It:
- Loads nodes and edges via
useNodesanduseEdges - Manages expansion state for flows via local
useState - Computes per-platform view statuses and flow rollup gauges
- Maps domain nodes to React Flow nodes with position, type, card-variant preference, and toggle handlers
- Renders the
Canvascomponent with computed nodes and edges - Pushes a detail panel on node click, onto the stack owned by the project layout
- Opens
NewNodeForm(Dialog) via a floating "New node" button for creating nodes - Opens
InsertBetweenDialogfrom compose-edge insert actions for search-or-create insertion in flow playlists
Component Map
components/
graph/
Canvas.tsx # ReactFlow wrapper — registers node/edge types, renders Controls, MiniMap, Background; `readOnly` drops editing, chrome and wheel capture
JourneyCanvas.tsx # Journey graph → ELK → Canvas, props only (JourneyMap is the controller)
SystemCanvas.tsx # System graph → ELK → Canvas, props only (SystemMap is the controller)
nodes/ # Custom React Flow node components
FlowNode.tsx # Container card for flow nodes with rollup gauges
ViewNode.tsx # Variant-based View cards (compact/large), API actions, platform/API popovers
PlatformGaugeList.tsx # Shared stacked gauge renderer for flow cards and panels
DataModelNode.tsx # Parallel layer — amber, Database icon
ApiEndpointNode.tsx # Parallel layer — teal, Plug icon
node-styles.ts # Status/platform style maps
edges/
ComposeEdge.tsx # Straight — hierarchy (composes)
CrossLayerEdge.tsx # Dashed straight — registered in Canvas for calls, displays, queries
landing/ # Everything below the hero on `/` — the page is data, previews are real components
content.ts # PARTS and SECTIONS: chapter order, grouping and copy (plus `cards`/`links` for preview-less sections); SOURCE_CAPTION per seed source
fixtures.ts # Node/version ids each preview pins into its seed (tests/landing pins them)
quality-fixture.ts # The illustrative Kritik section behind the `quality` chapter (source `pilot-audit` = Pebbles + this)
LandingPage.tsx # Loops PARTS → SECTIONS; loads the seeds once, pre-slices per preview (lib/landing/prepare.ts)
LandingPart.tsx # Chapter: sticky column with kicker, title, intro, index; sections on the right
LandingSection.tsx # Title → why → PreviewFrame → what/how
PreviewFrame.tsx # App-chrome frame: breadcrumb, LIVE pill, fixed height, error boundary, caption
previews/
ids.ts # The preview catalogue (PreviewId, PreviewSource, PREVIEW_META)
registry.tsx # Record<PreviewId, component> — coverage is a type error, not an empty frame
*Preview.tsx # One per catalogue id; server components unless they need interaction (the canvases)
client/ReadOnly*.tsx # Thin client wrappers supplying the no-op handler a leaf requires
client/JourneyPreviewCanvas.tsx # The read-only journey both journey previews share (definition + expanded flows as props)
CodeBlock.tsx # The mono block the agent previews share (diff, validator run, tool call, prompt)
McpDiagram.tsx # Inline SVG in the canvas's node-card vocabulary: one projection/three readers, the write path
samples/ # Hand-written illustrative data (the agent-skill diff)
lib/landing/
seeds.ts # The seed sources: self-map, pebbles, pilot-audit
slice.ts / prepare.ts # Induced sub-bundle per preview, computed server side
generated/ # Real `arkaik validate` output and a real `list_nodes` call, from scripts/generate/generate-landing-samples.js
maps/
JourneyMap.tsx # The Journey map surface: expansion state, editing, dialogs, toolbar
SystemMap.tsx # The System map surface: species tiers, cross-layer edges, connect-to-create
MapCard.tsx # Maps-index card with kind badge + subgraph counts
MapEditorDialog.tsx # Create/edit custom maps (project.metadata.maps)
delivery/
DeliveryBoard.tsx # Status columns of (node × platform) items
PlatformItemCard.tsx # Slim node×platform card
DeliveryFilterBar.tsx # Platform/species chips, all-statuses toggle, search
overview/
OverviewSection.tsx # Shared dashboard card shell (title + jump-off link)
PlatformGaugesCard.tsx # Product-wide per-platform delivery gauges (PlatformGaugeList)
DeliverySnapshotCard.tsx # Board column totals without the board
ReleasePulseCard.tsx # Tagged releases, newest first, with change counts
BacklogCard.tsx # Open ideas/requests summary + first rows
InventoryCard.tsx # Census by species with status dots; rows link into the library
HealthCard.tsx # Doc-health indicators with per-indicator evidence links
MapsCard.tsx # Every map with live subgraph counts
layout/
CommandPalette.tsx # ⌘K overlay: ranked search over a catalogue (project or docs)
Minimap.tsx # React Flow minimap wrapper (unused — Canvas uses @xyflow/react MiniMap directly)
ProjectSidebar.tsx # Persistent in-project sidebar navigation
ProjectSwitcher.tsx # Sidebar header dropdown for cross-project navigation
StatusBadge.tsx # Colored pill with status label
library/
LibraryFilterBar.tsx # Species/search/display controls for the library page
NodeCard.tsx # Gallery-mode card for a single node
NodeTable.tsx # Directory-mode sortable table for nodes
generate/
PromptBuilderForm.tsx # Use-case aware form (pitch/plan/extend) and advanced options
PromptOutput.tsx # Prompt preview, token estimate, copy/download actions
panels/
NewNodeForm.tsx # Dialog form for creating a node with species-aware status/platform defaults
InsertBetweenDialog.tsx # Dialog for insert-between actions: choose view/flow, search existing, or create inline
PanelStack.tsx # Content-agnostic push-panel grid: the surface is cell 0; keyboard, focus, breadcrumb, window
ProjectPanels.tsx # Binds the stack to panel kind: node detail (id → node), or the raw bundle
NodeDetailPanel.tsx # One panel's body: edit node fields, platform-specific statuses, computed rollups, and flow playlists
PlaylistEditor.tsx # Flow-only playlist editor: add/remove/reorder and branch editing
PlaylistEntryRow.tsx # Recursive playlist row renderer for condition/junction branches
NodeSearchCombobox.tsx # Search-or-create selector for flow/view references
PlatformVariants.tsx # Platform tab switcher with per-platform status and notes
RawBundlePanel.tsx # Raw JSON/YAML bundle viewer/editor — a stack column (guarded edit + save-back)
AcceptanceMembershipField.tsx # An acceptance's Product picker, derived from what it covers (D5)
AcceptanceAuthoredFields.tsx # An acceptance's own fields in the intro block: gherkin, values, decompose
DecisionEditor.tsx # A decision's body: context, decision, consequences, decision status
acceptances/ # AcceptanceMatrix + its filter bar
decisions/ # DecisionLog
pyramid/ # Value-elements pyramid: tier groups, element cards/rows, toolbar
journal/ # describe-event.ts — one event's human sentence, shared by changelog/history
values/ # ValueBadge, ValuePicker (Bain value elements)
publik/ # PublishDialog, ImportSnapshotButton
sync/ # Synk provider, per-project control, restore dialog, onboarding banner
settings/ # Product manager, repo links, token manager
ui/ # shadcn/ui primitives (button, card, dialog, input, etc.)
dropdown-menu.tsx # Radix dropdown wrapper used by the project switcher
popover.tsx # Radix popover wrapper used by View card API/platform details
sidebar.tsx # shadcn sidebar primitives used by the project layout shell
Data Flow
IndexedDB (Dexie) | hosted graph API (app/api/graph) | in-memory seed
↕ (read/write)
localProvider / remoteProvider / seedProvider
↕ (routed by project id)
routingProvider (implements DataProvider, the default behind getProvider())
↕ (async calls — reads are conditional where the backend has a validator)
Query cache (TanStack Query; one client per browser)
· lib/data/project-queries.ts is its ONLY writer: keys, entry shapes,
reducers, write-backs, and the invalidateProject/invalidateProjects
seams the writers that bypass the hooks call (raw-bundle save,
import, archive, the projects page)
↕ (observers)
Hooks: useNodes, useEdges, useProject, useProjects, useJournal
↕ (state)
app/project/[id]/layout.tsx (sidebar shell + route-aware navigation)
↕ (props)
ProjectSidebar + ProjectSwitcher
↕ (route changes)
components/maps/JourneyMap.tsx (expansion state; buildJourneyGraph builds topology)
↕ (props)
Canvas → ReactFlow → Custom Nodes/Edges
↕ (click events)
NodeDetailPanel → Hook (updateNode) → Provider → Storage
NewNodeForm (Dialog) → Hook (addNode) → Provider → Storage
View card variant selector → Hook (useProject.updateProject) → Provider → Storage
Library route data flow:
Hooks: useNodes, useEdges
↕ (state)
app/project/[id]/library/page.tsx
↕ (props)
LibraryFilterBar + NodeCard/NodeTable
↕ (click events)
NodeDetailPanel / NewNodeForm → hooks → Provider → Storage
Project-shell navigation flow:
Hooks: useProject, useProjects
↕ (state)
app/project/[id]/layout.tsx
↕ (props)
ProjectSidebar / ProjectSwitcher
↕ (pathname + searchParams)
Route-aware active states + cross-project navigation
Docs route flow:
app/docs/layout.tsx
↕ (server-loaded nav items)
DocsSidebar
↕ (pathname)
Route-aware docs links
↕ (slug lookup)
app/docs/page.tsx + app/docs/[...slug]/page.tsx
↕
lib/utils/docs.ts (filesystem discovery + slug-safe lookup)
↕
components/docs/MarkdownContent.tsx (react-markdown + GFM + highlighting)
Docs pages are rendered from markdown at request/build time using server-side file reads. The home route (`/docs`) is pinned to repository `README.md`, while nested routes resolve to markdown under `docs/`. Unknown paths redirect back to `/docs`. The same index also feeds the docs ⌘K palette (`getDocsSearchPages()` → `components/docs/DocsSearch.tsx`), so the sidebar and the palette can never disagree about what exists.
Prompt generation flow:
app/generate/page.tsx
↕ (local state)
components/generate/PromptBuilderForm.tsx
↕ (typed config)
lib/prompts/assemble.ts
↕ (text blocks)
lib/prompts/blocks.ts + lib/prompts/types.ts
↕ (preview actions)
components/generate/PromptOutput.tsx
LLM affordance assets:
- `public/llms.txt` exposes a concise site + model manifest.
- `app/llms-full.txt/route.ts` serves a larger, plain-text context bundle for crawlers/agents.
- `public/schema/project-bundle.json` and `public/schema/example-bundle.json` define and demonstrate the import contract.
- `public/robots.txt` and `app/sitemap.ts` support discoverability.
All data mutations flow through the DataProvider interface (lib/data/data-provider.ts), reached through the getProvider()/setProvider() seam (lib/data/provider-registry.ts). The default is a routing provider that dispatches each call by project id across three implementations: localProvider over IndexedDB (Dexie — lib/data/db.ts, writing per project rather than rewriting the whole store), remoteProvider over the hosted graph API for prj_-prefixed projects, and seedProvider over per-tab memory for the built-in public self-map. No hook or component knows which one answered. Full routing rules, and the deliberate reversal of "the browser is the source of truth" for hosted projects, are in data-layer.md and spec/services.md.
Playlist Expansion
The project page manages one expansion set as local useState:
expandedFlows— which flows show their direct flow/view children
When project.root_node_id is present, the canvas walks the full compose closure from that node — views always render and chain the walk onward; flows render as collapsed cards. When it is missing, root nodes are inferred from nodes with no compose parent. The first top-level flow auto-expands on initial load.
Expanded flows reveal ordered children from metadata.playlist and composes edges. Positions are computed by ELK (lib/utils/elk-layout.ts, layered algorithm over compose edges).
Canvas visibility rule (Journey map):
- Rendered nodes:
flow,view - Not rendered here:
data-model,api-endpoint(still persisted; they render as standalone cards on the System map,/project/[id]/maps/system— spec/maps.md)
Card rendering is per map, set from the header's Display popover and resolved
by resolveMapDisplay (spec/maps.md § Display Options). Three
independent options, not a two-way preset:
images— the view's screenshot, falling back to its cover art (default on)flow_platforms— a flow card's delivery as the Pyramid'srings(default) or stackedbarsview_platforms— a view card's availability as footerchips(default) or labelledrows
Storage: project.metadata.map_display[mapId], plus a definition-level display
for agent-authored custom maps. The superseded project-wide
project.metadata.view_card_variant is no longer read — it still parses and
round-trips, but every map now starts from the defaults above.
Node Detail Panel
Clicking any node pushes a column onto the panel stack — an inline grid, no
overlay and no focus trap, in which the surface itself is the first cell. The
stack is owned by ProjectPanelsProvider in app/project/[id]/layout.tsx (a
page segment would remount on every param change and reset it), rendered by
PanelStack, and bound to what a panel can be — a node, or the raw bundle — by
ProjectPanels. Its rule, URL contract (?node=) and the split across the
three files are documented in conventions.md § Panel Stack;
the transitions themselves are pure, in lib/utils/panel-stack.ts.
NodeDetailPanel is one panel's body — the stack owns the frame, the chrome
and the keyboard. Historically only the five node-bearing surfaces (Journey
map, System map, library, delivery, acceptances) rendered the stack at all; the
intent, as the pages move onto PageShell, is that every project page mounts
it, since the raw bundle panel needs no node data to be worth reaching. Those
five pass ProjectPanels their own data and handlers and none keeps a
selected-node of its own; a page that passes none still gets the grid, and a
?node= it cannot resolve renders a body saying so rather than an empty
column. RawBundlePanel is the other thing a panel can be — a project-level
raw-JSON/YAML view that is a column like any other, so mounting is opening and
closing is the stack's. It needs no node data, which is the reason every project
page should mount the grid.
The body carries:
- Editable fields: title, description, and species-aware status/platform controls
- Connections: cross-layer nodes (data-model, api-endpoint); clicking one pushes it as the next panel
- Where Used: reverse reference list showing which flow playlists currently include the selected node
- Platform Variants (view only): per-platform status + notes stored in
node.metadata - Computed gauges (
flow): read-only per-platform rollups built from descendant views - Playlist editor (
flow): orderedmetadata.playlist.entriesediting with add/remove/reorder and recursive condition/junction branch editing
Flow playlist editing uses fuzzy search-or-create for view and flow entries. When adding a flow reference, cycle checks run before persisting and invalid inserts are blocked with toast feedback.
Compose edges in expanded sequences expose a single insert action. It opens InsertBetweenDialog, where users choose view, flow, condition, or junction. For view/flow, the dialog reuses NodeSearchCombobox to select existing nodes or create new ones inline; for condition/junction, it inserts structured entries with sensible defaults. Node-reference inserts are placed at the correct playlist position and ensure the compose edge exists.
Edits call useNodes.updateNode which flows through the DataProvider.
Library
The library route (app/project/[id]/library/page.tsx) is the project-wide browser for reusable nodes.
- Gallery view: card layout using
NodeCardfor scanning titles, species/status badges, and flow playlist previews. - Directory view: sortable table using
NodeTablefor dense auditing (id,title,species,status,used in). - Filter controls: species selection is owned by the sidebar (
?species=deep links);LibraryFilterBarowns text search and the gallery/directory display toggle.
Library interactions reuse the same edit/create surfaces as canvas (ProjectPanels, NewNodeForm) so data mutation paths stay identical.
Sidebar Navigation
Project-level navigation is defined in app/project/[id]/layout.tsx and rendered by ProjectSidebar + ProjectSwitcher.
- Sidebar links are route-aware across the whole shell (overview, pyramid, delivery, design, changelog, the maps group, library and its per-species
?species=rows, acceptances, decisions) and preserve active state from pathname/search params. - The switcher supports cross-project navigation while keeping users in the closest equivalent destination.
- Keeping navigation in the shared project layout avoids duplicated route chrome in child pages.
Command palette (⌘K)
The same destinations are reachable by typing. app/project/[id]/layout.tsx owns
the palette's open state (⌘K / Ctrl+K, plus the sidebar's Search row) and
dispatches the non-navigating commands; CommandPalette renders the overlay and
lib/utils/command-palette.ts holds the catalogue and the pure ranking rules
(prefix > word-start > substring > subsequence, synonyms one tier below labels).
- The catalogue mirrors the sidebar — a destination added there belongs here too,
and
tests/app/command-palette.test.jspins the pairing. - Enter validates the top suggestion, Tab fills in its wording, arrows browse.
- Publish lives in the layout rather than the sidebar, so both triggers open one dialog.
The palette is catalogue-agnostic: buildProjectCommands and buildDocsCommands
are two lists feeding one overlay, one ranker and one shortcut. A new surface
needs a builder, not a palette.
Docs palette (⌘K in /docs)
components/docs/DocsSearch.tsx mounts the same overlay in the docs header —
it owns the trigger button, the shortcut and the open state, because the docs
shell has no other command state to share. Its catalogue comes from
getDocsSearchPages() (lib/utils/docs.ts), the navigation tree flattened back
into landable pages, and crosses to the client as plain data.
- The page's own address is a synonym, hyphens and spaces both: "hosted projects"
finds
/docs/hosted-projectseven though its title reads Hosted Projects & the Agent Plane. - The folder trail rides along as the row's hint ("Spec"), so same-named pages in different sections stay tellable apart.
- Publish is a project action and is simply absent from the catalogue; the theme command is shared, so ⌘K switches it from either space.
Theming
next-themesfor light/dark modeThemeProviderwraps the entire app in the root layoutThemeTogglecomponent for user switching- Tailwind CSS with shadcn/ui design tokens
- Sidebar theme tokens live in
app/globals.cssand back the shadcn sidebar primitives
Source References
- Graph orchestration: components/maps/JourneyMap.tsx, lib/utils/journey-graph.ts, lib/utils/system-graph.ts
- Project shell: app/project/[id]/layout.tsx
- Panel stack: lib/utils/panel-stack.ts, components/panels/PanelStack.tsx, lib/hooks/useProjectPanels.tsx, components/panels/ProjectPanels.tsx
- Library orchestration: app/project/[id]/library/page.tsx
- Sidebar components: components/layout/ProjectSidebar.tsx, components/layout/ProjectSwitcher.tsx
- Command palette: components/layout/CommandPalette.tsx, lib/utils/command-palette.ts, components/docs/DocsSearch.tsx
- Docs shell + renderer: app/docs/layout.tsx, app/docs/page.tsx, app/docs/[...slug]/page.tsx, components/layout/DocsSidebar.tsx, components/docs/MarkdownContent.tsx, components/docs/DocsSearch.tsx, lib/utils/docs.ts
- Prompt builder: app/generate/page.tsx, components/generate/PromptBuilderForm.tsx, components/generate/PromptOutput.tsx, lib/prompts/assemble.ts, lib/prompts/blocks.ts, lib/prompts/types.ts
- React Flow registry: components/graph/Canvas.tsx
- Data hooks: lib/hooks/useNodes.ts, lib/hooks/useEdges.ts, lib/hooks/useProject.ts, lib/hooks/useProjects.ts
- Data providers: lib/data/data-provider.ts, lib/data/provider-registry.ts, lib/data/routing-provider.ts, lib/data/local-provider.ts, lib/data/remote-provider.ts, lib/data/seed-provider.ts
- LLM surfaces: public/llms.txt, app/llms-full.txt/route.ts, public/schema/project-bundle.json, public/schema/example-bundle.json, public/robots.txt, app/sitemap.ts