@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,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
+ }