@proletariat/cli 0.3.36 → 0.3.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/branch/where.js +6 -17
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/execution/config.js +4 -14
- package/dist/commands/execution/logs.js +6 -0
- package/dist/commands/execution/view.js +8 -0
- package/dist/commands/init.js +4 -8
- package/dist/commands/mcp-server.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/health.js +4 -4
- package/dist/commands/session/list.js +1 -19
- package/dist/commands/session/peek.js +6 -6
- package/dist/commands/session/poke.js +2 -2
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +17 -9
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workspace/prune.js +3 -3
- package/dist/hooks/init.js +21 -10
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/runners.d.ts +34 -0
- package/dist/lib/execution/runners.js +134 -7
- package/dist/lib/execution/session-utils.d.ts +5 -0
- package/dist/lib/execution/session-utils.js +45 -3
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +1 -1
- package/dist/lib/execution/storage.js +17 -2
- package/dist/lib/execution/types.d.ts +1 -0
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +52 -0
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/base.js +207 -0
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-json.d.ts +5 -0
- package/dist/lib/prompt-json.js +9 -0
- package/oclif.manifest.json +3293 -3190
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spec operations for PMO.
|
|
3
|
+
*
|
|
4
|
+
* This module uses Drizzle ORM for type-safe database queries.
|
|
3
5
|
*/
|
|
4
|
-
import {
|
|
6
|
+
import { eq, and, like, or, asc, sql } from 'drizzle-orm';
|
|
7
|
+
import { pmoSpecs, pmoProjects, pmoTickets, pmoWorkflowStatuses, pmoProjectSpecs, pmoSpecDependencies, } from '../../database/drizzle-schema.js';
|
|
5
8
|
import { PMOError } from '../types.js';
|
|
6
9
|
import { generateEntityId } from '../utils.js';
|
|
7
10
|
import { rowToSpec, rowToTicket, wrapSqliteError } from './helpers.js';
|
|
8
|
-
const T = PMO_TABLES;
|
|
9
11
|
export class SpecStorage {
|
|
10
12
|
ctx;
|
|
11
13
|
constructor(ctx) {
|
|
@@ -18,16 +20,25 @@ export class SpecStorage {
|
|
|
18
20
|
const id = spec.id || generateEntityId(this.ctx.db, 'spec');
|
|
19
21
|
const now = Date.now();
|
|
20
22
|
try {
|
|
21
|
-
this.ctx.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
this.ctx.drizzle.insert(pmoSpecs).values({
|
|
24
|
+
id,
|
|
25
|
+
title: spec.title || 'Untitled Spec',
|
|
26
|
+
status: spec.status || 'draft',
|
|
27
|
+
type: spec.type || null,
|
|
28
|
+
tags: spec.tags ? JSON.stringify(spec.tags) : null,
|
|
29
|
+
problem: spec.problem || null,
|
|
30
|
+
solution: spec.solution || null,
|
|
31
|
+
decisions: spec.decisions || null,
|
|
32
|
+
notNow: spec.notNow || null,
|
|
33
|
+
uiUx: spec.uiUx || null,
|
|
34
|
+
acceptanceCriteria: spec.acceptanceCriteria || null,
|
|
35
|
+
openQuestions: spec.openQuestions || null,
|
|
36
|
+
requirementsFunctional: spec.requirementsFunctional || null,
|
|
37
|
+
requirementsTechnical: spec.requirementsTechnical || null,
|
|
38
|
+
context: spec.context || null,
|
|
39
|
+
createdAt: String(now),
|
|
40
|
+
updatedAt: String(now),
|
|
41
|
+
}).run();
|
|
31
42
|
}
|
|
32
43
|
catch (err) {
|
|
33
44
|
wrapSqliteError('Spec', 'create', err);
|
|
@@ -58,14 +69,29 @@ export class SpecStorage {
|
|
|
58
69
|
* Get a spec by ID.
|
|
59
70
|
*/
|
|
60
71
|
async getSpec(id) {
|
|
61
|
-
const row = this.ctx.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
const row = this.ctx.drizzle
|
|
73
|
+
.select({
|
|
74
|
+
id: pmoSpecs.id,
|
|
75
|
+
title: pmoSpecs.title,
|
|
76
|
+
status: pmoSpecs.status,
|
|
77
|
+
type: pmoSpecs.type,
|
|
78
|
+
tags: pmoSpecs.tags,
|
|
79
|
+
problem: pmoSpecs.problem,
|
|
80
|
+
solution: pmoSpecs.solution,
|
|
81
|
+
decisions: pmoSpecs.decisions,
|
|
82
|
+
not_now: pmoSpecs.notNow,
|
|
83
|
+
ui_ux: pmoSpecs.uiUx,
|
|
84
|
+
acceptance_criteria: pmoSpecs.acceptanceCriteria,
|
|
85
|
+
open_questions: pmoSpecs.openQuestions,
|
|
86
|
+
requirements_functional: pmoSpecs.requirementsFunctional,
|
|
87
|
+
requirements_technical: pmoSpecs.requirementsTechnical,
|
|
88
|
+
context: pmoSpecs.context,
|
|
89
|
+
created_at: pmoSpecs.createdAt,
|
|
90
|
+
updated_at: pmoSpecs.updatedAt,
|
|
91
|
+
})
|
|
92
|
+
.from(pmoSpecs)
|
|
93
|
+
.where(eq(pmoSpecs.id, id))
|
|
94
|
+
.get();
|
|
69
95
|
if (!row)
|
|
70
96
|
return null;
|
|
71
97
|
return rowToSpec(row);
|
|
@@ -74,30 +100,45 @@ export class SpecStorage {
|
|
|
74
100
|
* List specs with optional filters.
|
|
75
101
|
*/
|
|
76
102
|
async listSpecs(filter) {
|
|
77
|
-
let query =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
let query = this.ctx.drizzle
|
|
104
|
+
.select({
|
|
105
|
+
id: pmoSpecs.id,
|
|
106
|
+
title: pmoSpecs.title,
|
|
107
|
+
status: pmoSpecs.status,
|
|
108
|
+
type: pmoSpecs.type,
|
|
109
|
+
tags: pmoSpecs.tags,
|
|
110
|
+
problem: pmoSpecs.problem,
|
|
111
|
+
solution: pmoSpecs.solution,
|
|
112
|
+
decisions: pmoSpecs.decisions,
|
|
113
|
+
not_now: pmoSpecs.notNow,
|
|
114
|
+
ui_ux: pmoSpecs.uiUx,
|
|
115
|
+
acceptance_criteria: pmoSpecs.acceptanceCriteria,
|
|
116
|
+
open_questions: pmoSpecs.openQuestions,
|
|
117
|
+
requirements_functional: pmoSpecs.requirementsFunctional,
|
|
118
|
+
requirements_technical: pmoSpecs.requirementsTechnical,
|
|
119
|
+
context: pmoSpecs.context,
|
|
120
|
+
created_at: pmoSpecs.createdAt,
|
|
121
|
+
updated_at: pmoSpecs.updatedAt,
|
|
122
|
+
})
|
|
123
|
+
.from(pmoSpecs)
|
|
124
|
+
.$dynamic();
|
|
125
|
+
const conditions = [];
|
|
86
126
|
if (filter?.status) {
|
|
87
|
-
|
|
88
|
-
params.push(filter.status);
|
|
127
|
+
conditions.push(eq(pmoSpecs.status, filter.status));
|
|
89
128
|
}
|
|
90
129
|
if (filter?.type) {
|
|
91
|
-
|
|
92
|
-
params.push(filter.type);
|
|
130
|
+
conditions.push(eq(pmoSpecs.type, filter.type));
|
|
93
131
|
}
|
|
94
132
|
if (filter?.search) {
|
|
95
|
-
|
|
96
|
-
params.push(`%${filter.search}%`, `%${filter.search}%`, `%${filter.search}%`, `%${filter.search}%`);
|
|
133
|
+
conditions.push(or(like(pmoSpecs.id, `%${filter.search}%`), like(pmoSpecs.title, `%${filter.search}%`), like(pmoSpecs.problem, `%${filter.search}%`), like(pmoSpecs.solution, `%${filter.search}%`)));
|
|
97
134
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
if (conditions.length > 0) {
|
|
136
|
+
query = query.where(and(...conditions));
|
|
137
|
+
}
|
|
138
|
+
const rows = query
|
|
139
|
+
.orderBy(asc(pmoSpecs.title))
|
|
140
|
+
.all();
|
|
141
|
+
return rows.map((row) => rowToSpec(row));
|
|
101
142
|
}
|
|
102
143
|
/**
|
|
103
144
|
* Update a spec.
|
|
@@ -107,69 +148,42 @@ export class SpecStorage {
|
|
|
107
148
|
if (!existing) {
|
|
108
149
|
throw new PMOError('NOT_FOUND', `Spec not found: ${id}`);
|
|
109
150
|
}
|
|
110
|
-
const updates =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
params.push(changes.uiUx);
|
|
147
|
-
}
|
|
148
|
-
if (changes.acceptanceCriteria !== undefined) {
|
|
149
|
-
updates.push('acceptance_criteria = ?');
|
|
150
|
-
params.push(changes.acceptanceCriteria);
|
|
151
|
-
}
|
|
152
|
-
if (changes.openQuestions !== undefined) {
|
|
153
|
-
updates.push('open_questions = ?');
|
|
154
|
-
params.push(changes.openQuestions);
|
|
155
|
-
}
|
|
156
|
-
if (changes.requirementsFunctional !== undefined) {
|
|
157
|
-
updates.push('requirements_functional = ?');
|
|
158
|
-
params.push(changes.requirementsFunctional);
|
|
159
|
-
}
|
|
160
|
-
if (changes.requirementsTechnical !== undefined) {
|
|
161
|
-
updates.push('requirements_technical = ?');
|
|
162
|
-
params.push(changes.requirementsTechnical);
|
|
163
|
-
}
|
|
164
|
-
if (changes.context !== undefined) {
|
|
165
|
-
updates.push('context = ?');
|
|
166
|
-
params.push(changes.context);
|
|
167
|
-
}
|
|
168
|
-
if (updates.length > 0) {
|
|
169
|
-
updates.push('updated_at = ?');
|
|
170
|
-
params.push(Date.now());
|
|
171
|
-
params.push(id);
|
|
172
|
-
this.ctx.db.prepare(`UPDATE ${T.specs} SET ${updates.join(', ')} WHERE id = ?`).run(...params);
|
|
151
|
+
const updates = {};
|
|
152
|
+
if (changes.title !== undefined)
|
|
153
|
+
updates.title = changes.title;
|
|
154
|
+
if (changes.status !== undefined)
|
|
155
|
+
updates.status = changes.status;
|
|
156
|
+
if (changes.type !== undefined)
|
|
157
|
+
updates.type = changes.type;
|
|
158
|
+
if (changes.tags !== undefined)
|
|
159
|
+
updates.tags = JSON.stringify(changes.tags);
|
|
160
|
+
if (changes.problem !== undefined)
|
|
161
|
+
updates.problem = changes.problem;
|
|
162
|
+
if (changes.solution !== undefined)
|
|
163
|
+
updates.solution = changes.solution;
|
|
164
|
+
if (changes.decisions !== undefined)
|
|
165
|
+
updates.decisions = changes.decisions;
|
|
166
|
+
if (changes.notNow !== undefined)
|
|
167
|
+
updates.notNow = changes.notNow;
|
|
168
|
+
if (changes.uiUx !== undefined)
|
|
169
|
+
updates.uiUx = changes.uiUx;
|
|
170
|
+
if (changes.acceptanceCriteria !== undefined)
|
|
171
|
+
updates.acceptanceCriteria = changes.acceptanceCriteria;
|
|
172
|
+
if (changes.openQuestions !== undefined)
|
|
173
|
+
updates.openQuestions = changes.openQuestions;
|
|
174
|
+
if (changes.requirementsFunctional !== undefined)
|
|
175
|
+
updates.requirementsFunctional = changes.requirementsFunctional;
|
|
176
|
+
if (changes.requirementsTechnical !== undefined)
|
|
177
|
+
updates.requirementsTechnical = changes.requirementsTechnical;
|
|
178
|
+
if (changes.context !== undefined)
|
|
179
|
+
updates.context = changes.context;
|
|
180
|
+
if (Object.keys(updates).length > 0) {
|
|
181
|
+
updates.updatedAt = String(Date.now());
|
|
182
|
+
this.ctx.drizzle
|
|
183
|
+
.update(pmoSpecs)
|
|
184
|
+
.set(updates)
|
|
185
|
+
.where(eq(pmoSpecs.id, id))
|
|
186
|
+
.run();
|
|
173
187
|
}
|
|
174
188
|
return (await this.getSpec(id));
|
|
175
189
|
}
|
|
@@ -182,8 +196,12 @@ export class SpecStorage {
|
|
|
182
196
|
throw new PMOError('NOT_FOUND', `Spec not found: ${id}`);
|
|
183
197
|
}
|
|
184
198
|
try {
|
|
185
|
-
this.ctx.
|
|
186
|
-
this.ctx.
|
|
199
|
+
this.ctx.drizzle.delete(pmoSpecs).where(eq(pmoSpecs.id, id)).run();
|
|
200
|
+
this.ctx.drizzle
|
|
201
|
+
.update(pmoTickets)
|
|
202
|
+
.set({ specId: null })
|
|
203
|
+
.where(eq(pmoTickets.specId, id))
|
|
204
|
+
.run();
|
|
187
205
|
}
|
|
188
206
|
catch (err) {
|
|
189
207
|
wrapSqliteError('Spec', 'delete', err);
|
|
@@ -194,9 +212,11 @@ export class SpecStorage {
|
|
|
194
212
|
*/
|
|
195
213
|
async linkTicketToSpec(ticketId, specId) {
|
|
196
214
|
// Verify ticket exists and get its project
|
|
197
|
-
const ticket = this.ctx.
|
|
198
|
-
|
|
199
|
-
|
|
215
|
+
const ticket = this.ctx.drizzle
|
|
216
|
+
.select({ id: pmoTickets.id, projectId: pmoTickets.projectId })
|
|
217
|
+
.from(pmoTickets)
|
|
218
|
+
.where(eq(pmoTickets.id, ticketId))
|
|
219
|
+
.get();
|
|
200
220
|
if (!ticket) {
|
|
201
221
|
throw new PMOError('NOT_FOUND', `Ticket not found: ${ticketId}`, ticketId);
|
|
202
222
|
}
|
|
@@ -205,57 +225,80 @@ export class SpecStorage {
|
|
|
205
225
|
if (!spec) {
|
|
206
226
|
throw new PMOError('NOT_FOUND', `Spec not found: ${specId}`);
|
|
207
227
|
}
|
|
208
|
-
this.ctx.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.ctx.updateBoardTimestamp(ticket.
|
|
228
|
+
this.ctx.drizzle
|
|
229
|
+
.update(pmoTickets)
|
|
230
|
+
.set({ specId, updatedAt: String(Date.now()) })
|
|
231
|
+
.where(eq(pmoTickets.id, ticketId))
|
|
232
|
+
.run();
|
|
233
|
+
this.ctx.updateBoardTimestamp(ticket.projectId);
|
|
214
234
|
}
|
|
215
235
|
/**
|
|
216
236
|
* Unlink a ticket from a spec.
|
|
217
237
|
*/
|
|
218
238
|
async unlinkTicketFromSpec(ticketId, specId) {
|
|
219
239
|
// Get ticket's project for board timestamp update
|
|
220
|
-
const ticket = this.ctx.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
const ticket = this.ctx.drizzle
|
|
241
|
+
.select({ projectId: pmoTickets.projectId })
|
|
242
|
+
.from(pmoTickets)
|
|
243
|
+
.where(eq(pmoTickets.id, ticketId))
|
|
244
|
+
.get();
|
|
245
|
+
this.ctx.drizzle
|
|
246
|
+
.update(pmoTickets)
|
|
247
|
+
.set({ specId: null, updatedAt: String(Date.now()) })
|
|
248
|
+
.where(and(eq(pmoTickets.id, ticketId), eq(pmoTickets.specId, specId)))
|
|
249
|
+
.run();
|
|
228
250
|
if (ticket) {
|
|
229
|
-
this.ctx.updateBoardTimestamp(ticket.
|
|
251
|
+
this.ctx.updateBoardTimestamp(ticket.projectId);
|
|
230
252
|
}
|
|
231
253
|
}
|
|
232
254
|
/**
|
|
233
255
|
* Get tickets for a spec.
|
|
234
256
|
*/
|
|
235
257
|
async getTicketsForSpec(projectId, specId) {
|
|
236
|
-
const rows = this.ctx.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
const rows = this.ctx.drizzle
|
|
259
|
+
.select({
|
|
260
|
+
id: pmoTickets.id,
|
|
261
|
+
project_id: pmoTickets.projectId,
|
|
262
|
+
title: pmoTickets.title,
|
|
263
|
+
description: pmoTickets.description,
|
|
264
|
+
priority: pmoTickets.priority,
|
|
265
|
+
category: pmoTickets.category,
|
|
266
|
+
status_id: pmoTickets.statusId,
|
|
267
|
+
owner: pmoTickets.owner,
|
|
268
|
+
assignee: pmoTickets.assignee,
|
|
269
|
+
branch: pmoTickets.branch,
|
|
270
|
+
spec_id: pmoTickets.specId,
|
|
271
|
+
epic_id: pmoTickets.epicId,
|
|
272
|
+
labels: pmoTickets.labels,
|
|
273
|
+
position: pmoTickets.position,
|
|
274
|
+
created_at: pmoTickets.createdAt,
|
|
275
|
+
updated_at: pmoTickets.updatedAt,
|
|
276
|
+
last_synced_from_spec: pmoTickets.lastSyncedFromSpec,
|
|
277
|
+
last_synced_from_board: pmoTickets.lastSyncedFromBoard,
|
|
278
|
+
column_id: pmoWorkflowStatuses.id,
|
|
279
|
+
column_name: pmoWorkflowStatuses.name,
|
|
280
|
+
project_name: sql `NULL`,
|
|
281
|
+
})
|
|
282
|
+
.from(pmoTickets)
|
|
283
|
+
.leftJoin(pmoWorkflowStatuses, eq(pmoTickets.statusId, pmoWorkflowStatuses.id))
|
|
284
|
+
.where(and(eq(pmoTickets.projectId, projectId), eq(pmoTickets.specId, specId)))
|
|
285
|
+
.orderBy(asc(pmoWorkflowStatuses.position), asc(pmoTickets.position), asc(pmoTickets.createdAt))
|
|
286
|
+
.all();
|
|
287
|
+
return Promise.all(rows.map((row) => rowToTicket(this.ctx.drizzle, row)));
|
|
247
288
|
}
|
|
248
289
|
/**
|
|
249
290
|
* Get specs for a ticket.
|
|
250
291
|
*/
|
|
251
292
|
async getSpecsForTicket(ticketId) {
|
|
252
|
-
const ticket = this.ctx.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
293
|
+
const ticket = this.ctx.drizzle
|
|
294
|
+
.select({ specId: pmoTickets.specId })
|
|
295
|
+
.from(pmoTickets)
|
|
296
|
+
.where(eq(pmoTickets.id, ticketId))
|
|
297
|
+
.get();
|
|
298
|
+
if (!ticket || !ticket.specId) {
|
|
256
299
|
return [];
|
|
257
300
|
}
|
|
258
|
-
const spec = await this.getSpec(ticket.
|
|
301
|
+
const spec = await this.getSpec(ticket.specId);
|
|
259
302
|
return spec ? [spec] : [];
|
|
260
303
|
}
|
|
261
304
|
/**
|
|
@@ -270,31 +313,38 @@ export class SpecStorage {
|
|
|
270
313
|
if (!dependsOnSpec) {
|
|
271
314
|
throw new PMOError('NOT_FOUND', `Spec not found: ${dependsOnId}`);
|
|
272
315
|
}
|
|
273
|
-
this.ctx.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
316
|
+
this.ctx.drizzle
|
|
317
|
+
.insert(pmoSpecDependencies)
|
|
318
|
+
.values({
|
|
319
|
+
specId,
|
|
320
|
+
dependsOnSpecId: dependsOnId,
|
|
321
|
+
createdAt: String(Date.now()),
|
|
322
|
+
})
|
|
323
|
+
.onConflictDoNothing()
|
|
324
|
+
.run();
|
|
277
325
|
}
|
|
278
326
|
/**
|
|
279
327
|
* Remove a spec dependency.
|
|
280
328
|
*/
|
|
281
329
|
async removeSpecDependency(specId, dependsOnId) {
|
|
282
|
-
this.ctx.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
330
|
+
this.ctx.drizzle
|
|
331
|
+
.delete(pmoSpecDependencies)
|
|
332
|
+
.where(and(eq(pmoSpecDependencies.specId, specId), eq(pmoSpecDependencies.dependsOnSpecId, dependsOnId)))
|
|
333
|
+
.run();
|
|
286
334
|
}
|
|
287
335
|
/**
|
|
288
336
|
* Get dependencies for a spec.
|
|
289
337
|
*/
|
|
290
338
|
async getSpecDependencies(specId) {
|
|
291
|
-
const rows = this.ctx.
|
|
292
|
-
|
|
293
|
-
|
|
339
|
+
const rows = this.ctx.drizzle
|
|
340
|
+
.select({ dependsOnSpecId: pmoSpecDependencies.dependsOnSpecId })
|
|
341
|
+
.from(pmoSpecDependencies)
|
|
342
|
+
.where(eq(pmoSpecDependencies.specId, specId))
|
|
343
|
+
.all();
|
|
294
344
|
const specs = [];
|
|
295
345
|
for (const row of rows) {
|
|
296
346
|
// eslint-disable-next-line no-await-in-loop -- Sequential lookup for relationship chain
|
|
297
|
-
const spec = await this.getSpec(row.
|
|
347
|
+
const spec = await this.getSpec(row.dependsOnSpecId);
|
|
298
348
|
if (spec)
|
|
299
349
|
specs.push(spec);
|
|
300
350
|
}
|
|
@@ -304,13 +354,15 @@ export class SpecStorage {
|
|
|
304
354
|
* Get specs that depend on a spec.
|
|
305
355
|
*/
|
|
306
356
|
async getSpecDependents(specId) {
|
|
307
|
-
const rows = this.ctx.
|
|
308
|
-
|
|
309
|
-
|
|
357
|
+
const rows = this.ctx.drizzle
|
|
358
|
+
.select({ specId: pmoSpecDependencies.specId })
|
|
359
|
+
.from(pmoSpecDependencies)
|
|
360
|
+
.where(eq(pmoSpecDependencies.dependsOnSpecId, specId))
|
|
361
|
+
.all();
|
|
310
362
|
const specs = [];
|
|
311
363
|
for (const row of rows) {
|
|
312
364
|
// eslint-disable-next-line no-await-in-loop -- Sequential lookup for relationship chain
|
|
313
|
-
const spec = await this.getSpec(row.
|
|
365
|
+
const spec = await this.getSpec(row.specId);
|
|
314
366
|
if (spec)
|
|
315
367
|
specs.push(spec);
|
|
316
368
|
}
|
|
@@ -321,9 +373,11 @@ export class SpecStorage {
|
|
|
321
373
|
*/
|
|
322
374
|
async linkProjectToSpec(projectId, specId) {
|
|
323
375
|
// Verify project exists
|
|
324
|
-
const project = this.ctx.
|
|
325
|
-
|
|
326
|
-
|
|
376
|
+
const project = this.ctx.drizzle
|
|
377
|
+
.select({ id: pmoProjects.id })
|
|
378
|
+
.from(pmoProjects)
|
|
379
|
+
.where(eq(pmoProjects.id, projectId))
|
|
380
|
+
.get();
|
|
327
381
|
if (!project) {
|
|
328
382
|
throw new PMOError('NOT_FOUND', `Project "${projectId}" not found`);
|
|
329
383
|
}
|
|
@@ -332,59 +386,91 @@ export class SpecStorage {
|
|
|
332
386
|
if (!spec) {
|
|
333
387
|
throw new PMOError('NOT_FOUND', `Spec "${specId}" not found`);
|
|
334
388
|
}
|
|
335
|
-
this.ctx.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
389
|
+
this.ctx.drizzle
|
|
390
|
+
.insert(pmoProjectSpecs)
|
|
391
|
+
.values({
|
|
392
|
+
projectId,
|
|
393
|
+
specId,
|
|
394
|
+
createdAt: String(Date.now()),
|
|
395
|
+
})
|
|
396
|
+
.onConflictDoNothing()
|
|
397
|
+
.run();
|
|
339
398
|
}
|
|
340
399
|
/**
|
|
341
400
|
* Unlink a project from a spec.
|
|
342
401
|
*/
|
|
343
402
|
async unlinkProjectFromSpec(projectId, specId) {
|
|
344
|
-
this.ctx.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
403
|
+
this.ctx.drizzle
|
|
404
|
+
.delete(pmoProjectSpecs)
|
|
405
|
+
.where(and(eq(pmoProjectSpecs.projectId, projectId), eq(pmoProjectSpecs.specId, specId)))
|
|
406
|
+
.run();
|
|
348
407
|
}
|
|
349
408
|
/**
|
|
350
409
|
* Get specs for a project.
|
|
351
410
|
*/
|
|
352
411
|
async getSpecsForProject(projectId) {
|
|
353
|
-
const rows = this.ctx.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
412
|
+
const rows = this.ctx.drizzle
|
|
413
|
+
.select({
|
|
414
|
+
id: pmoSpecs.id,
|
|
415
|
+
title: pmoSpecs.title,
|
|
416
|
+
status: pmoSpecs.status,
|
|
417
|
+
type: pmoSpecs.type,
|
|
418
|
+
tags: pmoSpecs.tags,
|
|
419
|
+
problem: pmoSpecs.problem,
|
|
420
|
+
solution: pmoSpecs.solution,
|
|
421
|
+
decisions: pmoSpecs.decisions,
|
|
422
|
+
not_now: pmoSpecs.notNow,
|
|
423
|
+
ui_ux: pmoSpecs.uiUx,
|
|
424
|
+
acceptance_criteria: pmoSpecs.acceptanceCriteria,
|
|
425
|
+
open_questions: pmoSpecs.openQuestions,
|
|
426
|
+
requirements_functional: pmoSpecs.requirementsFunctional,
|
|
427
|
+
requirements_technical: pmoSpecs.requirementsTechnical,
|
|
428
|
+
context: pmoSpecs.context,
|
|
429
|
+
created_at: pmoSpecs.createdAt,
|
|
430
|
+
updated_at: pmoSpecs.updatedAt,
|
|
431
|
+
})
|
|
432
|
+
.from(pmoSpecs)
|
|
433
|
+
.innerJoin(pmoProjectSpecs, eq(pmoSpecs.id, pmoProjectSpecs.specId))
|
|
434
|
+
.where(eq(pmoProjectSpecs.projectId, projectId))
|
|
435
|
+
.orderBy(asc(pmoSpecs.title))
|
|
436
|
+
.all();
|
|
437
|
+
return rows.map((row) => rowToSpec(row));
|
|
365
438
|
}
|
|
366
439
|
/**
|
|
367
440
|
* Get projects for a spec.
|
|
368
441
|
*/
|
|
369
442
|
async getProjectsForSpec(specId) {
|
|
370
|
-
const rows = this.ctx.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
443
|
+
const rows = this.ctx.drizzle
|
|
444
|
+
.select({
|
|
445
|
+
id: pmoProjects.id,
|
|
446
|
+
name: pmoProjects.name,
|
|
447
|
+
template: pmoProjects.template,
|
|
448
|
+
description: pmoProjects.description,
|
|
449
|
+
status: pmoProjects.status,
|
|
450
|
+
phaseId: pmoProjects.phaseId,
|
|
451
|
+
workflowId: pmoProjects.workflowId,
|
|
452
|
+
isArchived: pmoProjects.isArchived,
|
|
453
|
+
targetDate: pmoProjects.targetDate,
|
|
454
|
+
initiativeId: pmoProjects.initiativeId,
|
|
455
|
+
createdAt: pmoProjects.createdAt,
|
|
456
|
+
updatedAt: pmoProjects.updatedAt,
|
|
457
|
+
})
|
|
458
|
+
.from(pmoProjects)
|
|
459
|
+
.innerJoin(pmoProjectSpecs, eq(pmoProjects.id, pmoProjectSpecs.projectId))
|
|
460
|
+
.where(eq(pmoProjectSpecs.specId, specId))
|
|
461
|
+
.orderBy(asc(pmoProjects.name))
|
|
462
|
+
.all();
|
|
377
463
|
return rows.map((row) => ({
|
|
378
464
|
id: row.id,
|
|
379
465
|
name: row.name,
|
|
380
466
|
template: row.template || undefined,
|
|
381
467
|
description: row.description || undefined,
|
|
382
468
|
status: (row.status || 'active'),
|
|
383
|
-
phaseId: row.
|
|
384
|
-
isArchived: row.
|
|
385
|
-
targetDate: row.
|
|
386
|
-
createdAt: new Date(row.
|
|
387
|
-
updatedAt: new Date(row.
|
|
469
|
+
phaseId: row.phaseId || undefined,
|
|
470
|
+
isArchived: row.isArchived ?? false,
|
|
471
|
+
targetDate: row.targetDate ? new Date(row.targetDate) : undefined,
|
|
472
|
+
createdAt: new Date(row.createdAt || Date.now()),
|
|
473
|
+
updatedAt: new Date(row.updatedAt || Date.now()),
|
|
388
474
|
}));
|
|
389
475
|
}
|
|
390
476
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Tickets reference workflow statuses directly via status_id.
|
|
4
4
|
* Tickets have a position column for force-ranked ordering within a status.
|
|
5
5
|
* Positions use gapped integers (1000, 2000, 3000...) for stable reordering.
|
|
6
|
+
*
|
|
7
|
+
* This module uses Drizzle ORM for type-safe database queries.
|
|
6
8
|
*/
|
|
7
9
|
import { CreateTicketInput, Ticket, TicketFilter } from '../types.js';
|
|
8
10
|
import { StorageContext } from './types.js';
|