10 KiB
Non-Linear: Data Model
Overview
The data model consists of two overlaid graphs on a set of typed nodes:
- Decomposition Tree — strict parent→child hierarchy (DAG)
- Association Graph — lateral many-to-many connections with typed edges
These are deliberately separated. The decomposition tree is the spine of the product. The association graph is an overlay that adds context without defining structure.
Node Types
The graph has two main node types: components and issues. Components form the skeleton; issues are work attached to that skeleton.
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 |
How They Relate
Project (root)
├── Component: auth-service (→ repo: github.com/team/auth)
│ ├── Component: oauth-module (→ directory: /src/oauth)
│ │ ├── Issue: implement refresh tokens
│ │ └── Issue: fix token expiry bug
│ └── Component: session-manager (→ directory: /src/sessions)
│ └── Issue: add Redis session store
├── Component: frontend-app (→ repo: github.com/team/web)
│ ├── Component: dashboard (→ directory: /src/views/dashboard)
│ └── Component: auth-ui (→ directory: /src/views/auth)
│ └── Issue: redesign login page
Components are the skeleton — stable structure that mirrors your codebase. Issues are the work — they flow through statuses, get assigned, join cycles. 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
Association Graph (Lateral Links)
Link Types
| Type | Semantics | Directionality |
|---|---|---|
blocks |
A blocks B | Directed |
blocked_by |
Inverse of blocks | Directed (auto-created) |
relates_to |
General association | Undirected |
duplicates |
A is a duplicate of B | Directed |
depends_on |
Component A depends on component B | Directed |
depends_on is specifically for inter-component dependencies — can be inferred from code (import graphs, API calls) or declared manually.
Backlinks (Inbound Link Surfacing)
Every node surfaces inbound lateral links: "3 tasks blocked by this," "2 components depend on this."
Cross-Project Links (v0.2+)
Deferred. Will enable cross-project/cross-repo dependency tracking.
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 |