@task-mcp/shared 1.0.21 → 1.0.23
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 +122 -0
- package/package.json +1 -1
- package/src/algorithms/critical-path.ts +43 -38
- package/src/algorithms/dependency-integrity.ts +9 -15
- package/src/algorithms/tech-analysis.ts +27 -27
- package/src/algorithms/topological-sort.ts +71 -26
- package/src/schemas/index.ts +2 -13
- package/src/schemas/response-format.ts +6 -6
- package/src/schemas/response-schema.ts +25 -20
- package/src/schemas/task.ts +4 -22
- package/src/utils/dashboard-renderer.ts +27 -59
- package/src/utils/date.ts +2 -10
- package/src/utils/hierarchy.ts +4 -5
- package/src/utils/index.ts +12 -6
- package/src/utils/natural-language.ts +210 -83
- package/src/utils/projection.ts +8 -8
- package/src/utils/terminal-ui.ts +53 -54
- package/src/utils/workspace.ts +16 -4
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @task-mcp/shared
|
|
2
|
+
|
|
3
|
+
Core algorithms, schemas, and utilities for task-mcp. Zero MCP dependency - can be used standalone.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @task-mcp/shared
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Modules
|
|
12
|
+
|
|
13
|
+
### Algorithms
|
|
14
|
+
|
|
15
|
+
Graph algorithms for task dependency management.
|
|
16
|
+
|
|
17
|
+
| Module | Description |
|
|
18
|
+
|--------|-------------|
|
|
19
|
+
| `critical-path` | CPM (Critical Path Method) - finds longest path, parallel tasks, bottlenecks |
|
|
20
|
+
| `topological-sort` | Kahn's algorithm for dependency ordering with cycle detection |
|
|
21
|
+
| `dependency-integrity` | Validates dependency graph integrity (cycles, orphans, self-refs) |
|
|
22
|
+
| `tech-analysis` | Analyzes tasks for technology stack patterns |
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
findCriticalPath,
|
|
27
|
+
findParallelTasks,
|
|
28
|
+
detectBottlenecks,
|
|
29
|
+
topologicalSort,
|
|
30
|
+
validateDependencies
|
|
31
|
+
} from "@task-mcp/shared";
|
|
32
|
+
|
|
33
|
+
// Find critical path through task graph
|
|
34
|
+
const { criticalPath, totalDuration } = findCriticalPath(tasks);
|
|
35
|
+
|
|
36
|
+
// Find tasks that can run in parallel
|
|
37
|
+
const parallelGroups = findParallelTasks(tasks);
|
|
38
|
+
|
|
39
|
+
// Topological sort with cycle detection
|
|
40
|
+
const { sorted, hasCycle } = topologicalSort(tasks);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Schemas
|
|
44
|
+
|
|
45
|
+
Zod schemas for validation and type inference.
|
|
46
|
+
|
|
47
|
+
| Schema | Description |
|
|
48
|
+
|--------|-------------|
|
|
49
|
+
| `Task` | Task with status, priority, dependencies, hierarchy |
|
|
50
|
+
| `InboxItem` | Quick capture items pending triage |
|
|
51
|
+
| `ResponseFormat` | `concise` / `standard` / `detailed` output modes |
|
|
52
|
+
| `View` | Filtered task views (today, blocked, quick-wins) |
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { TaskSchema, Task, Priority, Status } from "@task-mcp/shared";
|
|
56
|
+
|
|
57
|
+
// Validate and parse
|
|
58
|
+
const task = TaskSchema.parse(rawData);
|
|
59
|
+
|
|
60
|
+
// Type inference
|
|
61
|
+
type Task = z.infer<typeof TaskSchema>;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Utilities
|
|
65
|
+
|
|
66
|
+
| Utility | Description |
|
|
67
|
+
|---------|-------------|
|
|
68
|
+
| `natural-language` | Parse "task !high @context #tag due:tomorrow" syntax |
|
|
69
|
+
| `workspace` | Auto-detect workspace from git repository name |
|
|
70
|
+
| `projection` | Format tasks to concise/standard/detailed (70-88% token savings) |
|
|
71
|
+
| `hierarchy` | Build parent-child tree, find descendants/ancestors |
|
|
72
|
+
| `date` | Parse relative dates ("tomorrow", "next week", "in 3 days") |
|
|
73
|
+
| `id` | Generate collision-resistant IDs (`task_xxxx`, `inbox_xxxx`) |
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
parseNaturalLanguage,
|
|
78
|
+
detectWorkspace,
|
|
79
|
+
formatTask,
|
|
80
|
+
buildHierarchy
|
|
81
|
+
} from "@task-mcp/shared";
|
|
82
|
+
|
|
83
|
+
// Parse natural language input
|
|
84
|
+
const parsed = parseNaturalLanguage("Review PR !high @work #urgent due:tomorrow");
|
|
85
|
+
// { title: "Review PR", priority: "high", contexts: ["work"], tags: ["urgent"], dueDate: "2024-01-02" }
|
|
86
|
+
|
|
87
|
+
// Detect workspace from git
|
|
88
|
+
const workspace = await detectWorkspace(); // "task-mcp"
|
|
89
|
+
|
|
90
|
+
// Format for token efficiency
|
|
91
|
+
const concise = formatTask(task, "concise");
|
|
92
|
+
// { id, title, status, priority } - minimal fields
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
shared/
|
|
99
|
+
├── algorithms/ # Graph algorithms (no external deps)
|
|
100
|
+
│ ├── critical-path.ts
|
|
101
|
+
│ ├── topological-sort.ts
|
|
102
|
+
│ └── dependency-integrity.ts
|
|
103
|
+
├── schemas/ # Zod schemas + type exports
|
|
104
|
+
│ ├── task.ts
|
|
105
|
+
│ ├── inbox.ts
|
|
106
|
+
│ └── response-format.ts
|
|
107
|
+
└── utils/ # Pure utility functions
|
|
108
|
+
├── natural-language.ts
|
|
109
|
+
├── workspace.ts
|
|
110
|
+
└── projection.ts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Design Principles
|
|
114
|
+
|
|
115
|
+
1. **Zero MCP dependency** - Can be used in any TypeScript project
|
|
116
|
+
2. **Zod-first validation** - Runtime safety with static types
|
|
117
|
+
3. **Algorithm efficiency** - O(n+e) graph operations
|
|
118
|
+
4. **Token optimization** - Projection system reduces LLM token usage by 70-88%
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { Task } from "../schemas/task.js";
|
|
2
|
-
import {
|
|
3
|
-
topologicalSort,
|
|
4
|
-
findDependents,
|
|
5
|
-
priorityToNumber,
|
|
6
|
-
} from "./topological-sort.js";
|
|
2
|
+
import { topologicalSort, priorityToNumber } from "./topological-sort.js";
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Task with computed CPM (Critical Path Method) values
|
|
@@ -43,9 +39,7 @@ function getTaskDuration(task: Task): number {
|
|
|
43
39
|
* Get blocked_by dependencies for a task
|
|
44
40
|
*/
|
|
45
41
|
function getBlockedByDeps(task: Task): string[] {
|
|
46
|
-
return (task.dependencies ?? [])
|
|
47
|
-
.filter((d) => d.type === "blocked_by")
|
|
48
|
-
.map((d) => d.taskId);
|
|
42
|
+
return (task.dependencies ?? []).filter((d) => d.type === "blocked_by").map((d) => d.taskId);
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
/**
|
|
@@ -110,7 +104,10 @@ function calculateProjectDuration(taskMap: Map<string, CPMTask>): number {
|
|
|
110
104
|
* This is O(n * d) where d is average dependencies, done once upfront
|
|
111
105
|
* Allows O(1) successor lookup instead of O(n) per task
|
|
112
106
|
*/
|
|
113
|
-
function buildSuccessorIndex(
|
|
107
|
+
function buildSuccessorIndex(
|
|
108
|
+
sortedTasks: Task[],
|
|
109
|
+
taskMap: Map<string, CPMTask>
|
|
110
|
+
): Map<string, CPMTask[]> {
|
|
114
111
|
const successorIndex = new Map<string, CPMTask[]>();
|
|
115
112
|
|
|
116
113
|
// Initialize empty arrays for all tasks
|
|
@@ -244,9 +241,7 @@ function extractCriticalPath(sortedTasks: Task[], taskMap: Map<string, CPMTask>)
|
|
|
244
241
|
* Find top bottlenecks (critical tasks blocking the most downstream work)
|
|
245
242
|
*/
|
|
246
243
|
function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
|
|
247
|
-
return [...criticalPath]
|
|
248
|
-
.sort((a, b) => b.dependentCount - a.dependentCount)
|
|
249
|
-
.slice(0, limit);
|
|
244
|
+
return [...criticalPath].sort((a, b) => b.dependentCount - a.dependentCount).slice(0, limit);
|
|
250
245
|
}
|
|
251
246
|
|
|
252
247
|
/**
|
|
@@ -262,9 +257,7 @@ function findBottlenecks(criticalPath: CPMTask[], limit = 5): CPMTask[] {
|
|
|
262
257
|
*/
|
|
263
258
|
export function criticalPathAnalysis(tasks: Task[]): CPMResult {
|
|
264
259
|
// Filter to only pending/in_progress tasks
|
|
265
|
-
const activeTasks = tasks.filter(
|
|
266
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
267
|
-
);
|
|
260
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
268
261
|
|
|
269
262
|
if (activeTasks.length === 0) {
|
|
270
263
|
return {
|
|
@@ -315,18 +308,15 @@ export function criticalPathAnalysis(tasks: Task[]): CPMResult {
|
|
|
315
308
|
|
|
316
309
|
/**
|
|
317
310
|
* Find tasks that can be executed in parallel (no dependencies between them)
|
|
311
|
+
* Optimized to O(n + e) where n = number of tasks, e = number of dependencies
|
|
318
312
|
*/
|
|
319
313
|
export function findParallelTasks(tasks: Task[]): Task[][] {
|
|
320
|
-
const activeTasks = tasks.filter(
|
|
321
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
322
|
-
);
|
|
314
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
323
315
|
|
|
324
316
|
if (activeTasks.length === 0) return [];
|
|
325
317
|
|
|
326
318
|
// Find tasks with no uncompleted dependencies
|
|
327
|
-
const completedIds = new Set(
|
|
328
|
-
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
329
|
-
);
|
|
319
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
330
320
|
|
|
331
321
|
const available = activeTasks.filter((task) => {
|
|
332
322
|
const deps = getBlockedByDeps(task);
|
|
@@ -335,7 +325,26 @@ export function findParallelTasks(tasks: Task[]): Task[][] {
|
|
|
335
325
|
|
|
336
326
|
if (available.length <= 1) return [available];
|
|
337
327
|
|
|
338
|
-
//
|
|
328
|
+
// Build dependency Sets for O(1) lookup - O(n + e) total
|
|
329
|
+
const dependsOn = new Map<string, Set<string>>();
|
|
330
|
+
for (const task of available) {
|
|
331
|
+
const deps = (task.dependencies ?? []).map((d) => d.taskId);
|
|
332
|
+
dependsOn.set(task.id, new Set(deps));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Build reverse dependency index (who depends on me) - O(e) total
|
|
336
|
+
const dependedBy = new Map<string, Set<string>>();
|
|
337
|
+
for (const task of available) {
|
|
338
|
+
dependedBy.set(task.id, new Set());
|
|
339
|
+
}
|
|
340
|
+
for (const task of available) {
|
|
341
|
+
const deps = dependsOn.get(task.id) ?? new Set();
|
|
342
|
+
for (const depId of deps) {
|
|
343
|
+
dependedBy.get(depId)?.add(task.id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Group tasks that don't depend on each other - O(n + e)
|
|
339
348
|
const groups: Task[][] = [];
|
|
340
349
|
const processed = new Set<string>();
|
|
341
350
|
|
|
@@ -345,15 +354,18 @@ export function findParallelTasks(tasks: Task[]): Task[][] {
|
|
|
345
354
|
const group: Task[] = [task];
|
|
346
355
|
processed.add(task.id);
|
|
347
356
|
|
|
357
|
+
// Get all tasks that this task depends on or that depend on this task
|
|
358
|
+
const conflicting = new Set<string>();
|
|
359
|
+
const taskDeps = dependsOn.get(task.id) ?? new Set();
|
|
360
|
+
const taskDependedBy = dependedBy.get(task.id) ?? new Set();
|
|
361
|
+
for (const id of taskDeps) conflicting.add(id);
|
|
362
|
+
for (const id of taskDependedBy) conflicting.add(id);
|
|
363
|
+
|
|
348
364
|
for (const other of available) {
|
|
349
365
|
if (processed.has(other.id)) continue;
|
|
350
366
|
|
|
351
|
-
//
|
|
352
|
-
const
|
|
353
|
-
const otherDeps = (other.dependencies ?? []).map((d) => d.taskId);
|
|
354
|
-
|
|
355
|
-
const independent =
|
|
356
|
-
!taskDeps.includes(other.id) && !otherDeps.includes(task.id);
|
|
367
|
+
// O(1) check: tasks are independent if neither depends on the other
|
|
368
|
+
const independent = !conflicting.has(other.id) && !dependsOn.get(other.id)?.has(task.id);
|
|
357
369
|
|
|
358
370
|
if (independent) {
|
|
359
371
|
group.push(other);
|
|
@@ -377,9 +389,7 @@ export function suggestNextTask(
|
|
|
377
389
|
maxMinutes?: number;
|
|
378
390
|
} = {}
|
|
379
391
|
): Task | null {
|
|
380
|
-
const activeTasks = tasks.filter(
|
|
381
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
382
|
-
);
|
|
392
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
383
393
|
|
|
384
394
|
if (activeTasks.length === 0) return null;
|
|
385
395
|
|
|
@@ -387,9 +397,7 @@ export function suggestNextTask(
|
|
|
387
397
|
const cpm = criticalPathAnalysis(tasks);
|
|
388
398
|
|
|
389
399
|
// Filter by availability (all dependencies completed)
|
|
390
|
-
const completedIds = new Set(
|
|
391
|
-
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
392
|
-
);
|
|
400
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
393
401
|
|
|
394
402
|
let candidates = cpm.tasks.filter((task) => {
|
|
395
403
|
const deps = getBlockedByDeps(task);
|
|
@@ -409,9 +417,7 @@ export function suggestNextTask(
|
|
|
409
417
|
|
|
410
418
|
// Filter by time if specified
|
|
411
419
|
if (options.maxMinutes) {
|
|
412
|
-
const timeFiltered = candidates.filter(
|
|
413
|
-
(t) => getTaskDuration(t) <= options.maxMinutes!
|
|
414
|
-
);
|
|
420
|
+
const timeFiltered = candidates.filter((t) => getTaskDuration(t) <= options.maxMinutes!);
|
|
415
421
|
if (timeFiltered.length > 0) {
|
|
416
422
|
candidates = timeFiltered;
|
|
417
423
|
}
|
|
@@ -434,4 +440,3 @@ export function suggestNextTask(
|
|
|
434
440
|
scored.sort((a, b) => b.score - a.score);
|
|
435
441
|
return scored[0]?.task ?? null;
|
|
436
442
|
}
|
|
437
|
-
|
|
@@ -35,9 +35,7 @@ export interface ValidateDependencyOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* Validates if a dependency can be added between two tasks
|
|
37
37
|
*/
|
|
38
|
-
export function validateDependency(
|
|
39
|
-
options: ValidateDependencyOptions
|
|
40
|
-
): DependencyValidationResult {
|
|
38
|
+
export function validateDependency(options: ValidateDependencyOptions): DependencyValidationResult {
|
|
41
39
|
const { taskId, blockedBy, tasks, checkDuplicates = true } = options;
|
|
42
40
|
|
|
43
41
|
// 1. Self-dependency check
|
|
@@ -50,8 +48,11 @@ export function validateDependency(
|
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
50
|
|
|
51
|
+
// Build taskMap once for O(1) lookups
|
|
52
|
+
const taskMap = new Map(tasks.map((t) => [t.id, t]));
|
|
53
|
+
|
|
53
54
|
// 2. Task existence check
|
|
54
|
-
const task =
|
|
55
|
+
const task = taskMap.get(taskId);
|
|
55
56
|
if (!task) {
|
|
56
57
|
return {
|
|
57
58
|
valid: false,
|
|
@@ -62,7 +63,7 @@ export function validateDependency(
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
// 3. Blocker task existence check
|
|
65
|
-
const blocker =
|
|
66
|
+
const blocker = taskMap.get(blockedBy);
|
|
66
67
|
if (!blocker) {
|
|
67
68
|
return {
|
|
68
69
|
valid: false,
|
|
@@ -114,9 +115,7 @@ export interface InvalidDependencyReference {
|
|
|
114
115
|
/**
|
|
115
116
|
* Find all invalid dependency references (orphaned references)
|
|
116
117
|
*/
|
|
117
|
-
export function findInvalidDependencies(
|
|
118
|
-
tasks: Task[]
|
|
119
|
-
): InvalidDependencyReference[] {
|
|
118
|
+
export function findInvalidDependencies(tasks: Task[]): InvalidDependencyReference[] {
|
|
120
119
|
const taskIds = new Set(tasks.map((t) => t.id));
|
|
121
120
|
const invalid: InvalidDependencyReference[] = [];
|
|
122
121
|
|
|
@@ -239,17 +238,12 @@ export interface DependencyIntegrityReport {
|
|
|
239
238
|
/**
|
|
240
239
|
* Check overall dependency integrity of a project
|
|
241
240
|
*/
|
|
242
|
-
export function checkDependencyIntegrity(
|
|
243
|
-
tasks: Task[]
|
|
244
|
-
): DependencyIntegrityReport {
|
|
241
|
+
export function checkDependencyIntegrity(tasks: Task[]): DependencyIntegrityReport {
|
|
245
242
|
const selfDeps = findSelfDependencies(tasks);
|
|
246
243
|
const invalidRefs = findInvalidDependencies(tasks);
|
|
247
244
|
const cycles = detectCircularDependencies(tasks);
|
|
248
245
|
|
|
249
|
-
const totalDependencies = tasks.reduce(
|
|
250
|
-
(sum, t) => sum + (t.dependencies?.length ?? 0),
|
|
251
|
-
0
|
|
252
|
-
);
|
|
246
|
+
const totalDependencies = tasks.reduce((sum, t) => sum + (t.dependencies?.length ?? 0), 0);
|
|
253
247
|
|
|
254
248
|
const issueCount = selfDeps.length + invalidRefs.length + cycles.length;
|
|
255
249
|
const valid = issueCount === 0;
|
|
@@ -5,14 +5,14 @@ import type { Task, TechArea, RiskLevel } from "../schemas/task.js";
|
|
|
5
5
|
* Based on dependency flow: DB changes → Infrastructure → Backend → Frontend → Tests
|
|
6
6
|
*/
|
|
7
7
|
const TECH_ORDER: Record<TechArea, number> = {
|
|
8
|
-
schema: 0,
|
|
9
|
-
infra: 0,
|
|
10
|
-
devops: 1,
|
|
11
|
-
backend: 2,
|
|
12
|
-
frontend: 3,
|
|
13
|
-
test: 4,
|
|
14
|
-
docs: 4,
|
|
15
|
-
refactor: 5,
|
|
8
|
+
schema: 0, // DB/schema changes first
|
|
9
|
+
infra: 0, // Infrastructure setup
|
|
10
|
+
devops: 1, // CI/CD pipelines
|
|
11
|
+
backend: 2, // API/server
|
|
12
|
+
frontend: 3, // UI
|
|
13
|
+
test: 4, // Tests
|
|
14
|
+
docs: 4, // Documentation
|
|
15
|
+
refactor: 5, // Refactoring last
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -143,9 +143,7 @@ function hasBreakingChange(task: Task): boolean {
|
|
|
143
143
|
*/
|
|
144
144
|
export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
|
|
145
145
|
// Filter to active tasks only
|
|
146
|
-
const activeTasks = tasks.filter(
|
|
147
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
148
|
-
);
|
|
146
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
149
147
|
|
|
150
148
|
if (activeTasks.length === 0) {
|
|
151
149
|
return {
|
|
@@ -175,7 +173,10 @@ export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
|
|
|
175
173
|
if (aBreaking !== bBreaking) return aBreaking - bBreaking;
|
|
176
174
|
|
|
177
175
|
// 4. Priority as tiebreaker (higher priority first)
|
|
178
|
-
return (
|
|
176
|
+
return (
|
|
177
|
+
(PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) -
|
|
178
|
+
(PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER)
|
|
179
|
+
);
|
|
179
180
|
});
|
|
180
181
|
|
|
181
182
|
// Group into phases by tech level
|
|
@@ -258,9 +259,7 @@ function getPrimaryArea(task: Task): TechArea | "mixed" {
|
|
|
258
259
|
*/
|
|
259
260
|
export function findBreakingChanges(tasks: Task[]): Task[] {
|
|
260
261
|
return tasks.filter(
|
|
261
|
-
(t) =>
|
|
262
|
-
(t.status === "pending" || t.status === "in_progress") &&
|
|
263
|
-
hasBreakingChange(t)
|
|
262
|
+
(t) => (t.status === "pending" || t.status === "in_progress") && hasBreakingChange(t)
|
|
264
263
|
);
|
|
265
264
|
}
|
|
266
265
|
|
|
@@ -284,16 +283,21 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
|
|
|
284
283
|
|
|
285
284
|
// Initialize all groups
|
|
286
285
|
const allAreas: TechArea[] = [
|
|
287
|
-
"schema",
|
|
286
|
+
"schema",
|
|
287
|
+
"infra",
|
|
288
|
+
"devops",
|
|
289
|
+
"backend",
|
|
290
|
+
"frontend",
|
|
291
|
+
"test",
|
|
292
|
+
"docs",
|
|
293
|
+
"refactor",
|
|
288
294
|
];
|
|
289
295
|
for (const area of allAreas) {
|
|
290
296
|
groups.set(area, []);
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
// Group active tasks
|
|
294
|
-
const activeTasks = tasks.filter(
|
|
295
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
296
|
-
);
|
|
300
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
297
301
|
|
|
298
302
|
for (const task of activeTasks) {
|
|
299
303
|
const areas = task.techStack?.areas ?? [];
|
|
@@ -314,9 +318,9 @@ export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
|
|
|
314
318
|
* Complexity distribution by level
|
|
315
319
|
*/
|
|
316
320
|
export interface ComplexityDistribution {
|
|
317
|
-
low: number;
|
|
321
|
+
low: number; // 1 to LOW_MAX
|
|
318
322
|
medium: number; // LOW_MAX+1 to MEDIUM_MAX
|
|
319
|
-
high: number;
|
|
323
|
+
high: number; // MEDIUM_MAX+1 to 10
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
/**
|
|
@@ -337,9 +341,7 @@ export interface ComplexitySummary {
|
|
|
337
341
|
* Analyze complexity distribution across tasks
|
|
338
342
|
*/
|
|
339
343
|
export function getComplexitySummary(tasks: Task[]): ComplexitySummary {
|
|
340
|
-
const activeTasks = tasks.filter(
|
|
341
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
342
|
-
);
|
|
344
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
343
345
|
|
|
344
346
|
const analyzed = activeTasks.filter((t) => t.complexity?.score !== undefined);
|
|
345
347
|
const unanalyzed = activeTasks.filter((t) => t.complexity?.score === undefined);
|
|
@@ -394,9 +396,7 @@ export interface TechStackSummary {
|
|
|
394
396
|
* Analyze tech stack distribution across tasks
|
|
395
397
|
*/
|
|
396
398
|
export function getTechStackSummary(tasks: Task[]): TechStackSummary {
|
|
397
|
-
const activeTasks = tasks.filter(
|
|
398
|
-
(t) => t.status === "pending" || t.status === "in_progress"
|
|
399
|
-
);
|
|
399
|
+
const activeTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
400
400
|
|
|
401
401
|
const analyzed = activeTasks.filter((t) => t.techStack?.areas !== undefined);
|
|
402
402
|
const unanalyzed = activeTasks.filter((t) => t.techStack?.areas === undefined);
|
|
@@ -115,20 +115,13 @@ export function topologicalSort(tasks: Task[]): Task[] {
|
|
|
115
115
|
/**
|
|
116
116
|
* Detect if adding a dependency would create a cycle
|
|
117
117
|
*/
|
|
118
|
-
export function wouldCreateCycle(
|
|
119
|
-
tasks: Task[],
|
|
120
|
-
fromId: string,
|
|
121
|
-
toId: string
|
|
122
|
-
): boolean {
|
|
118
|
+
export function wouldCreateCycle(tasks: Task[], fromId: string, toId: string): boolean {
|
|
123
119
|
// Create a temporary task list with the new dependency
|
|
124
120
|
const tempTasks = tasks.map((t) => {
|
|
125
121
|
if (t.id === fromId) {
|
|
126
122
|
return {
|
|
127
123
|
...t,
|
|
128
|
-
dependencies: [
|
|
129
|
-
...(t.dependencies ?? []),
|
|
130
|
-
{ taskId: toId, type: "blocked_by" as const },
|
|
131
|
-
],
|
|
124
|
+
dependencies: [...(t.dependencies ?? []), { taskId: toId, type: "blocked_by" as const }],
|
|
132
125
|
};
|
|
133
126
|
}
|
|
134
127
|
return t;
|
|
@@ -142,23 +135,76 @@ export function wouldCreateCycle(
|
|
|
142
135
|
}
|
|
143
136
|
}
|
|
144
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Build inverted indices for efficient dependency lookups
|
|
140
|
+
* Returns:
|
|
141
|
+
* - dependentsIndex: taskId -> [tasks that depend on this task]
|
|
142
|
+
* - dependenciesIndex: taskId -> [tasks this task depends on]
|
|
143
|
+
*
|
|
144
|
+
* Time complexity: O(n + e) where n = tasks, e = edges
|
|
145
|
+
*/
|
|
146
|
+
export function buildDependencyIndices(tasks: Task[]): {
|
|
147
|
+
taskMap: Map<string, Task>;
|
|
148
|
+
dependentsIndex: Map<string, string[]>;
|
|
149
|
+
dependenciesIndex: Map<string, string[]>;
|
|
150
|
+
} {
|
|
151
|
+
const taskMap = new Map<string, Task>();
|
|
152
|
+
const dependentsIndex = new Map<string, string[]>();
|
|
153
|
+
const dependenciesIndex = new Map<string, string[]>();
|
|
154
|
+
|
|
155
|
+
// Initialize maps - O(n)
|
|
156
|
+
for (const task of tasks) {
|
|
157
|
+
taskMap.set(task.id, task);
|
|
158
|
+
dependentsIndex.set(task.id, []);
|
|
159
|
+
dependenciesIndex.set(task.id, []);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build indices - O(e)
|
|
163
|
+
for (const task of tasks) {
|
|
164
|
+
const deps = (task.dependencies ?? [])
|
|
165
|
+
.filter((d) => d.type === "blocked_by")
|
|
166
|
+
.map((d) => d.taskId);
|
|
167
|
+
|
|
168
|
+
// Store direct dependencies for this task
|
|
169
|
+
dependenciesIndex.set(
|
|
170
|
+
task.id,
|
|
171
|
+
deps.filter((depId) => taskMap.has(depId))
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Update dependents index (reverse lookup)
|
|
175
|
+
for (const depId of deps) {
|
|
176
|
+
if (taskMap.has(depId)) {
|
|
177
|
+
dependentsIndex.get(depId)!.push(task.id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { taskMap, dependentsIndex, dependenciesIndex };
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
/**
|
|
146
186
|
* Find all tasks that depend on a given task (directly or transitively)
|
|
187
|
+
*
|
|
188
|
+
* Time complexity: O(n + e) with inverted index
|
|
147
189
|
*/
|
|
148
190
|
export function findDependents(tasks: Task[], taskId: string): Task[] {
|
|
191
|
+
const { taskMap, dependentsIndex } = buildDependencyIndices(tasks);
|
|
192
|
+
|
|
149
193
|
const visited = new Set<string>();
|
|
150
194
|
const result: Task[] = [];
|
|
151
195
|
|
|
152
196
|
function dfs(id: string) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
const dependentIds = dependentsIndex.get(id);
|
|
198
|
+
if (!dependentIds) return;
|
|
199
|
+
|
|
200
|
+
for (const depId of dependentIds) {
|
|
201
|
+
if (!visited.has(depId)) {
|
|
202
|
+
visited.add(depId);
|
|
203
|
+
const task = taskMap.get(depId);
|
|
204
|
+
if (task) {
|
|
205
|
+
result.push(task);
|
|
206
|
+
dfs(depId);
|
|
207
|
+
}
|
|
162
208
|
}
|
|
163
209
|
}
|
|
164
210
|
}
|
|
@@ -169,21 +215,20 @@ export function findDependents(tasks: Task[], taskId: string): Task[] {
|
|
|
169
215
|
|
|
170
216
|
/**
|
|
171
217
|
* Find all tasks that a given task depends on (directly or transitively)
|
|
218
|
+
*
|
|
219
|
+
* Time complexity: O(n + e) with pre-built index
|
|
172
220
|
*/
|
|
173
221
|
export function findDependencies(tasks: Task[], taskId: string): Task[] {
|
|
174
|
-
const taskMap =
|
|
222
|
+
const { taskMap, dependenciesIndex } = buildDependencyIndices(tasks);
|
|
223
|
+
|
|
175
224
|
const visited = new Set<string>();
|
|
176
225
|
const result: Task[] = [];
|
|
177
226
|
|
|
178
227
|
function dfs(id: string) {
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
181
|
-
|
|
182
|
-
const deps = (task.dependencies ?? [])
|
|
183
|
-
.filter((d) => d.type === "blocked_by")
|
|
184
|
-
.map((d) => d.taskId);
|
|
228
|
+
const depIds = dependenciesIndex.get(id);
|
|
229
|
+
if (!depIds) return;
|
|
185
230
|
|
|
186
|
-
for (const depId of
|
|
231
|
+
for (const depId of depIds) {
|
|
187
232
|
if (!visited.has(depId)) {
|
|
188
233
|
visited.add(depId);
|
|
189
234
|
const depTask = taskMap.get(depId);
|
package/src/schemas/index.ts
CHANGED
|
@@ -19,21 +19,10 @@ export {
|
|
|
19
19
|
} from "./task.js";
|
|
20
20
|
|
|
21
21
|
// View schemas
|
|
22
|
-
export {
|
|
23
|
-
SmartViewFilter,
|
|
24
|
-
SortField,
|
|
25
|
-
SortOrder,
|
|
26
|
-
SmartView,
|
|
27
|
-
BuiltInView,
|
|
28
|
-
} from "./view.js";
|
|
22
|
+
export { SmartViewFilter, SortField, SortOrder, SmartView, BuiltInView } from "./view.js";
|
|
29
23
|
|
|
30
24
|
// Inbox schemas
|
|
31
|
-
export {
|
|
32
|
-
InboxStatus,
|
|
33
|
-
InboxItem,
|
|
34
|
-
InboxCreateInput,
|
|
35
|
-
InboxUpdateInput,
|
|
36
|
-
} from "./inbox.js";
|
|
25
|
+
export { InboxStatus, InboxItem, InboxCreateInput, InboxUpdateInput } from "./inbox.js";
|
|
37
26
|
|
|
38
27
|
// Response format schemas (token optimization)
|
|
39
28
|
export {
|
|
@@ -129,9 +129,9 @@ export interface DashboardPriorityBreakdown {
|
|
|
129
129
|
|
|
130
130
|
// Dependency metrics
|
|
131
131
|
export interface DashboardDependencyMetrics {
|
|
132
|
-
ready: number;
|
|
133
|
-
blocked: number;
|
|
134
|
-
noDeps: number;
|
|
132
|
+
ready: number; // No dependencies or all satisfied
|
|
133
|
+
blocked: number; // Has unsatisfied dependencies
|
|
134
|
+
noDeps: number; // Tasks with no dependencies
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// Next task recommendation
|
|
@@ -139,12 +139,12 @@ export interface DashboardNextTask {
|
|
|
139
139
|
id: string;
|
|
140
140
|
title: string;
|
|
141
141
|
priority: string;
|
|
142
|
-
reason: string;
|
|
142
|
+
reason: string; // Why this task is recommended
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Critical path info
|
|
146
146
|
export interface DashboardCriticalPath {
|
|
147
|
-
length: number;
|
|
147
|
+
length: number; // Total duration in minutes
|
|
148
148
|
taskCount: number; // Number of tasks on critical path
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -162,7 +162,7 @@ export interface DashboardStatusBreakdown {
|
|
|
162
162
|
export interface DashboardSubtaskProgress {
|
|
163
163
|
completed: number;
|
|
164
164
|
total: number;
|
|
165
|
-
pct: number;
|
|
165
|
+
pct: number; // 0-100
|
|
166
166
|
inProgress: number;
|
|
167
167
|
pending: number;
|
|
168
168
|
blocked: number;
|