@task-mcp/shared 1.0.3 → 1.0.6
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/dist/algorithms/critical-path.d.ts.map +1 -1
- package/dist/algorithms/critical-path.js +50 -26
- package/dist/algorithms/critical-path.js.map +1 -1
- package/dist/algorithms/dependency-integrity.d.ts +73 -0
- package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.js +189 -0
- package/dist/algorithms/dependency-integrity.js.map +1 -0
- package/dist/algorithms/index.d.ts +2 -0
- package/dist/algorithms/index.d.ts.map +1 -1
- package/dist/algorithms/index.js +2 -0
- package/dist/algorithms/index.js.map +1 -1
- package/dist/algorithms/tech-analysis.d.ts +106 -0
- package/dist/algorithms/tech-analysis.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.js +296 -0
- package/dist/algorithms/tech-analysis.js.map +1 -0
- package/dist/algorithms/tech-analysis.test.d.ts +2 -0
- package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.test.js +338 -0
- package/dist/algorithms/tech-analysis.test.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -1
- package/dist/algorithms/topological-sort.js +60 -8
- package/dist/algorithms/topological-sort.js.map +1 -1
- package/dist/schemas/inbox.d.ts +24 -0
- package/dist/schemas/inbox.d.ts.map +1 -0
- package/dist/schemas/inbox.js +25 -0
- package/dist/schemas/inbox.js.map +1 -0
- package/dist/schemas/index.d.ts +3 -1
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +9 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/response-format.d.ts +79 -0
- package/dist/schemas/response-format.d.ts.map +1 -0
- package/dist/schemas/response-format.js +17 -0
- package/dist/schemas/response-format.js.map +1 -0
- package/dist/schemas/task.d.ts +57 -0
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +34 -0
- package/dist/schemas/task.js.map +1 -1
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +17 -2
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/hierarchy.d.ts +75 -0
- package/dist/utils/hierarchy.d.ts.map +1 -0
- package/dist/utils/hierarchy.js +179 -0
- package/dist/utils/hierarchy.js.map +1 -0
- package/dist/utils/id.d.ts +51 -1
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +124 -4
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/id.test.d.ts +2 -0
- package/dist/utils/id.test.d.ts.map +1 -0
- package/dist/utils/id.test.js +228 -0
- package/dist/utils/id.test.js.map +1 -0
- package/dist/utils/index.d.ts +4 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/natural-language.d.ts +45 -0
- package/dist/utils/natural-language.d.ts.map +1 -1
- package/dist/utils/natural-language.js +86 -0
- package/dist/utils/natural-language.js.map +1 -1
- package/dist/utils/projection.d.ts +65 -0
- package/dist/utils/projection.d.ts.map +1 -0
- package/dist/utils/projection.js +181 -0
- package/dist/utils/projection.js.map +1 -0
- package/dist/utils/projection.test.d.ts +2 -0
- package/dist/utils/projection.test.d.ts.map +1 -0
- package/dist/utils/projection.test.js +400 -0
- package/dist/utils/projection.test.js.map +1 -0
- package/package.json +1 -1
- package/src/algorithms/critical-path.ts +56 -24
- package/src/algorithms/dependency-integrity.ts +270 -0
- package/src/algorithms/index.ts +28 -0
- package/src/algorithms/tech-analysis.test.ts +413 -0
- package/src/algorithms/tech-analysis.ts +412 -0
- package/src/algorithms/topological-sort.ts +66 -9
- package/src/schemas/inbox.ts +32 -0
- package/src/schemas/index.ts +31 -0
- package/src/schemas/response-format.ts +108 -0
- package/src/schemas/task.ts +50 -0
- package/src/utils/date.ts +18 -2
- package/src/utils/hierarchy.ts +224 -0
- package/src/utils/id.test.ts +281 -0
- package/src/utils/id.ts +139 -4
- package/src/utils/index.ts +46 -2
- package/src/utils/natural-language.ts +113 -0
- package/src/utils/projection.test.ts +505 -0
- package/src/utils/projection.ts +251 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import type { Task, TechArea, RiskLevel } from "../schemas/task.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tech area ordering rules (lower = execute first)
|
|
5
|
+
* Based on dependency flow: DB changes → Infrastructure → Backend → Frontend → Tests
|
|
6
|
+
*/
|
|
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
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Risk level ordering (lower = safer, execute first)
|
|
20
|
+
*/
|
|
21
|
+
const RISK_ORDER: Record<RiskLevel, number> = {
|
|
22
|
+
low: 0,
|
|
23
|
+
medium: 1,
|
|
24
|
+
high: 2,
|
|
25
|
+
critical: 3,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Result of safe order suggestion
|
|
30
|
+
*/
|
|
31
|
+
export interface SafeOrderResult {
|
|
32
|
+
/** Tasks ordered for safe execution */
|
|
33
|
+
orderedTasks: Task[];
|
|
34
|
+
/** Grouping by phase/tech level */
|
|
35
|
+
phases: SafeOrderPhase[];
|
|
36
|
+
/** Summary statistics */
|
|
37
|
+
summary: {
|
|
38
|
+
totalTasks: number;
|
|
39
|
+
breakingChanges: number;
|
|
40
|
+
highRiskCount: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A phase in the safe execution order
|
|
46
|
+
*/
|
|
47
|
+
export interface SafeOrderPhase {
|
|
48
|
+
/** Phase number (1-based) */
|
|
49
|
+
phase: number;
|
|
50
|
+
/** Primary tech area for this phase */
|
|
51
|
+
primaryArea: TechArea | "mixed";
|
|
52
|
+
/** Tasks in this phase */
|
|
53
|
+
tasks: Task[];
|
|
54
|
+
/** Notes about this phase */
|
|
55
|
+
notes: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the minimum tech order value for a task
|
|
60
|
+
* If multiple areas, use the lowest (most foundational)
|
|
61
|
+
*/
|
|
62
|
+
function getMinTechOrder(task: Task): number {
|
|
63
|
+
const areas = task.techStack?.areas ?? [];
|
|
64
|
+
if (areas.length === 0) return TECH_ORDER.backend; // Default to backend
|
|
65
|
+
|
|
66
|
+
return Math.min(...areas.map((area) => TECH_ORDER[area]));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the risk order value for a task
|
|
71
|
+
*/
|
|
72
|
+
function getRiskOrder(task: Task): number {
|
|
73
|
+
const riskLevel = task.techStack?.riskLevel ?? "medium";
|
|
74
|
+
return RISK_ORDER[riskLevel];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a task has breaking changes
|
|
79
|
+
*/
|
|
80
|
+
function hasBreakingChange(task: Task): boolean {
|
|
81
|
+
return task.techStack?.hasBreakingChange === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Suggest safe execution order for tasks
|
|
86
|
+
*
|
|
87
|
+
* Ordering strategy:
|
|
88
|
+
* 1. Tech level (schema → infra → devops → backend → frontend → test → docs → refactor)
|
|
89
|
+
* 2. Risk level within same tech level (low → medium → high → critical)
|
|
90
|
+
* 3. Breaking changes last within same tech/risk level
|
|
91
|
+
* 4. Priority as tiebreaker (critical > high > medium > low)
|
|
92
|
+
*/
|
|
93
|
+
export function suggestSafeOrder(tasks: Task[]): SafeOrderResult {
|
|
94
|
+
// Filter to active tasks only
|
|
95
|
+
const activeTasks = tasks.filter(
|
|
96
|
+
(t) => t.status === "pending" || t.status === "in_progress"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (activeTasks.length === 0) {
|
|
100
|
+
return {
|
|
101
|
+
orderedTasks: [],
|
|
102
|
+
phases: [],
|
|
103
|
+
summary: {
|
|
104
|
+
totalTasks: 0,
|
|
105
|
+
breakingChanges: 0,
|
|
106
|
+
highRiskCount: 0,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Sort tasks
|
|
112
|
+
const sorted = [...activeTasks].sort((a, b) => {
|
|
113
|
+
// 1. Tech level (lower first)
|
|
114
|
+
const techDiff = getMinTechOrder(a) - getMinTechOrder(b);
|
|
115
|
+
if (techDiff !== 0) return techDiff;
|
|
116
|
+
|
|
117
|
+
// 2. Risk level (lower first)
|
|
118
|
+
const riskDiff = getRiskOrder(a) - getRiskOrder(b);
|
|
119
|
+
if (riskDiff !== 0) return riskDiff;
|
|
120
|
+
|
|
121
|
+
// 3. Breaking changes last
|
|
122
|
+
const aBreaking = hasBreakingChange(a) ? 1 : 0;
|
|
123
|
+
const bBreaking = hasBreakingChange(b) ? 1 : 0;
|
|
124
|
+
if (aBreaking !== bBreaking) return aBreaking - bBreaking;
|
|
125
|
+
|
|
126
|
+
// 4. Priority as tiebreaker (higher priority first)
|
|
127
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
128
|
+
return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Group into phases by tech level
|
|
132
|
+
const phases: SafeOrderPhase[] = [];
|
|
133
|
+
let currentTechOrder = -1;
|
|
134
|
+
let currentPhase: SafeOrderPhase | null = null;
|
|
135
|
+
|
|
136
|
+
for (const task of sorted) {
|
|
137
|
+
const techOrder = getMinTechOrder(task);
|
|
138
|
+
|
|
139
|
+
if (techOrder !== currentTechOrder) {
|
|
140
|
+
// Start new phase
|
|
141
|
+
const primaryArea = getPrimaryArea(task);
|
|
142
|
+
currentPhase = {
|
|
143
|
+
phase: phases.length + 1,
|
|
144
|
+
primaryArea,
|
|
145
|
+
tasks: [task],
|
|
146
|
+
notes: [],
|
|
147
|
+
};
|
|
148
|
+
phases.push(currentPhase);
|
|
149
|
+
currentTechOrder = techOrder;
|
|
150
|
+
} else {
|
|
151
|
+
// Add to current phase
|
|
152
|
+
currentPhase!.tasks.push(task);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add notes to phases
|
|
157
|
+
for (const phase of phases) {
|
|
158
|
+
const breakingCount = phase.tasks.filter(hasBreakingChange).length;
|
|
159
|
+
const highRiskCount = phase.tasks.filter(
|
|
160
|
+
(t) => t.techStack?.riskLevel === "high" || t.techStack?.riskLevel === "critical"
|
|
161
|
+
).length;
|
|
162
|
+
|
|
163
|
+
if (breakingCount > 0) {
|
|
164
|
+
phase.notes.push(`${breakingCount} breaking change(s) - test thoroughly`);
|
|
165
|
+
}
|
|
166
|
+
if (highRiskCount > 0) {
|
|
167
|
+
phase.notes.push(`${highRiskCount} high-risk task(s) - review carefully`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Calculate summary
|
|
172
|
+
const breakingChanges = sorted.filter(hasBreakingChange).length;
|
|
173
|
+
const highRiskCount = sorted.filter(
|
|
174
|
+
(t) => t.techStack?.riskLevel === "high" || t.techStack?.riskLevel === "critical"
|
|
175
|
+
).length;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
orderedTasks: sorted,
|
|
179
|
+
phases,
|
|
180
|
+
summary: {
|
|
181
|
+
totalTasks: sorted.length,
|
|
182
|
+
breakingChanges,
|
|
183
|
+
highRiskCount,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the primary tech area for a task
|
|
190
|
+
*/
|
|
191
|
+
function getPrimaryArea(task: Task): TechArea | "mixed" {
|
|
192
|
+
const areas = task.techStack?.areas;
|
|
193
|
+
if (!areas || areas.length === 0) return "mixed";
|
|
194
|
+
|
|
195
|
+
// Return the one with lowest order (most foundational)
|
|
196
|
+
let primary: TechArea = areas[0]!;
|
|
197
|
+
for (let i = 1; i < areas.length; i++) {
|
|
198
|
+
const area = areas[i]!;
|
|
199
|
+
if (TECH_ORDER[area] < TECH_ORDER[primary]) {
|
|
200
|
+
primary = area;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return primary;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Find tasks with breaking changes
|
|
208
|
+
*/
|
|
209
|
+
export function findBreakingChanges(tasks: Task[]): Task[] {
|
|
210
|
+
return tasks.filter(
|
|
211
|
+
(t) =>
|
|
212
|
+
(t.status === "pending" || t.status === "in_progress") &&
|
|
213
|
+
hasBreakingChange(t)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Find high-risk tasks (high or critical risk level)
|
|
219
|
+
*/
|
|
220
|
+
export function findHighRiskTasks(tasks: Task[]): Task[] {
|
|
221
|
+
return tasks.filter(
|
|
222
|
+
(t) =>
|
|
223
|
+
(t.status === "pending" || t.status === "in_progress") &&
|
|
224
|
+
(t.techStack?.riskLevel === "high" || t.techStack?.riskLevel === "critical")
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Group tasks by tech area
|
|
230
|
+
* A task may appear in multiple groups if it spans multiple areas
|
|
231
|
+
*/
|
|
232
|
+
export function groupByTechArea(tasks: Task[]): Map<TechArea, Task[]> {
|
|
233
|
+
const groups = new Map<TechArea, Task[]>();
|
|
234
|
+
|
|
235
|
+
// Initialize all groups
|
|
236
|
+
const allAreas: TechArea[] = [
|
|
237
|
+
"schema", "infra", "devops", "backend", "frontend", "test", "docs", "refactor"
|
|
238
|
+
];
|
|
239
|
+
for (const area of allAreas) {
|
|
240
|
+
groups.set(area, []);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Group active tasks
|
|
244
|
+
const activeTasks = tasks.filter(
|
|
245
|
+
(t) => t.status === "pending" || t.status === "in_progress"
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
for (const task of activeTasks) {
|
|
249
|
+
const areas = task.techStack?.areas ?? [];
|
|
250
|
+
if (areas.length === 0) {
|
|
251
|
+
// Default to backend if no area specified
|
|
252
|
+
groups.get("backend")!.push(task);
|
|
253
|
+
} else {
|
|
254
|
+
for (const area of areas) {
|
|
255
|
+
groups.get(area)!.push(task);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return groups;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Complexity distribution by level
|
|
265
|
+
*/
|
|
266
|
+
export interface ComplexityDistribution {
|
|
267
|
+
low: number; // 1-3
|
|
268
|
+
medium: number; // 4-6
|
|
269
|
+
high: number; // 7-10
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get complexity summary for a project
|
|
274
|
+
*/
|
|
275
|
+
export interface ComplexitySummary {
|
|
276
|
+
/** Distribution of complexity scores */
|
|
277
|
+
distribution: ComplexityDistribution;
|
|
278
|
+
/** Tasks that should be broken down (score >= 7) */
|
|
279
|
+
needsBreakdown: Task[];
|
|
280
|
+
/** Average complexity score */
|
|
281
|
+
averageScore: number;
|
|
282
|
+
/** Tasks without complexity analysis */
|
|
283
|
+
unanalyzed: Task[];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Analyze complexity distribution across tasks
|
|
288
|
+
*/
|
|
289
|
+
export function getComplexitySummary(tasks: Task[]): ComplexitySummary {
|
|
290
|
+
const activeTasks = tasks.filter(
|
|
291
|
+
(t) => t.status === "pending" || t.status === "in_progress"
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const analyzed = activeTasks.filter((t) => t.complexity?.score !== undefined);
|
|
295
|
+
const unanalyzed = activeTasks.filter((t) => t.complexity?.score === undefined);
|
|
296
|
+
|
|
297
|
+
// Calculate distribution
|
|
298
|
+
const distribution: ComplexityDistribution = {
|
|
299
|
+
low: 0,
|
|
300
|
+
medium: 0,
|
|
301
|
+
high: 0,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let totalScore = 0;
|
|
305
|
+
const needsBreakdown: Task[] = [];
|
|
306
|
+
|
|
307
|
+
for (const task of analyzed) {
|
|
308
|
+
const score = task.complexity!.score!;
|
|
309
|
+
totalScore += score;
|
|
310
|
+
|
|
311
|
+
if (score <= 3) {
|
|
312
|
+
distribution.low++;
|
|
313
|
+
} else if (score <= 6) {
|
|
314
|
+
distribution.medium++;
|
|
315
|
+
} else {
|
|
316
|
+
distribution.high++;
|
|
317
|
+
needsBreakdown.push(task);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
distribution,
|
|
323
|
+
needsBreakdown,
|
|
324
|
+
averageScore: analyzed.length > 0 ? totalScore / analyzed.length : 0,
|
|
325
|
+
unanalyzed,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get tech stack summary for a project
|
|
331
|
+
*/
|
|
332
|
+
export interface TechStackSummary {
|
|
333
|
+
/** Count of tasks per tech area */
|
|
334
|
+
areaCounts: Record<TechArea, number>;
|
|
335
|
+
/** Tasks with breaking changes */
|
|
336
|
+
breakingChanges: Task[];
|
|
337
|
+
/** Risk distribution */
|
|
338
|
+
riskDistribution: Record<RiskLevel, number>;
|
|
339
|
+
/** Tasks without tech stack analysis */
|
|
340
|
+
unanalyzed: Task[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Analyze tech stack distribution across tasks
|
|
345
|
+
*/
|
|
346
|
+
export function getTechStackSummary(tasks: Task[]): TechStackSummary {
|
|
347
|
+
const activeTasks = tasks.filter(
|
|
348
|
+
(t) => t.status === "pending" || t.status === "in_progress"
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const analyzed = activeTasks.filter((t) => t.techStack?.areas !== undefined);
|
|
352
|
+
const unanalyzed = activeTasks.filter((t) => t.techStack?.areas === undefined);
|
|
353
|
+
|
|
354
|
+
// Count by area
|
|
355
|
+
const areaCounts: Record<TechArea, number> = {
|
|
356
|
+
schema: 0,
|
|
357
|
+
infra: 0,
|
|
358
|
+
devops: 0,
|
|
359
|
+
backend: 0,
|
|
360
|
+
frontend: 0,
|
|
361
|
+
test: 0,
|
|
362
|
+
docs: 0,
|
|
363
|
+
refactor: 0,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
for (const task of analyzed) {
|
|
367
|
+
for (const area of task.techStack!.areas!) {
|
|
368
|
+
areaCounts[area]++;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Risk distribution
|
|
373
|
+
const riskDistribution: Record<RiskLevel, number> = {
|
|
374
|
+
low: 0,
|
|
375
|
+
medium: 0,
|
|
376
|
+
high: 0,
|
|
377
|
+
critical: 0,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (const task of analyzed) {
|
|
381
|
+
const risk = task.techStack?.riskLevel ?? "medium";
|
|
382
|
+
riskDistribution[risk]++;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Breaking changes
|
|
386
|
+
const breakingChanges = findBreakingChanges(activeTasks);
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
areaCounts,
|
|
390
|
+
breakingChanges,
|
|
391
|
+
riskDistribution,
|
|
392
|
+
unanalyzed,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Suggest number of subtasks based on complexity score
|
|
398
|
+
*
|
|
399
|
+
* Mapping:
|
|
400
|
+
* 1-2: 0 (no breakdown needed)
|
|
401
|
+
* 3-4: 2
|
|
402
|
+
* 5-6: 3-4
|
|
403
|
+
* 7-8: 5-6
|
|
404
|
+
* 9-10: 7-10
|
|
405
|
+
*/
|
|
406
|
+
export function suggestSubtaskCount(score: number): number {
|
|
407
|
+
if (score <= 2) return 0;
|
|
408
|
+
if (score <= 4) return 2;
|
|
409
|
+
if (score <= 6) return Math.ceil((score - 4) * 0.5 + 3); // 3-4
|
|
410
|
+
if (score <= 8) return Math.ceil((score - 6) * 0.5 + 5); // 5-6
|
|
411
|
+
return Math.ceil((score - 8) * 1.5 + 7); // 7-10
|
|
412
|
+
}
|
|
@@ -10,6 +10,67 @@ export interface TaskNode {
|
|
|
10
10
|
estimate: number; // Duration in minutes
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Max-heap implementation for priority queue (higher priority = higher value comes first)
|
|
15
|
+
* O(log n) insert and extract operations vs O(n log n) for sort-based approach
|
|
16
|
+
*/
|
|
17
|
+
class PriorityQueue<T> {
|
|
18
|
+
private heap: T[] = [];
|
|
19
|
+
private compare: (a: T, b: T) => number;
|
|
20
|
+
|
|
21
|
+
constructor(compare: (a: T, b: T) => number) {
|
|
22
|
+
this.compare = compare;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get length(): number {
|
|
26
|
+
return this.heap.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
push(item: T): void {
|
|
30
|
+
this.heap.push(item);
|
|
31
|
+
this.bubbleUp(this.heap.length - 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pop(): T | undefined {
|
|
35
|
+
if (this.heap.length === 0) return undefined;
|
|
36
|
+
if (this.heap.length === 1) return this.heap.pop();
|
|
37
|
+
|
|
38
|
+
const result = this.heap[0];
|
|
39
|
+
this.heap[0] = this.heap.pop()!;
|
|
40
|
+
this.bubbleDown(0);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private bubbleUp(index: number): void {
|
|
45
|
+
while (index > 0) {
|
|
46
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
47
|
+
if (this.compare(this.heap[index]!, this.heap[parentIndex]!) <= 0) break;
|
|
48
|
+
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex]!, this.heap[index]!];
|
|
49
|
+
index = parentIndex;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private bubbleDown(index: number): void {
|
|
54
|
+
const length = this.heap.length;
|
|
55
|
+
while (true) {
|
|
56
|
+
const leftChild = 2 * index + 1;
|
|
57
|
+
const rightChild = 2 * index + 2;
|
|
58
|
+
let largest = index;
|
|
59
|
+
|
|
60
|
+
if (leftChild < length && this.compare(this.heap[leftChild]!, this.heap[largest]!) > 0) {
|
|
61
|
+
largest = leftChild;
|
|
62
|
+
}
|
|
63
|
+
if (rightChild < length && this.compare(this.heap[rightChild]!, this.heap[largest]!) > 0) {
|
|
64
|
+
largest = rightChild;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (largest === index) break;
|
|
68
|
+
[this.heap[index], this.heap[largest]] = [this.heap[largest]!, this.heap[index]!];
|
|
69
|
+
index = largest;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
13
74
|
/**
|
|
14
75
|
* Convert priority string to numeric value
|
|
15
76
|
*/
|
|
@@ -71,21 +132,19 @@ export function topologicalSort(tasks: Task[]): Task[] {
|
|
|
71
132
|
}
|
|
72
133
|
}
|
|
73
134
|
|
|
74
|
-
// Initialize queue with nodes that have no dependencies
|
|
75
|
-
|
|
135
|
+
// Initialize priority queue with nodes that have no dependencies
|
|
136
|
+
// Using max-heap: higher priority comes out first
|
|
137
|
+
const queue = new PriorityQueue<TaskNode>((a, b) => a.priority - b.priority);
|
|
76
138
|
for (const node of nodes) {
|
|
77
139
|
if (inDegree.get(node.id) === 0) {
|
|
78
140
|
queue.push(node);
|
|
79
141
|
}
|
|
80
142
|
}
|
|
81
143
|
|
|
82
|
-
// Sort by priority (higher first)
|
|
83
|
-
queue.sort((a, b) => b.priority - a.priority);
|
|
84
|
-
|
|
85
144
|
const result: Task[] = [];
|
|
86
145
|
|
|
87
146
|
while (queue.length > 0) {
|
|
88
|
-
const current = queue.
|
|
147
|
+
const current = queue.pop()!;
|
|
89
148
|
const task = taskMap.get(current.id);
|
|
90
149
|
if (task) {
|
|
91
150
|
result.push(task);
|
|
@@ -98,9 +157,7 @@ export function topologicalSort(tasks: Task[]): Task[] {
|
|
|
98
157
|
|
|
99
158
|
if (newDegree === 0) {
|
|
100
159
|
const neighborNode = nodeMap.get(neighborId)!;
|
|
101
|
-
queue.push(neighborNode);
|
|
102
|
-
// Re-sort to maintain priority order
|
|
103
|
-
queue.sort((a, b) => b.priority - a.priority);
|
|
160
|
+
queue.push(neighborNode); // O(log n) insertion maintains heap property
|
|
104
161
|
}
|
|
105
162
|
}
|
|
106
163
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
// Inbox item status
|
|
4
|
+
export const InboxStatus = type("'pending' | 'promoted' | 'discarded'");
|
|
5
|
+
export type InboxStatus = typeof InboxStatus.infer;
|
|
6
|
+
|
|
7
|
+
// Inbox item schema - lightweight idea/memo capture
|
|
8
|
+
export const InboxItem = type({
|
|
9
|
+
id: "string",
|
|
10
|
+
content: "string", // The memo/idea content
|
|
11
|
+
capturedAt: "string", // ISO timestamp
|
|
12
|
+
"source?": "string", // Origin: 'cli', 'mcp', 'api', etc.
|
|
13
|
+
"tags?": "string[]", // Simple tags for organization
|
|
14
|
+
"promotedToTaskId?": "string", // Task ID if promoted
|
|
15
|
+
status: InboxStatus,
|
|
16
|
+
});
|
|
17
|
+
export type InboxItem = typeof InboxItem.infer;
|
|
18
|
+
|
|
19
|
+
// Inbox item creation input (minimal)
|
|
20
|
+
export const InboxCreateInput = type({
|
|
21
|
+
content: "string",
|
|
22
|
+
"source?": "string",
|
|
23
|
+
"tags?": "string[]",
|
|
24
|
+
});
|
|
25
|
+
export type InboxCreateInput = typeof InboxCreateInput.infer;
|
|
26
|
+
|
|
27
|
+
// Inbox item update input
|
|
28
|
+
export const InboxUpdateInput = type({
|
|
29
|
+
"content?": "string",
|
|
30
|
+
"tags?": "string[]",
|
|
31
|
+
});
|
|
32
|
+
export type InboxUpdateInput = typeof InboxUpdateInput.infer;
|
package/src/schemas/index.ts
CHANGED
|
@@ -6,6 +6,13 @@ export {
|
|
|
6
6
|
Dependency,
|
|
7
7
|
TimeEstimate,
|
|
8
8
|
Recurrence,
|
|
9
|
+
// Analysis schemas
|
|
10
|
+
ComplexityFactor,
|
|
11
|
+
ComplexityAnalysis,
|
|
12
|
+
TechArea,
|
|
13
|
+
RiskLevel,
|
|
14
|
+
TechStackAnalysis,
|
|
15
|
+
// Core schemas
|
|
9
16
|
Task,
|
|
10
17
|
TaskCreateInput,
|
|
11
18
|
TaskUpdateInput,
|
|
@@ -28,3 +35,27 @@ export {
|
|
|
28
35
|
SmartView,
|
|
29
36
|
BuiltInView,
|
|
30
37
|
} from "./view.js";
|
|
38
|
+
|
|
39
|
+
// Inbox schemas
|
|
40
|
+
export {
|
|
41
|
+
InboxStatus,
|
|
42
|
+
InboxItem,
|
|
43
|
+
InboxCreateInput,
|
|
44
|
+
InboxUpdateInput,
|
|
45
|
+
} from "./inbox.js";
|
|
46
|
+
|
|
47
|
+
// Response format schemas (token optimization)
|
|
48
|
+
export {
|
|
49
|
+
ResponseFormat,
|
|
50
|
+
DEFAULT_LIMIT,
|
|
51
|
+
MAX_LIMIT,
|
|
52
|
+
type PaginatedResponse,
|
|
53
|
+
type TaskSummary,
|
|
54
|
+
type TaskPreview,
|
|
55
|
+
type ProjectSummary,
|
|
56
|
+
type ProjectPreview,
|
|
57
|
+
type InboxSummary,
|
|
58
|
+
type InboxPreview,
|
|
59
|
+
type CriticalPathSummary,
|
|
60
|
+
type BottleneckSummary,
|
|
61
|
+
} from "./response-format.js";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Response Format Schema
|
|
5
|
+
*
|
|
6
|
+
* Token-efficient response formats for MCP tools.
|
|
7
|
+
* Based on Anthropic's recommended patterns for reducing token usage.
|
|
8
|
+
*
|
|
9
|
+
* - concise: Minimal fields (4-6), JSON format for machine processing
|
|
10
|
+
* - standard: Common fields (7-10), balanced for most use cases
|
|
11
|
+
* - detailed: Full object, human-readable format
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Response format options
|
|
15
|
+
export const ResponseFormat = type("'concise' | 'standard' | 'detailed'");
|
|
16
|
+
export type ResponseFormat = typeof ResponseFormat.infer;
|
|
17
|
+
|
|
18
|
+
// Default limits for pagination
|
|
19
|
+
export const DEFAULT_LIMIT = 20;
|
|
20
|
+
export const MAX_LIMIT = 100;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Paginated response wrapper
|
|
24
|
+
*/
|
|
25
|
+
export interface PaginatedResponse<T> {
|
|
26
|
+
items: T[];
|
|
27
|
+
total: number;
|
|
28
|
+
limit: number;
|
|
29
|
+
offset: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Task projection types - progressively more detailed
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// Concise: 4 essential fields (~30 tokens per task)
|
|
38
|
+
export interface TaskSummary {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
status: string;
|
|
42
|
+
priority: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Standard: 8 common fields (~60 tokens per task)
|
|
46
|
+
export interface TaskPreview extends TaskSummary {
|
|
47
|
+
dueDate?: string;
|
|
48
|
+
tags?: string[];
|
|
49
|
+
contexts?: string[];
|
|
50
|
+
parentId?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detailed: Full Task object (~200+ tokens per task)
|
|
54
|
+
// Use the full Task type from task.ts
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Project projection types
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// Concise: 4 essential fields
|
|
61
|
+
export interface ProjectSummary {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
status: string;
|
|
65
|
+
completionPercentage?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Standard: 7 common fields
|
|
69
|
+
export interface ProjectPreview extends ProjectSummary {
|
|
70
|
+
description?: string;
|
|
71
|
+
totalTasks?: number;
|
|
72
|
+
completedTasks?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Inbox projection types
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
// Concise: 3 essential fields
|
|
80
|
+
export interface InboxSummary {
|
|
81
|
+
id: string;
|
|
82
|
+
content: string;
|
|
83
|
+
status: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Standard: 5 common fields
|
|
87
|
+
export interface InboxPreview extends InboxSummary {
|
|
88
|
+
capturedAt: string;
|
|
89
|
+
tags?: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Analysis result types - optimized for token efficiency
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
// Critical path summary (concise format)
|
|
97
|
+
export interface CriticalPathSummary {
|
|
98
|
+
totalDuration: number;
|
|
99
|
+
taskCount: number;
|
|
100
|
+
taskIds: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Bottleneck summary (concise format)
|
|
104
|
+
export interface BottleneckSummary {
|
|
105
|
+
taskId: string;
|
|
106
|
+
title: string;
|
|
107
|
+
blockedCount: number;
|
|
108
|
+
}
|