digital-tasks 2.0.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.ts ADDED
@@ -0,0 +1,571 @@
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
+
39
+ import type {
40
+ Task,
41
+ AnyTask,
42
+ TaskStatus,
43
+ TaskPriority,
44
+ WorkerRef,
45
+ CreateTaskOptions,
46
+ FunctionDefinition,
47
+ } from './types.js'
48
+ import { createTask as createBaseTask } from './task.js'
49
+
50
+ // ============================================================================
51
+ // Execution Mode Types
52
+ // ============================================================================
53
+
54
+ /**
55
+ * How tasks should be executed relative to each other
56
+ */
57
+ export type ExecutionMode = 'parallel' | 'sequential'
58
+
59
+ /**
60
+ * Task node in a workflow - can be a single task or a group
61
+ */
62
+ export type TaskNode =
63
+ | TaskDefinition
64
+ | ParallelGroup
65
+ | SequentialGroup
66
+
67
+ /**
68
+ * Function type for the DSL
69
+ */
70
+ export type FunctionType = 'code' | 'generative' | 'agentic' | 'human'
71
+
72
+ /**
73
+ * A single task definition
74
+ */
75
+ export interface TaskDefinition {
76
+ __type: 'task'
77
+ title: string
78
+ description?: string
79
+ functionType?: FunctionType
80
+ priority?: TaskPriority
81
+ assignTo?: WorkerRef
82
+ tags?: string[]
83
+ subtasks?: TaskNode[]
84
+ metadata?: Record<string, unknown>
85
+ }
86
+
87
+ /**
88
+ * A group of tasks that can run in parallel
89
+ */
90
+ export interface ParallelGroup {
91
+ __type: 'parallel'
92
+ tasks: TaskNode[]
93
+ }
94
+
95
+ /**
96
+ * A group of tasks that must run sequentially
97
+ */
98
+ export interface SequentialGroup {
99
+ __type: 'sequential'
100
+ tasks: TaskNode[]
101
+ }
102
+
103
+ // ============================================================================
104
+ // Project Types
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Project status
109
+ */
110
+ export type ProjectStatus =
111
+ | 'draft'
112
+ | 'active'
113
+ | 'paused'
114
+ | 'completed'
115
+ | 'cancelled'
116
+
117
+ /**
118
+ * Project definition
119
+ */
120
+ export interface Project {
121
+ /** Unique project ID */
122
+ id: string
123
+ /** Project name */
124
+ name: string
125
+ /** Project description */
126
+ description?: string
127
+ /** Project status */
128
+ status: ProjectStatus
129
+ /** Root task nodes */
130
+ tasks: TaskNode[]
131
+ /** Default execution mode for top-level tasks */
132
+ defaultMode?: ExecutionMode
133
+ /** Project owner */
134
+ owner?: WorkerRef
135
+ /** Project tags */
136
+ tags?: string[]
137
+ /** Created timestamp */
138
+ createdAt: Date
139
+ /** Updated timestamp */
140
+ updatedAt: Date
141
+ /** Project metadata */
142
+ metadata?: Record<string, unknown>
143
+ }
144
+
145
+ /**
146
+ * Options for creating a project
147
+ */
148
+ export interface CreateProjectOptions {
149
+ name: string
150
+ description?: string
151
+ tasks?: TaskNode[]
152
+ defaultMode?: ExecutionMode
153
+ owner?: WorkerRef
154
+ tags?: string[]
155
+ metadata?: Record<string, unknown>
156
+ }
157
+
158
+ // ============================================================================
159
+ // Task DSL Functions
160
+ // ============================================================================
161
+
162
+ /**
163
+ * Create a task definition
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * task('Implement feature')
168
+ * task('Review PR', { priority: 'high', assignTo: { type: 'human', id: 'user_123' } })
169
+ * task('Parent task', {
170
+ * subtasks: [
171
+ * task('Subtask 1'),
172
+ * task('Subtask 2'),
173
+ * ]
174
+ * })
175
+ * ```
176
+ */
177
+ export function task(
178
+ title: string,
179
+ options?: Partial<Omit<TaskDefinition, '__type' | 'title'>>
180
+ ): TaskDefinition {
181
+ return {
182
+ __type: 'task',
183
+ title,
184
+ ...options,
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Create a group of tasks that can run in parallel
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * parallel(
194
+ * task('Design UI'),
195
+ * task('Write API specs'),
196
+ * task('Set up infrastructure'),
197
+ * )
198
+ * ```
199
+ */
200
+ export function parallel(...tasks: TaskNode[]): ParallelGroup {
201
+ return {
202
+ __type: 'parallel',
203
+ tasks,
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Create a group of tasks that must run sequentially
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * sequential(
213
+ * task('Implement backend'),
214
+ * task('Implement frontend'),
215
+ * task('Integration testing'),
216
+ * )
217
+ * ```
218
+ */
219
+ export function sequential(...tasks: TaskNode[]): SequentialGroup {
220
+ return {
221
+ __type: 'sequential',
222
+ tasks,
223
+ }
224
+ }
225
+
226
+ // ============================================================================
227
+ // Project DSL Functions
228
+ // ============================================================================
229
+
230
+ /**
231
+ * Generate a unique project ID
232
+ */
233
+ function generateProjectId(): string {
234
+ return `proj_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
235
+ }
236
+
237
+ /**
238
+ * Create a new project
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const project = createProject({
243
+ * name: 'Launch New Feature',
244
+ * description: 'Ship the new dashboard feature',
245
+ * tasks: [
246
+ * parallel(
247
+ * task('Design mockups'),
248
+ * task('Write technical spec'),
249
+ * ),
250
+ * sequential(
251
+ * task('Implement backend API'),
252
+ * task('Implement frontend UI'),
253
+ * task('Write tests'),
254
+ * task('Deploy to staging'),
255
+ * ),
256
+ * task('QA testing'),
257
+ * task('Deploy to production'),
258
+ * ],
259
+ * })
260
+ * ```
261
+ */
262
+ export function createProject(options: CreateProjectOptions): Project {
263
+ const now = new Date()
264
+ return {
265
+ id: generateProjectId(),
266
+ name: options.name,
267
+ description: options.description,
268
+ status: 'draft',
269
+ tasks: options.tasks || [],
270
+ defaultMode: options.defaultMode || 'sequential',
271
+ owner: options.owner,
272
+ tags: options.tags,
273
+ createdAt: now,
274
+ updatedAt: now,
275
+ metadata: options.metadata,
276
+ }
277
+ }
278
+
279
+ // ============================================================================
280
+ // Workflow Builder (Fluent API)
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Workflow builder for fluent task definition
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * const workflow = workflow('Feature Launch')
289
+ * .parallel(
290
+ * task('Design'),
291
+ * task('Spec'),
292
+ * )
293
+ * .then(task('Implement'))
294
+ * .then(task('Test'))
295
+ * .parallel(
296
+ * task('Deploy staging'),
297
+ * task('Update docs'),
298
+ * )
299
+ * .then(task('Deploy production'))
300
+ * .build()
301
+ * ```
302
+ */
303
+ export function workflow(name: string, description?: string) {
304
+ const tasks: TaskNode[] = []
305
+
306
+ const builder = {
307
+ /**
308
+ * Add tasks that can run in parallel
309
+ */
310
+ parallel(...nodes: TaskNode[]) {
311
+ tasks.push(parallel(...nodes))
312
+ return builder
313
+ },
314
+
315
+ /**
316
+ * Add tasks that must run sequentially
317
+ */
318
+ sequential(...nodes: TaskNode[]) {
319
+ tasks.push(sequential(...nodes))
320
+ return builder
321
+ },
322
+
323
+ /**
324
+ * Add a single task (sequential with previous)
325
+ */
326
+ then(...nodes: TaskNode[]) {
327
+ if (nodes.length === 1) {
328
+ tasks.push(nodes[0])
329
+ } else {
330
+ tasks.push(sequential(...nodes))
331
+ }
332
+ return builder
333
+ },
334
+
335
+ /**
336
+ * Add a task (alias for then)
337
+ */
338
+ task(title: string, options?: Partial<Omit<TaskDefinition, '__type' | 'title'>>) {
339
+ tasks.push(task(title, options))
340
+ return builder
341
+ },
342
+
343
+ /**
344
+ * Build the project
345
+ */
346
+ build(options?: Partial<Omit<CreateProjectOptions, 'name' | 'tasks'>>): Project {
347
+ return createProject({
348
+ name,
349
+ description,
350
+ tasks,
351
+ ...options,
352
+ })
353
+ },
354
+ }
355
+
356
+ return builder
357
+ }
358
+
359
+ // ============================================================================
360
+ // Task Materialization
361
+ // ============================================================================
362
+
363
+ /**
364
+ * Flatten task nodes into actual Task objects with dependencies
365
+ */
366
+ export async function materializeProject(
367
+ project: Project
368
+ ): Promise<{ project: Project; tasks: AnyTask[] }> {
369
+ const tasks: AnyTask[] = []
370
+ let taskIndex = 0
371
+
372
+ async function processNode(
373
+ node: TaskNode,
374
+ parentId?: string,
375
+ previousIds: string[] = [],
376
+ mode: ExecutionMode = 'sequential'
377
+ ): Promise<string[]> {
378
+ if (node.__type === 'task') {
379
+ const taskDef = node as TaskDefinition
380
+ const taskId = `${project.id}_task_${taskIndex++}`
381
+
382
+ // Create dependencies based on mode (as string array for CreateTaskOptions)
383
+ const dependencies = mode === 'sequential' && previousIds.length > 0
384
+ ? previousIds
385
+ : undefined
386
+
387
+ // Create a FunctionDefinition from the task definition
388
+ // Default to generative function type for DSL tasks
389
+ const functionDef = {
390
+ type: taskDef.functionType || 'generative',
391
+ name: taskDef.title,
392
+ description: taskDef.description,
393
+ args: {},
394
+ output: 'string',
395
+ } as FunctionDefinition
396
+
397
+ const newTask = await createBaseTask({
398
+ function: functionDef,
399
+ priority: taskDef.priority || 'normal',
400
+ assignTo: taskDef.assignTo,
401
+ tags: taskDef.tags,
402
+ parentId,
403
+ projectId: project.id,
404
+ dependencies,
405
+ metadata: {
406
+ ...taskDef.metadata,
407
+ _taskNodeIndex: taskIndex - 1,
408
+ },
409
+ })
410
+
411
+ // Override the generated ID with our predictable one
412
+ ;(newTask as AnyTask).id = taskId
413
+ tasks.push(newTask as AnyTask)
414
+
415
+ // Process subtasks
416
+ if (taskDef.subtasks && taskDef.subtasks.length > 0) {
417
+ let subtaskPrevIds: string[] = []
418
+ for (const subtask of taskDef.subtasks) {
419
+ subtaskPrevIds = await processNode(subtask, taskId, subtaskPrevIds, 'sequential')
420
+ }
421
+ }
422
+
423
+ return [taskId]
424
+ }
425
+
426
+ if (node.__type === 'parallel') {
427
+ const group = node as ParallelGroup
428
+ const allIds: string[] = []
429
+
430
+ // All tasks in parallel group can start simultaneously
431
+ // They don't depend on each other, only on previousIds
432
+ for (const child of group.tasks) {
433
+ const childIds = await processNode(child, parentId, previousIds, 'parallel')
434
+ allIds.push(...childIds)
435
+ }
436
+
437
+ return allIds
438
+ }
439
+
440
+ if (node.__type === 'sequential') {
441
+ const group = node as SequentialGroup
442
+ let currentPrevIds = previousIds
443
+
444
+ // Each task depends on the previous one
445
+ for (const child of group.tasks) {
446
+ currentPrevIds = await processNode(child, parentId, currentPrevIds, 'sequential')
447
+ }
448
+
449
+ return currentPrevIds
450
+ }
451
+
452
+ return []
453
+ }
454
+
455
+ // Process all root-level tasks
456
+ let previousIds: string[] = []
457
+ for (const node of project.tasks) {
458
+ previousIds = await processNode(node, undefined, previousIds, project.defaultMode || 'sequential')
459
+ }
460
+
461
+ return { project, tasks }
462
+ }
463
+
464
+ // ============================================================================
465
+ // Dependency Graph Utilities
466
+ // ============================================================================
467
+
468
+ /**
469
+ * Get all tasks that depend on a given task (dependants)
470
+ */
471
+ export function getDependants(taskId: string, allTasks: AnyTask[]): AnyTask[] {
472
+ return allTasks.filter(t =>
473
+ t.dependencies?.some(d => d.taskId === taskId && d.type === 'blocked_by')
474
+ )
475
+ }
476
+
477
+ /**
478
+ * Get all tasks that a given task depends on (dependencies)
479
+ */
480
+ export function getDependencies(task: AnyTask, allTasks: AnyTask[]): AnyTask[] {
481
+ if (!task.dependencies) return []
482
+
483
+ const depIds = task.dependencies
484
+ .filter(d => d.type === 'blocked_by')
485
+ .map(d => d.taskId)
486
+
487
+ return allTasks.filter(t => depIds.includes(t.id))
488
+ }
489
+
490
+ /**
491
+ * Get tasks that are ready to execute (no unsatisfied dependencies)
492
+ */
493
+ export function getReadyTasks(allTasks: AnyTask[]): AnyTask[] {
494
+ return allTasks.filter(t => {
495
+ if (t.status !== 'queued' && t.status !== 'pending') return false
496
+
497
+ if (!t.dependencies || t.dependencies.length === 0) return true
498
+
499
+ return t.dependencies
500
+ .filter(d => d.type === 'blocked_by')
501
+ .every(d => d.satisfied)
502
+ })
503
+ }
504
+
505
+ /**
506
+ * Check if a task graph has cycles
507
+ */
508
+ export function hasCycles(allTasks: AnyTask[]): boolean {
509
+ const visited = new Set<string>()
510
+ const recStack = new Set<string>()
511
+
512
+ function dfs(taskId: string): boolean {
513
+ visited.add(taskId)
514
+ recStack.add(taskId)
515
+
516
+ const task = allTasks.find(t => t.id === taskId)
517
+ if (task?.dependencies) {
518
+ for (const dep of task.dependencies) {
519
+ if (dep.type === 'blocked_by') {
520
+ if (!visited.has(dep.taskId)) {
521
+ if (dfs(dep.taskId)) return true
522
+ } else if (recStack.has(dep.taskId)) {
523
+ return true
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ recStack.delete(taskId)
530
+ return false
531
+ }
532
+
533
+ for (const task of allTasks) {
534
+ if (!visited.has(task.id)) {
535
+ if (dfs(task.id)) return true
536
+ }
537
+ }
538
+
539
+ return false
540
+ }
541
+
542
+ /**
543
+ * Sort tasks by their dependencies (tasks with no dependencies first)
544
+ */
545
+ export function sortTasks(allTasks: AnyTask[]): AnyTask[] {
546
+ const result: AnyTask[] = []
547
+ const visited = new Set<string>()
548
+
549
+ function visit(task: AnyTask) {
550
+ if (visited.has(task.id)) return
551
+ visited.add(task.id)
552
+
553
+ // Visit dependencies first
554
+ if (task.dependencies) {
555
+ for (const dep of task.dependencies) {
556
+ if (dep.type === 'blocked_by') {
557
+ const depTask = allTasks.find(t => t.id === dep.taskId)
558
+ if (depTask) visit(depTask)
559
+ }
560
+ }
561
+ }
562
+
563
+ result.push(task)
564
+ }
565
+
566
+ for (const task of allTasks) {
567
+ visit(task)
568
+ }
569
+
570
+ return result
571
+ }