digital-tasks 2.0.2 → 2.1.1
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/CHANGELOG.md +19 -0
- package/package.json +4 -5
- package/src/index.js +73 -0
- package/src/markdown.js +509 -0
- package/src/project.js +396 -0
- package/src/queue.js +346 -0
- package/src/task.js +320 -0
- package/src/types.js +14 -0
- package/test/markdown.test.js +451 -0
- package/test/project.test.js +427 -0
- package/test/queue.test.js +407 -0
- package/test/task.test.js +370 -0
package/src/project.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Management - Task workflows, dependencies, and execution modes
|
|
3
|
+
*
|
|
4
|
+
* Provides project management primitives for organizing tasks:
|
|
5
|
+
* - Projects/TaskLists as containers
|
|
6
|
+
* - Parallel vs Sequential execution
|
|
7
|
+
* - Dependencies and dependants (bidirectional)
|
|
8
|
+
* - Subtasks with inheritance
|
|
9
|
+
*
|
|
10
|
+
* ## Execution Modes
|
|
11
|
+
*
|
|
12
|
+
* Tasks can be organized for parallel or sequential execution:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* // Parallel - all can run simultaneously
|
|
16
|
+
* parallel(
|
|
17
|
+
* task('Design UI'),
|
|
18
|
+
* task('Write API specs'),
|
|
19
|
+
* task('Set up infrastructure'),
|
|
20
|
+
* )
|
|
21
|
+
*
|
|
22
|
+
* // Sequential - must run in order
|
|
23
|
+
* sequential(
|
|
24
|
+
* task('Implement backend'),
|
|
25
|
+
* task('Implement frontend'),
|
|
26
|
+
* task('Integration testing'),
|
|
27
|
+
* )
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* ## Markdown Syntax
|
|
31
|
+
*
|
|
32
|
+
* Tasks map to markdown checklists:
|
|
33
|
+
* - `- [ ]` = Parallel/unordered tasks
|
|
34
|
+
* - `1. [ ]` = Sequential/ordered tasks
|
|
35
|
+
*
|
|
36
|
+
* @packageDocumentation
|
|
37
|
+
*/
|
|
38
|
+
import { createTask as createBaseTask } from './task.js';
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Task DSL Functions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
/**
|
|
43
|
+
* Create a task definition
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* task('Implement feature')
|
|
48
|
+
* task('Review PR', { priority: 'high', assignTo: { type: 'human', id: 'user_123' } })
|
|
49
|
+
* task('Parent task', {
|
|
50
|
+
* subtasks: [
|
|
51
|
+
* task('Subtask 1'),
|
|
52
|
+
* task('Subtask 2'),
|
|
53
|
+
* ]
|
|
54
|
+
* })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function task(title, options) {
|
|
58
|
+
return {
|
|
59
|
+
__type: 'task',
|
|
60
|
+
title,
|
|
61
|
+
...options,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a group of tasks that can run in parallel
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* parallel(
|
|
70
|
+
* task('Design UI'),
|
|
71
|
+
* task('Write API specs'),
|
|
72
|
+
* task('Set up infrastructure'),
|
|
73
|
+
* )
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function parallel(...tasks) {
|
|
77
|
+
return {
|
|
78
|
+
__type: 'parallel',
|
|
79
|
+
tasks,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a group of tasks that must run sequentially
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* sequential(
|
|
88
|
+
* task('Implement backend'),
|
|
89
|
+
* task('Implement frontend'),
|
|
90
|
+
* task('Integration testing'),
|
|
91
|
+
* )
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function sequential(...tasks) {
|
|
95
|
+
return {
|
|
96
|
+
__type: 'sequential',
|
|
97
|
+
tasks,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Project DSL Functions
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Generate a unique project ID
|
|
105
|
+
*/
|
|
106
|
+
function generateProjectId() {
|
|
107
|
+
return `proj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a new project
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const project = createProject({
|
|
115
|
+
* name: 'Launch New Feature',
|
|
116
|
+
* description: 'Ship the new dashboard feature',
|
|
117
|
+
* tasks: [
|
|
118
|
+
* parallel(
|
|
119
|
+
* task('Design mockups'),
|
|
120
|
+
* task('Write technical spec'),
|
|
121
|
+
* ),
|
|
122
|
+
* sequential(
|
|
123
|
+
* task('Implement backend API'),
|
|
124
|
+
* task('Implement frontend UI'),
|
|
125
|
+
* task('Write tests'),
|
|
126
|
+
* task('Deploy to staging'),
|
|
127
|
+
* ),
|
|
128
|
+
* task('QA testing'),
|
|
129
|
+
* task('Deploy to production'),
|
|
130
|
+
* ],
|
|
131
|
+
* })
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function createProject(options) {
|
|
135
|
+
const now = new Date();
|
|
136
|
+
return {
|
|
137
|
+
id: generateProjectId(),
|
|
138
|
+
name: options.name,
|
|
139
|
+
description: options.description,
|
|
140
|
+
status: 'draft',
|
|
141
|
+
tasks: options.tasks || [],
|
|
142
|
+
defaultMode: options.defaultMode || 'sequential',
|
|
143
|
+
owner: options.owner,
|
|
144
|
+
tags: options.tags,
|
|
145
|
+
createdAt: now,
|
|
146
|
+
updatedAt: now,
|
|
147
|
+
metadata: options.metadata,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Workflow Builder (Fluent API)
|
|
152
|
+
// ============================================================================
|
|
153
|
+
/**
|
|
154
|
+
* Workflow builder for fluent task definition
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const workflow = workflow('Feature Launch')
|
|
159
|
+
* .parallel(
|
|
160
|
+
* task('Design'),
|
|
161
|
+
* task('Spec'),
|
|
162
|
+
* )
|
|
163
|
+
* .then(task('Implement'))
|
|
164
|
+
* .then(task('Test'))
|
|
165
|
+
* .parallel(
|
|
166
|
+
* task('Deploy staging'),
|
|
167
|
+
* task('Update docs'),
|
|
168
|
+
* )
|
|
169
|
+
* .then(task('Deploy production'))
|
|
170
|
+
* .build()
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function workflow(name, description) {
|
|
174
|
+
const tasks = [];
|
|
175
|
+
const builder = {
|
|
176
|
+
/**
|
|
177
|
+
* Add tasks that can run in parallel
|
|
178
|
+
*/
|
|
179
|
+
parallel(...nodes) {
|
|
180
|
+
tasks.push(parallel(...nodes));
|
|
181
|
+
return builder;
|
|
182
|
+
},
|
|
183
|
+
/**
|
|
184
|
+
* Add tasks that must run sequentially
|
|
185
|
+
*/
|
|
186
|
+
sequential(...nodes) {
|
|
187
|
+
tasks.push(sequential(...nodes));
|
|
188
|
+
return builder;
|
|
189
|
+
},
|
|
190
|
+
/**
|
|
191
|
+
* Add a single task (sequential with previous)
|
|
192
|
+
*/
|
|
193
|
+
then(...nodes) {
|
|
194
|
+
if (nodes.length === 1) {
|
|
195
|
+
tasks.push(nodes[0]);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
tasks.push(sequential(...nodes));
|
|
199
|
+
}
|
|
200
|
+
return builder;
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Add a task (alias for then)
|
|
204
|
+
*/
|
|
205
|
+
task(title, options) {
|
|
206
|
+
tasks.push(task(title, options));
|
|
207
|
+
return builder;
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Build the project
|
|
211
|
+
*/
|
|
212
|
+
build(options) {
|
|
213
|
+
return createProject({
|
|
214
|
+
name,
|
|
215
|
+
description,
|
|
216
|
+
tasks,
|
|
217
|
+
...options,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
return builder;
|
|
222
|
+
}
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Task Materialization
|
|
225
|
+
// ============================================================================
|
|
226
|
+
/**
|
|
227
|
+
* Flatten task nodes into actual Task objects with dependencies
|
|
228
|
+
*/
|
|
229
|
+
export async function materializeProject(project) {
|
|
230
|
+
const tasks = [];
|
|
231
|
+
let taskIndex = 0;
|
|
232
|
+
async function processNode(node, parentId, previousIds = [], mode = 'sequential') {
|
|
233
|
+
if (node.__type === 'task') {
|
|
234
|
+
const taskDef = node;
|
|
235
|
+
const taskId = `${project.id}_task_${taskIndex++}`;
|
|
236
|
+
// Create dependencies based on mode (as string array for CreateTaskOptions)
|
|
237
|
+
const dependencies = mode === 'sequential' && previousIds.length > 0
|
|
238
|
+
? previousIds
|
|
239
|
+
: undefined;
|
|
240
|
+
// Create a FunctionDefinition from the task definition
|
|
241
|
+
// Default to generative function type for DSL tasks
|
|
242
|
+
const functionDef = {
|
|
243
|
+
type: taskDef.functionType || 'generative',
|
|
244
|
+
name: taskDef.title,
|
|
245
|
+
description: taskDef.description,
|
|
246
|
+
args: {},
|
|
247
|
+
output: 'string',
|
|
248
|
+
};
|
|
249
|
+
const newTask = await createBaseTask({
|
|
250
|
+
function: functionDef,
|
|
251
|
+
priority: taskDef.priority || 'normal',
|
|
252
|
+
assignTo: taskDef.assignTo,
|
|
253
|
+
tags: taskDef.tags,
|
|
254
|
+
parentId,
|
|
255
|
+
projectId: project.id,
|
|
256
|
+
dependencies,
|
|
257
|
+
metadata: {
|
|
258
|
+
...taskDef.metadata,
|
|
259
|
+
_taskNodeIndex: taskIndex - 1,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
newTask.id = taskId;
|
|
263
|
+
tasks.push(newTask);
|
|
264
|
+
// Process subtasks
|
|
265
|
+
if (taskDef.subtasks && taskDef.subtasks.length > 0) {
|
|
266
|
+
let subtaskPrevIds = [];
|
|
267
|
+
for (const subtask of taskDef.subtasks) {
|
|
268
|
+
subtaskPrevIds = await processNode(subtask, taskId, subtaskPrevIds, 'sequential');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return [taskId];
|
|
272
|
+
}
|
|
273
|
+
if (node.__type === 'parallel') {
|
|
274
|
+
const group = node;
|
|
275
|
+
const allIds = [];
|
|
276
|
+
// All tasks in parallel group can start simultaneously
|
|
277
|
+
// They don't depend on each other, only on previousIds
|
|
278
|
+
for (const child of group.tasks) {
|
|
279
|
+
const childIds = await processNode(child, parentId, previousIds, 'parallel');
|
|
280
|
+
allIds.push(...childIds);
|
|
281
|
+
}
|
|
282
|
+
return allIds;
|
|
283
|
+
}
|
|
284
|
+
if (node.__type === 'sequential') {
|
|
285
|
+
const group = node;
|
|
286
|
+
let currentPrevIds = previousIds;
|
|
287
|
+
// Each task depends on the previous one
|
|
288
|
+
for (const child of group.tasks) {
|
|
289
|
+
currentPrevIds = await processNode(child, parentId, currentPrevIds, 'sequential');
|
|
290
|
+
}
|
|
291
|
+
return currentPrevIds;
|
|
292
|
+
}
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
// Process all root-level tasks
|
|
296
|
+
let previousIds = [];
|
|
297
|
+
for (const node of project.tasks) {
|
|
298
|
+
previousIds = await processNode(node, undefined, previousIds, project.defaultMode || 'sequential');
|
|
299
|
+
}
|
|
300
|
+
return { project, tasks };
|
|
301
|
+
}
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Dependency Graph Utilities
|
|
304
|
+
// ============================================================================
|
|
305
|
+
/**
|
|
306
|
+
* Get all tasks that depend on a given task (dependants)
|
|
307
|
+
*/
|
|
308
|
+
export function getDependants(taskId, allTasks) {
|
|
309
|
+
return allTasks.filter(t => t.dependencies?.some(d => d.taskId === taskId && d.type === 'blocked_by'));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get all tasks that a given task depends on (dependencies)
|
|
313
|
+
*/
|
|
314
|
+
export function getDependencies(task, allTasks) {
|
|
315
|
+
if (!task.dependencies)
|
|
316
|
+
return [];
|
|
317
|
+
const depIds = task.dependencies
|
|
318
|
+
.filter(d => d.type === 'blocked_by')
|
|
319
|
+
.map(d => d.taskId);
|
|
320
|
+
return allTasks.filter(t => depIds.includes(t.id));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get tasks that are ready to execute (no unsatisfied dependencies)
|
|
324
|
+
*/
|
|
325
|
+
export function getReadyTasks(allTasks) {
|
|
326
|
+
return allTasks.filter(t => {
|
|
327
|
+
if (t.status !== 'queued' && t.status !== 'pending')
|
|
328
|
+
return false;
|
|
329
|
+
if (!t.dependencies || t.dependencies.length === 0)
|
|
330
|
+
return true;
|
|
331
|
+
return t.dependencies
|
|
332
|
+
.filter(d => d.type === 'blocked_by')
|
|
333
|
+
.every(d => d.satisfied);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Check if a task graph has cycles
|
|
338
|
+
*/
|
|
339
|
+
export function hasCycles(allTasks) {
|
|
340
|
+
const visited = new Set();
|
|
341
|
+
const recStack = new Set();
|
|
342
|
+
function dfs(taskId) {
|
|
343
|
+
visited.add(taskId);
|
|
344
|
+
recStack.add(taskId);
|
|
345
|
+
const task = allTasks.find(t => t.id === taskId);
|
|
346
|
+
if (task?.dependencies) {
|
|
347
|
+
for (const dep of task.dependencies) {
|
|
348
|
+
if (dep.type === 'blocked_by') {
|
|
349
|
+
if (!visited.has(dep.taskId)) {
|
|
350
|
+
if (dfs(dep.taskId))
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
else if (recStack.has(dep.taskId)) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
recStack.delete(taskId);
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
for (const task of allTasks) {
|
|
363
|
+
if (!visited.has(task.id)) {
|
|
364
|
+
if (dfs(task.id))
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Sort tasks by their dependencies (tasks with no dependencies first)
|
|
372
|
+
*/
|
|
373
|
+
export function sortTasks(allTasks) {
|
|
374
|
+
const result = [];
|
|
375
|
+
const visited = new Set();
|
|
376
|
+
function visit(task) {
|
|
377
|
+
if (visited.has(task.id))
|
|
378
|
+
return;
|
|
379
|
+
visited.add(task.id);
|
|
380
|
+
// Visit dependencies first
|
|
381
|
+
if (task.dependencies) {
|
|
382
|
+
for (const dep of task.dependencies) {
|
|
383
|
+
if (dep.type === 'blocked_by') {
|
|
384
|
+
const depTask = allTasks.find(t => t.id === dep.taskId);
|
|
385
|
+
if (depTask)
|
|
386
|
+
visit(depTask);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
result.push(task);
|
|
391
|
+
}
|
|
392
|
+
for (const task of allTasks) {
|
|
393
|
+
visit(task);
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|