docs(02): complete data layer phase

This commit is contained in:
Lukas May
2026-01-30 19:14:06 +01:00
parent 71b132a61e
commit 115866ccc6
3 changed files with 167 additions and 111 deletions

View File

@@ -16,7 +16,7 @@ None
- [x] **Phase 1: Core Infrastructure** - CLI binary, server mode, process lifecycle, graceful shutdown
- [x] **Phase 1.1: Hexagonal Architecture** (INSERTED) - Tests, events, ports/adapters, tRPC
- [ ] **Phase 2: Data Layer** - SQLite database with task hierarchy schema
- [x] **Phase 2: Data Layer** - SQLite database with task hierarchy schema
- [ ] **Phase 3: Git Integration** - Worktree isolation per agent with proper lifecycle
- [ ] **Phase 4: Agent Lifecycle** - Spawn, stop, list agents with session persistence
- [ ] **Phase 5: Task Dispatch** - Task visibility, dependency dispatch, work queue
@@ -67,8 +67,8 @@ Plans:
**Plans**: 2 plans
Plans:
- [x] 02-01: SQLite Database Setup
- [ ] 02-02: Repository Layer
- [x] 02-01: SQLite Database Setup (Wave 1)
- [x] 02-02: Repository Layer (Wave 2)
### Phase 3: Git Integration
**Goal**: Git worktree management — create isolated worktrees per agent, preview diffs, integrate changes, cleanup
@@ -136,7 +136,7 @@ Phases execute in numeric order: 1 → 1.1 → 2 → 3 → 4 → 5 → 6 → 7
|-------|----------------|--------|-----------|
| 1. Core Infrastructure | 5/5 | Complete | 2026-01-30 |
| 1.1. Hexagonal Architecture | 6/6 | Complete | 2026-01-30 |
| 2. Data Layer | 1/2 | In progress | - |
| 2. Data Layer | 2/2 | Complete | 2026-01-30 |
| 3. Git Integration | 0/? | Not started | - |
| 4. Agent Lifecycle | 0/? | Not started | - |
| 5. Task Dispatch | 0/? | Not started | - |

View File

@@ -10,11 +10,11 @@ See: .planning/PROJECT.md (updated 2026-01-30)
## Current Position
Phase: 2 of 8 (Data Layer)
Plan: 2 of ? in current phase
Status: In progress
Last activity: 2026-01-30 — Completed 02-02-PLAN.md
Plan: 2 of 2 in current phase
Status: Phase complete
Last activity: 2026-01-30 — Completed Phase 2 (Data Layer)
Progress: ██████████████░░ 92%
Progress: ██████████████████ 100%
## Performance Metrics
@@ -29,7 +29,7 @@ Progress: ██████████████░░ 92%
|-------|-------|-------|----------|
| 1 | 5/5 | 15 min | 3 min |
| 1.1 | 6/6 | 15 min | 3 min |
| 2 | 2/? | 8 min | 4 min |
| 2 | 2/2 | 8 min | 4 min |
**Recent Trend:**
- Last 5 plans: 01.1-05 (2 min), 01.1-06 (4 min), 02-01 (3 min), 02-02 (5 min)
@@ -76,5 +76,5 @@ None yet.
## Session Continuity
Last session: 2026-01-30
Stopped at: Completed 02-02-PLAN.md (Repository Layer)
Stopped at: Completed Phase 2 (Data Layer) — all plans finished
Resume file: None

View File

@@ -5,18 +5,26 @@ type: execute
wave: 2
depends_on: ["02-01"]
files_modified:
- src/db/repository.ts
- src/db/drizzle-repository.ts
- src/db/repository.test.ts
- src/db/repositories/initiative-repository.ts
- src/db/repositories/phase-repository.ts
- src/db/repositories/plan-repository.ts
- src/db/repositories/task-repository.ts
- src/db/repositories/drizzle/initiative.ts
- src/db/repositories/drizzle/phase.ts
- src/db/repositories/drizzle/plan.ts
- src/db/repositories/drizzle/task.ts
- src/db/repositories/drizzle/index.ts
- src/db/repositories/index.ts
- src/db/index.ts
- src/db/repositories/drizzle/*.test.ts
autonomous: true
---
<objective>
Create repository layer following hexagonal architecture (port + adapter pattern).
Create repository layer following hexagonal architecture with separate repositories per aggregate.
Purpose: Provide clean data access abstraction that decouples business logic from Drizzle/SQLite implementation.
Output: TaskHierarchyRepository port interface and DrizzleTaskHierarchyRepository adapter with tests.
Purpose: Each aggregate (Initiative, Phase, Plan, Task) gets its own repository port + adapter. Clean boundaries, single responsibility, proper DDD.
Output: 4 repository interfaces, 4 Drizzle adapters, tests for each.
</objective>
<execution_context>
@@ -37,123 +45,170 @@ Output: TaskHierarchyRepository port interface and DrizzleTaskHierarchyRepositor
<tasks>
<task type="auto">
<name>Task 1: Create TaskHierarchyRepository port interface</name>
<files>src/db/repository.ts</files>
<name>Task 1: Create repository port interfaces</name>
<files>src/db/repositories/initiative-repository.ts, src/db/repositories/phase-repository.ts, src/db/repositories/plan-repository.ts, src/db/repositories/task-repository.ts, src/db/repositories/index.ts</files>
<action>
Create the repository PORT interface following hexagonal architecture patterns established in Phase 1.1.
Create separate repository PORT interfaces for each aggregate. Each repository only knows about its own aggregate.
Define TaskHierarchyRepository interface with:
src/db/repositories/initiative-repository.ts:
```typescript
export interface InitiativeRepository {
create(data: Omit<NewInitiative, 'id' | 'createdAt' | 'updatedAt'>): Promise<Initiative>;
findById(id: string): Promise<Initiative | null>;
findAll(): Promise<Initiative[]>;
update(id: string, data: Partial<...>): Promise<Initiative>;
delete(id: string): Promise<void>;
}
```
Initiative operations:
- createInitiative(data: NewInitiative): Promise&lt;Initiative&gt;
- getInitiative(id: string): Promise&lt;Initiative | null&gt;
- listInitiatives(): Promise&lt;Initiative[]&gt;
- updateInitiative(id: string, data: Partial&lt;NewInitiative&gt;): Promise&lt;Initiative&gt;
- deleteInitiative(id: string): Promise&lt;void&gt;
src/db/repositories/phase-repository.ts:
```typescript
export interface PhaseRepository {
create(data: Omit<NewPhase, 'id' | 'createdAt' | 'updatedAt'>): Promise<Phase>;
findById(id: string): Promise<Phase | null>;
findByInitiativeId(initiativeId: string): Promise<Phase[]>;
update(id: string, data: Partial<...>): Promise<Phase>;
delete(id: string): Promise<void>;
}
```
Phase operations:
- createPhase(data: NewPhase): Promise&lt;Phase&gt;
- getPhase(id: string): Promise&lt;Phase | null&gt;
- listPhasesByInitiative(initiativeId: string): Promise&lt;Phase[]&gt;
- updatePhase(id: string, data: Partial&lt;NewPhase&gt;): Promise&lt;Phase&gt;
- deletePhase(id: string): Promise&lt;void&gt;
src/db/repositories/plan-repository.ts:
```typescript
export interface PlanRepository {
create(data: Omit<NewPlan, 'id' | 'createdAt' | 'updatedAt'>): Promise<Plan>;
findById(id: string): Promise<Plan | null>;
findByPhaseId(phaseId: string): Promise<Plan[]>;
update(id: string, data: Partial<...>): Promise<Plan>;
delete(id: string): Promise<void>;
}
```
Plan operations:
- createPlan(data: NewPlan): Promise&lt;Plan&gt;
- getPlan(id: string): Promise&lt;Plan | null&gt;
- listPlansByPhase(phaseId: string): Promise&lt;Plan[]&gt;
- updatePlan(id: string, data: Partial&lt;NewPlan&gt;): Promise&lt;Plan&gt;
- deletePlan(id: string): Promise&lt;void&gt;
src/db/repositories/task-repository.ts:
```typescript
export interface TaskRepository {
create(data: Omit<NewTask, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
findById(id: string): Promise<Task | null>;
findByPlanId(planId: string): Promise<Task[]>;
update(id: string, data: Partial<...>): Promise<Task>;
delete(id: string): Promise<void>;
}
```
Task operations:
- createTask(data: NewTask): Promise&lt;Task&gt;
- getTask(id: string): Promise&lt;Task | null&gt;
- listTasksByPlan(planId: string): Promise&lt;Task[]&gt;
- updateTask(id: string, data: Partial&lt;NewTask&gt;): Promise&lt;Task&gt;
- deleteTask(id: string): Promise&lt;void&gt;
src/db/repositories/index.ts - re-export all interfaces.
Import types from schema.ts. Use Promise for all operations (even though better-sqlite3 is sync) to allow future async adapters.
Pattern: This is the PORT. Implementations are ADAPTERS. Just like EventBus in Phase 1.1.
Pattern: These are PORTS. Implementations are ADAPTERS. Same as EventBus in Phase 1.1.
Import types from ../schema.js. Use Promise for all operations.
</action>
<verify>npm run build succeeds - interface compiles correctly</verify>
<done>TaskHierarchyRepository interface exported with all CRUD operations</done>
<verify>npm run build succeeds - interfaces compile correctly</verify>
<done>4 separate repository interfaces exported, one per aggregate</done>
</task>
<task type="auto">
<name>Task 2: Create DrizzleTaskHierarchyRepository adapter</name>
<files>src/db/drizzle-repository.ts</files>
<name>Task 2: Create Drizzle repository adapters</name>
<files>src/db/repositories/drizzle/initiative.ts, src/db/repositories/drizzle/phase.ts, src/db/repositories/drizzle/plan.ts, src/db/repositories/drizzle/task.ts, src/db/repositories/drizzle/index.ts</files>
<action>
Implement the TaskHierarchyRepository interface using Drizzle ORM.
Implement each repository interface with a Drizzle adapter.
DrizzleTaskHierarchyRepository class:
- Constructor takes DrizzleDatabase instance (injected, not created internally)
- Implements all methods from TaskHierarchyRepository interface
- Uses nanoid for generating IDs (install nanoid if not present)
Each adapter class:
- Constructor takes DrizzleDatabase instance (dependency injection)
- Implements its repository interface
- Uses nanoid for generating IDs
- Sets createdAt/updatedAt timestamps automatically
- Orders results by appropriate fields (phases by number, tasks by order)
- Orders results appropriately (phases by number, tasks by order)
Implementation notes:
- Use Drizzle's eq() for where clauses
- Use returning() to get inserted/updated rows
- For updates, set updatedAt to current timestamp
- Throw if not found on update/delete (or silently succeed on delete - pick one, document it)
src/db/repositories/drizzle/initiative.ts:
- DrizzleInitiativeRepository implements InitiativeRepository
- CRUD operations on initiatives table only
Error handling:
- Throw descriptive errors for constraint violations
- Don't swallow SQLite errors - let them propagate
src/db/repositories/drizzle/phase.ts:
- DrizzlePhaseRepository implements PhaseRepository
- findByInitiativeId orders by number
- Foreign key to initiative enforced by SQLite
Export the class and re-export from src/db/index.ts
src/db/repositories/drizzle/plan.ts:
- DrizzlePlanRepository implements PlanRepository
- findByPhaseId orders by number
- Foreign key to phase enforced by SQLite
src/db/repositories/drizzle/task.ts:
- DrizzleTaskRepository implements TaskRepository
- findByPlanId orders by order field
- Foreign key to plan enforced by SQLite
src/db/repositories/drizzle/index.ts - re-export all adapters.
Install nanoid if not present.
Throw on update/delete if not found.
</action>
<verify>npm run build succeeds - no type errors in implementation</verify>
<done>DrizzleTaskHierarchyRepository implements all interface methods</done>
<verify>npm run build succeeds - no type errors in implementations</verify>
<done>4 Drizzle adapters implement their respective interfaces</done>
</task>
<task type="auto">
<name>Task 3: Write repository tests</name>
<files>src/db/repository.test.ts</files>
<files>src/db/repositories/drizzle/initiative.test.ts, src/db/repositories/drizzle/phase.test.ts, src/db/repositories/drizzle/plan.test.ts, src/db/repositories/drizzle/task.test.ts</files>
<action>
Write comprehensive tests for DrizzleTaskHierarchyRepository using Vitest.
Write tests for each Drizzle repository adapter using Vitest.
Test setup:
- Use in-memory database (':memory:') for isolation
- Create fresh database and apply schema before each test
- Use Drizzle's migrate or direct schema push for test setup
Test setup (shared helper or per-file):
- Use in-memory database (':memory:')
- Push schema before each test (drizzle-kit push or direct SQL)
- Fresh repository instance per test
Test cases:
initiative.test.ts:
- create generates id, sets timestamps
- findById returns null for non-existent
- findAll returns empty array initially
- update changes fields and updatedAt
- delete removes initiative (cascade tested separately)
Initiative tests:
- createInitiative creates with generated id
- getInitiative returns null for non-existent
- listInitiatives returns empty array initially
- updateInitiative updates fields and updatedAt
- deleteInitiative removes and cascades to children
phase.test.ts:
- create with valid initiativeId
- create with invalid initiativeId throws (FK constraint)
- findByInitiativeId returns only matching, ordered by number
- update/delete work correctly
Phase tests:
- createPhase with valid initiativeId
- createPhase with invalid initiativeId throws (foreign key)
- listPhasesByInitiative returns only matching phases
- deletePhase cascades to plans and tasks
plan.test.ts:
- create with valid phaseId
- create with invalid phaseId throws
- findByPhaseId returns only matching, ordered by number
- update/delete work correctly
Plan tests:
- createPlan with valid phaseId
- listPlansByPhase orders by number
- deletePlan cascades to tasks
task.test.ts:
- create with valid planId
- create with invalid planId throws
- findByPlanId returns only matching, ordered by order
- update status changes work
- delete works correctly
Task tests:
- createTask with valid planId
- listTasksByPlan orders by order field
- updateTask status changes work
Hierarchy tests:
- Full hierarchy creation (initiative -> phase -> plan -> task)
- Cascade delete removes entire subtree
- getTask on deleted plan's task returns null
Use describe/it pattern. Test real behavior, not implementation details.
Use describe/it pattern. Test behavior, not implementation.
</action>
<verify>npm test -- src/db/repository.test.ts passes all tests</verify>
<done>All repository operations have test coverage proving CRUD works correctly</done>
<verify>npm test -- src/db/repositories/drizzle passes all tests</verify>
<done>All repository adapters have test coverage</done>
</task>
<task type="auto">
<name>Task 4: Update exports and test cascade deletes</name>
<files>src/db/index.ts, src/db/repositories/drizzle/cascade.test.ts</files>
<action>
Update main db index to export repositories and add cascade delete test.
src/db/index.ts:
- Keep existing createDatabase export
- Add: export * from './repositories/index.js'
- Add: export * from './repositories/drizzle/index.js'
src/db/repositories/drizzle/cascade.test.ts:
- Test full hierarchy creation (initiative -> phase -> plan -> task)
- Delete initiative, verify all children removed
- Delete phase, verify plans and tasks removed
- Delete plan, verify tasks removed
- This tests SQLite foreign key cascade behavior through the adapters
These tests ensure the cascade delete configured in schema.ts works correctly when using the repository layer.
</action>
<verify>npm test -- src/db/repositories passes all tests including cascade</verify>
<done>Exports updated, cascade deletes proven working through repository layer</done>
</task>
</tasks>
@@ -161,17 +216,18 @@ Output: TaskHierarchyRepository port interface and DrizzleTaskHierarchyRepositor
<verification>
Before declaring plan complete:
- [ ] `npm run build` succeeds without errors
- [ ] `npm test -- src/db/repository.test.ts` passes all tests
- [ ] Repository interface is importable from src/db/index.ts
- [ ] Adapter is importable from src/db/index.ts
- [ ] Foreign key cascades work (delete initiative removes all children)
- [ ] `npm test -- src/db/repositories` passes all tests
- [ ] All 4 repository interfaces importable from src/db/index.ts
- [ ] All 4 adapters importable from src/db/index.ts
- [ ] Cascade deletes work through hierarchy
</verification>
<success_criteria>
- TaskHierarchyRepository port interface defines all CRUD operations
- DrizzleTaskHierarchyRepository adapter implements the interface
- Tests prove all operations work correctly
- Cascade deletes work through the hierarchy
- 4 separate repository port interfaces (one per aggregate)
- 4 Drizzle adapter implementations
- Each aggregate's repository only knows about its own table
- Tests prove CRUD operations work
- Cascade deletes verified
- No TypeScript errors
- All tests pass
</success_criteria>