docs(02): complete data layer phase
This commit is contained in:
@@ -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 | - |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Initiative>
|
||||
- getInitiative(id: string): Promise<Initiative | null>
|
||||
- listInitiatives(): Promise<Initiative[]>
|
||||
- updateInitiative(id: string, data: Partial<NewInitiative>): Promise<Initiative>
|
||||
- deleteInitiative(id: string): Promise<void>
|
||||
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<Phase>
|
||||
- getPhase(id: string): Promise<Phase | null>
|
||||
- listPhasesByInitiative(initiativeId: string): Promise<Phase[]>
|
||||
- updatePhase(id: string, data: Partial<NewPhase>): Promise<Phase>
|
||||
- deletePhase(id: string): Promise<void>
|
||||
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<Plan>
|
||||
- getPlan(id: string): Promise<Plan | null>
|
||||
- listPlansByPhase(phaseId: string): Promise<Plan[]>
|
||||
- updatePlan(id: string, data: Partial<NewPlan>): Promise<Plan>
|
||||
- deletePlan(id: string): Promise<void>
|
||||
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<Task>
|
||||
- getTask(id: string): Promise<Task | null>
|
||||
- listTasksByPlan(planId: string): Promise<Task[]>
|
||||
- updateTask(id: string, data: Partial<NewTask>): Promise<Task>
|
||||
- deleteTask(id: string): Promise<void>
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user