@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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@task-mcp/shared",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Shared utilities for task-mcp: types, algorithms, and natural language parsing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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(sortedTasks: Task[], taskMap: Map<string, CPMTask>): Map<string, CPMTask[]> {
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
- // Group tasks that don't depend on each other
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
- // Check if these tasks are independent
352
- const taskDeps = (task.dependencies ?? []).map((d) => d.taskId);
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 = tasks.find((t) => t.id === taskId);
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 = tasks.find((t) => t.id === blockedBy);
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, // 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
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 (PRIORITY_ORDER[a.priority] ?? DEFAULT_PRIORITY_ORDER) - (PRIORITY_ORDER[b.priority] ?? DEFAULT_PRIORITY_ORDER);
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", "infra", "devops", "backend", "frontend", "test", "docs", "refactor"
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; // 1 to LOW_MAX
321
+ low: number; // 1 to LOW_MAX
318
322
  medium: number; // LOW_MAX+1 to MEDIUM_MAX
319
- high: number; // MEDIUM_MAX+1 to 10
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
- for (const task of tasks) {
154
- const deps = (task.dependencies ?? [])
155
- .filter((d) => d.type === "blocked_by")
156
- .map((d) => d.taskId);
157
-
158
- if (deps.includes(id) && !visited.has(task.id)) {
159
- visited.add(task.id);
160
- result.push(task);
161
- dfs(task.id);
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 = new Map(tasks.map((t) => [t.id, t]));
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 task = taskMap.get(id);
180
- if (!task) return;
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 deps) {
231
+ for (const depId of depIds) {
187
232
  if (!visited.has(depId)) {
188
233
  visited.add(depId);
189
234
  const depTask = taskMap.get(depId);
@@ -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; // No dependencies or all satisfied
133
- blocked: number; // Has unsatisfied dependencies
134
- noDeps: number; // Tasks with no dependencies
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; // Why this task is recommended
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; // Total duration in minutes
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; // 0-100
165
+ pct: number; // 0-100
166
166
  inProgress: number;
167
167
  pending: number;
168
168
  blocked: number;