@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,350 @@
|
|
|
1
|
+
import { queryOne, queryAll, execute, generateId, now, buildSearchVector, loadTags, saveTags, deleteTags, ok, err, buildPaginationClause, countTasksByProject, type TaskCounts } from './base';
|
|
2
|
+
import type { Project, Result } from '../domain/types';
|
|
3
|
+
import { ProjectStatus, NotFoundError, ConflictError, ValidationError } from '../domain/types';
|
|
4
|
+
import { transaction } from '../db/client';
|
|
5
|
+
import { isValidTransition, getAllowedTransitions, isTerminalStatus } from '../services/status-validator';
|
|
6
|
+
|
|
7
|
+
interface ProjectRow {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
description: string | null;
|
|
12
|
+
status: string;
|
|
13
|
+
version: number;
|
|
14
|
+
created_at: string;
|
|
15
|
+
modified_at: string;
|
|
16
|
+
search_vector: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rowToProject(row: ProjectRow, tags?: string[]): Project {
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
name: row.name,
|
|
23
|
+
summary: row.summary,
|
|
24
|
+
description: row.description ?? undefined,
|
|
25
|
+
status: row.status as ProjectStatus,
|
|
26
|
+
version: row.version,
|
|
27
|
+
createdAt: new Date(row.created_at),
|
|
28
|
+
modifiedAt: new Date(row.modified_at),
|
|
29
|
+
searchVector: row.search_vector ?? undefined,
|
|
30
|
+
tags
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createProject(params: {
|
|
35
|
+
name: string;
|
|
36
|
+
summary: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
status?: ProjectStatus;
|
|
39
|
+
tags?: string[];
|
|
40
|
+
}): Result<Project> {
|
|
41
|
+
try {
|
|
42
|
+
// Validate name not empty
|
|
43
|
+
if (!params.name.trim()) {
|
|
44
|
+
throw new ValidationError('Project name cannot be empty');
|
|
45
|
+
}
|
|
46
|
+
if (!params.summary.trim()) {
|
|
47
|
+
throw new ValidationError('Project summary cannot be empty');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = transaction(() => {
|
|
51
|
+
const id = generateId();
|
|
52
|
+
const timestamp = now();
|
|
53
|
+
const status = params.status ?? ProjectStatus.PLANNING;
|
|
54
|
+
const searchVector = buildSearchVector(params.name, params.summary, params.description);
|
|
55
|
+
|
|
56
|
+
execute(
|
|
57
|
+
`INSERT INTO projects (id, name, summary, description, status, version, created_at, modified_at, search_vector)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
59
|
+
[id, params.name, params.summary, params.description ?? null, status, 1, timestamp, timestamp, searchVector]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Save tags if provided
|
|
63
|
+
if (params.tags && params.tags.length > 0) {
|
|
64
|
+
saveTags(id, 'PROJECT', params.tags);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const tags = params.tags && params.tags.length > 0 ? loadTags(id, 'PROJECT') : [];
|
|
68
|
+
|
|
69
|
+
return rowToProject({
|
|
70
|
+
id,
|
|
71
|
+
name: params.name,
|
|
72
|
+
summary: params.summary,
|
|
73
|
+
description: params.description ?? null,
|
|
74
|
+
status,
|
|
75
|
+
version: 1,
|
|
76
|
+
created_at: timestamp,
|
|
77
|
+
modified_at: timestamp,
|
|
78
|
+
search_vector: searchVector
|
|
79
|
+
}, tags);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return ok(result);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof ValidationError) {
|
|
85
|
+
return err(error.message, 'VALIDATION_ERROR');
|
|
86
|
+
}
|
|
87
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'CREATE_FAILED');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getProject(id: string): Result<Project> {
|
|
92
|
+
try {
|
|
93
|
+
const row = queryOne<ProjectRow>(
|
|
94
|
+
'SELECT * FROM projects WHERE id = ?',
|
|
95
|
+
[id]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!row) {
|
|
99
|
+
throw new NotFoundError('Project', id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const tags = loadTags(id, 'PROJECT');
|
|
103
|
+
return ok(rowToProject(row, tags));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof NotFoundError) {
|
|
106
|
+
return err(error.message, 'NOT_FOUND');
|
|
107
|
+
}
|
|
108
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'GET_FAILED');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function updateProject(
|
|
113
|
+
id: string,
|
|
114
|
+
params: {
|
|
115
|
+
name?: string;
|
|
116
|
+
summary?: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
status?: ProjectStatus;
|
|
119
|
+
tags?: string[];
|
|
120
|
+
version: number;
|
|
121
|
+
}
|
|
122
|
+
): Result<Project> {
|
|
123
|
+
try {
|
|
124
|
+
// Validate inputs
|
|
125
|
+
if (params.name !== undefined && !params.name.trim()) {
|
|
126
|
+
throw new ValidationError('Project name cannot be empty');
|
|
127
|
+
}
|
|
128
|
+
if (params.summary !== undefined && !params.summary.trim()) {
|
|
129
|
+
throw new ValidationError('Project summary cannot be empty');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = transaction(() => {
|
|
133
|
+
// Get existing project
|
|
134
|
+
const existing = queryOne<ProjectRow>(
|
|
135
|
+
'SELECT * FROM projects WHERE id = ?',
|
|
136
|
+
[id]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (!existing) {
|
|
140
|
+
throw new NotFoundError('Project', id);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check version matches (optimistic locking)
|
|
144
|
+
if (existing.version !== params.version) {
|
|
145
|
+
throw new ConflictError(
|
|
146
|
+
`Version mismatch: expected ${params.version}, got ${existing.version}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate status transition if status is being changed
|
|
151
|
+
if (params.status !== undefined && params.status !== existing.status) {
|
|
152
|
+
const currentStatus = existing.status;
|
|
153
|
+
const newStatus = params.status;
|
|
154
|
+
|
|
155
|
+
// Check if current status is terminal
|
|
156
|
+
if (isTerminalStatus('project', currentStatus)) {
|
|
157
|
+
throw new ValidationError(
|
|
158
|
+
`Cannot transition from terminal status '${currentStatus}'`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check if transition is valid
|
|
163
|
+
if (!isValidTransition('project', currentStatus, newStatus)) {
|
|
164
|
+
const allowed = getAllowedTransitions('project', currentStatus);
|
|
165
|
+
throw new ValidationError(
|
|
166
|
+
`Invalid status transition from '${currentStatus}' to '${newStatus}'. Allowed transitions: ${allowed.join(', ')}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Merge updated fields
|
|
172
|
+
const name = params.name ?? existing.name;
|
|
173
|
+
const summary = params.summary ?? existing.summary;
|
|
174
|
+
const description = params.description !== undefined ? params.description : existing.description;
|
|
175
|
+
const status = params.status ?? existing.status;
|
|
176
|
+
const newVersion = existing.version + 1;
|
|
177
|
+
const modifiedAt = now();
|
|
178
|
+
|
|
179
|
+
// Rebuild search vector with updated fields
|
|
180
|
+
const searchVector = buildSearchVector(name, summary, description ?? undefined);
|
|
181
|
+
|
|
182
|
+
execute(
|
|
183
|
+
`UPDATE projects
|
|
184
|
+
SET name = ?, summary = ?, description = ?, status = ?, version = ?, modified_at = ?, search_vector = ?
|
|
185
|
+
WHERE id = ?`,
|
|
186
|
+
[name, summary, description, status, newVersion, modifiedAt, searchVector, id]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Update tags if provided
|
|
190
|
+
if (params.tags !== undefined) {
|
|
191
|
+
saveTags(id, 'PROJECT', params.tags);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const tags = loadTags(id, 'PROJECT');
|
|
195
|
+
|
|
196
|
+
return rowToProject({
|
|
197
|
+
id: existing.id,
|
|
198
|
+
name,
|
|
199
|
+
summary,
|
|
200
|
+
description,
|
|
201
|
+
status,
|
|
202
|
+
version: newVersion,
|
|
203
|
+
created_at: existing.created_at,
|
|
204
|
+
modified_at: modifiedAt,
|
|
205
|
+
search_vector: searchVector
|
|
206
|
+
}, tags);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return ok(result);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof NotFoundError) {
|
|
212
|
+
return err(error.message, 'NOT_FOUND');
|
|
213
|
+
}
|
|
214
|
+
if (error instanceof ConflictError) {
|
|
215
|
+
return err(error.message, 'VERSION_CONFLICT');
|
|
216
|
+
}
|
|
217
|
+
if (error instanceof ValidationError) {
|
|
218
|
+
return err(error.message, 'VALIDATION_ERROR');
|
|
219
|
+
}
|
|
220
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'UPDATE_FAILED');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function deleteProject(id: string): Result<boolean> {
|
|
225
|
+
try {
|
|
226
|
+
const result = transaction(() => {
|
|
227
|
+
const existing = queryOne<ProjectRow>(
|
|
228
|
+
'SELECT id FROM projects WHERE id = ?',
|
|
229
|
+
[id]
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!existing) {
|
|
233
|
+
throw new NotFoundError('Project', id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Delete tags first
|
|
237
|
+
deleteTags(id, 'PROJECT');
|
|
238
|
+
|
|
239
|
+
// Delete project
|
|
240
|
+
execute('DELETE FROM projects WHERE id = ?', [id]);
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return ok(result);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof NotFoundError) {
|
|
248
|
+
return err(error.message, 'NOT_FOUND');
|
|
249
|
+
}
|
|
250
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'DELETE_FAILED');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function searchProjects(params: {
|
|
255
|
+
query?: string;
|
|
256
|
+
status?: string;
|
|
257
|
+
tags?: string;
|
|
258
|
+
limit?: number;
|
|
259
|
+
offset?: number;
|
|
260
|
+
}): Result<Project[]> {
|
|
261
|
+
try {
|
|
262
|
+
const whereClauses: string[] = [];
|
|
263
|
+
const queryParams: any[] = [];
|
|
264
|
+
|
|
265
|
+
// Text search via search_vector LIKE
|
|
266
|
+
if (params.query) {
|
|
267
|
+
whereClauses.push('search_vector LIKE ?');
|
|
268
|
+
queryParams.push(`%${params.query.toLowerCase()}%`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Status filter (supports multi-value "PLANNING,IN_DEVELOPMENT" and negation "!COMPLETED")
|
|
272
|
+
if (params.status) {
|
|
273
|
+
if (params.status.startsWith('!')) {
|
|
274
|
+
// Negation
|
|
275
|
+
const excludedStatus = params.status.substring(1);
|
|
276
|
+
whereClauses.push('status != ?');
|
|
277
|
+
queryParams.push(excludedStatus);
|
|
278
|
+
} else if (params.status.includes(',')) {
|
|
279
|
+
// Multi-value
|
|
280
|
+
const statuses = params.status.split(',').map(s => s.trim());
|
|
281
|
+
const placeholders = statuses.map(() => '?').join(',');
|
|
282
|
+
whereClauses.push(`status IN (${placeholders})`);
|
|
283
|
+
queryParams.push(...statuses);
|
|
284
|
+
} else {
|
|
285
|
+
// Single value
|
|
286
|
+
whereClauses.push('status = ?');
|
|
287
|
+
queryParams.push(params.status);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Tags filter via subquery on entity_tags
|
|
292
|
+
if (params.tags) {
|
|
293
|
+
const tags = params.tags.split(',').map(t => t.trim().toLowerCase());
|
|
294
|
+
whereClauses.push(`
|
|
295
|
+
id IN (
|
|
296
|
+
SELECT entity_id FROM entity_tags
|
|
297
|
+
WHERE entity_type = 'PROJECT' AND tag IN (${tags.map(() => '?').join(',')})
|
|
298
|
+
GROUP BY entity_id
|
|
299
|
+
HAVING COUNT(DISTINCT tag) = ?
|
|
300
|
+
)
|
|
301
|
+
`);
|
|
302
|
+
queryParams.push(...tags, tags.length);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
|
306
|
+
const paginationClause = buildPaginationClause({ limit: params.limit, offset: params.offset });
|
|
307
|
+
|
|
308
|
+
const sql = `
|
|
309
|
+
SELECT * FROM projects
|
|
310
|
+
${whereClause}
|
|
311
|
+
ORDER BY modified_at DESC
|
|
312
|
+
${paginationClause}
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
const rows = queryAll<ProjectRow>(sql, queryParams);
|
|
316
|
+
|
|
317
|
+
// Load tags for each result
|
|
318
|
+
const projects = rows.map(row => {
|
|
319
|
+
const tags = loadTags(row.id, 'PROJECT');
|
|
320
|
+
return rowToProject(row, tags);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return ok(projects);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'SEARCH_FAILED');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function getProjectOverview(id: string): Result<{ project: Project; taskCounts: TaskCounts }> {
|
|
330
|
+
try {
|
|
331
|
+
// Get project
|
|
332
|
+
const projectResult = getProject(id);
|
|
333
|
+
if (!projectResult.success) {
|
|
334
|
+
throw new NotFoundError('Project', id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Get task counts
|
|
338
|
+
const taskCounts = countTasksByProject(id);
|
|
339
|
+
|
|
340
|
+
return ok({
|
|
341
|
+
project: projectResult.data,
|
|
342
|
+
taskCounts
|
|
343
|
+
});
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (error instanceof NotFoundError) {
|
|
346
|
+
return err(error.message, 'NOT_FOUND');
|
|
347
|
+
}
|
|
348
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'OVERVIEW_FAILED');
|
|
349
|
+
}
|
|
350
|
+
}
|