Systematically compared @alkdev/taskgraph, @alkdev/operations, and
@alkdev/flowgraph against storage/arch specs and fixed all mismatches.
Key changes:
Tasks (storage/tasks.md + ADR-011):
- Rename TaskFrontmatter → TaskInput to match library export
- Fix dependsOn (was depends_on) in field mappings — library uses
camelCase; parseFrontmatter normalizes YAML snake_case on input
- Document DependencyEdge shape {from, to, qualityRetention?} and
DB↔library field mapping
- Document graph node vs DB column distinction (TaskGraphNodeAttrs
is a subset of TaskInput)
- Fix default risk fallback from low → medium (matches resolveDefaults)
- Fix cross-project guard column references (dependentTaskId, not taskId)
- Clarify @alkdev/taskgraph TS is source of truth; frontmatter is for
LLM output parsing and legacy imports, not Rust CLI
- Add complete library exports reference
Operations (storage/spokes.md + operations.md):
- Add version, title, _meta columns to operations table (required by
OperationSpec, were missing)
- Fix type casing: query/mutation/subscription (lowercase, matching
OperationType runtime values)
- Make outputSchema and accessControl NOT NULL (matching library)
- Document ErrorDefinition shape {code, description, schema, httpStatus?}
- Document _meta vs commonCols.metadata distinction
- Add registerAll, get, getHandler, getByName, list, subscribe methods
- Fix buildCallHandler signature ({ registry, callMap })
- Fix OperationType values (lowercase)
Call graph (storage/call-graph.md + call-graph.md):
- Change operationId to NOT NULL with RESTRICT FK (was nullable/SET NULL)
— matches flowgraph's required CallNodeAttrs.operationId
- Document sentinel __removed__ operation strategy for deletions
- Document ISO 8601 string ↔ timestamptz conversion requirement
- Rewrite CallEventMap to match actual library: flat dot-notation keys,
timestamp on all events, nested error structure, optional output on
completed event
- Remove call.running event (doesn't exist in library) — hub calls
updateStatus(running) directly on dispatch
- Fix buildCallHandler({ registry, callMap }) signature
- Fix PendingRequestMap constructor (positional EventTarget)
- Add updateCall/removeCall/graph methods to API summary
- Document abort cascade as hub logic, not flowgraph logic
- Add open questions for operation deletion and reactive vs call graph
semantics
Table reference (storage/table-reference.md):
- Update call_graph_nodes.operationId cascade to RESTRICT
- Update operations.type comment to lowercase
- Update status enum reference
76 lines
5.3 KiB
Markdown
76 lines
5.3 KiB
Markdown
# ADR-011: Database as source of truth for tasks
|
|
|
|
- **Status**: Accepted
|
|
- **Date**: 2026-04-19
|
|
- **Deciders**: alkdev
|
|
- **Supersedes**: Previous "dual representation" design where files were source of truth for content and DB for state
|
|
|
|
## Context
|
|
|
|
The SDD process uses tasks as markdown files (compatible with the `taskgraph` CLI). The hub coordinator needs to query and mutate task state at runtime across multiple parallel worktrees. We need a storage model that serves both authoring and runtime coordination.
|
|
|
|
Taskgraph's file-based model works well for single-agent, single-worktree workflows. In the hub's multi-agent, multi-worktree environment, files create problems:
|
|
|
|
- **Parallel worktrees**: Agent A marks a task `in-progress` in their worktree's file. Agent B can't see this — the file lives in A's working directory. The coordinator can't get a consistent view.
|
|
- **Merge conflicts**: Two agents editing the same task file in different worktrees creates git conflicts on merge.
|
|
- **Reliable coordination**: The coordinator needs to query "which tasks are pending?" without scanning filesystems across worktrees.
|
|
- **Atomic mutations**: Status changes must be immediately visible to all agents, not delayed until file merges.
|
|
|
|
Three options were considered:
|
|
|
|
1. **Files only** — The coordinator runs `taskgraph` CLI commands via bash to query status. Agents edit files directly.
|
|
2. **Database only** — Tasks are stored exclusively in Postgres. No markdown files.
|
|
3. **Database as source of truth, files as authoring surface** — The DB is the authoritative runtime representation. Markdown files serve as the Decomposer's authoring format, ingested to DB via sync. Taskgraph CLI used for offline analysis via DB export.
|
|
|
|
## Decision
|
|
|
|
We choose **Option 3: Database as source of truth, files as authoring surface**.
|
|
|
|
### Authority Model
|
|
|
|
| Aspect | Authority | Why |
|
|
|--------|-----------|-----|
|
|
| All task fields (structure, categorical estimates, metadata) | **DB** | Every taskgraph frontmatter field maps to a dedicated DB column. Queryable, concurrent-safe, consistent. |
|
|
| Task specification (body) | **DB** (`body` column) | Stored as markdown text. Agents append notes during execution. |
|
|
| Task creation/authoring | **Files** → sync → DB | Decomposer edits markdown files; sync ingests them into DB. |
|
|
| Runtime status mutations | **DB** (hub operations) | `hub.task.*` operations ensure all agents see consistent state. |
|
|
| Offline graph analysis | **Files** (taskgraph CLI) | Export from DB when needed for `taskgraph risk-path` etc. |
|
|
|
|
### Key Design Principles
|
|
|
|
1. **Every taskgraph frontmatter field is a proper DB column** — no fields relegated to JSONB `metadata`. `priority`, `assignee`, `dueAt`, `tags` get dedicated columns because they're queryable and filterable in coordinator workflows. The library type is `TaskInput` (TypeScript equivalent of the Rust `TaskFrontmatter`).
|
|
|
|
2. **Categorical fields are nullable, not NOT NULL with defaults** — `scope`, `risk`, `impact`, `level` are nullable (NULL = not yet assessed). This preserves the distinction between "deliberately assessed as low" and "nobody filled this in." The library uses `Type.Optional(Nullable(Enum))` on `TaskInput`, matching this model. For analysis, `resolveDefaults()` uses `medium` as the default risk (replacing the old `low` default) and `narrow`/`isolated` for scope/impact — these are computational fallbacks, not DB defaults, making unassessed tasks more visible in analysis rather than appearing optimistically safe.
|
|
|
|
3. **No `parentId`** — Grouping is handled by `path` (a nullable text column for scoped queries like `WHERE path LIKE 'implementation/%'`). Dependencies are in `task_dependencies`. These are separate concepts.
|
|
|
|
4. **No `removedAt` soft delete** — When a task file is removed, the sync DELETEs the DB row. Git history preserves file-level history. No DB duplication needed.
|
|
|
|
5. **`fileCreatedAt`/`fileModifiedAt`** — Dedicated columns for frontmatter timestamps, separate from DB `createdAt`/`updatedAt` (row lifecycle times).
|
|
|
|
## Consequences
|
|
|
|
**Positive**:
|
|
- Coordinator gets a reliable, consistent view of all task state across parallel worktrees.
|
|
- No merge conflicts from agents editing the same file in different worktrees.
|
|
- Status changes are atomic and immediately visible to all agents via hub operations.
|
|
- All taskgraph fields are queryable with proper SQL types and indexes.
|
|
- Taskgraph CLI still works for offline analysis via DB → file export.
|
|
- Nullable categorical fields provide the "not yet assessed" signal that defaults hide.
|
|
|
|
**Negative**:
|
|
- Two representations exist (files and DB), requiring a sync operation.
|
|
- Files are no longer the source of truth — they're the authoring surface. This is a conceptual shift from taskgraph's default model.
|
|
- DB → file export is needed for offline analysis (not automatic).
|
|
|
|
**Mitigation for negatives**:
|
|
- Sync is idempotent and can be run at any time after authoring.
|
|
- The DB is the authority; files are just one input method. Tasks can also be created via hub API.
|
|
- Export for offline analysis is a manual step (run when needed), not a continuous sync.
|
|
|
|
## Related
|
|
|
|
- ADR-001: JSONB data columns vs individual columns (same principle — proper columns for queryable fields)
|
|
- Cost-benefit framework: taskgraph framework docs
|
|
- Task storage: `docs/architecture/storage/tasks.md`
|
|
- taskgraph TaskInput: taskgraph source |