Architecture

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:

  1. Loads nodes and edges via useNodes and useEdges
  2. Manages expansion state for flows via local useState
  3. Computes per-platform view statuses and flow rollup gauges
  4. Maps domain nodes to React Flow nodes with position, type, card-variant preference, and toggle handlers
  5. Renders the Canvas component with computed nodes and edges
  6. Pushes a detail panel on node click, onto the stack owned by the project layout
  7. Opens NewNodeForm (Dialog) via a floating "New node" button for creating nodes
  8. Opens InsertBetweenDialog from 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/systemspec/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's rings (default) or stacked bars
  • view_platforms — a view card's availability as footer chips (default) or labelled rows

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): ordered metadata.playlist.entries editing 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 NodeCard for scanning titles, species/status badges, and flow playlist previews.
  • Directory view: sortable table using NodeTable for dense auditing (id, title, species, status, used in).
  • Filter controls: species selection is owned by the sidebar (?species= deep links); LibraryFilterBar owns 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.js pins 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-projects even 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-themes for light/dark mode
  • ThemeProvider wraps the entire app in the root layout
  • ThemeToggle component for user switching
  • Tailwind CSS with shadcn/ui design tokens
  • Sidebar theme tokens live in app/globals.css and back the shadcn sidebar primitives

Source References