Command Palette
Search for a command to run...
The Decision Graph
Core ConceptThe graph is Arbtr's core differentiator. It shows decisions as nodes and their relationships as edges.
What is the Graph?
The Decision Graph is a visual representation of your team's architectural decisions and how they relate to each other.
- Nodes are decisions (e.g., "Use PostgreSQL")
- Edges are relationships (e.g., "Supabase depends on PostgreSQL")
- Domains are clusters (e.g., "Database decisions")
Unlike a wiki or document list, the graph shows structure. You can see what depends on what, what conflicts with what, and what happens if something changes.
Reading Nodes
Each node encodes information visually:
Status Colors
Active
Open for discussion
Under Discussion
Active debate
Approved
Concluded: Yes
Rejected
Concluded: No
Deferred
Decided not to decide
Archived
Hidden from view
Importance Badges
Nodes show badges indicating how "load-bearing" a decision is:
Company-defining
High-impact
Default weight
Experimental
Reading Edges
Edges are colored and styled based on relationship type:
depends_on
B requires A to be true
conflicts_with
A and B are mutually exclusive
supersedes
B replaces A
enables
A makes B possible
constrains
A limits options for B
derived_from
B is based on or related to A
Interactive Guide
The graph includes a built-in guide panel that shows all available controls. Click the book icon in the bottom-right corner of the graph, or press ? to toggle it.
Canvas Controls
Node Controls
Node positions are saved automatically to the database.
Edge Controls
Edges support two routing modes: smooth bezier curves (default) and orthogonal step routing (90° angles).
Routing
Editing Step Routes
Labels
Domain Controls
Drag decisions into domains to group them visually.
Keyboard Shortcuts
Selection
Graph Linting
Arbtr automatically detects structural problems in your decision graph:
Circular Dependencies
A depends on B depends on C depends on A. Something is wrong.
Conflict + Dependency
A conflicts with B, but C depends on both. Impossible state.
Time Travel Paradox
A supersedes B, but A was created before B. Check the timeline.
Technical Implementation
Rendering
The graph is rendered using React Flow with custom node and edge components. Layout positions are persisted per-user per-team to graph_layouts.
- Initial layout: Dagre algorithm for hierarchical positioning
- Manual overrides saved to database on drag end
- Domain boundaries calculated from child node positions
- Edge routing: Bezier (default) or orthogonal step paths
Linting Algorithm
Graph linting runs client-side on every state change. It detects:
// Circular dependency detection
// Uses DFS with visited set - O(V+E)
function detectCycles(graph: Graph): DecisionId[][] {
// Returns array of cycle paths
}
// Conflict + dependency paradox
// If A conflicts_with B, nothing should depend on both
function detectConflictDependency(graph: Graph): LintWarning[] {
// Returns warnings with affected decisions
}
// Time travel paradox
// supersedes edges should flow from newer → older decisions
function detectTimeTravel(graph: Graph): LintWarning[] {
// Compares created_at timestamps
}Performance
The graph renders interactively up to ~500 nodes. Beyond that, consider:
- Filtering by domain to reduce visible nodes
- Archiving old decisions to remove from graph
- Collapsing domains to reduce rendering complexity
Data Model
Complete interfaces for graph nodes, edges, and domains.
interface GraphNode {
id: string
title: string
slug: string
status: DecisionStatus
importanceLevel?: DecisionImportanceLevel
domainId?: string
tags: string[]
// Computed counts for visual indicators
dependencyCount: number // Outgoing depends_on edges
conflictCount: number // conflicts_with edges
relationshipCount: number // Total edges
// Scenario support (ghost nodes)
scenarioId?: string // NULL = live, UUID = ghost
isGhost: boolean
// Position (persisted per-user)
position?: { x: number; y: number }
}
interface GraphEdge {
id: string
source: string // Source decision ID
target: string // Target decision ID
type: RelationshipType
note?: string // Optional annotation
createdBy?: string // Who created this relationship
createdAt: Date
// Routing data (persisted)
routingMode: 'smooth' | 'step'
stepPath?: { x: number; y: number }[]
labelPosition?: number // 0-1 along path
}
type RelationshipType =
| "depends_on" // Target requires source
| "conflicts_with" // Mutually exclusive (symmetric)
| "supersedes" // Target replaces source
| "enables" // Source makes target possible
| "constrains" // Source limits target's options
| "derived_from" // Target is based on source
interface GraphDomain {
id: string
name: string
color: string
description?: string
// Computed bounds from child nodes
bounds?: { x: number; y: number; width: number; height: number }
isCollapsed: boolean
nodeCount: number
}decisions, decision_relationships,domains, graph_layoutsNext
Relationships