@allpepper/task-orchestrator 0.1.0

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.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/src/db/client.ts +34 -0
  4. package/src/db/index.ts +1 -0
  5. package/src/db/migrate.ts +51 -0
  6. package/src/db/migrations/001_initial_schema.sql +160 -0
  7. package/src/domain/index.ts +1 -0
  8. package/src/domain/types.ts +225 -0
  9. package/src/index.ts +7 -0
  10. package/src/repos/base.ts +151 -0
  11. package/src/repos/dependencies.ts +356 -0
  12. package/src/repos/features.ts +507 -0
  13. package/src/repos/index.ts +4 -0
  14. package/src/repos/projects.ts +350 -0
  15. package/src/repos/sections.ts +505 -0
  16. package/src/repos/tags.example.ts +125 -0
  17. package/src/repos/tags.ts +175 -0
  18. package/src/repos/tasks.ts +581 -0
  19. package/src/repos/templates.ts +649 -0
  20. package/src/server.ts +121 -0
  21. package/src/services/index.ts +2 -0
  22. package/src/services/status-validator.ts +100 -0
  23. package/src/services/workflow.ts +104 -0
  24. package/src/tools/apply-template.ts +129 -0
  25. package/src/tools/get-blocked-tasks.ts +63 -0
  26. package/src/tools/get-next-status.ts +183 -0
  27. package/src/tools/get-next-task.ts +75 -0
  28. package/src/tools/get-tag-usage.ts +54 -0
  29. package/src/tools/index.ts +30 -0
  30. package/src/tools/list-tags.ts +56 -0
  31. package/src/tools/manage-container.ts +333 -0
  32. package/src/tools/manage-dependency.ts +198 -0
  33. package/src/tools/manage-sections.ts +388 -0
  34. package/src/tools/manage-template.ts +313 -0
  35. package/src/tools/query-container.ts +296 -0
  36. package/src/tools/query-dependencies.ts +68 -0
  37. package/src/tools/query-sections.ts +70 -0
  38. package/src/tools/query-templates.ts +137 -0
  39. package/src/tools/query-workflow-state.ts +198 -0
  40. package/src/tools/registry.ts +180 -0
  41. package/src/tools/rename-tag.ts +64 -0
  42. package/src/tools/setup-project.ts +189 -0
@@ -0,0 +1,507 @@
1
+ import {
2
+ queryOne,
3
+ queryAll,
4
+ execute,
5
+ generateId,
6
+ now,
7
+ loadTags,
8
+ saveTags,
9
+ deleteTags,
10
+ ok,
11
+ err,
12
+ buildSearchVector,
13
+ buildPaginationClause,
14
+ countTasksByFeature,
15
+ type TaskCounts
16
+ } from './base';
17
+ import { transaction } from '../db/client';
18
+ import type { Result, Feature, FeatureStatus, Priority } from '../domain/types';
19
+ import { NotFoundError, ValidationError, ConflictError, EntityType } from '../domain/types';
20
+ import { isValidTransition, getAllowedTransitions, isTerminalStatus } from '../services/status-validator';
21
+
22
+ // ============================================================================
23
+ // Database Row Types
24
+ // ============================================================================
25
+
26
+ interface FeatureRow {
27
+ id: string;
28
+ project_id: string | null;
29
+ name: string;
30
+ summary: string;
31
+ description: string | null;
32
+ status: string;
33
+ priority: string;
34
+ version: number;
35
+ created_at: string;
36
+ modified_at: string;
37
+ search_vector: string | null;
38
+ }
39
+
40
+ // ============================================================================
41
+ // Mappers
42
+ // ============================================================================
43
+
44
+ function rowToFeature(row: FeatureRow, tags?: string[]): Feature {
45
+ return {
46
+ id: row.id,
47
+ projectId: row.project_id ?? undefined,
48
+ name: row.name,
49
+ summary: row.summary,
50
+ description: row.description ?? undefined,
51
+ status: row.status as FeatureStatus,
52
+ priority: row.priority as Priority,
53
+ version: row.version,
54
+ createdAt: new Date(row.created_at),
55
+ modifiedAt: new Date(row.modified_at),
56
+ searchVector: row.search_vector ?? undefined,
57
+ tags: tags ?? []
58
+ };
59
+ }
60
+
61
+ // ============================================================================
62
+ // Validation
63
+ // ============================================================================
64
+
65
+ function validateFeatureParams(params: {
66
+ name?: string;
67
+ summary?: string;
68
+ status?: FeatureStatus;
69
+ priority?: Priority;
70
+ }): void {
71
+ if (params.name !== undefined && params.name.trim().length === 0) {
72
+ throw new ValidationError('Feature name cannot be empty');
73
+ }
74
+ if (params.summary !== undefined && params.summary.trim().length === 0) {
75
+ throw new ValidationError('Feature summary cannot be empty');
76
+ }
77
+ }
78
+
79
+ // ============================================================================
80
+ // Repository Functions
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Create a new feature
85
+ */
86
+ export function createFeature(params: {
87
+ projectId?: string;
88
+ name: string;
89
+ summary: string;
90
+ description?: string;
91
+ status?: FeatureStatus;
92
+ priority: Priority;
93
+ tags?: string[];
94
+ }): Result<Feature> {
95
+ try {
96
+ validateFeatureParams({
97
+ name: params.name,
98
+ summary: params.summary,
99
+ status: params.status,
100
+ priority: params.priority
101
+ });
102
+
103
+ // Validate project exists if provided
104
+ if (params.projectId) {
105
+ const projectExists = queryOne<{ id: string }>(
106
+ 'SELECT id FROM projects WHERE id = ?',
107
+ [params.projectId]
108
+ );
109
+ if (!projectExists) {
110
+ throw new ValidationError(`Project not found: ${params.projectId}`);
111
+ }
112
+ }
113
+
114
+ const feature = transaction(() => {
115
+ const id = generateId();
116
+ const timestamp = now();
117
+ const status = params.status ?? 'DRAFT';
118
+ const searchVector = buildSearchVector(params.name, params.summary, params.description);
119
+
120
+ execute(
121
+ `INSERT INTO features (
122
+ id, project_id, name, summary, description, status, priority,
123
+ version, created_at, modified_at, search_vector
124
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
125
+ [
126
+ id,
127
+ params.projectId ?? null,
128
+ params.name,
129
+ params.summary,
130
+ params.description ?? null,
131
+ status,
132
+ params.priority,
133
+ 1,
134
+ timestamp,
135
+ timestamp,
136
+ searchVector
137
+ ]
138
+ );
139
+
140
+ // Save tags if provided
141
+ if (params.tags && params.tags.length > 0) {
142
+ saveTags(id, EntityType.FEATURE, params.tags);
143
+ }
144
+
145
+ const row = queryOne<FeatureRow>('SELECT * FROM features WHERE id = ?', [id]);
146
+ if (!row) {
147
+ throw new Error('Failed to retrieve created feature');
148
+ }
149
+
150
+ const tags = loadTags(id, EntityType.FEATURE);
151
+ return rowToFeature(row, tags);
152
+ });
153
+
154
+ return ok(feature);
155
+ } catch (error) {
156
+ if (error instanceof ValidationError) {
157
+ return err(error.message, 'VALIDATION_ERROR');
158
+ }
159
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get a feature by ID
165
+ */
166
+ export function getFeature(id: string): Result<Feature> {
167
+ try {
168
+ const row = queryOne<FeatureRow>('SELECT * FROM features WHERE id = ?', [id]);
169
+
170
+ if (!row) {
171
+ throw new NotFoundError('Feature', id);
172
+ }
173
+
174
+ const tags = loadTags(id, EntityType.FEATURE);
175
+ const feature = rowToFeature(row, tags);
176
+
177
+ return ok(feature);
178
+ } catch (error) {
179
+ if (error instanceof NotFoundError) {
180
+ return err(error.message, 'NOT_FOUND');
181
+ }
182
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Update a feature (with optimistic locking)
188
+ */
189
+ export function updateFeature(
190
+ id: string,
191
+ params: {
192
+ name?: string;
193
+ summary?: string;
194
+ description?: string;
195
+ status?: FeatureStatus;
196
+ priority?: Priority;
197
+ projectId?: string;
198
+ tags?: string[];
199
+ version: number;
200
+ }
201
+ ): Result<Feature> {
202
+ try {
203
+ validateFeatureParams({
204
+ name: params.name,
205
+ summary: params.summary,
206
+ status: params.status,
207
+ priority: params.priority
208
+ });
209
+
210
+ // Validate project exists if provided
211
+ if (params.projectId !== undefined) {
212
+ if (params.projectId !== null) {
213
+ const projectExists = queryOne<{ id: string }>(
214
+ 'SELECT id FROM projects WHERE id = ?',
215
+ [params.projectId]
216
+ );
217
+ if (!projectExists) {
218
+ throw new ValidationError(`Project not found: ${params.projectId}`);
219
+ }
220
+ }
221
+ }
222
+
223
+ const feature = transaction(() => {
224
+ // Check if feature exists and version matches
225
+ const current = queryOne<FeatureRow>('SELECT * FROM features WHERE id = ?', [id]);
226
+
227
+ if (!current) {
228
+ throw new NotFoundError('Feature', id);
229
+ }
230
+
231
+ if (current.version !== params.version) {
232
+ throw new ConflictError(
233
+ `Version conflict: expected ${params.version}, found ${current.version}`
234
+ );
235
+ }
236
+
237
+ // Validate status transition if status is being updated
238
+ if (params.status !== undefined && params.status !== current.status) {
239
+ const currentStatus = current.status;
240
+
241
+ // Check if current status is terminal
242
+ if (isTerminalStatus('feature', currentStatus)) {
243
+ throw new ValidationError(
244
+ `Cannot transition from terminal status '${currentStatus}'`
245
+ );
246
+ }
247
+
248
+ // Check if the transition is valid
249
+ if (!isValidTransition('feature', currentStatus, params.status)) {
250
+ const allowed = getAllowedTransitions('feature', currentStatus);
251
+ throw new ValidationError(
252
+ `Invalid status transition from '${currentStatus}' to '${params.status}'. Allowed transitions: ${allowed.join(', ')}`
253
+ );
254
+ }
255
+ }
256
+
257
+ // Build update query dynamically based on provided params
258
+ const updates: string[] = [];
259
+ const values: any[] = [];
260
+
261
+ if (params.name !== undefined) {
262
+ updates.push('name = ?');
263
+ values.push(params.name);
264
+ }
265
+ if (params.summary !== undefined) {
266
+ updates.push('summary = ?');
267
+ values.push(params.summary);
268
+ }
269
+ if (params.description !== undefined) {
270
+ updates.push('description = ?');
271
+ values.push(params.description);
272
+ }
273
+ if (params.status !== undefined) {
274
+ updates.push('status = ?');
275
+ values.push(params.status);
276
+ }
277
+ if (params.priority !== undefined) {
278
+ updates.push('priority = ?');
279
+ values.push(params.priority);
280
+ }
281
+ if (params.projectId !== undefined) {
282
+ updates.push('project_id = ?');
283
+ values.push(params.projectId);
284
+ }
285
+
286
+ // Update search vector if any text field changed
287
+ if (params.name !== undefined || params.summary !== undefined || params.description !== undefined) {
288
+ const searchVector = buildSearchVector(
289
+ params.name ?? current.name,
290
+ params.summary ?? current.summary,
291
+ params.description !== undefined ? params.description : current.description
292
+ );
293
+ updates.push('search_vector = ?');
294
+ values.push(searchVector);
295
+ }
296
+
297
+ // Always update version and modified_at
298
+ updates.push('version = ?');
299
+ values.push(params.version + 1);
300
+
301
+ const timestamp = now();
302
+ updates.push('modified_at = ?');
303
+ values.push(timestamp);
304
+
305
+ // Add WHERE clause params
306
+ values.push(id);
307
+ values.push(params.version);
308
+
309
+ execute(
310
+ `UPDATE features SET ${updates.join(', ')} WHERE id = ? AND version = ?`,
311
+ values
312
+ );
313
+
314
+ // Update tags if provided
315
+ if (params.tags !== undefined) {
316
+ saveTags(id, EntityType.FEATURE, params.tags);
317
+ }
318
+
319
+ const row = queryOne<FeatureRow>('SELECT * FROM features WHERE id = ?', [id]);
320
+ if (!row) {
321
+ throw new Error('Failed to retrieve updated feature');
322
+ }
323
+
324
+ const tags = loadTags(id, EntityType.FEATURE);
325
+ return rowToFeature(row, tags);
326
+ });
327
+
328
+ return ok(feature);
329
+ } catch (error) {
330
+ if (error instanceof NotFoundError) {
331
+ return err(error.message, 'NOT_FOUND');
332
+ }
333
+ if (error instanceof ValidationError) {
334
+ return err(error.message, 'VALIDATION_ERROR');
335
+ }
336
+ if (error instanceof ConflictError) {
337
+ return err(error.message, 'CONFLICT');
338
+ }
339
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Delete a feature
345
+ */
346
+ export function deleteFeature(id: string): Result<boolean> {
347
+ try {
348
+ const result = transaction(() => {
349
+ // Check if feature exists
350
+ const exists = queryOne<{ id: string }>('SELECT id FROM features WHERE id = ?', [id]);
351
+
352
+ if (!exists) {
353
+ throw new NotFoundError('Feature', id);
354
+ }
355
+
356
+ // Delete associated tags
357
+ deleteTags(id, EntityType.FEATURE);
358
+
359
+ // Delete the feature
360
+ const changes = execute('DELETE FROM features WHERE id = ?', [id]);
361
+
362
+ return changes > 0;
363
+ });
364
+
365
+ return ok(result);
366
+ } catch (error) {
367
+ if (error instanceof NotFoundError) {
368
+ return err(error.message, 'NOT_FOUND');
369
+ }
370
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Search features with flexible filtering
376
+ */
377
+ export function searchFeatures(params: {
378
+ query?: string;
379
+ status?: string;
380
+ priority?: string;
381
+ projectId?: string;
382
+ tags?: string;
383
+ limit?: number;
384
+ offset?: number;
385
+ }): Result<Feature[]> {
386
+ try {
387
+ const conditions: string[] = [];
388
+ const values: any[] = [];
389
+
390
+ // Text search via search_vector
391
+ if (params.query) {
392
+ conditions.push('search_vector LIKE ?');
393
+ values.push(`%${params.query.toLowerCase()}%`);
394
+ }
395
+
396
+ // Status filter (supports multi-value and negation)
397
+ if (params.status) {
398
+ const statusFilters = params.status.split(',').map(s => s.trim());
399
+ const negations: string[] = [];
400
+ const inclusions: string[] = [];
401
+
402
+ for (const filter of statusFilters) {
403
+ if (filter.startsWith('!')) {
404
+ negations.push(filter.substring(1));
405
+ } else {
406
+ inclusions.push(filter);
407
+ }
408
+ }
409
+
410
+ if (inclusions.length > 0) {
411
+ conditions.push(`status IN (${inclusions.map(() => '?').join(', ')})`);
412
+ values.push(...inclusions);
413
+ }
414
+
415
+ if (negations.length > 0) {
416
+ conditions.push(`status NOT IN (${negations.map(() => '?').join(', ')})`);
417
+ values.push(...negations);
418
+ }
419
+ }
420
+
421
+ // Priority filter (supports multi-value and negation)
422
+ if (params.priority) {
423
+ const priorityFilters = params.priority.split(',').map(p => p.trim());
424
+ const negations: string[] = [];
425
+ const inclusions: string[] = [];
426
+
427
+ for (const filter of priorityFilters) {
428
+ if (filter.startsWith('!')) {
429
+ negations.push(filter.substring(1));
430
+ } else {
431
+ inclusions.push(filter);
432
+ }
433
+ }
434
+
435
+ if (inclusions.length > 0) {
436
+ conditions.push(`priority IN (${inclusions.map(() => '?').join(', ')})`);
437
+ values.push(...inclusions);
438
+ }
439
+
440
+ if (negations.length > 0) {
441
+ conditions.push(`priority NOT IN (${negations.map(() => '?').join(', ')})`);
442
+ values.push(...negations);
443
+ }
444
+ }
445
+
446
+ // Project filter
447
+ if (params.projectId) {
448
+ conditions.push('project_id = ?');
449
+ values.push(params.projectId);
450
+ }
451
+
452
+ // Tags filter
453
+ if (params.tags) {
454
+ const tagList = params.tags.split(',').map(t => t.trim().toLowerCase());
455
+ conditions.push(`id IN (
456
+ SELECT entity_id FROM entity_tags
457
+ WHERE entity_type = ? AND tag IN (${tagList.map(() => '?').join(', ')})
458
+ )`);
459
+ values.push(EntityType.FEATURE, ...tagList);
460
+ }
461
+
462
+ // Build query
463
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
464
+ const paginationClause = buildPaginationClause({
465
+ limit: params.limit,
466
+ offset: params.offset
467
+ });
468
+
469
+ const sql = `SELECT * FROM features ${whereClause} ORDER BY created_at DESC${paginationClause}`;
470
+ const rows = queryAll<FeatureRow>(sql, values);
471
+
472
+ // Load tags for each feature
473
+ const features = rows.map(row => {
474
+ const tags = loadTags(row.id, EntityType.FEATURE);
475
+ return rowToFeature(row, tags);
476
+ });
477
+
478
+ return ok(features);
479
+ } catch (error) {
480
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Get feature with task counts
486
+ */
487
+ export function getFeatureOverview(id: string): Result<{
488
+ feature: Feature;
489
+ taskCounts: TaskCounts;
490
+ }> {
491
+ try {
492
+ const featureResult = getFeature(id);
493
+
494
+ if (!featureResult.success) {
495
+ return featureResult as Result<any>;
496
+ }
497
+
498
+ const taskCounts = countTasksByFeature(id);
499
+
500
+ return ok({
501
+ feature: featureResult.data,
502
+ taskCounts
503
+ });
504
+ } catch (error) {
505
+ return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
506
+ }
507
+ }
@@ -0,0 +1,4 @@
1
+ export * as projects from './projects';
2
+ export * as features from './features';
3
+ export * as dependencies from './dependencies';
4
+ export * from './base';