@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.
- package/README.md +15 -0
- package/package.json +51 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrate.ts +51 -0
- package/src/db/migrations/001_initial_schema.sql +160 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/types.ts +225 -0
- package/src/index.ts +7 -0
- package/src/repos/base.ts +151 -0
- package/src/repos/dependencies.ts +356 -0
- package/src/repos/features.ts +507 -0
- package/src/repos/index.ts +4 -0
- package/src/repos/projects.ts +350 -0
- package/src/repos/sections.ts +505 -0
- package/src/repos/tags.example.ts +125 -0
- package/src/repos/tags.ts +175 -0
- package/src/repos/tasks.ts +581 -0
- package/src/repos/templates.ts +649 -0
- package/src/server.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/status-validator.ts +100 -0
- package/src/services/workflow.ts +104 -0
- package/src/tools/apply-template.ts +129 -0
- package/src/tools/get-blocked-tasks.ts +63 -0
- package/src/tools/get-next-status.ts +183 -0
- package/src/tools/get-next-task.ts +75 -0
- package/src/tools/get-tag-usage.ts +54 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-tags.ts +56 -0
- package/src/tools/manage-container.ts +333 -0
- package/src/tools/manage-dependency.ts +198 -0
- package/src/tools/manage-sections.ts +388 -0
- package/src/tools/manage-template.ts +313 -0
- package/src/tools/query-container.ts +296 -0
- package/src/tools/query-dependencies.ts +68 -0
- package/src/tools/query-sections.ts +70 -0
- package/src/tools/query-templates.ts +137 -0
- package/src/tools/query-workflow-state.ts +198 -0
- package/src/tools/registry.ts +180 -0
- package/src/tools/rename-tag.ts +64 -0
- 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
|
+
}
|