digital-tasks 2.0.1 → 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/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
+ }