15 KiB
Non-Linear: Data Model
Overview
The data model is organized as four layers stacked on a shared node topology — like layers in a GIS or Photoshop. Each layer answers a different question, has its own update cadence, and can be toggled independently in the UI and the agent API.
| Layer | Name | Question | Update Cadence |
|---|---|---|---|
| 1 | Structure | What is the project made of? | Stable — changes with major refactors |
| 2 | Work | What needs to be done? | High churn — changes daily |
| 3 | Code Connections | How do parts depend on each other technically? | Changes with the codebase |
| 4 | Artifacts | What context exists around this? | Updated as docs/designs evolve |
Two fundamental graph structures span the layers:
- Decomposition Tree — strict parent→child hierarchy (DAG). The spine of the product. Carries Layer 1 (component→component edges) and the attachment of Layer 2 nodes to Layer 1 nodes.
- Overlay Edges — lateral many-to-many connections with typed edges. Each edge type belongs to a specific layer (work coordination links in Layer 2, technical dependency links in Layer 3, artifact attachments in Layer 4).
The layers share the same spatial structure but carry different information. Layer 1 (Structure) is the base layer — all other layers attach to it.
Node Types
The graph has three node types: components, issues, and artifacts. Components form the skeleton (Layer 1); issues are work attached to that skeleton (Layer 2); artifacts are external context attached to any node (Layer 4). Alpha ships with components and issues only; artifacts are introduced post-alpha.
Component Node
A component represents a structural part of the system — a service, a module, a package, a directory. Components are relatively stable and map to code structure.
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
short_id |
string | Human-readable ID (NL-C12) |
type |
enum | Always component |
title |
string | Component name |
description |
text | What this component does (markdown), stored in Postgres |
labels |
string[] | Freeform tags (frontend, backend, infra) |
owner |
actor_id? | Team or person responsible |
parent_id |
node_id? | Parent in decomposition tree |
repo_link |
repo_ref? | Linked repository or directory (see Repo Integration) |
created_at |
timestamp | Creation time |
updated_at |
timestamp | Last modification |
Components can be parents of other components (nesting: service → module → submodule) or parents of issues.
Issue Node
An issue represents work to be done — a task, bug, feature, spike. Issues attach to components (or to the project root, or live in the inbox).
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
short_id |
string | Human-readable ID (NL-42) |
type |
enum | Always issue |
title |
string | Short descriptive title |
description |
text | Detailed description (markdown), stored in Postgres |
status |
enum | Current state (see Status below) |
labels |
string[] | Freeform tags for orthogonal concerns |
assignee |
actor_id? | User or agent assigned |
parent_id |
node_id? | Parent in decomposition tree (null = inbox item) |
created_at |
timestamp | Creation time |
updated_at |
timestamp | Last modification |
created_by |
actor_id | Creator (user or agent) |
cycle_id |
cycle_id? | Current cycle membership |
Artifact Node (Layer 4 — post-alpha)
An artifact represents external context — a document, design file, screenshot, or link — attached to any component or issue.
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
title |
string | Display name |
kind |
enum | link, file, embed |
url |
string? | External URL (Figma, Google Docs, Confluence, etc.) |
file_ref |
s3_key? | MinIO object key (for uploaded files) |
mime_type |
string? | MIME type (for uploaded files) |
size_bytes |
int? | File size (for uploaded files) |
attached_to |
node_id | Component or issue this artifact belongs to |
created_by |
actor_id | Uploader / linker |
created_at |
timestamp | Creation time |
Artifacts do not participate in the decomposition tree — they have no parent/child relationships of their own. They attach to components or issues via a dedicated HAS_ARTIFACT edge and are purely a Layer 4 overlay.
How They Relate
Project (root) Layer 1: Structure
├── Component: auth-service (→ repo) ─────────────────
│ ├── Component: oauth-module (→ dir)
│ │ ├── Issue: implement refresh tokens Layer 2: Work
│ │ └── Issue: fix token expiry bug ────────────────
│ │ └── Artifact: "error-screenshot.png" Layer 4: Artifacts
│ └── Component: session-manager (→ dir) ─────────────────
│ └── Issue: add Redis session store
├── Component: frontend-app (→ repo)
│ ├── Component: dashboard (→ dir)
│ │ └── Artifact: "dashboard-figma-mockup" (link) Layer 4
│ └── Component: auth-ui (→ dir)
│ └── Issue: redesign login page
│
│ [Layer 3 — Code Connections (not shown in tree, these are lateral edges):]
│ auth-service ──IMPORTS──► jwt-lib
│ frontend-app ──CALLS_API──► auth-service
Components are the skeleton (Layer 1) — stable structure that mirrors your codebase. Issues are the work (Layer 2) — they flow through statuses, get assigned, join cycles. Artifacts are the context (Layer 4) — docs, designs, and media that surround the work. Code connections (Layer 3) are lateral edges between components representing technical dependencies. This separation is cleaner than depth-as-type because a component at depth 3 is still a component, not a "task."
Hierarchy Rules
- Components can be children of: project root, other components
- Issues can be children of: project root, components, other issues (sub-tasks)
- Issues cannot be parents of components
- The tree remains a strict DAG — no cycles, single parent per node
Status (Issues Only)
Default statuses (customizable per project):
backlog— not yet plannedtodo— planned, not startedin_progress— actively being worked onin_review— awaiting reviewdone— completedcancelled— abandoned
Components don't have status — their "health" is derived from their children's statuses (e.g., "3/7 issues done, 1 blocked").
Status Transitions (v0.1 Default)
v0.1 ships with a permissive default model — any status can transition to any other status. This avoids blocking agents or humans with rigid workflows before real usage patterns emerge.
backlog ⇄ todo ⇄ in_progress ⇄ in_review ⇄ done
↕ ↕ ↕ ↕ ↕
cancelled
All transitions are bidirectional. Reopening (done → in_progress) and skipping stages (backlog → done) are allowed. The system logs every transition with actor + timestamp for auditability.
v0.2: Custom Transition Graphs. Projects will be able to define a custom state machine that restricts allowed transitions (e.g., "only owners can reopen done issues," "in_review can only move to done or in_progress"). The default permissive model remains available as a preset.
Labels
Labels handle all classification orthogonal to hierarchy and type:
- Kind (issues):
bug,feature,chore,spike - Area:
frontend,backend,infra,docs - Priority:
p0,p1,p2,p3 - Custom: anything the team defines
Repository Integration
Repo Link
A component can be linked to a code source:
| Field | Type | Description |
|---|---|---|
provider |
enum | github, gitlab, bitbucket |
repo_url |
string | Repository URL |
path |
string? | Subdirectory within repo (null = repo root) |
branch |
string? | Default branch to track (null = repo default) |
Multi-Repo Support
A project can span multiple repositories. Each repo (or subdirectory) maps to a component node:
- Monorepo: one repo → multiple component nodes (one per package/service)
- Multi-repo microservices: each repo → one top-level component node
- Mixed: some repos map to a single component, others are split
Skeleton Inference (Fast-Start)
When connecting repositories, the system proposes a component tree:
- Connect: OAuth via Authentik to GitHub/GitLab
- Select repos: pick which repositories belong to this project
- Scan: system reads repo structure — directories, package manifests (
package.json,pyproject.toml,go.mod,Cargo.toml), major module boundaries - Propose: AI suggests a component tree: "I see 3 services with these modules — here's a proposed skeleton"
- Adjust: user reviews, merges, splits, renames, rearranges
- Create: skeleton is committed — user starts attaching issues
Code ↔ Component Mapping
Because components are linked to repos/directories:
- Commits touching files in a component's path are automatically associated with that component
- PRs surfaced in the right component's activity feed without manual
NL-42tagging (explicit tagging still works for issue-level linking) - New files/directories can trigger suggestions: "A new directory
/src/notificationsappeared — add as a component?" - Agent context: agent scoped to a component subtree can reference the actual code structure
Decomposition Tree
The decomposition tree is a strict hierarchy:
- Single parent: Every node has exactly one parent (except roots)
- Directed: Edges flow from abstract (parent) to concrete (child)
- Acyclic: No node can be its own ancestor
- Single root per project: Each project has one root node (v0.1)
- Typed: Components form the skeleton, issues attach to it
Reparenting
Moving a node to a different parent is a structural change (plan-then-apply):
- Permission boundaries may shift
- For components: repo link association persists, but position in tree changes
- For issues: moving between components changes which codebase the work is conceptually "in"
- System warns about consequences before committing
Overlay Edges (Lateral Links)
Lateral links are typed edges that do not belong to the decomposition tree. Each link type belongs to a specific layer.
Layer 2: Work Coordination Links
These connect issues to other issues and describe work-level relationships.
| Type | Semantics | Directionality | Between |
|---|---|---|---|
blocks |
A blocks B | Directed | Issue → Issue |
blocked_by |
Inverse of blocks | Directed (auto-created) | Issue → Issue |
duplicates |
A is a duplicate of B | Directed | Issue → Issue |
relates_to |
General work association | Undirected | Issue ↔ Issue |
Layer 3: Code Connection Links (post-alpha)
These connect components to other components and describe technical dependencies. They can be auto-inferred from code analysis (import graphs, API routes, shared infrastructure) or declared manually. Layer 3 is introduced post-alpha once the component skeleton is stable and teams express a need for dependency visibility.
| Type | Semantics | Directionality | Between |
|---|---|---|---|
depends_on |
A depends on B at build/runtime | Directed | Component → Component |
imports |
A imports code from B | Directed | Component → Component |
calls_api |
A calls B's API at runtime | Directed | Component → Component |
shares_db |
A and B share a database | Undirected | Component ↔ Component |
Auto-inference sources for Layer 3 links:
- Import graphs: static analysis of
import,require,usestatements across repo boundaries - API route matching: outbound HTTP calls matched to known service endpoints
- Shared infrastructure: components linked to the same database, queue, or cache
Auto-inferred links are tagged source: inferred and can be overridden or dismissed manually.
Layer 4: Artifact Attachment Links (post-alpha)
These connect artifacts to the nodes they provide context for. Represented by the HAS_ARTIFACT edge. Introduced alongside Artifact nodes post-alpha.
Backlinks (Inbound Link Surfacing)
Every node surfaces inbound lateral links from all layers: "3 tasks blocked by this" (Layer 2), "2 components depend on this" (Layer 3), "1 design attached" (Layer 4). Backlink counts are shown per-layer when layer toggles are active.
Cross-Project Links (v0.2+)
Deferred. Will enable cross-project/cross-repo dependency tracking across all layers.
Triage Inbox
Issues with parent_id = null live in the rootless inbox. With repo integration, triage becomes smarter — the system (or an agent) can suggest which component an inbox issue belongs to based on keywords, file paths, or related commits.
Cycles
A cycle is a named set of issue references with a date range. Components don't join cycles — only issues do.
Change History
Every mutation is a stored event with actor + timestamp. For components, this includes repo link changes and skeleton inference events.
Workspace
A workspace is the top-level organizational container. It groups projects, users, and agents under a single identity boundary.
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
name |
string | Workspace name |
slug |
string | URL-safe identifier (my-team) |
projects |
project_id[] | Projects in this workspace |
members |
actor_id[] | Users and agents with workspace access |
created_at |
timestamp | Creation time |
- Users and agents belong to workspaces. A user can belong to multiple workspaces.
- Authentik maps to workspace-level identity — one Authentik group per workspace.
- Policies are per-project, but membership is per-workspace. A workspace member can be granted access to specific projects via project-level roles.
- Billing (if ever introduced) attaches to the workspace entity.
- v0.1 supports a single workspace per deployment. The entity exists from day one to avoid a painful migration later.
Project
| Field | Type | Description |
|---|---|---|
id |
UUID | Unique identifier |
name |
string | Project name |
workspace_id |
workspace_id | Parent workspace |
root_id |
node_id | Root node of decomposition tree |
repos |
repo_ref[] | Connected repositories |
created_at |
timestamp | Creation time |
members |
actor_id[] | Users and agents with project-level access |